@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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { Box, Flex, Text, Heading, Table } from "@radix-ui/themes";
|
|
4
|
+
import { ChevronRight, Plus } from "lucide-react";
|
|
5
|
+
import Layout from "../components/Layout.jsx";
|
|
6
|
+
import PageSubheader from "../components/PageSubheader.jsx";
|
|
7
|
+
import AddPipelineSidebar from "../components/AddPipelineSidebar.jsx";
|
|
8
|
+
import { Button } from "../components/ui/button.jsx";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PipelineList component displays available pipelines in a table layout
|
|
12
|
+
*
|
|
13
|
+
* Fetches pipeline data from /api/pipelines endpoint and handles:
|
|
14
|
+
* - Loading state during fetch
|
|
15
|
+
* - Error state for failed requests
|
|
16
|
+
* - Empty state when no pipelines are available
|
|
17
|
+
* - Table layout using Radix UI components
|
|
18
|
+
* - Add pipeline type functionality via sidebar
|
|
19
|
+
*/
|
|
20
|
+
export default function PipelineList() {
|
|
21
|
+
const [pipelines, setPipelines] = useState([]);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const fetchPipelines = async () => {
|
|
29
|
+
try {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
|
|
33
|
+
const response = await fetch("/api/pipelines");
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const errorData = await response.json().catch(() => ({}));
|
|
37
|
+
throw new Error(errorData.message || `HTTP ${response.status}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await response.json();
|
|
41
|
+
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
throw new Error(result.message || "Failed to load pipelines");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setPipelines(result.data?.pipelines || []);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError(err.message || "Failed to load pipelines");
|
|
49
|
+
setPipelines([]);
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
fetchPipelines();
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const breadcrumbs = [{ label: "Home", href: "/" }, { label: "Pipelines" }];
|
|
59
|
+
|
|
60
|
+
const openPipeline = (slug) => {
|
|
61
|
+
navigate(`/pipelines/${slug}`);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Loading state
|
|
65
|
+
if (loading) {
|
|
66
|
+
return (
|
|
67
|
+
<Layout>
|
|
68
|
+
<PageSubheader breadcrumbs={breadcrumbs} />
|
|
69
|
+
<Box>
|
|
70
|
+
<Box mb="8">
|
|
71
|
+
<Heading size="6" mb="4">
|
|
72
|
+
Loading pipeline types...
|
|
73
|
+
</Heading>
|
|
74
|
+
</Box>
|
|
75
|
+
</Box>
|
|
76
|
+
</Layout>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Error state
|
|
81
|
+
if (error) {
|
|
82
|
+
return (
|
|
83
|
+
<Layout>
|
|
84
|
+
<PageSubheader breadcrumbs={breadcrumbs} />
|
|
85
|
+
<Box>
|
|
86
|
+
<Box mb="8">
|
|
87
|
+
<Heading size="6" mb="4">
|
|
88
|
+
Failed
|
|
89
|
+
</Heading>
|
|
90
|
+
<Flex align="center" justify="center" className="min-h-64">
|
|
91
|
+
<Box className="text-center">
|
|
92
|
+
<Text size="2" color="gray" className="mt-2">
|
|
93
|
+
{error}
|
|
94
|
+
</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
</Flex>
|
|
97
|
+
</Box>
|
|
98
|
+
</Box>
|
|
99
|
+
</Layout>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Empty state
|
|
104
|
+
if (pipelines.length === 0) {
|
|
105
|
+
return (
|
|
106
|
+
<Layout>
|
|
107
|
+
<PageSubheader breadcrumbs={breadcrumbs} />
|
|
108
|
+
<Box>
|
|
109
|
+
<Box mb="8">
|
|
110
|
+
<Heading size="6" mb="4">
|
|
111
|
+
No pipelines available
|
|
112
|
+
</Heading>
|
|
113
|
+
<Flex align="center" justify="center" className="min-h-64">
|
|
114
|
+
<Box className="text-center">
|
|
115
|
+
<Text size="2" color="gray" className="mt-2">
|
|
116
|
+
Check back later for available pipelines.
|
|
117
|
+
</Text>
|
|
118
|
+
</Box>
|
|
119
|
+
</Flex>
|
|
120
|
+
</Box>
|
|
121
|
+
</Box>
|
|
122
|
+
</Layout>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Main content with pipeline table
|
|
127
|
+
return (
|
|
128
|
+
<Layout>
|
|
129
|
+
<PageSubheader breadcrumbs={breadcrumbs}>
|
|
130
|
+
<Button size="md" variant="solid" onClick={() => setSidebarOpen(true)}>
|
|
131
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
132
|
+
Add a Pipeline Type
|
|
133
|
+
</Button>
|
|
134
|
+
</PageSubheader>
|
|
135
|
+
<Box>
|
|
136
|
+
<Box mb="8">
|
|
137
|
+
<Heading size="6" mb="4">
|
|
138
|
+
Pipeline Types
|
|
139
|
+
</Heading>
|
|
140
|
+
|
|
141
|
+
<Table.Root radius="none">
|
|
142
|
+
<Table.Header>
|
|
143
|
+
<Table.Row>
|
|
144
|
+
<Table.ColumnHeaderCell>Pipeline Name</Table.ColumnHeaderCell>
|
|
145
|
+
<Table.ColumnHeaderCell>Description</Table.ColumnHeaderCell>
|
|
146
|
+
<Table.ColumnHeaderCell className="w-12"></Table.ColumnHeaderCell>
|
|
147
|
+
</Table.Row>
|
|
148
|
+
</Table.Header>
|
|
149
|
+
|
|
150
|
+
<Table.Body>
|
|
151
|
+
{pipelines.map((pipeline) => {
|
|
152
|
+
const pipelineName = pipeline.name;
|
|
153
|
+
const pipelineSlug = pipeline.slug;
|
|
154
|
+
const description = pipeline.description || "—";
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Table.Row
|
|
158
|
+
key={pipelineSlug}
|
|
159
|
+
className="group cursor-pointer hover:bg-slate-50/50 transition-colors"
|
|
160
|
+
onClick={() => openPipeline(pipelineSlug)}
|
|
161
|
+
onKeyDown={(e) => {
|
|
162
|
+
if (e.key === " ") {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
openPipeline(pipelineSlug);
|
|
165
|
+
} else if (e.key === "Enter") {
|
|
166
|
+
openPipeline(pipelineSlug);
|
|
167
|
+
}
|
|
168
|
+
}}
|
|
169
|
+
tabIndex={0}
|
|
170
|
+
aria-label={`Open ${pipelineName} pipeline`}
|
|
171
|
+
>
|
|
172
|
+
<Table.Cell>
|
|
173
|
+
<Flex direction="column" gap="1">
|
|
174
|
+
<Text
|
|
175
|
+
size="2"
|
|
176
|
+
weight="medium"
|
|
177
|
+
className="text-slate-900"
|
|
178
|
+
>
|
|
179
|
+
{pipelineName}
|
|
180
|
+
</Text>
|
|
181
|
+
<Text size="1" className="text-slate-500">
|
|
182
|
+
{pipelineSlug}
|
|
183
|
+
</Text>
|
|
184
|
+
</Flex>
|
|
185
|
+
</Table.Cell>
|
|
186
|
+
|
|
187
|
+
<Table.Cell>
|
|
188
|
+
<Text size="2" className="text-slate-700">
|
|
189
|
+
{description}
|
|
190
|
+
</Text>
|
|
191
|
+
</Table.Cell>
|
|
192
|
+
|
|
193
|
+
<Table.Cell>
|
|
194
|
+
<Button
|
|
195
|
+
variant="ghost"
|
|
196
|
+
size="sm"
|
|
197
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
198
|
+
aria-label={`View ${pipelineName} pipeline`}
|
|
199
|
+
>
|
|
200
|
+
<ChevronRight className="h-4 w-4" />
|
|
201
|
+
</Button>
|
|
202
|
+
</Table.Cell>
|
|
203
|
+
</Table.Row>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
206
|
+
</Table.Body>
|
|
207
|
+
</Table.Root>
|
|
208
|
+
</Box>
|
|
209
|
+
</Box>
|
|
210
|
+
|
|
211
|
+
<AddPipelineSidebar open={sidebarOpen} onOpenChange={setSidebarOpen} />
|
|
212
|
+
</Layout>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import { Box, Flex, Text } from "@radix-ui/themes";
|
|
4
|
+
import Layout from "../components/Layout.jsx";
|
|
5
|
+
import PageSubheader from "../components/PageSubheader.jsx";
|
|
6
|
+
import { Button } from "../components/ui/button.jsx";
|
|
7
|
+
|
|
8
|
+
import PipelineDAGGrid from "../components/PipelineDAGGrid.jsx";
|
|
9
|
+
import TaskCreationSidebar from "../components/TaskCreationSidebar.jsx";
|
|
10
|
+
import { AnalysisProgressTray } from "../components/AnalysisProgressTray.jsx";
|
|
11
|
+
import { useAnalysisProgress } from "../ui/client/hooks/useAnalysisProgress.js";
|
|
12
|
+
|
|
13
|
+
export default function PipelineTypeDetail() {
|
|
14
|
+
const { slug } = useParams();
|
|
15
|
+
const [pipeline, setPipeline] = useState(null);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
19
|
+
const [trayDismissed, setTrayDismissed] = useState(false);
|
|
20
|
+
|
|
21
|
+
const { status, startAnalysis, reset, ...progressState } =
|
|
22
|
+
useAnalysisProgress();
|
|
23
|
+
|
|
24
|
+
const handleAnalyze = () => {
|
|
25
|
+
setTrayDismissed(false);
|
|
26
|
+
startAnalysis(slug);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const fetchPipeline = async () => {
|
|
31
|
+
if (!slug) {
|
|
32
|
+
setError("No pipeline slug provided");
|
|
33
|
+
setLoading(false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
setLoading(true);
|
|
39
|
+
const response = await fetch(
|
|
40
|
+
`/api/pipelines/${encodeURIComponent(slug)}`
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const errorData = await response.json().catch(() => ({}));
|
|
45
|
+
throw new Error(
|
|
46
|
+
errorData.message || `Failed to load pipeline: ${response.status}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
if (!data.ok) {
|
|
52
|
+
throw new Error(data.message || "Failed to load pipeline");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setPipeline(data.data);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setError(err.message || "Failed to load pipeline");
|
|
58
|
+
} finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
fetchPipeline();
|
|
64
|
+
}, [slug]);
|
|
65
|
+
|
|
66
|
+
// Handle missing slug
|
|
67
|
+
if (!slug) {
|
|
68
|
+
return (
|
|
69
|
+
<Layout
|
|
70
|
+
pageTitle="Pipeline Details"
|
|
71
|
+
breadcrumbs={[
|
|
72
|
+
{ label: "Home", href: "/" },
|
|
73
|
+
{ label: "Pipelines", href: "/pipelines" },
|
|
74
|
+
]}
|
|
75
|
+
>
|
|
76
|
+
<Flex align="center" justify="center" className="min-h-64">
|
|
77
|
+
<Box className="text-center">
|
|
78
|
+
<Text size="5" weight="medium" color="red" className="mb-2">
|
|
79
|
+
No pipeline slug provided
|
|
80
|
+
</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
</Flex>
|
|
83
|
+
</Layout>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Loading state
|
|
88
|
+
if (loading) {
|
|
89
|
+
return (
|
|
90
|
+
<Layout
|
|
91
|
+
pageTitle="Pipeline Details"
|
|
92
|
+
breadcrumbs={[
|
|
93
|
+
{ label: "Home", href: "/" },
|
|
94
|
+
{ label: "Pipelines", href: "/pipelines" },
|
|
95
|
+
]}
|
|
96
|
+
>
|
|
97
|
+
<Flex align="center" justify="center" className="min-h-64">
|
|
98
|
+
<Box className="text-center">
|
|
99
|
+
<Text size="5" weight="medium" className="mb-2">
|
|
100
|
+
Loading pipeline details...
|
|
101
|
+
</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
</Flex>
|
|
104
|
+
</Layout>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Error state
|
|
109
|
+
if (error) {
|
|
110
|
+
return (
|
|
111
|
+
<Layout
|
|
112
|
+
pageTitle="Pipeline Details"
|
|
113
|
+
breadcrumbs={[
|
|
114
|
+
{ label: "Home", href: "/" },
|
|
115
|
+
{ label: "Pipelines", href: "/pipelines" },
|
|
116
|
+
]}
|
|
117
|
+
>
|
|
118
|
+
<Flex align="center" justify="center" className="min-h-64">
|
|
119
|
+
<Box className="text-center">
|
|
120
|
+
<Text size="5" weight="medium" color="red" className="mb-2">
|
|
121
|
+
Failed to load pipeline
|
|
122
|
+
</Text>
|
|
123
|
+
<Text size="2" color="gray" className="mt-2">
|
|
124
|
+
{error}
|
|
125
|
+
</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
</Flex>
|
|
128
|
+
</Layout>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// No pipeline data
|
|
133
|
+
if (!pipeline) {
|
|
134
|
+
return (
|
|
135
|
+
<Layout
|
|
136
|
+
pageTitle="Pipeline Details"
|
|
137
|
+
breadcrumbs={[
|
|
138
|
+
{ label: "Home", href: "/" },
|
|
139
|
+
{ label: "Pipelines", href: "/pipelines" },
|
|
140
|
+
]}
|
|
141
|
+
>
|
|
142
|
+
<Flex align="center" justify="center" className="min-h-64">
|
|
143
|
+
<Box className="text-center">
|
|
144
|
+
<Text size="5" weight="medium" className="mb-2">
|
|
145
|
+
Pipeline not found
|
|
146
|
+
</Text>
|
|
147
|
+
</Box>
|
|
148
|
+
</Flex>
|
|
149
|
+
</Layout>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pageTitle = pipeline.name || "Pipeline Details";
|
|
154
|
+
const breadcrumbs = [
|
|
155
|
+
{ label: "Home", href: "/" },
|
|
156
|
+
{ label: "Pipelines", href: "/pipelines" },
|
|
157
|
+
{ label: pipeline.name || slug },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Layout pageTitle={pageTitle} breadcrumbs={breadcrumbs}>
|
|
162
|
+
<PageSubheader breadcrumbs={breadcrumbs} maxWidth="max-w-7xl">
|
|
163
|
+
<Flex gap="3" align="center">
|
|
164
|
+
<Text size="2" color="gray">
|
|
165
|
+
Slug: {slug}
|
|
166
|
+
</Text>
|
|
167
|
+
<Button
|
|
168
|
+
variant="solid"
|
|
169
|
+
size="md"
|
|
170
|
+
onClick={() => setSidebarOpen(true)}
|
|
171
|
+
>
|
|
172
|
+
Add Task
|
|
173
|
+
</Button>
|
|
174
|
+
<Button
|
|
175
|
+
variant="outline"
|
|
176
|
+
size="md"
|
|
177
|
+
onClick={handleAnalyze}
|
|
178
|
+
disabled={status === "connecting" || status === "running"}
|
|
179
|
+
>
|
|
180
|
+
Analyze Pipeline
|
|
181
|
+
</Button>
|
|
182
|
+
</Flex>
|
|
183
|
+
</PageSubheader>
|
|
184
|
+
|
|
185
|
+
{/* Pipeline description */}
|
|
186
|
+
{pipeline.description && (
|
|
187
|
+
<Box className="mb-6">
|
|
188
|
+
<Text size="2" color="gray" className="leading-relaxed">
|
|
189
|
+
{pipeline.description}
|
|
190
|
+
</Text>
|
|
191
|
+
</Box>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Pipeline DAG - will be implemented in step 5 */}
|
|
195
|
+
<Box className="bg-gray-50 rounded-lg p-4">
|
|
196
|
+
<Text size="3" weight="medium" className="mb-4">
|
|
197
|
+
Pipeline Tasks
|
|
198
|
+
</Text>
|
|
199
|
+
|
|
200
|
+
{pipeline.tasks && pipeline.tasks.length > 0 ? (
|
|
201
|
+
<Box>
|
|
202
|
+
<Text size="2" color="gray" className="mb-4">
|
|
203
|
+
{pipeline.tasks.length} task
|
|
204
|
+
{pipeline.tasks.length !== 1 ? "s" : ""} defined
|
|
205
|
+
</Text>
|
|
206
|
+
|
|
207
|
+
<PipelineDAGGrid items={pipeline.tasks} pipelineSlug={slug} />
|
|
208
|
+
</Box>
|
|
209
|
+
) : (
|
|
210
|
+
<Box className="mb-4">
|
|
211
|
+
<Text size="2" color="gray">
|
|
212
|
+
No tasks defined for this pipeline
|
|
213
|
+
</Text>
|
|
214
|
+
</Box>
|
|
215
|
+
)}
|
|
216
|
+
</Box>
|
|
217
|
+
|
|
218
|
+
<TaskCreationSidebar
|
|
219
|
+
isOpen={sidebarOpen}
|
|
220
|
+
onClose={() => setSidebarOpen(false)}
|
|
221
|
+
pipelineSlug={slug}
|
|
222
|
+
/>
|
|
223
|
+
|
|
224
|
+
{!trayDismissed && (
|
|
225
|
+
<AnalysisProgressTray
|
|
226
|
+
{...progressState}
|
|
227
|
+
status={status}
|
|
228
|
+
pipelineSlug={slug}
|
|
229
|
+
onDismiss={() => setTrayDismissed(true)}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
</Layout>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -7,24 +7,23 @@ import { Box, Flex, Text, Tabs } from "@radix-ui/themes";
|
|
|
7
7
|
import { Progress } from "../components/ui/progress";
|
|
8
8
|
import { useJobListWithUpdates } from "../ui/client/hooks/useJobListWithUpdates";
|
|
9
9
|
import { adaptJobSummary } from "../ui/client/adapters/job-adapter";
|
|
10
|
-
import { TaskState, JobStatus } from "../config/statuses.js";
|
|
11
10
|
|
|
12
11
|
// Referenced components — leave these alone
|
|
13
12
|
import JobTable from "../components/JobTable";
|
|
14
13
|
import Layout from "../components/Layout.jsx";
|
|
15
14
|
|
|
16
|
-
export default function PromptPipelineDashboard(
|
|
15
|
+
export default function PromptPipelineDashboard() {
|
|
17
16
|
const navigate = useNavigate();
|
|
18
17
|
const hookResult = useJobListWithUpdates();
|
|
19
18
|
|
|
20
19
|
if (
|
|
20
|
+
/* eslint-disable-next-line no-undef */
|
|
21
21
|
process.env.NODE_ENV === "test" &&
|
|
22
22
|
(hookResult === undefined ||
|
|
23
23
|
hookResult === null ||
|
|
24
24
|
typeof hookResult !== "object" ||
|
|
25
25
|
Array.isArray(hookResult))
|
|
26
26
|
) {
|
|
27
|
-
// eslint-disable-next-line no-console
|
|
28
27
|
console.error(
|
|
29
28
|
"[PromptPipelineDashboard] useJobListWithUpdates returned unexpected value",
|
|
30
29
|
{
|
|
@@ -39,7 +38,7 @@ export default function PromptPipelineDashboard({ isConnected }) {
|
|
|
39
38
|
);
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
const { data: apiJobs,
|
|
41
|
+
const { data: apiJobs, error } = hookResult;
|
|
43
42
|
|
|
44
43
|
const jobs = useMemo(() => {
|
|
45
44
|
const src = Array.isArray(apiJobs) ? apiJobs : [];
|
|
@@ -57,26 +56,26 @@ export default function PromptPipelineDashboard({ isConnected }) {
|
|
|
57
56
|
// Shared ticker for live duration updates - removed useTicker
|
|
58
57
|
|
|
59
58
|
const errorCount = useMemo(
|
|
60
|
-
() => jobs.filter((j) => j.
|
|
59
|
+
() => jobs.filter((j) => j.displayCategory === "errors").length,
|
|
61
60
|
[jobs]
|
|
62
61
|
);
|
|
63
62
|
const currentCount = useMemo(
|
|
64
|
-
() => jobs.filter((j) => j.
|
|
63
|
+
() => jobs.filter((j) => j.displayCategory === "current").length,
|
|
65
64
|
[jobs]
|
|
66
65
|
);
|
|
67
66
|
const completedCount = useMemo(
|
|
68
|
-
() => jobs.filter((j) => j.
|
|
67
|
+
() => jobs.filter((j) => j.displayCategory === "complete").length,
|
|
69
68
|
[jobs]
|
|
70
69
|
);
|
|
71
70
|
|
|
72
71
|
const filteredJobs = useMemo(() => {
|
|
73
72
|
switch (activeTab) {
|
|
74
73
|
case "current":
|
|
75
|
-
return jobs.filter((j) => j.
|
|
74
|
+
return jobs.filter((j) => j.displayCategory === "current");
|
|
76
75
|
case "errors":
|
|
77
|
-
return jobs.filter((j) => j.
|
|
76
|
+
return jobs.filter((j) => j.displayCategory === "errors");
|
|
78
77
|
case "complete":
|
|
79
|
-
return jobs.filter((j) => j.
|
|
78
|
+
return jobs.filter((j) => j.displayCategory === "complete");
|
|
80
79
|
default:
|
|
81
80
|
return [];
|
|
82
81
|
}
|
|
@@ -86,7 +85,7 @@ export default function PromptPipelineDashboard({ isConnected }) {
|
|
|
86
85
|
|
|
87
86
|
// Aggregate progress for currently running jobs (for a subtle top progress bar)
|
|
88
87
|
const runningJobs = useMemo(
|
|
89
|
-
() => jobs.filter((j) => j.
|
|
88
|
+
() => jobs.filter((j) => j.displayCategory === "current"),
|
|
90
89
|
[jobs]
|
|
91
90
|
);
|
|
92
91
|
const aggregateProgress = useMemo(() => {
|
|
@@ -12,20 +12,24 @@ export async function deepseekChat({
|
|
|
12
12
|
model = "deepseek-chat",
|
|
13
13
|
temperature = 0.7,
|
|
14
14
|
maxTokens,
|
|
15
|
-
responseFormat,
|
|
15
|
+
responseFormat = "json_object",
|
|
16
16
|
topP,
|
|
17
17
|
frequencyPenalty,
|
|
18
18
|
presencePenalty,
|
|
19
19
|
stop,
|
|
20
|
+
stream = false,
|
|
20
21
|
maxRetries = 3,
|
|
21
22
|
}) {
|
|
22
|
-
// Enforce JSON mode - reject calls without proper JSON responseFormat
|
|
23
|
-
ensureJsonResponseFormat(responseFormat, "DeepSeek");
|
|
24
|
-
|
|
25
23
|
if (!process.env.DEEPSEEK_API_KEY) {
|
|
26
24
|
throw new Error("DeepSeek API key not configured");
|
|
27
25
|
}
|
|
28
26
|
|
|
27
|
+
// Determine if JSON mode is requested
|
|
28
|
+
const isJsonMode =
|
|
29
|
+
responseFormat?.type === "json_object" ||
|
|
30
|
+
responseFormat?.type === "json_schema" ||
|
|
31
|
+
responseFormat === "json";
|
|
32
|
+
|
|
29
33
|
const { systemMsg, userMsg } = extractMessages(messages);
|
|
30
34
|
|
|
31
35
|
let lastError;
|
|
@@ -47,10 +51,11 @@ export async function deepseekChat({
|
|
|
47
51
|
frequency_penalty: frequencyPenalty,
|
|
48
52
|
presence_penalty: presencePenalty,
|
|
49
53
|
stop,
|
|
54
|
+
stream,
|
|
50
55
|
};
|
|
51
56
|
|
|
52
|
-
// Add response format
|
|
53
|
-
if (
|
|
57
|
+
// Add response format only for JSON mode (streaming uses text mode)
|
|
58
|
+
if (isJsonMode && !stream) {
|
|
54
59
|
requestBody.response_format = { type: "json_object" };
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -73,22 +78,35 @@ export async function deepseekChat({
|
|
|
73
78
|
throw { status: response.status, ...error };
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
// Streaming mode - return async generator for real-time chunks
|
|
82
|
+
if (stream) {
|
|
83
|
+
return createStreamGenerator(response.body);
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
const data = await response.json();
|
|
77
87
|
const content = data.choices[0].message.content;
|
|
78
88
|
|
|
79
|
-
// Parse JSON
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
// Parse JSON only in JSON mode; return raw string for text mode
|
|
90
|
+
if (isJsonMode) {
|
|
91
|
+
const parsed = tryParseJSON(content);
|
|
92
|
+
if (!parsed) {
|
|
93
|
+
throw new ProviderJsonParseError(
|
|
94
|
+
"DeepSeek",
|
|
95
|
+
model,
|
|
96
|
+
content.substring(0, 200),
|
|
97
|
+
"Failed to parse JSON response from DeepSeek API"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
content: parsed,
|
|
102
|
+
usage: data.usage,
|
|
103
|
+
raw: data,
|
|
104
|
+
};
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
// Text mode - return raw string
|
|
90
108
|
return {
|
|
91
|
-
content
|
|
109
|
+
content,
|
|
92
110
|
usage: data.usage,
|
|
93
111
|
raw: data,
|
|
94
112
|
};
|
|
@@ -107,3 +125,45 @@ export async function deepseekChat({
|
|
|
107
125
|
|
|
108
126
|
throw lastError || new Error(`Failed after ${maxRetries + 1} attempts`);
|
|
109
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create async generator for streaming DeepSeek responses.
|
|
131
|
+
* DeepSeek uses Server-Sent Events format with "data:" prefix.
|
|
132
|
+
*/
|
|
133
|
+
async function* createStreamGenerator(stream) {
|
|
134
|
+
const decoder = new TextDecoder();
|
|
135
|
+
const reader = stream.getReader();
|
|
136
|
+
let buffer = "";
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done) break;
|
|
142
|
+
|
|
143
|
+
buffer += decoder.decode(value, { stream: true });
|
|
144
|
+
const lines = buffer.split("\n");
|
|
145
|
+
buffer = lines.pop(); // Keep incomplete line
|
|
146
|
+
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
if (line.startsWith("data: ")) {
|
|
149
|
+
const data = line.slice(6);
|
|
150
|
+
if (data === "[DONE]") continue;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(data);
|
|
154
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
155
|
+
// Skip only truly empty chunks; preserve whitespace-only content
|
|
156
|
+
if (content !== undefined && content !== null && content !== "") {
|
|
157
|
+
yield { content };
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Skip malformed JSON
|
|
161
|
+
console.warn("[deepseek] Failed to parse stream chunk:", e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
reader.releaseLock();
|
|
168
|
+
}
|
|
169
|
+
}
|