| 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() { |
| |
| const [token, setToken] = useState(() => getToken()) |
| const [user, setUser] = useState(null) |
| const [authMode, setAuthMode] = useState(null) |
|
|
| |
| const [sessions, setSessions] = useState([]) |
| const [currentSessionId, setCurrentSessionId] = useState(null) |
| const [sessionTitle, setSessionTitle] = useState('New Chat') |
|
|
| |
| const [messages, setMessages] = useState([]) |
| const [loading, setLoading] = useState(false) |
|
|
| |
| const [registry, setRegistry] = useState({}) |
| const [provider, setProvider] = useState('groq') |
| const [model, setModel] = useState('llama-3.3-70b-versatile') |
| const [agentMode, setAgentMode] = useState(false) |
|
|
| |
| const [attachments, setAttachments] = useState([]) |
|
|
| |
| const [sidebarOpen, setSidebarOpen] = useState(true) |
| const [modal, setModal] = useState(null) |
| const [theme, setTheme] = useState(() => localStorage.getItem('owngpt_v2_theme') || 'dark') |
| const [searchQ, setSearchQ] = useState('') |
|
|
| const chatRef = useRef(null) |
|
|
| |
| useEffect(() => { |
| document.documentElement.setAttribute('data-theme', theme) |
| localStorage.setItem('owngpt_v2_theme', theme) |
| }, [theme]) |
|
|
| |
| 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) |
| } |
| |
| api.get('/models').then(d => { |
| if (d?.providers) setRegistry(d.providers) |
| }).catch(() => {}) |
| }, []) |
|
|
| |
| useEffect(() => { |
| if (!token) { setUser(null); return } |
| api.get('/auth/me').then(me => { |
| setUser(me) |
| loadSessions('') |
| }).catch(() => { |
| saveToken(null); setToken(null); setUser(null) |
| }) |
| |
| }, [token]) |
|
|
| |
| useEffect(() => { |
| if (!user) return |
| const t = setTimeout(() => loadSessions(searchQ), 300) |
| return () => clearTimeout(t) |
| |
| }, [searchQ, user]) |
|
|
| |
| 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 {} |
| }, []) |
|
|
| |
| 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') |
| }, []) |
|
|
| |
| 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]) |
|
|
| |
| 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) } |
| } |
| }, []) |
|
|
| |
| 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() || ""; |
| |
| 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; |
| |
| setMessages(prev => prev.map(m => m._id === respId ? { ...m, content: fullText } : m)); |
| } else if (data.model_used) { |
| |
| 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) { |
| |
| 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]) |
|
|
| |
| const handleFeedback = useCallback(async (data) => { |
| await api.post('/feedback', data) |
| }, []) |
|
|
| |
| 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 ( |
| <div className="app-shell"> |
| <div className="bg-mesh"></div> |
| <Sidebar |
| open={sidebarOpen} |
| sessions={sessions} |
| currentSessionId={currentSessionId} |
| searchQ={searchQ} |
| onSearchChange={setSearchQ} |
| onNewSession={handleNewSession} |
| onSelectSession={handleSelectSession} |
| onDeleteSession={handleDeleteSession} |
| onFeedback={() => requireAuth(() => setModal('feedback'))} |
| onSettings={() => setModal('settings')} |
| /> |
| |
| <div className="main"> |
| <TopBar |
| onToggleSidebar={() => 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} |
| /> |
| |
| <ChatArea |
| ref={chatRef} |
| messages={messages} |
| loading={loading} |
| user={user} |
| onSuggestion={t => { requireAuth(() => handleSend(t)) }} |
| /> |
| |
| <Composer |
| onSend={handleSend} |
| onUpload={handleUpload} |
| attachments={attachments} |
| onRemoveAttachment={i => 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')} |
| /> |
| </div> |
| |
| {/* ββ Modals ββ */} |
| {authMode && ( |
| <AuthModal |
| mode={authMode} |
| onModeChange={setAuthMode} |
| onLogin={handleLogin} |
| onRegister={handleRegister} |
| onClose={() => setAuthMode(null)} |
| /> |
| )} |
| {modal === 'model' && ( |
| <ModelPicker |
| registry={registry} |
| currentProvider={provider} |
| currentModel={model} |
| onSelect={handleModelSelect} |
| onClose={() => setModal(null)} |
| /> |
| )} |
| {modal === 'feedback' && ( |
| <FeedbackModal |
| onSubmit={handleFeedback} |
| onClose={() => setModal(null)} |
| /> |
| )} |
| {modal === 'settings' && ( |
| <SettingsModal |
| theme={theme} |
| onTheme={setTheme} |
| onClearChats={() => { |
| 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)} |
| /> |
| )} |
| </div> |
| ) |
| } |
|
|
| export default AppShell |
|
|