142 lines
5.6 KiB
TypeScript
142 lines
5.6 KiB
TypeScript
import { useEffect, useState, useCallback } from 'react'
|
|
import { api } from './lib/api'
|
|
import type { Plan, Agent, QueueItem, PendingQuestion } from './lib/api'
|
|
import { useWebSocket } from './hooks/useWebSocket'
|
|
import { AgentStatus } from './components/AgentStatus'
|
|
import { PlanBoard } from './components/PlanBoard'
|
|
import { DirectorQueue } from './components/DirectorQueue'
|
|
import { PendingQuestions } from './components/PendingQuestions'
|
|
import { PlanDetail } from './components/PlanDetail'
|
|
import { NewPlanModal } from './components/NewPlanModal'
|
|
import { ClaudeModeToggle } from './components/ClaudeModeToggle'
|
|
|
|
type RightTab = 'queue' | 'questions'
|
|
|
|
function App() {
|
|
const [plans, setPlans] = useState<Plan[]>([])
|
|
const [agents, setAgents] = useState<Agent[]>([])
|
|
const [queue, setQueue] = useState<QueueItem[]>([])
|
|
const [questions, setQuestions] = useState<PendingQuestion[]>([])
|
|
const [rightTab, setRightTab] = useState<RightTab>('queue')
|
|
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null)
|
|
const [reviewItem, setReviewItem] = useState<QueueItem | null>(null)
|
|
const [showNewPlan, setShowNewPlan] = useState(false)
|
|
const [connected, setConnected] = useState(false)
|
|
|
|
const loadData = useCallback(() => {
|
|
api.plans.list().then(setPlans).catch(() => {})
|
|
api.agents.list().then(setAgents).catch(() => {})
|
|
api.queue.list().then(setQueue).catch(() => {})
|
|
api.questions.list().then(setQuestions).catch(() => {})
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [loadData])
|
|
|
|
useWebSocket(useCallback((event: string, _data: unknown) => {
|
|
if (event.startsWith('plan:')) loadData()
|
|
if (event.startsWith('task:')) loadData()
|
|
if (event.startsWith('agent:')) api.agents.list().then(setAgents)
|
|
if (event.startsWith('queue:')) api.queue.list().then(setQueue)
|
|
if (event.startsWith('question:')) api.questions.list().then(setQuestions)
|
|
if (event.startsWith('message:')) loadData()
|
|
setConnected(true)
|
|
}, [loadData]))
|
|
|
|
const handleReview = useCallback(async (item: QueueItem) => {
|
|
if (!item.plan_id) return
|
|
try {
|
|
const plan = await api.plans.get(item.plan_id)
|
|
setReviewItem(item)
|
|
setSelectedPlan(plan)
|
|
} catch (err) {
|
|
console.error('failed to load plan for review', err)
|
|
}
|
|
}, [])
|
|
|
|
const closeDetail = useCallback(() => {
|
|
setSelectedPlan(null)
|
|
setReviewItem(null)
|
|
}, [])
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
|
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-3">
|
|
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
|
<h1 className="font-display text-xl font-medium">Director</h1>
|
|
<div className="flex items-center gap-3">
|
|
<ClaudeModeToggle />
|
|
<button
|
|
onClick={() => setShowNewPlan(true)}
|
|
className="text-sm px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-700 text-white cursor-pointer"
|
|
>
|
|
+ New Plan
|
|
</button>
|
|
<span className={`w-2 h-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
<span className="text-xs text-gray-500">{connected ? 'Connected' : 'Disconnected'}</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto p-6 space-y-6">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-1">
|
|
<AgentStatus agents={agents} />
|
|
</div>
|
|
<div className="lg:col-span-2">
|
|
{/* Tab toggle for queue vs questions */}
|
|
<div className="flex items-center gap-1 mb-3">
|
|
<button
|
|
onClick={() => setRightTab('queue')}
|
|
className={`font-mono text-[10px] uppercase tracking-wider px-3 py-1.5 rounded-md transition-colors cursor-pointer ${
|
|
rightTab === 'queue'
|
|
? 'bg-blue-600 text-white'
|
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800'
|
|
}`}
|
|
>
|
|
queue
|
|
{queue.length > 0 && (
|
|
<span className="ml-1.5 tabular">{queue.length}</span>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => setRightTab('questions')}
|
|
className={`font-mono text-[10px] uppercase tracking-wider px-3 py-1.5 rounded-md transition-colors cursor-pointer ${
|
|
rightTab === 'questions'
|
|
? 'bg-blue-600 text-white'
|
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800'
|
|
} ${questions.length > 0 && rightTab !== 'questions' ? 'ring-1 ring-amber-400' : ''}`}
|
|
>
|
|
questions
|
|
{questions.length > 0 && (
|
|
<span className="ml-1.5 tabular">{questions.length}</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{rightTab === 'queue' && <DirectorQueue items={queue} onReview={handleReview} />}
|
|
{rightTab === 'questions' && <PendingQuestions questions={questions} onChange={loadData} />}
|
|
</div>
|
|
</div>
|
|
|
|
<PlanBoard plans={plans} onSelect={setSelectedPlan} />
|
|
</main>
|
|
|
|
{selectedPlan && (
|
|
<PlanDetail
|
|
plan={selectedPlan}
|
|
onClose={closeDetail}
|
|
reviewItem={reviewItem ?? undefined}
|
|
onReviewActioned={loadData}
|
|
/>
|
|
)}
|
|
{showNewPlan && (
|
|
<NewPlanModal onClose={() => setShowNewPlan(false)} onCreated={loadData} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default App
|