mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
feat: add NodePalette and PipelineCanvas components for enhanced flow node management
- Introduced NodePalette component to display draggable nodes categorized by type. - Implemented PipelineCanvas component to manage the flow of nodes and connections. - Integrated drag-and-drop functionality for adding nodes to the canvas. - Created a catalog of node types with associated metadata for rendering in the palette. - Established conversion functions between pipeline definitions and React Flow state. - Added inspector for editing node properties and managing connections. - Updated Next.js configuration for production and development environments. - Removed obsolete embed.go file and added nginx configuration for serving the app. - Updated package dependencies including @xyflow/react for enhanced functionality. - Added TypeScript definitions for CSS imports to support styling in components.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { lookup } from "@/lib/node-catalog";
|
||||
import type { PipelineNode } from "@/lib/pipeline-graph";
|
||||
|
||||
interface InspectorProps {
|
||||
node: PipelineNode | null;
|
||||
onChange: (next: PipelineNode) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function Inspector({ node, onChange, onDelete, onClose }: InspectorProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [paramsText, setParamsText] = useState("{}");
|
||||
const [paramsErr, setParamsErr] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
setName(node.name);
|
||||
setParamsText(JSON.stringify(node.parameters ?? {}, null, 2));
|
||||
setParamsErr(null);
|
||||
}, [node?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (!node) {
|
||||
return (
|
||||
<aside className="w-80 shrink-0 border-l bg-muted/20 p-4 text-sm text-muted-foreground">
|
||||
Select a node to edit its parameters.
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
const entry = lookup(node.type);
|
||||
|
||||
function commitName(next: string) {
|
||||
if (!node) return;
|
||||
onChange({ ...node, name: next });
|
||||
}
|
||||
function commitParams(text: string) {
|
||||
if (!node) return;
|
||||
try {
|
||||
const parsed = JSON.parse(text || "{}");
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error("parameters must be a JSON object");
|
||||
}
|
||||
setParamsErr(null);
|
||||
onChange({ ...node, parameters: parsed as Record<string, unknown> });
|
||||
} catch (e) {
|
||||
setParamsErr(e instanceof Error ? e.message : "invalid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="flex w-80 shrink-0 flex-col border-l bg-muted/20">
|
||||
<div className="flex items-center justify-between gap-2 border-b p-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold">{node.name}</div>
|
||||
<div className="truncate text-[11px] text-muted-foreground">{node.type}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant={entry ? "secondary" : "destructive"}>
|
||||
{entry ? "supported" : "unsupported"}
|
||||
</Badge>
|
||||
<Button size="icon" variant="ghost" onClick={onClose} aria-label="Close">
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 overflow-y-auto p-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="node-name">Name</Label>
|
||||
<Input
|
||||
id="node-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={() => name !== node.name && commitName(name)}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Names are used as references in <code>{`{{ $('Name').json.x }}`}</code>{" "}
|
||||
expressions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="node-params">Parameters (JSON)</Label>
|
||||
<Textarea
|
||||
id="node-params"
|
||||
value={paramsText}
|
||||
onChange={(e) => setParamsText(e.target.value)}
|
||||
onBlur={() => commitParams(paramsText)}
|
||||
rows={14}
|
||||
spellCheck={false}
|
||||
className="text-xs"
|
||||
/>
|
||||
{paramsErr && (
|
||||
<p className="text-[11px] text-destructive">{paramsErr}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{entry && (
|
||||
<div className="rounded-md border bg-background p-2 text-[11px] text-muted-foreground">
|
||||
{entry.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full text-destructive hover:bg-destructive/10"
|
||||
onClick={() => onDelete(node.id)}
|
||||
>
|
||||
<Trash2 />
|
||||
Delete node
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user