Implement agent node functionality in canvas, including connection policies and UI updates. Add support for agent node type in node catalog, templates, and connection validation. Update documentation to reflect new agent capabilities and ensure proper handling of input sources. Enhance adjustment preview to include crop node. Add tests for agent connection policies.
This commit is contained in:
@@ -53,7 +53,7 @@ Alle verfügbaren Node-Typen sind in `lib/canvas-node-catalog.ts` definiert:
|
||||
| **ai-output** (KI-Ausgabe) | `prompt`, `video-prompt`, `ai-text`, `ai-video`, `agent-output` | KI-generierte Inhalte |
|
||||
| **transform** (Transformation) | `crop`, `bg-remove`, `upscale` | Bildbearbeitung-Transformationen |
|
||||
| **image-edit** (Bildbearbeitung) | `curves`, `color-adjust`, `light-adjust`, `detail-adjust` | Preset-basierte Adjustments |
|
||||
| **control** (Steuerung & Flow) | `condition`, `loop`, `parallel`, `switch` | Kontrollfluss-Elemente |
|
||||
| **control** (Steuerung & Flow) | `condition`, `loop`, `parallel`, `switch`, `agent` | Kontrollfluss-Elemente |
|
||||
| **layout** (Canvas & Layout) | `group`, `frame`, `note`, `compare` | Layout-Elemente |
|
||||
|
||||
### Node-Typen im Detail
|
||||
@@ -68,6 +68,7 @@ Alle verfügbaren Node-Typen sind in `lib/canvas-node-catalog.ts` definiert:
|
||||
| `video-prompt` | 2 | ✅ | ai-output | source: `video-prompt-out`, target: `video-prompt-in` |
|
||||
| `ai-text` | 2 | 🔲 | ai-output | source: `text-out`, target: `text-in` |
|
||||
| `ai-video` | 2 | ✅ (systemOutput) | ai-output | source: `video-out`, target: `video-in` |
|
||||
| `agent` | 2 | ✅ | control | target: `agent-in` (input-only MVP) |
|
||||
| `agent-output` | 3 | 🔲 | ai-output | systemOutput: true |
|
||||
| `crop` | 2 | 🔲 | transform | 🔲 |
|
||||
| `bg-remove` | 2 | 🔲 | transform | 🔲 |
|
||||
@@ -115,6 +116,7 @@ Zweistufiger Node-Flow analog `prompt → ai-image`:
|
||||
image: 280 × 200 prompt: 288 × 220
|
||||
text: 256 × 120 ai-image: 320 × 408
|
||||
video-prompt: 288 × 220 ai-video: 360 × 280
|
||||
agent: 360 × 320
|
||||
group: 400 × 300 frame: 400 × 300
|
||||
note: 208 × 100 compare: 500 × 380
|
||||
```
|
||||
@@ -213,6 +215,7 @@ Im **Light Mode** wird der eigentliche Edge-`stroke` ebenfalls aus dieser Akzent
|
||||
| `ai-image-node.tsx` | KI-Bild-Output-Node mit Bildvorschau, Metadaten, Retry |
|
||||
| `video-prompt-node.tsx` | KI-Video-Steuer-Node mit Modell-/Dauer-Selector, Credit-Anzeige, Generate-Button |
|
||||
| `ai-video-node.tsx` | KI-Video-Output-Node mit Video-Player, Metadaten, Retry-Button |
|
||||
| `agent-node.tsx` | Statischer Agent-Input-Node (Campaign Distributor) mit Kanal-/Input-/Output-Metadaten |
|
||||
|
||||
---
|
||||
|
||||
@@ -278,5 +281,6 @@ useCanvasData (use-canvas-data.ts)
|
||||
- **Optimistic IDs:** Temporäre Nodes/Edges erhalten IDs mit `optimistic_` / `optimistic_edge_`-Prefix, werden durch echte Convex-IDs ersetzt, sobald die Mutation abgeschlossen ist.
|
||||
- **Node-Taxonomie:** Alle Node-Typen sind in `lib/canvas-node-catalog.ts` definiert. Phase-2/3 Nodes haben `implemented: false` und `disabledHint`.
|
||||
- **Video-Connection-Policy:** `video-prompt` darf **nur** mit `ai-video` verbunden werden (und umgekehrt). `text → video-prompt` ist erlaubt (Prompt-Quelle). `ai-video → compare` ist erlaubt.
|
||||
- **Agent-MVP:** `agent` ist aktuell input-only (`agent-in`), ohne ausgehenden Handle. Er akzeptiert nur Content-/Kontext-Quellen (z. B. `render`, `compare`, `text`, `image`), keine Prompt-Steuerknoten.
|
||||
- **Convex Generated Types:** `api.ai.generateVideo` wird u. U. nicht in `convex/_generated/api.d.ts` exportiert. Der Code verwendet `api as unknown as {...}` als Workaround. Ein `npx convex dev`-Zyklus würde die Typen korrekt generieren.
|
||||
- **Canvas Graph Query:** Der Canvas nutzt `canvasGraph.get` (aus `convex/canvasGraph.ts`) statt separater `nodes.list`/`edges.list` Queries. Optimistic Updates laufen über `canvas-graph-query-cache.ts`.
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
Frame,
|
||||
Focus,
|
||||
GitCompare,
|
||||
Crop as CropIcon,
|
||||
Bot,
|
||||
ImageDown,
|
||||
Image,
|
||||
Package,
|
||||
@@ -28,12 +30,14 @@ const NODE_ICONS: Record<CanvasNodeTemplate["type"], LucideIcon> = {
|
||||
text: Type,
|
||||
prompt: Sparkles,
|
||||
"video-prompt": Video,
|
||||
agent: Bot,
|
||||
note: StickyNote,
|
||||
frame: Frame,
|
||||
compare: GitCompare,
|
||||
group: FolderOpen,
|
||||
asset: Package,
|
||||
video: Video,
|
||||
crop: CropIcon,
|
||||
curves: Sparkles,
|
||||
"color-adjust": Palette,
|
||||
"light-adjust": Sun,
|
||||
@@ -48,12 +52,14 @@ const NODE_SEARCH_KEYWORDS: Partial<
|
||||
text: ["text", "typo"],
|
||||
prompt: ["prompt", "ai", "generate", "ki-bild", "ki", "bild"],
|
||||
"video-prompt": ["video", "ai", "ki-video", "ki", "prompt"],
|
||||
agent: ["agent", "campaign", "distribution", "social"],
|
||||
note: ["note", "sticky", "notiz"],
|
||||
frame: ["frame", "artboard"],
|
||||
compare: ["compare", "before", "after", "vergleich"],
|
||||
group: ["group", "gruppe", "folder"],
|
||||
asset: ["asset", "freepik", "stock"],
|
||||
video: ["video", "pexels", "clip"],
|
||||
crop: ["crop", "resize", "ratio"],
|
||||
curves: ["curves", "tone", "contrast"],
|
||||
"color-adjust": ["color", "hue", "saturation"],
|
||||
"light-adjust": ["light", "exposure", "brightness"],
|
||||
|
||||
@@ -16,6 +16,7 @@ import LightAdjustNode from "./nodes/light-adjust-node";
|
||||
import DetailAdjustNode from "./nodes/detail-adjust-node";
|
||||
import RenderNode from "./nodes/render-node";
|
||||
import CropNode from "./nodes/crop-node";
|
||||
import AgentNode from "./nodes/agent-node";
|
||||
|
||||
/**
|
||||
* Node-Type-Map für React Flow.
|
||||
@@ -43,4 +44,5 @@ export const nodeTypes = {
|
||||
"detail-adjust": DetailAdjustNode,
|
||||
crop: CropNode,
|
||||
render: RenderNode,
|
||||
agent: AgentNode,
|
||||
} as const;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import type { PipelineStep } from "@/lib/image-pipeline/contracts";
|
||||
import { buildHistogramPlot } from "@/lib/image-pipeline/histogram-plot";
|
||||
|
||||
const PREVIEW_PIPELINE_TYPES = new Set(["curves", "color-adjust", "light-adjust", "detail-adjust"]);
|
||||
const PREVIEW_PIPELINE_TYPES = new Set(["crop", "curves", "color-adjust", "light-adjust", "detail-adjust"]);
|
||||
|
||||
export default function AdjustmentPreview({
|
||||
nodeId,
|
||||
|
||||
90
components/canvas/nodes/agent-node.tsx
Normal file
90
components/canvas/nodes/agent-node.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { Bot } from "lucide-react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
|
||||
import { getAgentTemplate } from "@/lib/agent-templates";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
|
||||
type AgentNodeData = {
|
||||
templateId?: string;
|
||||
canvasId?: string;
|
||||
_status?: string;
|
||||
_statusMessage?: string;
|
||||
};
|
||||
|
||||
type AgentNodeType = Node<AgentNodeData, "agent">;
|
||||
|
||||
const DEFAULT_AGENT_TEMPLATE_ID = "campaign-distributor";
|
||||
|
||||
function CompactList({ items }: { items: readonly string[] }) {
|
||||
return (
|
||||
<ul className="space-y-1">
|
||||
{items.slice(0, 4).map((item) => (
|
||||
<li key={item} className="truncate text-[11px] text-foreground/90" title={item}>
|
||||
- {item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AgentNode({ data, selected }: NodeProps<AgentNodeType>) {
|
||||
const nodeData = data as AgentNodeData;
|
||||
const template =
|
||||
getAgentTemplate(nodeData.templateId ?? DEFAULT_AGENT_TEMPLATE_ID) ??
|
||||
getAgentTemplate(DEFAULT_AGENT_TEMPLATE_ID);
|
||||
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseNodeWrapper
|
||||
nodeType="agent"
|
||||
selected={selected}
|
||||
status={nodeData._status}
|
||||
statusMessage={nodeData._statusMessage}
|
||||
className="min-w-[300px] border-amber-500/30"
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="agent-in"
|
||||
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
||||
/>
|
||||
|
||||
<div className="flex h-full flex-col gap-3 p-3">
|
||||
<header className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-amber-700 dark:text-amber-300">
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
<span>{template.emoji}</span>
|
||||
<span>{template.name}</span>
|
||||
</div>
|
||||
<p className="line-clamp-2 text-xs text-muted-foreground">{template.description}</p>
|
||||
</header>
|
||||
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Channels
|
||||
</p>
|
||||
<CompactList items={template.channels} />
|
||||
</section>
|
||||
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Expected Inputs
|
||||
</p>
|
||||
<CompactList items={template.expectedInputs} />
|
||||
</section>
|
||||
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Expected Outputs
|
||||
</p>
|
||||
<CompactList items={template.expectedOutputs} />
|
||||
</section>
|
||||
</div>
|
||||
</BaseNodeWrapper>
|
||||
);
|
||||
}
|
||||
@@ -45,6 +45,7 @@ const RESIZE_CONFIGS: Record<string, ResizeConfig> = {
|
||||
"detail-adjust": { minWidth: 300, minHeight: 820 },
|
||||
crop: { minWidth: 320, minHeight: 520 },
|
||||
render: { minWidth: 260, minHeight: 300, keepAspectRatio: true },
|
||||
agent: { minWidth: 300, minHeight: 280 },
|
||||
text: { minWidth: 220, minHeight: 90 },
|
||||
note: { minWidth: 200, minHeight: 90 },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user