inputs for workflows

This commit is contained in:
2026-02-25 08:34:38 -06:00
parent 91dfc52a1f
commit adb9f30464
3 changed files with 317 additions and 12 deletions

View File

@@ -1,5 +1,11 @@
import { useState, useMemo } from "react";
import { Search, X, ChevronDown, ChevronRight, GripVertical } from "lucide-react";
import {
Search,
X,
ChevronDown,
ChevronRight,
GripVertical,
} from "lucide-react";
import type { PaletteAction } from "@/types/workflow";
interface ActionPaletteProps {
@@ -24,7 +30,7 @@ export default function ActionPalette({
action.label?.toLowerCase().includes(query) ||
action.ref?.toLowerCase().includes(query) ||
action.description?.toLowerCase().includes(query) ||
action.pack_ref?.toLowerCase().includes(query)
action.pack_ref?.toLowerCase().includes(query),
);
}, [actions, searchQuery]);
@@ -38,7 +44,7 @@ export default function ActionPalette({
grouped.get(packRef)!.push(action);
});
return new Map(
[...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0]))
[...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0])),
);
}, [filteredActions]);
@@ -55,7 +61,7 @@ export default function ActionPalette({
};
return (
<div className="w-64 border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden">
<div className="flex flex-col h-full overflow-hidden">
<div className="p-3 border-b border-gray-200 bg-white flex-shrink-0">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider mb-2">
Action Palette
@@ -93,7 +99,9 @@ export default function ActionPalette({
</div>
) : filteredActions.length === 0 ? (
<div className="text-center py-8">
<p className="text-xs text-gray-500">No actions match your search</p>
<p className="text-xs text-gray-500">
No actions match your search
</p>
<button
onClick={() => setSearchQuery("")}
className="mt-1 text-xs text-blue-600 hover:text-blue-800"
@@ -158,7 +166,7 @@ export default function ActionPalette({
)}
</div>
);
}
},
)}
</div>
)}

View File

@@ -0,0 +1,246 @@
import { useState } from "react";
import { Pencil, Plus, X, LogIn, LogOut } from "lucide-react";
import SchemaBuilder from "@/components/common/SchemaBuilder";
import type { ParamDefinition } from "@/types/workflow";
interface WorkflowInputsPanelProps {
parameters: Record<string, ParamDefinition>;
output: Record<string, ParamDefinition>;
onParametersChange: (parameters: Record<string, ParamDefinition>) => void;
onOutputChange: (output: Record<string, ParamDefinition>) => void;
}
type ModalTarget = "parameters" | "output" | null;
function ParamSummaryList({
schema,
emptyMessage,
onEdit,
}: {
schema: Record<string, ParamDefinition>;
emptyMessage: string;
onEdit: () => void;
}) {
const entries = Object.entries(schema);
if (entries.length === 0) {
return (
<button
onClick={onEdit}
className="w-full px-3 py-4 border-2 border-dashed border-gray-200 rounded-lg text-center hover:border-blue-300 hover:bg-blue-50/30 transition-colors group"
>
<Plus className="w-4 h-4 text-gray-300 group-hover:text-blue-400 mx-auto mb-1" />
<span className="text-[11px] text-gray-400 group-hover:text-blue-500">
{emptyMessage}
</span>
</button>
);
}
return (
<div className="space-y-1">
{entries.map(([name, def]) => (
<div
key={name}
className="flex items-center gap-1.5 px-2 py-1.5 bg-white border border-gray-150 rounded-md"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="font-mono text-[11px] font-medium text-gray-800 truncate">
{name}
</span>
{def.required && (
<span className="text-[9px] font-semibold text-red-500">*</span>
)}
</div>
<div className="flex items-center gap-1.5">
<span className="text-[10px] text-blue-600/70">{def.type}</span>
{def.secret && (
<span className="text-[9px] text-amber-500">secret</span>
)}
{def.default !== undefined && (
<span
className="text-[9px] text-gray-400 truncate max-w-[80px]"
title={`default: ${JSON.stringify(def.default)}`}
>
= {JSON.stringify(def.default)}
</span>
)}
</div>
</div>
</div>
))}
<button
onClick={onEdit}
className="flex items-center gap-1 px-2 py-1 text-[11px] text-gray-400 hover:text-blue-600 transition-colors w-full"
>
<Pencil className="w-3 h-3" />
Edit
</button>
</div>
);
}
export default function WorkflowInputsPanel({
parameters,
output,
onParametersChange,
onOutputChange,
}: WorkflowInputsPanelProps) {
const [modalTarget, setModalTarget] = useState<ModalTarget>(null);
// Draft state for the modal so changes only apply on confirm
const [draftSchema, setDraftSchema] = useState<
Record<string, ParamDefinition>
>({});
const openModal = (target: ModalTarget) => {
if (target === "parameters") {
setDraftSchema({ ...parameters });
} else if (target === "output") {
setDraftSchema({ ...output });
}
setModalTarget(target);
};
const handleConfirm = () => {
if (modalTarget === "parameters") {
onParametersChange(draftSchema);
} else if (modalTarget === "output") {
onOutputChange(draftSchema);
}
setModalTarget(null);
};
const handleCancel = () => {
setModalTarget(null);
};
const modalLabel =
modalTarget === "parameters" ? "Input Parameters" : "Output";
return (
<>
<div className="flex flex-col h-full overflow-hidden">
<div className="flex-1 overflow-y-auto p-3 space-y-4">
{/* Input Parameters */}
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5">
<LogIn className="w-3.5 h-3.5 text-green-500" />
<h4 className="text-xs font-semibold text-gray-600 uppercase tracking-wider">
Inputs
</h4>
</div>
{Object.keys(parameters).length > 0 && (
<span className="text-[10px] text-gray-400">
{Object.keys(parameters).length}
</span>
)}
</div>
<p className="text-[10px] text-gray-400 mb-2">
Referenced via{" "}
<code className="px-0.5 bg-gray-100 rounded">
{"{{ params.<name> }}"}
</code>
</p>
<ParamSummaryList
schema={parameters}
emptyMessage="Add input parameters"
onEdit={() => openModal("parameters")}
/>
</div>
{/* Output Schema */}
<div className="border-t border-gray-200 pt-3">
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5">
<LogOut className="w-3.5 h-3.5 text-violet-500" />
<h4 className="text-xs font-semibold text-gray-600 uppercase tracking-wider">
Output
</h4>
</div>
{Object.keys(output).length > 0 && (
<span className="text-[10px] text-gray-400">
{Object.keys(output).length}
</span>
)}
</div>
<p className="text-[10px] text-gray-400 mb-2">
Values this workflow produces on completion.
</p>
<ParamSummaryList
schema={output}
emptyMessage="Add output schema"
onEdit={() => openModal("output")}
/>
</div>
</div>
</div>
{/* Full-screen modal for SchemaBuilder editing */}
{modalTarget && (
<div className="fixed inset-0 z-[70] flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40"
onClick={handleCancel}
/>
{/* Modal */}
<div className="relative bg-white rounded-xl shadow-2xl border border-gray-200 flex flex-col w-full max-w-2xl max-h-[80vh] mx-4">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 flex-shrink-0">
<div>
<h2 className="text-base font-semibold text-gray-900">
{modalLabel}
</h2>
<p className="text-xs text-gray-500 mt-0.5">
{modalTarget === "parameters"
? "Define the inputs this workflow accepts when executed."
: "Define the outputs this workflow produces upon completion."}
</p>
</div>
<button
onClick={handleCancel}
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-4">
<SchemaBuilder
value={draftSchema}
onChange={(schema) =>
setDraftSchema(schema as Record<string, ParamDefinition>)
}
placeholder={
modalTarget === "parameters"
? '{"message": {"type": "string", "required": true}}'
: '{"result": {"type": "string"}}'
}
/>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 px-6 py-3 border-t border-gray-200 bg-gray-50 rounded-b-xl flex-shrink-0">
<button
onClick={handleCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
onClick={handleConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"
>
Apply
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@@ -9,10 +9,13 @@ import {
Code,
LayoutDashboard,
X,
Zap,
Settings2,
} from "lucide-react";
import yaml from "js-yaml";
import type { WorkflowYamlDefinition } from "@/types/workflow";
import ActionPalette from "@/components/workflows/ActionPalette";
import WorkflowInputsPanel from "@/components/workflows/WorkflowInputsPanel";
import WorkflowCanvas from "@/components/workflows/WorkflowCanvas";
import type { EdgeHoverInfo } from "@/components/workflows/WorkflowEdges";
import TaskInspector from "@/components/workflows/TaskInspector";
@@ -83,6 +86,7 @@ export default function WorkflowBuilderPage() {
const [saveSuccess, setSaveSuccess] = useState(false);
const [initialized, setInitialized] = useState(false);
const [showYamlPreview, setShowYamlPreview] = useState(false);
const [sidebarTab, setSidebarTab] = useState<"actions" | "inputs">("actions");
const [highlightedTransition, setHighlightedTransition] = useState<{
taskId: string;
transitionIndex: number;
@@ -747,12 +751,59 @@ export default function WorkflowBuilderPage() {
</div>
) : (
<>
{/* Left: Action Palette */}
<ActionPalette
actions={paletteActions}
isLoading={actionsLoading}
onAddTask={handleAddTaskFromPalette}
/>
{/* Left sidebar: tabbed Actions / Inputs */}
<div className="w-64 border-r border-gray-200 bg-gray-50 flex flex-col h-full overflow-hidden">
{/* Tab header */}
<div className="flex border-b border-gray-200 bg-white flex-shrink-0">
<button
onClick={() => setSidebarTab("actions")}
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${
sidebarTab === "actions"
? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-50"
}`}
>
<Zap className="w-3.5 h-3.5" />
Actions
</button>
<button
onClick={() => setSidebarTab("inputs")}
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${
sidebarTab === "inputs"
? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-50"
}`}
>
<Settings2 className="w-3.5 h-3.5" />
Inputs
{Object.keys(state.parameters).length > 0 && (
<span className="text-[10px] bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded-full">
{Object.keys(state.parameters).length}
</span>
)}
</button>
</div>
{/* Tab content */}
{sidebarTab === "actions" ? (
<ActionPalette
actions={paletteActions}
isLoading={actionsLoading}
onAddTask={handleAddTaskFromPalette}
/>
) : (
<WorkflowInputsPanel
parameters={state.parameters}
output={state.output}
onParametersChange={(parameters) =>
setState((prev) => ({ ...prev, parameters }))
}
onOutputChange={(output) =>
setState((prev) => ({ ...prev, output }))
}
/>
)}
</div>
{/* Center: Canvas */}
<WorkflowCanvas