@ryanfw/prompt-orchestration-pipeline 0.12.0 → 0.13.1

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.
Files changed (60) hide show
  1. package/package.json +10 -1
  2. package/src/cli/analyze-task.js +51 -0
  3. package/src/cli/index.js +8 -0
  4. package/src/components/AddPipelineSidebar.jsx +144 -0
  5. package/src/components/AnalysisProgressTray.jsx +87 -0
  6. package/src/components/JobTable.jsx +4 -3
  7. package/src/components/Layout.jsx +142 -139
  8. package/src/components/MarkdownRenderer.jsx +149 -0
  9. package/src/components/PipelineDAGGrid.jsx +404 -0
  10. package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
  11. package/src/components/SchemaPreviewPanel.jsx +97 -0
  12. package/src/components/StageTimeline.jsx +36 -0
  13. package/src/components/TaskAnalysisDisplay.jsx +227 -0
  14. package/src/components/TaskCreationSidebar.jsx +447 -0
  15. package/src/components/TaskDetailSidebar.jsx +119 -117
  16. package/src/components/TaskFilePane.jsx +94 -39
  17. package/src/components/ui/button.jsx +59 -27
  18. package/src/components/ui/sidebar.jsx +118 -0
  19. package/src/config/models.js +99 -67
  20. package/src/core/config.js +4 -1
  21. package/src/llm/index.js +129 -9
  22. package/src/pages/PipelineDetail.jsx +6 -6
  23. package/src/pages/PipelineList.jsx +214 -0
  24. package/src/pages/PipelineTypeDetail.jsx +234 -0
  25. package/src/providers/deepseek.js +76 -16
  26. package/src/providers/openai.js +61 -34
  27. package/src/task-analysis/enrichers/analysis-writer.js +62 -0
  28. package/src/task-analysis/enrichers/schema-deducer.js +145 -0
  29. package/src/task-analysis/enrichers/schema-writer.js +74 -0
  30. package/src/task-analysis/extractors/artifacts.js +137 -0
  31. package/src/task-analysis/extractors/llm-calls.js +176 -0
  32. package/src/task-analysis/extractors/stages.js +51 -0
  33. package/src/task-analysis/index.js +103 -0
  34. package/src/task-analysis/parser.js +28 -0
  35. package/src/task-analysis/utils/ast.js +43 -0
  36. package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
  37. package/src/ui/client/index.css +64 -0
  38. package/src/ui/client/main.jsx +4 -0
  39. package/src/ui/client/sse-fetch.js +120 -0
  40. package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
  41. package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
  42. package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
  43. package/src/ui/dist/index.html +2 -2
  44. package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
  45. package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
  46. package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
  47. package/src/ui/endpoints/pipelines-endpoint.js +133 -0
  48. package/src/ui/endpoints/schema-file-endpoint.js +105 -0
  49. package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
  50. package/src/ui/endpoints/task-creation-endpoint.js +114 -0
  51. package/src/ui/endpoints/task-save-endpoint.js +101 -0
  52. package/src/ui/express-app.js +45 -0
  53. package/src/ui/lib/analysis-lock.js +67 -0
  54. package/src/ui/lib/sse.js +30 -0
  55. package/src/ui/server.js +4 -0
  56. package/src/ui/utils/slug.js +31 -0
  57. package/src/ui/watcher.js +28 -2
  58. package/src/ui/dist/assets/index-B320avRx.js +0 -26613
  59. package/src/ui/dist/assets/index-B320avRx.js.map +0 -1
  60. package/src/ui/dist/assets/style-BYCoLBnK.css +0 -62
@@ -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
+ }