@llamaindex/workflow-debugger 0.1.8 → 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.
@@ -0,0 +1,299 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ useWorkflowsClient,
17
+ } from "@llamaindex/ui";
18
+ import { Send } from "lucide-react";
19
+ import { JsonSchemaEditor } from "./json-schema-editor";
20
+ import type { JSONValue } from "./workflow-config-panel";
21
+ import {
22
+ postEventsByHandlerId,
23
+ getWorkflowsByNameEvents,
24
+ } from "@llamaindex/workflows-client";
25
+
26
+ interface EventSchema {
27
+ title?: string;
28
+ type?: string;
29
+ properties?: Record<string, unknown>;
30
+ required?: string[];
31
+ }
32
+
33
+ interface SendEventDialogProps {
34
+ handlerId: string | null;
35
+ workflowName: string | null;
36
+ disabled?: boolean;
37
+ }
38
+
39
+ export function SendEventDialog({
40
+ handlerId,
41
+ workflowName,
42
+ disabled,
43
+ }: SendEventDialogProps) {
44
+ const [open, setOpen] = useState(false);
45
+ const [eventSchemas, setEventSchemas] = useState<EventSchema[]>([]);
46
+ const [selectedEventType, setSelectedEventType] = useState<string | null>(
47
+ null,
48
+ );
49
+ const [eventData, setEventData] = useState<Record<string, JSONValue>>({});
50
+ const [loading, setLoading] = useState(false);
51
+ const [schemaErrors, setSchemaErrors] = useState<Record<
52
+ string,
53
+ string | null
54
+ > | null>(null);
55
+ const [sending, setSending] = useState(false);
56
+ const [sendError, setSendError] = useState<string | null>(null);
57
+ const [sendSuccess, setSendSuccess] = useState(false);
58
+
59
+ const workflowsClient = useWorkflowsClient();
60
+
61
+ // Fetch event schemas when dialog opens
62
+ useEffect(() => {
63
+ if (!open || !workflowName) return;
64
+
65
+ const fetchEventSchemas = async (): Promise<void> => {
66
+ setLoading(true);
67
+ setSendError(null);
68
+ try {
69
+ const response = await getWorkflowsByNameEvents({
70
+ client: workflowsClient,
71
+ path: { name: workflowName },
72
+ });
73
+
74
+ const data = response.data;
75
+ if (!data) {
76
+ setEventSchemas([]);
77
+ return;
78
+ }
79
+ if (!data.events) {
80
+ setEventSchemas([]);
81
+ return;
82
+ }
83
+
84
+ setEventSchemas(data.events);
85
+
86
+ // Auto-select first event if available
87
+ if (data.events && data.events.length > 0) {
88
+ const firstEvent = data.events[0];
89
+ const title = firstEvent.title as string;
90
+ setSelectedEventType(title || null);
91
+ }
92
+ } catch (error) {
93
+ console.error("Failed to fetch event schemas:", error);
94
+ setSendError(
95
+ error instanceof Error ? error.message : "Failed to fetch events",
96
+ );
97
+ setEventSchemas([]);
98
+ } finally {
99
+ setLoading(false);
100
+ }
101
+ };
102
+
103
+ void fetchEventSchemas();
104
+ }, [open, workflowName, workflowsClient]);
105
+
106
+ // Reset state when dialog closes
107
+ useEffect(() => {
108
+ if (!open) {
109
+ setSelectedEventType(null);
110
+ setEventData({});
111
+ setSchemaErrors(null);
112
+ setSendError(null);
113
+ setSendSuccess(false);
114
+ }
115
+ }, [open]);
116
+
117
+ // Reset event data when event type changes
118
+ useEffect(() => {
119
+ setEventData({});
120
+ setSendError(null);
121
+ setSendSuccess(false);
122
+ }, [selectedEventType]);
123
+
124
+ const selectedSchema = useMemo(() => {
125
+ if (!selectedEventType) return null;
126
+ return (
127
+ eventSchemas.find((schema) => schema.title === selectedEventType) || null
128
+ );
129
+ }, [selectedEventType, eventSchemas]);
130
+
131
+ const hasSchemaErrors = useMemo(() => {
132
+ if (!schemaErrors) return false;
133
+ return Object.values(schemaErrors).some((error) => error !== null);
134
+ }, [schemaErrors]);
135
+
136
+ const handleSendEvent = async () => {
137
+ if (!handlerId || !selectedEventType) return;
138
+
139
+ setSending(true);
140
+ setSendError(null);
141
+ setSendSuccess(false);
142
+
143
+ try {
144
+ const payload = {
145
+ type: selectedEventType,
146
+ data: eventData,
147
+ };
148
+
149
+ const { error } = await postEventsByHandlerId({
150
+ client: workflowsClient,
151
+ path: { handler_id: handlerId },
152
+ body: {
153
+ event: JSON.stringify(payload),
154
+ },
155
+ });
156
+
157
+ if (error) {
158
+ throw new Error(
159
+ typeof error === "string" ? error : JSON.stringify(error),
160
+ );
161
+ }
162
+
163
+ setSendSuccess(true);
164
+
165
+ // Close dialog after a short delay to show success message
166
+ setTimeout(() => {
167
+ setOpen(false);
168
+ }, 1000);
169
+ } catch (error) {
170
+ console.error("Failed to send event:", error);
171
+ setSendError(
172
+ error instanceof Error ? error.message : "Failed to send event",
173
+ );
174
+ } finally {
175
+ setSending(false);
176
+ }
177
+ };
178
+
179
+ const canSend =
180
+ !sending &&
181
+ !hasSchemaErrors &&
182
+ selectedEventType &&
183
+ handlerId &&
184
+ !sendSuccess;
185
+
186
+ return (
187
+ <Dialog open={open} onOpenChange={setOpen}>
188
+ <DialogTrigger asChild>
189
+ <Button
190
+ variant="outline"
191
+ size="sm"
192
+ disabled={disabled || !handlerId || !workflowName}
193
+ title={
194
+ !handlerId
195
+ ? "No active handler"
196
+ : !workflowName
197
+ ? "No workflow selected"
198
+ : "Send event to workflow"
199
+ }
200
+ >
201
+ <Send className="h-4 w-4 mr-2" />
202
+ Send Event
203
+ </Button>
204
+ </DialogTrigger>
205
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
206
+ <DialogHeader>
207
+ <DialogTitle>Send Event to Workflow</DialogTitle>
208
+ <DialogDescription>
209
+ Send a custom event to the running workflow handler.
210
+ </DialogDescription>
211
+ </DialogHeader>
212
+
213
+ <div className="space-y-4 py-4">
214
+ {loading ? (
215
+ <div className="text-center text-muted-foreground">
216
+ Loading event types...
217
+ </div>
218
+ ) : eventSchemas.length === 0 ? (
219
+ <div className="text-center text-muted-foreground">
220
+ {sendError || "No events available for this workflow"}
221
+ </div>
222
+ ) : (
223
+ <>
224
+ {/* Event Type Selector */}
225
+ <div className="space-y-2">
226
+ <label className="text-sm font-medium">Event Type</label>
227
+ <Select
228
+ value={selectedEventType || ""}
229
+ onValueChange={setSelectedEventType}
230
+ >
231
+ <SelectTrigger>
232
+ <SelectValue placeholder="Select event type..." />
233
+ </SelectTrigger>
234
+ <SelectContent>
235
+ {eventSchemas.map((schema) => (
236
+ <SelectItem
237
+ key={schema.title || "unknown"}
238
+ value={schema.title || ""}
239
+ >
240
+ {schema.title || "Unnamed Event"}
241
+ </SelectItem>
242
+ ))}
243
+ </SelectContent>
244
+ </Select>
245
+ </div>
246
+
247
+ {/* Event Data Editor */}
248
+ {selectedSchema && (
249
+ <div className="space-y-2">
250
+ <label className="text-sm font-medium">Event Data</label>
251
+ <JsonSchemaEditor
252
+ schema={{
253
+ properties: selectedSchema.properties as Record<
254
+ string,
255
+ {
256
+ type?: string;
257
+ title?: string;
258
+ description?: string;
259
+ }
260
+ >,
261
+ required: selectedSchema.required,
262
+ }}
263
+ values={eventData}
264
+ onChange={setEventData}
265
+ onErrorsChange={setSchemaErrors}
266
+ className="space-y-3 border border-border rounded-lg p-4 bg-muted/30"
267
+ />
268
+ </div>
269
+ )}
270
+
271
+ {/* Error Messages */}
272
+ {sendError && (
273
+ <div className="text-destructive text-sm p-3 bg-destructive/10 border border-destructive/20 rounded">
274
+ {sendError}
275
+ </div>
276
+ )}
277
+
278
+ {/* Success Message */}
279
+ {sendSuccess && (
280
+ <div className="text-green-600 text-sm p-3 bg-green-50 border border-green-200 rounded">
281
+ Event sent successfully!
282
+ </div>
283
+ )}
284
+ </>
285
+ )}
286
+ </div>
287
+
288
+ <DialogFooter>
289
+ <Button variant="outline" onClick={() => setOpen(false)}>
290
+ Cancel
291
+ </Button>
292
+ <Button onClick={handleSendEvent} disabled={!canSend}>
293
+ {sending ? "Sending..." : "Send Event"}
294
+ </Button>
295
+ </DialogFooter>
296
+ </DialogContent>
297
+ </Dialog>
298
+ );
299
+ }
@@ -0,0 +1,247 @@
1
+ import { useState, useEffect } from "react";
2
+ import {
3
+ useWorkflowsClient,
4
+ Button,
5
+ Textarea,
6
+ Skeleton,
7
+ useWorkflow,
8
+ useHandlers,
9
+ } from "@llamaindex/ui";
10
+ import { PanelRightClose } from "lucide-react";
11
+ import { JsonSchemaEditor } from "./json-schema-editor";
12
+ import { getWorkflowsByNameSchema } from "@llamaindex/workflows-client";
13
+
14
+ export type JSONValue =
15
+ | null
16
+ | string
17
+ | number
18
+ | boolean
19
+ | { [key: string]: JSONValue }
20
+ | Array<JSONValue>;
21
+
22
+ interface WorkflowConfigPanelProps {
23
+ selectedWorkflow: string;
24
+ onRunStart: (handlerId: string) => void;
25
+ activeHandlerId: string | null;
26
+ onCollapse?: () => void;
27
+ }
28
+
29
+ interface SchemaProperty {
30
+ type: string;
31
+ title?: string;
32
+ description?: string;
33
+ }
34
+
35
+ interface Schema {
36
+ properties?: Record<string, SchemaProperty>;
37
+ required?: string[];
38
+ }
39
+
40
+ export function WorkflowConfigPanel({
41
+ selectedWorkflow,
42
+ onRunStart,
43
+ onCollapse,
44
+ }: WorkflowConfigPanelProps) {
45
+ const [schema, setSchema] = useState<Schema | null>(null);
46
+ const [formData, setFormData] = useState<{ [key: string]: JSONValue }>({});
47
+ const [loading, setLoading] = useState(false);
48
+ const [error, setError] = useState<string | null>(null);
49
+ const [rawInput, setRawInput] = useState<string>("");
50
+ const [rawInputError, setRawInputError] = useState<string | null>(null);
51
+ const [rawJsonErrors, setRawJsonErrors] = useState<
52
+ Record<string, string | null>
53
+ >({});
54
+ const [isCreating, setIsCreating] = useState(false);
55
+
56
+ const workflowsClient = useWorkflowsClient();
57
+ const { createHandler } = useWorkflow(selectedWorkflow);
58
+ const { setHandler } = useHandlers();
59
+
60
+ useEffect(() => {
61
+ const fetchSchema = async () => {
62
+ if (!selectedWorkflow) return;
63
+
64
+ try {
65
+ setLoading(true);
66
+ setError(null);
67
+ const { data, error } = await getWorkflowsByNameSchema({
68
+ client: workflowsClient,
69
+ path: { name: selectedWorkflow },
70
+ });
71
+ if (data) {
72
+ setSchema((data as { start?: Schema | null })?.start ?? null);
73
+ } else {
74
+ throw new Error(error ? String(error) : "Failed to fetch schema");
75
+ }
76
+ setFormData({});
77
+ setRawInput(JSON.stringify({}, null, 2));
78
+ setRawInputError(null);
79
+ setRawJsonErrors({});
80
+ } catch (err) {
81
+ setError(err instanceof Error ? err.message : "Failed to fetch schema");
82
+ setSchema(null);
83
+ } finally {
84
+ setLoading(false);
85
+ }
86
+ };
87
+
88
+ if (selectedWorkflow) {
89
+ fetchSchema();
90
+ } else {
91
+ setSchema(null);
92
+ setFormData({});
93
+ setRawInput("");
94
+ setRawInputError(null);
95
+ setRawJsonErrors({});
96
+ }
97
+ }, [selectedWorkflow, workflowsClient]);
98
+
99
+ const handleRunWorkflow = async () => {
100
+ if (!selectedWorkflow) return;
101
+
102
+ try {
103
+ setIsCreating(true);
104
+ const handler = await createHandler(formData);
105
+ setHandler(handler);
106
+ onRunStart(handler.handler_id);
107
+
108
+ // Auto-collapse the config panel after starting a run
109
+ if (onCollapse) {
110
+ onCollapse();
111
+ }
112
+ } catch (err) {
113
+ console.error("Failed to run workflow:", err);
114
+ } finally {
115
+ setIsCreating(false);
116
+ }
117
+ };
118
+
119
+ const hasSchemaFields = Boolean(
120
+ schema?.properties && Object.keys(schema.properties).length > 0,
121
+ );
122
+
123
+ if (!selectedWorkflow) {
124
+ return (
125
+ <div className="h-full flex flex-col">
126
+ <div className="p-4 border-b border-border flex items-center justify-between">
127
+ <h2 className="font-semibold text-sm">Workflow Configuration</h2>
128
+ {onCollapse && (
129
+ <Button
130
+ variant="ghost"
131
+ size="sm"
132
+ onClick={onCollapse}
133
+ title="Hide configuration panel (Ctrl+B)"
134
+ >
135
+ <PanelRightClose className="h-4 w-4" />
136
+ </Button>
137
+ )}
138
+ </div>
139
+ <div className="flex-1 flex items-center justify-center">
140
+ <p className="text-muted-foreground text-center">
141
+ Select a workflow to configure and run
142
+ </p>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ return (
149
+ <div className="h-full flex flex-col">
150
+ {/* Header */}
151
+ <div className="p-4 border-b border-border flex items-center justify-between">
152
+ <h2 className="font-semibold text-sm">Configure: {selectedWorkflow}</h2>
153
+ {onCollapse && (
154
+ <Button
155
+ variant="ghost"
156
+ size="sm"
157
+ onClick={onCollapse}
158
+ title="Hide configuration panel (Ctrl+B)"
159
+ >
160
+ <PanelRightClose className="h-4 w-4" />
161
+ </Button>
162
+ )}
163
+ </div>
164
+
165
+ {/* Content */}
166
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
167
+ {loading && (
168
+ <div className="space-y-4">
169
+ <Skeleton className="h-4 w-32" />
170
+ <Skeleton className="h-20 w-full" />
171
+ <Skeleton className="h-4 w-32" />
172
+ <Skeleton className="h-20 w-full" />
173
+ </div>
174
+ )}
175
+
176
+ {error && (
177
+ <div className="text-destructive text-sm p-3 bg-destructive/10 border border-destructive/20 rounded">
178
+ Error loading schema: {error}
179
+ </div>
180
+ )}
181
+
182
+ {!loading && !error && (
183
+ <>
184
+ {/* Form Fields */}
185
+ <div className="space-y-4">
186
+ {hasSchemaFields ? (
187
+ <JsonSchemaEditor
188
+ schema={schema}
189
+ values={formData}
190
+ onChange={setFormData}
191
+ onErrorsChange={setRawJsonErrors}
192
+ />
193
+ ) : (
194
+ <div className="space-y-2">
195
+ <label htmlFor="raw-input" className="text-sm font-medium">
196
+ Input (JSON)
197
+ </label>
198
+ <Textarea
199
+ id="raw-input"
200
+ value={rawInput}
201
+ onChange={(e) => {
202
+ setRawInput(e.target.value);
203
+ try {
204
+ const parsed = JSON.parse(e.target.value);
205
+ setFormData(parsed);
206
+ setRawInputError(null);
207
+ } catch {
208
+ // Keep editing raw input until valid JSON
209
+ setRawInputError("Invalid JSON");
210
+ }
211
+ }}
212
+ placeholder='{"key": "value"}'
213
+ className={`font-mono ${rawInputError ? "border-destructive focus-visible:ring-destructive" : ""}`}
214
+ rows={8}
215
+ />
216
+ {rawInputError && (
217
+ <p className="text-xs text-destructive mt-1">
218
+ {rawInputError}
219
+ </p>
220
+ )}
221
+ </div>
222
+ )}
223
+ </div>
224
+ </>
225
+ )}
226
+ </div>
227
+
228
+ {/* Footer with Run Button */}
229
+ {!loading && !error && (
230
+ <div className="p-4 border-t border-border">
231
+ {Object.values(rawJsonErrors).some((e) => e) && (
232
+ <p className="text-xs text-destructive mb-2">
233
+ Fix invalid JSON fields before running.
234
+ </p>
235
+ )}
236
+ <Button
237
+ onClick={handleRunWorkflow}
238
+ disabled={isCreating || Object.values(rawJsonErrors).some((e) => e)}
239
+ className="w-full"
240
+ >
241
+ {isCreating ? "Starting..." : "Run Workflow"}
242
+ </Button>
243
+ </div>
244
+ )}
245
+ </div>
246
+ );
247
+ }