Files
stack/apps/web/src/components/widgets/AgentStatusWidget.tsx
Jason Woltje f47dd8bc92 feat: add domains, ideas, layouts, widgets API modules
- Add DomainsModule with full CRUD, search, and activity logging
- Add IdeasModule with quick capture endpoint
- Add LayoutsModule for user dashboard layouts
- Add WidgetsModule for widget definitions (read-only)
- Update ActivityService with domain/idea logging methods
- Register all new modules in AppModule
2026-01-29 13:47:03 -06:00

170 lines
5.6 KiB
TypeScript

/**
* Agent Status Widget - shows running agents
*/
import { useState, useEffect } from "react";
import { Bot, Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
import type { WidgetProps } from "@mosaic/shared";
interface Agent {
id: string;
name: string;
status: "IDLE" | "WORKING" | "WAITING" | "ERROR" | "TERMINATED";
currentTask?: string;
lastHeartbeat: string;
taskCount: number;
}
export function AgentStatusWidget({ id: _id, config: _config }: WidgetProps) {
const [agents, setAgents] = useState<Agent[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Mock data for now - will fetch from API later
useEffect(() => {
setIsLoading(true);
setTimeout(() => {
setAgents([
{
id: "1",
name: "Code Review Agent",
status: "WORKING",
currentTask: "Reviewing PR #123",
lastHeartbeat: new Date().toISOString(),
taskCount: 42,
},
{
id: "2",
name: "Documentation Agent",
status: "IDLE",
lastHeartbeat: new Date().toISOString(),
taskCount: 15,
},
{
id: "3",
name: "Test Runner Agent",
status: "ERROR",
currentTask: "Failed to run tests",
lastHeartbeat: new Date(Date.now() - 300000).toISOString(),
taskCount: 28,
},
]);
setIsLoading(false);
}, 500);
}, []);
const getStatusIcon = (status: Agent["status"]) => {
switch (status) {
case "WORKING":
return <Activity className="w-4 h-4 text-blue-500 animate-pulse" />;
case "IDLE":
return <Clock className="w-4 h-4 text-gray-400" />;
case "WAITING":
return <Clock className="w-4 h-4 text-yellow-500" />;
case "ERROR":
return <AlertCircle className="w-4 h-4 text-red-500" />;
case "TERMINATED":
return <CheckCircle className="w-4 h-4 text-gray-500" />;
default:
return <Clock className="w-4 h-4 text-gray-400" />;
}
};
const getStatusText = (status: Agent["status"]) => {
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
};
const getTimeSinceLastHeartbeat = (timestamp: string) => {
const now = new Date();
const last = new Date(timestamp);
const diffMs = now.getTime() - last.getTime();
if (diffMs < 60000) return "Just now";
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m ago`;
if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)}h ago`;
return `${Math.floor(diffMs / 86400000)}d ago`;
};
const stats = {
total: agents.length,
working: agents.filter((a) => a.status === "WORKING").length,
idle: agents.filter((a) => a.status === "IDLE").length,
error: agents.filter((a) => a.status === "ERROR").length,
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-gray-500 text-sm">Loading agents...</div>
</div>
);
}
return (
<div className="flex flex-col h-full space-y-3">
{/* Summary stats */}
<div className="grid grid-cols-4 gap-1 text-center text-xs">
<div className="bg-gray-50 rounded p-2">
<div className="text-lg font-bold text-gray-900">{stats.total}</div>
<div className="text-gray-500">Total</div>
</div>
<div className="bg-blue-50 rounded p-2">
<div className="text-lg font-bold text-blue-600">{stats.working}</div>
<div className="text-blue-500">Working</div>
</div>
<div className="bg-gray-100 rounded p-2">
<div className="text-lg font-bold text-gray-600">{stats.idle}</div>
<div className="text-gray-500">Idle</div>
</div>
<div className="bg-red-50 rounded p-2">
<div className="text-lg font-bold text-red-600">{stats.error}</div>
<div className="text-red-500">Error</div>
</div>
</div>
{/* Agent list */}
<div className="flex-1 overflow-auto space-y-2">
{agents.length === 0 ? (
<div className="text-center text-gray-500 text-sm py-4">
No agents configured
</div>
) : (
agents.map((agent) => (
<div
key={agent.id}
className={`p-3 rounded-lg border ${
agent.status === "ERROR"
? "bg-red-50 border-red-200"
: agent.status === "WORKING"
? "bg-blue-50 border-blue-200"
: "bg-gray-50 border-gray-200"
}`}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Bot className="w-4 h-4 text-gray-600" />
<span className="text-sm font-medium text-gray-900">
{agent.name}
</span>
</div>
<div className="flex items-center gap-1 text-xs text-gray-500">
{getStatusIcon(agent.status)}
<span>{getStatusText(agent.status)}</span>
</div>
</div>
{agent.currentTask && (
<div className="text-xs text-gray-600 mb-1">{agent.currentTask}</div>
)}
<div className="flex items-center justify-between text-xs text-gray-400">
<span>{agent.taskCount} tasks completed</span>
<span>{getTimeSinceLastHeartbeat(agent.lastHeartbeat)}</span>
</div>
</div>
))
)}
</div>
</div>
);
}