Proyecto ChatBot Con Gemini
This commit is contained in:
6
chatbot-admin/src/App.css
Normal file
6
chatbot-admin/src/App.css
Normal file
@@ -0,0 +1,6 @@
|
||||
#root {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
40
chatbot-admin/src/App.tsx
Normal file
40
chatbot-admin/src/App.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
// src/App.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import AdminPanel from './components/AdminPanel';
|
||||
import Login from './components/Login';
|
||||
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
|
||||
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
localStorage.setItem('jwt_token', token);
|
||||
} else {
|
||||
localStorage.removeItem('jwt_token');
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const handleLogout = () => {
|
||||
setToken(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
{token ? (
|
||||
<AdminPanel onLogout={handleLogout} />
|
||||
) : (
|
||||
<Login onLoginSuccess={setToken} />
|
||||
)}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
27
chatbot-admin/src/api/apiClient.ts
Normal file
27
chatbot-admin/src/api/apiClient.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// src/api/apiClient.ts
|
||||
import axios from 'axios';
|
||||
|
||||
// Creamos la instancia de Axios
|
||||
const apiClient = axios.create({
|
||||
// Es una buena práctica establecer la URL base aquí
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL
|
||||
});
|
||||
|
||||
// Añadimos el interceptor para inyectar el token JWT en cada petición
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('jwt_token');
|
||||
if (token) {
|
||||
// Aseguramos que la cabecera de autorización se establezca correctamente
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
// Manejamos errores en la configuración de la petición
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Exportamos la instancia configurada para que otros archivos puedan usarla
|
||||
export default apiClient;
|
||||
46
chatbot-admin/src/components/AdminPanel.tsx
Normal file
46
chatbot-admin/src/components/AdminPanel.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
// src/components/AdminPanel.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Typography, AppBar, Toolbar, IconButton, Tabs, Tab } from '@mui/material';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
|
||||
// Importamos los dos componentes que mostraremos en las pestañas
|
||||
import ContextManager from './ContextManager'; // Renombraremos el AdminPanel original
|
||||
import LogsViewer from './LogsViewer';
|
||||
|
||||
interface AdminPanelProps {
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
// El componente se convierte en un contenedor con pestañas
|
||||
const AdminPanel: React.FC<AdminPanelProps> = ({ onLogout }) => {
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setCurrentTab(newValue);
|
||||
};
|
||||
|
||||
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" />
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
|
||||
{/* Mostramos el componente correspondiente a la pestaña activa */}
|
||||
{currentTab === 0 && <ContextManager onAuthError={onLogout} />}
|
||||
{currentTab === 1 && <LogsViewer onAuthError={onLogout} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPanel;
|
||||
213
chatbot-admin/src/components/ContextManager.tsx
Normal file
213
chatbot-admin/src/components/ContextManager.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
// src/components/AdminPanel.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 EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import apiClient from '../api/apiClient';
|
||||
|
||||
interface ContextoItem {
|
||||
id: number;
|
||||
clave: string;
|
||||
valor: string;
|
||||
descripcion: string;
|
||||
fechaActualizacion: string;
|
||||
}
|
||||
|
||||
interface ContextManagerProps {
|
||||
onAuthError: () => void;
|
||||
}
|
||||
|
||||
const ContextManager: React.FC<ContextManagerProps> = ({ onAuthError }) => {
|
||||
const [rows, setRows] = useState<ContextoItem[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<Partial<ContextoItem>>({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Estado para el diálogo de confirmación de borrado
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState<number | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/admin/contexto');
|
||||
setRows(response.data);
|
||||
} catch (err) {
|
||||
setError('No se pudieron cargar los datos.');
|
||||
if (axios.isAxiosError(err) && err.response?.status === 401) {
|
||||
onAuthError();
|
||||
}
|
||||
}
|
||||
}, [onAuthError]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleOpen = (item?: ContextoItem) => {
|
||||
if (item) {
|
||||
setIsEdit(true);
|
||||
setCurrentRow(item);
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
setCurrentRow({ clave: '', valor: '', descripcion: '' });
|
||||
}
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (isEdit) {
|
||||
await apiClient.put(`/api/admin/contexto/${currentRow.id}`, currentRow);
|
||||
} else {
|
||||
await apiClient.post('/api/admin/contexto', currentRow);
|
||||
}
|
||||
fetchData();
|
||||
handleClose();
|
||||
} catch (err) {
|
||||
setError('Error al guardar el item.');
|
||||
}
|
||||
};
|
||||
|
||||
// Abre el diálogo de confirmación
|
||||
const handleDeleteClick = (id: number) => {
|
||||
setItemToDelete(id);
|
||||
setConfirmOpen(true);
|
||||
};
|
||||
|
||||
// Cierra el diálogo de confirmación
|
||||
const handleConfirmClose = () => {
|
||||
setConfirmOpen(false);
|
||||
setItemToDelete(null);
|
||||
};
|
||||
|
||||
// Ejecuta la eliminación si se confirma
|
||||
const handleConfirmDelete = async () => {
|
||||
if (itemToDelete !== null) {
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/contexto/${itemToDelete}`);
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
setError('Error al eliminar el item.');
|
||||
} finally {
|
||||
handleConfirmClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'clave', headerName: 'Clave', width: 200 },
|
||||
{ field: 'valor', headerName: 'Valor', width: 350 },
|
||||
{ field: 'descripcion', headerName: 'Descripción', flex: 1 },
|
||||
{
|
||||
field: 'fechaActualizacion',
|
||||
headerName: 'Última Actualización',
|
||||
width: 200,
|
||||
valueGetter: (value) => new Date(value).toLocaleString(),
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
width: 100,
|
||||
getActions: (params) => [
|
||||
<GridActionsCellItem
|
||||
icon={<EditIcon />}
|
||||
label="Editar"
|
||||
onClick={() => handleOpen(params.row as ContextoItem)}
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
icon={<DeleteIcon />}
|
||||
label="Eliminar"
|
||||
// Llama a la función que abre el diálogo
|
||||
onClick={() => handleDeleteClick(params.id as number)}
|
||||
/>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContextManager;
|
||||
81
chatbot-admin/src/components/Login.tsx
Normal file
81
chatbot-admin/src/components/Login.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
// src/components/Login.tsx
|
||||
import React from 'react';
|
||||
import { Box, Button, TextField, Typography, Paper, Alert } from '@mui/material';
|
||||
|
||||
interface LoginProps {
|
||||
onLoginSuccess: (token: string) => void;
|
||||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [error, setError] = React.useState('');
|
||||
|
||||
const handleLogin = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
|
||||
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 }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
onLoginSuccess(data.token);
|
||||
} else {
|
||||
setError('Usuario o contraseña incorrectos.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('No se pudo conectar con el servidor. Inténtalo de nuevo más tarde.');
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
72
chatbot-admin/src/components/LogsViewer.tsx
Normal file
72
chatbot-admin/src/components/LogsViewer.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
// src/components/LogsViewer.tsx
|
||||
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 apiClient from '../api/apiClient';
|
||||
|
||||
interface ConversacionLog {
|
||||
id: number;
|
||||
usuarioMensaje: string;
|
||||
botRespuesta: string;
|
||||
fecha: string;
|
||||
}
|
||||
|
||||
interface LogsViewerProps {
|
||||
onAuthError: () => void; // Función para desloguear si el token es inválido
|
||||
}
|
||||
|
||||
const LogsViewer: React.FC<LogsViewerProps> = ({ onAuthError }) => {
|
||||
const [logs, setLogs] = useState<ConversacionLog[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/admin/logs');
|
||||
setLogs(response.data);
|
||||
} catch (err) {
|
||||
setError('No se pudieron cargar los logs.');
|
||||
if (axios.isAxiosError(err) && err.response?.status === 401) {
|
||||
onAuthError(); // Llamamos a la función de logout si hay un error de autenticación
|
||||
}
|
||||
}
|
||||
}, [onAuthError]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [fetchLogs]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'fecha',
|
||||
headerName: 'Fecha',
|
||||
width: 200,
|
||||
valueGetter: (value) => new Date(value).toLocaleString(),
|
||||
},
|
||||
{ field: 'usuarioMensaje', headerName: 'Mensaje del Usuario', flex: 1 },
|
||||
{ field: 'botRespuesta', headerName: 'Respuesta del Bot', flex: 1 },
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogsViewer;
|
||||
66
chatbot-admin/src/index.css
Normal file
66
chatbot-admin/src/index.css
Normal file
@@ -0,0 +1,66 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
10
chatbot-admin/src/main.tsx
Normal file
10
chatbot-admin/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user