Add ATC Command Center v1 with light UI theme.
Agent hub dashboard, FastAPI backend, and Docker stack for VM 304 MCP. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
type Props = {
|
||||
color: string
|
||||
state: 'idle' | 'walk' | 'fetch' | 'return'
|
||||
label: string
|
||||
}
|
||||
|
||||
export function AgentSprite({ color, state, label }: Props) {
|
||||
const bob = state === 'idle' ? { y: [0, -3, 0] } : state === 'walk' || state === 'return' ? { y: [0, -6, 0] } : { y: 0 }
|
||||
const scale = state === 'fetch' ? 0.92 : 1
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex flex-col items-center"
|
||||
animate={{ ...bob, scale }}
|
||||
transition={{ repeat: Infinity, duration: state === 'walk' || state === 'return' ? 0.35 : 2 }}
|
||||
>
|
||||
<svg width="56" height="72" viewBox="0 0 56 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="28" cy="68" rx="16" ry="4" fill={color} opacity="0.2" />
|
||||
<rect x="18" y="36" width="20" height="26" rx="4" fill="#f8fafc" stroke={color} strokeWidth="1.5" />
|
||||
<rect x="10" y="38" width="8" height="18" rx="3" fill="#f1f5f9" stroke={color} strokeWidth="1" />
|
||||
<rect x="38" y="38" width="8" height="18" rx="3" fill="#f1f5f9" stroke={color} strokeWidth="1" />
|
||||
<rect x="20" y="58" width="7" height="12" rx="2" fill="#e2e8f0" stroke={color} strokeWidth="1" />
|
||||
<rect x="29" y="58" width="7" height="12" rx="2" fill="#e2e8f0" stroke={color} strokeWidth="1" />
|
||||
<circle cx="28" cy="22" r="12" fill="#f8fafc" stroke={color} strokeWidth="1.5" />
|
||||
<path d="M14 20 Q28 8 42 20 L40 24 Q28 14 16 24 Z" fill={color} opacity="0.9" />
|
||||
<rect x="14" y="20" width="28" height="4" rx="1" fill={color} />
|
||||
<path d="M16 24 Q16 34 20 36" stroke={color} strokeWidth="2" fill="none" />
|
||||
<path d="M40 24 Q40 34 36 36" stroke={color} strokeWidth="2" fill="none" />
|
||||
<rect x="12" y="22" width="6" height="10" rx="2" fill={color} opacity="0.7" />
|
||||
<rect x="38" y="22" width="6" height="10" rx="2" fill={color} opacity="0.7" />
|
||||
<path d="M36 36 L42 44" stroke={color} strokeWidth="1.5" />
|
||||
<circle cx="43" cy="45" r="2" fill={color} />
|
||||
<rect x="20" y="20" width="16" height="5" rx="2" fill={color} opacity="0.3" />
|
||||
{state === 'fetch' && (
|
||||
<motion.circle
|
||||
cx="46" cy="30" r="4"
|
||||
fill={color}
|
||||
animate={{ opacity: [0.4, 1, 0.4] }}
|
||||
transition={{ repeat: Infinity, duration: 0.6 }}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<span className="text-[10px] font-mono font-semibold mt-1 truncate max-w-[72px]" style={{ color }}>{label}</span>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { AgentSprite } from './AgentSprite'
|
||||
import type { Agent, AgentAnim, Zone } from '../types'
|
||||
|
||||
const ZONE_X: Record<string, number> = {
|
||||
docker: 8,
|
||||
db: 28,
|
||||
lakehouse: 50,
|
||||
hadoop: 72,
|
||||
etl: 92,
|
||||
}
|
||||
|
||||
const DESK_X = 50
|
||||
|
||||
type Props = {
|
||||
agents: Agent[]
|
||||
zones: Zone[]
|
||||
animations: Record<string, AgentAnim>
|
||||
}
|
||||
|
||||
export function OpsFloor({ agents, zones, animations }: Props) {
|
||||
return (
|
||||
<div className="glass-strong rounded-2xl p-5 relative overflow-hidden min-h-[300px]">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="font-display text-lg font-bold text-neon-cyan neon-text-cyan tracking-wide">OPS FLOOR</h2>
|
||||
<span className="text-xs font-mono px-2.5 py-1 rounded-full bg-green-50 text-neon-green border border-neon-green/30 animate-pulse">● LIVE</span>
|
||||
</div>
|
||||
|
||||
<div className="relative h-28 mb-4">
|
||||
{zones.map((z) => (
|
||||
<div
|
||||
key={z.id}
|
||||
className="absolute top-0 -translate-x-1/2 text-center"
|
||||
style={{ left: `${z.x}%` }}
|
||||
>
|
||||
<div
|
||||
className="rounded-xl px-3 py-3 min-w-[92px] text-[9px] font-mono tracking-wider font-semibold bg-white/90"
|
||||
style={{ border: `2px solid ${z.color}`, boxShadow: `0 4px 16px ${z.color}22`, color: z.color }}
|
||||
>
|
||||
{z.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none" preserveAspectRatio="none">
|
||||
{zones.map((z) => (
|
||||
<line
|
||||
key={`path-${z.id}`}
|
||||
x1={`${DESK_X}%`} y1="85%" x2={`${z.x}%`} y2="35%"
|
||||
stroke={z.color} strokeWidth="2" strokeDasharray="6 4" opacity="0.45"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="relative h-28 rounded-xl bg-gradient-to-b from-slate-50 to-white border border-slate-100">
|
||||
{agents.map((agent, i) => {
|
||||
const anim = animations[agent.id] || { agentId: agent.id, state: 'idle' as const }
|
||||
const targetX = anim.state === 'idle' ? 12 + i * 17 : ZONE_X[anim.zone || agent.zone] ?? DESK_X
|
||||
const y = anim.state === 'fetch' ? 8 : anim.state === 'idle' ? 0 : 4
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
className="absolute bottom-2 -translate-x-1/2"
|
||||
animate={{ left: `${targetX}%`, y }}
|
||||
transition={{ type: 'spring', stiffness: 80, damping: 14 }}
|
||||
>
|
||||
<AgentSprite
|
||||
color={agent.color}
|
||||
state={anim.state}
|
||||
label={agent.name.split(' ')[0]}
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { FormEvent, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
onSubmit: (message: string) => void
|
||||
busy: boolean
|
||||
}
|
||||
|
||||
export function PromptBar({ onSubmit, busy }: Props) {
|
||||
const [text, setText] = useState('')
|
||||
|
||||
const handle = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!text.trim() || busy) return
|
||||
onSubmit(text.trim())
|
||||
setText('')
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handle} className="glass-strong rounded-2xl p-4 flex gap-3 items-center shadow-card">
|
||||
<span className="text-2xl" aria-hidden>💬</span>
|
||||
<input
|
||||
className="flex-1 bg-white border border-slate-200 rounded-xl px-4 py-2.5 outline-none font-mono text-sm text-ink placeholder:text-ink-faint focus:border-neon-cyan focus:ring-2 focus:ring-neon-cyan/20 transition"
|
||||
placeholder="Vraag je agents... bijv. Hoe staat Debezium er voor?"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busy || !text.trim()}
|
||||
className="px-5 py-2.5 rounded-xl font-display text-sm font-semibold text-white disabled:opacity-40 transition-all hover:brightness-110"
|
||||
style={{
|
||||
background: busy ? '#94a3b8' : 'linear-gradient(135deg, #0099cc, #8844cc)',
|
||||
boxShadow: busy ? 'none' : '0 4px 16px rgba(0, 153, 204, 0.35)',
|
||||
}}
|
||||
>
|
||||
{busy ? 'Bezig...' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user