- 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
113 lines
3.4 KiB
TypeScript
113 lines
3.4 KiB
TypeScript
"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<string, string> = {
|
|
"claude-3-5-sonnet": "Claude 3.5",
|
|
"gpt-4o": "GPT-4o",
|
|
local: "Lokal",
|
|
};
|
|
|
|
const MODEL_COLORS: Record<string, string> = {
|
|
"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 (
|
|
<div
|
|
onClick={() => 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 */}
|
|
<div
|
|
className={[
|
|
"flex items-center gap-2 rounded-t-xl px-3 py-2",
|
|
isActive ? "bg-indigo-50" : "bg-slate-50",
|
|
].join(" ")}
|
|
>
|
|
<Bot
|
|
size={16}
|
|
className={isActive ? "text-indigo-600" : "text-slate-500"}
|
|
/>
|
|
<span className="font-semibold text-sm text-slate-800 truncate flex-1">
|
|
{nodeData.label}
|
|
</span>
|
|
{isActive && (
|
|
<span className="text-xs text-indigo-600 font-medium">aktiv</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="px-3 py-2 space-y-2">
|
|
{/* System prompt preview */}
|
|
<p className="text-xs text-slate-500 line-clamp-2 min-h-[2rem]">
|
|
{nodeData.systemPrompt || (
|
|
<span className="italic text-slate-300">Kein System-Prompt</span>
|
|
)}
|
|
</p>
|
|
|
|
{/* Model badge */}
|
|
<span
|
|
className={[
|
|
"inline-block text-xs px-2 py-0.5 rounded-full border font-medium",
|
|
MODEL_COLORS[nodeData.model] ?? MODEL_COLORS["local"],
|
|
].join(" ")}
|
|
>
|
|
{MODEL_LABELS[nodeData.model] ?? nodeData.model}
|
|
</span>
|
|
|
|
{/* Tool toggles */}
|
|
{(nodeData.tools.webSearch || nodeData.tools.pdfReader) && (
|
|
<div className="flex gap-2">
|
|
{nodeData.tools.webSearch && (
|
|
<span className="flex items-center gap-1 text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full">
|
|
<Globe size={10} />
|
|
Web
|
|
</span>
|
|
)}
|
|
{nodeData.tools.pdfReader && (
|
|
<span className="flex items-center gap-1 text-xs text-purple-600 bg-purple-50 px-2 py-0.5 rounded-full">
|
|
<FileText size={10} />
|
|
PDF
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Handles */}
|
|
<Handle
|
|
type="target"
|
|
position={Position.Left}
|
|
className="!w-3 !h-3 !bg-slate-400 !border-white !border-2"
|
|
/>
|
|
<Handle
|
|
type="source"
|
|
position={Position.Right}
|
|
className="!w-3 !h-3 !bg-indigo-500 !border-white !border-2"
|
|
/>
|
|
</div>
|
|
);
|
|
});
|