From 216fdd95892d962da3b4dcd75148a795d74823bf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 17:03:32 +0000 Subject: [PATCH] Implement Phase 2: Next.js + React Flow frontend MVP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/.gitignore | 41 + frontend/README.md | 36 + frontend/app/components/ArchitectCanvas.tsx | 87 + frontend/app/components/NavTabs.tsx | 38 + .../app/components/edges/ConditionalEdge.tsx | 61 + frontend/app/components/nodes/AgentNode.tsx | 113 + .../components/panels/NodeSettingsPanel.tsx | 135 + .../app/components/panels/NodeSidebar.tsx | 62 + frontend/app/favicon.ico | Bin 0 -> 25931 bytes frontend/app/globals.css | 26 + frontend/app/hooks/useCouncilWebSocket.ts | 74 + frontend/app/konferenzzimmer/page.tsx | 143 + frontend/app/layout.tsx | 22 + frontend/app/page.tsx | 5 + frontend/app/rat-architekt/page.tsx | 81 + frontend/app/store/council-store.ts | 125 + frontend/app/types/council.ts | 73 + frontend/app/utils/api-client.ts | 56 + frontend/app/utils/blueprint-parser.ts | 71 + frontend/eslint.config.mjs | 18 + frontend/next.config.ts | 7 + frontend/package-lock.json | 6887 +++++++++++++++++ frontend/package.json | 30 + frontend/postcss.config.mjs | 7 + frontend/public/file.svg | 1 + frontend/public/globe.svg | 1 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/public/window.svg | 1 + frontend/tsconfig.json | 34 + 30 files changed, 8237 insertions(+) create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/app/components/ArchitectCanvas.tsx create mode 100644 frontend/app/components/NavTabs.tsx create mode 100644 frontend/app/components/edges/ConditionalEdge.tsx create mode 100644 frontend/app/components/nodes/AgentNode.tsx create mode 100644 frontend/app/components/panels/NodeSettingsPanel.tsx create mode 100644 frontend/app/components/panels/NodeSidebar.tsx create mode 100644 frontend/app/favicon.ico create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/hooks/useCouncilWebSocket.ts create mode 100644 frontend/app/konferenzzimmer/page.tsx create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/app/rat-architekt/page.tsx create mode 100644 frontend/app/store/council-store.ts create mode 100644 frontend/app/types/council.ts create mode 100644 frontend/app/utils/api-client.ts create mode 100644 frontend/app/utils/blueprint-parser.ts create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/next.config.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/file.svg create mode 100644 frontend/public/globe.svg create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/public/window.svg create mode 100644 frontend/tsconfig.json diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/app/components/ArchitectCanvas.tsx b/frontend/app/components/ArchitectCanvas.tsx new file mode 100644 index 0000000..acfbb60 --- /dev/null +++ b/frontend/app/components/ArchitectCanvas.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useCallback } from "react"; +import { + ReactFlow, + Background, + Controls, + MiniMap, + BackgroundVariant, + useReactFlow, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; + +import { AgentNode } from "@/app/components/nodes/AgentNode"; +import { ConditionalEdge } from "@/app/components/edges/ConditionalEdge"; +import { useCouncilStore } from "@/app/store/council-store"; +import { AgentNodeData } from "@/app/types/council"; + +const NODE_TYPES = { agentNode: AgentNode }; +const EDGE_TYPES = { conditionalEdge: ConditionalEdge }; + +// Main React Flow canvas — lives inside a ReactFlowProvider +export function ArchitectCanvas() { + const nodes = useCouncilStore((s) => s.nodes); + const edges = useCouncilStore((s) => s.edges); + const onNodesChange = useCouncilStore((s) => s.onNodesChange); + const onEdgesChange = useCouncilStore((s) => s.onEdgesChange); + const onConnect = useCouncilStore((s) => s.onConnect); + const addAgentNode = useCouncilStore((s) => s.addAgentNode); + const selectNode = useCouncilStore((s) => s.selectNode); + + const { screenToFlowPosition } = useReactFlow(); + + const onDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }, []); + + const onDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + const type = event.dataTransfer.getData("application/reactflow"); + if (type !== "agentNode") return; + + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + addAgentNode(position); + }, + [screenToFlowPosition, addAgentNode] + ); + + const onPaneClick = useCallback(() => { + selectNode(null); + }, [selectNode]); + + return ( +
+ + + + { + const d = n.data as AgentNodeData; + return d?.isActive ? "#6366f1" : "#94a3b8"; + }} + className="!bg-white !border-slate-200" + /> + +
+ ); +} diff --git a/frontend/app/components/NavTabs.tsx b/frontend/app/components/NavTabs.tsx new file mode 100644 index 0000000..d33ff03 --- /dev/null +++ b/frontend/app/components/NavTabs.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Network, MessagesSquare } from "lucide-react"; + +const TABS = [ + { href: "/rat-architekt", label: "Rat-Architekt", icon: Network }, + { href: "/konferenzzimmer", label: "Konferenzzimmer", icon: MessagesSquare }, +]; + +export function NavTabs() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/frontend/app/components/edges/ConditionalEdge.tsx b/frontend/app/components/edges/ConditionalEdge.tsx new file mode 100644 index 0000000..42cbd33 --- /dev/null +++ b/frontend/app/components/edges/ConditionalEdge.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { memo } from "react"; +import { + EdgeProps, + BaseEdge, + EdgeLabelRenderer, + getSmoothStepPath, +} from "@xyflow/react"; + +export const ConditionalEdge = memo(function ConditionalEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + markerEnd, +}: EdgeProps) { + const [edgePath, labelX, labelY] = getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + const condition = data?.condition as string | undefined; + + return ( + <> + + {condition && ( + +
+ {condition} +
+
+ )} + + ); +}); diff --git a/frontend/app/components/nodes/AgentNode.tsx b/frontend/app/components/nodes/AgentNode.tsx new file mode 100644 index 0000000..5fa887c --- /dev/null +++ b/frontend/app/components/nodes/AgentNode.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { memo } from "react"; +import { Handle, Position, NodeProps } from "@xyflow/react"; +import { Bot, Globe, FileText } from "lucide-react"; +import { AgentNodeData } from "@/app/types/council"; +import { useCouncilStore } from "@/app/store/council-store"; + +const MODEL_LABELS: Record = { + "claude-3-5-sonnet": "Claude 3.5", + "gpt-4o": "GPT-4o", + local: "Lokal", +}; + +const MODEL_COLORS: Record = { + "claude-3-5-sonnet": "bg-orange-100 text-orange-700 border-orange-300", + "gpt-4o": "bg-green-100 text-green-700 border-green-300", + local: "bg-gray-100 text-gray-700 border-gray-300", +}; + +export const AgentNode = memo(function AgentNode({ + id, + data, + selected, +}: NodeProps) { + const nodeData = data as AgentNodeData; + const selectNode = useCouncilStore((s) => s.selectNode); + + const isActive = nodeData.isActive; + + return ( +
selectNode(id)} + className={[ + "w-52 rounded-xl border-2 bg-white shadow-md transition-all duration-300 cursor-pointer", + isActive + ? "border-indigo-500 shadow-indigo-200 shadow-lg animate-pulse" + : selected + ? "border-indigo-400 shadow-indigo-100" + : "border-slate-200 hover:border-slate-400", + ].join(" ")} + > + {/* Header */} +
+ + + {nodeData.label} + + {isActive && ( + aktiv + )} +
+ + {/* Body */} +
+ {/* System prompt preview */} +

+ {nodeData.systemPrompt || ( + Kein System-Prompt + )} +

+ + {/* Model badge */} + + {MODEL_LABELS[nodeData.model] ?? nodeData.model} + + + {/* Tool toggles */} + {(nodeData.tools.webSearch || nodeData.tools.pdfReader) && ( +
+ {nodeData.tools.webSearch && ( + + + Web + + )} + {nodeData.tools.pdfReader && ( + + + PDF + + )} +
+ )} +
+ + {/* Handles */} + + +
+ ); +}); diff --git a/frontend/app/components/panels/NodeSettingsPanel.tsx b/frontend/app/components/panels/NodeSettingsPanel.tsx new file mode 100644 index 0000000..ee71fb5 --- /dev/null +++ b/frontend/app/components/panels/NodeSettingsPanel.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { X, Bot } from "lucide-react"; +import { AgentNodeData, LLMModel } from "@/app/types/council"; +import { useCouncilStore } from "@/app/store/council-store"; + +const MODELS: { value: LLMModel; label: string }[] = [ + { value: "claude-3-5-sonnet", label: "Claude 3.5 Sonnet" }, + { value: "gpt-4o", label: "GPT-4o" }, + { value: "local", label: "Lokal" }, +]; + +// Right-side panel shown when an AgentNode is selected +export function NodeSettingsPanel() { + const selectedNodeId = useCouncilStore((s) => s.selectedNodeId); + const nodes = useCouncilStore((s) => s.nodes); + const updateNodeData = useCouncilStore((s) => s.updateNodeData); + const selectNode = useCouncilStore((s) => s.selectNode); + + const node = nodes.find((n) => n.id === selectedNodeId); + const data = node?.data as AgentNodeData | undefined; + + // Local draft to avoid re-renders on every keystroke + const [draft, setDraft] = useState(null); + + useEffect(() => { + setDraft(data ?? null); + }, [selectedNodeId, data]); + + if (!selectedNodeId || !draft) return null; + + const commit = (partial: Partial) => { + const updated = { ...draft, ...partial }; + setDraft(updated); + updateNodeData(selectedNodeId, partial); + }; + + return ( +