Feat: Prompt Test
- Se implementa el testeo de prompts en el panel de admin sin activación. - Se realizan mejoras visuales en los controles.
This commit is contained in:
@@ -336,7 +336,9 @@ namespace ChatbotApi.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var promptBuilder = new StringBuilder();
|
var promptBuilder = new StringBuilder();
|
||||||
var systemInstructions = await GetActiveSystemPromptsAsync();
|
var systemInstructions = !string.IsNullOrWhiteSpace(request.SystemPromptOverride)
|
||||||
|
? request.SystemPromptOverride
|
||||||
|
: await GetActiveSystemPromptsAsync();
|
||||||
|
|
||||||
promptBuilder.AppendLine("<instrucciones_sistema>");
|
promptBuilder.AppendLine("<instrucciones_sistema>");
|
||||||
promptBuilder.AppendLine("Eres DiaBot, asistente virtual de El Día (La Plata, Argentina).");
|
promptBuilder.AppendLine("Eres DiaBot, asistente virtual de El Día (La Plata, Argentina).");
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ public class ChatRequest
|
|||||||
public string? ContextUrl { get; set; }
|
public string? ContextUrl { get; set; }
|
||||||
public string? ConversationSummary { get; set; }
|
public string? ConversationSummary { get; set; }
|
||||||
public List<string>? ShownArticles { get; set; }
|
public List<string>? ShownArticles { get; set; }
|
||||||
|
public string? SystemPromptOverride { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,8 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import AdminPanel from './components/AdminPanel';
|
import AdminPanel from './components/AdminPanel';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
|
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||||
|
import theme from './theme';
|
||||||
const darkTheme = createTheme({
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
|
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
|
||||||
@@ -26,7 +21,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
{token ? (
|
{token ? (
|
||||||
<AdminPanel onLogout={handleLogout} />
|
<AdminPanel onLogout={handleLogout} />
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, Typography, Button, TextField, Paper, List, ListItem,
|
Box, Typography, Button, TextField, Paper, IconButton, Switch,
|
||||||
ListItemText, ListItemSecondaryAction, IconButton, Switch,
|
|
||||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||||
Snackbar, Alert
|
Snackbar, Alert, Grid, Card, CardContent, CardActions, Chip, Divider,
|
||||||
|
CircularProgress
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
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';
|
import apiClient from '../api/apiClient';
|
||||||
|
|
||||||
interface SystemPrompt {
|
interface SystemPrompt {
|
||||||
@@ -32,10 +35,26 @@ const SystemPromptManager: React.FC<SystemPromptManagerProps> = ({ onAuthError }
|
|||||||
severity: 'success'
|
severity: 'success'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Playground state
|
||||||
|
const [openPlayground, setOpenPlayground] = useState(false);
|
||||||
|
const [playgroundPrompt, setPlaygroundPrompt] = useState<SystemPrompt | null>(null);
|
||||||
|
const [chatInput, setChatInput] = useState('');
|
||||||
|
const [chatHistory, setChatHistory] = useState<{ role: 'user' | 'bot'; text: string }[]>([]);
|
||||||
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
|
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPrompts();
|
fetchPrompts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openPlayground) scrollToBottom();
|
||||||
|
}, [chatHistory, openPlayground]);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
const fetchPrompts = async () => {
|
const fetchPrompts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/api/SystemPrompts');
|
const response = await apiClient.get('/api/SystemPrompts');
|
||||||
@@ -96,82 +115,201 @@ const SystemPromptManager: React.FC<SystemPromptManagerProps> = ({ onAuthError }
|
|||||||
setOpenDialog(true);
|
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<string>.
|
||||||
|
// 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<string> 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 (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
|
||||||
<Typography variant="h5">Gestión de Prompts del Sistema</Typography>
|
<Typography variant="h4">Gestión de Prompts</Typography>
|
||||||
<Button variant="contained" color="primary" onClick={openCreate}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={openCreate}
|
||||||
|
sx={{ px: 3, py: 1, borderRadius: 2 }}
|
||||||
|
>
|
||||||
Nuevo Prompt
|
Nuevo Prompt
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Paper>
|
{prompts.length === 0 ? (
|
||||||
<List>
|
<Paper sx={{ p: 4, textAlign: 'center' }}>
|
||||||
|
<Typography color="textSecondary">No hay prompts definidos.</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<Grid container spacing={3}>
|
||||||
{prompts.map((prompt) => (
|
{prompts.map((prompt) => (
|
||||||
<ListItem key={prompt.id} divider>
|
<Grid size={{ xs: 12, md: 6, lg: 4 }} key={prompt.id}>
|
||||||
<ListItemText
|
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', position: 'relative' }}>
|
||||||
primary={
|
<CardContent sx={{ flexGrow: 1 }}>
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start" mb={2}>
|
||||||
<Typography variant="subtitle1" fontWeight="bold">
|
<Typography variant="h6" fontWeight="bold" sx={{ color: 'primary.light' }}>
|
||||||
{prompt.name}
|
{prompt.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
{prompt.isActive && (
|
<Switch
|
||||||
<Box
|
checked={prompt.isActive}
|
||||||
component="span"
|
onChange={() => handleToggleActive(prompt.id)}
|
||||||
sx={{
|
color="success"
|
||||||
ml: 2,
|
size="small"
|
||||||
px: 1,
|
/>
|
||||||
py: 0.5,
|
|
||||||
bgcolor: 'success.light',
|
|
||||||
color: 'white',
|
|
||||||
borderRadius: 1,
|
|
||||||
fontSize: '0.75rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ACTIVO
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
|
||||||
secondary={
|
{prompt.isActive && (
|
||||||
<Typography
|
<Chip label="ACTIVO EN PRODUCCIÓN" color="success" size="small" sx={{ mb: 2, fontWeight: 'bold' }} />
|
||||||
variant="body2"
|
)}
|
||||||
color="textSecondary"
|
|
||||||
sx={{
|
<Typography variant="body2" color="textSecondary" sx={{
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 4,
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}}
|
bgcolor: 'background.default',
|
||||||
>
|
p: 1.5,
|
||||||
|
borderRadius: 1,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}>
|
||||||
{prompt.content}
|
{prompt.content}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
</CardContent>
|
||||||
/>
|
<Divider sx={{ opacity: 0.1 }} />
|
||||||
<ListItemSecondaryAction>
|
<CardActions sx={{ justifyContent: 'space-between', px: 2, py: 1.5 }}>
|
||||||
<Switch
|
<Button
|
||||||
edge="end"
|
size="small"
|
||||||
checked={prompt.isActive}
|
color="info"
|
||||||
onChange={() => handleToggleActive(prompt.id)}
|
startIcon={<PlayArrowIcon />}
|
||||||
color="primary"
|
onClick={() => openTestPlayground(prompt)}
|
||||||
/>
|
>
|
||||||
<IconButton edge="end" aria-label="edit" onClick={() => openEdit(prompt)} sx={{ ml: 1 }}>
|
Probar
|
||||||
<EditIcon />
|
</Button>
|
||||||
</IconButton>
|
<Box>
|
||||||
<IconButton edge="end" aria-label="delete" onClick={() => handleDelete(prompt.id)} sx={{ ml: 1 }}>
|
<IconButton size="small" onClick={() => openEdit(prompt)} color="primary">
|
||||||
<DeleteIcon />
|
<EditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItemSecondaryAction>
|
<IconButton size="small" onClick={() => handleDelete(prompt.id)} color="error">
|
||||||
</ListItem>
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
))}
|
))}
|
||||||
{prompts.length === 0 && (
|
</Grid>
|
||||||
<ListItem>
|
)}
|
||||||
<ListItemText primary="No hay prompts definidos. El sistema usará el comportamiento por defecto." />
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
|
{/* --- EDIT / CREATE DIALOG --- */}
|
||||||
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
|
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
|
||||||
<DialogTitle>{currentPrompt.id ? 'Editar Prompt' : 'Nuevo Prompt'}</DialogTitle>
|
<DialogTitle>{currentPrompt.id ? 'Editar Prompt' : 'Nuevo Prompt'}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -182,21 +320,21 @@ const SystemPromptManager: React.FC<SystemPromptManagerProps> = ({ onAuthError }
|
|||||||
fullWidth
|
fullWidth
|
||||||
value={currentPrompt.name || ''}
|
value={currentPrompt.name || ''}
|
||||||
onChange={(e) => setCurrentPrompt({ ...currentPrompt, name: e.target.value })}
|
onChange={(e) => setCurrentPrompt({ ...currentPrompt, name: e.target.value })}
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 3, mt: 1 }}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
|
||||||
label="Contenido del Prompt (Instrucciones de Sistema)"
|
label="Contenido del Prompt (Instrucciones de Sistema)"
|
||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
multiline
|
||||||
rows={10}
|
rows={12}
|
||||||
value={currentPrompt.content || ''}
|
value={currentPrompt.content || ''}
|
||||||
onChange={(e) => setCurrentPrompt({ ...currentPrompt, content: e.target.value })}
|
onChange={(e) => 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' }}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
<Button onClick={() => setOpenDialog(false)} color="secondary">
|
<Button onClick={() => setOpenDialog(false)} color="inherit">
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave} color="primary" variant="contained">
|
<Button onClick={handleSave} color="primary" variant="contained">
|
||||||
@@ -205,6 +343,70 @@ const SystemPromptManager: React.FC<SystemPromptManagerProps> = ({ onAuthError }
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* --- PLAYGROUND DIALOG --- */}
|
||||||
|
<Dialog
|
||||||
|
open={openPlayground}
|
||||||
|
onClose={() => setOpenPlayground(false)}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: { height: '80vh', display: 'flex', flexDirection: 'column' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ borderBottom: '1px solid rgba(255,255,255,0.1)', display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6">Playground de Pruebas</Typography>
|
||||||
|
<Typography variant="caption" color="textSecondary">
|
||||||
|
Probando: <span style={{ color: '#b47cff' }}>{playgroundPrompt?.name}</span> (No afecta producción)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button size="small" onClick={() => setOpenPlayground(false)}>Cerrar</Button>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', p: 0 }}>
|
||||||
|
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
{chatHistory.map((msg, idx) => (
|
||||||
|
<Box
|
||||||
|
key={idx}
|
||||||
|
sx={{
|
||||||
|
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
||||||
|
maxWidth: '80%',
|
||||||
|
bgcolor: msg.role === 'user' ? 'primary.main' : 'background.paper',
|
||||||
|
color: msg.role === 'user' ? 'white' : 'text.primary',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">{msg.text}</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{isTyping && (
|
||||||
|
<Box sx={{ alignSelf: 'flex-start', p: 2 }}>
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ p: 2, bgcolor: 'background.paper', borderTop: '1px solid rgba(255,255,255,0.1)', display: 'flex', gap: 1 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
placeholder="Escribe un mensaje de prueba..."
|
||||||
|
value={chatInput}
|
||||||
|
onChange={(e) => setChatInput(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handlePlaygroundSend()}
|
||||||
|
autoComplete="off"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<IconButton color="primary" onClick={handlePlaygroundSend} disabled={isTyping || !chatInput.trim()}>
|
||||||
|
<SendIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={snackbar.open}
|
open={snackbar.open}
|
||||||
autoHideDuration={6000}
|
autoHideDuration={6000}
|
||||||
|
|||||||
87
chatbot-admin/src/theme.ts
Normal file
87
chatbot-admin/src/theme.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { createTheme, alpha } from '@mui/material/styles';
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
primary: {
|
||||||
|
main: '#7c4dff', // Deep Purple
|
||||||
|
light: '#b47cff',
|
||||||
|
dark: '#3f1dcb',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#ff4081', // Pink A200
|
||||||
|
light: '#ff79b0',
|
||||||
|
dark: '#c60055',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: '#121212',
|
||||||
|
paper: '#1e1e1e',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
h5: {
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontWeight: 700,
|
||||||
|
background: 'linear-gradient(45deg, #7c4dff 30%, #ff4081 90%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: {
|
||||||
|
body: {
|
||||||
|
// Optional: Background pattern or gradient
|
||||||
|
backgroundImage: 'radial-gradient(circle at 50% 50%, #1f1f1f 0%, #000000 100%)',
|
||||||
|
backgroundAttachment: 'fixed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPaper: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundImage: 'none', // Remove default gradient
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: alpha('#1e1e1e', 0.6),
|
||||||
|
backdropFilter: 'blur(12px)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.12)',
|
||||||
|
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-4px)',
|
||||||
|
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
|
||||||
|
border: '1px solid rgba(124, 77, 255, 0.5)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
textTransform: 'none',
|
||||||
|
borderRadius: 8,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 4px 12px rgba(124, 77, 255, 0.4)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
Reference in New Issue
Block a user