diff --git a/src/components/workflow/NodeDetailPopup.tsx b/src/components/workflow/NodeDetailPopup.tsx index 2853814..af1bed9 100644 --- a/src/components/workflow/NodeDetailPopup.tsx +++ b/src/components/workflow/NodeDetailPopup.tsx @@ -32,7 +32,7 @@ import { import type { Agent, Team, PipelineConfig, PipelineStep, TeamMessage } from '@/types' import type { AgentNodeData } from './nodes/AgentNode' import type { ConditionalNodeData, ConditionOperator } from './nodes/ConditionalNode' -import type { HttpNodeData } from './nodes/HttpNode' +import type { HttpNodeData, HttpMethod } from './nodes/HttpNode' import type { ExecutionNodeState } from './useExecutionState' interface NodeDetailPopupProps { @@ -42,6 +42,7 @@ interface NodeDetailPopupProps { executionStates: Map | null runMessages?: TeamMessage[] onDelete?: (nodeId: string) => void + onUpdateNodeData?: (nodeId: string, newData: Record) => void onClose: () => void } @@ -52,7 +53,7 @@ const EXEC_STATUS_CONFIG: Record(null) const { getNodesBounds, flowToScreenPosition } = useReactFlow() const [showInput, setShowInput] = useState(false) @@ -301,91 +302,224 @@ export function NodeDetailPopup({ node, team, agents, executionStates, runMessag )} - {/* Conditional node config */} - {node.type === 'conditionalNode' && ( - <> - {/* Header override for conditional */} -
-
-

Condition Config

- {(node.data as unknown as ConditionalNodeData).conditions.map((c, i) => { - const opLabel: Record = { - contains: 'contains', not_contains: 'not contains', - equals: '==', not_equals: '!=', - starts_with: 'starts with', ends_with: 'ends with', - regex: 'regex', gt: '>', lt: '<', - is_empty: 'is empty', is_not_empty: 'is not empty', - llm_judge: 'LLM judge', - } - return ( -
-
- {c.field} - {opLabel[c.operator]} - {c.value && "{c.value}"} -
+ {/* Conditional node config — editable form */} + {node.type === 'conditionalNode' && onUpdateNodeData && (() => { + const condData = node.data as unknown as ConditionalNodeData + const condition = condData.conditions[0] as ConditionalNodeData['conditions'][number] | undefined + const opOptions: { value: ConditionOperator; label: string }[] = [ + { value: 'contains', label: 'contains' }, + { value: 'not_contains', label: 'not contains' }, + { value: 'equals', label: '==' }, + { value: 'not_equals', label: '!=' }, + { value: 'starts_with', label: 'starts with' }, + { value: 'ends_with', label: 'ends with' }, + { value: 'regex', label: 'regex' }, + { value: 'gt', label: '>' }, + { value: 'lt', label: '<' }, + { value: 'is_empty', label: 'is empty' }, + { value: 'is_not_empty', label: 'is not empty' }, + { value: 'llm_judge', label: 'LLM judge' }, + ] + const hideValue = condition?.operator === 'is_empty' || condition?.operator === 'is_not_empty' + const updateCondition = (patch: Partial<{ field: string; operator: ConditionOperator; value: string }>) => { + const updated = { ...(condition ?? { field: 'output', operator: 'contains' as ConditionOperator, value: '' }), ...patch } + onUpdateNodeData(node.id, { conditions: [updated] }) + } + return ( + <> +
+
+

Condition Config

+ {/* Label */} +
+ + onUpdateNodeData(node.id, { label: e.target.value })} + className="w-full mt-0.5 rounded-md bg-white/5 border border-white/10 px-2 py-1 text-[11px] text-gray-200 focus:border-amber-500/50 focus:outline-none transition-colors" + placeholder="Condition name" + /> +
+ {/* Field */} +
+ + updateCondition({ field: e.target.value })} + className="w-full mt-0.5 rounded-md bg-white/5 border border-white/10 px-2 py-1 text-[11px] text-amber-400 font-mono focus:border-amber-500/50 focus:outline-none transition-colors" + placeholder="output" + /> +
+ {/* Operator */} +
+ + +
+ {/* Value (hidden for is_empty/is_not_empty) */} + {!hideValue && ( +
+ + updateCondition({ value: e.target.value })} + className="w-full mt-0.5 rounded-md bg-white/5 border border-white/10 px-2 py-1 text-[11px] text-gray-200 font-mono focus:border-amber-500/50 focus:outline-none transition-colors" + placeholder={condition?.operator === 'llm_judge' ? 'Is this response positive?' : 'comparison value'} + />
- ) - })} -

- Condition editing coming soon — configure via API. -

-
- - )} + )} +

+ Connect the True and False output handles to different agents to create branches. +

+
+ + ) + })()} - {/* HTTP node config */} - {node.type === 'httpNode' && ( - <> -
-
-

HTTP Config

- {(() => { - const httpData = node.data as unknown as HttpNodeData - const methodColors: Record = { - GET: 'text-green-400', POST: 'text-blue-400', - PUT: 'text-amber-400', DELETE: 'text-red-400', - PATCH: 'text-purple-400', - } - return ( - <> -
-
- - {httpData.method} - - - {httpData.url || 'No URL set'} - -
-
- {httpData.headers.length > 0 && ( -
-

Headers ({httpData.headers.length})

- {httpData.headers.slice(0, 3).map((h, i) => ( -

- {h.key}: {h.value} -

- ))} - {httpData.headers.length > 3 && ( -

+{httpData.headers.length - 3} more

- )} -
- )} -
- - Timeout: {httpData.timeout || 30}s - + {/* HTTP node config — editable form */} + {node.type === 'httpNode' && onUpdateNodeData && (() => { + const httpData = node.data as unknown as HttpNodeData + const methods: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] + const methodColors: Record = { + GET: 'text-green-400', POST: 'text-blue-400', + PUT: 'text-amber-400', DELETE: 'text-red-400', + PATCH: 'text-purple-400', + } + const showBody = httpData.method === 'POST' || httpData.method === 'PUT' || httpData.method === 'PATCH' + return ( + <> +
+
+

HTTP Config

+ {/* Label */} +
+ + onUpdateNodeData(node.id, { label: e.target.value })} + className="w-full mt-0.5 rounded-md bg-white/5 border border-white/10 px-2 py-1 text-[11px] text-gray-200 focus:border-cyan-500/50 focus:outline-none transition-colors" + placeholder="Request name" + /> +
+ {/* Method + URL inline */} +
+
+ + +
+
+ + onUpdateNodeData(node.id, { url: e.target.value })} + className="w-full mt-0.5 rounded-md bg-white/5 border border-white/10 px-2 py-1 text-[11px] text-gray-200 font-mono focus:border-cyan-500/50 focus:outline-none transition-colors" + placeholder="https://api.example.com/endpoint" + /> +
+
+ {/* Headers */} +
+
+ + +
+ {httpData.headers.map((h, i) => ( +
+ { + const updated = [...httpData.headers] + updated[i] = { ...updated[i], key: e.target.value } + onUpdateNodeData(node.id, { headers: updated }) + }} + className="w-1/3 rounded-md bg-white/5 border border-white/10 px-1.5 py-0.5 text-[10px] text-gray-300 font-mono focus:border-cyan-500/50 focus:outline-none" + placeholder="Key" + /> + { + const updated = [...httpData.headers] + updated[i] = { ...updated[i], value: e.target.value } + onUpdateNodeData(node.id, { headers: updated }) + }} + className="flex-1 rounded-md bg-white/5 border border-white/10 px-1.5 py-0.5 text-[10px] text-gray-300 font-mono focus:border-cyan-500/50 focus:outline-none" + placeholder="Value" + /> +
-

- HTTP editing coming soon — configure via API. -

- - ) - })()} -
- - )} + ))} +
+ {/* Body (POST/PUT/PATCH only) */} + {showBody && ( +
+ +