Files
patchy631--ai-engineering-hub/open-agent-builder/components/app/(home)/sections/workflow-builder/DataNodePanel.tsx.backup
T
2026-07-13 12:37:47 +08:00

753 lines
32 KiB
Plaintext

"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
import type { Node } from "@xyflow/react";
import { toast } from "sonner";
interface DataNodePanelProps {
node: Node | null;
onClose: () => void;
onDelete: (nodeId: string) => void;
onUpdate: (nodeId: string, data: any) => void;
}
export default function DataNodePanel({
node,
onClose,
onDelete,
onUpdate,
}: DataNodePanelProps) {
const nodeData = node?.data as any;
const nodeType = nodeData?.nodeType?.toLowerCase() || "";
// Transform state
const [transformScript, setTransformScript] = useState(
nodeData?.transformScript ||
`// Transform the input data
return {
...input,
processed: true,
timestamp: new Date().toISOString()
};`,
);
// Transform mode (Expressions vs Object vs Advanced)
const [transformMode, setTransformMode] = useState<
"expressions" | "object" | "advanced"
>(nodeData?.transformMode || "expressions");
const [expressionPairs, setExpressionPairs] = useState<
Array<{ key: string; value: string }>
>(nodeData?.expressionPairs || [{ key: "", value: "" }]);
// JSON Schema Builder state (for Object mode)
const [schemaMode, setSchemaMode] = useState<"simple" | "advanced">(
nodeData?.schemaMode || "simple",
);
const [schemaName, setSchemaName] = useState(
nodeData?.schemaName || "blog_post_details",
);
const [schemaFields, setSchemaFields] = useState<
Array<{ name: string; type: string; value?: string; description?: string }>
>(
nodeData?.schemaFields || [
{
name: "title",
type: "STR",
value: "",
description: "The main heading of the blog post",
},
{
name: "author",
type: "STR",
value: "",
description: "The full name of the post's author",
},
{
name: "date_published",
type: "STR",
value: "",
description: "Date the post was published (YYYY-MM-DD)",
},
{
name: "content",
type: "STR",
value: "",
description: "The full content/body of the blog post",
},
{
name: "tags",
type: "ARR",
value: "",
description: "A list of tag strings associated with the blog post",
},
],
);
const [isGenerating, setIsGenerating] = useState(false);
const [generatedSchema, setGeneratedSchema] = useState("");
// Set State variables
const [stateKey, setStateKey] = useState(nodeData?.stateKey || "myVariable");
const [stateValue, setStateValue] = useState(nodeData?.stateValue || "value");
const [valueType, setValueType] = useState<
"string" | "number" | "boolean" | "json" | "expression"
>(nodeData?.valueType || "string");
// Auto-save changes
useEffect(() => {
if (!node?.id) return;
const timeoutId = setTimeout(() => {
onUpdate(node.id, {
transformScript,
transformMode,
expressionPairs,
stateKey,
stateValue,
valueType,
schemaName,
schemaFields,
schemaMode,
});
}, 500);
return () => clearTimeout(timeoutId);
}, [
transformScript,
transformMode,
expressionPairs,
stateKey,
stateValue,
valueType,
schemaName,
schemaFields,
schemaMode,
]);
// Generate schema using AI
const handleGenerateSchema = async () => {
setIsGenerating(true);
try {
// Build schema from fields
const properties: any = {};
schemaFields.forEach((field) => {
if (field.name) {
const typeMap: any = {
STR: "string",
NUM: "number",
BOOL: "boolean",
ARR: "array",
OBJ: "object",
};
const fieldDef: any = {
type: typeMap[field.type] || "string",
};
if (field.description) {
fieldDef.description = field.description;
}
if (field.value) {
fieldDef.default = field.value;
}
// Handle array type
if (field.type === "ARR") {
fieldDef.items = { type: "string" };
}
properties[field.name] = fieldDef;
}
});
const schema = {
type: "object",
properties,
additionalProperties: false,
required: schemaFields.filter((f) => f.name).map((f) => f.name),
};
const schemaJson = JSON.stringify(schema, null, 2);
setGeneratedSchema(schemaJson);
// Update transform script to use this schema
const newScript = `// Extract data matching the schema
const result = ${schemaJson};
// TODO: Map input data to schema fields
return {
${schemaFields.map((f) => ` ${f.name}: input.${f.name} || ${f.value ? `"${f.value}"` : "null"}`).join(",\n")}
};`;
setTransformScript(newScript);
toast.success("Schema generated!", {
description: "Transform script updated with schema structure",
});
} catch (error) {
toast.error("Failed to generate schema", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsGenerating(false);
}
};
const renderValueInput = () => {
switch (valueType) {
case "boolean":
return (
<select
value={stateValue}
onChange={(e) => setStateValue(e.target.value)}
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
>
<option value="true">true</option>
<option value="false">false</option>
</select>
);
case "number":
return (
<input
type="text"
value={stateValue}
onChange={(e) => setStateValue(e.target.value)}
placeholder="42 or {{lastOutput.count}}"
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
/>
);
case "json":
return (
<textarea
value={stateValue}
onChange={(e) => setStateValue(e.target.value)}
rows={4}
placeholder='{"key": "value"} or {{lastOutput}}'
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
/>
);
case "expression":
return (
<textarea
value={stateValue}
onChange={(e) => setStateValue(e.target.value)}
rows={3}
placeholder="input.price * 1.1"
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
/>
);
default:
return (
<input
type="text"
value={stateValue}
onChange={(e) => setStateValue(e.target.value)}
placeholder="Hello {{input.name}}"
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
/>
);
}
};
return (
<AnimatePresence>
{node && (
<motion.aside
initial={{ x: 400, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 400, opacity: 0 }}
transition={{ duration: 0.3 }}
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
>
{/* Header */}
<div className="p-20 border-b border-border-faint">
<div className="flex items-center justify-between mb-8">
<h2 className="text-label-large text-accent-black font-medium">
{nodeData?.nodeName || "Data"}
</h2>
<div className="flex items-center gap-8">
<button
onClick={() => onDelete(node?.id || "")}
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
title="Delete node"
>
<svg
className="w-16 h-16 text-black-alpha-48 group-hover:text-black-alpha-64"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
<button
onClick={onClose}
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
>
<svg
className="w-16 h-16 text-black-alpha-48"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<p className="text-body-small text-black-alpha-48">
{nodeType.includes("transform")
? "Transform data using JavaScript"
: nodeType.includes("state")
? "Set workflow state variables"
: "Configure data operation"}
</p>
</div>
{/* Form Fields */}
<div className="p-20 space-y-24">
{/* Transform Node - Multiple Modes */}
{nodeType.includes("transform") && (
<>
{/* Mode Selector */}
<div className="flex gap-4 mb-16">
<button
onClick={() => setTransformMode("expressions")}
className={`flex-1 px-12 py-8 rounded-8 text-sm font-medium transition-colors ${
transformMode === "expressions"
? "bg-accent-black text-white"
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
}`}
>
Expressions
</button>
<button
onClick={() => setTransformMode("object")}
className={`flex-1 px-12 py-8 rounded-8 text-sm font-medium transition-colors ${
transformMode === "object"
? "bg-accent-black text-white"
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
}`}
>
Object
</button>
</div>
<p className="text-sm text-black-alpha-48 mb-16">
{transformMode === "expressions"
? "Reshape data using key-value expressions"
: "Build a JSON object schema"}
</p>
{transformMode === "expressions" ? (
<>
{/* Expression Mode - Key/Value Pairs */}
<div className="space-y-10">
{expressionPairs.map((pair, index) => (
<div key={index} className="flex items-center gap-8">
<input
type="text"
value={pair.key}
onChange={(e) => {
const updated = [...expressionPairs];
updated[index].key = e.target.value;
setExpressionPairs(updated);
}}
placeholder="Key"
className="flex-1 px-12 py-8 bg-background-base border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
/>
<input
type="text"
value={pair.value}
onChange={(e) => {
const updated = [...expressionPairs];
updated[index].value = e.target.value;
setExpressionPairs(updated);
}}
placeholder="input.foo + 1"
className="flex-1 px-12 py-8 bg-background-base border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
/>
<button
onClick={() =>
setExpressionPairs(
expressionPairs.filter((_, i) => i !== index),
)
}
className="w-28 h-28 rounded-6 hover:bg-black-alpha-4 flex items-center justify-center"
>
<svg
className="w-14 h-14 text-black-alpha-48"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
))}
<button
onClick={() =>
setExpressionPairs([
...expressionPairs,
{ key: "", value: "" },
])
}
className="w-full px-12 py-8 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-sm text-accent-black transition-colors flex items-center justify-center gap-6"
>
<svg
className="w-14 h-14"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add
</button>
<p className="text-xs text-black-alpha-48 mt-8">
Use Common Expression Language.{" "}
<a href="#" className="text-heat-100">
Learn more
</a>
</p>
</div>
</>
) : transformMode === "object" ? (
<>
{/* Object Mode - JSON Schema Builder */}
<div>
<div className="flex items-center justify-between mb-12">
<h3 className="text-sm font-medium text-accent-black">
Structured output (JSON)
</h3>
<div className="flex gap-4">
<button
onClick={() => setSchemaMode("simple")}
className={`px-12 py-6 rounded-8 text-xs font-medium transition-colors ${
schemaMode === "simple"
? "bg-accent-black text-white"
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
}`}
>
Simple
</button>
<button
onClick={() => setSchemaMode("advanced")}
className={`px-12 py-6 rounded-8 text-xs font-medium transition-colors ${
schemaMode === "advanced"
? "bg-accent-black text-white"
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
}`}
>
Advanced
</button>
</div>
</div>
<p className="text-sm text-black-alpha-48 mb-16">
The model will generate a JSON object that matches this
schema.
</p>
{schemaMode === "simple" ? (
<>
{/* Schema Name */}
<div className="mb-16">
<label className="block text-sm font-medium text-accent-black mb-8">
Name
</label>
<input
type="text"
value={schemaName}
onChange={(e) => setSchemaName(e.target.value)}
className="w-full px-14 py-10 bg-background-base border border-border-faint rounded-10 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
/>
</div>
{/* Properties */}
<div className="mb-16">
<div className="grid grid-cols-[100px_80px_1fr_80px_40px] gap-8 mb-10 text-xs text-black-alpha-48 font-medium">
<div>Name</div>
<div>Type</div>
<div>Value</div>
<div></div>
<div></div>
</div>
<div className="space-y-8">
{schemaFields.map((field, index) => (
<div
key={index}
className="grid grid-cols-[100px_80px_1fr_80px_40px] gap-8 items-center"
>
<svg
className="w-16 h-16 text-teal-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<input
type="text"
value={field.name}
onChange={(e) => {
const updated = [...schemaFields];
updated[index].name = e.target.value;
setSchemaFields(updated);
}}
className="px-10 py-6 bg-background-base border border-border-faint rounded-6 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
/>
<select
value={field.type}
onChange={(e) => {
const updated = [...schemaFields];
updated[index].type = e.target.value;
setSchemaFields(updated);
}}
className="px-8 py-6 bg-accent-white border border-border-faint rounded-6 text-xs text-accent-black focus:outline-none focus:border-heat-100"
>
<option value="STR">STR</option>
<option value="NUM">NUM</option>
<option value="BOOL">BOOL</option>
<option value="ARR">ARR</option>
<option value="OBJ">OBJ</option>
</select>
<input
type="text"
value={field.value || ""}
onChange={(e) => {
const updated = [...schemaFields];
updated[index].value = e.target.value;
setSchemaFields(updated);
}}
placeholder="Enter value..."
className="px-10 py-6 bg-background-base border border-border-faint rounded-6 text-sm text-accent-black focus:outline-none focus:border-heat-100"
/>
<button className="px-10 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-xs text-accent-black">
Enter
</button>
<button
onClick={() => {
setSchemaFields(
schemaFields.filter(
(_, i) => i !== index,
),
);
}}
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 flex items-center justify-center"
>
<svg
className="w-16 h-16 text-black-alpha-48"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
))}
</div>
{/* Add Field Button */}
<button
onClick={() => {
setSchemaFields([
...schemaFields,
{ name: "", type: "STR", value: "" },
]);
}}
className="mt-12 px-16 py-8 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-sm text-accent-black transition-colors flex items-center gap-6"
>
<svg
className="w-14 h-14"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add
</button>
</div>
{/* Generate Button */}
<button
onClick={handleGenerateSchema}
disabled={isGenerating}
className="w-full px-20 py-12 bg-heat-100 hover:bg-heat-200 text-white rounded-10 text-sm font-medium transition-all active:scale-[0.98] flex items-center justify-center gap-8 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isGenerating ? (
<>
<svg
className="w-16 h-16 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Generating...
</>
) : (
<>
<svg
className="w-16 h-16"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
Generate
</>
)}
</button>
</>
) : null}
</div>
</>
) : null}
</>
)}
{/* Set State Node - Separate from Transform */}
{nodeType.includes("state") && !nodeType.includes("transform") && (
<>
<div>
<h3 className="text-sm font-medium text-accent-black mb-12">
Set global variables
</h3>
<p className="text-sm text-black-alpha-48 mb-16">
Assign values to workflow's state variables
</p>
{/* State Assignments */}
<div className="space-y-12">
<div className="p-12 bg-background-base rounded-10 border border-border-faint">
<div className="space-y-12">
{/* Variable Name */}
<div>
<label className="block text-sm text-accent-black mb-6">
Variable Name
</label>
<input
type="text"
value={stateKey}
onChange={(e) => setStateKey(e.target.value)}
placeholder="myVariable"
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
/>
<p className="text-xs text-black-alpha-48 mt-4">
Access later with <code className="px-4 py-1 bg-background-base rounded text-heat-100 font-mono text-xs">{`{{state.${stateKey}}}`}</code>
</p>
</div>
{/* Value Type */}
<div>
<label className="block text-sm text-accent-black mb-6">
Value Type
</label>
<select
value={valueType}
onChange={(e) => setValueType(e.target.value as any)}
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors appearance-none cursor-pointer"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="json">JSON Object</option>
<option value="expression">JavaScript Expression</option>
</select>
</div>
{/* Value Input */}
<div>
<label className="block text-sm text-accent-black mb-6">
Value
</label>
{renderValueInput()}
<p className="text-xs text-black-alpha-48 mt-4">
{valueType === 'string' && 'Use {{variables}} to reference other data'}
{valueType === 'number' && 'Can use {{lastOutput.price}} to reference numbers'}
{valueType === 'boolean' && 'true or false'}
{valueType === 'json' && 'Valid JSON object or array'}
{valueType === 'expression' && 'JavaScript expression like: input.x + lastOutput.y'}
</p>
</div>
</div>
</div>
{/* Add Assignment Button */}
<button className="px-12 py-8 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-sm text-accent-black transition-colors flex items-center gap-6">
<svg
className="w-14 h-14"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add
</button>
</div>
</div>
</>
)}
</div>
</motion.aside>
)}
</AnimatePresence>
);
}