OwnGPT.v2 / client /src /App.jsx
parthib07's picture
Upload 199 files
212c959 verified
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 (
<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