Initial commit: StudyOS platform

This commit is contained in:
renato97
2026-06-08 16:53:18 -03:00
commit b7d1e7319f
39 changed files with 9815 additions and 0 deletions

186
client/src/App.jsx Normal file
View 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>
);
}