import AppShell from './AppShell' import { useState, useEffect, useCallback, useRef } from 'react' import { api, getToken, saveToken } from './utils/api' import Sidebar from './components/Sidebar' import TopBar from './components/TopBar' import ChatArea from './components/ChatArea' import Composer from './components/Composer' import AuthModal from './components/AuthModal' import ModelPicker from './components/ModelPicker' import FeedbackModal from './components/FeedbackModal' import SettingsModal from './components/SettingsModal' function newUid() { return crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) }) } function LegacyApp() { /* ── Auth ─────────────────────────────────── */ const [token, setToken] = useState(() => getToken()) const [user, setUser] = useState(null) const [authMode, setAuthMode] = useState(null) // 'login' | 'register' | null /* ── Sessions ─────────────────────────────── */ const [sessions, setSessions] = useState([]) const [currentSessionId, setCurrentSessionId] = useState(null) const [sessionTitle, setSessionTitle] = useState('New Chat') /* ── Messages ─────────────────────────────── */ const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(false) /* ── Model registry ───────────────────────── */ const [registry, setRegistry] = useState({}) const [provider, setProvider] = useState('groq') const [model, setModel] = useState('llama-3.3-70b-versatile') const [agentMode, setAgentMode] = useState(false) /* ── Attachments ──────────────────────────── */ const [attachments, setAttachments] = useState([]) /* ── UI state ─────────────────────────────── */ const [sidebarOpen, setSidebarOpen] = useState(true) const [modal, setModal] = useState(null) // 'model'|'feedback'|'settings' const [theme, setTheme] = useState(() => localStorage.getItem('owngpt_v2_theme') || 'dark') const [searchQ, setSearchQ] = useState('') const chatRef = useRef(null) /* ── Apply theme ──────────────────────────── */ useEffect(() => { document.documentElement.setAttribute('data-theme', theme) localStorage.setItem('owngpt_v2_theme', theme) }, [theme]) /* ── Boot: capture URL token → load me + models ── */ useEffect(() => { const params = new URLSearchParams(window.location.search) const urlToken = params.get('access_token') if (urlToken) { saveToken(urlToken); setToken(urlToken) window.history.replaceState({}, '', window.location.pathname) } // load model registry (always) api.get('/models').then(d => { if (d?.providers) setRegistry(d.providers) }).catch(() => {}) }, []) /* ── Auth check ───────────────────────────── */ useEffect(() => { if (!token) { setUser(null); return } api.get('/auth/me').then(me => { setUser(me) loadSessions('') }).catch(() => { saveToken(null); setToken(null); setUser(null) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]) /* ── Session search ───────────────────────── */ useEffect(() => { if (!user) return const t = setTimeout(() => loadSessions(searchQ), 300) return () => clearTimeout(t) // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQ, user]) /* ── Helpers ──────────────────────────────── */ const scrollBottom = useCallback(() => { setTimeout(() => { chatRef.current?.scrollTo({ top: 9999999, behavior: 'smooth' }) }, 30) }, []) const loadSessions = useCallback(async (q) => { const url = q ? `/search?query=${encodeURIComponent(q)}` : '/search' try { const d = await api.get(url); setSessions(d.sessions || []) } catch {} }, []) /* ── Auth handlers ────────────────────────── */ const handleLogin = useCallback(async (username, password) => { const d = await api.post('/auth/login', { username, password }) saveToken(d.access_token); setToken(d.access_token) setAuthMode(null) }, []) const handleRegister = useCallback(async (username, password) => { await api.post('/auth/register', { username, password }) await handleLogin(username, password) }, [handleLogin]) const handleLogout = useCallback(() => { saveToken(null); setToken(null); setUser(null) setSessions([]); setMessages([]); setCurrentSessionId(null); setSessionTitle('New Chat') }, []) /* ── Session handlers ─────────────────────── */ const handleNewSession = useCallback(() => { setCurrentSessionId(null); setMessages([]); setSessionTitle('New Chat'); setAttachments([]) setSearchQ('') }, []) const handleSelectSession = useCallback(async (s) => { setCurrentSessionId(s.session_id); setSessionTitle(s.title || 'New Chat') setSessions(prev => prev.map(x => ({ ...x, _active: x.session_id === s.session_id }))) setMessages([]) try { const d = await api.get(`/history/${encodeURIComponent(s.session_id)}`) setMessages(d.messages || []) } catch {} scrollBottom() }, [scrollBottom]) const handleDeleteSession = useCallback(async (sessionId) => { if (!window.confirm('Delete this chat? This cannot be undone.')) return try { await api.delete(`/session/${encodeURIComponent(sessionId)}`) } catch {} if (currentSessionId === sessionId) handleNewSession() setSessions(prev => prev.filter(s => s.session_id !== sessionId)) }, [currentSessionId, handleNewSession]) const handleRenameSession = useCallback(async (newTitle) => { if (!currentSessionId) return try { await api.patch(`/session/${encodeURIComponent(currentSessionId)}/rename`, { title: newTitle }) } catch {} setSessionTitle(newTitle) setSessions(prev => prev.map(s => s.session_id === currentSessionId ? { ...s, title: newTitle } : s)) }, [currentSessionId]) /* ── File upload ──────────────────────────── */ const handleUpload = useCallback(async (files) => { for (const file of Array.from(files)) { const form = new FormData(); form.append('file', file) try { const d = await api.postForm('/upload', form) setAttachments(prev => [...prev, { filename: d.filename, kind: d.kind, extracted_text: d.extracted_text }]) } catch (e) { alert('Upload failed: ' + e.message) } } }, []) /* ── Send message ─────────────────────────── */ const handleSend = useCallback(async (text) => { if (!user) { setAuthMode('login'); return } if (!text.trim() && attachments.length === 0) return const sessionId = currentSessionId || newUid() if (!currentSessionId) { setCurrentSessionId(sessionId) setSessionTitle(text.slice(0, 48) || 'New Chat') } const userMsg = { role: 'user', content: text, session_id: sessionId, attachments: [...attachments], _id: newUid(), } setMessages(prev => [...prev, userMsg]) setAttachments([]) setLoading(true) scrollBottom() try { const respId = newUid(); setMessages(prev => [...prev, { role: 'assistant', content: '', model_used: model, provider, session_id: sessionId, _id: respId, }]); const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(getToken() ? { 'Authorization': `Bearer ${getToken()}` } : {}) }, body: JSON.stringify({ message: text, session_id: sessionId, provider, model, agent_mode: agentMode, attachments: attachments.map(a => ({ filename: a.filename, kind: a.kind, extracted_text: a.extracted_text })), }) }); if (!response.ok) throw new Error(`Server returned ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let done = false; let fullText = ""; let buffer = ""; while (!done) { const { value, done: doneReading } = await reader.read(); done = doneReading; if (value) { buffer += decoder.decode(value, { stream: true }); let boundaries = buffer.split(/\r?\n\r?\n/); buffer = boundaries.pop() || ""; // retain incomplete event in buffer for (let block of boundaries) { const lines = block.split(/\r?\n/); let dataStr = null; for (let line of lines) { if (line.startsWith('data: ')) { dataStr = line.slice(6); } } if (!dataStr) continue; try { const data = JSON.parse(dataStr); if (data.text) { fullText += data.text; // incrementally update state so UI renders instantly setMessages(prev => prev.map(m => m._id === respId ? { ...m, content: fullText } : m)); } else if (data.model_used) { // Done event metadata payload setMessages(prev => prev.map(m => m._id === respId ? { ...m, model_used: data.model_used, provider: data.provider, content: data.response ? data.response : m.content } : m)); } else if (data.detail) { // Error event fullText += `\n\n⚠️ Error: ${data.detail}`; setMessages(prev => prev.map(m => m._id === respId ? { ...m, content: fullText } : m)); } } catch (e) { console.warn("Failed to parse SSE data block", e, dataStr); } } } } loadSessions(searchQ) } catch (e) { setMessages(prev => [...prev, { role: 'assistant', content: `⚠️ Error: ${e.message}`, _id: newUid() }]) } finally { setLoading(false); scrollBottom() } }, [user, currentSessionId, attachments, provider, model, agentMode, scrollBottom, loadSessions, searchQ]) /* ── Feedback ─────────────────────────────── */ const handleFeedback = useCallback(async (data) => { await api.post('/feedback', data) }, []) /* ── Model select ─────────────────────────── */ const handleModelSelect = useCallback((prov, mod) => { setProvider(prov); setModel(mod) setModal(null) }, []) const requireAuth = useCallback((action) => { if (!user) { setAuthMode('login'); return false } action?.() return true }, [user]) return (
requireAuth(() => setModal('feedback'))} onSettings={() => setModal('settings')} />
setSidebarOpen(o => !o)} sessionTitle={sessionTitle} user={user} onRename={handleRenameSession} onLogin={() => setAuthMode('login')} onLogout={handleLogout} onFeedback={() => requireAuth(() => setModal('feedback'))} onSettings={() => setModal('settings')} theme={theme} onTheme={setTheme} currentSessionId={currentSessionId} /> { requireAuth(() => handleSend(t)) }} /> setAttachments(prev => prev.filter((_, idx) => idx !== i))} provider={provider} model={model} registry={registry} agentMode={agentMode} onToggleAgent={() => setAgentMode(a => !a)} onOpenModelPicker={() => setModal('model')} disabled={!user} onClickDisabled={() => setAuthMode('login')} />
{/* ── Modals ── */} {authMode && ( setAuthMode(null)} /> )} {modal === 'model' && ( setModal(null)} /> )} {modal === 'feedback' && ( setModal(null)} /> )} {modal === 'settings' && ( { if (window.confirm('Clear the current chat from this screen? Saved history stays in the sidebar.')) { setMessages([]); setCurrentSessionId(null); setSessionTitle('New Chat'); setAttachments([]) } }} onClose={() => setModal(null)} /> )}
) } export default AppShell