@llamaindex/workflow-debugger 0.1.9 → 0.2.0

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/eslint.config.js +26 -0
  3. package/{dist/index.html → index.html} +1 -2
  4. package/package.json +12 -17
  5. package/postcss.config.js +6 -0
  6. package/src/App.tsx +18 -0
  7. package/src/components/code-block.tsx +48 -0
  8. package/src/components/error-boundary.tsx +85 -0
  9. package/src/components/json-schema-editor.tsx +291 -0
  10. package/src/components/run-details-panel.tsx +290 -0
  11. package/src/components/run-list-panel.tsx +83 -0
  12. package/src/components/send-event-dialog.tsx +299 -0
  13. package/src/components/workflow-config-panel.tsx +247 -0
  14. package/src/components/workflow-debugger.tsx +342 -0
  15. package/src/components/workflow-visualization.tsx +720 -0
  16. package/src/index.css +86 -0
  17. package/src/main.tsx +24 -0
  18. package/tailwind.config.js +9 -0
  19. package/tests/json-schema-editor.test.tsx +62 -0
  20. package/tests/test-setup.ts +1 -0
  21. package/tsconfig.build.json +5 -0
  22. package/tsconfig.json +16 -0
  23. package/ui_sample.png +0 -0
  24. package/vite.config.ts +46 -0
  25. package/vitest.config.ts +16 -0
  26. package/dist/app.css +0 -10
  27. package/dist/app.js +0 -624
  28. package/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  29. package/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  30. package/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  31. package/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  32. package/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  33. package/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  34. package/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  35. package/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  36. package/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  37. package/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  38. package/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  39. package/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  40. package/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  41. package/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  42. package/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  43. package/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  44. package/dist/assets/KaTeX_Main-Bold.woff +0 -0
  45. package/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  46. package/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  47. package/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  48. package/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  49. package/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  50. package/dist/assets/KaTeX_Main-Italic.woff +0 -0
  51. package/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  52. package/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  53. package/dist/assets/KaTeX_Main-Regular.woff +0 -0
  54. package/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  55. package/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  56. package/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  57. package/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  58. package/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  59. package/dist/assets/KaTeX_Math-Italic.woff +0 -0
  60. package/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  61. package/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  62. package/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  63. package/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  64. package/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  65. package/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  66. package/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  67. package/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  68. package/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  69. package/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  70. package/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  71. package/dist/assets/KaTeX_Script-Regular.woff +0 -0
  72. package/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  73. package/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  74. package/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  75. package/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  76. package/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  77. package/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  78. package/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  79. package/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  80. package/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  81. package/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  82. package/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  83. package/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  84. package/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  85. package/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  86. package/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
@@ -0,0 +1,291 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import {
3
+ Input,
4
+ Textarea,
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from "@llamaindex/ui";
11
+ import type { JSONValue } from "./workflow-config-panel";
12
+
13
+ export interface SimpleSchemaProperty {
14
+ type?: string;
15
+ title?: string;
16
+ description?: string;
17
+ }
18
+
19
+ export interface SimpleSchema {
20
+ properties?: Record<string, SimpleSchemaProperty>;
21
+ required?: string[];
22
+ }
23
+
24
+ export interface JsonSchemaEditorProps {
25
+ schema: SimpleSchema | null;
26
+ values: Record<string, JSONValue>;
27
+ onChange: (values: Record<string, JSONValue>) => void;
28
+ onErrorsChange?: (errors: Record<string, string | null>) => void;
29
+ className?: string;
30
+ }
31
+
32
+ function isComplexType(type?: string): boolean {
33
+ if (!type) return false;
34
+ const lower = type.toLowerCase();
35
+ return (
36
+ lower.includes("object") ||
37
+ lower.includes("array") ||
38
+ lower.includes("map") ||
39
+ lower.includes("dict") ||
40
+ /\w+\s*\[.*\]/.test(type)
41
+ );
42
+ }
43
+
44
+ function getTypeHintAndPlaceholder(type?: string): {
45
+ hint: string;
46
+ placeholder: string;
47
+ } {
48
+ const lower = (type || "").toLowerCase();
49
+ const looksLikeArray =
50
+ lower.includes("array") ||
51
+ lower.includes("list") ||
52
+ /\w+\s*\[.*\]/.test(type || "");
53
+ const looksLikeObject =
54
+ lower.includes("object") || lower.includes("map") || lower.includes("dict");
55
+ if (looksLikeArray) {
56
+ return {
57
+ hint: 'Expected: JSON array (e.g., ["a", "b"])',
58
+ placeholder: '["item1", "item2"]',
59
+ };
60
+ }
61
+ if (looksLikeObject) {
62
+ return {
63
+ hint: 'Expected: JSON object (e.g., {"key": "value"})',
64
+ placeholder: '{"key": "value"}',
65
+ };
66
+ }
67
+ return {
68
+ hint: "Expected: valid JSON (object or array)",
69
+ placeholder: "Enter value as JSON",
70
+ };
71
+ }
72
+
73
+ export function JsonSchemaEditor({
74
+ schema,
75
+ values,
76
+ onChange,
77
+ onErrorsChange,
78
+ className,
79
+ }: JsonSchemaEditorProps) {
80
+ const properties = schema?.properties || {};
81
+ const required = new Set(schema?.required || []);
82
+
83
+ const [rawJsonValues, setRawJsonValues] = useState<Record<string, string>>(
84
+ {},
85
+ );
86
+ const [rawJsonErrors, setRawJsonErrors] = useState<
87
+ Record<string, string | null>
88
+ >({});
89
+
90
+ useEffect(() => {
91
+ onErrorsChange?.(rawJsonErrors);
92
+ }, [rawJsonErrors, onErrorsChange]);
93
+
94
+ // Initialize raw values for complex fields when schema or values change
95
+ useEffect(() => {
96
+ const nextRaw: Record<string, string> = { ...rawJsonValues };
97
+ for (const [key, def] of Object.entries(properties)) {
98
+ if (!isComplexType(def.type)) continue;
99
+ if (nextRaw[key] === undefined) {
100
+ const v = values[key];
101
+ nextRaw[key] = v !== undefined ? JSON.stringify(v, null, 2) : "";
102
+ }
103
+ }
104
+ setRawJsonValues(nextRaw);
105
+ }, [properties, values]);
106
+
107
+ const handleValueChange = (key: string, newValue: JSONValue) => {
108
+ onChange({ ...values, [key]: newValue });
109
+ };
110
+
111
+ const items = useMemo(() => Object.entries(properties), [properties]);
112
+
113
+ if (items.length === 0) return null;
114
+
115
+ return (
116
+ <div className={className}>
117
+ {items.map(([fieldName, fieldSchema]) => {
118
+ const fieldId = `field-${fieldName}`;
119
+ const fieldTitle = fieldSchema.title || fieldName;
120
+ const fieldType = fieldSchema.type || "string";
121
+ const fieldDescription = fieldSchema.description || "";
122
+
123
+ if (fieldType === "string") {
124
+ return (
125
+ <div key={fieldName} className="space-y-2">
126
+ <label htmlFor={fieldId} className="text-sm font-medium">
127
+ {fieldTitle}
128
+ {required.has(fieldName) && (
129
+ <span className="text-destructive ml-1">*</span>
130
+ )}
131
+ </label>
132
+ <Textarea
133
+ id={fieldId}
134
+ value={(values[fieldName] as string) || ""}
135
+ onChange={(e) => handleValueChange(fieldName, e.target.value)}
136
+ placeholder={
137
+ fieldDescription || `Enter ${fieldTitle.toLowerCase()}`
138
+ }
139
+ rows={3}
140
+ />
141
+ {fieldDescription && (
142
+ <p className="text-xs text-muted-foreground">
143
+ {fieldDescription}
144
+ </p>
145
+ )}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ if (fieldType === "number" || fieldType === "integer") {
151
+ return (
152
+ <div key={fieldName} className="space-y-2">
153
+ <label htmlFor={fieldId} className="text-sm font-medium">
154
+ {fieldTitle}
155
+ {required.has(fieldName) && (
156
+ <span className="text-destructive ml-1">*</span>
157
+ )}
158
+ </label>
159
+ <Input
160
+ id={fieldId}
161
+ type="number"
162
+ value={
163
+ ((values[fieldName] as number) ?? "") as unknown as string
164
+ }
165
+ onChange={(e) => {
166
+ const val = e.target.value;
167
+ if (val === "") {
168
+ handleValueChange(fieldName, null);
169
+ return;
170
+ }
171
+ const parsed =
172
+ fieldType === "integer"
173
+ ? parseInt(val, 10)
174
+ : parseFloat(val);
175
+ handleValueChange(
176
+ fieldName,
177
+ Number.isNaN(parsed)
178
+ ? null
179
+ : (parsed as unknown as JSONValue),
180
+ );
181
+ }}
182
+ placeholder={
183
+ fieldDescription || `Enter ${fieldTitle.toLowerCase()}`
184
+ }
185
+ step={fieldType === "integer" ? "1" : "any"}
186
+ />
187
+ {fieldDescription && (
188
+ <p className="text-xs text-muted-foreground">
189
+ {fieldDescription}
190
+ </p>
191
+ )}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ if (fieldType === "boolean") {
197
+ return (
198
+ <div key={fieldName} className="space-y-2">
199
+ <label htmlFor={fieldId} className="text-sm font-medium">
200
+ {fieldTitle}
201
+ {required.has(fieldName) && (
202
+ <span className="text-destructive ml-1">*</span>
203
+ )}
204
+ </label>
205
+ <Select
206
+ onValueChange={(value) =>
207
+ handleValueChange(fieldName, value === "true")
208
+ }
209
+ value={String(Boolean(values[fieldName]))}
210
+ >
211
+ <SelectTrigger>
212
+ <SelectValue placeholder="Select..." />
213
+ </SelectTrigger>
214
+ <SelectContent>
215
+ <SelectItem value="true">True</SelectItem>
216
+ <SelectItem value="false">False</SelectItem>
217
+ </SelectContent>
218
+ </Select>
219
+ {fieldDescription && (
220
+ <p className="text-xs text-muted-foreground">
221
+ {fieldDescription}
222
+ </p>
223
+ )}
224
+ </div>
225
+ );
226
+ }
227
+
228
+ // Complex types: object / array / list[...] / map
229
+ const { hint, placeholder } = getTypeHintAndPlaceholder(fieldType);
230
+ const raw =
231
+ rawJsonValues[fieldName] ??
232
+ (values[fieldName] !== undefined
233
+ ? JSON.stringify(values[fieldName], null, 2)
234
+ : "");
235
+ const hasError = Boolean(rawJsonErrors[fieldName]);
236
+
237
+ return (
238
+ <div key={fieldName} className="space-y-2">
239
+ <label htmlFor={fieldId} className="text-sm font-medium">
240
+ {fieldTitle} (JSON)
241
+ {required.has(fieldName) && (
242
+ <span className="text-destructive ml-1">*</span>
243
+ )}
244
+ </label>
245
+ <Textarea
246
+ id={fieldId}
247
+ value={raw}
248
+ onChange={(e) => {
249
+ const text = e.target.value;
250
+ setRawJsonValues((prev) => ({ ...prev, [fieldName]: text }));
251
+ if (text.trim() === "") {
252
+ setRawJsonErrors((prev) => ({ ...prev, [fieldName]: null }));
253
+ // Clear value when empty
254
+ const next = { ...values };
255
+ delete next[fieldName];
256
+ onChange(next);
257
+ return;
258
+ }
259
+ try {
260
+ const parsed = JSON.parse(text);
261
+ setRawJsonErrors((prev) => ({ ...prev, [fieldName]: null }));
262
+ handleValueChange(fieldName, parsed);
263
+ } catch {
264
+ setRawJsonErrors((prev) => ({
265
+ ...prev,
266
+ [fieldName]: "Invalid JSON",
267
+ }));
268
+ }
269
+ }}
270
+ placeholder={fieldDescription || placeholder}
271
+ className={`font-mono ${hasError ? "border-destructive focus-visible:ring-destructive" : ""}`}
272
+ rows={3}
273
+ />
274
+ {hasError ? (
275
+ <p className="text-xs text-destructive mt-1">
276
+ {rawJsonErrors[fieldName]}
277
+ </p>
278
+ ) : (
279
+ <p className="text-xs text-muted-foreground">{hint}</p>
280
+ )}
281
+ {fieldDescription && (
282
+ <p className="text-xs text-muted-foreground">
283
+ {fieldDescription}
284
+ </p>
285
+ )}
286
+ </div>
287
+ );
288
+ })}
289
+ </div>
290
+ );
291
+ }
@@ -0,0 +1,290 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import {
3
+ Badge,
4
+ Button,
5
+ Label,
6
+ StopEvent,
7
+ Switch,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ isBuiltInEvent,
15
+ } from "@llamaindex/ui";
16
+ import { useHandler, type WorkflowEvent } from "@llamaindex/ui";
17
+ import { CodeBlock } from "./code-block";
18
+ import { useStreamEventBatcher } from "@llamaindex/ui";
19
+ import { WorkflowVisualization } from "./workflow-visualization";
20
+ import { SendEventDialog } from "./send-event-dialog";
21
+
22
+ type JSONValue =
23
+ | null
24
+ | string
25
+ | number
26
+ | boolean
27
+ | { [key: string]: JSONValue }
28
+ | Array<JSONValue>;
29
+
30
+ interface RunDetailsPanelProps {
31
+ handlerId: string;
32
+ selectedWorkflow?: string | null;
33
+ tab?: "visualization" | "events";
34
+ onTabChange?: (value: "visualization" | "events") => void;
35
+ }
36
+
37
+ export function RunDetailsPanel({
38
+ handlerId,
39
+ selectedWorkflow,
40
+ }: RunDetailsPanelProps) {
41
+ const { state, subscribeToEvents } = useHandler(handlerId);
42
+ const [compactJson, setCompactJson] = useState(false);
43
+ const [hideInternal, setHideInternal] = useState(true);
44
+ const [finalResult, setFinalResult] = useState<JSONValue | null>(null);
45
+ const [finalResultError, setFinalResultError] = useState<string | null>(null);
46
+ const {
47
+ items: events,
48
+ push,
49
+ clear,
50
+ } = useStreamEventBatcher<WorkflowEvent>(
51
+ 100,
52
+ (a, b) => a.timestamp.getTime() - b.timestamp.getTime(),
53
+ );
54
+
55
+ const formatJsonData = (data: unknown) => {
56
+ if (typeof data === "string") {
57
+ return data;
58
+ }
59
+ if (!data || typeof data !== "object") {
60
+ return String(data ?? "");
61
+ }
62
+ return JSON.stringify(data, null, compactJson ? 0 : 2);
63
+ };
64
+
65
+ const formatTime = (epochMs?: number): string => {
66
+ if (!epochMs) return "";
67
+ const d = new Date(epochMs);
68
+ const base = d.toLocaleTimeString([], {
69
+ hour12: false,
70
+ hour: "2-digit",
71
+ minute: "2-digit",
72
+ second: "2-digit",
73
+ });
74
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
75
+ return `${base}.${ms}`;
76
+ };
77
+
78
+ useEffect(() => {
79
+ if (state.status === "running") {
80
+ const { disconnect } = subscribeToEvents(
81
+ {
82
+ onData: (event: WorkflowEvent) => {
83
+ push(event);
84
+ },
85
+ onSuccess(allEvents) {
86
+ setFinalResult(
87
+ ((allEvents[allEvents.length - 1] as StopEvent)?.data?.[
88
+ "result"
89
+ ] ?? null) as JSONValue | null,
90
+ );
91
+ },
92
+ onError(error) {
93
+ setFinalResultError(error.message);
94
+ },
95
+ },
96
+ true,
97
+ );
98
+ return () => {
99
+ disconnect();
100
+ };
101
+ }
102
+ }, [subscribeToEvents, state.status, push]);
103
+
104
+ // Reset events, timestamps and result when switching handlers
105
+ useEffect(() => {
106
+ clear();
107
+ setFinalResult(null);
108
+ setFinalResultError(null);
109
+ }, [handlerId, clear]);
110
+
111
+ const displayedEvents: WorkflowEvent[] = useMemo(
112
+ () =>
113
+ hideInternal ? events.filter((event) => !isBuiltInEvent(event)) : events,
114
+ [events, hideInternal],
115
+ );
116
+
117
+ return (
118
+ <div className="h-full flex flex-col">
119
+ {/* Header */}
120
+ <div className="p-4 border-b border-border">
121
+ <div className="flex items-center justify-between">
122
+ <h2 className="font-semibold text-sm">Run Details</h2>
123
+ <div className="flex items-center gap-2">
124
+ <Badge
125
+ variant={state.status === "completed" ? "default" : "secondary"}
126
+ >
127
+ {state.status}
128
+ </Badge>
129
+ <SendEventDialog
130
+ handlerId={handlerId}
131
+ workflowName={selectedWorkflow ?? null}
132
+ disabled={
133
+ !state ||
134
+ state.status === "completed" ||
135
+ state.status === "failed"
136
+ }
137
+ />
138
+ </div>
139
+ </div>
140
+ <div className="flex items-center justify-between gap-2 mt-2">
141
+ <span className="text-xs text-muted-foreground font-mono">
142
+ {state.handler_id}
143
+ </span>
144
+ <span className="text-xs text-muted-foreground font-mono">
145
+ Last updated: {state.updated_at?.getTime()}
146
+ </span>
147
+ </div>
148
+ </div>
149
+
150
+ {/* Side-by-side content */}
151
+ <div className="flex-1 flex overflow-hidden">
152
+ {/* Left: Visualization */}
153
+ <div className="flex-1 p-4 overflow-auto border-r border-border">
154
+ <WorkflowVisualization
155
+ workflowName={selectedWorkflow || null}
156
+ events={events.map((e: WorkflowEvent) => ({
157
+ type: e.type,
158
+ data: e.data,
159
+ }))}
160
+ className="w-full h-full min-h-[400px]"
161
+ isComplete={
162
+ state.status === "completed" || state.status === "failed"
163
+ }
164
+ />
165
+ </div>
166
+
167
+ {/* Right: Events */}
168
+ <div className="w-[480px] flex flex-col overflow-hidden">
169
+ {/* Events Header */}
170
+ <div className="p-4 pb-2 border-b border-border">
171
+ <div className="flex items-center justify-between mb-3">
172
+ <h4 className="font-medium text-sm">
173
+ Event Stream ({displayedEvents.length})
174
+ </h4>
175
+ <div className="flex items-center gap-3">
176
+ <div className="flex items-center gap-2">
177
+ <Label htmlFor="hide-internal" className="text-xs">
178
+ Hide internal
179
+ </Label>
180
+ <Switch
181
+ id="hide-internal"
182
+ checked={hideInternal}
183
+ onCheckedChange={(v: boolean) =>
184
+ setHideInternal(Boolean(v))
185
+ }
186
+ />
187
+ </div>
188
+ <Button
189
+ variant="ghost"
190
+ size="sm"
191
+ onClick={() => setCompactJson(!compactJson)}
192
+ >
193
+ {compactJson ? "Formatted" : "Compact"}
194
+ </Button>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ {/* Events List */}
200
+ <div className="flex-1 overflow-auto">
201
+ {!state.handler_id ? (
202
+ <div className="flex items-center justify-center h-full">
203
+ <p className="text-muted-foreground text-sm">
204
+ Start a run to see events
205
+ </p>
206
+ </div>
207
+ ) : events.length === 0 ? (
208
+ <div className="flex items-center justify-center h-full">
209
+ <p className="text-muted-foreground text-sm">
210
+ No events yet...
211
+ </p>
212
+ </div>
213
+ ) : (
214
+ <Table>
215
+ <TableHeader className="sticky top-0 bg-background z-10">
216
+ <TableRow>
217
+ <TableHead className="w-8">#</TableHead>
218
+ <TableHead>Event</TableHead>
219
+ </TableRow>
220
+ </TableHeader>
221
+ <TableBody>
222
+ {displayedEvents.map((item, index) => {
223
+ const event = item;
224
+ return (
225
+ <TableRow
226
+ key={`${event.type}-${index}`}
227
+ data-event-index={index}
228
+ className={`cursor-pointer transition-colors`}
229
+ >
230
+ <TableCell className="text-xs text-muted-foreground align-top">
231
+ {index + 1}
232
+ </TableCell>
233
+ <TableCell className="py-3">
234
+ <div className="space-y-2">
235
+ <div className="flex items-baseline justify-between gap-2">
236
+ <div className="flex items-center gap-2 flex-1 min-w-0">
237
+ <code className="text-sm font-mono truncate">
238
+ {event.type}
239
+ </code>
240
+ </div>
241
+ {event.timestamp !== undefined ? (
242
+ <span className="text-[10px] text-muted-foreground font-mono whitespace-nowrap">
243
+ {formatTime(event.timestamp.getTime())}
244
+ </span>
245
+ ) : null}
246
+ </div>
247
+ <CodeBlock
248
+ language={
249
+ typeof event.data === "string" ? "text" : "json"
250
+ }
251
+ value={formatJsonData(event.data)}
252
+ wrapLongLines={compactJson}
253
+ className="rounded border max-h-64 overflow-auto"
254
+ />
255
+ </div>
256
+ </TableCell>
257
+ </TableRow>
258
+ );
259
+ })}
260
+ </TableBody>
261
+ </Table>
262
+ )}
263
+ </div>
264
+
265
+ {/* Final Result Section */}
266
+ {(finalResult || finalResultError) && (
267
+ <div className="p-4 border-t border-border">
268
+ <h4 className="font-medium text-sm mb-2">Final Result</h4>
269
+ {finalResultError ? (
270
+ <div className="text-destructive text-sm p-3 bg-destructive/10 border border-destructive/20 rounded">
271
+ Failed to load final result: {finalResultError}
272
+ </div>
273
+ ) : (
274
+ <CodeBlock
275
+ language={typeof finalResult === "string" ? "text" : "json"}
276
+ value={
277
+ typeof finalResult === "string"
278
+ ? (finalResult as string)
279
+ : JSON.stringify(finalResult, null, 2)
280
+ }
281
+ className="rounded border overflow-hidden"
282
+ />
283
+ )}
284
+ </div>
285
+ )}
286
+ </div>
287
+ </div>
288
+ </div>
289
+ );
290
+ }
@@ -0,0 +1,83 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Skeleton, HandlerState, useHandlers } from "@llamaindex/ui";
3
+
4
+ interface RunListPanelProps {
5
+ activeHandlerId: string | null;
6
+ onHandlerSelect: (handlerId: string) => void;
7
+ }
8
+
9
+ export function RunListPanel({
10
+ activeHandlerId,
11
+ onHandlerSelect,
12
+ }: RunListPanelProps) {
13
+ const { state: handlersState, sync } = useHandlers();
14
+ const handlers = handlersState.handlers;
15
+ const [loading, setLoading] = useState(false);
16
+
17
+ useEffect(() => {
18
+ setLoading(true);
19
+ sync().finally(() => setLoading(false));
20
+ }, [sync]);
21
+
22
+ const formatHandlerDisplay = (handler: HandlerState) => {
23
+ const name = handler.workflow_name || "Unknown";
24
+ return `${name}`;
25
+ };
26
+
27
+ const handlerList = Object.values(handlers);
28
+
29
+ return (
30
+ <div className="h-full flex flex-col text-sm">
31
+ {/* Header */}
32
+ <div className="px-3 py-2 border-b border-border">
33
+ <div className="flex items-center justify-between">
34
+ <h2 className="font-semibold text-xs tracking-tight">Recent Runs</h2>
35
+ </div>
36
+ </div>
37
+
38
+ {/* Content */}
39
+ <div className="flex-1 overflow-y-auto">
40
+ {/* Loading State */}
41
+ {loading && (
42
+ <div className="p-2 space-y-2">
43
+ <Skeleton className="h-8 w-full" />
44
+ <Skeleton className="h-8 w-full" />
45
+ <Skeleton className="h-8 w-full" />
46
+ </div>
47
+ )}
48
+
49
+ {/* Handlers List */}
50
+ {!loading && (
51
+ <div className="p-2 divide-y divide-border">
52
+ {handlerList.length === 0 ? (
53
+ <div className="text-muted-foreground text-xs p-3 text-center">
54
+ No runs yet. Start a workflow to see runs here.
55
+ </div>
56
+ ) : (
57
+ handlerList.map((handler) => (
58
+ <button
59
+ key={handler.handler_id}
60
+ onClick={() => onHandlerSelect(handler.handler_id)}
61
+ className={`w-full text-left px-2 py-1.5 transition-colors cursor-pointer border border-collapse ${
62
+ activeHandlerId === handler.handler_id
63
+ ? "bg-accent "
64
+ : "hover:bg-accent"
65
+ }`}
66
+ >
67
+ <div className="grid grid-cols-[1fr_auto] items-center gap-2">
68
+ <span className="truncate text-xs">
69
+ {formatHandlerDisplay(handler)}
70
+ </span>
71
+ <span className="text-[10px] text-muted-foreground font-mono">
72
+ {handler.handler_id.slice(-8)}
73
+ </span>
74
+ </div>
75
+ </button>
76
+ ))
77
+ )}
78
+ </div>
79
+ )}
80
+ </div>
81
+ </div>
82
+ );
83
+ }