Files
FusionAGI/frontend/src/App.tsx
defiQUG c052b07662
Some checks failed
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lint (push) Has been cancelled
Tests / docker (push) Has been cancelled
Initial commit: add .gitignore and README
2026-02-09 21:51:42 -08:00

156 lines
5.0 KiB
TypeScript

import { useState, useCallback } from 'react'
import { AvatarGrid } from './components/AvatarGrid'
import { ConsensusPanel } from './components/ConsensusPanel'
import { ChatMessage } from './components/ChatMessage'
import type { HeadContribution, FinalResponse } from './types'
import './App.css'
type ViewMode = 'normal' | 'explain' | 'developer'
function App() {
const [sessionId, setSessionId] = useState<string | null>(null)
const [prompt, setPrompt] = useState('')
const [messages, setMessages] = useState<{ role: 'user' | 'assistant'; content: string; data?: FinalResponse }[]>([])
const [loading, setLoading] = useState(false)
const [activeHeads, setActiveHeads] = useState<string[]>([])
const [speakingHead, setSpeakingHead] = useState<string | null>(null) // current head "speaking" in UI
const [headSummaries, setHeadSummaries] = useState<Record<string, string>>({})
const [viewMode, setViewMode] = useState<ViewMode>('normal')
const [lastResponse, setLastResponse] = useState<FinalResponse | null>(null)
const parseJson = useCallback(async (r: Response) => {
const text = await r.text()
if (!text.trim()) throw new Error('Empty response from API')
try {
return JSON.parse(text)
} catch {
throw new Error(`Invalid JSON from API: ${text.slice(0, 100)}`)
}
}, [])
const ensureSession = useCallback(async () => {
if (sessionId) return sessionId
const r = await fetch('/v1/sessions', { method: 'POST' })
const j = await parseJson(r)
if (!j.session_id) throw new Error('No session_id in response')
setSessionId(j.session_id)
return j.session_id
}, [sessionId, parseJson])
const handleSubmit = useCallback(async () => {
if (!prompt.trim()) return
const sid = await ensureSession()
if (!sid) return
setMessages((m) => [...m, { role: 'user', content: prompt }])
setPrompt('')
setLoading(true)
setSpeakingHead(null)
setActiveHeads(['logic', 'research', 'strategy', 'security', 'safety'])
try {
const r = await fetch(`/v1/sessions/${sid}/prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
})
const data = await parseJson(r)
if (!r.ok) throw new Error(data.detail || 'Request failed')
setLastResponse(data)
if (data.response_mode === 'show_dissent' || data.response_mode === 'explain') {
setViewMode('explain')
}
const contribs = data.head_contributions || []
setHeadSummaries(
Object.fromEntries(contribs.map((c: { head_id: string; summary: string }) => [c.head_id, c.summary]))
)
setSpeakingHead(contribs[0]?.head_id ?? null)
setMessages((m) => [
...m,
{
role: 'assistant',
content: data.final_answer,
data,
},
])
} catch (e) {
setMessages((m) => [
...m,
{ role: 'assistant', content: `Error: ${(e as Error).message}`, data: undefined },
])
} finally {
setLoading(false)
setActiveHeads([])
}
}, [prompt, ensureSession, parseJson])
const HEAD_IDS = [
'logic', 'research', 'systems', 'strategy', 'product',
'security', 'safety', 'reliability', 'cost', 'data', 'devex', 'witness',
]
return (
<div className="app">
<header className="header">
<h1>FusionAGI Dvādaśa</h1>
<div className="mode-toggle">
{(['normal', 'explain', 'developer'] as const).map((m) => (
<button
key={m}
className={viewMode === m ? 'active' : ''}
onClick={() => setViewMode(m)}
>
{m}
</button>
))}
</div>
</header>
<div className="main">
<div className="chat-area">
<AvatarGrid
headIds={HEAD_IDS}
activeHeads={activeHeads}
speakingHead={speakingHead}
headSummaries={headSummaries}
/>
<div className="messages">
{messages.map((msg, i) => (
<ChatMessage
key={i}
message={msg}
viewMode={viewMode}
/>
))}
{loading && <div className="loading">Heads running</div>}
</div>
<div className="input-row">
<input
id="prompt-input"
name="prompt"
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
placeholder="Ask FusionAGI… (/head strategy, /show dissent)"
autoComplete="off"
aria-label="Ask FusionAGI"
/>
<button onClick={handleSubmit} disabled={loading}>
Send
</button>
</div>
</div>
<ConsensusPanel
response={lastResponse}
viewMode={viewMode}
expanded={viewMode !== 'normal'}
/>
</div>
</div>
)
}
export default App