Feat Gestion de Fuentes URLs
This commit is contained in:
@@ -1,17 +1,16 @@
|
||||
// src/components/AdminPanel.tsx
|
||||
// EN: 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 ContextManager from './ContextManager';
|
||||
import LogsViewer from './LogsViewer';
|
||||
import SourceManager from './SourceManager';
|
||||
|
||||
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);
|
||||
|
||||
@@ -33,12 +32,13 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ onLogout }) => {
|
||||
<Tabs value={currentTab} onChange={handleTabChange} textColor="inherit" indicatorColor="secondary">
|
||||
<Tab label="Gestor de Contexto" />
|
||||
<Tab label="Historial de Conversaciones" />
|
||||
<Tab label="Gestor de Fuentes" />
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
|
||||
{/* Mostramos el componente correspondiente a la pestaña activa */}
|
||||
{currentTab === 0 && <ContextManager onAuthError={onLogout} />}
|
||||
{currentTab === 1 && <LogsViewer onAuthError={onLogout} />}
|
||||
{currentTab === 2 && <SourceManager onAuthError={onLogout} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
208
chatbot-admin/src/components/SourceManager.tsx
Normal file
208
chatbot-admin/src/components/SourceManager.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
// EN: src/components/SourceManager.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, TextField, Chip, Switch, FormControlLabel } 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 FuenteContexto {
|
||||
id: number;
|
||||
nombre: string;
|
||||
url: string;
|
||||
descripcionParaIA: string;
|
||||
activo: boolean;
|
||||
}
|
||||
|
||||
interface SourceManagerProps {
|
||||
onAuthError: () => void;
|
||||
}
|
||||
|
||||
const SourceManager: React.FC<SourceManagerProps> = ({ onAuthError }) => {
|
||||
const [rows, setRows] = useState<FuenteContexto[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<Partial<FuenteContexto>>({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState<number | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
// --- ENDPOINT ---
|
||||
const response = await apiClient.get('/api/admin/fuentes');
|
||||
setRows(response.data);
|
||||
} catch (err) {
|
||||
setError('No se pudieron cargar las fuentes de contexto.');
|
||||
if (axios.isAxiosError(err) && err.response?.status === 401) {
|
||||
onAuthError();
|
||||
}
|
||||
}
|
||||
}, [onAuthError]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleOpen = (item?: FuenteContexto) => {
|
||||
if (item) {
|
||||
setIsEdit(true);
|
||||
setCurrentRow(item);
|
||||
} else {
|
||||
// --- ESTADO INICIAL ---
|
||||
setIsEdit(false);
|
||||
setCurrentRow({ nombre: '', url: '', descripcionParaIA: '', activo: true });
|
||||
}
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (isEdit) {
|
||||
await apiClient.put(`/api/admin/fuentes/${currentRow.id}`, currentRow);
|
||||
} else {
|
||||
await apiClient.post('/api/admin/fuentes', currentRow);
|
||||
}
|
||||
fetchData();
|
||||
handleClose();
|
||||
} catch (err) {
|
||||
setError('Error al guardar la fuente.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: number) => {
|
||||
setItemToDelete(id);
|
||||
setConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmClose = () => {
|
||||
setConfirmOpen(false);
|
||||
setItemToDelete(null);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (itemToDelete !== null) {
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/fuentes/${itemToDelete}`);
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
setError('Error al eliminar la fuente.');
|
||||
} finally {
|
||||
handleConfirmClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- DEFINICIÓN DE COLUMNAS ---
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'nombre', headerName: 'Nombre', width: 200 },
|
||||
{ field: 'url', headerName: 'URL', width: 350 },
|
||||
{ field: 'descripcionParaIA', headerName: 'Descripción para IA', flex: 1 },
|
||||
{
|
||||
field: 'activo',
|
||||
headerName: 'Activo',
|
||||
width: 100,
|
||||
renderCell: (params) => (
|
||||
<Chip label={params.value ? 'Sí' : 'No'} color={params.value ? 'success' : 'default'} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
width: 100,
|
||||
getActions: (params) => [
|
||||
<GridActionsCellItem
|
||||
icon={<EditIcon />}
|
||||
label="Editar"
|
||||
onClick={() => handleOpen(params.row as FuenteContexto)}
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
icon={<DeleteIcon />}
|
||||
label="Eliminar"
|
||||
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: 2 }}>
|
||||
Añadir Nueva Fuente
|
||||
</Button>
|
||||
<Box sx={{ height: 600, width: '100%' }}>
|
||||
<DataGrid rows={rows} columns={columns} pageSizeOptions={[10, 25, 50]} />
|
||||
</Box>
|
||||
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>{isEdit ? 'Editar Fuente' : 'Añadir Nueva Fuente'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="Nombre"
|
||||
fullWidth
|
||||
value={currentRow.nombre || ''}
|
||||
onChange={(e) => setCurrentRow({ ...currentRow, nombre: e.target.value })}
|
||||
helperText="Un nombre corto y descriptivo (ej: FAQs de Suscripción)."
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="URL"
|
||||
fullWidth
|
||||
value={currentRow.url || ''}
|
||||
onChange={(e) => setCurrentRow({ ...currentRow, url: e.target.value })}
|
||||
helperText="La URL completa de la página que el bot debe leer."
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="Descripción para la IA"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={currentRow.descripcionParaIA || ''}
|
||||
onChange={(e) => setCurrentRow({ ...currentRow, descripcionParaIA: e.target.value })}
|
||||
helperText="¡Crucial! Describe en una frase para qué sirve esta fuente. Ej: 'Usar para responder preguntas sobre cómo registrarse, iniciar sesión o por qué es obligatorio el registro'."
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={currentRow.activo ?? true}
|
||||
onChange={(e) => setCurrentRow({ ...currentRow, activo: e.target.checked })}
|
||||
/>
|
||||
}
|
||||
label="Fuente activa"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancelar</Button>
|
||||
<Button onClick={handleSave} variant="contained">Guardar</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
open={confirmOpen}
|
||||
onClose={handleConfirmClose}
|
||||
>
|
||||
<DialogTitle>Confirmar Eliminación</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContent>
|
||||
¿Estás seguro de que quieres eliminar este item? Esta acción no se puede deshacer.
|
||||
</DialogContent>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleConfirmClose}>Cancelar</Button>
|
||||
<Button onClick={handleConfirmDelete} color="error" variant="contained">
|
||||
Eliminar
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourceManager;
|
||||
Reference in New Issue
Block a user