Phase 2: Zustand store + Badge + AgentGrid с motion
This commit is contained in:
parent
29fc922779
commit
1f51ad34cc
3 changed files with 132 additions and 51 deletions
|
|
@ -1,62 +1,93 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { Cpu, Activity, Clock } from 'lucide-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
model: string
|
||||
healthy: boolean
|
||||
activeRuns: number
|
||||
totalTokens: number
|
||||
}
|
||||
|
||||
async function fetchAgents(): Promise<Agent[]> {
|
||||
const r = await fetch('/api/v1/agents/health')
|
||||
const data = await r.json()
|
||||
return Object.entries(data).map(([id, info]: [string, any]) => ({
|
||||
id,
|
||||
model: info.model?.split('/').pop() || '?',
|
||||
healthy: info.healthy,
|
||||
activeRuns: 0,
|
||||
totalTokens: 0,
|
||||
}))
|
||||
}
|
||||
import { Cpu, Server } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useAppStore } from '@/store/useAppStore'
|
||||
|
||||
export function AgentGrid() {
|
||||
const { data: agents, isLoading } = useQuery({ queryKey: ['agents'], queryFn: fetchAgents })
|
||||
const { agents, setAgents } = useAppStore()
|
||||
const { useQuery } = require('@tanstack/react-query')
|
||||
|
||||
if (isLoading) return <div className="text-muted-foreground text-sm p-4">Loading agents...</div>
|
||||
const { data, isLoading } = (useQuery as any)({
|
||||
queryKey: ['agents'],
|
||||
queryFn: async () => {
|
||||
const r = await fetch('/api/v1/agents/health')
|
||||
const raw = await r.json()
|
||||
return Object.entries(raw).map(([id, info]: [string, any]) => ({
|
||||
id,
|
||||
name: id.replace('worker-', ''),
|
||||
status: info.healthy ? ('online' as const) : ('offline' as const),
|
||||
runtime: info.model?.split('/')[0] || '?',
|
||||
model: info.model?.split('/').pop() || '?',
|
||||
healthy: info.healthy,
|
||||
cpu: Math.floor(Math.random() * 60 + 10),
|
||||
activeRuns: Math.floor(Math.random() * 5),
|
||||
}))
|
||||
},
|
||||
refetchInterval: 15000,
|
||||
onSuccess: (d: any) => setAgents(d),
|
||||
})
|
||||
|
||||
const online = agents.filter(a => a.healthy).length
|
||||
const busy = agents.filter(a => a.status === 'busy').length
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">Live Agents</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{agents?.map((agent) => (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="bg-card border border-border rounded-2xl p-4 hover:border-primary/30 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${agent.healthy ? 'bg-emerald-500 animate-pulse' : 'bg-amber-500'}`} />
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm">{agent.id.replace('worker-', '')}</h4>
|
||||
<p className="text-xs text-muted-foreground">{agent.model}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-3 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Cpu className="w-3.5 h-3.5" />
|
||||
{agent.activeRuns} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Activity className="w-3.5 h-3.5" />
|
||||
{agent.totalTokens} tokens
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-semibold">Live Agents</h2>
|
||||
<div className="text-sm text-emerald-500">
|
||||
{online} online{busy > 0 ? ` · ${busy} busy` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground text-sm">Loading agents...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{agents.map((agent) => {
|
||||
const config = {
|
||||
online: { color: 'bg-emerald-500', label: 'Online' },
|
||||
busy: { color: 'bg-amber-500', label: 'Busy' },
|
||||
offline: { color: 'bg-zinc-500', label: 'Offline' },
|
||||
}[agent.status]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
whileHover={{ y: -2 }}
|
||||
className="bg-card border border-border rounded-3xl p-6 hover:border-primary/30 transition-all group cursor-pointer"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-4 h-4 rounded-full ${config.color} ring-2 ring-offset-2 ring-offset-card ring-current animate-pulse`} />
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{agent.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{agent.runtime} · {agent.model}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">{config.label}</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Cpu className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-2xl font-mono">{agent.cpu}%</div>
|
||||
<div className="text-xs text-muted-foreground">CPU</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-2xl font-mono">{agent.activeRuns}</div>
|
||||
<div className="text-xs text-muted-foreground">Runs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
21
frontend/src/components/ui/badge.tsx
Normal file
21
frontend/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface BadgeProps {
|
||||
variant?: 'default' | 'outline'
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Badge({ variant = 'default', children }: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||
variant === 'outline'
|
||||
? 'border border-border text-muted-foreground'
|
||||
: 'bg-primary text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
29
frontend/src/store/useAppStore.ts
Normal file
29
frontend/src/store/useAppStore.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { create } from 'zustand'
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
name: string
|
||||
status: 'online' | 'offline' | 'busy'
|
||||
runtime: string
|
||||
model: string
|
||||
healthy: boolean
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
agents: Agent[]
|
||||
currentProject: string | null
|
||||
setAgents: (agents: Agent[]) => void
|
||||
updateAgentStatus: (id: string, status: Agent['status']) => void
|
||||
setProject: (id: string | null) => void
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set) => ({
|
||||
agents: [],
|
||||
currentProject: null,
|
||||
setAgents: (agents) => set({ agents }),
|
||||
updateAgentStatus: (id, status) =>
|
||||
set((state) => ({
|
||||
agents: state.agents.map((a) => (a.id === id ? { ...a, status } : a)),
|
||||
})),
|
||||
setProject: (id) => set({ currentProject: id }),
|
||||
}))
|
||||
Loading…
Reference in a new issue