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:
parent
06aec41a8a
commit
216fdd9589
30 changed files with 8237 additions and 0 deletions
125
frontend/app/store/council-store.ts
Normal file
125
frontend/app/store/council-store.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// Zustand store for canvas state and council run state
|
||||
import { create } from "zustand";
|
||||
import { Node, Edge, addEdge, applyNodeChanges, applyEdgeChanges, NodeChange, EdgeChange, Connection } from "@xyflow/react";
|
||||
import { AgentNodeData, CouncilRun, LLMModel } from "@/app/types/council";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
interface CouncilStore {
|
||||
// Canvas
|
||||
nodes: Node<AgentNodeData>[];
|
||||
edges: Edge[];
|
||||
selectedNodeId: string | null;
|
||||
councilName: string;
|
||||
|
||||
// Execution
|
||||
activeRun: CouncilRun | null;
|
||||
activeNodeId: string | null;
|
||||
|
||||
// Canvas actions
|
||||
onNodesChange: (changes: NodeChange[]) => void;
|
||||
onEdgesChange: (changes: EdgeChange[]) => void;
|
||||
onConnect: (connection: Connection) => void;
|
||||
addAgentNode: (position: { x: number; y: number }) => void;
|
||||
updateNodeData: (nodeId: string, data: Partial<AgentNodeData>) => void;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
setCouncilName: (name: string) => void;
|
||||
setNodes: (nodes: Node<AgentNodeData>[]) => void;
|
||||
setEdges: (edges: Edge[]) => void;
|
||||
|
||||
// Run actions
|
||||
setActiveRun: (run: CouncilRun | null) => void;
|
||||
setActiveNodeId: (nodeId: string | null) => void;
|
||||
markNodeActive: (nodeName: string) => void;
|
||||
clearActiveNode: () => void;
|
||||
}
|
||||
|
||||
function makeDefaultNodeData(label: string): AgentNodeData {
|
||||
return {
|
||||
label,
|
||||
systemPrompt: "",
|
||||
model: "claude-3-5-sonnet",
|
||||
tools: { webSearch: false, pdfReader: false },
|
||||
isActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
export const useCouncilStore = create<CouncilStore>((set, get) => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
selectedNodeId: null,
|
||||
councilName: "Mein Rat",
|
||||
activeRun: null,
|
||||
activeNodeId: null,
|
||||
|
||||
onNodesChange: (changes) =>
|
||||
set((state) => ({
|
||||
nodes: applyNodeChanges(changes, state.nodes) as Node<AgentNodeData>[],
|
||||
})),
|
||||
|
||||
onEdgesChange: (changes) =>
|
||||
set((state) => ({
|
||||
edges: applyEdgeChanges(changes, state.edges),
|
||||
})),
|
||||
|
||||
onConnect: (connection) =>
|
||||
set((state) => ({
|
||||
edges: addEdge(
|
||||
{ ...connection, type: "default", data: { type: "linear" } },
|
||||
state.edges
|
||||
),
|
||||
})),
|
||||
|
||||
addAgentNode: (position) => {
|
||||
const id = nanoid();
|
||||
const count = get().nodes.length + 1;
|
||||
const newNode: Node<AgentNodeData> = {
|
||||
id,
|
||||
type: "agentNode",
|
||||
position,
|
||||
data: makeDefaultNodeData(`Agent ${count}`),
|
||||
};
|
||||
set((state) => ({ nodes: [...state.nodes, newNode] }));
|
||||
},
|
||||
|
||||
updateNodeData: (nodeId, data) =>
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
|
||||
),
|
||||
})),
|
||||
|
||||
selectNode: (nodeId) => set({ selectedNodeId: nodeId }),
|
||||
|
||||
setCouncilName: (name) => set({ councilName: name }),
|
||||
|
||||
setNodes: (nodes) => set({ nodes }),
|
||||
|
||||
setEdges: (edges) => set({ edges }),
|
||||
|
||||
setActiveRun: (run) => set({ activeRun: run }),
|
||||
|
||||
setActiveNodeId: (nodeId) => set({ activeNodeId: nodeId }),
|
||||
|
||||
markNodeActive: (nodeName) => {
|
||||
const node = get().nodes.find((n) => n.data.label === nodeName);
|
||||
if (node) {
|
||||
set((state) => ({
|
||||
activeNodeId: node.id,
|
||||
nodes: state.nodes.map((n) => ({
|
||||
...n,
|
||||
data: { ...n.data, isActive: n.id === node.id },
|
||||
})),
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
clearActiveNode: () =>
|
||||
set((state) => ({
|
||||
activeNodeId: null,
|
||||
nodes: state.nodes.map((n) => ({
|
||||
...n,
|
||||
data: { ...n.data, isActive: false },
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue