@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.
Files changed (83) hide show
  1. package/package.json +11 -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/DAGGrid.jsx +157 -47
  7. package/src/components/JobTable.jsx +4 -3
  8. package/src/components/Layout.jsx +142 -139
  9. package/src/components/MarkdownRenderer.jsx +149 -0
  10. package/src/components/PipelineDAGGrid.jsx +404 -0
  11. package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
  12. package/src/components/SchemaPreviewPanel.jsx +97 -0
  13. package/src/components/StageTimeline.jsx +36 -0
  14. package/src/components/TaskAnalysisDisplay.jsx +227 -0
  15. package/src/components/TaskCreationSidebar.jsx +447 -0
  16. package/src/components/TaskDetailSidebar.jsx +119 -117
  17. package/src/components/TaskFilePane.jsx +94 -39
  18. package/src/components/ui/RestartJobModal.jsx +26 -6
  19. package/src/components/ui/StopJobModal.jsx +183 -0
  20. package/src/components/ui/button.jsx +59 -27
  21. package/src/components/ui/sidebar.jsx +118 -0
  22. package/src/config/models.js +99 -67
  23. package/src/core/config.js +11 -4
  24. package/src/core/lifecycle-policy.js +62 -0
  25. package/src/core/pipeline-runner.js +312 -217
  26. package/src/core/status-writer.js +84 -0
  27. package/src/llm/index.js +129 -9
  28. package/src/pages/Code.jsx +8 -1
  29. package/src/pages/PipelineDetail.jsx +84 -2
  30. package/src/pages/PipelineList.jsx +214 -0
  31. package/src/pages/PipelineTypeDetail.jsx +234 -0
  32. package/src/pages/PromptPipelineDashboard.jsx +10 -11
  33. package/src/providers/deepseek.js +76 -16
  34. package/src/providers/openai.js +61 -34
  35. package/src/task-analysis/enrichers/analysis-writer.js +62 -0
  36. package/src/task-analysis/enrichers/schema-deducer.js +145 -0
  37. package/src/task-analysis/enrichers/schema-writer.js +74 -0
  38. package/src/task-analysis/extractors/artifacts.js +137 -0
  39. package/src/task-analysis/extractors/llm-calls.js +176 -0
  40. package/src/task-analysis/extractors/stages.js +51 -0
  41. package/src/task-analysis/index.js +103 -0
  42. package/src/task-analysis/parser.js +28 -0
  43. package/src/task-analysis/utils/ast.js +43 -0
  44. package/src/ui/client/adapters/job-adapter.js +60 -0
  45. package/src/ui/client/api.js +233 -8
  46. package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
  47. package/src/ui/client/hooks/useJobList.js +14 -1
  48. package/src/ui/client/index.css +64 -0
  49. package/src/ui/client/main.jsx +4 -0
  50. package/src/ui/client/sse-fetch.js +120 -0
  51. package/src/ui/dist/app.js +262 -0
  52. package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
  53. package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
  54. package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
  55. package/src/ui/dist/favicon.svg +12 -0
  56. package/src/ui/dist/index.html +2 -2
  57. package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
  58. package/src/ui/endpoints/file-endpoints.js +330 -0
  59. package/src/ui/endpoints/job-control-endpoints.js +1001 -0
  60. package/src/ui/endpoints/job-endpoints.js +62 -0
  61. package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
  62. package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
  63. package/src/ui/endpoints/pipelines-endpoint.js +133 -0
  64. package/src/ui/endpoints/schema-file-endpoint.js +105 -0
  65. package/src/ui/endpoints/sse-endpoints.js +223 -0
  66. package/src/ui/endpoints/state-endpoint.js +85 -0
  67. package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
  68. package/src/ui/endpoints/task-creation-endpoint.js +114 -0
  69. package/src/ui/endpoints/task-save-endpoint.js +101 -0
  70. package/src/ui/endpoints/upload-endpoints.js +406 -0
  71. package/src/ui/express-app.js +227 -0
  72. package/src/ui/lib/analysis-lock.js +67 -0
  73. package/src/ui/lib/sse.js +30 -0
  74. package/src/ui/server.js +42 -1880
  75. package/src/ui/sse-broadcast.js +93 -0
  76. package/src/ui/utils/http-utils.js +139 -0
  77. package/src/ui/utils/mime-types.js +196 -0
  78. package/src/ui/utils/slug.js +31 -0
  79. package/src/ui/vite.config.js +22 -0
  80. package/src/ui/watcher.js +28 -2
  81. package/src/utils/jobs.js +39 -0
  82. package/src/ui/dist/assets/index-DeDzq-Kk.js +0 -23863
  83. 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
+ }