@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.
- package/README.md +1 -2
- package/package.json +1 -2
- package/src/api/validators/json.js +39 -0
- package/src/components/DAGGrid.jsx +392 -303
- package/src/components/JobCard.jsx +14 -12
- package/src/components/JobDetail.jsx +54 -51
- package/src/components/JobTable.jsx +72 -23
- package/src/components/Layout.jsx +145 -42
- package/src/components/LiveText.jsx +47 -0
- package/src/components/PageSubheader.jsx +75 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -0
- package/src/components/UploadSeed.jsx +0 -70
- package/src/components/ui/Logo.jsx +16 -0
- package/src/components/ui/RestartJobModal.jsx +140 -0
- package/src/components/ui/toast.jsx +138 -0
- package/src/config/models.js +322 -0
- package/src/config/statuses.js +119 -0
- package/src/core/config.js +4 -34
- package/src/core/file-io.js +13 -28
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +65 -26
- package/src/core/status-writer.js +213 -58
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +321 -437
- package/src/llm/index.js +258 -86
- package/src/pages/Code.jsx +351 -0
- package/src/pages/PipelineDetail.jsx +124 -15
- package/src/pages/PromptPipelineDashboard.jsx +20 -88
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +20 -21
- package/src/providers/gemini.js +226 -0
- package/src/providers/openai.js +36 -106
- package/src/providers/zhipu.js +136 -0
- package/src/ui/client/adapters/job-adapter.js +42 -28
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -179
- package/src/ui/client/index.css +15 -0
- package/src/ui/client/index.html +2 -1
- package/src/ui/client/main.jsx +19 -14
- package/src/ui/client/time-store.js +161 -0
- package/src/ui/config-bridge.js +15 -24
- package/src/ui/config-bridge.node.js +15 -24
- package/src/ui/dist/assets/{index-CxcrauYR.js → index-DqkbzXZ1.js} +2132 -1086
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +4 -3
- package/src/ui/job-reader.js +0 -108
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +252 -0
- package/src/ui/sse-enhancer.js +0 -1
- package/src/ui/transformers/list-transformer.js +32 -12
- package/src/ui/transformers/status-transformer.js +29 -42
- package/src/utils/dag.js +8 -4
- package/src/utils/duration.js +13 -19
- package/src/utils/formatters.js +27 -0
- package/src/utils/geometry-equality.js +83 -0
- package/src/utils/pipelines.js +5 -1
- package/src/utils/time-utils.js +40 -0
- package/src/utils/token-cost-calculator.js +294 -0
- package/src/utils/ui.jsx +18 -20
- package/src/components/ui/select.jsx +0 -27
- package/src/lib/utils.js +0 -6
- package/src/ui/client/hooks/useTicker.js +0 -26
- package/src/ui/config-bridge.browser.js +0 -149
- 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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
177
|
+
startTransition(() => {
|
|
178
|
+
setData(jobData);
|
|
179
|
+
setError(null);
|
|
180
|
+
setIsRefreshing(false);
|
|
181
|
+
});
|
|
253
182
|
}
|
|
254
183
|
} catch (err) {
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/ui/client/index.css
CHANGED
|
@@ -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 {
|
package/src/ui/client/index.html
CHANGED
|
@@ -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=
|
|
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>
|
package/src/ui/client/main.jsx
CHANGED
|
@@ -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
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
);
|