Files
Chatbot-ElDia/chatbot-admin/src/components/SourceManager.tsx

208 lines
6.6 KiB
TypeScript

// 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 sobre 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;