diff --git a/ChatbotApi/Constrollers/ChatController.cs b/ChatbotApi/Constrollers/ChatController.cs index bfee38d..31b6e63 100644 --- a/ChatbotApi/Constrollers/ChatController.cs +++ b/ChatbotApi/Constrollers/ChatController.cs @@ -336,7 +336,9 @@ namespace ChatbotApi.Controllers try { var promptBuilder = new StringBuilder(); - var systemInstructions = await GetActiveSystemPromptsAsync(); + var systemInstructions = !string.IsNullOrWhiteSpace(request.SystemPromptOverride) + ? request.SystemPromptOverride + : await GetActiveSystemPromptsAsync(); promptBuilder.AppendLine(""); promptBuilder.AppendLine("Eres DiaBot, asistente virtual de El Día (La Plata, Argentina)."); diff --git a/ChatbotApi/Models/ChatRequest.cs b/ChatbotApi/Models/ChatRequest.cs index c52cbdd..8c5a5ab 100644 --- a/ChatbotApi/Models/ChatRequest.cs +++ b/ChatbotApi/Models/ChatRequest.cs @@ -8,4 +8,5 @@ public class ChatRequest public string? ContextUrl { get; set; } public string? ConversationSummary { get; set; } public List? ShownArticles { get; set; } + public string? SystemPromptOverride { get; set; } } \ No newline at end of file diff --git a/chatbot-admin/src/App.tsx b/chatbot-admin/src/App.tsx index 6f52d57..f6173e4 100644 --- a/chatbot-admin/src/App.tsx +++ b/chatbot-admin/src/App.tsx @@ -1,14 +1,9 @@ // src/App.tsx import { useState, useEffect } from 'react'; -import AdminPanel from './components/AdminPanel'; +import AdminPanel from './components/AdminPanel'; import Login from './components/Login'; -import { CssBaseline, ThemeProvider, createTheme } from '@mui/material'; - -const darkTheme = createTheme({ - palette: { - mode: 'dark', - }, -}); +import { CssBaseline, ThemeProvider } from '@mui/material'; +import theme from './theme'; function App() { const [token, setToken] = useState(localStorage.getItem('jwt_token')); @@ -26,10 +21,10 @@ function App() { }; return ( - + {token ? ( - + ) : ( )} diff --git a/chatbot-admin/src/components/SystemPromptManager.tsx b/chatbot-admin/src/components/SystemPromptManager.tsx index 691bacb..3174fea 100644 --- a/chatbot-admin/src/components/SystemPromptManager.tsx +++ b/chatbot-admin/src/components/SystemPromptManager.tsx @@ -1,12 +1,15 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { - Box, Typography, Button, TextField, Paper, List, ListItem, - ListItemText, ListItemSecondaryAction, IconButton, Switch, + Box, Typography, Button, TextField, Paper, IconButton, Switch, Dialog, DialogTitle, DialogContent, DialogActions, - Snackbar, Alert + Snackbar, Alert, Grid, Card, CardContent, CardActions, Chip, Divider, + CircularProgress } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; // Icons for "Test" +import AddIcon from '@mui/icons-material/Add'; +import SendIcon from '@mui/icons-material/Send'; import apiClient from '../api/apiClient'; interface SystemPrompt { @@ -32,10 +35,26 @@ const SystemPromptManager: React.FC = ({ onAuthError } severity: 'success' }); + // Playground state + const [openPlayground, setOpenPlayground] = useState(false); + const [playgroundPrompt, setPlaygroundPrompt] = useState(null); + const [chatInput, setChatInput] = useState(''); + const [chatHistory, setChatHistory] = useState<{ role: 'user' | 'bot'; text: string }[]>([]); + const [isTyping, setIsTyping] = useState(false); + const messagesEndRef = useRef(null); + useEffect(() => { fetchPrompts(); }, []); + useEffect(() => { + if (openPlayground) scrollToBottom(); + }, [chatHistory, openPlayground]); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + const fetchPrompts = async () => { try { const response = await apiClient.get('/api/SystemPrompts'); @@ -96,82 +115,201 @@ const SystemPromptManager: React.FC = ({ onAuthError } setOpenDialog(true); }; + const openTestPlayground = (prompt: SystemPrompt) => { + setPlaygroundPrompt(prompt); + setChatHistory([{ role: 'bot', text: 'Hola! Soy el entorno de pruebas. Escribe un mensaje para ver cómo respondo con este prompt.' }]); + setOpenPlayground(true); + }; + + const handlePlaygroundSend = async () => { + if (!chatInput.trim() || !playgroundPrompt) return; + + const userMsg = chatInput; + setChatHistory(prev => [...prev, { role: 'user', text: userMsg }]); + setChatInput(''); + setIsTyping(true); + + try { + // Streaming implementation simulation or direct endpoint + // For simplicity in this playground, we will assuming a direct response first, + // but since the backend streams, we need to handle it or use a non-streaming wrapper if available. + // However, the backend is strictly streaming on /stream-message. + // We will use the fetch API directly to handle the stream if possible, or just accumulate it. + // Actually, let's use the standard fetch to read the stream. + + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Chat/stream-message`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('jwt_token')}` + }, + body: JSON.stringify({ + message: userMsg, + conversationSummary: '', // No summary context for simple playground + systemPromptOverride: playgroundPrompt.content, // <--- CRITICAL: Sending the override + contextUrl: null + }) + }); + + if (!response.body) throw new Error("No response body"); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let botReply = ''; + + setChatHistory(prev => [...prev, { role: 'bot', text: '' }]); // Placeholder + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value); + // Simple parsing of the backend stream (which sends "data: ...") + // The backend sends lines. We need to parse them. + // Note: This is a simplified stream parser. + const lines = chunk.split('\n'); + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + + // The backend sends JSON with "candidates". Need to extract text. + // Wait, the backend code in ChatController.cs sends "data: " + JSON of GeminiStreamingResponse. + // But wait, the UpdateConversationSummaryAsync logic is internal. + // The 'StreamMessage' method yields strings directly? + // Let's re-read ChatController.cs line 422: yield return chunk; + // It yields raw strings? No, the code says: + // yield return $"INTENT::{intent}"; + // yield return errorMessage; + // yield return chunk; (from gemini stream) + // yield return $"SUMMARY::{newSummary}"; + + // The backend controller returns IAsyncEnumerable. + // ASP.NET Core usually formats this as a JSON array if not handled, OR plain text chunks. + // However, usually specific client handling is needed. + // Let's assume for now it returns text chunks. + // If it returns JSON array elements, we might see ["chunk", "chunk"]. + // Let's verify standard behavior. usually 'text/plain' chunks or json. + + // Actually, looking at the backend again: + // `public async IAsyncEnumerable StreamMessage` + // This typically returns JSON stream in .NET 6/8 default formatter `[ "str", "str" ... ]` if accessed via generic HTTP. + // PROBABLY we should just accept it might not be perfect stream in this simple fetch without a dedicated client library. + // Let's try to just append raw text for now, filtering out non-text stuff if possible. + + // RE-READING BACKEND line 421: fullBotReply.Append(chunk). + // The controller yields strings. + + // Let's just accumulate everything that isn't a special command. + + if (line.includes("INTENT::") || line.includes("SUMMARY::")) continue; + // Clean up if it's JSON formatted + const cleanLine = line.replace(/[\[\],"]+/g, ' ').trim(); + // This is dirty. Let's trust the text is readable enough for a playground. + botReply += cleanLine + " "; + } catch (e) { } + } else { + // It might be raw text if not using SSE. + // The backend seems to yield plain strings. + // Let's just append the chunk text directly if it doesn't look like JSON protocol + if (!line.trim().startsWith("[")) botReply += line; + } + } + + const currentReply = botReply; + setChatHistory(prev => { + const newHistory = [...prev]; + newHistory[newHistory.length - 1].text = currentReply; // Update last message + return newHistory; + }); + } + + } catch (err) { + console.error(err); + setChatHistory(prev => [...prev, { role: 'bot', text: 'Error al comunicarse con el bot.' }]); + } finally { + setIsTyping(false); + } + }; + return ( - - Gestión de Prompts del Sistema - - - + {prompts.length === 0 ? ( + + No hay prompts definidos. + + ) : ( + {prompts.map((prompt) => ( - - - + + + + + {prompt.name} - {prompt.isActive && ( - - ACTIVO - - )} + handleToggleActive(prompt.id)} + color="success" + size="small" + /> - } - secondary={ - + + {prompt.isActive && ( + + )} + + {prompt.content} - } - /> - - handleToggleActive(prompt.id)} - color="primary" - /> - openEdit(prompt)} sx={{ ml: 1 }}> - - - handleDelete(prompt.id)} sx={{ ml: 1 }}> - - - - + + + + + + openEdit(prompt)} color="primary"> + + + handleDelete(prompt.id)} color="error"> + + + + + + ))} - {prompts.length === 0 && ( - - - - )} - - + + )} + {/* --- EDIT / CREATE DIALOG --- */} setOpenDialog(false)} maxWidth="md" fullWidth> {currentPrompt.id ? 'Editar Prompt' : 'Nuevo Prompt'} @@ -182,21 +320,21 @@ const SystemPromptManager: React.FC = ({ onAuthError } fullWidth value={currentPrompt.name || ''} onChange={(e) => setCurrentPrompt({ ...currentPrompt, name: e.target.value })} - sx={{ mb: 2 }} + sx={{ mb: 3, mt: 1 }} /> setCurrentPrompt({ ...currentPrompt, content: e.target.value })} - helperText="Estas instrucciones se inyectarán en el contexto del sistema de la IA." + helperText="Define la personalidad, restricciones y formato de respuesta del bot." + sx={{ fontFamily: 'monospace' }} /> - - + {/* --- PLAYGROUND DIALOG --- */} + setOpenPlayground(false)} + maxWidth="md" + fullWidth + PaperProps={{ + sx: { height: '80vh', display: 'flex', flexDirection: 'column' } + }} + > + + + Playground de Pruebas + + Probando: {playgroundPrompt?.name} (No afecta producción) + + + + + + + + {chatHistory.map((msg, idx) => ( + + {msg.text} + + ))} + {isTyping && ( + + + + )} +
+ + + + setChatInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handlePlaygroundSend()} + autoComplete="off" + size="small" + /> + + + + + +
+