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 ( +