Proyecto ChatBot Con Gemini
This commit is contained in:
24
chatbot-admin/.gitignore
vendored
Normal file
24
chatbot-admin/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
73
chatbot-admin/README.md
Normal file
73
chatbot-admin/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
23
chatbot-admin/eslint.config.js
Normal file
23
chatbot-admin/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
12
chatbot-admin/index.html
Normal file
12
chatbot-admin/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>chatbot-admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4530
chatbot-admin/package-lock.json
generated
Normal file
4530
chatbot-admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
chatbot-admin/package.json
Normal file
36
chatbot-admin/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "chatbot-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.18.0",
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
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>,
|
||||
)
|
||||
28
chatbot-admin/tsconfig.app.json
Normal file
28
chatbot-admin/tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
chatbot-admin/tsconfig.json
Normal file
7
chatbot-admin/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
chatbot-admin/tsconfig.node.json
Normal file
26
chatbot-admin/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
chatbot-admin/vite.config.ts
Normal file
7
chatbot-admin/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user