@ryanfw/prompt-orchestration-pipeline 0.11.0 → 0.13.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/package.json +11 -1
- package/src/cli/analyze-task.js +51 -0
- package/src/cli/index.js +8 -0
- package/src/components/AddPipelineSidebar.jsx +144 -0
- package/src/components/AnalysisProgressTray.jsx +87 -0
- package/src/components/DAGGrid.jsx +157 -47
- package/src/components/JobTable.jsx +4 -3
- package/src/components/Layout.jsx +142 -139
- package/src/components/MarkdownRenderer.jsx +149 -0
- package/src/components/PipelineDAGGrid.jsx +404 -0
- package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
- package/src/components/SchemaPreviewPanel.jsx +97 -0
- package/src/components/StageTimeline.jsx +36 -0
- package/src/components/TaskAnalysisDisplay.jsx +227 -0
- package/src/components/TaskCreationSidebar.jsx +447 -0
- package/src/components/TaskDetailSidebar.jsx +119 -117
- package/src/components/TaskFilePane.jsx +94 -39
- package/src/components/ui/RestartJobModal.jsx +26 -6
- package/src/components/ui/StopJobModal.jsx +183 -0
- package/src/components/ui/button.jsx +59 -27
- package/src/components/ui/sidebar.jsx +118 -0
- package/src/config/models.js +99 -67
- package/src/core/config.js +11 -4
- package/src/core/lifecycle-policy.js +62 -0
- package/src/core/pipeline-runner.js +312 -217
- package/src/core/status-writer.js +84 -0
- package/src/llm/index.js +129 -9
- package/src/pages/Code.jsx +8 -1
- package/src/pages/PipelineDetail.jsx +84 -2
- package/src/pages/PipelineList.jsx +214 -0
- package/src/pages/PipelineTypeDetail.jsx +234 -0
- package/src/pages/PromptPipelineDashboard.jsx +10 -11
- package/src/providers/deepseek.js +76 -16
- package/src/providers/openai.js +61 -34
- package/src/task-analysis/enrichers/analysis-writer.js +62 -0
- package/src/task-analysis/enrichers/schema-deducer.js +145 -0
- package/src/task-analysis/enrichers/schema-writer.js +74 -0
- package/src/task-analysis/extractors/artifacts.js +137 -0
- package/src/task-analysis/extractors/llm-calls.js +176 -0
- package/src/task-analysis/extractors/stages.js +51 -0
- package/src/task-analysis/index.js +103 -0
- package/src/task-analysis/parser.js +28 -0
- package/src/task-analysis/utils/ast.js +43 -0
- package/src/ui/client/adapters/job-adapter.js +60 -0
- package/src/ui/client/api.js +233 -8
- package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
- package/src/ui/client/hooks/useJobList.js +14 -1
- package/src/ui/client/index.css +64 -0
- package/src/ui/client/main.jsx +4 -0
- package/src/ui/client/sse-fetch.js +120 -0
- package/src/ui/dist/app.js +262 -0
- package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
- package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
- package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
- package/src/ui/dist/favicon.svg +12 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
- package/src/ui/endpoints/file-endpoints.js +330 -0
- package/src/ui/endpoints/job-control-endpoints.js +1001 -0
- package/src/ui/endpoints/job-endpoints.js +62 -0
- package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
- package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
- package/src/ui/endpoints/pipelines-endpoint.js +133 -0
- package/src/ui/endpoints/schema-file-endpoint.js +105 -0
- package/src/ui/endpoints/sse-endpoints.js +223 -0
- package/src/ui/endpoints/state-endpoint.js +85 -0
- package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
- package/src/ui/endpoints/task-creation-endpoint.js +114 -0
- package/src/ui/endpoints/task-save-endpoint.js +101 -0
- package/src/ui/endpoints/upload-endpoints.js +406 -0
- package/src/ui/express-app.js +227 -0
- package/src/ui/lib/analysis-lock.js +67 -0
- package/src/ui/lib/sse.js +30 -0
- package/src/ui/server.js +42 -1880
- package/src/ui/sse-broadcast.js +93 -0
- package/src/ui/utils/http-utils.js +139 -0
- package/src/ui/utils/mime-types.js +196 -0
- package/src/ui/utils/slug.js +31 -0
- package/src/ui/vite.config.js +22 -0
- package/src/ui/watcher.js +28 -2
- package/src/utils/jobs.js +39 -0
- package/src/ui/dist/assets/index-DeDzq-Kk.js +0 -23863
- package/src/ui/dist/assets/style-aBtD_Yrs.css +0 -62
package/src/ui/client/api.js
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Restart a job with clean-slate mode
|
|
6
|
+
* Restart a job with clean-slate mode or from a specific task
|
|
7
7
|
*
|
|
8
8
|
* @param {string} jobId - The ID of the job to restart
|
|
9
9
|
* @param {Object} opts - Options object
|
|
10
|
-
* @param {
|
|
11
|
-
* @param {boolean} opts.
|
|
10
|
+
* @param {string} [opts.fromTask] - Task ID to restart from (inclusive)
|
|
11
|
+
* @param {boolean} [opts.singleTask] - Whether to run only the target task and then stop
|
|
12
|
+
* @param {Object} [opts.options] - Additional options for the restart
|
|
13
|
+
* @param {boolean} [opts.options.clearTokenUsage=true] - Whether to clear token usage
|
|
12
14
|
* @returns {Promise<Object>} Parsed JSON response from the server
|
|
13
15
|
* @throws {Object} Structured error object with { code, message } for non-2xx responses
|
|
14
16
|
*/
|
|
@@ -19,8 +21,16 @@ export async function restartJob(jobId, opts = {}) {
|
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
const requestBody = opts.fromTask
|
|
22
|
-
? {
|
|
23
|
-
|
|
24
|
+
? {
|
|
25
|
+
fromTask: opts.fromTask,
|
|
26
|
+
options,
|
|
27
|
+
...(opts.singleTask !== undefined && { singleTask: opts.singleTask }),
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
mode: "clean-slate",
|
|
31
|
+
options,
|
|
32
|
+
...(opts.singleTask !== undefined && { singleTask: opts.singleTask }),
|
|
33
|
+
};
|
|
24
34
|
|
|
25
35
|
try {
|
|
26
36
|
const response = await fetch(
|
|
@@ -67,6 +77,164 @@ export async function restartJob(jobId, opts = {}) {
|
|
|
67
77
|
}
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Rescan a job to synchronize tasks with the pipeline definition, detecting both added and removed tasks.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} jobId - The ID of the job to rescan
|
|
84
|
+
* @returns {Promise<Object>} Parsed JSON response from the server
|
|
85
|
+
* @throws {Object} Structured error object with { code, message } for non-2xx responses
|
|
86
|
+
*/
|
|
87
|
+
export async function rescanJob(jobId) {
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(
|
|
90
|
+
`/api/jobs/${encodeURIComponent(jobId)}/rescan`,
|
|
91
|
+
{
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
// Try to parse error response, fall back to status text if parsing fails
|
|
101
|
+
let errorData;
|
|
102
|
+
try {
|
|
103
|
+
errorData = await response.json();
|
|
104
|
+
} catch {
|
|
105
|
+
errorData = { message: response.statusText };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Throw structured error with code and message
|
|
109
|
+
throw {
|
|
110
|
+
code: errorData.code || getErrorCodeFromStatus(response.status),
|
|
111
|
+
message:
|
|
112
|
+
errorData.message || getErrorMessageFromStatus(response.status),
|
|
113
|
+
status: response.status,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Return parsed JSON for successful responses
|
|
118
|
+
return await response.json();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Re-throw structured errors as-is
|
|
121
|
+
if (error.code && error.message) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle network errors or other unexpected errors
|
|
126
|
+
throw {
|
|
127
|
+
code: "network_error",
|
|
128
|
+
message: error.message || "Failed to connect to server",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start a specific pending task for a job that is not actively running
|
|
135
|
+
*
|
|
136
|
+
* @param {string} jobId - The ID of the job
|
|
137
|
+
* @param {string} taskId - The ID of the task to start
|
|
138
|
+
* @returns {Promise<Object>} Parsed JSON response from the server
|
|
139
|
+
* @throws {Object} Structured error object with { code, message } for non-2xx responses
|
|
140
|
+
*/
|
|
141
|
+
export async function startTask(jobId, taskId) {
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(
|
|
144
|
+
`/api/jobs/${encodeURIComponent(jobId)}/tasks/${encodeURIComponent(taskId)}/start`,
|
|
145
|
+
{
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
// Try to parse error response, fall back to status text if parsing fails
|
|
155
|
+
let errorData;
|
|
156
|
+
try {
|
|
157
|
+
errorData = await response.json();
|
|
158
|
+
} catch {
|
|
159
|
+
errorData = { message: response.statusText };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Throw structured error with code and message
|
|
163
|
+
throw {
|
|
164
|
+
code: errorData.code || getErrorCodeFromStatus(response.status),
|
|
165
|
+
message: getStartTaskErrorMessage(errorData, response.status),
|
|
166
|
+
status: response.status,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Return parsed JSON for successful responses
|
|
171
|
+
return await response.json();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Re-throw structured errors as-is
|
|
174
|
+
if (error.code && error.message) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Handle network errors or other unexpected errors
|
|
179
|
+
throw {
|
|
180
|
+
code: "network_error",
|
|
181
|
+
message: error.message || "Failed to connect to server",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stop a running job's pipeline
|
|
188
|
+
*
|
|
189
|
+
* @param {string} jobId - The ID of the job to stop
|
|
190
|
+
* @returns {Promise<Object>} Parsed JSON response from the server
|
|
191
|
+
* @throws {Object} Structured error object with { code, message } for non-2xx responses
|
|
192
|
+
*/
|
|
193
|
+
export async function stopJob(jobId) {
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(
|
|
196
|
+
`/api/jobs/${encodeURIComponent(jobId)}/stop`,
|
|
197
|
+
{
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
// Try to parse error response, fall back to status text if parsing fails
|
|
207
|
+
let errorData;
|
|
208
|
+
try {
|
|
209
|
+
errorData = await response.json();
|
|
210
|
+
} catch {
|
|
211
|
+
errorData = { message: response.statusText };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Throw structured error with code and message
|
|
215
|
+
throw {
|
|
216
|
+
code: errorData.code || getErrorCodeFromStatus(response.status),
|
|
217
|
+
message: getStopErrorMessage(errorData, response.status),
|
|
218
|
+
status: response.status,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Return parsed JSON for successful responses
|
|
223
|
+
return await response.json();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// Re-throw structured errors as-is
|
|
226
|
+
if (error.code && error.message) {
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Handle network errors or other unexpected errors
|
|
231
|
+
throw {
|
|
232
|
+
code: "network_error",
|
|
233
|
+
message: error.message || "Failed to connect to server",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
70
238
|
/**
|
|
71
239
|
* Map HTTP status codes to error codes for structured error handling
|
|
72
240
|
*/
|
|
@@ -108,9 +276,6 @@ function getRestartErrorMessage(errorData, status) {
|
|
|
108
276
|
if (errorData.code === "job_running") {
|
|
109
277
|
return "Job is currently running; restart is unavailable.";
|
|
110
278
|
}
|
|
111
|
-
if (errorData.code === "unsupported_lifecycle") {
|
|
112
|
-
return "Job must be in current to restart.";
|
|
113
|
-
}
|
|
114
279
|
if (errorData.message?.includes("job_running")) {
|
|
115
280
|
return "Job is currently running; restart is unavailable.";
|
|
116
281
|
}
|
|
@@ -132,3 +297,63 @@ function getRestartErrorMessage(errorData, status) {
|
|
|
132
297
|
// Fall back to provided message or default
|
|
133
298
|
return errorData.message || "Failed to restart job.";
|
|
134
299
|
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get specific error message from error response for start task functionality
|
|
303
|
+
*/
|
|
304
|
+
function getStartTaskErrorMessage(errorData, status) {
|
|
305
|
+
// Handle specific 409 conflict errors
|
|
306
|
+
if (status === 409) {
|
|
307
|
+
if (errorData.code === "job_running") {
|
|
308
|
+
return "Job is currently running; start is unavailable.";
|
|
309
|
+
}
|
|
310
|
+
if (errorData.code === "dependencies_not_satisfied") {
|
|
311
|
+
return "Dependencies not satisfied for task.";
|
|
312
|
+
}
|
|
313
|
+
if (errorData.code === "unsupported_lifecycle") {
|
|
314
|
+
return "Job must be in current to start a task.";
|
|
315
|
+
}
|
|
316
|
+
return "Request conflict.";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Handle 404 errors
|
|
320
|
+
if (status === 404) {
|
|
321
|
+
return "Job not found.";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Handle 400 errors
|
|
325
|
+
if (status === 400) {
|
|
326
|
+
return errorData.message || "Bad request";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Handle 500 errors
|
|
330
|
+
if (status === 500) {
|
|
331
|
+
return "Internal server error";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fall back to provided message or default
|
|
335
|
+
return errorData.message || "Failed to start task.";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get specific error message from error response for stop functionality
|
|
340
|
+
*/
|
|
341
|
+
function getStopErrorMessage(errorData, status) {
|
|
342
|
+
// Handle 404 errors
|
|
343
|
+
if (status === 404) {
|
|
344
|
+
return "Job not found.";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Handle 409 errors
|
|
348
|
+
if (status === 409) {
|
|
349
|
+
return errorData.message || "Job stop is already in progress.";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Handle 500 errors
|
|
353
|
+
if (status === 500) {
|
|
354
|
+
return "Internal server error";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Fall back to provided message or default
|
|
358
|
+
return errorData.message || `Request failed with status ${status}`;
|
|
359
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from "react";
|
|
2
|
+
import { fetchSSE } from "../sse-fetch.js";
|
|
3
|
+
|
|
4
|
+
const initialState = {
|
|
5
|
+
status: "idle",
|
|
6
|
+
pipelineSlug: null,
|
|
7
|
+
totalTasks: 0,
|
|
8
|
+
completedTasks: 0,
|
|
9
|
+
totalArtifacts: 0,
|
|
10
|
+
completedArtifacts: 0,
|
|
11
|
+
currentTask: null,
|
|
12
|
+
currentArtifact: null,
|
|
13
|
+
error: null,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* useAnalysisProgress - Hook for managing pipeline analysis progress via SSE
|
|
18
|
+
*
|
|
19
|
+
* Provides state and controls for triggering pipeline analysis with real-time
|
|
20
|
+
* progress updates via Server-Sent Events.
|
|
21
|
+
*
|
|
22
|
+
* @returns {Object} { ...state, startAnalysis, reset }
|
|
23
|
+
*/
|
|
24
|
+
export function useAnalysisProgress() {
|
|
25
|
+
const [state, setState] = useState(initialState);
|
|
26
|
+
const cancelRef = useRef(null);
|
|
27
|
+
|
|
28
|
+
const reset = useCallback(() => {
|
|
29
|
+
if (cancelRef.current) {
|
|
30
|
+
cancelRef.current();
|
|
31
|
+
cancelRef.current = null;
|
|
32
|
+
}
|
|
33
|
+
setState(initialState);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const handleEvent = useCallback((event, data) => {
|
|
37
|
+
switch (event) {
|
|
38
|
+
case "started":
|
|
39
|
+
setState((prev) => ({
|
|
40
|
+
...prev,
|
|
41
|
+
status: "running",
|
|
42
|
+
pipelineSlug: data.pipelineSlug || prev.pipelineSlug,
|
|
43
|
+
totalTasks: data.totalTasks || 0,
|
|
44
|
+
totalArtifacts: data.totalArtifacts || 0,
|
|
45
|
+
}));
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case "task:start":
|
|
49
|
+
setState((prev) => ({
|
|
50
|
+
...prev,
|
|
51
|
+
currentTask: data.taskId || null,
|
|
52
|
+
}));
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case "artifact:start":
|
|
56
|
+
setState((prev) => ({
|
|
57
|
+
...prev,
|
|
58
|
+
currentArtifact: data.artifactName || null,
|
|
59
|
+
}));
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case "artifact:complete":
|
|
63
|
+
setState((prev) => ({
|
|
64
|
+
...prev,
|
|
65
|
+
completedArtifacts: prev.completedArtifacts + 1,
|
|
66
|
+
}));
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case "task:complete":
|
|
70
|
+
setState((prev) => ({
|
|
71
|
+
...prev,
|
|
72
|
+
completedTasks: prev.completedTasks + 1,
|
|
73
|
+
currentArtifact: null,
|
|
74
|
+
}));
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case "complete":
|
|
78
|
+
setState((prev) => ({
|
|
79
|
+
...prev,
|
|
80
|
+
status: "complete",
|
|
81
|
+
currentTask: null,
|
|
82
|
+
currentArtifact: null,
|
|
83
|
+
}));
|
|
84
|
+
if (cancelRef.current) {
|
|
85
|
+
cancelRef.current = null;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case "error":
|
|
90
|
+
setState((prev) => ({
|
|
91
|
+
...prev,
|
|
92
|
+
status: "error",
|
|
93
|
+
error: data.message || "Unknown error",
|
|
94
|
+
}));
|
|
95
|
+
if (cancelRef.current) {
|
|
96
|
+
cancelRef.current = null;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
default:
|
|
101
|
+
console.warn("Unknown SSE event:", event);
|
|
102
|
+
}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const startAnalysis = useCallback(
|
|
106
|
+
async (pipelineSlug) => {
|
|
107
|
+
// Reset previous state
|
|
108
|
+
if (cancelRef.current) {
|
|
109
|
+
cancelRef.current();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setState({
|
|
113
|
+
...initialState,
|
|
114
|
+
status: "connecting",
|
|
115
|
+
pipelineSlug,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const url = `/api/pipelines/${encodeURIComponent(pipelineSlug)}/analyze`;
|
|
119
|
+
|
|
120
|
+
const sse = fetchSSE(
|
|
121
|
+
url,
|
|
122
|
+
{},
|
|
123
|
+
handleEvent,
|
|
124
|
+
// Error handler for HTTP errors
|
|
125
|
+
(errorData) => {
|
|
126
|
+
setState((prev) => ({
|
|
127
|
+
...prev,
|
|
128
|
+
status: "error",
|
|
129
|
+
error:
|
|
130
|
+
errorData.message ||
|
|
131
|
+
`HTTP ${errorData.status || "error"}: ${errorData.statusText || "Unknown error"}`,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
cancelRef.current = sse.cancel;
|
|
136
|
+
},
|
|
137
|
+
[handleEvent]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
...state,
|
|
142
|
+
startAnalysis,
|
|
143
|
+
reset,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -20,7 +20,20 @@ export function useJobList() {
|
|
|
20
20
|
setData(null);
|
|
21
21
|
} else {
|
|
22
22
|
const json = await res.json();
|
|
23
|
-
|
|
23
|
+
if (json && typeof json === "object" && "ok" in json) {
|
|
24
|
+
if (json.ok && Array.isArray(json.data)) {
|
|
25
|
+
setData(json.data);
|
|
26
|
+
} else if (!json.ok) {
|
|
27
|
+
setError({ code: json.code, message: json.message });
|
|
28
|
+
setData(null);
|
|
29
|
+
} else {
|
|
30
|
+
// Fallback: data is not an array; treat as empty to avoid crashes
|
|
31
|
+
setData([]);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// Legacy path: response is already an array
|
|
35
|
+
setData(Array.isArray(json) ? json : []);
|
|
36
|
+
}
|
|
24
37
|
}
|
|
25
38
|
} catch (err) {
|
|
26
39
|
if (err.name === "AbortError") {
|
package/src/ui/client/index.css
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
@import "@radix-ui/themes/styles.css" layer(radix);
|
|
3
3
|
@import "tailwindcss";
|
|
4
4
|
|
|
5
|
+
/* Steel Terminal Theme - CSS Variables */
|
|
6
|
+
:root {
|
|
7
|
+
/* Base colors */
|
|
8
|
+
--background: 30 5% 97%; /* Soft warm gray */
|
|
9
|
+
--foreground: 210 15% 15%; /* Deep slate */
|
|
10
|
+
--card: 0 0% 100%; /* Pure white */
|
|
11
|
+
--card-foreground: 210 15% 15%; /* Deep slate */
|
|
12
|
+
--popover: 0 0% 100%; /* Pure white */
|
|
13
|
+
--popover-foreground: 210 15% 15%; /* Deep slate */
|
|
14
|
+
|
|
15
|
+
/* Primary colors - Indigo for primary actions with clear active state */
|
|
16
|
+
--primary: 243deg 75% 59%; /* #4f46e5 - indigo-600 */
|
|
17
|
+
--primary-foreground: 0deg 0% 100%; /* #ffffff */
|
|
18
|
+
--primary-hover: 243deg 77% 49%; /* #4338ca - indigo-700 */
|
|
19
|
+
|
|
20
|
+
/* Secondary colors - For outline buttons */
|
|
21
|
+
--secondary: 210deg 20% 98%; /* #f8fafc - slate-50 */
|
|
22
|
+
--secondary-foreground: 243deg 75% 30%; /* #312e81 - indigo-950 */
|
|
23
|
+
|
|
24
|
+
/* Destructive colors - For danger actions */
|
|
25
|
+
--destructive: 0deg 84% 60%; /* #dc2626 - red-600 */
|
|
26
|
+
--destructive-foreground: 0deg 0% 100%; /* #ffffff */
|
|
27
|
+
|
|
28
|
+
/* Muted colors - For disabled/subtle states */
|
|
29
|
+
--muted: 210deg 40% 96%; /* #f1f5f9 - slate-100 */
|
|
30
|
+
--muted-foreground: 243deg 50% 45%; /* #6366f1 - indigo-500 */
|
|
31
|
+
|
|
32
|
+
/* Accent colors - For highlights */
|
|
33
|
+
--accent: 210deg 40% 96%; /* #f1f5f9 - slate-100 */
|
|
34
|
+
--accent-foreground: 243deg 75% 59%; /* #4f46e5 - indigo-600 */
|
|
35
|
+
|
|
36
|
+
/* Border and input colors */
|
|
37
|
+
--border: 214deg 32% 91%; /* #e2e8f0 - slate-200 */
|
|
38
|
+
--input: 214deg 32% 91%; /* #e2e8f0 - slate-200 */
|
|
39
|
+
|
|
40
|
+
/* Ring - For focus states */
|
|
41
|
+
--ring: 243deg 75% 59%; /* #4f46e5 - indigo-600 */
|
|
42
|
+
|
|
43
|
+
/* Status colors */
|
|
44
|
+
--success: 142deg 76% 36%; /* #16a34a - green-600 */
|
|
45
|
+
--success-foreground: 0deg 0% 100%;
|
|
46
|
+
--warning: 38deg 92% 50%; /* #ca8a04 - yellow-600 */
|
|
47
|
+
--warning-foreground: 0deg 0% 100%;
|
|
48
|
+
--error: 0deg 84% 60%; /* #dc2626 - red-600 */
|
|
49
|
+
--error-foreground: 0deg 0% 100%;
|
|
50
|
+
--info: 215deg 90% 52%; /* #0284c7 - sky-600 */
|
|
51
|
+
--info-foreground: 0deg 0% 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
5
54
|
.radix-themes {
|
|
6
55
|
--heading-font-family: "Source Sans 3", sans-serif;
|
|
7
56
|
--body-font-family: "Source Sans 3", sans-serif;
|
|
@@ -44,3 +93,18 @@ body {
|
|
|
44
93
|
margin: 0 auto;
|
|
45
94
|
padding: 2rem;
|
|
46
95
|
}
|
|
96
|
+
|
|
97
|
+
/* Bouncing wave animation for typing indicators */
|
|
98
|
+
@keyframes bounce-wave {
|
|
99
|
+
0%,
|
|
100
|
+
100% {
|
|
101
|
+
transform: translateY(0);
|
|
102
|
+
}
|
|
103
|
+
50% {
|
|
104
|
+
transform: translateY(-8px);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.animate-bounce-wave {
|
|
109
|
+
animation: bounce-wave 0.6s ease-in-out infinite;
|
|
110
|
+
}
|
package/src/ui/client/main.jsx
CHANGED
|
@@ -17,6 +17,8 @@ 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
19
|
import Code from "@/pages/Code.jsx";
|
|
20
|
+
import PipelineList from "@/pages/PipelineList.jsx";
|
|
21
|
+
import PipelineTypeDetail from "@/pages/PipelineTypeDetail.jsx";
|
|
20
22
|
import { Theme } from "@radix-ui/themes";
|
|
21
23
|
import { ToastProvider } from "@/components/ui/toast.jsx";
|
|
22
24
|
|
|
@@ -34,6 +36,8 @@ ReactDOM.createRoot(document.getElementById("root")).render(
|
|
|
34
36
|
<Routes>
|
|
35
37
|
<Route path="/" element={<PromptPipelineDashboard />} />
|
|
36
38
|
<Route path="/pipeline/:jobId" element={<PipelineDetail />} />
|
|
39
|
+
<Route path="/pipelines" element={<PipelineList />} />
|
|
40
|
+
<Route path="/pipelines/:slug" element={<PipelineTypeDetail />} />
|
|
37
41
|
<Route path="/code" element={<Code />} />
|
|
38
42
|
</Routes>
|
|
39
43
|
</BrowserRouter>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Server-Sent Events from a fetch response stream.
|
|
3
|
+
* Supports POST requests (unlike native EventSource).
|
|
4
|
+
*
|
|
5
|
+
* @param {string} url - The URL to fetch
|
|
6
|
+
* @param {RequestInit} options - Fetch options (method defaults to POST)
|
|
7
|
+
* @param {function} onEvent - Callback for each SSE event: (eventName, parsedData) => void
|
|
8
|
+
* @param {function} onError - Callback for HTTP errors: (errorData) => void
|
|
9
|
+
* @returns {{ cancel: () => void }} Object with cancel method to abort the fetch
|
|
10
|
+
*/
|
|
11
|
+
export function fetchSSE(url, options = {}, onEvent, onError) {
|
|
12
|
+
if (typeof onEvent !== "function") {
|
|
13
|
+
throw new Error("onEvent callback is required");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const requestOptions = {
|
|
18
|
+
method: "POST",
|
|
19
|
+
...options,
|
|
20
|
+
signal: controller.signal,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
fetch(url, requestOptions)
|
|
24
|
+
.then(async (response) => {
|
|
25
|
+
// Handle HTTP errors before attempting to read stream
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
let errorData = {};
|
|
28
|
+
try {
|
|
29
|
+
errorData = await response.json();
|
|
30
|
+
} catch {
|
|
31
|
+
// If response isn't JSON, just use status text
|
|
32
|
+
errorData = {
|
|
33
|
+
ok: false,
|
|
34
|
+
code: "http_error",
|
|
35
|
+
message: response.statusText,
|
|
36
|
+
status: response.status,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (typeof onError === "function") {
|
|
40
|
+
onError(errorData);
|
|
41
|
+
} else {
|
|
42
|
+
console.error(`[sse-fetch] HTTP ${response.status}:`, errorData);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const reader = response.body.getReader();
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
let buffer = "";
|
|
50
|
+
|
|
51
|
+
while (true) {
|
|
52
|
+
const { done, value } = await reader.read();
|
|
53
|
+
if (done) break;
|
|
54
|
+
|
|
55
|
+
buffer += decoder.decode(value, { stream: true });
|
|
56
|
+
const events = buffer.split("\n\n");
|
|
57
|
+
|
|
58
|
+
// Keep the last partial event in the buffer
|
|
59
|
+
buffer = events.pop();
|
|
60
|
+
|
|
61
|
+
for (const eventText of events) {
|
|
62
|
+
if (!eventText.trim()) continue;
|
|
63
|
+
|
|
64
|
+
const event = parseSSEEvent(eventText);
|
|
65
|
+
if (event) {
|
|
66
|
+
onEvent(event.type, event.data);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Process any remaining content in buffer
|
|
72
|
+
if (buffer.trim()) {
|
|
73
|
+
const event = parseSSEEvent(buffer);
|
|
74
|
+
if (event) {
|
|
75
|
+
onEvent(event.type, event.data);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
.catch((error) => {
|
|
80
|
+
if (error.name === "AbortError") {
|
|
81
|
+
return; // Expected when cancel() is called
|
|
82
|
+
}
|
|
83
|
+
console.error("[sse-fetch] Error:", error);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
cancel: () => controller.abort(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse a single SSE event string.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} eventText - The raw SSE event text
|
|
95
|
+
* @returns {{ type: string, data: object }|null} Parsed event or null if invalid
|
|
96
|
+
*/
|
|
97
|
+
function parseSSEEvent(eventText) {
|
|
98
|
+
let type = null;
|
|
99
|
+
let data = null;
|
|
100
|
+
|
|
101
|
+
for (const line of eventText.split("\n")) {
|
|
102
|
+
const trimmedLine = line.trim();
|
|
103
|
+
if (trimmedLine.startsWith("event:")) {
|
|
104
|
+
type = trimmedLine.slice(6).trim();
|
|
105
|
+
} else if (trimmedLine.startsWith("data:")) {
|
|
106
|
+
const dataStr = trimmedLine.slice(5).trim();
|
|
107
|
+
try {
|
|
108
|
+
data = JSON.parse(dataStr);
|
|
109
|
+
} catch {
|
|
110
|
+
console.error("[sse-fetch] Failed to parse data:", dataStr);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!type || data === null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { type, data };
|
|
120
|
+
}
|