@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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Heading, Table, Code, Text } from "@radix-ui/themes";
|
|
3
|
+
import Layout from "../components/Layout.jsx";
|
|
4
|
+
import PageSubheader from "../components/PageSubheader.jsx";
|
|
5
|
+
import { Button } from "../components/ui/button.jsx";
|
|
6
|
+
|
|
7
|
+
const ioFunctions = [
|
|
8
|
+
{
|
|
9
|
+
name: "writeArtifact",
|
|
10
|
+
description: "Write an artifact file",
|
|
11
|
+
params:
|
|
12
|
+
'name: string, content: string, options?: { mode?: "replace"|"append"=replace }',
|
|
13
|
+
returns: "Promise<string>",
|
|
14
|
+
notes: "Writes to {workDir}/files/artifacts; updates tasks-status.json",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "writeLog",
|
|
18
|
+
description: "Write a log file",
|
|
19
|
+
params:
|
|
20
|
+
'name: string, content: string, options?: { mode?: "append"|"replace"=append }',
|
|
21
|
+
returns: "Promise<string>",
|
|
22
|
+
notes:
|
|
23
|
+
"Writes to {workDir}/files/logs; default append; updates tasks-status.json",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "writeTmp",
|
|
27
|
+
description: "Write a temporary file",
|
|
28
|
+
params:
|
|
29
|
+
'name: string, content: string, options?: { mode?: "replace"|"append"=replace }',
|
|
30
|
+
returns: "Promise<string>",
|
|
31
|
+
notes: "Writes to {workDir}/files/tmp; updates tasks-status.json",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "readArtifact",
|
|
35
|
+
description: "Read an artifact file",
|
|
36
|
+
params: "name: string",
|
|
37
|
+
returns: "Promise<string>",
|
|
38
|
+
notes: "Reads from {workDir}/files/artifacts",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "readLog",
|
|
42
|
+
description: "Read a log file",
|
|
43
|
+
params: "name: string",
|
|
44
|
+
returns: "Promise<string>",
|
|
45
|
+
notes: "Reads from {workDir}/files/logs",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "readTmp",
|
|
49
|
+
description: "Read a temporary file",
|
|
50
|
+
params: "name: string",
|
|
51
|
+
returns: "Promise<string>",
|
|
52
|
+
notes: "Reads from {workDir}/files/tmp",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "getTaskDir",
|
|
56
|
+
description: "Get the task directory path",
|
|
57
|
+
params: "",
|
|
58
|
+
returns: "string",
|
|
59
|
+
notes: "Returns {workDir}/tasks/{taskName}",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "getCurrentStage",
|
|
63
|
+
description: "Get the current stage name",
|
|
64
|
+
params: "",
|
|
65
|
+
returns: "string",
|
|
66
|
+
notes: "Calls injected getStage()",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const sampleSeed = {
|
|
71
|
+
name: "some-name",
|
|
72
|
+
pipeline: "content-generation",
|
|
73
|
+
data: {
|
|
74
|
+
type: "some-type",
|
|
75
|
+
contentType: "blog-post",
|
|
76
|
+
targetAudience: "software-developers",
|
|
77
|
+
tone: "professional-yet-accessible",
|
|
78
|
+
length: "1500-2000 words",
|
|
79
|
+
outputFormat: "blog-post",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default function CodePage() {
|
|
84
|
+
const [llmFunctions, setLlmFunctions] = useState(null);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
fetch("/api/llm/functions")
|
|
88
|
+
.then((res) => res.json())
|
|
89
|
+
.then(setLlmFunctions)
|
|
90
|
+
.catch(console.error);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const breadcrumbs = [{ label: "Home", href: "/" }, { label: "Code" }];
|
|
94
|
+
|
|
95
|
+
const handleCopySeed = () => {
|
|
96
|
+
navigator.clipboard.writeText(JSON.stringify(sampleSeed, null, 2));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Layout>
|
|
101
|
+
<PageSubheader breadcrumbs={breadcrumbs} />
|
|
102
|
+
<Box>
|
|
103
|
+
{/* Seed File Example Section */}
|
|
104
|
+
<Box mb="8">
|
|
105
|
+
<Heading size="6" mb="4">
|
|
106
|
+
Seed File Example
|
|
107
|
+
</Heading>
|
|
108
|
+
<Text as="p" mb="3" size="2">
|
|
109
|
+
A seed file is a JSON object used to start a new pipeline job. It
|
|
110
|
+
defines the job name, the pipeline to run, and any contextual data
|
|
111
|
+
the pipeline requires to begin.
|
|
112
|
+
</Text>
|
|
113
|
+
<Text as="p" mb="3" size="2" weight="bold">
|
|
114
|
+
Required fields:
|
|
115
|
+
</Text>
|
|
116
|
+
<ul className="list-disc list-inside mb-4 space-y-1">
|
|
117
|
+
<li className="text-sm text-gray-700">
|
|
118
|
+
<Text as="span" weight="bold">
|
|
119
|
+
name
|
|
120
|
+
</Text>{" "}
|
|
121
|
+
(string): Human-friendly title; non-empty, printable only, ≤120
|
|
122
|
+
chars; must be unique.
|
|
123
|
+
</li>
|
|
124
|
+
<li className="text-sm text-gray-700">
|
|
125
|
+
<Text as="span" weight="bold">
|
|
126
|
+
pipeline
|
|
127
|
+
</Text>{" "}
|
|
128
|
+
(string): Pipeline slug defined in your registry (e.g.,
|
|
129
|
+
content-generation).
|
|
130
|
+
</li>
|
|
131
|
+
<li className="text-sm text-gray-700">
|
|
132
|
+
<Text as="span" weight="bold">
|
|
133
|
+
data
|
|
134
|
+
</Text>{" "}
|
|
135
|
+
(object): Required but flexible; include any arbitrary keys your
|
|
136
|
+
pipeline tasks expect.
|
|
137
|
+
</li>
|
|
138
|
+
</ul>
|
|
139
|
+
<Box mb="3">
|
|
140
|
+
<Button
|
|
141
|
+
size="1"
|
|
142
|
+
onClick={handleCopySeed}
|
|
143
|
+
data-testid="copy-seed-example"
|
|
144
|
+
>
|
|
145
|
+
Copy
|
|
146
|
+
</Button>
|
|
147
|
+
</Box>
|
|
148
|
+
<pre className="text-xs bg-gray-50 p-3 rounded overflow-auto max-h-60 border border-gray-200">
|
|
149
|
+
{JSON.stringify(sampleSeed, null, 2)}
|
|
150
|
+
</pre>
|
|
151
|
+
</Box>
|
|
152
|
+
|
|
153
|
+
<Heading size="6" mb="4">
|
|
154
|
+
Pipeline Task IO API
|
|
155
|
+
</Heading>
|
|
156
|
+
<Box overflowX="auto">
|
|
157
|
+
<Table.Root>
|
|
158
|
+
<Table.Header>
|
|
159
|
+
<Table.Row>
|
|
160
|
+
<Table.ColumnHeaderCell>Function</Table.ColumnHeaderCell>
|
|
161
|
+
<Table.ColumnHeaderCell>Parameters</Table.ColumnHeaderCell>
|
|
162
|
+
<Table.ColumnHeaderCell>Returns</Table.ColumnHeaderCell>
|
|
163
|
+
<Table.ColumnHeaderCell>Notes</Table.ColumnHeaderCell>
|
|
164
|
+
</Table.Row>
|
|
165
|
+
</Table.Header>
|
|
166
|
+
<Table.Body>
|
|
167
|
+
{ioFunctions.map((fn) => (
|
|
168
|
+
<Table.Row key={fn.name}>
|
|
169
|
+
<Table.RowHeaderCell>
|
|
170
|
+
<Code size="3">io.{fn.name}</Code>
|
|
171
|
+
</Table.RowHeaderCell>
|
|
172
|
+
<Table.Cell>
|
|
173
|
+
<Code size="3">{fn.params || "—"}</Code>
|
|
174
|
+
</Table.Cell>
|
|
175
|
+
<Table.Cell>
|
|
176
|
+
<Code size="3">{fn.returns}</Code>
|
|
177
|
+
</Table.Cell>
|
|
178
|
+
<Table.Cell>
|
|
179
|
+
{fn.description}
|
|
180
|
+
<br />
|
|
181
|
+
{fn.notes}
|
|
182
|
+
</Table.Cell>
|
|
183
|
+
</Table.Row>
|
|
184
|
+
))}
|
|
185
|
+
</Table.Body>
|
|
186
|
+
</Table.Root>
|
|
187
|
+
</Box>
|
|
188
|
+
|
|
189
|
+
<Heading size="6" mt="8" mb="4">
|
|
190
|
+
Pipeline Task LLM API
|
|
191
|
+
</Heading>
|
|
192
|
+
<Box mb="4">
|
|
193
|
+
<Heading size="4" mb="2">
|
|
194
|
+
Arguments
|
|
195
|
+
</Heading>
|
|
196
|
+
<Code size="3" mb="4">
|
|
197
|
+
{`{
|
|
198
|
+
messages: Array<{role: "system"|"user"|"assistant", content: string }>,
|
|
199
|
+
temperature?: number,
|
|
200
|
+
maxTokens?: number,
|
|
201
|
+
responseFormat?: "json" | { type: "json_object" | { type: "json_schema", name: string, json_schema: object } },
|
|
202
|
+
stop?: string | string[],
|
|
203
|
+
topP?: number,
|
|
204
|
+
frequencyPenalty?: number,
|
|
205
|
+
presencePenalty?: number,
|
|
206
|
+
seed?: number,
|
|
207
|
+
provider?: string,
|
|
208
|
+
model?: string,
|
|
209
|
+
metadata?: object,
|
|
210
|
+
maxRetries?: number
|
|
211
|
+
}`}
|
|
212
|
+
</Code>
|
|
213
|
+
<Heading size="4" mb="2">
|
|
214
|
+
Returns
|
|
215
|
+
</Heading>
|
|
216
|
+
<Code size="3">{`Promise<{ content: any, usage?: object, raw?: any }>`}</Code>
|
|
217
|
+
</Box>
|
|
218
|
+
|
|
219
|
+
{llmFunctions && (
|
|
220
|
+
<Box overflowX="auto">
|
|
221
|
+
<Table.Root>
|
|
222
|
+
<Table.Header>
|
|
223
|
+
<Table.Row>
|
|
224
|
+
<Table.ColumnHeaderCell>Function</Table.ColumnHeaderCell>
|
|
225
|
+
<Table.ColumnHeaderCell>Model</Table.ColumnHeaderCell>
|
|
226
|
+
</Table.Row>
|
|
227
|
+
</Table.Header>
|
|
228
|
+
<Table.Body>
|
|
229
|
+
{Object.entries(llmFunctions).map(([provider, functions]) =>
|
|
230
|
+
functions.map((fn) => (
|
|
231
|
+
<Table.Row key={fn.fullPath}>
|
|
232
|
+
<Table.RowHeaderCell>
|
|
233
|
+
<Code size="3">{fn.fullPath}</Code>
|
|
234
|
+
</Table.RowHeaderCell>
|
|
235
|
+
<Table.Cell>
|
|
236
|
+
<Code size="3">{fn.model}</Code>
|
|
237
|
+
</Table.Cell>
|
|
238
|
+
</Table.Row>
|
|
239
|
+
))
|
|
240
|
+
)}
|
|
241
|
+
</Table.Body>
|
|
242
|
+
</Table.Root>
|
|
243
|
+
</Box>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
<Heading size="6" mt="8" mb="4">
|
|
247
|
+
Validation API
|
|
248
|
+
</Heading>
|
|
249
|
+
<Text as="p" mb="3" size="2">
|
|
250
|
+
Schema validation helper available to task stages via validators.
|
|
251
|
+
</Text>
|
|
252
|
+
<Box mb="4">
|
|
253
|
+
<Heading size="4" mb="2">
|
|
254
|
+
Function Signature
|
|
255
|
+
</Heading>
|
|
256
|
+
<Code size="3">
|
|
257
|
+
{`validateWithSchema(schema: object, data: object | string): { valid: true } | { valid: false, errors: AjvError[] }`}
|
|
258
|
+
</Code>
|
|
259
|
+
<Heading size="4" mb="2" mt="4">
|
|
260
|
+
Behavior
|
|
261
|
+
</Heading>
|
|
262
|
+
<ul className="list-disc list-inside mb-4 space-y-1">
|
|
263
|
+
<li className="text-sm text-gray-700">
|
|
264
|
+
<Text as="span">
|
|
265
|
+
Parses string data to JSON; on parse failure returns{" "}
|
|
266
|
+
{`{ valid:false, errors:[{ keyword:"type", message:"must be a valid JSON object (string parsing failed)"} ]`}
|
|
267
|
+
</Text>
|
|
268
|
+
</li>
|
|
269
|
+
<li className="text-sm text-gray-700">
|
|
270
|
+
<Text as="span">
|
|
271
|
+
Uses Ajv({`{ allErrors: true, strict: false }`}); compiles
|
|
272
|
+
provided schema
|
|
273
|
+
</Text>
|
|
274
|
+
</li>
|
|
275
|
+
<li className="text-sm text-gray-700">
|
|
276
|
+
<Text as="span">Returns AJV errors array when invalid</Text>
|
|
277
|
+
</li>
|
|
278
|
+
</ul>
|
|
279
|
+
<Heading size="4" mb="2">
|
|
280
|
+
Source
|
|
281
|
+
</Heading>
|
|
282
|
+
<Code size="3">src/api/validators/json.js</Code>
|
|
283
|
+
<Heading size="4" mb="2" mt="4">
|
|
284
|
+
Usage Example
|
|
285
|
+
</Heading>
|
|
286
|
+
<Code size="3">{`export const validateStructure = async ({
|
|
287
|
+
io,
|
|
288
|
+
flags,
|
|
289
|
+
validators: { validateWithSchema },
|
|
290
|
+
}) => {
|
|
291
|
+
const content = await io.readArtifact("research-output.json");
|
|
292
|
+
const result = validateWithSchema(researchJsonSchema, content);
|
|
293
|
+
|
|
294
|
+
if (!result.valid) {
|
|
295
|
+
console.warn("[Research:validateStructure] Validation failed", result.errors);
|
|
296
|
+
return { output: {}, flags: { ...flags, validationFailed: true } };
|
|
297
|
+
}
|
|
298
|
+
return { output: {}, flags };
|
|
299
|
+
};`}</Code>
|
|
300
|
+
</Box>
|
|
301
|
+
|
|
302
|
+
<Heading size="6" mt="8" mb="4">
|
|
303
|
+
Environment Configuration
|
|
304
|
+
</Heading>
|
|
305
|
+
<Box mb="4">
|
|
306
|
+
<Heading size="4" mb="2">
|
|
307
|
+
Example .env Configuration
|
|
308
|
+
</Heading>
|
|
309
|
+
<Box overflowX="auto">
|
|
310
|
+
<Table.Root>
|
|
311
|
+
<Table.Header>
|
|
312
|
+
<Table.Row>
|
|
313
|
+
<Table.ColumnHeaderCell>
|
|
314
|
+
Environment Variable
|
|
315
|
+
</Table.ColumnHeaderCell>
|
|
316
|
+
</Table.Row>
|
|
317
|
+
</Table.Header>
|
|
318
|
+
<Table.Body>
|
|
319
|
+
<Table.Row>
|
|
320
|
+
<Table.RowHeaderCell>
|
|
321
|
+
<Code size="3">OPENAI_API_KEY=</Code>
|
|
322
|
+
</Table.RowHeaderCell>
|
|
323
|
+
</Table.Row>
|
|
324
|
+
<Table.Row>
|
|
325
|
+
<Table.RowHeaderCell>
|
|
326
|
+
<Code size="3">DEEPSEEK_API_KEY=</Code>
|
|
327
|
+
</Table.RowHeaderCell>
|
|
328
|
+
</Table.Row>
|
|
329
|
+
<Table.Row>
|
|
330
|
+
<Table.RowHeaderCell>
|
|
331
|
+
<Code size="3">GEMINI_API_KEY=</Code>
|
|
332
|
+
</Table.RowHeaderCell>
|
|
333
|
+
</Table.Row>
|
|
334
|
+
<Table.Row>
|
|
335
|
+
<Table.RowHeaderCell>
|
|
336
|
+
<Code size="3">ANTHROPIC_API_KEY=</Code>
|
|
337
|
+
</Table.RowHeaderCell>
|
|
338
|
+
</Table.Row>
|
|
339
|
+
<Table.Row>
|
|
340
|
+
<Table.RowHeaderCell>
|
|
341
|
+
<Code size="3">ZHIPU_API_KEY=</Code>
|
|
342
|
+
</Table.RowHeaderCell>
|
|
343
|
+
</Table.Row>
|
|
344
|
+
</Table.Body>
|
|
345
|
+
</Table.Root>
|
|
346
|
+
</Box>
|
|
347
|
+
</Box>
|
|
348
|
+
</Box>
|
|
349
|
+
</Layout>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { useParams } from "react-router-dom";
|
|
2
|
+
import { data, useParams } from "react-router-dom";
|
|
3
3
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
|
4
|
+
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
4
5
|
import JobDetail from "../components/JobDetail.jsx";
|
|
5
6
|
import { useJobDetailWithUpdates } from "../ui/client/hooks/useJobDetailWithUpdates.js";
|
|
6
7
|
import Layout from "../components/Layout.jsx";
|
|
8
|
+
import PageSubheader from "../components/PageSubheader.jsx";
|
|
7
9
|
import { statusBadge } from "../utils/ui.jsx";
|
|
10
|
+
import { formatCurrency4, formatTokensCompact } from "../utils/formatters.js";
|
|
8
11
|
|
|
9
12
|
export default function PipelineDetail() {
|
|
10
13
|
const { jobId } = useParams();
|
|
@@ -12,7 +15,10 @@ export default function PipelineDetail() {
|
|
|
12
15
|
// Handle missing job ID (undefined/null)
|
|
13
16
|
if (jobId === undefined || jobId === null) {
|
|
14
17
|
return (
|
|
15
|
-
<Layout
|
|
18
|
+
<Layout
|
|
19
|
+
pageTitle="Pipeline Details"
|
|
20
|
+
breadcrumbs={[{ label: "Home", href: "/" }]}
|
|
21
|
+
>
|
|
16
22
|
<Flex align="center" justify="center" className="min-h-64">
|
|
17
23
|
<Box className="text-center">
|
|
18
24
|
<Text size="5" weight="medium" color="red" className="mb-2">
|
|
@@ -24,11 +30,31 @@ export default function PipelineDetail() {
|
|
|
24
30
|
);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
data: job,
|
|
35
|
+
loading,
|
|
36
|
+
error,
|
|
37
|
+
isRefreshing,
|
|
38
|
+
isHydrated,
|
|
39
|
+
} = useJobDetailWithUpdates(jobId);
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
// Only show loading screen on initial load, not during refresh
|
|
42
|
+
const showLoadingScreen = loading && !isHydrated;
|
|
43
|
+
|
|
44
|
+
if (showLoadingScreen) {
|
|
30
45
|
return (
|
|
31
|
-
<Layout
|
|
46
|
+
<Layout
|
|
47
|
+
pageTitle="Pipeline Details"
|
|
48
|
+
breadcrumbs={[
|
|
49
|
+
{ label: "Home", href: "/" },
|
|
50
|
+
{
|
|
51
|
+
label:
|
|
52
|
+
job && job?.pipelineLabel
|
|
53
|
+
? job.pipelineLabel
|
|
54
|
+
: "Pipeline Details",
|
|
55
|
+
},
|
|
56
|
+
]}
|
|
57
|
+
>
|
|
32
58
|
<Flex align="center" justify="center" className="min-h-64">
|
|
33
59
|
<Box className="text-center">
|
|
34
60
|
<Text size="5" weight="medium" className="mb-2">
|
|
@@ -42,7 +68,18 @@ export default function PipelineDetail() {
|
|
|
42
68
|
|
|
43
69
|
if (error) {
|
|
44
70
|
return (
|
|
45
|
-
<Layout
|
|
71
|
+
<Layout
|
|
72
|
+
pageTitle="Pipeline Details"
|
|
73
|
+
breadcrumbs={[
|
|
74
|
+
{ label: "Home", href: "/" },
|
|
75
|
+
{
|
|
76
|
+
label:
|
|
77
|
+
job && job?.pipelineLabel
|
|
78
|
+
? job.pipelineLabel
|
|
79
|
+
: "Pipeline Details",
|
|
80
|
+
},
|
|
81
|
+
]}
|
|
82
|
+
>
|
|
46
83
|
<Flex align="center" justify="center" className="min-h-64">
|
|
47
84
|
<Box className="text-center">
|
|
48
85
|
<Text size="5" weight="medium" color="red" className="mb-2">
|
|
@@ -58,8 +95,15 @@ export default function PipelineDetail() {
|
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
if (!job) {
|
|
98
|
+
const pipelineDisplay = "Pipeline Details";
|
|
61
99
|
return (
|
|
62
|
-
<Layout
|
|
100
|
+
<Layout
|
|
101
|
+
pageTitle="Pipeline Details"
|
|
102
|
+
breadcrumbs={[
|
|
103
|
+
{ label: "Home", href: "/" },
|
|
104
|
+
{ label: job.pipelineLabel || "Pipeline Details" },
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
63
107
|
<Flex align="center" justify="center" className="min-h-64">
|
|
64
108
|
<Box className="text-center">
|
|
65
109
|
<Text size="5" weight="medium" className="mb-2">
|
|
@@ -89,22 +133,87 @@ export default function PipelineDetail() {
|
|
|
89
133
|
return { tasks: pipelineTasks };
|
|
90
134
|
})();
|
|
91
135
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
136
|
+
const pageTitle = job.name || "Pipeline Details";
|
|
137
|
+
|
|
138
|
+
const breadcrumbs = [
|
|
139
|
+
{ label: "Home", href: "/" },
|
|
140
|
+
{
|
|
141
|
+
label: job && job?.pipelineLabel ? job.pipelineLabel : "Pipeline Details",
|
|
142
|
+
},
|
|
143
|
+
...(job.name ? [{ label: job.name }] : []),
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// Derive cost data from job object with safe fallbacks
|
|
147
|
+
const totalCost = job?.totalCost || job?.costs?.summary?.totalCost || 0;
|
|
148
|
+
const totalTokens = job?.totalTokens || job?.costs?.summary?.totalTokens || 0;
|
|
149
|
+
const totalInputTokens = job?.costs?.summary?.totalInputTokens || 0;
|
|
150
|
+
const totalOutputTokens = job?.costs?.summary?.totalOutputTokens || 0;
|
|
151
|
+
|
|
152
|
+
// Create cost indicator with tooltip when token data is available
|
|
153
|
+
const costIndicator = (
|
|
154
|
+
<Text size="2" color="gray">
|
|
155
|
+
Cost: {totalCost > 0 ? formatCurrency4(totalCost) : "—"}
|
|
156
|
+
</Text>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const costIndicatorWithTooltip =
|
|
160
|
+
totalCost > 0 && totalTokens > 0 ? (
|
|
161
|
+
<Tooltip.Provider delayDuration={100}>
|
|
162
|
+
<Tooltip.Root>
|
|
163
|
+
<Tooltip.Trigger asChild>
|
|
164
|
+
<Box
|
|
165
|
+
className="cursor-help border-b border-dotted border-gray-400 hover:border-gray-600 transition-colors"
|
|
166
|
+
aria-label={`Total cost: ${formatCurrency4(totalCost)}, ${formatTokensCompact(totalTokens)}`}
|
|
167
|
+
>
|
|
168
|
+
{costIndicator}
|
|
169
|
+
</Box>
|
|
170
|
+
</Tooltip.Trigger>
|
|
171
|
+
<Tooltip.Portal>
|
|
172
|
+
<Tooltip.Content
|
|
173
|
+
className="bg-gray-900 text-white px-2 py-1 rounded text-xs max-w-xs"
|
|
174
|
+
sideOffset={5}
|
|
175
|
+
>
|
|
176
|
+
<div className="space-y-1">
|
|
177
|
+
<div className="font-semibold">
|
|
178
|
+
{formatTokensCompact(totalTokens)}
|
|
179
|
+
</div>
|
|
180
|
+
{totalInputTokens > 0 && totalOutputTokens > 0 && (
|
|
181
|
+
<div className="text-gray-300">
|
|
182
|
+
Input: {formatTokensCompact(totalInputTokens)} • Output:{" "}
|
|
183
|
+
{formatTokensCompact(totalOutputTokens)}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
<Tooltip.Arrow className="fill-gray-900" />
|
|
188
|
+
</Tooltip.Content>
|
|
189
|
+
</Tooltip.Portal>
|
|
190
|
+
</Tooltip.Root>
|
|
191
|
+
</Tooltip.Provider>
|
|
192
|
+
) : (
|
|
193
|
+
costIndicator
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Right side content for PageSubheader: job ID, cost indicator, and status badge
|
|
197
|
+
const subheaderRightContent = (
|
|
198
|
+
<Flex align="center" gap="3" className="shrink-0 flex-wrap">
|
|
95
199
|
<Text size="2" color="gray">
|
|
96
200
|
ID: {job.id || jobId}
|
|
97
201
|
</Text>
|
|
202
|
+
{costIndicatorWithTooltip}
|
|
98
203
|
{statusBadge(job.status)}
|
|
99
204
|
</Flex>
|
|
100
205
|
);
|
|
101
206
|
|
|
102
207
|
return (
|
|
103
|
-
<Layout
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
208
|
+
<Layout pageTitle={pageTitle} breadcrumbs={breadcrumbs}>
|
|
209
|
+
<PageSubheader breadcrumbs={breadcrumbs} maxWidth="max-w-7xl">
|
|
210
|
+
{subheaderRightContent}
|
|
211
|
+
{isRefreshing && (
|
|
212
|
+
<Text size="2" color="blue" className="ml-3 animate-pulse">
|
|
213
|
+
Refreshing...
|
|
214
|
+
</Text>
|
|
215
|
+
)}
|
|
216
|
+
</PageSubheader>
|
|
108
217
|
<JobDetail job={job} pipeline={pipeline} />
|
|
109
218
|
</Layout>
|
|
110
219
|
);
|