import React, { useState, useEffect, useCallback } from 'react'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, } from 'recharts'; import { X, Plus, Trash2, TestTube, Loader2, AlertTriangle, } from 'lucide-react'; import { getModels, createModel, updateModel, deleteModel, testModel, getConfig, updateConfig, testVlm, getPdfs, deletePdf, getProgress, resetProgressTopic, backupDatabase, restoreDatabase, downloadExamPdf, } from '../lib/api'; function ToggleSwitch({ checked, onChange }) { return ( {children} ); } export default function Settings() { const [activeTab, setActiveTab] = useState('models'); // Models tab const [models, setModels] = useState([]); const [modelModalOpen, setModelModalOpen] = useState(false); const [editingModel, setEditingModel] = useState(null); const [modelForm, setModelForm] = useState({ name: '', api_base: '', api_key: '', provider: 'openai', is_default_main: false, is_default_fork: false, is_default_exam: false, }); const [modelLoading, setModelLoading] = useState(false); const [testResults, setTestResults] = useState({}); // PDFs & VLM tab const [pdfs, setPdfs] = useState([]); const [vlmUrl, setVlmUrl] = useState(''); const [vlmSaving, setVlmSaving] = useState(false); const [vlmTestResult, setVlmTestResult] = useState(null); // Progress tab const [progress, setProgress] = useState([]); const [restoreFile, setRestoreFile] = useState(null); const [restoreLoading, setRestoreLoading] = useState(false); const refreshModels = useCallback(async () => { try { const rows = await getModels(); setModels(rows); } catch (err) { console.error('[Settings] refresh models error:', err.message); } }, []); const refreshPdfs = useCallback(async () => { try { const rows = await getPdfs(); setPdfs(rows); } catch (err) { console.error('[Settings] refresh pdfs error:', err.message); } }, []); const refreshProgress = useCallback(async () => { try { const rows = await getProgress(); setProgress(rows); } catch (err) { console.error('[Settings] refresh progress error:', err.message); } }, []); const loadConfig = useCallback(async () => { try { const cfg = await getConfig(); if (cfg.vlm_endpoint) { setVlmUrl(cfg.vlm_endpoint); } } catch (err) { console.error('[Settings] load config error:', err.message); } }, []); useEffect(() => { refreshModels(); refreshPdfs(); refreshProgress(); loadConfig(); }, []); // Model handlers const openAddModel = () => { setEditingModel(null); setModelForm({ name: '', api_base: '', api_key: '', provider: 'openai', is_default_main: false, is_default_fork: false, is_default_exam: false, }); setModelModalOpen(true); }; const openEditModel = (model) => { setEditingModel(model); setModelForm({ name: model.name || '', api_base: model.api_base || '', api_key: model.api_key || '', provider: model.provider || 'openai', is_default_main: !!model.is_default_main, is_default_fork: !!model.is_default_fork, is_default_exam: !!model.is_default_exam, }); setModelModalOpen(true); }; const handleSaveModel = async () => { if (!modelForm.name || !modelForm.api_base) return; setModelLoading(true); try { const body = { name: modelForm.name, api_base: modelForm.api_base, api_key: modelForm.api_key, provider: modelForm.provider, is_default_main: modelForm.is_default_main, is_default_fork: modelForm.is_default_fork, is_default_exam: modelForm.is_default_exam, }; if (editingModel) { await updateModel(editingModel.id, body); } else { await createModel(body); } await refreshModels(); setModelModalOpen(false); } catch (err) { alert(err.message); } finally { setModelLoading(false); } }; const handleDeleteModel = async (id) => { if (!window.confirm('¿Eliminar este modelo? No se puede recuperar.')) return; try { await deleteModel(id); await refreshModels(); } catch (err) { alert(err.message); } }; const handleTestModel = async (id) => { setTestResults((prev) => ({ ...prev, [id]: { loading: true } })); try { const result = await testModel(id); setTestResults((prev) => ({ ...prev, [id]: { loading: false, result } })); } catch (err) { setTestResults((prev) => ({ ...prev, [id]: { loading: false, error: err.message } })); } }; const handleToggleDefault = async (model, field) => { const newValue = !model[field]; try { await updateModel(model.id, { [field]: newValue }); await refreshModels(); } catch (err) { alert(err.message); } }; // VLM handlers const handleSaveVlm = async () => { setVlmSaving(true); try { await updateConfig('vlm_endpoint', vlmUrl); setVlmTestResult({ success: true, message: 'Guardado' }); } catch (err) { setVlmTestResult({ success: false, message: err.message }); } finally { setVlmSaving(false); } }; const handleTestVlm = async () => { setVlmTestResult({ loading: true }); try { const result = await testVlm(vlmUrl); setVlmTestResult({ success: result.success, message: result.success ? `${result.message} (${result.latency_ms}ms)` : result.message, }); } catch (err) { setVlmTestResult({ success: false, message: err.message, }); } }; const handleDeletePdf = async (id) => { if (!window.confirm('¿Eliminar este PDF?')) return; try { await deletePdf(id); await refreshPdfs(); } catch (err) { alert(err.message); } }; // Progress handlers const handleResetTopic = async (topic) => { if (!window.confirm(`¿Resetear progreso de "${topic}"?`)) return; try { await resetProgressTopic(topic); await refreshProgress(); } catch (err) { alert(err.message); } }; const handleDownloadBackup = async () => { try { await backupDatabase(); } catch (err) { alert(err.message); } }; const handleRestoreBackup = async () => { if (!restoreFile) return; if (!window.confirm('¿Restaurar base de datos? Se reemplazará el estado actual.')) return; setRestoreLoading(true); try { await restoreDatabase(restoreFile); window.location.reload(); } catch (err) { alert(err.message); setRestoreLoading(false); } }; const progressChartData = progress.map((p) => ({ name: p.topic, pct: p.percentage, fill: p.percentage >= 80 ? 'var(--accent-green)' : p.percentage >= 50 ? 'var(--accent-amber)' : 'var(--accent-coral)', })); return (

Settings

{/* Tab 1: Modelos */} {activeTab === 'models' && (
{models.map((model) => ( ))} {models.length === 0 && ( )}
Name Provider API Base Main Fork Exam Actions
{model.name} {model.provider} {model.api_base} handleToggleDefault(model, 'is_default_main')} /> handleToggleDefault(model, 'is_default_fork')} /> handleToggleDefault(model, 'is_default_exam')} />
{testResults[model.id]?.result && ( {testResults[model.id].result.latency_ms}ms )} {testResults[model.id]?.error && ( Error )}
No hay modelos configurados
)} {/* Tab 2: PDFs & VLM */} {activeTab === 'pdfs' && (

VLM Endpoint

setVlmUrl(e.target.value)} placeholder="https://vlm.example.com/v1/chat" className="vlm-input" />
{vlmTestResult && (
{vlmTestResult.loading ? ( ) : vlmTestResult.success ? ( ) : ( )} {vlmTestResult.message}
)}

PDFs cargados

{pdfs.map((pdf) => ( ))} {pdfs.length === 0 && ( )}
Nombre Orden Acciones
{pdf.filename} {pdf.sort_order}
No hay PDFs cargados
)} {/* Tab 3: Progreso */} {activeTab === 'progress' && (

Gráfico de progreso

[`${value}%`, 'Porcentaje']} /> {progressChartData.map((entry, index) => ( ))}
{progress.map((p) => ( ))} {progress.length === 0 && ( )}
Topic Exercises Done Exercises Correct Percentage Actions
{p.topic} {p.exercises_done} {p.exercises_correct}
= 80 ? 'var(--accent-green)' : p.percentage >= 50 ? 'var(--accent-amber)' : 'var(--accent-coral)', }} />
{p.percentage}%
No hay progreso registrado
)} {/* Tab 4: Backup */} {activeTab === 'backup' && (

Base de datos

setRestoreFile(e.target.files?.[0] || null)} />
)} {/* Tab 5: Datos */} {activeTab === 'datos' && (

Exportar examen simulado

Genera un PDF con los 20 temas más recientes de tu progreso.

)} {/* Model Modal */} {modelModalOpen && ( setModelModalOpen(false)} >
setModelForm((f) => ({ ...f, name: e.target.value }))} placeholder="gpt-4o" />
setModelForm((f) => ({ ...f, api_base: e.target.value }))} placeholder="https://api.openai.com/v1" />
setModelForm((f) => ({ ...f, api_key: e.target.value }))} placeholder="sk-..." />
)}
); }