@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.
- 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 +13 -11
- package/src/components/JobDetail.jsx +41 -71
- package/src/components/JobTable.jsx +32 -22
- package/src/components/Layout.jsx +0 -21
- package/src/components/LiveText.jsx +47 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -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 +2 -164
- package/src/core/file-io.js +1 -1
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +52 -20
- package/src/core/status-writer.js +147 -3
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +267 -443
- package/src/llm/index.js +167 -52
- package/src/pages/Code.jsx +57 -3
- package/src/pages/PipelineDetail.jsx +92 -22
- package/src/pages/PromptPipelineDashboard.jsx +15 -36
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +17 -34
- 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 +16 -26
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -178
- package/src/ui/client/index.css +9 -0
- package/src/ui/client/index.html +1 -0
- package/src/ui/client/main.jsx +18 -15
- 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-WgJUlSmE.js → index-DqkbzXZ1.js} +1408 -771
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +3 -2
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +231 -33
- package/src/ui/transformers/status-transformer.js +18 -31
- 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 +4 -7
- package/src/utils/ui.jsx +14 -16
- 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-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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
177
|
+
startTransition(() => {
|
|
178
|
+
setData(jobData);
|
|
179
|
+
setError(null);
|
|
180
|
+
setIsRefreshing(false);
|
|
181
|
+
});
|
|
252
182
|
}
|
|
253
183
|
} catch (err) {
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/ui/client/index.css
CHANGED
|
@@ -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 */
|
package/src/ui/client/index.html
CHANGED
package/src/ui/client/main.jsx
CHANGED
|
@@ -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
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
);
|