Implement Phase 2: Next.js + React Flow frontend MVP

- 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
This commit is contained in:
Claude 2026-02-20 17:03:32 +00:00
parent 06aec41a8a
commit 216fdd9589
No known key found for this signature in database
30 changed files with 8237 additions and 0 deletions

View file

@ -0,0 +1,74 @@
"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 };
}