@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,227 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Table } from "@radix-ui/themes";
|
|
3
|
+
import { SidebarSection } from "./ui/sidebar.jsx";
|
|
4
|
+
import { Badge } from "./ui/badge.jsx";
|
|
5
|
+
import { Button } from "./ui/button.jsx";
|
|
6
|
+
import { StageTimeline } from "./StageTimeline.jsx";
|
|
7
|
+
import { SchemaPreviewPanel } from "./SchemaPreviewPanel.jsx";
|
|
8
|
+
|
|
9
|
+
const formatDate = (isoString) => {
|
|
10
|
+
if (
|
|
11
|
+
!isoString ||
|
|
12
|
+
(typeof isoString !== "string" && !(isoString instanceof Date))
|
|
13
|
+
) {
|
|
14
|
+
return "Unknown";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const date = isoString instanceof Date ? isoString : new Date(isoString);
|
|
18
|
+
|
|
19
|
+
if (Number.isNaN(date.getTime())) {
|
|
20
|
+
return "Unknown";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
25
|
+
dateStyle: "medium",
|
|
26
|
+
timeStyle: "short",
|
|
27
|
+
}).format(date);
|
|
28
|
+
} catch {
|
|
29
|
+
return "Unknown";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ArtifactTable = ({
|
|
34
|
+
artifacts,
|
|
35
|
+
showRequired,
|
|
36
|
+
emptyMessage,
|
|
37
|
+
onViewSchema,
|
|
38
|
+
}) => {
|
|
39
|
+
if (artifacts.length === 0) {
|
|
40
|
+
return (
|
|
41
|
+
<div className="text-sm text-muted-foreground italic">{emptyMessage}</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Table.Root size="1">
|
|
47
|
+
<Table.Header>
|
|
48
|
+
<Table.Row>
|
|
49
|
+
<Table.ColumnHeaderCell>File</Table.ColumnHeaderCell>
|
|
50
|
+
<Table.ColumnHeaderCell>Stage</Table.ColumnHeaderCell>
|
|
51
|
+
{showRequired && (
|
|
52
|
+
<Table.ColumnHeaderCell>Required</Table.ColumnHeaderCell>
|
|
53
|
+
)}
|
|
54
|
+
<Table.ColumnHeaderCell>Actions</Table.ColumnHeaderCell>
|
|
55
|
+
</Table.Row>
|
|
56
|
+
</Table.Header>
|
|
57
|
+
<Table.Body>
|
|
58
|
+
{artifacts.map((artifact, idx) => (
|
|
59
|
+
<Table.Row key={idx}>
|
|
60
|
+
<Table.Cell>
|
|
61
|
+
<code className="text-sm">{artifact.fileName}</code>
|
|
62
|
+
</Table.Cell>
|
|
63
|
+
<Table.Cell>
|
|
64
|
+
<Badge intent="blue">{artifact.stage}</Badge>
|
|
65
|
+
</Table.Cell>
|
|
66
|
+
{showRequired && (
|
|
67
|
+
<Table.Cell>
|
|
68
|
+
{artifact.required && <Badge intent="red">required</Badge>}
|
|
69
|
+
</Table.Cell>
|
|
70
|
+
)}
|
|
71
|
+
<Table.Cell>
|
|
72
|
+
{artifact.fileName.endsWith(".json") && (
|
|
73
|
+
<div className="flex gap-1">
|
|
74
|
+
<Button
|
|
75
|
+
variant="ghost"
|
|
76
|
+
size="sm"
|
|
77
|
+
onClick={() => onViewSchema(artifact.fileName, "schema")}
|
|
78
|
+
>
|
|
79
|
+
Schema
|
|
80
|
+
</Button>
|
|
81
|
+
<Button
|
|
82
|
+
variant="ghost"
|
|
83
|
+
size="sm"
|
|
84
|
+
onClick={() => onViewSchema(artifact.fileName, "sample")}
|
|
85
|
+
>
|
|
86
|
+
Sample
|
|
87
|
+
</Button>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</Table.Cell>
|
|
91
|
+
</Table.Row>
|
|
92
|
+
))}
|
|
93
|
+
</Table.Body>
|
|
94
|
+
</Table.Root>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const ModelList = ({ models }) => {
|
|
99
|
+
if (models.length === 0) {
|
|
100
|
+
return <div className="text-sm text-muted-foreground">No models used</div>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ul className="space-y-1 text-sm">
|
|
105
|
+
{models.map((model, idx) => (
|
|
106
|
+
<li key={idx} className="text-slate-700">
|
|
107
|
+
{model.provider}.{model.method} @ {model.stage}
|
|
108
|
+
</li>
|
|
109
|
+
))}
|
|
110
|
+
</ul>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const TaskAnalysisDisplay = React.memo(
|
|
115
|
+
({ analysis, loading, error, pipelineSlug }) => {
|
|
116
|
+
const [previewFile, setPreviewFile] = useState(null);
|
|
117
|
+
const [previewContent, setPreviewContent] = useState(null);
|
|
118
|
+
const [previewLoading, setPreviewLoading] = useState(false);
|
|
119
|
+
const [previewError, setPreviewError] = useState(null);
|
|
120
|
+
|
|
121
|
+
const handleViewSchema = async (fileName, type) => {
|
|
122
|
+
setPreviewFile({ fileName, type });
|
|
123
|
+
setPreviewLoading(true);
|
|
124
|
+
setPreviewError(null);
|
|
125
|
+
setPreviewContent(null);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(
|
|
129
|
+
`/api/pipelines/${encodeURIComponent(
|
|
130
|
+
pipelineSlug
|
|
131
|
+
)}/schemas/${encodeURIComponent(fileName)}?type=${type}`
|
|
132
|
+
);
|
|
133
|
+
const data = await res.json();
|
|
134
|
+
if (!res.ok) throw new Error(data.message || "Failed to load");
|
|
135
|
+
setPreviewContent(data.data);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
setPreviewError(err.message);
|
|
138
|
+
} finally {
|
|
139
|
+
setPreviewLoading(false);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleClosePreview = () => {
|
|
144
|
+
setPreviewFile(null);
|
|
145
|
+
setPreviewContent(null);
|
|
146
|
+
setPreviewError(null);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (loading) {
|
|
150
|
+
return (
|
|
151
|
+
<div className="p-6 text-sm text-muted-foreground" aria-busy="true">
|
|
152
|
+
Loading analysis...
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (error) {
|
|
158
|
+
return (
|
|
159
|
+
<div className="p-6 text-sm text-red-600" role="alert">
|
|
160
|
+
{error}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (analysis === null) {
|
|
166
|
+
return (
|
|
167
|
+
<div className="p-6 text-sm text-muted-foreground">
|
|
168
|
+
No analysis available
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div>
|
|
175
|
+
<SidebarSection title="Artifacts">
|
|
176
|
+
<div className="space-y-4">
|
|
177
|
+
<div>
|
|
178
|
+
<h4 className="text-sm font-medium text-slate-700 mb-2">Reads</h4>
|
|
179
|
+
<ArtifactTable
|
|
180
|
+
artifacts={analysis.artifacts.reads}
|
|
181
|
+
showRequired
|
|
182
|
+
emptyMessage="No reads"
|
|
183
|
+
onViewSchema={handleViewSchema}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
|
188
|
+
Writes
|
|
189
|
+
</h4>
|
|
190
|
+
<ArtifactTable
|
|
191
|
+
artifacts={analysis.artifacts.writes}
|
|
192
|
+
showRequired={false}
|
|
193
|
+
emptyMessage="No writes"
|
|
194
|
+
onViewSchema={handleViewSchema}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</SidebarSection>
|
|
199
|
+
|
|
200
|
+
<SidebarSection title="Stages">
|
|
201
|
+
<StageTimeline stages={analysis.stages} />
|
|
202
|
+
</SidebarSection>
|
|
203
|
+
|
|
204
|
+
<SidebarSection title="Models">
|
|
205
|
+
<ModelList models={analysis.models} />
|
|
206
|
+
</SidebarSection>
|
|
207
|
+
|
|
208
|
+
<div className="px-6 pb-6 text-xs text-muted-foreground">
|
|
209
|
+
Analyzed at: {formatDate(analysis.analyzedAt)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{previewFile && (
|
|
213
|
+
<SchemaPreviewPanel
|
|
214
|
+
fileName={previewFile.fileName}
|
|
215
|
+
type={previewFile.type}
|
|
216
|
+
content={previewContent}
|
|
217
|
+
loading={previewLoading}
|
|
218
|
+
error={previewError}
|
|
219
|
+
onClose={handleClosePreview}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
TaskAnalysisDisplay.displayName = "TaskAnalysisDisplay";
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Button } from "./ui/button.jsx";
|
|
3
|
+
import { Sidebar, SidebarFooter } from "./ui/sidebar.jsx";
|
|
4
|
+
import { MarkdownRenderer } from "./MarkdownRenderer.jsx";
|
|
5
|
+
|
|
6
|
+
const TASK_PROPOSAL_REGEX =
|
|
7
|
+
/\[TASK_PROPOSAL\]\r?\nFILENAME:\s*(\S+)\r?\nTASKNAME:\s*(\S+)\r?\nCODE:\s*```javascript\s*([\s\S]*?)\s*```\s*\[\/TASK_PROPOSAL\]/;
|
|
8
|
+
|
|
9
|
+
function parseTaskProposal(content) {
|
|
10
|
+
const match = content.match(TASK_PROPOSAL_REGEX);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
|
|
13
|
+
const [, filename, taskName, code] = match;
|
|
14
|
+
return { filename, taskName, code, proposalBlock: match[0] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function TaskProposalCard({ proposal, isCreating, onCreate }) {
|
|
18
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="mt-3 p-4 bg-card border border-border rounded-lg shadow-sm">
|
|
22
|
+
<div className="flex items-start justify-between mb-2">
|
|
23
|
+
<div>
|
|
24
|
+
<div className="flex items-center gap-2 mb-1">
|
|
25
|
+
<span className="px-2 py-0.5 text-xs font-medium bg-primary/10 text-primary rounded-full">
|
|
26
|
+
Task Proposal
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<p className="text-sm font-medium text-foreground">
|
|
30
|
+
{proposal.filename}
|
|
31
|
+
</p>
|
|
32
|
+
<p className="text-xs text-muted-foreground">{proposal.taskName}</p>
|
|
33
|
+
</div>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
37
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
38
|
+
>
|
|
39
|
+
{isExpanded ? "Hide" : "Show"} code
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
{isExpanded && (
|
|
43
|
+
<pre className="mt-2 p-3 bg-muted rounded text-xs overflow-x-auto">
|
|
44
|
+
<code>{proposal.code}</code>
|
|
45
|
+
</pre>
|
|
46
|
+
)}
|
|
47
|
+
{proposal.error && (
|
|
48
|
+
<p className="mt-2 text-sm text-destructive">{proposal.error}</p>
|
|
49
|
+
)}
|
|
50
|
+
{proposal.created && (
|
|
51
|
+
<p className="mt-2 text-sm text-green-600 dark:text-green-400">
|
|
52
|
+
✓ Task created successfully
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
{!proposal.created && (
|
|
56
|
+
<div className="mt-3">
|
|
57
|
+
<Button
|
|
58
|
+
variant="solid"
|
|
59
|
+
size="sm"
|
|
60
|
+
onClick={onCreate}
|
|
61
|
+
disabled={isCreating}
|
|
62
|
+
>
|
|
63
|
+
{isCreating ? "Creating..." : "Create Task"}
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function TaskCreationSidebar({ isOpen, onClose, pipelineSlug }) {
|
|
72
|
+
const [messages, setMessages] = useState([]);
|
|
73
|
+
const [input, setInput] = useState("");
|
|
74
|
+
const [isSending, setIsSending] = useState(false);
|
|
75
|
+
const [isWaiting, setIsWaiting] = useState(false);
|
|
76
|
+
const [isReceiving, setIsReceiving] = useState(false);
|
|
77
|
+
const [error, setError] = useState(null);
|
|
78
|
+
const [taskProposals, setTaskProposals] = useState({});
|
|
79
|
+
const [creatingTask, setCreatingTask] = useState({});
|
|
80
|
+
const messagesEndRef = useRef(null);
|
|
81
|
+
|
|
82
|
+
// Auto-scroll to bottom when messages change
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
85
|
+
}, [messages]);
|
|
86
|
+
|
|
87
|
+
// Detect task proposals in assistant messages
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const newProposals = {};
|
|
90
|
+
messages.forEach((msg, i) => {
|
|
91
|
+
if (msg.role === "assistant") {
|
|
92
|
+
const proposal = parseTaskProposal(msg.content);
|
|
93
|
+
if (proposal) {
|
|
94
|
+
newProposals[i] = proposal;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
setTaskProposals(newProposals);
|
|
99
|
+
}, [messages]);
|
|
100
|
+
|
|
101
|
+
// Beforeunload warning when messages exist
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (messages.length === 0) return;
|
|
104
|
+
const handleUnload = (e) => {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
e.returnValue = "";
|
|
107
|
+
};
|
|
108
|
+
/* eslint-disable-next-line no-restricted-globals */
|
|
109
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
110
|
+
return () => {
|
|
111
|
+
/* eslint-disable-next-line no-restricted-globals */
|
|
112
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
113
|
+
};
|
|
114
|
+
}, [messages.length]);
|
|
115
|
+
|
|
116
|
+
// Close handler with confirmation
|
|
117
|
+
const handleClose = () => {
|
|
118
|
+
/* eslint-disable-next-line no-restricted-globals */
|
|
119
|
+
if (messages.length > 0 && !confirm("Close and lose conversation?")) return;
|
|
120
|
+
setMessages([]);
|
|
121
|
+
setInput("");
|
|
122
|
+
setError(null);
|
|
123
|
+
setTaskProposals({});
|
|
124
|
+
setCreatingTask({});
|
|
125
|
+
onClose();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleSend = (e) => {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
|
|
131
|
+
if (!input.trim()) return;
|
|
132
|
+
|
|
133
|
+
const newMessage = { role: "user", content: input.trim() };
|
|
134
|
+
setMessages([...messages, newMessage]);
|
|
135
|
+
setInput("");
|
|
136
|
+
setIsSending(true);
|
|
137
|
+
setIsWaiting(true);
|
|
138
|
+
setIsReceiving(false);
|
|
139
|
+
setError(null);
|
|
140
|
+
|
|
141
|
+
sendToAPI([...messages, newMessage]);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleCreateTask = async (messageIndex, proposal) => {
|
|
145
|
+
setCreatingTask((prev) => ({ ...prev, [messageIndex]: true }));
|
|
146
|
+
setTaskProposals((prev) => {
|
|
147
|
+
const updated = { ...prev };
|
|
148
|
+
if (updated[messageIndex]) {
|
|
149
|
+
updated[messageIndex] = { ...updated[messageIndex], error: null };
|
|
150
|
+
}
|
|
151
|
+
return updated;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const response = await fetch("/api/tasks/create", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
pipelineSlug,
|
|
160
|
+
filename: proposal.filename,
|
|
161
|
+
taskName: proposal.taskName,
|
|
162
|
+
code: proposal.code,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (response.ok) {
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
setTaskProposals((prev) => {
|
|
169
|
+
const updated = { ...prev };
|
|
170
|
+
if (updated[messageIndex]) {
|
|
171
|
+
updated[messageIndex] = {
|
|
172
|
+
...updated[messageIndex],
|
|
173
|
+
created: true,
|
|
174
|
+
path: data.path,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return updated;
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
const errorData = await response.json();
|
|
181
|
+
setTaskProposals((prev) => {
|
|
182
|
+
const updated = { ...prev };
|
|
183
|
+
if (updated[messageIndex]) {
|
|
184
|
+
updated[messageIndex] = {
|
|
185
|
+
...updated[messageIndex],
|
|
186
|
+
error:
|
|
187
|
+
(typeof errorData === "string"
|
|
188
|
+
? errorData
|
|
189
|
+
: errorData.error || errorData.message) ||
|
|
190
|
+
"Failed to create task",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return updated;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
setTaskProposals((prev) => {
|
|
198
|
+
const updated = { ...prev };
|
|
199
|
+
if (updated[messageIndex]) {
|
|
200
|
+
updated[messageIndex] = {
|
|
201
|
+
...updated[messageIndex],
|
|
202
|
+
error: "Network error: " + err.message,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return updated;
|
|
206
|
+
});
|
|
207
|
+
} finally {
|
|
208
|
+
setCreatingTask((prev) => ({ ...prev, [messageIndex]: false }));
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const sendToAPI = async (allMessages) => {
|
|
213
|
+
console.log("[TaskCreationSidebar] Starting API call with:", {
|
|
214
|
+
messageCount: allMessages.length,
|
|
215
|
+
pipelineSlug,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Add empty assistant message to accumulate response
|
|
219
|
+
setMessages([...allMessages, { role: "assistant", content: "" }]);
|
|
220
|
+
|
|
221
|
+
// Transition: sending → waiting after 300ms
|
|
222
|
+
setTimeout(() => setIsSending(false), 300);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
console.log("[TaskCreationSidebar] Fetching /api/ai/task-plan...");
|
|
226
|
+
const response = await fetch("/api/ai/task-plan", {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify({ messages: allMessages, pipelineSlug }),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log("[TaskCreationSidebar] Response received:", {
|
|
233
|
+
status: response.status,
|
|
234
|
+
statusText: response.statusText,
|
|
235
|
+
ok: response.ok,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
const errorText = await response.text();
|
|
240
|
+
console.error("[TaskCreationSidebar] Non-OK response:", errorText);
|
|
241
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const reader = response.body.getReader();
|
|
245
|
+
const decoder = new TextDecoder();
|
|
246
|
+
let buffer = "";
|
|
247
|
+
let currentEvent = "";
|
|
248
|
+
let chunksReceived = 0;
|
|
249
|
+
|
|
250
|
+
console.log("[TaskCreationSidebar] Starting to read SSE stream...");
|
|
251
|
+
while (true) {
|
|
252
|
+
const { done, value } = await reader.read();
|
|
253
|
+
if (done) {
|
|
254
|
+
console.log(
|
|
255
|
+
"[TaskCreationSidebar] Stream ended. Total chunks received:",
|
|
256
|
+
chunksReceived
|
|
257
|
+
);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
buffer += decoder.decode(value, { stream: true });
|
|
262
|
+
const lines = buffer.split("\n");
|
|
263
|
+
buffer = lines.pop(); // Keep incomplete line in buffer
|
|
264
|
+
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
if (line.startsWith("event: ")) {
|
|
267
|
+
currentEvent = line.slice(7);
|
|
268
|
+
console.log("[TaskCreationSidebar] SSE event type:", currentEvent);
|
|
269
|
+
} else if (line.startsWith("data: ")) {
|
|
270
|
+
try {
|
|
271
|
+
const data = JSON.parse(line.slice(6));
|
|
272
|
+
console.log("[TaskCreationSidebar] SSE data received:", {
|
|
273
|
+
eventType: currentEvent,
|
|
274
|
+
hasContent: !!data.content,
|
|
275
|
+
message: data.message,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (currentEvent === "chunk" && data.content) {
|
|
279
|
+
// Transition: waiting → receiving on first chunk
|
|
280
|
+
if (chunksReceived === 0) {
|
|
281
|
+
setIsWaiting(false);
|
|
282
|
+
setIsReceiving(true);
|
|
283
|
+
}
|
|
284
|
+
chunksReceived++;
|
|
285
|
+
setMessages((prev) => {
|
|
286
|
+
const updated = [...prev];
|
|
287
|
+
// Create shallow copy of message object to avoid mutation
|
|
288
|
+
const lastMsg = { ...updated[updated.length - 1] };
|
|
289
|
+
lastMsg.content += data.content;
|
|
290
|
+
updated[updated.length - 1] = lastMsg;
|
|
291
|
+
return updated;
|
|
292
|
+
});
|
|
293
|
+
} else if (currentEvent === "error" && data.message) {
|
|
294
|
+
console.error(
|
|
295
|
+
"[TaskCreationSidebar] SSE error event:",
|
|
296
|
+
data.message
|
|
297
|
+
);
|
|
298
|
+
setError(data.message);
|
|
299
|
+
} else if (currentEvent === "done") {
|
|
300
|
+
console.log("[TaskCreationSidebar] SSE done event received");
|
|
301
|
+
}
|
|
302
|
+
// Reset current event after processing data
|
|
303
|
+
currentEvent = "";
|
|
304
|
+
} catch (parseError) {
|
|
305
|
+
console.error(
|
|
306
|
+
"[TaskCreationSidebar] Failed to parse SSE data:",
|
|
307
|
+
parseError,
|
|
308
|
+
"Raw line:",
|
|
309
|
+
line
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error("[TaskCreationSidebar] Error in sendToAPI:", err);
|
|
317
|
+
setError(`Connection failed: ${err.message}`);
|
|
318
|
+
} finally {
|
|
319
|
+
console.log("[TaskCreationSidebar] sendToAPI completed");
|
|
320
|
+
setIsWaiting(false);
|
|
321
|
+
setIsReceiving(false);
|
|
322
|
+
setIsSending(false);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<Sidebar
|
|
328
|
+
open={isOpen}
|
|
329
|
+
onOpenChange={(open) => {
|
|
330
|
+
if (!open) handleClose();
|
|
331
|
+
}}
|
|
332
|
+
title="Task Assistant"
|
|
333
|
+
description="Describe the task you want to create"
|
|
334
|
+
contentClassName="flex flex-col max-h-screen"
|
|
335
|
+
>
|
|
336
|
+
{/* Messages area */}
|
|
337
|
+
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
338
|
+
{messages.map((msg, i) => (
|
|
339
|
+
<div key={i}>
|
|
340
|
+
<div
|
|
341
|
+
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
|
|
342
|
+
>
|
|
343
|
+
<div
|
|
344
|
+
className={`rounded-lg p-3 max-w-full ${
|
|
345
|
+
msg.role === "user"
|
|
346
|
+
? "bg-primary text-primary-foreground"
|
|
347
|
+
: "bg-muted text-muted-foreground"
|
|
348
|
+
}`}
|
|
349
|
+
>
|
|
350
|
+
{msg.role === "assistant" ? (
|
|
351
|
+
<>
|
|
352
|
+
<MarkdownRenderer
|
|
353
|
+
content={msg.content.replace(TASK_PROPOSAL_REGEX, "")}
|
|
354
|
+
/>
|
|
355
|
+
{isWaiting && !msg.content && (
|
|
356
|
+
<div className="flex items-center gap-1 text-muted-foreground mt-2">
|
|
357
|
+
{Array.from({ length: 5 }).map((_, idx) => (
|
|
358
|
+
<span
|
|
359
|
+
key={idx}
|
|
360
|
+
className="animate-bounce-wave"
|
|
361
|
+
style={{ animationDelay: `${idx * 0.1}s` }}
|
|
362
|
+
>
|
|
363
|
+
•
|
|
364
|
+
</span>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</>
|
|
369
|
+
) : (
|
|
370
|
+
<p className="whitespace-pre-wrap">{msg.content}</p>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
{taskProposals[i] && (
|
|
375
|
+
<TaskProposalCard
|
|
376
|
+
proposal={taskProposals[i]}
|
|
377
|
+
isCreating={creatingTask[i]}
|
|
378
|
+
onCreate={() => handleCreateTask(i, taskProposals[i])}
|
|
379
|
+
/>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
))}
|
|
383
|
+
|
|
384
|
+
<div ref={messagesEndRef} />
|
|
385
|
+
|
|
386
|
+
{error && (
|
|
387
|
+
<div className="mt-4 p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
|
|
388
|
+
<p className="text-destructive font-medium mb-2">{error}</p>
|
|
389
|
+
<Button
|
|
390
|
+
variant="destructive"
|
|
391
|
+
size="md"
|
|
392
|
+
onClick={() => {
|
|
393
|
+
setError(null);
|
|
394
|
+
// Re-send last user message
|
|
395
|
+
const lastUserMessageIndex = [...messages]
|
|
396
|
+
.reverse()
|
|
397
|
+
.findIndex((m) => m.role === "user");
|
|
398
|
+
if (lastUserMessageIndex !== -1) {
|
|
399
|
+
const lastUserMessage =
|
|
400
|
+
messages[messages.length - 1 - lastUserMessageIndex];
|
|
401
|
+
const messagesBeforeLastUserMessage = messages.slice(
|
|
402
|
+
0,
|
|
403
|
+
messages.length - 1 - lastUserMessageIndex
|
|
404
|
+
);
|
|
405
|
+
sendToAPI([
|
|
406
|
+
...messagesBeforeLastUserMessage,
|
|
407
|
+
lastUserMessage,
|
|
408
|
+
]);
|
|
409
|
+
}
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
Retry
|
|
413
|
+
</Button>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
{/* Input area */}
|
|
419
|
+
<SidebarFooter className="bg-card">
|
|
420
|
+
<form onSubmit={handleSend} className="w-full">
|
|
421
|
+
<textarea
|
|
422
|
+
value={input}
|
|
423
|
+
onChange={(e) => setInput(e.target.value)}
|
|
424
|
+
disabled={isSending || isWaiting || isReceiving}
|
|
425
|
+
placeholder="Describe the task you want to create..."
|
|
426
|
+
rows={3}
|
|
427
|
+
className="w-full border rounded-md px-3 py-2 resize-none disabled:bg-muted disabled:cursor-not-allowed mb-3 focus:outline-none focus:ring-2 focus:ring-ring bg-background"
|
|
428
|
+
aria-label="Task description input"
|
|
429
|
+
/>
|
|
430
|
+
<div className="flex justify-end">
|
|
431
|
+
<Button
|
|
432
|
+
variant="solid"
|
|
433
|
+
size="md"
|
|
434
|
+
type="submit"
|
|
435
|
+
disabled={isSending || isWaiting || isReceiving || !input.trim()}
|
|
436
|
+
>
|
|
437
|
+
{isSending && "Sending..."}
|
|
438
|
+
{isWaiting && "Thinking..."}
|
|
439
|
+
{isReceiving && "Receiving..."}
|
|
440
|
+
{!isSending && !isWaiting && !isReceiving && "Send"}
|
|
441
|
+
</Button>
|
|
442
|
+
</div>
|
|
443
|
+
</form>
|
|
444
|
+
</SidebarFooter>
|
|
445
|
+
</Sidebar>
|
|
446
|
+
);
|
|
447
|
+
}
|