@workflow/web-shared 4.1.0-beta.55 → 4.1.0-beta.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +34 -2
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/run-trace-view.d.ts +4 -1
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +2 -2
- package/dist/components/run-trace-view.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +122 -45
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/copyable-data-block.d.ts +4 -0
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -0
- package/dist/components/sidebar/copyable-data-block.js +33 -0
- package/dist/components/sidebar/copyable-data-block.js.map +1 -0
- package/dist/components/sidebar/detail-card.d.ts +7 -1
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +12 -3
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +30 -16
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +37 -7
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts +19 -0
- package/dist/components/ui/error-stack-block.d.ts.map +1 -0
- package/dist/components/ui/error-stack-block.js +39 -0
- package/dist/components/ui/error-stack-block.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts +7 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +137 -24
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/package.json +10 -10
- package/src/components/event-list-view.tsx +53 -2
- package/src/components/run-trace-view.tsx +9 -0
- package/src/components/sidebar/attribute-panel.tsx +285 -127
- package/src/components/sidebar/copyable-data-block.tsx +51 -0
- package/src/components/sidebar/detail-card.tsx +28 -2
- package/src/components/sidebar/entity-detail-panel.tsx +138 -81
- package/src/components/sidebar/events-list.tsx +72 -21
- package/src/components/ui/error-stack-block.tsx +80 -0
- package/src/components/workflow-trace-view.tsx +208 -28
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
4
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
5
5
|
import type { ModelMessage } from 'ai';
|
|
6
|
-
import type { ReactNode } from 'react';
|
|
7
|
-
import { useMemo, useState } from 'react';
|
|
6
|
+
import type { KeyboardEvent, ReactNode } from 'react';
|
|
7
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
8
|
+
import { toast } from 'sonner';
|
|
8
9
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
9
|
-
import {
|
|
10
|
+
import { StreamClickContext } from '../ui/data-inspector';
|
|
10
11
|
import { ErrorCard } from '../ui/error-card';
|
|
12
|
+
import {
|
|
13
|
+
ErrorStackBlock,
|
|
14
|
+
isStructuredErrorWithStack,
|
|
15
|
+
} from '../ui/error-stack-block';
|
|
16
|
+
import { Skeleton } from '../ui/skeleton';
|
|
11
17
|
import { ConversationView } from './conversation-view';
|
|
18
|
+
import { CopyableDataBlock } from './copyable-data-block';
|
|
12
19
|
import { DetailCard } from './detail-card';
|
|
13
20
|
|
|
14
21
|
/**
|
|
@@ -26,6 +33,9 @@ function TabButton({
|
|
|
26
33
|
return (
|
|
27
34
|
<button
|
|
28
35
|
type="button"
|
|
36
|
+
role="tab"
|
|
37
|
+
aria-selected={active}
|
|
38
|
+
tabIndex={active ? 0 : -1}
|
|
29
39
|
onClick={onClick}
|
|
30
40
|
className="px-3 py-1.5 text-[11px] font-medium transition-colors -mb-px"
|
|
31
41
|
style={{
|
|
@@ -49,49 +59,97 @@ function TabButton({
|
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
/**
|
|
52
|
-
*
|
|
62
|
+
* Shared tabbed container with accessible ARIA roles and keyboard navigation.
|
|
63
|
+
* Used by ConversationWithTabs for the conversation/JSON toggle.
|
|
53
64
|
*/
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
function TabbedContainer<T extends string>({
|
|
66
|
+
tabs,
|
|
67
|
+
activeTab,
|
|
68
|
+
onTabChange,
|
|
69
|
+
ariaLabel,
|
|
70
|
+
children,
|
|
57
71
|
}: {
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
tabs: { id: T; label: string }[];
|
|
73
|
+
activeTab: T;
|
|
74
|
+
onTabChange: (tab: T) => void;
|
|
75
|
+
ariaLabel: string;
|
|
76
|
+
children: ReactNode;
|
|
60
77
|
}) {
|
|
61
|
-
const
|
|
62
|
-
|
|
78
|
+
const handleKeyDown = useCallback(
|
|
79
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
80
|
+
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return;
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
const currentIndex = tabs.findIndex((t) => t.id === activeTab);
|
|
83
|
+
const nextIndex =
|
|
84
|
+
event.key === 'ArrowRight'
|
|
85
|
+
? (currentIndex + 1) % tabs.length
|
|
86
|
+
: (currentIndex - 1 + tabs.length) % tabs.length;
|
|
87
|
+
onTabChange(tabs[nextIndex].id);
|
|
88
|
+
},
|
|
89
|
+
[tabs, activeTab, onTabChange]
|
|
63
90
|
);
|
|
64
91
|
|
|
65
92
|
return (
|
|
66
|
-
<
|
|
93
|
+
<div
|
|
94
|
+
className="rounded-md border"
|
|
95
|
+
style={{
|
|
96
|
+
borderColor: 'var(--ds-gray-300)',
|
|
97
|
+
backgroundColor: 'transparent',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
67
100
|
<div
|
|
68
|
-
className="
|
|
101
|
+
className="flex gap-1 border-b"
|
|
102
|
+
role="tablist"
|
|
103
|
+
aria-label={ariaLabel}
|
|
104
|
+
onKeyDown={handleKeyDown}
|
|
69
105
|
style={{
|
|
70
106
|
borderColor: 'var(--ds-gray-300)',
|
|
71
107
|
backgroundColor: 'transparent',
|
|
72
108
|
}}
|
|
73
109
|
>
|
|
74
|
-
|
|
75
|
-
className="flex gap-1 border-b"
|
|
76
|
-
style={{
|
|
77
|
-
borderColor: 'var(--ds-gray-300)',
|
|
78
|
-
backgroundColor: 'transparent',
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
110
|
+
{tabs.map((tab) => (
|
|
81
111
|
<TabButton
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
key={tab.id}
|
|
113
|
+
active={activeTab === tab.id}
|
|
114
|
+
onClick={() => onTabChange(tab.id)}
|
|
84
115
|
>
|
|
85
|
-
|
|
116
|
+
{tab.label}
|
|
86
117
|
</TabButton>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div role="tabpanel">{children}</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const conversationTabs = [
|
|
127
|
+
{ id: 'conversation' as const, label: 'Conversation' },
|
|
128
|
+
{ id: 'json' as const, label: 'Raw JSON' },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Tabbed view for conversation and raw JSON
|
|
133
|
+
*/
|
|
134
|
+
function ConversationWithTabs({
|
|
135
|
+
conversation,
|
|
136
|
+
args,
|
|
137
|
+
}: {
|
|
138
|
+
conversation: ModelMessage[];
|
|
139
|
+
args: unknown[];
|
|
140
|
+
}) {
|
|
141
|
+
const [activeTab, setActiveTab] = useState<'conversation' | 'json'>(
|
|
142
|
+
'conversation'
|
|
143
|
+
);
|
|
94
144
|
|
|
145
|
+
return (
|
|
146
|
+
<DetailCard summary={`Input (${conversation.length} messages)`}>
|
|
147
|
+
<TabbedContainer
|
|
148
|
+
tabs={conversationTabs}
|
|
149
|
+
activeTab={activeTab}
|
|
150
|
+
onTabChange={setActiveTab}
|
|
151
|
+
ariaLabel="Conversation view"
|
|
152
|
+
>
|
|
95
153
|
{activeTab === 'conversation' ? (
|
|
96
154
|
<ConversationView messages={conversation} />
|
|
97
155
|
) : (
|
|
@@ -105,7 +163,7 @@ function ConversationWithTabs({
|
|
|
105
163
|
: JsonBlock(args)}
|
|
106
164
|
</div>
|
|
107
165
|
)}
|
|
108
|
-
</
|
|
166
|
+
</TabbedContainer>
|
|
109
167
|
</DetailCard>
|
|
110
168
|
);
|
|
111
169
|
}
|
|
@@ -115,16 +173,17 @@ function ConversationWithTabs({
|
|
|
115
173
|
* custom theming, nodeRenderer for StreamRef/ClassInstanceRef, etc.)
|
|
116
174
|
*/
|
|
117
175
|
function JsonBlock(value: unknown) {
|
|
118
|
-
return
|
|
119
|
-
<div
|
|
120
|
-
className="overflow-x-auto rounded-md border p-3"
|
|
121
|
-
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
122
|
-
>
|
|
123
|
-
<DataInspector data={value} />
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
176
|
+
return <CopyableDataBlock data={value} />;
|
|
126
177
|
}
|
|
127
178
|
|
|
179
|
+
const hasDisplayContent = (value: unknown): boolean => {
|
|
180
|
+
if (value == null) return false;
|
|
181
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
182
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
183
|
+
if (typeof value === 'object') return Object.keys(value).length > 0;
|
|
184
|
+
return true;
|
|
185
|
+
};
|
|
186
|
+
|
|
128
187
|
type AttributeKey =
|
|
129
188
|
| keyof Step
|
|
130
189
|
| keyof WorkflowRun
|
|
@@ -272,16 +331,23 @@ const attributeToDisplayFn: Record<
|
|
|
272
331
|
retryAfter: localMillisecondTime,
|
|
273
332
|
resumeAt: localMillisecondTime,
|
|
274
333
|
// Resolved attributes, won't actually use this function
|
|
275
|
-
metadata:
|
|
334
|
+
metadata: (value: unknown) => {
|
|
335
|
+
if (!hasDisplayContent(value)) return null;
|
|
336
|
+
return JsonBlock(value);
|
|
337
|
+
},
|
|
276
338
|
input: (value: unknown, context?: DisplayContext) => {
|
|
277
339
|
// Check if input has args + closure vars structure
|
|
278
340
|
if (value && typeof value === 'object' && 'args' in value) {
|
|
279
|
-
const { args, closureVars } = value as {
|
|
341
|
+
const { args, closureVars, thisVal } = value as {
|
|
280
342
|
args: unknown[];
|
|
281
343
|
closureVars?: Record<string, unknown>;
|
|
344
|
+
thisVal?: unknown;
|
|
282
345
|
};
|
|
283
346
|
const argCount = Array.isArray(args) ? args.length : 0;
|
|
284
|
-
const
|
|
347
|
+
const argLabel = argCount === 1 ? 'argument' : 'arguments';
|
|
348
|
+
const hasClosureVars = hasDisplayContent(closureVars);
|
|
349
|
+
const hasThisVal = hasDisplayContent(thisVal);
|
|
350
|
+
const hasArgs = hasDisplayContent(args);
|
|
285
351
|
|
|
286
352
|
// Check if this is a doStreamStep - show conversation view with tabs
|
|
287
353
|
if (context?.stepName && isDoStreamStep(context.stepName)) {
|
|
@@ -295,17 +361,37 @@ const attributeToDisplayFn: Record<
|
|
|
295
361
|
{JsonBlock(closureVars)}
|
|
296
362
|
</DetailCard>
|
|
297
363
|
)}
|
|
364
|
+
{hasThisVal && (
|
|
365
|
+
<DetailCard summary="This Value">
|
|
366
|
+
{JsonBlock(thisVal)}
|
|
367
|
+
</DetailCard>
|
|
368
|
+
)}
|
|
298
369
|
</>
|
|
299
370
|
);
|
|
300
371
|
}
|
|
301
372
|
}
|
|
302
373
|
|
|
374
|
+
// Don't render an empty "Input (0 arguments)" card when no input exists.
|
|
375
|
+
if (!hasArgs && !hasClosureVars && !hasThisVal) {
|
|
376
|
+
return (
|
|
377
|
+
<DetailCard
|
|
378
|
+
summary="Input (no data)"
|
|
379
|
+
disabled
|
|
380
|
+
summaryClassName="text-base py-2"
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
303
385
|
return (
|
|
304
386
|
<>
|
|
305
|
-
<DetailCard
|
|
387
|
+
<DetailCard
|
|
388
|
+
summary={`Input (${argCount} ${argLabel})`}
|
|
389
|
+
summaryClassName="text-base py-2"
|
|
390
|
+
contentClassName="mt-0"
|
|
391
|
+
>
|
|
306
392
|
{Array.isArray(args)
|
|
307
393
|
? args.map((v, i) => (
|
|
308
|
-
<div className="mt-2" key={i}>
|
|
394
|
+
<div className="mt-2 first:mt-0" key={i}>
|
|
309
395
|
{JsonBlock(v)}
|
|
310
396
|
</div>
|
|
311
397
|
))
|
|
@@ -316,17 +402,34 @@ const attributeToDisplayFn: Record<
|
|
|
316
402
|
{JsonBlock(closureVars)}
|
|
317
403
|
</DetailCard>
|
|
318
404
|
)}
|
|
405
|
+
{hasThisVal && (
|
|
406
|
+
<DetailCard summary="this">{JsonBlock(thisVal)}</DetailCard>
|
|
407
|
+
)}
|
|
319
408
|
</>
|
|
320
409
|
);
|
|
321
410
|
}
|
|
322
411
|
|
|
323
412
|
// Fallback: treat as plain array or object
|
|
324
413
|
const argCount = Array.isArray(value) ? value.length : 0;
|
|
414
|
+
const argLabel = argCount === 1 ? 'argument' : 'arguments';
|
|
415
|
+
if (!hasDisplayContent(value)) {
|
|
416
|
+
return (
|
|
417
|
+
<DetailCard
|
|
418
|
+
summary="Input (no data)"
|
|
419
|
+
disabled
|
|
420
|
+
summaryClassName="text-base py-2"
|
|
421
|
+
/>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
325
424
|
return (
|
|
326
|
-
<DetailCard
|
|
425
|
+
<DetailCard
|
|
426
|
+
summary={`Input (${argCount} ${argLabel})`}
|
|
427
|
+
summaryClassName="text-base py-2"
|
|
428
|
+
contentClassName="mt-0"
|
|
429
|
+
>
|
|
327
430
|
{Array.isArray(value)
|
|
328
431
|
? value.map((v, i) => (
|
|
329
|
-
<div className="mt-2" key={i}>
|
|
432
|
+
<div className="mt-2 first:mt-0" key={i}>
|
|
330
433
|
{JsonBlock(v)}
|
|
331
434
|
</div>
|
|
332
435
|
))
|
|
@@ -335,72 +438,46 @@ const attributeToDisplayFn: Record<
|
|
|
335
438
|
);
|
|
336
439
|
},
|
|
337
440
|
output: (value: unknown) => {
|
|
338
|
-
|
|
441
|
+
if (!hasDisplayContent(value)) return null;
|
|
442
|
+
return (
|
|
443
|
+
<DetailCard
|
|
444
|
+
summary="Output"
|
|
445
|
+
summaryClassName="text-base py-2"
|
|
446
|
+
contentClassName="mt-0"
|
|
447
|
+
>
|
|
448
|
+
{JsonBlock(value)}
|
|
449
|
+
</DetailCard>
|
|
450
|
+
);
|
|
339
451
|
},
|
|
340
452
|
error: (value: unknown) => {
|
|
341
|
-
|
|
342
|
-
if (value && typeof value === 'object' && 'message' in value) {
|
|
343
|
-
const error = value as {
|
|
344
|
-
message: string;
|
|
345
|
-
stack?: string;
|
|
346
|
-
code?: string;
|
|
347
|
-
};
|
|
453
|
+
if (!hasDisplayContent(value)) return null;
|
|
348
454
|
|
|
455
|
+
// If the error object has a `stack` field, render it as readable
|
|
456
|
+
// pre-formatted text. Otherwise fall back to the raw JSON viewer.
|
|
457
|
+
if (isStructuredErrorWithStack(value)) {
|
|
349
458
|
return (
|
|
350
|
-
<DetailCard
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
className="text-[11px] font-medium"
|
|
357
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
358
|
-
>
|
|
359
|
-
Error Code:{' '}
|
|
360
|
-
</span>
|
|
361
|
-
<code
|
|
362
|
-
className="text-[11px]"
|
|
363
|
-
style={{ color: 'var(--ds-gray-1000)' }}
|
|
364
|
-
>
|
|
365
|
-
{error.code}
|
|
366
|
-
</code>
|
|
367
|
-
</div>
|
|
368
|
-
)}
|
|
369
|
-
{/* Show stack if available, otherwise just the message */}
|
|
370
|
-
<pre
|
|
371
|
-
className="text-[11px] overflow-x-auto rounded-md border p-3"
|
|
372
|
-
style={{
|
|
373
|
-
borderColor: 'var(--ds-gray-300)',
|
|
374
|
-
backgroundColor: 'var(--ds-gray-100)',
|
|
375
|
-
color: 'var(--ds-gray-1000)',
|
|
376
|
-
whiteSpace: 'pre-wrap',
|
|
377
|
-
}}
|
|
378
|
-
>
|
|
379
|
-
<code>{error.stack || error.message}</code>
|
|
380
|
-
</pre>
|
|
381
|
-
</div>
|
|
459
|
+
<DetailCard
|
|
460
|
+
summary="Error"
|
|
461
|
+
summaryClassName="text-base py-2"
|
|
462
|
+
contentClassName="mt-0"
|
|
463
|
+
>
|
|
464
|
+
<ErrorStackBlock value={value} />
|
|
382
465
|
</DetailCard>
|
|
383
466
|
);
|
|
384
467
|
}
|
|
385
468
|
|
|
386
|
-
// Fallback for plain string errors
|
|
387
469
|
return (
|
|
388
|
-
<DetailCard
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
color: 'var(--ds-gray-1000)',
|
|
395
|
-
whiteSpace: 'pre-wrap',
|
|
396
|
-
}}
|
|
397
|
-
>
|
|
398
|
-
<code>{String(value)}</code>
|
|
399
|
-
</pre>
|
|
470
|
+
<DetailCard
|
|
471
|
+
summary="Error"
|
|
472
|
+
summaryClassName="text-base py-2"
|
|
473
|
+
contentClassName="mt-0"
|
|
474
|
+
>
|
|
475
|
+
{JsonBlock(value)}
|
|
400
476
|
</DetailCard>
|
|
401
477
|
);
|
|
402
478
|
},
|
|
403
479
|
eventData: (value: unknown) => {
|
|
480
|
+
if (!hasDisplayContent(value)) return null;
|
|
404
481
|
return <DetailCard summary="Event Data">{JsonBlock(value)}</DetailCard>;
|
|
405
482
|
},
|
|
406
483
|
};
|
|
@@ -439,6 +516,44 @@ export const AttributeBlock = ({
|
|
|
439
516
|
inline?: boolean;
|
|
440
517
|
context?: DisplayContext;
|
|
441
518
|
}) => {
|
|
519
|
+
const isExpandableLoadingTarget =
|
|
520
|
+
attribute === 'input' ||
|
|
521
|
+
attribute === 'output' ||
|
|
522
|
+
attribute === 'eventData';
|
|
523
|
+
if (isLoading && isExpandableLoadingTarget) {
|
|
524
|
+
const label =
|
|
525
|
+
attribute === 'eventData'
|
|
526
|
+
? 'Event Data'
|
|
527
|
+
: attribute === 'output'
|
|
528
|
+
? 'Output'
|
|
529
|
+
: 'Input';
|
|
530
|
+
return (
|
|
531
|
+
<div
|
|
532
|
+
className={`my-2 flex flex-col ${attribute === 'input' || attribute === 'output' ? 'gap-2 my-3.5' : 'gap-0'}`}
|
|
533
|
+
>
|
|
534
|
+
<span
|
|
535
|
+
className={`${attribute === 'input' || attribute === 'output' ? 'text-base' : 'text-xs'} font-medium first-letter:uppercase`}
|
|
536
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
537
|
+
>
|
|
538
|
+
{attribute}
|
|
539
|
+
</span>
|
|
540
|
+
<DetailCard
|
|
541
|
+
summary={label}
|
|
542
|
+
summaryClassName="text-base py-2"
|
|
543
|
+
disabled
|
|
544
|
+
/>
|
|
545
|
+
<div
|
|
546
|
+
className="overflow-x-auto rounded-md border p-3"
|
|
547
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
548
|
+
>
|
|
549
|
+
<Skeleton className="h-4 w-[38%]" />
|
|
550
|
+
<Skeleton className="mt-2 h-4 w-[88%]" />
|
|
551
|
+
<Skeleton className="mt-2 h-4 w-[72%]" />
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
442
557
|
const displayFn =
|
|
443
558
|
attributeToDisplayFn[attribute as keyof typeof attributeToDisplayFn];
|
|
444
559
|
if (!displayFn) {
|
|
@@ -475,9 +590,12 @@ export const AttributeBlock = ({
|
|
|
475
590
|
/>
|
|
476
591
|
</div>
|
|
477
592
|
)}
|
|
478
|
-
<div
|
|
593
|
+
<div
|
|
594
|
+
key={attribute}
|
|
595
|
+
className={`my-2 flex flex-col ${attribute === 'input' || attribute === 'output' || attribute === 'error' ? 'gap-2 my-3.5' : 'gap-0'}`}
|
|
596
|
+
>
|
|
479
597
|
<span
|
|
480
|
-
className=
|
|
598
|
+
className={`${attribute === 'input' || attribute === 'output' || attribute === 'error' ? 'text-base' : 'text-xs'} font-medium first-letter:uppercase`}
|
|
481
599
|
style={{ color: 'var(--ds-gray-700)' }}
|
|
482
600
|
>
|
|
483
601
|
{attribute}
|
|
@@ -575,6 +693,16 @@ export const AttributePanel = ({
|
|
|
575
693
|
}),
|
|
576
694
|
[displayData.stepName]
|
|
577
695
|
);
|
|
696
|
+
const handleCopyModuleSpecifier = useCallback((value: string) => {
|
|
697
|
+
navigator.clipboard
|
|
698
|
+
.writeText(value)
|
|
699
|
+
.then(() => {
|
|
700
|
+
toast.success('moduleSpecifier copied');
|
|
701
|
+
})
|
|
702
|
+
.catch(() => {
|
|
703
|
+
toast.error('Failed to copy moduleSpecifier');
|
|
704
|
+
});
|
|
705
|
+
}, []);
|
|
578
706
|
|
|
579
707
|
return (
|
|
580
708
|
<StreamClickContext.Provider value={onStreamClick}>
|
|
@@ -582,36 +710,66 @@ export const AttributePanel = ({
|
|
|
582
710
|
{/* Basic attributes in a vertical layout with border */}
|
|
583
711
|
{visibleBasicAttributes.length > 0 && (
|
|
584
712
|
<div
|
|
585
|
-
className="flex flex-col
|
|
713
|
+
className="mb-3 flex flex-col overflow-hidden rounded-lg border"
|
|
586
714
|
style={{
|
|
587
715
|
borderColor: 'var(--ds-gray-300)',
|
|
588
|
-
backgroundColor: 'var(--ds-gray-100)',
|
|
589
716
|
}}
|
|
590
717
|
>
|
|
591
|
-
{orderedBasicAttributes.map((attribute) =>
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
718
|
+
{orderedBasicAttributes.map((attribute, index) => {
|
|
719
|
+
const displayValue = attributeToDisplayFn[
|
|
720
|
+
attribute as keyof typeof attributeToDisplayFn
|
|
721
|
+
]?.(displayData[attribute as keyof typeof displayData]);
|
|
722
|
+
const isModuleSpecifier = attribute === 'moduleSpecifier';
|
|
723
|
+
const moduleSpecifierValue =
|
|
724
|
+
typeof displayValue === 'string'
|
|
725
|
+
? displayValue
|
|
726
|
+
: String(displayValue ?? displayData.moduleSpecifier ?? '');
|
|
727
|
+
const showDivider = index < orderedBasicAttributes.length - 1;
|
|
728
|
+
|
|
729
|
+
return (
|
|
730
|
+
<div key={attribute} className="py-1">
|
|
731
|
+
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
732
|
+
<span
|
|
733
|
+
className="text-[14px] first-letter:uppercase"
|
|
734
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
735
|
+
>
|
|
736
|
+
{getAttributeDisplayName(attribute)}
|
|
737
|
+
</span>
|
|
738
|
+
{isModuleSpecifier ? (
|
|
739
|
+
<button
|
|
740
|
+
type="button"
|
|
741
|
+
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
742
|
+
style={{
|
|
743
|
+
color: 'var(--ds-gray-1000)',
|
|
744
|
+
background: 'transparent',
|
|
745
|
+
border: 'none',
|
|
746
|
+
padding: 0,
|
|
747
|
+
}}
|
|
748
|
+
title={moduleSpecifierValue}
|
|
749
|
+
onClick={() =>
|
|
750
|
+
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
751
|
+
}
|
|
752
|
+
>
|
|
753
|
+
{moduleSpecifierValue}
|
|
754
|
+
</button>
|
|
755
|
+
) : (
|
|
756
|
+
<span
|
|
757
|
+
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
758
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
759
|
+
>
|
|
760
|
+
{displayValue}
|
|
761
|
+
</span>
|
|
762
|
+
)}
|
|
763
|
+
</div>
|
|
764
|
+
{showDivider ? (
|
|
765
|
+
<div
|
|
766
|
+
className="mx-2.5 border-b"
|
|
767
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
768
|
+
/>
|
|
769
|
+
) : null}
|
|
770
|
+
</div>
|
|
771
|
+
);
|
|
772
|
+
})}
|
|
615
773
|
</div>
|
|
616
774
|
)}
|
|
617
775
|
{error ? (
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Copy } from 'lucide-react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
import { DataInspector } from '../ui/data-inspector';
|
|
6
|
+
|
|
7
|
+
const serializeForClipboard = (value: unknown): string => {
|
|
8
|
+
if (typeof value === 'string') return value;
|
|
9
|
+
if (
|
|
10
|
+
typeof value === 'number' ||
|
|
11
|
+
typeof value === 'boolean' ||
|
|
12
|
+
value === null
|
|
13
|
+
) {
|
|
14
|
+
return String(value);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(value, null, 2);
|
|
18
|
+
} catch {
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function CopyableDataBlock({ data }: { data: unknown }) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className="relative overflow-x-auto rounded-md border p-3 pt-9"
|
|
27
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
28
|
+
>
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
aria-label="Copy data"
|
|
32
|
+
title="Copy"
|
|
33
|
+
className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border !bg-[var(--ds-background-100)] !text-[var(--ds-gray-800)] transition-transform transition-colors duration-100 hover:!bg-[var(--ds-gray-alpha-200)] active:!scale-95 active:!bg-[var(--ds-gray-alpha-300)]"
|
|
34
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
35
|
+
onClick={() => {
|
|
36
|
+
navigator.clipboard
|
|
37
|
+
.writeText(serializeForClipboard(data))
|
|
38
|
+
.then(() => {
|
|
39
|
+
toast.success('Copied to clipboard');
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {
|
|
42
|
+
toast.error('Failed to copy');
|
|
43
|
+
});
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Copy size={12} />
|
|
47
|
+
</button>
|
|
48
|
+
<DataInspector data={data} />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -4,19 +4,45 @@ export function DetailCard({
|
|
|
4
4
|
summary,
|
|
5
5
|
children,
|
|
6
6
|
onToggle,
|
|
7
|
+
disabled = false,
|
|
8
|
+
summaryClassName,
|
|
9
|
+
contentClassName,
|
|
7
10
|
}: {
|
|
8
11
|
summary: ReactNode;
|
|
9
12
|
children?: ReactNode;
|
|
10
13
|
/** Called when the detail card is expanded/collapsed */
|
|
11
14
|
onToggle?: (open: boolean) => void;
|
|
15
|
+
/** Renders a non-expandable summary card when true. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Extra classes for the summary row. */
|
|
18
|
+
summaryClassName?: string;
|
|
19
|
+
/** Extra classes for expanded content wrapper. */
|
|
20
|
+
contentClassName?: string;
|
|
12
21
|
}) {
|
|
22
|
+
if (disabled) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={`rounded-md border px-2.5 py-1.5 text-xs ${summaryClassName ?? ''}`}
|
|
26
|
+
style={{
|
|
27
|
+
borderColor: 'var(--ds-gray-300)',
|
|
28
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
29
|
+
color: 'var(--ds-gray-700)',
|
|
30
|
+
cursor: 'not-allowed',
|
|
31
|
+
opacity: 0.8,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{summary}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
return (
|
|
14
40
|
<details
|
|
15
41
|
className="group"
|
|
16
42
|
onToggle={(e) => onToggle?.((e.target as HTMLDetailsElement).open)}
|
|
17
43
|
>
|
|
18
44
|
<summary
|
|
19
|
-
className=
|
|
45
|
+
className={`cursor-pointer rounded-md border px-2.5 py-1.5 text-xs hover:brightness-95 ${summaryClassName ?? ''}`}
|
|
20
46
|
style={{
|
|
21
47
|
borderColor: 'var(--ds-gray-300)',
|
|
22
48
|
backgroundColor: 'var(--ds-gray-100)',
|
|
@@ -26,7 +52,7 @@ export function DetailCard({
|
|
|
26
52
|
{summary}
|
|
27
53
|
</summary>
|
|
28
54
|
{/* Expanded content with connecting line */}
|
|
29
|
-
<div className=
|
|
55
|
+
<div className={`relative pl-6 mt-3 ${contentClassName ?? ''}`}>
|
|
30
56
|
{/* Curved connecting line - vertical part from summary */}
|
|
31
57
|
<div
|
|
32
58
|
className="absolute left-3 -top-3 w-px h-3"
|