chore: remove stub React dashboard (dashboard-web/)
The React UI was added as a feature-intent mockup and has served that purpose — all planned features are captured in QUALITY_ANALYSIS.md and mirrored in the Python backend (marketplace route, swarm coordinator, data models). The stub was not buildable (missing package.json / shadcn) and was creating confusion about which UI is canonical. The HTMX dashboard (src/dashboard/) is the authoritative, working UI. https://claude.ai/code/session_0183Nzcy7TMqjrAopnTtygds
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||
<title>Timmy Time — Mission Control</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script
|
||||
defer
|
||||
src="%VITE_ANALYTICS_ENDPOINT%/umami"
|
||||
data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import NotFound from "@/pages/NotFound";
|
||||
import { Route, Switch } from "wouter";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/" component={Dashboard} />
|
||||
<Route path="/404" component={NotFound} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ThemeProvider defaultTheme="dark">
|
||||
<TooltipProvider>
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: 'oklch(0.12 0.01 260)',
|
||||
border: '1px solid oklch(0.22 0.01 260)',
|
||||
borderTop: '2px solid oklch(0.75 0.18 55)',
|
||||
color: 'oklch(0.9 0.005 90)',
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '12px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Router />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Chat interface
|
||||
* Terminal-style command input with >_ prompt cursor
|
||||
* Messages displayed with typewriter aesthetic, typing indicator
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Send } from "lucide-react";
|
||||
import { MOCK_CHAT, type ChatMessage } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ChatPanel() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>(MOCK_CHAT);
|
||||
const [input, setInput] = useState("");
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages, isTyping]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
|
||||
// Handle slash commands
|
||||
if (input.startsWith("/")) {
|
||||
const cmd = input.slice(1).trim().split(" ")[0];
|
||||
toast(`Command recognized: /${cmd}`, {
|
||||
description: "Slash commands will be processed when connected to the backend.",
|
||||
});
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
id: `c-${Date.now()}`,
|
||||
role: "user",
|
||||
content: input,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
setInput("");
|
||||
setIsTyping(true);
|
||||
|
||||
// Simulate response with typing delay
|
||||
setTimeout(() => {
|
||||
setIsTyping(false);
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: `c-${Date.now() + 1}`,
|
||||
role: "assistant",
|
||||
content: "I hear you, boss. Running locally on Ollama — no cloud, no telemetry. Your sovereignty is intact.\n\nThis is a demo interface. Connect me to your local Ollama instance to get real responses.\n\nSats are sovereignty, boss.",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
setMessages(prev => [...prev, assistantMsg]);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Messages area */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-auto p-4 space-y-4">
|
||||
{messages.map((msg, i) => (
|
||||
<motion.div
|
||||
key={msg.id}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: i < 4 ? i * 0.05 : 0 }}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div className={`max-w-[80%] ${msg.role === 'system' ? 'w-full' : ''}`}>
|
||||
{/* Role label */}
|
||||
<div className={`text-[10px] uppercase tracking-[0.15em] mb-1 ${
|
||||
msg.role === 'user' ? 'text-right text-muted-foreground' :
|
||||
msg.role === 'system' ? 'text-cyber-cyan' :
|
||||
'text-btc-orange'
|
||||
}`}>
|
||||
{msg.role === 'assistant' ? '// TIMMY' :
|
||||
msg.role === 'system' ? '// SYSTEM' :
|
||||
'// YOU'}
|
||||
</div>
|
||||
|
||||
{/* Message bubble */}
|
||||
<div className={`text-[13px] leading-relaxed ${
|
||||
msg.role === 'user'
|
||||
? 'bg-accent border border-border px-4 py-3 text-foreground'
|
||||
: msg.role === 'system'
|
||||
? 'bg-cyber-cyan/5 border border-cyber-cyan/20 px-4 py-3 text-cyber-cyan/80 font-mono text-[11px]'
|
||||
: 'bg-card border border-border px-4 py-3 text-foreground'
|
||||
}`}
|
||||
style={msg.role === 'assistant' ? { borderTop: '2px solid oklch(0.75 0.18 55)' } : undefined}
|
||||
>
|
||||
{msg.content.split('\n').map((line, j) => (
|
||||
<p key={j} className={j > 0 ? 'mt-2' : ''}>
|
||||
{line || '\u00A0'}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Timestamp */}
|
||||
<div className={`text-[9px] text-muted-foreground mt-1 ${
|
||||
msg.role === 'user' ? 'text-right' : ''
|
||||
}`}>
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* Typing indicator */}
|
||||
{isTyping && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex justify-start"
|
||||
>
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-[0.15em] mb-1 text-btc-orange">
|
||||
// TIMMY
|
||||
</div>
|
||||
<div className="bg-card border border-border px-4 py-3 text-foreground"
|
||||
style={{ borderTop: '2px solid oklch(0.75 0.18 55)' }}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-btc-orange animate-pulse" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-btc-orange animate-pulse" style={{ animationDelay: '0.2s' }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-btc-orange animate-pulse" style={{ animationDelay: '0.4s' }} />
|
||||
<span className="text-[11px] text-muted-foreground ml-2">thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border p-3">
|
||||
<div
|
||||
className="flex items-center gap-2 bg-input border border-border px-3 py-2.5 focus-within:border-btc-orange/50 transition-colors"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
<span className="terminal-prompt text-[13px] select-none">>_</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message or /command..."
|
||||
className="flex-1 bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground outline-none"
|
||||
style={{ fontSize: '16px' }}
|
||||
disabled={isTyping}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-btc-orange hover:text-btc-orange/80 hover:bg-btc-orange/10"
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || isTyping}
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1.5 text-[9px] text-muted-foreground">
|
||||
<span>ENTER to send</span>
|
||||
<span className="text-border">|</span>
|
||||
<span>/help for commands</span>
|
||||
<span className="text-border">|</span>
|
||||
<span className="text-electric-green/60">Local LLM — no cloud</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Right context panel
|
||||
* Shows details for selected agent or task, voice NLU, or roadmap
|
||||
*/
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
AGENT_CATALOG, MOCK_TASKS, VOICE_INTENTS, ROADMAP,
|
||||
type Agent, type Task
|
||||
} from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
User, Zap, Mic, Map, ChevronRight,
|
||||
CheckCircle, Clock, AlertCircle, Loader2, Gavel
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const HERO_IMG = "https://private-us-east-1.manuscdn.com/sessionFile/hmEvCGQLHKyGnx6qwMSEHn/sandbox/qiXHjJUmj8lqJymwhLI5B2-img-1_1771695716000_na1fn_aGVyby1iYW5uZXI.png?x-oss-process=image/resize,w_1920,h_1920/format,webp/quality,q_80&Expires=1798761600&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9wcml2YXRlLXVzLWVhc3QtMS5tYW51c2Nkbi5jb20vc2Vzc2lvbkZpbGUvaG1FdkNHUUxIS3lHbng2cXdNU0VIbi9zYW5kYm94L3FpWEhqSlVtajhscUp5bXdoTEk1QjItaW1nLTFfMTc3MTY5NTcxNjAwMF9uYTFmbl9hR1Z5YnkxaVlXNXVaWEkucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLHdfMTkyMCxoXzE5MjAvZm9ybWF0LHdlYnAvcXVhbGl0eSxxXzgwIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzk4NzYxNjAwfX19XX0_&Key-Pair-Id=K2HSFNDJXOU9YS&Signature=Yeq1vIhtaEw73bIbDJlFsrAQeoce-YaWvid7nAdYEKAA41Xxzh8iioV-HmHsbldg~z674zIlRc0KeBIdV2hH2O8yBRN6KjP-BMO9QHDbGeBbTw3Bd5uEbh7GmZUXb7klkd0yStYYQcIjwTPBcJ7dMkiQ4AV1k5u63gQDm1FS-hqRGqzcS97ZQc0eSd3Ij2CKLrF7OXc4Xu6wB8CxzLD87mTdnvOtLobjHgvFdl6KVkUTIHjh97fL8YRlN5My6N3BGW-E8l-ZNVnWT22qfiHcpVD4kk6S6yu~v7OpBY3-1if3am5B2prST3bHxGMKsQlTwttr~xEpX4ZYF1dAJy0n2Q__";
|
||||
|
||||
interface ContextPanelProps {
|
||||
selectedAgent: string | null;
|
||||
selectedTask: string | null;
|
||||
}
|
||||
|
||||
export default function ContextPanel({ selectedAgent, selectedTask }: ContextPanelProps) {
|
||||
const agent = selectedAgent ? AGENT_CATALOG.find(a => a.id === selectedAgent) : null;
|
||||
const task = selectedTask ? MOCK_TASKS.find(t => t.id === selectedTask) : null;
|
||||
|
||||
return (
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Agent detail */}
|
||||
{agent && <AgentDetail agent={agent} />}
|
||||
|
||||
{/* Task detail */}
|
||||
{task && <TaskDetail task={task} />}
|
||||
|
||||
{/* Voice NLU */}
|
||||
<VoiceNLUPanel />
|
||||
|
||||
{/* Roadmap */}
|
||||
<RoadmapPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentDetail({ agent }: { agent: Agent }) {
|
||||
return (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, x: 16 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="panel"
|
||||
>
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<User className="w-3 h-3" />
|
||||
// AGENT DETAIL
|
||||
</div>
|
||||
<div className="p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`status-dot ${
|
||||
agent.status === 'active' ? 'status-dot-active' : 'status-dot-planned'
|
||||
}`} />
|
||||
<span className="text-[14px] font-semibold text-foreground">{agent.name}</span>
|
||||
<Badge variant="outline" className="text-[9px] h-4 ml-auto">
|
||||
{agent.status.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] text-btc-orange font-medium">{agent.role}</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{agent.description}
|
||||
</p>
|
||||
|
||||
<Separator className="bg-border" />
|
||||
|
||||
<div className="space-y-2 text-[11px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Rate</span>
|
||||
{agent.rateSats === 0 ? (
|
||||
<span className="text-electric-green">FREE</span>
|
||||
) : (
|
||||
<span className="sat-amount flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
{agent.rateSats} sats/task
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Capabilities</span>
|
||||
<span className="text-foreground">{agent.capabilities.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.capabilities.map(cap => (
|
||||
<span key={cap} className="text-[9px] px-1.5 py-0.5 bg-accent text-accent-foreground border border-border">
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{agent.status === 'active' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full h-7 text-[10px] text-btc-orange hover:bg-btc-orange/10 border border-btc-orange/30"
|
||||
onClick={() => toast("Chat with " + agent.name, { description: "Switch to the Chat tab to interact." })}
|
||||
>
|
||||
OPEN CHAT
|
||||
<ChevronRight className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDetail({ task }: { task: Task }) {
|
||||
return (
|
||||
<motion.div
|
||||
key={task.id}
|
||||
initial={{ opacity: 0, x: 16 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="panel"
|
||||
>
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
// TASK DETAIL
|
||||
</div>
|
||||
<div className="p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-muted-foreground font-mono">{task.id}</span>
|
||||
<TaskStatusBadge status={task.status} />
|
||||
</div>
|
||||
|
||||
<p className="text-[12px] text-foreground leading-relaxed">
|
||||
{task.description}
|
||||
</p>
|
||||
|
||||
<Separator className="bg-border" />
|
||||
|
||||
<div className="space-y-2 text-[11px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Assigned</span>
|
||||
<span className={task.assignedAgent ? 'text-btc-orange' : 'text-muted-foreground'}>
|
||||
{task.assignedAgent || 'Unassigned'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Created</span>
|
||||
<span className="text-foreground">
|
||||
{new Date(task.createdAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{task.completedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Completed</span>
|
||||
<span className="text-electric-green">
|
||||
{new Date(task.completedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.result && (
|
||||
<>
|
||||
<Separator className="bg-border" />
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">Result</div>
|
||||
<p className="text-[11px] text-foreground bg-accent p-2 border border-border">
|
||||
{task.result}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskStatusBadge({ status }: { status: Task["status"] }) {
|
||||
const colors: Record<Task["status"], string> = {
|
||||
pending: "bg-muted text-muted-foreground",
|
||||
bidding: "bg-warning-amber/20 text-warning-amber border-warning-amber/30",
|
||||
assigned: "bg-cyber-cyan/20 text-cyber-cyan border-cyber-cyan/30",
|
||||
running: "bg-btc-orange/20 text-btc-orange border-btc-orange/30",
|
||||
completed: "bg-electric-green/20 text-electric-green border-electric-green/30",
|
||||
failed: "bg-danger-red/20 text-danger-red border-danger-red/30",
|
||||
};
|
||||
return (
|
||||
<span className={`text-[9px] uppercase tracking-wider px-1.5 py-0.5 border ${colors[status]}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function VoiceNLUPanel() {
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Mic className="w-3 h-3" />
|
||||
// VOICE NLU
|
||||
</div>
|
||||
<div className="p-3 space-y-2">
|
||||
<p className="text-[10px] text-muted-foreground mb-2">
|
||||
Supported voice intents — local regex-based NLU, no cloud.
|
||||
</p>
|
||||
{VOICE_INTENTS.map(intent => (
|
||||
<div key={intent.name} className="flex items-start gap-2 text-[11px]">
|
||||
<span className="text-btc-orange font-medium w-14 flex-shrink-0 uppercase text-[10px]">
|
||||
{intent.name}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-muted-foreground">{intent.description}</span>
|
||||
<div className="text-[9px] text-muted-foreground/60 font-mono mt-0.5 truncate">
|
||||
"{intent.example}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoadmapPanel() {
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Map className="w-3 h-3" />
|
||||
// ROADMAP
|
||||
</div>
|
||||
<div className="p-3 space-y-2">
|
||||
{ROADMAP.map((item, i) => (
|
||||
<div key={item.version} className="flex items-start gap-2 text-[11px]">
|
||||
<div className="flex flex-col items-center mt-0.5">
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
item.status === 'complete' ? 'bg-electric-green' :
|
||||
item.status === 'current' ? 'bg-btc-orange' : 'bg-muted-foreground'
|
||||
}`} />
|
||||
{i < ROADMAP.length - 1 && (
|
||||
<span className="w-px h-6 bg-border mt-1" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-foreground">{item.version}</span>
|
||||
<span className="text-btc-orange">{item.name}</span>
|
||||
{item.status === 'current' && (
|
||||
<span className="text-[8px] px-1 py-0.5 bg-btc-orange/20 text-btc-orange border border-btc-orange/30">
|
||||
CURRENT
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-muted-foreground text-[10px]">{item.milestone}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Agent Marketplace
|
||||
* Browse agents, see capabilities, hire with sats
|
||||
* Lightning payment visualization as hero
|
||||
*/
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AGENT_CATALOG, type Agent } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { Store, Zap, ChevronRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const LIGHTNING_IMG = "https://private-us-east-1.manuscdn.com/sessionFile/hmEvCGQLHKyGnx6qwMSEHn/sandbox/qiXHjJUmj8lqJymwhLI5B2-img-3_1771695706000_na1fn_bGlnaHRuaW5nLXBheW1lbnQ.png?x-oss-process=image/resize,w_1920,h_1920/format,webp/quality,q_80&Expires=1798761600&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9wcml2YXRlLXVzLWVhc3QtMS5tYW51c2Nkbi5jb20vc2Vzc2lvbkZpbGUvaG1FdkNHUUxIS3lHbng2cXdNU0VIbi9zYW5kYm94L3FpWEhqSlVtajhscUp5bXdoTEk1QjItaW1nLTNfMTc3MTY5NTcwNjAwMF9uYTFmbl9iR2xuYUhSdWFXNW5MWEJoZVcxbGJuUS5wbmc~eC1vc3MtcHJvY2Vzcz1pbWFnZS9yZXNpemUsd18xOTIwLGhfMTkyMC9mb3JtYXQsd2VicC9xdWFsaXR5LHFfODAiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3OTg3NjE2MDB9fX1dfQ__&Key-Pair-Id=K2HSFNDJXOU9YS&Signature=gWuDcQJeJeaEupkqbE5tOSIOgB6A2LjDuU7w6nK8RzOSmeWWy~4AJVsm68hi--j22DFlv7hDWhZnoQ9WdyU0oCn3tIUFPaaamtcUY-9qBE3yw9VjAnBRJjG3ppnfVSFY-KaVvuX2hjkgzeknhsEmSuIo55yL6Y8c4CwsoVeLW7AloD9ou-2xBEKNObQqwRG~FP~cMMLOyNoPDzwclB8B~Imm3Qd~0-LAfKDp0nksbpBV87IN8YKsFxyAV5Bq~Mm-wqlGJZwBGzYfOPQQUNaTYZ2zzIidxTMNDLUE70fgc~oI2~0i2ebq-~8QFJwuLywTVycxV61BKssTsiOMBizE0g__";
|
||||
|
||||
interface MarketplacePanelProps {
|
||||
onSelectAgent: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function MarketplacePanel({ onSelectAgent }: MarketplacePanelProps) {
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
{/* Hero banner */}
|
||||
<div className="relative h-[200px] overflow-hidden mb-6 border border-border"
|
||||
style={{ borderTop: '2px solid oklch(0.75 0.18 55)' }}
|
||||
>
|
||||
<img
|
||||
src={LIGHTNING_IMG}
|
||||
alt="Lightning Network"
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background via-background/60 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-btc-orange mb-1">
|
||||
// AGENT MARKETPLACE
|
||||
</div>
|
||||
<p className="text-[13px] text-foreground max-w-md leading-relaxed">
|
||||
Hire specialized agents with Lightning sats. Each agent bids on tasks
|
||||
through the L402 auction system. No API keys — just sats.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent catalog */}
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-muted-foreground mb-3">
|
||||
// AVAILABLE AGENTS
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{AGENT_CATALOG.map((agent, i) => (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.06 }}
|
||||
className="panel p-4 hover:bg-panel-hover transition-colors group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`status-dot ${
|
||||
agent.status === 'active' ? 'status-dot-active' : 'status-dot-planned'
|
||||
}`} />
|
||||
<span className="text-[14px] font-semibold text-foreground">{agent.name}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-muted-foreground">{agent.role}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{agent.rateSats === 0 ? (
|
||||
<Badge variant="outline" className="text-[9px] border-electric-green/30 text-electric-green">
|
||||
FREE
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3 text-btc-orange" />
|
||||
<span className="sat-amount text-[13px]">{agent.rateSats}</span>
|
||||
<span className="text-[9px] text-muted-foreground">sats/task</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed mb-3">
|
||||
{agent.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{agent.capabilities.map(cap => (
|
||||
<span key={cap} className="text-[9px] px-1.5 py-0.5 bg-accent text-accent-foreground border border-border">
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[9px] ${
|
||||
agent.status === 'active'
|
||||
? 'border-electric-green/30 text-electric-green'
|
||||
: 'border-muted-foreground/30 text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{agent.status.toUpperCase()}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-[10px] text-btc-orange hover:text-btc-orange/80 hover:bg-btc-orange/10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => {
|
||||
if (agent.status === 'active') {
|
||||
onSelectAgent(agent.id);
|
||||
} else {
|
||||
toast("Agent not yet available", {
|
||||
description: `${agent.name} is planned for a future release.`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{agent.status === 'active' ? 'VIEW' : 'COMING SOON'}
|
||||
<ChevronRight className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Left sidebar with stacked status panels
|
||||
* Each panel has a 2px Bitcoin orange top border and monospace headers
|
||||
*/
|
||||
|
||||
import { Activity, Bell, Zap, Users, ChevronRight } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
AGENT_CATALOG, MOCK_HEALTH, MOCK_NOTIFICATIONS,
|
||||
} from "@/lib/data";
|
||||
|
||||
interface StatusSidebarProps {
|
||||
onSelectAgent: (id: string) => void;
|
||||
selectedAgent: string | null;
|
||||
}
|
||||
|
||||
export default function StatusSidebar({ onSelectAgent, selectedAgent }: StatusSidebarProps) {
|
||||
return (
|
||||
<div className="p-3 space-y-3">
|
||||
{/* System Health Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Activity className="w-3 h-3" />
|
||||
<span>// SYSTEM HEALTH</span>
|
||||
</div>
|
||||
<div className="p-3 text-[12px]">
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Ollama</td>
|
||||
<td className="text-right py-1">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.ollama === 'up' ? 'status-dot-active' : 'status-dot-danger'}`} />
|
||||
<span className={MOCK_HEALTH.ollama === 'up' ? 'text-electric-green' : 'text-danger-red'}>
|
||||
{MOCK_HEALTH.ollama.toUpperCase()}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Model</td>
|
||||
<td className="text-right py-1 text-foreground">{MOCK_HEALTH.model}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Swarm</td>
|
||||
<td className="text-right py-1">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.swarmRegistry === 'active' ? 'status-dot-active' : 'status-dot-warning'}`} />
|
||||
<span className={MOCK_HEALTH.swarmRegistry === 'active' ? 'text-electric-green' : 'text-warning-amber'}>
|
||||
{MOCK_HEALTH.swarmRegistry.toUpperCase()}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Uptime</td>
|
||||
<td className="text-right py-1 text-foreground">{MOCK_HEALTH.uptime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Tasks</td>
|
||||
<td className="text-right py-1 text-foreground">
|
||||
<span className="text-electric-green">{MOCK_HEALTH.completedTasks}</span>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<span>{MOCK_HEALTH.totalTasks}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agents Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Users className="w-3 h-3" />
|
||||
<span>// AGENTS</span>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
{AGENT_CATALOG.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
onClick={() => onSelectAgent(agent.id)}
|
||||
className={`w-full text-left px-3 py-2 flex items-center gap-2.5 text-[12px] transition-colors hover:bg-accent ${
|
||||
selectedAgent === agent.id ? 'bg-accent border-l-2 border-btc-orange' : ''
|
||||
}`}
|
||||
>
|
||||
<span className={`status-dot flex-shrink-0 ${
|
||||
agent.status === 'active' ? 'status-dot-active' :
|
||||
agent.status === 'planned' ? 'status-dot-planned' : 'status-dot-danger'
|
||||
}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-foreground">{agent.name}</span>
|
||||
{agent.rateSats > 0 && (
|
||||
<span className="text-[10px] text-btc-orange">{agent.rateSats} sats</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground truncate block">
|
||||
{agent.role}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* L402 Balance Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Zap className="w-3 h-3" />
|
||||
<span>// L402 TREASURY</span>
|
||||
</div>
|
||||
<div className="p-3 text-[12px]">
|
||||
<div className="text-center mb-3">
|
||||
<div className="sat-amount text-[20px] font-bold">
|
||||
₿ {MOCK_HEALTH.l402Balance.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5">satoshis available</div>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Protocol</td>
|
||||
<td className="text-right py-1 text-foreground">L402 / Lightning</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Macaroon</td>
|
||||
<td className="text-right py-1">
|
||||
<span className="text-electric-green text-[10px] uppercase tracking-wider">Valid</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-muted-foreground py-1 pr-2">Network</td>
|
||||
<td className="text-right py-1 text-foreground">Testnet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications Panel */}
|
||||
<div className="panel">
|
||||
<div className="panel-header flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="w-3 h-3" />
|
||||
<span>// NOTIFICATIONS</span>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-[9px] h-4 px-1.5 bg-btc-orange/20 text-btc-orange border-0">
|
||||
{MOCK_NOTIFICATIONS.filter(n => !n.read).length} new
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
{MOCK_NOTIFICATIONS.slice(0, 4).map((notif) => (
|
||||
<div
|
||||
key={notif.id}
|
||||
className={`px-3 py-2 text-[11px] border-b border-border/50 last:border-0 ${
|
||||
!notif.read ? 'bg-accent/50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
{!notif.read && <span className="w-1.5 h-1.5 rounded-full bg-btc-orange flex-shrink-0" />}
|
||||
<span className="font-medium text-foreground truncate">{notif.title}</span>
|
||||
<span className="text-[9px] text-muted-foreground ml-auto flex-shrink-0">
|
||||
{getCategoryIcon(notif.category)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground truncate pl-3">{notif.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getCategoryIcon(category: string): string {
|
||||
switch (category) {
|
||||
case "swarm": return "⚡";
|
||||
case "task": return "📋";
|
||||
case "agent": return "🤖";
|
||||
case "system": return "⚙️";
|
||||
case "payment": return "₿";
|
||||
default: return "•";
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Swarm management panel
|
||||
* Shows agent constellation visualization and live event feed
|
||||
*/
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AGENT_CATALOG, MOCK_WS_EVENTS, type Agent } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { Network, Radio, Zap } from "lucide-react";
|
||||
|
||||
const SWARM_IMG = "https://private-us-east-1.manuscdn.com/sessionFile/hmEvCGQLHKyGnx6qwMSEHn/sandbox/qiXHjJUmj8lqJymwhLI5B2-img-2_1771695716000_na1fn_c3dhcm0tbmV0d29yaw.png?x-oss-process=image/resize,w_1920,h_1920/format,webp/quality,q_80&Expires=1798761600&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9wcml2YXRlLXVzLWVhc3QtMS5tYW51c2Nkbi5jb20vc2Vzc2lvbkZpbGUvaG1FdkNHUUxIS3lHbng2cXdNU0VIbi9zYW5kYm94L3FpWEhqSlVtajhscUp5bXdoTEk1QjItaW1nLTJfMTc3MTY5NTcxNjAwMF9uYTFmbl9jM2RoY20wdGJtVjBkMjl5YXcucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLHdfMTkyMCxoXzE5MjAvZm9ybWF0LHdlYnAvcXVhbGl0eSxxXzgwIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzk4NzYxNjAwfX19XX0_&Key-Pair-Id=K2HSFNDJXOU9YS&Signature=rJ6lQ-h3pSQDDcUkGSTmXY2409jDYW2LdC9FU2ifVTnfppMXRrupq2SRC4e5P~Q5zx2r1ckGCWAi954bOr62u43lAXcxXn-FbW7PPVhoh3hx2LqGQrPLbSNbMw0-2AYO~4iKbUa~7igW2XdxeErPWs-fNzAfukvyh84cIAroFaLTdRT3IZR0amkWG8KSg5WWvv80lv0fO-zthT6kZDfPrSAHg0Opvtzy00ll~0lPq8V69DK3BP51GxIBiUPShjD1WgSrJsLbB7TLpug5PgTeeBRx80W0I6HIVxmRWQBOdmM~ziHQyNs8EhtCD7lYks8izHxCquCsFTuflp9IdrCIAQ__";
|
||||
|
||||
interface SwarmPanelProps {
|
||||
onSelectAgent: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function SwarmPanel({ onSelectAgent }: SwarmPanelProps) {
|
||||
const activeAgents = AGENT_CATALOG.filter(a => a.status === "active");
|
||||
const plannedAgents = AGENT_CATALOG.filter(a => a.status === "planned");
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col lg:flex-row">
|
||||
{/* Swarm visualization */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Network image */}
|
||||
<div className="relative h-[300px] lg:h-[400px] overflow-hidden border-b border-border">
|
||||
<img
|
||||
src={SWARM_IMG}
|
||||
alt="Swarm Network Topology"
|
||||
className="w-full h-full object-cover opacity-70"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/40 to-transparent" />
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-btc-orange mb-1">
|
||||
// SWARM TOPOLOGY
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-[11px] text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="status-dot status-dot-active" />
|
||||
{activeAgents.length} active
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="status-dot status-dot-planned" />
|
||||
{plannedAgents.length} planned
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Network className="w-3 h-3" />
|
||||
{AGENT_CATALOG.length} total
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent grid */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-4">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-muted-foreground mb-3">
|
||||
// REGISTERED AGENTS
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{AGENT_CATALOG.map((agent, i) => (
|
||||
<motion.button
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
onClick={() => onSelectAgent(agent.id)}
|
||||
className="panel text-left p-3 hover:bg-panel-hover transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`status-dot ${
|
||||
agent.status === 'active' ? 'status-dot-active' : 'status-dot-planned'
|
||||
}`} />
|
||||
<span className="text-[12px] font-semibold text-foreground">{agent.name}</span>
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1.5 border-border ml-auto">
|
||||
{agent.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mb-1.5">{agent.role}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.capabilities.map(cap => (
|
||||
<span key={cap} className="text-[9px] px-1.5 py-0.5 bg-accent text-accent-foreground">
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Live event feed */}
|
||||
<div className="lg:w-[300px] border-t lg:border-t-0 lg:border-l border-border flex flex-col">
|
||||
<div className="panel-header flex items-center gap-2">
|
||||
<Radio className="w-3 h-3 text-electric-green" />
|
||||
// LIVE FEED
|
||||
<span className="ml-auto flex items-center gap-1 text-electric-green">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-electric-green animate-pulse" />
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 max-h-[300px] lg:max-h-none">
|
||||
<div className="p-2 space-y-1">
|
||||
{[...MOCK_WS_EVENTS].reverse().map((evt, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, x: 16 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.03 }}
|
||||
className="px-3 py-2 text-[11px] border-b border-border/30"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
<span className={`text-[9px] font-semibold uppercase ${getEventColor(evt.event)}`}>
|
||||
{evt.event.replace(/_/g, ' ')}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground ml-auto">
|
||||
{new Date(evt.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-[10px]">
|
||||
{formatEventData(evt)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getEventColor(event: string): string {
|
||||
if (event.includes('completed')) return 'text-electric-green';
|
||||
if (event.includes('assigned')) return 'text-btc-orange';
|
||||
if (event.includes('bid')) return 'text-warning-amber';
|
||||
if (event.includes('joined')) return 'text-cyber-cyan';
|
||||
if (event.includes('posted')) return 'text-foreground';
|
||||
return 'text-muted-foreground';
|
||||
}
|
||||
|
||||
function formatEventData(evt: { event: string; data: Record<string, unknown> }): string {
|
||||
const d = evt.data;
|
||||
if (evt.event === 'agent_joined') return `${d.name} joined the swarm`;
|
||||
if (evt.event === 'task_posted') return `"${d.description}"`;
|
||||
if (evt.event === 'task_assigned') return `→ ${d.agent_id}`;
|
||||
if (evt.event === 'task_completed') return `✓ ${d.agent_id}: ${d.result}`;
|
||||
if (evt.event === 'bid_submitted') return `${d.agent_id} bid ${d.bid_sats} sats`;
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Task management panel
|
||||
* Task list with status badges, filtering, and auction indicators
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MOCK_TASKS, type Task } from "@/lib/data";
|
||||
import { motion } from "framer-motion";
|
||||
import { ListTodo, Plus, Filter, Clock, CheckCircle, AlertCircle, Loader2, Gavel } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface TasksPanelProps {
|
||||
onSelectTask: (id: string) => void;
|
||||
}
|
||||
|
||||
type StatusFilter = "all" | Task["status"];
|
||||
|
||||
export default function TasksPanel({ onSelectTask }: TasksPanelProps) {
|
||||
const [filter, setFilter] = useState<StatusFilter>("all");
|
||||
const [tasks] = useState<Task[]>(MOCK_TASKS);
|
||||
|
||||
const filtered = filter === "all" ? tasks : tasks.filter(t => t.status === filter);
|
||||
|
||||
const statusCounts = {
|
||||
all: tasks.length,
|
||||
pending: tasks.filter(t => t.status === "pending").length,
|
||||
bidding: tasks.filter(t => t.status === "bidding").length,
|
||||
assigned: tasks.filter(t => t.status === "assigned").length,
|
||||
running: tasks.filter(t => t.status === "running").length,
|
||||
completed: tasks.filter(t => t.status === "completed").length,
|
||||
failed: tasks.filter(t => t.status === "failed").length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header with filters */}
|
||||
<div className="border-b border-border p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-muted-foreground flex items-center gap-2">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
// TASK QUEUE
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] text-btc-orange hover:text-btc-orange/80 hover:bg-btc-orange/10"
|
||||
onClick={() => toast("Feature coming soon", { description: "Task creation requires backend connection." })}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
NEW TASK
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Filter pills */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(["all", "pending", "bidding", "running", "completed"] as StatusFilter[]).map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setFilter(s)}
|
||||
className={`text-[10px] uppercase tracking-wider px-2 py-1 transition-colors ${
|
||||
filter === s
|
||||
? 'bg-btc-orange/20 text-btc-orange border border-btc-orange/30'
|
||||
: 'bg-accent text-muted-foreground border border-transparent hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{s} ({statusCounts[s]})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task list */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3 space-y-2">
|
||||
{filtered.map((task, i) => (
|
||||
<motion.button
|
||||
key={task.id}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
onClick={() => onSelectTask(task.id)}
|
||||
className="panel w-full text-left p-3 hover:bg-panel-hover transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<StatusIcon status={task.status} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[10px] text-muted-foreground font-mono">{task.id}</span>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
<p className="text-[12px] text-foreground leading-snug mb-1.5">
|
||||
{task.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-[10px] text-muted-foreground">
|
||||
{task.assignedAgent && (
|
||||
<span className="flex items-center gap-1">
|
||||
→ <span className="text-btc-orange">{task.assignedAgent}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-2.5 h-2.5" />
|
||||
{new Date(task.createdAt).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground text-[12px]">
|
||||
No tasks matching filter "{filter}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusIcon({ status }: { status: Task["status"] }) {
|
||||
const cls = "w-4 h-4 mt-0.5 flex-shrink-0";
|
||||
switch (status) {
|
||||
case "pending": return <Clock className={`${cls} text-muted-foreground`} />;
|
||||
case "bidding": return <Gavel className={`${cls} text-warning-amber`} />;
|
||||
case "assigned": return <AlertCircle className={`${cls} text-cyber-cyan`} />;
|
||||
case "running": return <Loader2 className={`${cls} text-btc-orange animate-spin`} />;
|
||||
case "completed": return <CheckCircle className={`${cls} text-electric-green`} />;
|
||||
case "failed": return <AlertCircle className={`${cls} text-danger-red`} />;
|
||||
}
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: Task["status"] }) {
|
||||
const colors: Record<Task["status"], string> = {
|
||||
pending: "bg-muted text-muted-foreground",
|
||||
bidding: "bg-warning-amber/20 text-warning-amber border-warning-amber/30",
|
||||
assigned: "bg-cyber-cyan/20 text-cyber-cyan border-cyber-cyan/30",
|
||||
running: "bg-btc-orange/20 text-btc-orange border-btc-orange/30",
|
||||
completed: "bg-electric-green/20 text-electric-green border-electric-green/30",
|
||||
failed: "bg-danger-red/20 text-danger-red border-danger-red/30",
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`text-[9px] uppercase tracking-wider px-1.5 py-0.5 border ${colors[status]}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Top navigation bar
|
||||
* Bitcoin orange accent line at top, system title, notification bell
|
||||
*/
|
||||
|
||||
import { Bell, Menu, Terminal, Zap } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MOCK_HEALTH } from "@/lib/data";
|
||||
|
||||
interface TopBarProps {
|
||||
unreadCount: number;
|
||||
onToggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export default function TopBar({ unreadCount, onToggleSidebar }: TopBarProps) {
|
||||
return (
|
||||
<header className="h-12 flex-shrink-0 border-b border-border bg-card flex items-center px-4 gap-3"
|
||||
style={{ borderTop: '2px solid oklch(0.75 0.18 55)' }}
|
||||
>
|
||||
{/* Mobile menu button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="lg:hidden h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||
onClick={onToggleSidebar}
|
||||
>
|
||||
<Menu className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Logo / Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-btc-orange" />
|
||||
<span className="text-[13px] font-semibold tracking-[0.05em] text-foreground">
|
||||
TIMMY TIME
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground tracking-[0.1em]">
|
||||
MISSION CONTROL
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Status indicators */}
|
||||
<div className="hidden sm:flex items-center gap-4 text-[11px] text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.ollama === 'up' ? 'status-dot-active' : 'status-dot-danger'}`} />
|
||||
<span>OLLAMA</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`status-dot ${MOCK_HEALTH.swarmRegistry === 'active' ? 'status-dot-active' : 'status-dot-warning'}`} />
|
||||
<span>SWARM</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Zap className="w-3 h-3 text-btc-orange" />
|
||||
<span className="text-btc-orange font-medium">
|
||||
{MOCK_HEALTH.l402Balance.toLocaleString()} sats
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 w-4 h-4 rounded-full bg-btc-orange text-[9px] font-bold text-black flex items-center justify-center">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Version */}
|
||||
<span className="text-[9px] text-muted-foreground tracking-wider hidden md:inline">
|
||||
v2.0.0
|
||||
</span>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Hacker Aesthetic with Bitcoin Soul
|
||||
* Base: True black (#000000) with blue tint on panels (#080c12)
|
||||
* Primary: Bitcoin orange (#f7931a)
|
||||
* Active/Health: Electric green (#39ff14)
|
||||
* Text: White (#e8e8e8) > Steel gray (#6b7280) > Dark gray (#374151)
|
||||
* Font: JetBrains Mono throughout
|
||||
*/
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'JetBrains Mono', monospace;
|
||||
--font-mono: 'Fira Code', 'JetBrains Mono', monospace;
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
/* Custom sovereign colors */
|
||||
--color-btc-orange: oklch(0.75 0.18 55);
|
||||
--color-btc-orange-dim: oklch(0.55 0.14 55);
|
||||
--color-electric-green: oklch(0.85 0.3 145);
|
||||
--color-electric-green-dim: oklch(0.55 0.2 145);
|
||||
--color-cyber-cyan: oklch(0.8 0.15 200);
|
||||
--color-warning-amber: oklch(0.8 0.18 80);
|
||||
--color-danger-red: oklch(0.65 0.25 25);
|
||||
--color-panel: oklch(0.12 0.01 260);
|
||||
--color-panel-hover: oklch(0.16 0.01 260);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.25rem;
|
||||
--background: oklch(0.05 0.005 260);
|
||||
--foreground: oklch(0.9 0.005 90);
|
||||
--card: oklch(0.1 0.008 260);
|
||||
--card-foreground: oklch(0.9 0.005 90);
|
||||
--popover: oklch(0.12 0.01 260);
|
||||
--popover-foreground: oklch(0.9 0.005 90);
|
||||
--primary: oklch(0.75 0.18 55);
|
||||
--primary-foreground: oklch(0.1 0.005 55);
|
||||
--secondary: oklch(0.15 0.008 260);
|
||||
--secondary-foreground: oklch(0.7 0.01 90);
|
||||
--muted: oklch(0.18 0.008 260);
|
||||
--muted-foreground: oklch(0.55 0.01 260);
|
||||
--accent: oklch(0.15 0.01 260);
|
||||
--accent-foreground: oklch(0.9 0.005 90);
|
||||
--destructive: oklch(0.55 0.22 25);
|
||||
--destructive-foreground: oklch(0.95 0 0);
|
||||
--border: oklch(0.22 0.01 260);
|
||||
--input: oklch(0.18 0.01 260);
|
||||
--ring: oklch(0.75 0.18 55);
|
||||
--chart-1: oklch(0.75 0.18 55);
|
||||
--chart-2: oklch(0.85 0.3 145);
|
||||
--chart-3: oklch(0.8 0.15 200);
|
||||
--chart-4: oklch(0.8 0.18 80);
|
||||
--chart-5: oklch(0.65 0.25 25);
|
||||
--sidebar: oklch(0.08 0.008 260);
|
||||
--sidebar-foreground: oklch(0.85 0.005 90);
|
||||
--sidebar-primary: oklch(0.75 0.18 55);
|
||||
--sidebar-primary-foreground: oklch(0.1 0.005 55);
|
||||
--sidebar-accent: oklch(0.14 0.01 260);
|
||||
--sidebar-accent-foreground: oklch(0.9 0.005 90);
|
||||
--sidebar-border: oklch(0.2 0.01 260);
|
||||
--sidebar-ring: oklch(0.75 0.18 55);
|
||||
}
|
||||
|
||||
/* Dark is the only theme — no light mode */
|
||||
.dark {
|
||||
--background: oklch(0.05 0.005 260);
|
||||
--foreground: oklch(0.9 0.005 90);
|
||||
--card: oklch(0.1 0.008 260);
|
||||
--card-foreground: oklch(0.9 0.005 90);
|
||||
--popover: oklch(0.12 0.01 260);
|
||||
--popover-foreground: oklch(0.9 0.005 90);
|
||||
--primary: oklch(0.75 0.18 55);
|
||||
--primary-foreground: oklch(0.1 0.005 55);
|
||||
--secondary: oklch(0.15 0.008 260);
|
||||
--secondary-foreground: oklch(0.7 0.01 90);
|
||||
--muted: oklch(0.18 0.008 260);
|
||||
--muted-foreground: oklch(0.55 0.01 260);
|
||||
--accent: oklch(0.15 0.01 260);
|
||||
--accent-foreground: oklch(0.9 0.005 90);
|
||||
--destructive: oklch(0.55 0.22 25);
|
||||
--destructive-foreground: oklch(0.95 0 0);
|
||||
--border: oklch(0.22 0.01 260);
|
||||
--input: oklch(0.18 0.01 260);
|
||||
--ring: oklch(0.75 0.18 55);
|
||||
--chart-1: oklch(0.75 0.18 55);
|
||||
--chart-2: oklch(0.85 0.3 145);
|
||||
--chart-3: oklch(0.8 0.15 200);
|
||||
--chart-4: oklch(0.8 0.18 80);
|
||||
--chart-5: oklch(0.65 0.25 25);
|
||||
--sidebar: oklch(0.08 0.008 260);
|
||||
--sidebar-foreground: oklch(0.85 0.005 90);
|
||||
--sidebar-primary: oklch(0.75 0.18 55);
|
||||
--sidebar-primary-foreground: oklch(0.1 0.005 55);
|
||||
--sidebar-accent: oklch(0.14 0.01 260);
|
||||
--sidebar-accent-foreground: oklch(0.9 0.005 90);
|
||||
--sidebar-border: oklch(0.2 0.01 260);
|
||||
--sidebar-ring: oklch(0.75 0.18 55);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[role="button"]:not([aria-disabled="true"]),
|
||||
[type="button"]:not(:disabled),
|
||||
[type="submit"]:not(:disabled),
|
||||
[type="reset"]:not(:disabled),
|
||||
a[href],
|
||||
select:not(:disabled),
|
||||
input[type="checkbox"]:not(:disabled),
|
||||
input[type="radio"]:not(:disabled) {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
max-width: 1600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sovereign Terminal custom components */
|
||||
.panel {
|
||||
@apply bg-card border border-border;
|
||||
border-top: 2px solid var(--color-btc-orange);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@apply text-[11px] uppercase tracking-[0.15em] text-muted-foreground px-4 py-2 border-b border-border;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@apply inline-block w-2 h-2 rounded-full;
|
||||
}
|
||||
|
||||
.status-dot-active {
|
||||
@apply bg-electric-green;
|
||||
box-shadow: 0 0 6px var(--color-electric-green);
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot-warning {
|
||||
@apply bg-warning-amber;
|
||||
box-shadow: 0 0 6px var(--color-warning-amber);
|
||||
}
|
||||
|
||||
.status-dot-danger {
|
||||
@apply bg-danger-red;
|
||||
box-shadow: 0 0 6px var(--color-danger-red);
|
||||
}
|
||||
|
||||
.status-dot-planned {
|
||||
@apply bg-muted-foreground;
|
||||
}
|
||||
|
||||
.terminal-prompt {
|
||||
@apply text-btc-orange font-semibold;
|
||||
}
|
||||
|
||||
.sat-amount {
|
||||
@apply text-btc-orange font-mono font-medium;
|
||||
}
|
||||
|
||||
/* Scanline overlay */
|
||||
.scanline-overlay {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.03) 2px,
|
||||
rgba(0, 0, 0, 0.03) 4px
|
||||
);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes typewriter-cursor {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes scan-line {
|
||||
0% { transform: translateY(-100%); }
|
||||
100% { transform: translateY(100vh); }
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: oklch(0.3 0.01 260);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(0.4 0.01 260);
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Hacker Aesthetic with Bitcoin Soul
|
||||
* Static data for the dashboard — mirrors the Python backend models
|
||||
*/
|
||||
|
||||
export interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
description: string;
|
||||
capabilities: string[];
|
||||
rateSats: number;
|
||||
status: "active" | "planned" | "offline";
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
description: string;
|
||||
status: "pending" | "bidding" | "assigned" | "running" | "completed" | "failed";
|
||||
assignedAgent: string | null;
|
||||
result: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
title: string;
|
||||
message: string;
|
||||
category: "swarm" | "task" | "agent" | "system" | "payment";
|
||||
timestamp: string;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface WSEvent {
|
||||
event: string;
|
||||
data: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// ── Agent Catalog ─────────────────────────────────────────────────────────
|
||||
export const AGENT_CATALOG: Agent[] = [
|
||||
{
|
||||
id: "timmy",
|
||||
name: "Timmy",
|
||||
role: "Sovereign Commander",
|
||||
description: "Primary AI companion. Coordinates the swarm, manages tasks, and maintains sovereignty.",
|
||||
capabilities: ["chat", "reasoning", "coordination"],
|
||||
rateSats: 0,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "echo",
|
||||
name: "Echo",
|
||||
role: "Research Analyst",
|
||||
description: "Deep research and information synthesis. Reads, summarizes, and cross-references sources.",
|
||||
capabilities: ["research", "summarization", "fact-checking"],
|
||||
rateSats: 50,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "mace",
|
||||
name: "Mace",
|
||||
role: "Security Sentinel",
|
||||
description: "Network security, threat assessment, and system hardening recommendations.",
|
||||
capabilities: ["security", "monitoring", "threat-analysis"],
|
||||
rateSats: 75,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "helm",
|
||||
name: "Helm",
|
||||
role: "System Navigator",
|
||||
description: "Infrastructure management, deployment automation, and system configuration.",
|
||||
capabilities: ["devops", "automation", "configuration"],
|
||||
rateSats: 60,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "seer",
|
||||
name: "Seer",
|
||||
role: "Data Oracle",
|
||||
description: "Data analysis, pattern recognition, and predictive insights from local datasets.",
|
||||
capabilities: ["analytics", "visualization", "prediction"],
|
||||
rateSats: 65,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "forge",
|
||||
name: "Forge",
|
||||
role: "Code Smith",
|
||||
description: "Code generation, refactoring, debugging, and test writing.",
|
||||
capabilities: ["coding", "debugging", "testing"],
|
||||
rateSats: 55,
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "quill",
|
||||
name: "Quill",
|
||||
role: "Content Scribe",
|
||||
description: "Long-form writing, editing, documentation, and content creation.",
|
||||
capabilities: ["writing", "editing", "documentation"],
|
||||
rateSats: 45,
|
||||
status: "planned",
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mock Tasks ────────────────────────────────────────────────────────────
|
||||
export const MOCK_TASKS: Task[] = [
|
||||
{
|
||||
id: "t-001",
|
||||
description: "Analyze Bitcoin whitepaper and summarize key innovations",
|
||||
status: "completed",
|
||||
assignedAgent: "timmy",
|
||||
result: "Summary generated: 3 key innovations identified — decentralized consensus, proof-of-work, and UTXO model.",
|
||||
createdAt: "2026-02-21T10:00:00Z",
|
||||
completedAt: "2026-02-21T10:02:30Z",
|
||||
},
|
||||
{
|
||||
id: "t-002",
|
||||
description: "Scan local network for open ports and vulnerabilities",
|
||||
status: "bidding",
|
||||
assignedAgent: null,
|
||||
result: null,
|
||||
createdAt: "2026-02-21T14:30:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "t-003",
|
||||
description: "Generate unit tests for the L402 proxy module",
|
||||
status: "running",
|
||||
assignedAgent: "forge",
|
||||
result: null,
|
||||
createdAt: "2026-02-21T15:00:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "t-004",
|
||||
description: "Write documentation for the swarm coordinator API",
|
||||
status: "pending",
|
||||
assignedAgent: null,
|
||||
result: null,
|
||||
createdAt: "2026-02-21T16:00:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "t-005",
|
||||
description: "Research self-custody best practices for 2026",
|
||||
status: "assigned",
|
||||
assignedAgent: "echo",
|
||||
result: null,
|
||||
createdAt: "2026-02-21T16:30:00Z",
|
||||
completedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mock Notifications ────────────────────────────────────────────────────
|
||||
export const MOCK_NOTIFICATIONS: Notification[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Swarm Online",
|
||||
message: "Timmy coordinator initialized. Swarm registry active.",
|
||||
category: "system",
|
||||
timestamp: "2026-02-21T10:00:00Z",
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Task Completed",
|
||||
message: "Bitcoin whitepaper analysis finished in 2m 30s.",
|
||||
category: "task",
|
||||
timestamp: "2026-02-21T10:02:30Z",
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Auction Started",
|
||||
message: "Network scan task open for bidding. 15s auction window.",
|
||||
category: "swarm",
|
||||
timestamp: "2026-02-21T14:30:00Z",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Agent Assigned",
|
||||
message: "Forge won the bid for test generation at 55 sats.",
|
||||
category: "agent",
|
||||
timestamp: "2026-02-21T15:00:05Z",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "L402 Payment",
|
||||
message: "Invoice settled: 75 sats for Mace security scan.",
|
||||
category: "payment",
|
||||
timestamp: "2026-02-21T15:30:00Z",
|
||||
read: false,
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mock WebSocket Events ─────────────────────────────────────────────────
|
||||
export const MOCK_WS_EVENTS: WSEvent[] = [
|
||||
{ event: "agent_joined", data: { agent_id: "timmy", name: "Timmy" }, timestamp: "2026-02-21T10:00:00Z" },
|
||||
{ event: "task_posted", data: { task_id: "t-001", description: "Analyze Bitcoin whitepaper" }, timestamp: "2026-02-21T10:00:05Z" },
|
||||
{ event: "task_assigned", data: { task_id: "t-001", agent_id: "timmy" }, timestamp: "2026-02-21T10:00:10Z" },
|
||||
{ event: "task_completed", data: { task_id: "t-001", agent_id: "timmy", result: "Analysis complete" }, timestamp: "2026-02-21T10:02:30Z" },
|
||||
{ event: "task_posted", data: { task_id: "t-002", description: "Scan local network" }, timestamp: "2026-02-21T14:30:00Z" },
|
||||
{ event: "bid_submitted", data: { task_id: "t-002", agent_id: "mace", bid_sats: 75 }, timestamp: "2026-02-21T14:30:05Z" },
|
||||
{ event: "task_posted", data: { task_id: "t-003", description: "Generate unit tests" }, timestamp: "2026-02-21T15:00:00Z" },
|
||||
{ event: "task_assigned", data: { task_id: "t-003", agent_id: "forge" }, timestamp: "2026-02-21T15:00:05Z" },
|
||||
];
|
||||
|
||||
// ── Mock Chat History ─────────────────────────────────────────────────────
|
||||
export const MOCK_CHAT: ChatMessage[] = [
|
||||
{
|
||||
id: "c-001",
|
||||
role: "system",
|
||||
content: "// TIMMY v1.0.0 — Sovereign Commander online\n// Ollama backend: llama3.2\n// All systems nominal. No cloud. No telemetry.",
|
||||
timestamp: "2026-02-21T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "c-002",
|
||||
role: "user",
|
||||
content: "What is sovereignty in the context of Bitcoin?",
|
||||
timestamp: "2026-02-21T10:01:00Z",
|
||||
},
|
||||
{
|
||||
id: "c-003",
|
||||
role: "assistant",
|
||||
content: "Sovereignty in Bitcoin means self-custody — you hold your own keys, you validate your own transactions, you run your own node. No intermediary can freeze your funds, censor your payments, or inflate away your savings.\n\nThe same principle applies here: Timmy runs locally on your hardware, uses Ollama for inference, and stores everything in SQLite. No cloud dependency. No API keys to revoke. Your AI, your rules.\n\nSats are sovereignty, boss.",
|
||||
timestamp: "2026-02-21T10:01:15Z",
|
||||
},
|
||||
];
|
||||
|
||||
// ── System Health ─────────────────────────────────────────────────────────
|
||||
export interface SystemHealth {
|
||||
ollama: "up" | "down" | "unknown";
|
||||
model: string;
|
||||
swarmRegistry: "active" | "inactive";
|
||||
activeAgents: number;
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
uptime: string;
|
||||
l402Balance: number;
|
||||
}
|
||||
|
||||
export const MOCK_HEALTH: SystemHealth = {
|
||||
ollama: "up",
|
||||
model: "llama3.2",
|
||||
swarmRegistry: "active",
|
||||
activeAgents: 1,
|
||||
totalTasks: 5,
|
||||
completedTasks: 1,
|
||||
uptime: "4h 21m",
|
||||
l402Balance: 12_450,
|
||||
};
|
||||
|
||||
// ── Voice NLU Intents ─────────────────────────────────────────────────────
|
||||
export const VOICE_INTENTS = [
|
||||
{ name: "chat", description: "General conversation", example: "Tell me about self-custody" },
|
||||
{ name: "status", description: "System status query", example: "How are you?" },
|
||||
{ name: "swarm", description: "Swarm management", example: "Spawn agent Echo" },
|
||||
{ name: "task", description: "Task management", example: "Create task: scan network" },
|
||||
{ name: "help", description: "List commands", example: "What can you do?" },
|
||||
{ name: "voice", description: "Voice settings", example: "Speak slower" },
|
||||
];
|
||||
|
||||
// ── Roadmap ───────────────────────────────────────────────────────────────
|
||||
export const ROADMAP = [
|
||||
{ version: "1.0.0", name: "Genesis", milestone: "Agno + Ollama + SQLite + Dashboard", status: "complete" as const },
|
||||
{ version: "2.0.0", name: "Exodus", milestone: "MCP tools + multi-agent swarm", status: "current" as const },
|
||||
{ version: "3.0.0", name: "Revelation", milestone: "Bitcoin Lightning treasury + single .app", status: "planned" as const },
|
||||
];
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* DESIGN: "Sovereign Terminal" — Hacker Aesthetic with Bitcoin Soul
|
||||
* Three-column asymmetric layout:
|
||||
* Left (narrow): Status cards — agents, health, notifications, L402
|
||||
* Center (wide): Active workspace — Chat, Swarm, Tasks, Marketplace tabs
|
||||
* Right (medium): Context panel — details, auctions, invoices
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Terminal, Cpu, Zap, Bell, Shield,
|
||||
MessageSquare, Network, ListTodo, Store,
|
||||
ChevronRight, Activity, Volume2
|
||||
} from "lucide-react";
|
||||
|
||||
import StatusSidebar from "@/components/StatusSidebar";
|
||||
import ChatPanel from "@/components/ChatPanel";
|
||||
import SwarmPanel from "@/components/SwarmPanel";
|
||||
import TasksPanel from "@/components/TasksPanel";
|
||||
import MarketplacePanel from "@/components/MarketplacePanel";
|
||||
import ContextPanel from "@/components/ContextPanel";
|
||||
import TopBar from "@/components/TopBar";
|
||||
import { MOCK_NOTIFICATIONS } from "@/lib/data";
|
||||
|
||||
type TabValue = "chat" | "swarm" | "tasks" | "marketplace";
|
||||
|
||||
export default function Dashboard() {
|
||||
const [activeTab, setActiveTab] = useState<TabValue>("chat");
|
||||
const [selectedAgent, setSelectedAgent] = useState<string | null>("timmy");
|
||||
const [selectedTask, setSelectedTask] = useState<string | null>(null);
|
||||
const [showMobileSidebar, setShowMobileSidebar] = useState(false);
|
||||
const unreadCount = MOCK_NOTIFICATIONS.filter(n => !n.read).length;
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col overflow-hidden bg-background">
|
||||
{/* Scanline overlay */}
|
||||
<div className="scanline-overlay" />
|
||||
|
||||
{/* Top bar */}
|
||||
<TopBar
|
||||
unreadCount={unreadCount}
|
||||
onToggleSidebar={() => setShowMobileSidebar(!showMobileSidebar)}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left sidebar — status panels */}
|
||||
<aside className="hidden lg:flex w-[280px] flex-shrink-0 flex-col border-r border-border overflow-hidden">
|
||||
<ScrollArea className="flex-1">
|
||||
<StatusSidebar
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
selectedAgent={selectedAgent}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
|
||||
{/* Mobile sidebar overlay */}
|
||||
<AnimatePresence>
|
||||
{showMobileSidebar && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.5 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="lg:hidden fixed inset-0 bg-black z-40"
|
||||
onClick={() => setShowMobileSidebar(false)}
|
||||
/>
|
||||
<motion.aside
|
||||
initial={{ x: -280 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -280 }}
|
||||
transition={{ type: "tween", duration: 0.2 }}
|
||||
className="lg:hidden fixed left-0 top-0 bottom-0 w-[280px] bg-background border-r border-border z-50 overflow-auto"
|
||||
>
|
||||
<div className="pt-14">
|
||||
<StatusSidebar
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
setShowMobileSidebar(false);
|
||||
}}
|
||||
selectedAgent={selectedAgent}
|
||||
/>
|
||||
</div>
|
||||
</motion.aside>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Center workspace */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as TabValue)}
|
||||
className="flex-1 flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="border-b border-border px-4">
|
||||
<TabsList className="bg-transparent h-10 gap-0 p-0">
|
||||
<TabsTrigger
|
||||
value="chat"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<MessageSquare className="w-3.5 h-3.5 mr-1.5" />
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="swarm"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Network className="w-3.5 h-3.5 mr-1.5" />
|
||||
Swarm
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="tasks"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ListTodo className="w-3.5 h-3.5 mr-1.5" />
|
||||
Tasks
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="marketplace"
|
||||
className="data-[state=active]:bg-transparent data-[state=active]:text-btc-orange data-[state=active]:border-b-2 data-[state=active]:border-btc-orange rounded-none px-4 text-[11px] uppercase tracking-[0.12em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Store className="w-3.5 h-3.5 mr-1.5" />
|
||||
Marketplace
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<TabsContent value="chat" className="h-full m-0 p-0">
|
||||
<ChatPanel />
|
||||
</TabsContent>
|
||||
<TabsContent value="swarm" className="h-full m-0 p-0">
|
||||
<SwarmPanel
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="tasks" className="h-full m-0 p-0">
|
||||
<TasksPanel
|
||||
onSelectTask={(id) => {
|
||||
setSelectedTask(id);
|
||||
setSelectedAgent(null);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="marketplace" className="h-full m-0 p-0">
|
||||
<MarketplacePanel
|
||||
onSelectAgent={(id) => {
|
||||
setSelectedAgent(id);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</main>
|
||||
|
||||
{/* Right context panel */}
|
||||
<aside className="hidden xl:flex w-[320px] flex-shrink-0 flex-col border-l border-border overflow-hidden">
|
||||
<ScrollArea className="flex-1">
|
||||
<ContextPanel
|
||||
selectedAgent={selectedAgent}
|
||||
selectedTask={selectedTask}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Redirect } from "wouter";
|
||||
|
||||
export default function Home() {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
export default function NotFound() {
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-4">
|
||||
<Terminal className="w-12 h-12 text-btc-orange mx-auto" />
|
||||
<div className="text-[11px] uppercase tracking-[0.15em] text-btc-orange">
|
||||
// ERROR 404
|
||||
</div>
|
||||
<h1 className="text-[24px] font-bold text-foreground">
|
||||
Route not found
|
||||
</h1>
|
||||
<p className="text-[13px] text-muted-foreground max-w-sm mx-auto">
|
||||
The requested path does not exist in Mission Control.
|
||||
Check the URL or return to the dashboard.
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[11px] text-btc-orange hover:bg-btc-orange/10 border border-btc-orange/30 mt-4"
|
||||
onClick={() => setLocation("/")}
|
||||
>
|
||||
RETURN TO MISSION CONTROL
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user