Skip to content

Commit 22f99c1

Browse files
ersinkocclaude
andcommitted
🎨 feat(ui): redesign 10 workflow node components with unique visual identities
Each node type now has a distinctive visual structure: - LLM: gradient indigo header, provider/model chips, temperature meter, JSON badge - Tool: blue left accent strip, monospace tool name, args count badge - Trigger: gradient green header, trigger type badge, cron/webhook preview - Condition: diamond icon, dark code block expression, split true/false zones - HTTP: colored method badge (GET=green, POST=blue, DELETE=red), URL preview, auth indicator - Code: dark terminal header with window dots, language badge, line-numbered preview - ForEach: dashed loop icon, item variable chip, max iterations badge, each/done zones - Filter: prominent funnel icon, in→out flow indicator, condition code block - Aggregate: mini bar chart visual, large operation badge, field display - Notification: severity-colored left stripe, severity badge, message preview Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0773bed commit 22f99c1

10 files changed

Lines changed: 700 additions & 345 deletions

File tree

‎packages/ui/src/components/workflows/AggregateNode.tsx‎

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/**
22
* AggregateNode — Aggregates collection data (sum, count, avg, etc.) in workflows.
3-
* Amber/yellow color theme.
3+
* Statistics visual with large operation badge, field name display,
4+
* and chart-like icon area.
45
*/
56

67
import { memo } from 'react';
78
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
8-
import { BarChart, CheckCircle2, XCircle, Activity, AlertCircle } from '../icons';
9+
import { BarChart3, CheckCircle2, XCircle, Activity, AlertCircle } from '../icons';
910
import type { NodeExecutionStatus } from '../../api/types';
1011

1112
export interface AggregateNodeData extends Record<string, unknown> {
@@ -24,6 +25,17 @@ export interface AggregateNodeData extends Record<string, unknown> {
2425

2526
export type AggregateNodeType = Node<AggregateNodeData>;
2627

28+
const operationColors: Record<string, { bg: string; text: string }> = {
29+
sum: { bg: 'bg-amber-500', text: 'text-white' },
30+
count: { bg: 'bg-blue-500', text: 'text-white' },
31+
avg: { bg: 'bg-violet-500', text: 'text-white' },
32+
min: { bg: 'bg-emerald-500', text: 'text-white' },
33+
max: { bg: 'bg-red-500', text: 'text-white' },
34+
groupBy: { bg: 'bg-indigo-500', text: 'text-white' },
35+
flatten: { bg: 'bg-teal-500', text: 'text-white' },
36+
unique: { bg: 'bg-pink-500', text: 'text-white' },
37+
};
38+
2739
const statusStyles: Record<NodeExecutionStatus, { border: string; bg: string }> = {
2840
pending: { border: 'border-amber-300 dark:border-amber-700', bg: '' },
2941
running: { border: 'border-warning', bg: 'bg-warning/5' },
@@ -43,12 +55,15 @@ function AggregateNodeComponent({ data, selected }: NodeProps<AggregateNodeType>
4355
const status = (data.executionStatus as NodeExecutionStatus | undefined) ?? 'pending';
4456
const style = statusStyles[status];
4557
const StatusIcon = statusIcons[status];
58+
const operation = (data.operation as string) ?? '';
59+
const opStyle = operationColors[operation] ?? { bg: 'bg-amber-500', text: 'text-white' };
60+
const field = (data.field as string) ?? '';
4661

4762
return (
4863
<div
4964
className={`
50-
relative min-w-[180px] max-w-[260px] rounded-lg border-2 shadow-sm
51-
bg-amber-50 dark:bg-amber-950/30
65+
relative min-w-[180px] max-w-[260px] rounded-lg border-2 shadow-sm overflow-hidden
66+
bg-white dark:bg-gray-900
5267
${style.border} ${style.bg}
5368
${selected ? 'ring-2 ring-amber-500 ring-offset-1' : ''}
5469
${status === 'running' ? 'animate-pulse' : ''}
@@ -61,10 +76,15 @@ function AggregateNodeComponent({ data, selected }: NodeProps<AggregateNodeType>
6176
className="!w-3 !h-3 !bg-amber-500 !border-2 !border-white dark:!border-amber-950"
6277
/>
6378

64-
<div className="px-3 py-2.5">
79+
{/* Header with chart icon area */}
80+
<div className="px-3 py-2 bg-amber-50 dark:bg-amber-950/30">
6581
<div className="flex items-center gap-2">
66-
<div className="w-6 h-6 rounded-full bg-amber-500/20 flex items-center justify-center shrink-0">
67-
<BarChart className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
82+
{/* Chart-like icon area */}
83+
<div className="w-7 h-7 rounded bg-amber-500/15 flex items-end justify-center gap-px pb-1 shrink-0">
84+
<div className="w-1 h-2 bg-amber-400 rounded-t" />
85+
<div className="w-1 h-3.5 bg-amber-500 rounded-t" />
86+
<div className="w-1 h-2.5 bg-amber-400 rounded-t" />
87+
<div className="w-1 h-4 bg-amber-600 rounded-t" />
6888
</div>
6989
<span className="font-medium text-sm text-amber-900 dark:text-amber-100 truncate flex-1">
7090
{(data.label as string) || 'Aggregate'}
@@ -83,29 +103,39 @@ function AggregateNodeComponent({ data, selected }: NodeProps<AggregateNodeType>
83103
/>
84104
)}
85105
</div>
106+
</div>
86107

87-
{data.operation && (
88-
<div className="flex items-center gap-1.5 mt-1">
89-
<span className="px-1.5 py-0.5 text-[9px] font-bold rounded bg-amber-500/20 text-amber-700 dark:text-amber-300 uppercase">
90-
{data.operation as string}
91-
</span>
92-
</div>
108+
{/* Body */}
109+
<div className="px-3 py-2 space-y-1.5">
110+
{/* Large operation badge */}
111+
{operation && (
112+
<span
113+
className={`inline-block px-2.5 py-1 text-[11px] font-extrabold rounded uppercase tracking-wider ${opStyle.bg} ${opStyle.text}`}
114+
>
115+
{operation}
116+
</span>
93117
)}
94118

95-
{data.field && (
96-
<p className="text-[10px] text-amber-600/70 dark:text-amber-400/50 mt-1 truncate font-mono">
97-
{data.field as string}
98-
</p>
119+
{/* Field name */}
120+
{field && (
121+
<div className="flex items-center gap-1">
122+
<BarChart3 className="w-2.5 h-2.5 text-amber-400" />
123+
<p className="text-[10px] text-amber-700 dark:text-amber-300 font-mono truncate">
124+
{field}
125+
</p>
126+
</div>
99127
)}
100128

129+
{/* Error message */}
101130
{status === 'error' && data.executionError && (
102-
<p className="text-xs text-error mt-1 truncate" title={data.executionError as string}>
131+
<p className="text-xs text-error truncate" title={data.executionError as string}>
103132
{data.executionError as string}
104133
</p>
105134
)}
106135

136+
{/* Duration */}
107137
{data.executionDuration != null && (
108-
<p className="text-[10px] text-text-muted dark:text-dark-text-muted mt-1">
138+
<p className="text-[10px] text-text-muted dark:text-dark-text-muted">
109139
{(data.executionDuration as number) < 1000
110140
? `${data.executionDuration}ms`
111141
: `${((data.executionDuration as number) / 1000).toFixed(1)}s`}

‎packages/ui/src/components/workflows/CodeNode.tsx‎

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* CodeNode — ReactFlow node for inline code execution in workflows.
3-
* Supports JavaScript, Python, and Shell languages.
4-
* Teal/cyan color theme.
3+
* Developer-focused design with dark terminal-like header,
4+
* language badge, and code preview in monospace.
55
*/
66

77
import { memo } from 'react';
@@ -23,10 +23,10 @@ export interface CodeNodeData extends Record<string, unknown> {
2323

2424
export type CodeNodeType = Node<CodeNodeData>;
2525

26-
const languageLabels: Record<string, string> = {
27-
javascript: 'JS',
28-
python: 'PY',
29-
shell: 'SH',
26+
const languageConfig: Record<string, { label: string; color: string; bg: string }> = {
27+
javascript: { label: 'JS', color: 'text-amber-300', bg: 'bg-amber-500/20' },
28+
python: { label: 'PY', color: 'text-blue-300', bg: 'bg-blue-500/20' },
29+
shell: { label: 'SH', color: 'text-emerald-300', bg: 'bg-emerald-500/20' },
3030
};
3131

3232
const statusStyles: Record<NodeExecutionStatus, { border: string; bg: string }> = {
@@ -49,15 +49,20 @@ function CodeNodeComponent({ data, selected }: NodeProps<CodeNodeType>) {
4949
const style = statusStyles[status];
5050
const StatusIcon = statusIcons[status];
5151
const lang = (data.language as string) ?? 'javascript';
52+
const langConf = languageConfig[lang] ?? { label: lang.toUpperCase(), color: 'text-gray-300', bg: 'bg-gray-500/20' };
53+
const code = (data.code as string) ?? '';
5254

53-
// First non-empty line of code for preview
54-
const codePreview = ((data.code as string) ?? '').split('\n').find((l) => l.trim()) ?? '';
55+
// First 2 non-empty lines of code for preview
56+
const codeLines = code
57+
.split('\n')
58+
.filter((l) => l.trim())
59+
.slice(0, 2);
5560

5661
return (
5762
<div
5863
className={`
59-
relative min-w-[180px] max-w-[260px] rounded-lg border-2 shadow-sm
60-
bg-teal-50 dark:bg-teal-950/30
64+
relative min-w-[180px] max-w-[280px] rounded-lg border-2 shadow-sm overflow-hidden
65+
bg-white dark:bg-gray-900
6166
${style.border} ${style.bg}
6267
${selected ? 'ring-2 ring-teal-500 ring-offset-1' : ''}
6368
${status === 'running' ? 'animate-pulse' : ''}
@@ -71,53 +76,67 @@ function CodeNodeComponent({ data, selected }: NodeProps<CodeNodeType>) {
7176
className="!w-3 !h-3 !bg-teal-500 !border-2 !border-white dark:!border-teal-950"
7277
/>
7378

74-
{/* Content */}
75-
<div className="px-3 py-2.5">
76-
{/* Header */}
77-
<div className="flex items-center gap-2">
78-
<div className="w-6 h-6 rounded-full bg-teal-500/20 flex items-center justify-center shrink-0">
79-
<Terminal className="w-3.5 h-3.5 text-teal-600 dark:text-teal-400" />
80-
</div>
81-
<span className="font-medium text-sm text-teal-900 dark:text-teal-100 truncate flex-1">
82-
{(data.label as string) || 'Code'}
83-
</span>
84-
{StatusIcon && (
85-
<StatusIcon
86-
className={`w-4 h-4 shrink-0 ${
87-
status === 'success'
88-
? 'text-success'
89-
: status === 'error'
90-
? 'text-error'
91-
: status === 'running'
92-
? 'text-warning'
93-
: 'text-text-muted'
94-
}`}
95-
/>
96-
)}
79+
{/* Dark terminal-like header */}
80+
<div className="bg-gray-900 dark:bg-gray-950 px-3 py-2 flex items-center gap-2">
81+
{/* Terminal window dots */}
82+
<div className="flex items-center gap-1 shrink-0">
83+
<div className="w-2 h-2 rounded-full bg-red-400" />
84+
<div className="w-2 h-2 rounded-full bg-amber-400" />
85+
<div className="w-2 h-2 rounded-full bg-emerald-400" />
9786
</div>
87+
<Terminal className="w-3.5 h-3.5 text-gray-400 shrink-0" />
88+
<span className="font-medium text-sm text-gray-200 truncate flex-1">
89+
{(data.label as string) || 'Code'}
90+
</span>
91+
{/* Language badge */}
92+
<span
93+
className={`px-1.5 py-0.5 text-[9px] font-bold rounded ${langConf.bg} ${langConf.color}`}
94+
>
95+
{langConf.label}
96+
</span>
97+
{StatusIcon && (
98+
<StatusIcon
99+
className={`w-4 h-4 shrink-0 ${
100+
status === 'success'
101+
? 'text-emerald-400'
102+
: status === 'error'
103+
? 'text-red-400'
104+
: status === 'running'
105+
? 'text-amber-400'
106+
: 'text-gray-500'
107+
}`}
108+
/>
109+
)}
110+
</div>
98111

99-
{/* Language badge + code preview */}
100-
<div className="flex items-center gap-1.5 mt-1">
101-
<span className="px-1.5 py-0.5 text-[9px] font-bold rounded bg-teal-500/20 text-teal-700 dark:text-teal-300">
102-
{languageLabels[lang] ?? lang.toUpperCase()}
103-
</span>
104-
{codePreview && (
105-
<p className="text-[10px] text-teal-600/70 dark:text-teal-400/50 truncate font-mono flex-1">
106-
{codePreview}
107-
</p>
108-
)}
112+
{/* Code preview in terminal style */}
113+
{codeLines.length > 0 && (
114+
<div className="bg-gray-850 dark:bg-gray-900 border-t border-gray-800 px-3 py-1.5">
115+
{codeLines.map((line, i) => (
116+
<div key={i} className="flex items-start gap-1.5">
117+
<span className="text-[9px] text-gray-600 font-mono select-none w-3 text-right shrink-0">
118+
{i + 1}
119+
</span>
120+
<p className="text-[10px] text-teal-300 dark:text-teal-400 font-mono truncate flex-1">
121+
{line}
122+
</p>
123+
</div>
124+
))}
109125
</div>
126+
)}
110127

128+
{/* Footer area */}
129+
<div className="px-3 py-1.5">
111130
{/* Error message */}
112131
{status === 'error' && data.executionError && (
113-
<p className="text-xs text-error mt-1 truncate" title={data.executionError as string}>
132+
<p className="text-xs text-error truncate" title={data.executionError as string}>
114133
{data.executionError as string}
115134
</p>
116135
)}
117136

118137
{/* Duration */}
119138
{data.executionDuration != null && (
120-
<p className="text-[10px] text-text-muted dark:text-dark-text-muted mt-1">
139+
<p className="text-[10px] text-text-muted dark:text-dark-text-muted">
121140
{(data.executionDuration as number) < 1000
122141
? `${data.executionDuration}ms`
123142
: `${((data.executionDuration as number) / 1000).toFixed(1)}s`}

0 commit comments

Comments
 (0)