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