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,56 @@
// API client for the FastAPI backend
import { CouncilBlueprint, CouncilRun } from "@/app/types/council";
const BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`API error ${res.status}: ${text}`);
}
return res.json() as Promise<T>;
}
// Council blueprint CRUD
export const councilApi = {
list: () => request<CouncilBlueprint[]>("/api/councils/"),
get: (id: string) => request<CouncilBlueprint>(`/api/councils/${id}`),
create: (blueprint: CouncilBlueprint) =>
request<CouncilBlueprint>("/api/councils/", {
method: "POST",
body: JSON.stringify(blueprint),
}),
update: (id: string, blueprint: CouncilBlueprint) =>
request<CouncilBlueprint>(`/api/councils/${id}`, {
method: "PUT",
body: JSON.stringify(blueprint),
}),
delete: (id: string) =>
request<void>(`/api/councils/${id}`, { method: "DELETE" }),
};
// Council run (execution)
export const runApi = {
start: (input_topic: string) =>
request<CouncilRun>("/api/run", {
method: "POST",
body: JSON.stringify({ input_topic }),
}),
status: (run_id: string) =>
request<CouncilRun>(`/api/run/${run_id}`),
};
// WebSocket URL helper
export function wsUrl(run_id: string): string {
const wsBase = BASE_URL.replace(/^http/, "ws");
return `${wsBase}/ws/council/${run_id}`;
}

View file

@ -0,0 +1,71 @@
// Parser: React Flow graph state → CouncilBlueprint JSON (backend format)
import { Node, Edge } from "@xyflow/react";
import { AgentNodeData, BlueprintEdge, BlueprintNode, CouncilBlueprint, EdgeType } from "@/app/types/council";
export function parseGraphToBlueprint(
nodes: Node<AgentNodeData>[],
edges: Edge[],
name: string,
existingId?: string
): CouncilBlueprint {
const blueprintNodes: BlueprintNode[] = nodes.map((node) => ({
id: node.id,
label: node.data.label,
systemPrompt: node.data.systemPrompt,
model: node.data.model,
tools: node.data.tools,
position: { x: node.position.x, y: node.position.y },
}));
const blueprintEdges: BlueprintEdge[] = edges.map((edge) => {
const edgeType: EdgeType = (edge.data?.type as EdgeType) ?? "linear";
const result: BlueprintEdge = {
id: edge.id,
source: edge.source,
target: edge.target,
type: edgeType,
};
if (edgeType === "conditional" && edge.data?.condition) {
result.condition = edge.data.condition as string;
}
return result;
});
return {
version: 1,
id: existingId,
name,
nodes: blueprintNodes,
edges: blueprintEdges,
};
}
// Reverse: CouncilBlueprint → React Flow nodes/edges (for loading saved blueprints)
export function parseBlueprintToGraph(blueprint: CouncilBlueprint): {
nodes: Node<AgentNodeData>[];
edges: Edge[];
} {
const nodes: Node<AgentNodeData>[] = blueprint.nodes.map((n) => ({
id: n.id,
type: "agentNode",
position: n.position,
data: {
label: n.label,
systemPrompt: n.systemPrompt,
model: n.model,
tools: n.tools,
},
}));
const edges: Edge[] = blueprint.edges.map((e) => ({
id: e.id,
source: e.source,
target: e.target,
type: e.type === "conditional" ? "conditionalEdge" : "default",
data: { type: e.type, condition: e.condition },
label: e.type === "conditional" ? e.condition : undefined,
animated: e.type === "conditional",
}));
return { nodes, edges };
}