Initial commit: StudyOS platform
This commit is contained in:
186
client/src/App.jsx
Normal file
186
client/src/App.jsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import MainChat from './components/MainChat';
|
||||
import ChatInput from './components/ChatInput';
|
||||
import MessageBubble from './components/MessageBubble';
|
||||
import ForkPanel from './components/ForkPanel';
|
||||
import Settings from './pages/Settings';
|
||||
import useChat from './hooks/useChat';
|
||||
import usePdfs from './hooks/usePdfs';
|
||||
import useProgress from './hooks/useProgress';
|
||||
import {
|
||||
getConversations,
|
||||
createConversation,
|
||||
getModels,
|
||||
forkConversation,
|
||||
mergeConversation,
|
||||
getNotes,
|
||||
} from './lib/api';
|
||||
import './App.css';
|
||||
|
||||
export default function App() {
|
||||
const [conversaciones, setConversaciones] = useState([]);
|
||||
const [conversationActiva, setConversationActiva] = useState(null);
|
||||
const [forkActivo, setForkActivo] = useState(null);
|
||||
const [modelos, setModelos] = useState([]);
|
||||
const [modeloSeleccionado, setModeloSeleccionado] = useState(null);
|
||||
const [notas, setNotas] = useState([]);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const pdfsHook = usePdfs();
|
||||
const progressHook = useProgress();
|
||||
const chatHook = useChat({
|
||||
conversationId: conversationActiva?.id ?? null,
|
||||
onProgressUpdate: progressHook.updateExercise,
|
||||
});
|
||||
|
||||
// Load initial data
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const [models, convs, notesList] = await Promise.all([
|
||||
getModels(),
|
||||
getConversations(),
|
||||
getNotes(),
|
||||
]);
|
||||
if (!mounted) return;
|
||||
setModelos(models);
|
||||
setConversaciones(convs);
|
||||
setNotas(notesList);
|
||||
const defaultModel = models.find((m) => m.is_default_main) || models[0] || null;
|
||||
setModeloSeleccionado(defaultModel);
|
||||
// Load PDFs and progress
|
||||
pdfsHook.refresh();
|
||||
progressHook.refresh();
|
||||
} catch (err) {
|
||||
console.error('[App] load error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
// Load messages when active conversation changes
|
||||
useEffect(() => {
|
||||
if (!conversationActiva) return;
|
||||
chatHook.setActiveId(conversationActiva.id);
|
||||
}, [conversationActiva?.id]);
|
||||
|
||||
const handleSelectConversation = useCallback((conv) => {
|
||||
setConversationActiva(conv);
|
||||
setForkActivo(null);
|
||||
}, []);
|
||||
|
||||
const handleNewConversation = useCallback(async () => {
|
||||
try {
|
||||
const conv = await createConversation({
|
||||
title: 'Nueva conversación',
|
||||
model_id: modeloSeleccionado?.id ?? null,
|
||||
});
|
||||
setConversaciones((prev) => [conv, ...prev]);
|
||||
setConversationActiva(conv);
|
||||
setForkActivo(null);
|
||||
} catch (err) {
|
||||
console.error('[App] new conversation error:', err.message);
|
||||
}
|
||||
}, [modeloSeleccionado]);
|
||||
|
||||
const handleFork = useCallback(async (topic) => {
|
||||
if (!conversationActiva) return;
|
||||
try {
|
||||
const fork = await forkConversation(conversationActiva.id, {
|
||||
topic,
|
||||
model_id: modeloSeleccionado?.id ?? null,
|
||||
});
|
||||
setConversaciones((prev) => [fork, ...prev]);
|
||||
setForkActivo(fork);
|
||||
} catch (err) {
|
||||
console.error('[App] fork error:', err.message);
|
||||
}
|
||||
}, [conversationActiva, modeloSeleccionado]);
|
||||
|
||||
const handleMergeFork = useCallback(async () => {
|
||||
if (!forkActivo) return;
|
||||
try {
|
||||
await mergeConversation(forkActivo.id);
|
||||
setForkActivo(null);
|
||||
// Refresh conversations list (sidebar needs fresh data)
|
||||
const convs = await getConversations();
|
||||
setConversaciones(convs);
|
||||
// Refresh messages of parent conversation
|
||||
if (conversationActiva) {
|
||||
chatHook.setActiveId(conversationActiva.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[App] merge error:', err.message);
|
||||
}
|
||||
}, [forkActivo, conversationActiva, chatHook]);
|
||||
|
||||
const handleCloseFork = useCallback(() => {
|
||||
setForkActivo(null);
|
||||
}, []);
|
||||
|
||||
const mainConvs = conversaciones.filter((c) => c.type === 'main');
|
||||
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<>
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggle={() => setSidebarCollapsed((s) => !s)}
|
||||
pdfs={pdfsHook.pdfs}
|
||||
progress={progressHook.progress}
|
||||
conversations={mainConvs}
|
||||
activeConversation={conversationActiva}
|
||||
notes={notas}
|
||||
onSelectConversation={handleSelectConversation}
|
||||
onNewConversation={handleNewConversation}
|
||||
onUploadPdf={pdfsHook.uploadPdf}
|
||||
onReorderPdf={pdfsHook.reorderPdf}
|
||||
onDeletePdf={pdfsHook.deletePdf}
|
||||
onResetTopic={progressHook.resetTopic}
|
||||
onNavigateSettings={() => navigate('/settings')}
|
||||
/>
|
||||
<main className="app-main">
|
||||
<MainChat
|
||||
conversation={conversationActiva}
|
||||
modelo={modeloSeleccionado}
|
||||
modelos={modelos}
|
||||
onModelChange={setModeloSeleccionado}
|
||||
onFork={handleFork}
|
||||
messages={chatHook.messages}
|
||||
isStreaming={chatHook.isStreaming}
|
||||
MessageBubbleComponent={MessageBubble}
|
||||
/>
|
||||
<ChatInput
|
||||
onSend={(text, pdfIds, attachments) =>
|
||||
chatHook.sendMessage(text, pdfIds, attachments)
|
||||
}
|
||||
isStreaming={chatHook.isStreaming}
|
||||
availablePdfs={pdfsHook.pdfs}
|
||||
/>
|
||||
</main>
|
||||
<ForkPanel
|
||||
forkId={forkActivo?.id ?? null}
|
||||
forkTitle={forkActivo?.title || ''}
|
||||
onClose={handleCloseFork}
|
||||
onMerge={handleMergeFork}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user