- Scaffold Next.js 15 app with TypeScript, Tailwind, App Router - Install @xyflow/react, Zustand, Lucide icons, nanoid - Define council types (AgentNodeData, CouncilBlueprint, WSMessage, etc.) - Implement Zustand store for canvas and run state - Build custom AgentNode component (label, system prompt, model badge, tool chips, active pulse) - Build ConditionalEdge component (dashed indigo line with condition label) - Build NodeSidebar (drag-and-drop + click to add agents) - Build NodeSettingsPanel (name, system prompt, model selector, tool toggles) - Build ArchitectCanvas (React Flow canvas with drop zone, minimap, controls) - Build blueprint parser (React Flow JSON ↔ CouncilBlueprint JSON) - Build API client for FastAPI backend (CRUD + run endpoints) - Build useCouncilWebSocket hook for live agent status via WebSocket - Build Tab A: Rat-Architekt (canvas builder with save/export toolbar) - Build Tab B: Konferenzzimmer (execution view with live diagram + result panel) - Add NavTabs navigation with CouncilOS branding - All TypeScript checks passing https://claude.ai/code/session_01EkbecUVn7esdxLCXxVVRDX
74 lines
2 KiB
TypeScript
74 lines
2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useCallback } from "react";
|
|
import { WSMessage } from "@/app/types/council";
|
|
import { wsUrl } from "@/app/utils/api-client";
|
|
import { useCouncilStore } from "@/app/store/council-store";
|
|
|
|
interface Options {
|
|
run_id: string | null;
|
|
onComplete?: (result: string) => void;
|
|
onError?: (error: string) => void;
|
|
}
|
|
|
|
// WebSocket hook for live agent status updates during a council run
|
|
export function useCouncilWebSocket({ run_id, onComplete, onError }: Options) {
|
|
const ws = useRef<WebSocket | null>(null);
|
|
const markNodeActive = useCouncilStore((s) => s.markNodeActive);
|
|
const clearActiveNode = useCouncilStore((s) => s.clearActiveNode);
|
|
const setActiveRun = useCouncilStore((s) => s.setActiveRun);
|
|
|
|
const disconnect = useCallback(() => {
|
|
if (ws.current) {
|
|
ws.current.close();
|
|
ws.current = null;
|
|
}
|
|
clearActiveNode();
|
|
}, [clearActiveNode]);
|
|
|
|
useEffect(() => {
|
|
if (!run_id) return;
|
|
|
|
const socket = new WebSocket(wsUrl(run_id));
|
|
ws.current = socket;
|
|
|
|
socket.onmessage = (event: MessageEvent<string>) => {
|
|
let msg: WSMessage;
|
|
try {
|
|
msg = JSON.parse(event.data) as WSMessage;
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
switch (msg.type) {
|
|
case "node_enter":
|
|
if (msg.node_name) markNodeActive(msg.node_name);
|
|
break;
|
|
case "node_exit":
|
|
clearActiveNode();
|
|
break;
|
|
case "run_complete":
|
|
clearActiveNode();
|
|
setActiveRun(null);
|
|
if (msg.result) onComplete?.(msg.result);
|
|
disconnect();
|
|
break;
|
|
case "run_error":
|
|
clearActiveNode();
|
|
setActiveRun(null);
|
|
if (msg.error) onError?.(msg.error);
|
|
disconnect();
|
|
break;
|
|
}
|
|
};
|
|
|
|
socket.onerror = () => {
|
|
onError?.("WebSocket-Verbindungsfehler");
|
|
disconnect();
|
|
};
|
|
|
|
return () => disconnect();
|
|
}, [run_id]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
return { disconnect };
|
|
}
|