Feat: Estetico

- Se aplican estilos al resto de controles por uniformidad.
This commit is contained in:
2025-12-05 13:21:24 -03:00
parent 5cef67a2bf
commit 7e9e3ba87e
5 changed files with 267 additions and 173 deletions

View File

@@ -1,12 +1,12 @@
// EN: src/components/AdminPanel.tsx
import React, { useState } from 'react';
import { Box, Typography, AppBar, Toolbar, IconButton, Tabs, Tab } from '@mui/material';
import { Box, Typography, AppBar, Toolbar, IconButton, Tabs, Tab, Container } from '@mui/material';
import LogoutIcon from '@mui/icons-material/Logout';
import SmartToyIcon from '@mui/icons-material/SmartToy';
import ContextManager from './ContextManager';
import LogsViewer from './LogsViewer';
import SourceManager from './SourceManager';
import SystemPromptManager from './SystemPromptManager';
interface AdminPanelProps {
@@ -21,28 +21,57 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ onLogout }) => {
};
return (
<Box>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Panel de Administración del Chatbot
</Typography>
<IconButton color="inherit" onClick={onLogout} aria-label="Cerrar sesión">
<LogoutIcon />
</IconButton>
</Toolbar>
<Tabs value={currentTab} onChange={handleTabChange} textColor="inherit" indicatorColor="secondary">
<Tab label="Gestor de Contexto" />
<Tab label="Historial de Conversaciones" />
<Tab label="Gestor de Fuentes" />
<Tab label="Gestor de Prompts" />
</Tabs>
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<AppBar
position="sticky"
elevation={0}
sx={{
background: 'rgba(18, 18, 18, 0.8)',
backdropFilter: 'blur(12px)',
borderBottom: '1px solid rgba(255,255,255,0.08)'
}}
>
<Container maxWidth="xl">
<Toolbar disableGutters>
<SmartToyIcon sx={{ mr: 2, color: 'primary.main' }} />
<Typography
variant="h6"
component="div"
sx={{
flexGrow: 1,
fontWeight: 700,
letterSpacing: '-0.5px'
}}
>
Chatbot Admin
</Typography>
<IconButton onClick={onLogout} color="error" title="Cerrar sesión">
<LogoutIcon />
</IconButton>
</Toolbar>
<Tabs
value={currentTab}
onChange={handleTabChange}
textColor="inherit"
indicatorColor="secondary"
sx={{ minHeight: '48px' }}
>
<Tab label="Contexto" sx={{ fontWeight: 600 }} />
<Tab label="Historial" sx={{ fontWeight: 600 }} />
<Tab label="Fuentes" sx={{ fontWeight: 600 }} />
<Tab label="Prompts / IA" sx={{ fontWeight: 600 }} />
</Tabs>
</Container>
</AppBar>
{currentTab === 0 && <ContextManager onAuthError={onLogout} />}
{currentTab === 1 && <LogsViewer onAuthError={onLogout} />}
{currentTab === 2 && <SourceManager onAuthError={onLogout} />}
{currentTab === 3 && <SystemPromptManager onAuthError={onLogout} />}
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Container maxWidth="xl">
{currentTab === 0 && <ContextManager onAuthError={onLogout} />}
{currentTab === 1 && <LogsViewer onAuthError={onLogout} />}
{currentTab === 2 && <SourceManager onAuthError={onLogout} />}
{currentTab === 3 && <SystemPromptManager onAuthError={onLogout} />}
</Container>
</Box>
</Box>
);
};

View File

@@ -1,9 +1,9 @@
// src/components/AdminPanel.tsx
// src/components/ContextManager.tsx
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid';
import type { GridColDef } from '@mui/x-data-grid';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Alert, DialogContentText, TextField } from '@mui/material';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Alert, DialogContentText, TextField, Card, Typography } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
@@ -122,90 +122,99 @@ const ContextManager: React.FC<ContextManagerProps> = ({ onAuthError }) => {
icon={<EditIcon />}
label="Editar"
onClick={() => handleOpen(params.row as ContextoItem)}
color="inherit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Eliminar"
// Llama a la función que abre el diálogo
onClick={() => handleDeleteClick(params.id as number)}
color="inherit"
/>,
],
},
];
return (
<Box sx={{ p: 4 }}>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Button startIcon={<AddIcon />} variant="contained" onClick={() => handleOpen()} sx={{ mb: 1 }}>
Añadir Nuevo Item
</Button>
<Box sx={{ height: 600, width: '100%' }}>
<DataGrid rows={rows} columns={columns} pageSizeOptions={[10, 25, 50, 100]} />
</Box>
<Box sx={{ p: 4 }}>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Button startIcon={<AddIcon />} variant="contained" onClick={() => handleOpen()} sx={{ mb: 1 }}>
Añadir Nuevo Item
</Button>
<Box sx={{ height: 600, width: '100%' }}>
<DataGrid rows={rows} columns={columns} pageSizeOptions={[10, 25, 50, 100]} />
</Box>
{/* --- DIÁLOGO DE EDICIÓN/CREACIÓN --- */}
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="sm">
<DialogTitle>{isEdit ? 'Editar Item' : 'Añadir Nuevo Item'}</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="Clave"
fullWidth
value={currentRow.clave || ''}
disabled={isEdit} // La clave no se puede editar una vez creada
onChange={(e) => setCurrentRow({ ...currentRow, clave: e.target.value })}
helperText={!isEdit ? "Esta clave no se podrá modificar en el futuro." : ""}
/>
<TextField
margin="dense"
label="Valor"
fullWidth
multiline
rows={4}
value={currentRow.valor || ''}
onChange={(e) => setCurrentRow({ ...currentRow, valor: e.target.value })}
/>
<TextField
margin="dense"
label="Descripción"
fullWidth
value={currentRow.descripcion || ''}
onChange={(e) => setCurrentRow({ ...currentRow, descripcion: e.target.value })}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancelar</Button>
<Button onClick={handleSave} variant="contained">Guardar</Button>
</DialogActions>
</Dialog>
{/* --- DIÁLOGO DE CONFIRMACIÓN DE BORRADO --- */}
<Dialog
open={confirmOpen}
onClose={handleConfirmClose}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4">Gestor de Contexto</Typography>
<Button
startIcon={<AddIcon />}
variant="contained"
color="secondary"
onClick={() => handleOpen()}
>
<DialogTitle>Confirmar Eliminación</DialogTitle>
<DialogContent>
<DialogContentText>
¿Estás seguro de que quieres eliminar este item? Esta acción no se puede deshacer.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleConfirmClose}>Cancelar</Button>
<Button onClick={handleConfirmDelete} color="error" variant="contained">
Eliminar
</Button>
</DialogActions>
</Dialog>
Nuevo Item
</Button>
</Box>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Card>
<Box sx={{ height: 600, width: '100%' }}>
<DataGrid
rows={rows}
columns={columns}
pageSizeOptions={[10, 25, 50, 100]}
sx={{ border: 'none' }}
/>
</Box>
</Card>
{/* --- DIÁLOGO DE EDICIÓN/CREACIÓN --- */}
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="sm">
<DialogTitle>{isEdit ? 'Editar Item' : 'Añadir Nuevo Item'}</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="Clave"
fullWidth
value={currentRow.clave || ''}
disabled={isEdit} // La clave no se puede editar una vez creada
onChange={(e) => setCurrentRow({ ...currentRow, clave: e.target.value })}
helperText={!isEdit ? "Esta clave no se podrá modificar en el futuro." : ""}
/>
<TextField
margin="dense"
label="Valor"
fullWidth
multiline
rows={4}
value={currentRow.valor || ''}
onChange={(e) => setCurrentRow({ ...currentRow, valor: e.target.value })}
/>
<TextField
margin="dense"
label="Descripción"
fullWidth
value={currentRow.descripcion || ''}
onChange={(e) => setCurrentRow({ ...currentRow, descripcion: e.target.value })}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancelar</Button>
<Button onClick={handleSave} variant="contained">Guardar</Button>
</DialogActions>
</Dialog>
{/* --- DIÁLOGO DE CONFIRMACIÓN DE BORRADO --- */}
<Dialog
open={confirmOpen}
onClose={handleConfirmClose}
>
<DialogTitle>Confirmar Eliminación</DialogTitle>
<DialogContent>
<DialogContentText>
¿Estás seguro de que quieres eliminar este item? Esta acción no se puede deshacer.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleConfirmClose}>Cancelar</Button>
<Button onClick={handleConfirmDelete} color="error" variant="contained">
Eliminar
</Button>
</DialogActions>
</Dialog>
</Box>
);
};

View File

@@ -1,6 +1,7 @@
// src/components/Login.tsx
import React from 'react';
import { Box, Button, TextField, Typography, Paper, Alert } from '@mui/material';
import { Box, Button, TextField, Typography, Card, CardContent, Alert } from '@mui/material';
import apiClient from '../api/apiClient';
interface LoginProps {
onLoginSuccess: (token: string) => void;
@@ -10,70 +11,94 @@ const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState('');
const [loading, setLoading] = React.useState(false);
const handleLogin = async (event: React.FormEvent) => {
event.preventDefault();
setError('');
setLoading(true);
try {
// Usamos la variable de entorno para la URL de la API
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
// Using apiClient ensures consistently handled headers/configs
const response = await apiClient.post('/api/auth/login', { username, password });
if (response.ok) {
const data = await response.json();
onLoginSuccess(data.token);
// Axios returns data directly in response.data
if (response.data && response.data.token) {
onLoginSuccess(response.data.token);
} else {
setError('Usuario o contraseña incorrectos.');
setError('Respuesta inesperada del servidor.');
}
} catch (err) {
setError('No se pudo conectar con el servidor. Inténtalo de nuevo más tarde.');
} catch (err: any) {
console.error(err);
if (err.response && err.response.status === 401) {
setError('Usuario o contraseña incorrectos.');
} else {
setError('No se pudo conectar con el servidor.');
}
} finally {
setLoading(false);
}
};
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<Paper elevation={6} sx={{ padding: 4, width: '100%', maxWidth: 400 }}>
<Typography variant="h4" component="h1" gutterBottom textAlign="center">
Iniciar Sesión
</Typography>
<Typography variant="body2" textAlign="center" sx={{ mb: 3 }}>
Gestor de Contexto del Chatbot
</Typography>
<form onSubmit={handleLogin}>
<TextField
label="Usuario"
variant="outlined"
fullWidth
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
/>
<TextField
label="Contraseña"
type="password"
variant="outlined"
fullWidth
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
<Button
type="submit"
variant="contained"
fullWidth
size="large"
sx={{ mt: 3 }}
>
Entrar
</Button>
</form>
</Paper>
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: 'radial-gradient(circle at 50% 50%, #1a1a1a 0%, #000 100%)' // Fallback/Enhancement
}}>
<Card sx={{ maxWidth: 450, width: '100%', mx: 2 }}>
<CardContent sx={{ p: 4, display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ textAlign: 'center', mb: 1 }}>
<Typography variant="h4" gutterBottom>
Bienvenido
</Typography>
<Typography variant="body2" color="text.secondary">
Inicia sesión para gestionar el Chatbot
</Typography>
</Box>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<TextField
label="Usuario"
variant="outlined"
fullWidth
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}
/>
<TextField
label="Contraseña"
type="password"
variant="outlined"
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}
/>
{error && <Alert severity="error">{error}</Alert>}
<Button
type="submit"
variant="contained"
fullWidth
size="large"
disabled={loading}
sx={{
mt: 1,
py: 1.5,
background: 'linear-gradient(45deg, #7c4dff 30%, #ff4081 90%)',
fontSize: '1rem'
}}
>
{loading ? 'Entrando...' : 'Iniciar Sesión'}
</Button>
</form>
</CardContent>
</Card>
</Box>
);
};

View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
import { Box, Typography, Alert } from '@mui/material';
import { Box, Typography, Alert, Card } from '@mui/material';
import apiClient from '../api/apiClient';
interface ConversacionLog {
@@ -66,23 +66,28 @@ const LogsViewer: React.FC<LogsViewerProps> = ({ onAuthError }) => {
];
return (
<Box sx={{ p: 4 }}>
<Typography variant="h4" gutterBottom>
Historial de Conversaciones
</Typography>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Box sx={{ height: 700, width: '100%' }}>
<DataGrid
rows={logs}
columns={columns}
pageSizeOptions={[25, 50, 100]}
initialState={{
sorting: {
sortModel: [{ field: 'fecha', sort: 'desc' }],
},
}}
/>
<Box>
<Box sx={{ mb: 3 }}>
<Typography variant="h4">Historial de Conversaciones</Typography>
</Box>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Card>
<Box sx={{ height: 700, width: '100%' }}>
<DataGrid
rows={logs}
columns={columns}
pageSizeOptions={[25, 50, 100]}
initialState={{
sorting: {
sortModel: [{ field: 'fecha', sort: 'desc' }],
},
}}
sx={{ border: 'none' }}
/>
</Box>
</Card>
</Box>
);
};

View File

@@ -3,7 +3,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid';
import type { GridColDef } from '@mui/x-data-grid';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Alert, TextField, Chip, Switch, FormControlLabel } from '@mui/material';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Alert, TextField, Chip, Switch, FormControlLabel, Card, Typography } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
@@ -109,7 +109,12 @@ const SourceManager: React.FC<SourceManagerProps> = ({ onAuthError }) => {
headerName: 'Activo',
width: 100,
renderCell: (params) => (
<Chip label={params.value ? 'Sí' : 'No'} color={params.value ? 'success' : 'default'} />
<Chip
label={params.value ? 'Sí' : 'No'}
color={params.value ? 'success' : 'default'}
variant={params.value ? 'filled' : 'outlined'}
size="small"
/>
),
},
{
@@ -121,25 +126,45 @@ const SourceManager: React.FC<SourceManagerProps> = ({ onAuthError }) => {
icon={<EditIcon />}
label="Editar"
onClick={() => handleOpen(params.row as FuenteContexto)}
color="inherit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Eliminar"
onClick={() => handleDeleteClick(params.id as number)}
color="inherit" // Keeping it generic to avoid type errors
/>,
],
},
];
return (
<Box sx={{ p: 4 }}>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Button startIcon={<AddIcon />} variant="contained" onClick={() => handleOpen()} sx={{ mb: 2 }}>
Añadir Nueva Fuente
</Button>
<Box sx={{ height: 600, width: '100%' }}>
<DataGrid rows={rows} columns={columns} pageSizeOptions={[10, 25, 50]} />
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4">Gestor de Fuentes</Typography>
<Button
startIcon={<AddIcon />}
variant="contained"
color="secondary"
onClick={() => handleOpen()}
>
Nueva Fuente
</Button>
</Box>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Card>
<Box sx={{ height: 600, width: '100%' }}>
<DataGrid
rows={rows}
columns={columns}
pageSizeOptions={[10, 25, 50]}
sx={{ border: 'none' }}
/>
</Box>
</Card>
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="md">
<DialogTitle>{isEdit ? 'Editar Fuente' : 'Añadir Nueva Fuente'}</DialogTitle>
<DialogContent>
@@ -186,6 +211,7 @@ const SourceManager: React.FC<SourceManagerProps> = ({ onAuthError }) => {
/>
}
label="Fuente activa"
sx={{ mt: 2 }}
/>
</DialogContent>
<DialogActions>