@ryanfw/prompt-orchestration-pipeline 0.6.0 → 0.7.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 (61) hide show
  1. package/README.md +1 -2
  2. package/package.json +1 -2
  3. package/src/api/validators/json.js +39 -0
  4. package/src/components/DAGGrid.jsx +392 -303
  5. package/src/components/JobCard.jsx +13 -11
  6. package/src/components/JobDetail.jsx +41 -71
  7. package/src/components/JobTable.jsx +32 -22
  8. package/src/components/Layout.jsx +0 -21
  9. package/src/components/LiveText.jsx +47 -0
  10. package/src/components/TaskDetailSidebar.jsx +216 -0
  11. package/src/components/TimerText.jsx +82 -0
  12. package/src/components/ui/RestartJobModal.jsx +140 -0
  13. package/src/components/ui/toast.jsx +138 -0
  14. package/src/config/models.js +322 -0
  15. package/src/config/statuses.js +119 -0
  16. package/src/core/config.js +2 -164
  17. package/src/core/file-io.js +1 -1
  18. package/src/core/module-loader.js +54 -40
  19. package/src/core/pipeline-runner.js +52 -20
  20. package/src/core/status-writer.js +147 -3
  21. package/src/core/symlink-bridge.js +57 -0
  22. package/src/core/symlink-utils.js +94 -0
  23. package/src/core/task-runner.js +267 -443
  24. package/src/llm/index.js +167 -52
  25. package/src/pages/Code.jsx +57 -3
  26. package/src/pages/PipelineDetail.jsx +92 -22
  27. package/src/pages/PromptPipelineDashboard.jsx +15 -36
  28. package/src/providers/anthropic.js +83 -69
  29. package/src/providers/base.js +52 -0
  30. package/src/providers/deepseek.js +17 -34
  31. package/src/providers/gemini.js +226 -0
  32. package/src/providers/openai.js +36 -106
  33. package/src/providers/zhipu.js +136 -0
  34. package/src/ui/client/adapters/job-adapter.js +16 -26
  35. package/src/ui/client/api.js +134 -0
  36. package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -178
  37. package/src/ui/client/index.css +9 -0
  38. package/src/ui/client/index.html +1 -0
  39. package/src/ui/client/main.jsx +18 -15
  40. package/src/ui/client/time-store.js +161 -0
  41. package/src/ui/config-bridge.js +15 -24
  42. package/src/ui/config-bridge.node.js +15 -24
  43. package/src/ui/dist/assets/{index-WgJUlSmE.js → index-DqkbzXZ1.js} +1408 -771
  44. package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
  45. package/src/ui/dist/index.html +3 -2
  46. package/src/ui/public/favicon.svg +12 -0
  47. package/src/ui/server.js +231 -33
  48. package/src/ui/transformers/status-transformer.js +18 -31
  49. package/src/utils/dag.js +8 -4
  50. package/src/utils/duration.js +13 -19
  51. package/src/utils/formatters.js +27 -0
  52. package/src/utils/geometry-equality.js +83 -0
  53. package/src/utils/pipelines.js +5 -1
  54. package/src/utils/time-utils.js +40 -0
  55. package/src/utils/token-cost-calculator.js +4 -7
  56. package/src/utils/ui.jsx +14 -16
  57. package/src/components/ui/select.jsx +0 -27
  58. package/src/lib/utils.js +0 -6
  59. package/src/ui/client/hooks/useTicker.js +0 -26
  60. package/src/ui/config-bridge.browser.js +0 -149
  61. package/src/ui/dist/assets/style-x0V-5m8e.css +0 -62
@@ -1,45 +1,10 @@
1
1
  import { useState, useEffect, useRef, useCallback, useMemo } from "react";
2
+ import { useTransition } from "react";
2
3
  import { adaptJobDetail } from "../adapters/job-adapter.js";
3
4
 
4
5
  // Export debounce constant for tests
5
6
  export const REFRESH_DEBOUNCE_MS = 200;
6
7
 
7
- // Instrumentation helper for useJobDetailWithUpdates
8
- const createHookLogger = (jobId) => {
9
- const prefix = `[useJobDetailWithUpdates:${jobId || "unknown"}]`;
10
- return {
11
- log: (message, data = null) => {
12
- console.log(`${prefix} ${message}`, data ? data : "");
13
- },
14
- warn: (message, data = null) => {
15
- console.warn(`${prefix} ${message}`, data ? data : "");
16
- },
17
- error: (message, data = null) => {
18
- console.error(`${prefix} ${message}`, data ? data : "");
19
- },
20
- group: (label) => console.group(`${prefix} ${label}`),
21
- groupEnd: () => console.groupEnd(),
22
- table: (data, title) => {
23
- console.log(`${prefix} ${title}:`);
24
- console.table(data);
25
- },
26
- sse: (eventType, eventData) => {
27
- console.log(
28
- `%c${prefix} SSE Event: ${eventType}`,
29
- "color: #0066cc; font-weight: bold;",
30
- eventData
31
- );
32
- },
33
- state: (stateName, value) => {
34
- console.log(
35
- `%c${prefix} State Change: ${stateName}`,
36
- "color: #006600; font-weight: bold;",
37
- value
38
- );
39
- },
40
- };
41
- };
42
-
43
8
  /**
44
9
  * fetchJobDetail - Extracted fetch logic for job details
45
10
  *
@@ -173,12 +138,13 @@ function matchesJobTasksStatusPath(path, jobId) {
173
138
  * @returns {Object} { data, loading, error, connectionStatus }
174
139
  */
175
140
  export function useJobDetailWithUpdates(jobId) {
176
- const logger = useMemo(() => createHookLogger(jobId), [jobId]);
177
-
178
141
  const [data, setData] = useState(null);
179
142
  const [loading, setLoading] = useState(true);
180
143
  const [error, setError] = useState(null);
181
144
  const [connectionStatus, setConnectionStatus] = useState("disconnected");
145
+ const [isRefreshing, setIsRefreshing] = useState(false);
146
+ const [isPending, startTransition] = useTransition();
147
+ const [isHydrated, setIsHydrated] = useState(false);
182
148
 
183
149
  const esRef = useRef(null);
184
150
  const reconnectTimer = useRef(null);
@@ -187,79 +153,46 @@ export function useJobDetailWithUpdates(jobId) {
187
153
  const mountedRef = useRef(true);
188
154
  const refetchTimerRef = useRef(null);
189
155
 
190
- // Log hook initialization and state changes
191
- useEffect(() => {
192
- logger.group("Hook Initialization");
193
- logger.log("Job ID:", jobId);
194
- logger.log("Initial state:", { data, loading, error, connectionStatus });
195
- logger.groupEnd();
196
- }, [jobId, logger]);
197
-
198
- useEffect(() => {
199
- logger.state("data", data);
200
- }, [data, logger]);
201
-
202
- useEffect(() => {
203
- logger.state("loading", loading);
204
- }, [loading, logger]);
205
-
206
- useEffect(() => {
207
- logger.state("error", error);
208
- }, [error, logger]);
209
-
210
- useEffect(() => {
211
- logger.state("connectionStatus", connectionStatus);
212
- }, [connectionStatus, logger]);
213
-
214
156
  // Debounced refetch helper (called directly from handlers)
215
157
  const scheduleDebouncedRefetch = useCallback(
216
158
  (context = {}) => {
217
- logger.group("Debounced Refetch Request");
218
- logger.log("Request context:", context);
219
- logger.log("Scheduling debounced refetch");
220
159
  if (refetchTimerRef.current) {
221
- logger.log("Clearing existing refetch timer");
222
160
  clearTimeout(refetchTimerRef.current);
223
161
  }
224
162
  refetchTimerRef.current = setTimeout(async () => {
225
163
  if (!mountedRef.current || !hydratedRef.current) {
226
- logger.warn(
227
- "Refetch aborted - component not mounted or not hydrated",
228
- { mounted: mountedRef.current, hydrated: hydratedRef.current }
229
- );
230
- logger.groupEnd();
231
164
  return;
232
165
  }
233
- logger.log("Executing debounced refetch");
234
- logger.log("Refetch jobId:", jobId);
235
166
  const abortController = new AbortController();
236
167
  try {
168
+ startTransition(() => {
169
+ setIsRefreshing(true);
170
+ });
171
+
237
172
  const jobData = await fetchJobDetail(jobId, {
238
173
  signal: abortController.signal,
239
174
  });
240
- logger.log("Refetch response received");
241
- logger.log("Refetch job data preview:", {
242
- status: jobData?.status,
243
- hasTasks: !!jobData?.tasks,
244
- taskKeys: jobData?.tasks ? Object.keys(jobData.tasks) : [],
245
- });
175
+
246
176
  if (mountedRef.current) {
247
- logger.log("Refetch successful, updating data");
248
- setData(jobData);
249
- setError(null);
250
- } else {
251
- logger.warn("Refetch completed but component is unmounted");
177
+ startTransition(() => {
178
+ setData(jobData);
179
+ setError(null);
180
+ setIsRefreshing(false);
181
+ });
252
182
  }
253
183
  } catch (err) {
254
- logger.error("Failed to refetch job detail:", err);
255
- if (mountedRef.current) setError(err.message);
184
+ if (mountedRef.current) {
185
+ startTransition(() => {
186
+ setError(err.message);
187
+ setIsRefreshing(false);
188
+ });
189
+ }
256
190
  } finally {
257
191
  refetchTimerRef.current = null;
258
- logger.groupEnd();
259
192
  }
260
193
  }, REFRESH_DEBOUNCE_MS);
261
194
  },
262
- [jobId, logger]
195
+ [jobId]
263
196
  );
264
197
 
265
198
  // Reset state when jobId changes
@@ -268,6 +201,7 @@ export function useJobDetailWithUpdates(jobId) {
268
201
  setLoading(true);
269
202
  setError(null);
270
203
  setConnectionStatus("disconnected");
204
+ setIsHydrated(false);
271
205
  hydratedRef.current = false;
272
206
  eventQueue.current = [];
273
207
  if (refetchTimerRef.current) {
@@ -281,109 +215,98 @@ export function useJobDetailWithUpdates(jobId) {
281
215
  if (!jobId || !mountedRef.current) return;
282
216
 
283
217
  const doFetch = async () => {
284
- logger.group("Initial Data Fetch");
285
218
  try {
286
- setLoading(true);
219
+ // Only set loading to true if we haven't hydrated yet
220
+ if (!hydratedRef.current) {
221
+ setLoading(true);
222
+ }
287
223
  setError(null);
288
- logger.log("Starting initial job data fetch");
289
224
 
290
225
  const jobData = await fetchJobDetail(jobId);
291
- logger.log("Initial fetch successful", jobData);
292
226
 
293
227
  // Apply any queued events to the fresh data (purely), and detect if a refetch is needed
294
228
  let finalData = jobData;
295
229
  let queuedNeedsRefetch = false;
296
230
  if (eventQueue.current.length > 0) {
297
- logger.log(`Processing ${eventQueue.current.length} queued events`);
298
231
  for (const ev of eventQueue.current) {
299
- logger.log("Processing queued event:", ev);
300
232
  if (ev.type === "state:change") {
301
233
  const d = (ev.payload && (ev.payload.data || ev.payload)) || {};
302
234
  if (
303
235
  typeof d.path === "string" &&
304
236
  matchesJobTasksStatusPath(d.path, jobId)
305
237
  ) {
306
- logger.log(
307
- "Queued state:change matches tasks-status path, scheduling refetch"
308
- );
309
238
  queuedNeedsRefetch = true;
310
239
  continue; // don't apply to data
311
240
  }
312
241
  }
313
242
  finalData = applyJobEvent(finalData, ev, jobId);
314
- logger.log("Applied queued event, result:", finalData);
315
243
  }
316
244
  eventQueue.current = [];
317
245
  }
318
246
 
319
247
  if (mountedRef.current) {
320
- logger.log("Updating state with final data");
321
- setData(finalData);
322
- setError(null);
323
- hydratedRef.current = true;
324
- logger.log("Component hydrated");
248
+ startTransition(() => {
249
+ setData(finalData);
250
+ setError(null);
251
+ const wasHydrated = hydratedRef.current;
252
+ hydratedRef.current = true;
253
+
254
+ // Update state only when transitioning to hydrated
255
+ if (!wasHydrated) {
256
+ setIsHydrated(true);
257
+ setLoading(false);
258
+ }
259
+ });
325
260
 
326
261
  // Now that we're hydrated, if any queued path-only change was seen, schedule a refetch
327
262
  if (queuedNeedsRefetch) {
328
- logger.log("Scheduling refetch for queued path changes");
329
263
  scheduleDebouncedRefetch();
330
264
  }
331
265
  }
332
266
  } catch (err) {
333
- logger.error("Failed to fetch job detail:", err);
334
267
  if (mountedRef.current) {
335
- setError(err.message);
336
- setData(null);
268
+ startTransition(() => {
269
+ setError(err.message);
270
+ setData(null);
271
+ if (!hydratedRef.current) {
272
+ setLoading(false);
273
+ }
274
+ });
337
275
  }
338
276
  } finally {
339
- if (mountedRef.current) {
277
+ if (mountedRef.current && !hydratedRef.current) {
340
278
  setLoading(false);
341
279
  }
342
- logger.groupEnd();
343
280
  }
344
281
  };
345
282
 
346
283
  doFetch();
347
- }, [jobId, scheduleDebouncedRefetch, logger]);
284
+ }, [jobId, scheduleDebouncedRefetch]);
348
285
 
349
286
  // Set up SSE connection
350
287
  useEffect(() => {
351
288
  if (!jobId) {
352
- logger.log("SSE setup skipped - no jobId available", {
353
- hasJobId: !!jobId,
354
- hasExistingEs: !!esRef.current,
355
- isMounted: mountedRef.current,
356
- });
357
289
  return undefined;
358
290
  }
359
291
  if (esRef.current) {
360
- logger.log("Closing existing EventSource before reinitializing");
361
292
  try {
362
293
  esRef.current.close();
363
- } catch (err) {
364
- logger.warn("Error closing existing EventSource during reinit", err);
365
- }
294
+ } catch (err) {}
366
295
  esRef.current = null;
367
296
  }
368
297
 
369
- logger.group("SSE Connection Setup");
370
- logger.log("Setting up SSE connection for job:", jobId);
371
-
372
298
  // Helper to attach listeners to a given EventSource instance
373
299
  const attachListeners = (es) => {
374
300
  const onOpen = () => {
375
- logger.log("SSE connection opened");
376
301
  if (mountedRef.current) {
377
302
  setConnectionStatus("connected");
378
303
  }
379
304
  };
380
305
 
381
306
  const onError = () => {
382
- logger.warn("SSE connection error");
383
307
  // Derive state from readyState when possible
384
308
  try {
385
309
  const rs = esRef.current?.readyState;
386
- logger.log("SSE readyState:", rs);
387
310
  if (rs === 0) {
388
311
  if (mountedRef.current) setConnectionStatus("disconnected");
389
312
  } else if (rs === 1) {
@@ -394,7 +317,6 @@ export function useJobDetailWithUpdates(jobId) {
394
317
  if (mountedRef.current) setConnectionStatus("disconnected");
395
318
  }
396
319
  } catch (err) {
397
- logger.error("Error getting readyState:", err);
398
320
  if (mountedRef.current) setConnectionStatus("disconnected");
399
321
  }
400
322
 
@@ -404,7 +326,6 @@ export function useJobDetailWithUpdates(jobId) {
404
326
  esRef.current.readyState === 2 &&
405
327
  mountedRef.current
406
328
  ) {
407
- logger.log("Scheduling SSE reconnection");
408
329
  if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
409
330
  reconnectTimer.current = setTimeout(() => {
410
331
  if (!mountedRef.current) return;
@@ -419,7 +340,6 @@ export function useJobDetailWithUpdates(jobId) {
419
340
  const eventsUrl = jobId
420
341
  ? `/api/events?jobId=${encodeURIComponent(jobId)}`
421
342
  : "/api/events";
422
- logger.log("Creating new EventSource for reconnection");
423
343
  const newEs = new EventSource(eventsUrl);
424
344
  newEs.addEventListener("open", onOpen);
425
345
  newEs.addEventListener("job:updated", onJobUpdated);
@@ -431,7 +351,7 @@ export function useJobDetailWithUpdates(jobId) {
431
351
 
432
352
  esRef.current = newEs;
433
353
  } catch (err) {
434
- logger.error("Failed to reconnect SSE:", err);
354
+ console.error("Failed to reconnect SSE:", err);
435
355
  }
436
356
  }, 2000);
437
357
  }
@@ -442,18 +362,12 @@ export function useJobDetailWithUpdates(jobId) {
442
362
  const payload = evt && evt.data ? JSON.parse(evt.data) : null;
443
363
  const eventObj = { type, payload };
444
364
 
445
- logger.sse(type, payload);
446
-
447
365
  // Filter events by jobId - only process events for our job when jobId is present
448
366
  if (payload && payload.jobId && payload.jobId !== jobId) {
449
- logger.log(
450
- `Ignoring event for different job: ${payload.jobId} (current: ${jobId})`
451
- );
452
367
  return; // Ignore events for other jobs
453
368
  }
454
369
 
455
370
  if (!hydratedRef.current) {
456
- logger.log(`Queueing event until hydration: ${type}`);
457
371
  // Queue events until hydration completes
458
372
  eventQueue.current = (eventQueue.current || []).concat(eventObj);
459
373
  return;
@@ -462,55 +376,33 @@ export function useJobDetailWithUpdates(jobId) {
462
376
  // Path-matching state:change → schedule debounced refetch
463
377
  if (type === "state:change") {
464
378
  const d = (payload && (payload.data || payload)) || {};
465
- logger.log("Processing state:change event:", d);
466
379
  if (
467
380
  typeof d.path === "string" &&
468
381
  matchesJobTasksStatusPath(d.path, jobId)
469
382
  ) {
470
- logger.log(
471
- `state:change matches tasks-status path: ${d.path}, scheduling refetch`
472
- );
473
383
  scheduleDebouncedRefetch({
474
384
  reason: "state:change",
475
385
  path: d.path,
476
386
  });
477
387
  return; // no direct setData
478
- } else {
479
- logger.log(
480
- `state:change does not match tasks-status path: ${d.path}`
481
- );
482
388
  }
483
389
  }
484
390
 
485
391
  // Apply event using pure reducer (includes direct state:change with id)
486
- setData((prev) => {
487
- logger.group("Applying SSE event to state");
488
- logger.log("Previous state snapshot:", {
489
- hasTasks: !!prev?.tasks,
490
- taskKeys: prev?.tasks ? Object.keys(prev.tasks) : [],
491
- status: prev?.status,
492
- });
493
- logger.log("Incoming event payload:", payload);
494
- const next = applyJobEvent(prev, eventObj, jobId);
495
- try {
496
- if (JSON.stringify(prev) === JSON.stringify(next)) {
497
- logger.log("Event application resulted in no state change");
498
- logger.groupEnd();
499
- return prev;
392
+ startTransition(() => {
393
+ setData((prev) => {
394
+ const next = applyJobEvent(prev, eventObj, jobId);
395
+ try {
396
+ if (JSON.stringify(prev) === JSON.stringify(next)) {
397
+ return prev;
398
+ }
399
+ } catch (e) {
400
+ console.error("Error comparing states:", e);
500
401
  }
501
- } catch (e) {
502
- logger.error("Error comparing states:", e);
503
- }
504
- logger.log("Event applied, state updated", {
505
- hasTasks: !!next?.tasks,
506
- taskKeys: next?.tasks ? Object.keys(next.tasks) : [],
507
- status: next?.status,
402
+ return next;
508
403
  });
509
- logger.groupEnd();
510
- return next;
511
404
  });
512
405
  } catch (err) {
513
- logger.error("Failed to handle SSE event:", err);
514
406
  // Non-fatal: keep queue intact and continue
515
407
  // eslint-disable-next-line no-console
516
408
  console.error("Failed to handle SSE event:", err);
@@ -524,7 +416,6 @@ export function useJobDetailWithUpdates(jobId) {
524
416
  handleIncomingEvent("status:changed", evt);
525
417
  const onStateChange = (evt) => handleIncomingEvent("state:change", evt);
526
418
 
527
- logger.log("Attaching SSE event listeners");
528
419
  es.addEventListener("open", onOpen);
529
420
  es.addEventListener("job:updated", onJobUpdated);
530
421
  es.addEventListener("job:created", onJobCreated);
@@ -535,15 +426,12 @@ export function useJobDetailWithUpdates(jobId) {
535
426
 
536
427
  // Set connection status from readyState when possible
537
428
  if (es.readyState === 1 && mountedRef.current) {
538
- logger.log("SSE already open, setting connected");
539
429
  setConnectionStatus("connected");
540
430
  } else if (es.readyState === 0 && mountedRef.current) {
541
- logger.log("SSE connecting, setting disconnected");
542
431
  setConnectionStatus("disconnected");
543
432
  }
544
433
 
545
434
  return () => {
546
- logger.log("Cleaning up SSE connection");
547
435
  try {
548
436
  es.removeEventListener("open", onOpen);
549
437
  es.removeEventListener("job:updated", onJobUpdated);
@@ -553,9 +441,8 @@ export function useJobDetailWithUpdates(jobId) {
553
441
  es.removeEventListener("state:change", onStateChange);
554
442
  es.removeEventListener("error", onError);
555
443
  es.close();
556
- logger.log("SSE connection closed");
557
444
  } catch (err) {
558
- logger.error("Error during SSE cleanup:", err);
445
+ console.error("Error during SSE cleanup:", err);
559
446
  }
560
447
  if (reconnectTimer.current) {
561
448
  clearTimeout(reconnectTimer.current);
@@ -570,22 +457,19 @@ export function useJobDetailWithUpdates(jobId) {
570
457
  const eventsUrl = jobId
571
458
  ? `/api/events?jobId=${encodeURIComponent(jobId)}`
572
459
  : "/api/events";
573
- logger.log(`Creating EventSource with URL: ${eventsUrl}`);
574
460
  const es = new EventSource(eventsUrl);
575
461
  esRef.current = es;
576
462
 
577
463
  const cleanup = attachListeners(es);
578
- logger.groupEnd(); // End SSE Connection Setup group
579
464
  return cleanup;
580
465
  } catch (err) {
581
- logger.error("Failed to create SSE connection:", err);
466
+ console.error("Failed to create SSE connection:", err);
582
467
  if (mountedRef.current) {
583
468
  setConnectionStatus("error");
584
469
  }
585
- logger.groupEnd(); // End SSE Connection Setup group
586
470
  return undefined;
587
471
  }
588
- }, [jobId, scheduleDebouncedRefetch, logger]);
472
+ }, [jobId, scheduleDebouncedRefetch]);
589
473
 
590
474
  // Mount/unmount lifecycle: ensure mountedRef is true on mount (StrictMode-safe)
591
475
  useEffect(() => {
@@ -614,5 +498,8 @@ export function useJobDetailWithUpdates(jobId) {
614
498
  loading,
615
499
  error,
616
500
  connectionStatus,
501
+ isRefreshing,
502
+ isTransitioning: isPending,
503
+ isHydrated,
617
504
  };
618
505
  }
@@ -5,6 +5,15 @@
5
5
  .radix-themes {
6
6
  --heading-font-family: "Source Sans 3", sans-serif;
7
7
  --body-font-family: "Source Sans 3", sans-serif;
8
+ --cursor-button: pointer;
9
+ --cursor-checkbox: pointer;
10
+ --cursor-disabled: default;
11
+ --cursor-link: pointer;
12
+ --cursor-menu-item: pointer;
13
+ --cursor-radio: pointer;
14
+ --cursor-slider-thumb: grab;
15
+ --cursor-slider-thumb-active: grabbing;
16
+ --cursor-switch: pointer;
8
17
  }
9
18
 
10
19
  /* Reset and base styles */
@@ -10,6 +10,7 @@
10
10
  rel="stylesheet"
11
11
  />
12
12
  <title>Prompt Pipeline Dashboard</title>
13
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
13
14
  </head>
14
15
  <body>
15
16
  <div id="root"></div>
@@ -18,23 +18,26 @@ import PromptPipelineDashboard from "@/pages/PromptPipelineDashboard.jsx";
18
18
  import PipelineDetail from "@/pages/PipelineDetail.jsx";
19
19
  import Code from "@/pages/Code.jsx";
20
20
  import { Theme } from "@radix-ui/themes";
21
+ import { ToastProvider } from "@/components/ui/toast.jsx";
21
22
 
22
23
  ReactDOM.createRoot(document.getElementById("root")).render(
23
24
  <React.StrictMode>
24
- <Theme
25
- accentColor="iris"
26
- grayColor="gray"
27
- panelBackground="solid"
28
- scaling="100%"
29
- radius="full"
30
- >
31
- <BrowserRouter>
32
- <Routes>
33
- <Route path="/" element={<PromptPipelineDashboard />} />
34
- <Route path="/pipeline/:jobId" element={<PipelineDetail />} />
35
- <Route path="/code" element={<Code />} />
36
- </Routes>
37
- </BrowserRouter>
38
- </Theme>
25
+ <ToastProvider>
26
+ <Theme
27
+ accentColor="iris"
28
+ grayColor="gray"
29
+ panelBackground="solid"
30
+ scaling="100%"
31
+ radius="full"
32
+ >
33
+ <BrowserRouter>
34
+ <Routes>
35
+ <Route path="/" element={<PromptPipelineDashboard />} />
36
+ <Route path="/pipeline/:jobId" element={<PipelineDetail />} />
37
+ <Route path="/code" element={<Code />} />
38
+ </Routes>
39
+ </BrowserRouter>
40
+ </Theme>
41
+ </ToastProvider>
39
42
  </React.StrictMode>
40
43
  );