@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.
- package/CHANGELOG.md +205 -0
- package/eslint.config.js +26 -0
- package/{dist/index.html → index.html} +1 -2
- package/package.json +12 -17
- package/postcss.config.js +6 -0
- package/src/App.tsx +18 -0
- package/src/components/code-block.tsx +48 -0
- package/src/components/error-boundary.tsx +85 -0
- package/src/components/json-schema-editor.tsx +291 -0
- package/src/components/run-details-panel.tsx +290 -0
- package/src/components/run-list-panel.tsx +83 -0
- package/src/components/send-event-dialog.tsx +299 -0
- package/src/components/workflow-config-panel.tsx +247 -0
- package/src/components/workflow-debugger.tsx +342 -0
- package/src/components/workflow-visualization.tsx +720 -0
- package/src/index.css +86 -0
- package/src/main.tsx +24 -0
- package/tailwind.config.js +9 -0
- package/tests/json-schema-editor.test.tsx +62 -0
- package/tests/test-setup.ts +1 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +16 -0
- package/ui_sample.png +0 -0
- package/vite.config.ts +46 -0
- package/vitest.config.ts +16 -0
- package/dist/app.css +0 -10
- package/dist/app.js +0 -624
- package/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
- package/dist/assets/KaTeX_AMS-Regular.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold.ttf +0 -0
- package/dist/assets/KaTeX_Main-Bold.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Italic.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Main-Regular.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/dist/assets/KaTeX_Math-Italic.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular.woff +0 -0
- package/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular.woff +0 -0
- package/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Size3-Regular.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
- 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
|
+
}
|