@ryanfw/prompt-orchestration-pipeline 0.12.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 (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,149 @@
1
+ import React, { useState } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import rehypeHighlight from "rehype-highlight";
5
+ import "highlight.js/styles/github-dark.css";
6
+
7
+ /**
8
+ * MarkdownRenderer component for rendering markdown content with syntax highlighting
9
+ * @param {Object} props - Component props
10
+ * @param {string} props.content - Markdown content to render
11
+ * @param {string} props.className - Additional CSS classes
12
+ */
13
+ export function MarkdownRenderer({ content, className = "" }) {
14
+ const [copiedCode, setCopiedCode] = useState(null);
15
+
16
+ // Handle code copy
17
+ const handleCopyCode = async (code) => {
18
+ try {
19
+ await navigator.clipboard.writeText(code);
20
+ setCopiedCode(code);
21
+ setTimeout(() => setCopiedCode(null), 2000);
22
+ } catch (err) {
23
+ console.error("Failed to copy code:", err);
24
+ }
25
+ };
26
+
27
+ // Custom code block component with copy button
28
+ const CodeBlock = ({ children, className: codeClassName }) => {
29
+ const language = codeClassName?.replace("language-", "") || "text";
30
+ const code = React.Children.toArray(children).join("");
31
+ const isCopied = copiedCode === code;
32
+
33
+ return (
34
+ <div className="relative group">
35
+ <pre className="!bg-muted !text-foreground rounded-lg p-4 overflow-x-auto mt-3 mb-3">
36
+ <code className={codeClassName}>{children}</code>
37
+ </pre>
38
+ <button
39
+ onClick={() => handleCopyCode(code)}
40
+ className="absolute top-2 right-2 bg-muted-foreground/80 text-background text-xs px-2 py-1 rounded hover:bg-muted-foreground transition-opacity opacity-0 group-hover:opacity-100"
41
+ aria-label="Copy code to clipboard"
42
+ >
43
+ {isCopied ? "Copied!" : "Copy"}
44
+ </button>
45
+ {language !== "text" && (
46
+ <span className="absolute top-2 left-2 text-xs text-muted-foreground">
47
+ {language}
48
+ </span>
49
+ )}
50
+ </div>
51
+ );
52
+ };
53
+
54
+ return (
55
+ <div
56
+ className={`!max-w-none prose prose-sm dark:prose-invert ${className}`}
57
+ >
58
+ <ReactMarkdown
59
+ remarkPlugins={[remarkGfm]}
60
+ rehypePlugins={[rehypeHighlight]}
61
+ components={{
62
+ code: CodeBlock,
63
+ h1: ({ children }) => (
64
+ <h1 className="text-xl font-bold mb-3 text-foreground">
65
+ {children}
66
+ </h1>
67
+ ),
68
+ h2: ({ children }) => (
69
+ <h2 className="text-lg font-semibold mb-2 text-foreground">
70
+ {children}
71
+ </h2>
72
+ ),
73
+ h3: ({ children }) => (
74
+ <h3 className="text-base font-medium mb-2 text-foreground">
75
+ {children}
76
+ </h3>
77
+ ),
78
+ p: ({ children }) => (
79
+ <p className="mb-3 text-foreground leading-relaxed">{children}</p>
80
+ ),
81
+ ul: ({ children }) => (
82
+ <ul className="list-disc pl-5 mb-3 space-y-1 text-foreground">
83
+ {children}
84
+ </ul>
85
+ ),
86
+ ol: ({ children }) => (
87
+ <ol className="list-decimal pl-5 mb-3 space-y-1 text-foreground">
88
+ {children}
89
+ </ol>
90
+ ),
91
+ li: ({ children }) => <li className="ml-2">{children}</li>,
92
+ a: ({ children, href }) => (
93
+ <a
94
+ href={href}
95
+ className="text-primary hover:text-primary/80 underline"
96
+ target="_blank"
97
+ rel="noopener noreferrer"
98
+ >
99
+ {children}
100
+ </a>
101
+ ),
102
+ blockquote: ({ children }) => (
103
+ <blockquote className="border-l-4 border-primary/50 pl-4 py-2 my-3 bg-muted/30 italic text-foreground/80">
104
+ {children}
105
+ </blockquote>
106
+ ),
107
+ table: ({ children }) => (
108
+ <div className="overflow-x-auto my-4">
109
+ <table className="min-w-full border-collapse border border-border text-foreground">
110
+ {children}
111
+ </table>
112
+ </div>
113
+ ),
114
+ thead: ({ children }) => (
115
+ <thead className="bg-muted/50">{children}</thead>
116
+ ),
117
+ tbody: ({ children }) => <tbody>{children}</tbody>,
118
+ tr: ({ children }) => (
119
+ <tr className="border-b border-border hover:bg-muted/20">
120
+ {children}
121
+ </tr>
122
+ ),
123
+ th: ({ children }) => (
124
+ <th className="border border-border px-4 py-2 text-left font-semibold">
125
+ {children}
126
+ </th>
127
+ ),
128
+ td: ({ children }) => (
129
+ <td className="border border-border px-4 py-2">{children}</td>
130
+ ),
131
+ hr: () => <hr className="my-4 border-border" />,
132
+ strong: ({ children }) => (
133
+ <strong className="font-bold text-foreground">{children}</strong>
134
+ ),
135
+ em: ({ children }) => (
136
+ <em className="italic text-foreground">{children}</em>
137
+ ),
138
+ del: ({ children }) => (
139
+ <del className="line-through text-muted-foreground">{children}</del>
140
+ ),
141
+ }}
142
+ >
143
+ {content}
144
+ </ReactMarkdown>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ export default MarkdownRenderer;
@@ -0,0 +1,404 @@
1
+ import React, {
2
+ useLayoutEffect,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ createRef,
7
+ } from "react";
8
+ import { areGeometriesEqual } from "../utils/geometry-equality.js";
9
+ import { PipelineTypeTaskSidebar } from "./PipelineTypeTaskSidebar.jsx";
10
+
11
+ // Utility to check for reduced motion preference
12
+ const prefersReducedMotion = () => {
13
+ if (typeof window === "undefined") return false;
14
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
15
+ };
16
+
17
+ // Helpers: capitalize fallback step ids (upperFirst only; do not alter provided titles)
18
+ function upperFirst(s) {
19
+ return typeof s === "string" && s.length > 0
20
+ ? s.charAt(0).toUpperCase() + s.slice(1)
21
+ : s;
22
+ }
23
+
24
+ // Format step name for display
25
+ function formatStepName(item, idx) {
26
+ const raw = item.title ?? item.id ?? `Step ${idx + 1}`;
27
+ // If item has a title, assume it's curated and leave unchanged; otherwise capitalize fallback
28
+ return upperFirst(item.title ? item.title : raw);
29
+ }
30
+
31
+ // Get CSS classes for card header based on status
32
+ const getHeaderClasses = (status) => {
33
+ switch (status) {
34
+ case "definition":
35
+ return "bg-blue-50 border-blue-200 text-blue-700";
36
+ default:
37
+ return "bg-gray-100 border-gray-200 text-gray-700";
38
+ }
39
+ };
40
+
41
+ // Simplified card component for pipeline type view
42
+ const PipelineCard = React.memo(function PipelineCard({
43
+ item,
44
+ idx,
45
+ nodeRef,
46
+ status,
47
+ onClick,
48
+ }) {
49
+ const reducedMotion = prefersReducedMotion();
50
+
51
+ return (
52
+ <div
53
+ ref={nodeRef}
54
+ role="listitem"
55
+ tabIndex={0}
56
+ onClick={onClick}
57
+ onKeyDown={(e) => {
58
+ if (e.key === "Enter" || e.key === " ") {
59
+ e.preventDefault();
60
+ onClick();
61
+ }
62
+ }}
63
+ className={`cursor-pointer rounded-lg border border-gray-400 bg-white overflow-hidden flex flex-col ${reducedMotion ? "" : "transition-all duration-200 ease-in-out"} outline outline-2 outline-transparent hover:outline-gray-400/70 focus-visible:outline-blue-500/60`}
64
+ >
65
+ <div
66
+ data-role="card-header"
67
+ className={`rounded-t-lg px-4 py-2 border-b flex items-center justify-between gap-3 ${reducedMotion ? "" : "transition-opacity duration-300 ease-in-out"} ${getHeaderClasses(status)}`}
68
+ >
69
+ <div className="font-medium truncate">{formatStepName(item, idx)}</div>
70
+ <div className="flex items-center gap-2">
71
+ <span
72
+ className={`text-[11px] uppercase tracking-wide opacity-80${reducedMotion ? "" : " transition-opacity duration-200"}`}
73
+ >
74
+ {status}
75
+ </span>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ });
81
+
82
+ /**
83
+ * PipelineDAGGrid component for static visualization of pipeline types
84
+ * @param {Object} props
85
+ * @param {Array} props.items - Array of pipeline items with id, title?, status?
86
+ * @param {number} props.cols - Number of columns for grid layout (default: 3)
87
+ * @param {string} props.pipelineSlug - Slug identifier for the pipeline
88
+ */
89
+ function PipelineDAGGrid({ items, cols = 3, pipelineSlug }) {
90
+ const overlayRef = useRef(null);
91
+ const gridRef = useRef(null);
92
+ const nodeRefs = useRef([]);
93
+ const [lines, setLines] = useState([]);
94
+ const [effectiveCols, setEffectiveCols] = useState(cols);
95
+ const [openIdx, setOpenIdx] = useState(-1);
96
+
97
+ // Previous geometry snapshot for throttling connector recomputation
98
+ const prevGeometryRef = useRef(null);
99
+ const rafRef = useRef(null);
100
+
101
+ // Create refs for each node
102
+ nodeRefs.current = useMemo(
103
+ () => items.map((_, i) => nodeRefs.current[i] ?? createRef()),
104
+ [items.length]
105
+ );
106
+
107
+ // Responsive: force single-column on narrow screens
108
+ useLayoutEffect(() => {
109
+ // Skip in test environment
110
+ if (process.env.NODE_ENV === "test") {
111
+ setEffectiveCols(cols);
112
+ return;
113
+ }
114
+
115
+ const mq = window.matchMedia("(min-width: 1024px)");
116
+ const apply = () => setEffectiveCols(mq.matches ? cols : 1);
117
+ apply();
118
+
119
+ const handleChange = () => apply();
120
+ mq.addEventListener
121
+ ? mq.addEventListener("change", handleChange)
122
+ : mq.addListener(handleChange);
123
+
124
+ return () => {
125
+ mq.removeEventListener
126
+ ? mq.removeEventListener("change", handleChange)
127
+ : mq.removeListener(handleChange);
128
+ };
129
+ }, [cols]);
130
+
131
+ // Calculate visual order for snake-like layout
132
+ const visualOrder = useMemo(() => {
133
+ if (effectiveCols === 1) {
134
+ return Array.from({ length: items.length }, (_, i) => i);
135
+ }
136
+
137
+ const order = [];
138
+ const rows = Math.ceil(items.length / effectiveCols);
139
+
140
+ for (let r = 0; r < rows; r++) {
141
+ const start = r * effectiveCols;
142
+ const end = Math.min(start + effectiveCols, items.length);
143
+ const slice = Array.from({ length: end - start }, (_, k) => start + k);
144
+ const rowLen = slice.length;
145
+
146
+ const isReversedRow = r % 2 === 1; // odd rows RTL
147
+ if (isReversedRow) {
148
+ // Reverse order for odd rows (snake pattern)
149
+ const reversed = slice.reverse();
150
+ const pad = effectiveCols - rowLen;
151
+ order.push(...Array(pad).fill(-1), ...reversed);
152
+ } else {
153
+ order.push(...slice);
154
+ }
155
+ }
156
+
157
+ return order;
158
+ }, [items.length, effectiveCols]);
159
+
160
+ // Calculate connector lines between cards with throttling
161
+ useLayoutEffect(() => {
162
+ // Skip entirely in test environment to prevent hanging
163
+ if (process.env.NODE_ENV === "test") {
164
+ return;
165
+ }
166
+
167
+ // Skip if no window or no items
168
+ if (
169
+ typeof window === "undefined" ||
170
+ !overlayRef.current ||
171
+ items.length === 0
172
+ ) {
173
+ return;
174
+ }
175
+
176
+ // Throttled compute function using requestAnimationFrame
177
+ const compute = () => {
178
+ // Cancel any pending RAF
179
+ if (rafRef.current) {
180
+ cancelAnimationFrame(rafRef.current);
181
+ }
182
+
183
+ rafRef.current = requestAnimationFrame(() => {
184
+ if (!overlayRef.current) return;
185
+
186
+ const overlayBox = overlayRef.current.getBoundingClientRect();
187
+ const boxes = nodeRefs.current.map((r) => {
188
+ const el = r.current;
189
+ if (!el) return null;
190
+
191
+ const b = el.getBoundingClientRect();
192
+ const headerEl = el.querySelector('[data-role="card-header"]');
193
+ const hr = headerEl ? headerEl.getBoundingClientRect() : null;
194
+ const headerMidY = hr
195
+ ? hr.top - overlayBox.top + hr.height / 2
196
+ : b.top - overlayBox.top + Math.min(24, b.height / 6);
197
+
198
+ return {
199
+ left: b.left - overlayBox.left,
200
+ top: b.top - overlayBox.top,
201
+ width: b.width,
202
+ height: b.height,
203
+ right: b.right - overlayBox.left,
204
+ bottom: b.bottom - overlayBox.top,
205
+ cx: b.left - overlayBox.left + b.width / 2,
206
+ cy: b.top - overlayBox.top + b.height / 2,
207
+ headerMidY,
208
+ };
209
+ });
210
+
211
+ // Check if geometry changed significantly
212
+ const currentGeometry = {
213
+ overlayBox,
214
+ boxes: boxes.filter(Boolean),
215
+ effectiveCols,
216
+ itemsLength: items.length,
217
+ };
218
+
219
+ const geometryChanged =
220
+ !prevGeometryRef.current ||
221
+ !areGeometriesEqual(prevGeometryRef.current, currentGeometry);
222
+
223
+ if (!geometryChanged) {
224
+ rafRef.current = null;
225
+ return;
226
+ }
227
+
228
+ const newLines = [];
229
+ for (let i = 0; i < items.length - 1; i++) {
230
+ const a = boxes[i];
231
+ const b = boxes[i + 1];
232
+ if (!a || !b) continue;
233
+
234
+ const rowA = Math.floor(i / effectiveCols);
235
+ const rowB = Math.floor((i + 1) / effectiveCols);
236
+ const sameRow = rowA === rowB;
237
+
238
+ if (sameRow) {
239
+ // Horizontal connection
240
+ const leftToRight = rowA % 2 === 0;
241
+ if (leftToRight) {
242
+ const start = { x: a.right, y: a.headerMidY };
243
+ const end = { x: b.left, y: b.headerMidY };
244
+ const midX = (start.x + end.x) / 2;
245
+ newLines.push({
246
+ d: `M ${start.x} ${start.y} L ${midX} ${start.y} L ${midX} ${end.y} L ${end.x} ${end.y}`,
247
+ });
248
+ } else {
249
+ const start = { x: a.left, y: a.headerMidY };
250
+ const end = { x: b.right, y: b.headerMidY };
251
+ const midX = (start.x + end.x) / 2;
252
+ newLines.push({
253
+ d: `M ${start.x} ${start.y} L ${midX} ${start.y} L ${midX} ${end.y} L ${end.x} ${end.y}`,
254
+ });
255
+ }
256
+ } else {
257
+ // Vertical connection
258
+ const start = { x: a.cx, y: a.bottom };
259
+ const end = { x: b.cx, y: b.top };
260
+ const midY = (start.y + end.y) / 2;
261
+ newLines.push({
262
+ d: `M ${start.x} ${start.y} L ${start.x} ${midY} L ${end.x} ${midY} L ${end.x} ${end.y}`,
263
+ });
264
+ }
265
+ }
266
+
267
+ prevGeometryRef.current = currentGeometry;
268
+ setLines(newLines);
269
+ rafRef.current = null;
270
+ });
271
+ };
272
+
273
+ // Initial compute
274
+ compute();
275
+
276
+ // Set up observers only if ResizeObserver is available and not in test
277
+ let ro = null;
278
+ if (
279
+ typeof ResizeObserver !== "undefined" &&
280
+ process.env.NODE_ENV !== "test"
281
+ ) {
282
+ ro = new ResizeObserver(compute);
283
+ if (gridRef.current) ro.observe(gridRef.current);
284
+ nodeRefs.current.forEach((r) => r.current && ro.observe(r.current));
285
+ }
286
+
287
+ const handleResize = () => compute();
288
+ window.addEventListener("resize", handleResize);
289
+
290
+ return () => {
291
+ if (ro) ro.disconnect();
292
+ window.removeEventListener("resize", handleResize);
293
+ if (rafRef.current) {
294
+ cancelAnimationFrame(rafRef.current);
295
+ }
296
+ };
297
+ }, [items.length, effectiveCols, visualOrder]);
298
+
299
+ // Handle card click to open sidebar
300
+ const handleCardClick = (idx) => {
301
+ setOpenIdx(openIdx === idx ? -1 : idx);
302
+ };
303
+
304
+ // Handle Escape key to close sidebar
305
+ React.useEffect(() => {
306
+ const handleKeyDown = (e) => {
307
+ if (e.key === "Escape" && openIdx !== -1) {
308
+ setOpenIdx(-1);
309
+ }
310
+ };
311
+
312
+ if (openIdx !== -1) {
313
+ document.addEventListener("keydown", handleKeyDown);
314
+ return () => document.removeEventListener("keydown", handleKeyDown);
315
+ }
316
+ }, [openIdx]);
317
+
318
+ return (
319
+ <div className="relative w-full" role="list">
320
+ {/* SVG overlay for connector lines */}
321
+ <svg
322
+ ref={overlayRef}
323
+ className="absolute inset-0 w-full h-full pointer-events-none z-10"
324
+ aria-hidden="true"
325
+ >
326
+ <defs>
327
+ <marker
328
+ id="arrow"
329
+ viewBox="0 0 10 10"
330
+ refX="10"
331
+ refY="5"
332
+ markerWidth="8"
333
+ markerHeight="8"
334
+ orient="auto"
335
+ markerUnits="userSpaceOnUse"
336
+ className="text-gray-400"
337
+ >
338
+ <path d="M 0 0 L 10 5 L 0 10 z" />
339
+ </marker>
340
+ </defs>
341
+ {lines.map((line, idx) => (
342
+ <g key={idx}>
343
+ <path
344
+ d={line.d}
345
+ fill="none"
346
+ stroke="currentColor"
347
+ strokeWidth="1"
348
+ strokeLinecap="square"
349
+ className="text-gray-400"
350
+ strokeLinejoin="round"
351
+ markerEnd="url(#arrow)"
352
+ />
353
+ </g>
354
+ ))}
355
+ </svg>
356
+
357
+ {/* Grid of pipeline cards */}
358
+ <div
359
+ ref={gridRef}
360
+ className="grid grid-cols-1 lg:grid-cols-3 gap-16 relative z-0"
361
+ >
362
+ {visualOrder.map((idx, mapIndex) => {
363
+ if (idx === -1) {
364
+ return (
365
+ <div
366
+ key={`ghost-${mapIndex}`}
367
+ className="invisible"
368
+ aria-hidden="true"
369
+ />
370
+ );
371
+ }
372
+
373
+ const item = items[idx];
374
+ const status = item.status || "definition";
375
+
376
+ return (
377
+ <PipelineCard
378
+ key={item.id ?? idx}
379
+ idx={idx}
380
+ nodeRef={nodeRefs.current[idx]}
381
+ status={status}
382
+ onClick={() => handleCardClick(idx)}
383
+ item={item}
384
+ />
385
+ );
386
+ })}
387
+ </div>
388
+
389
+ {/* PipelineTypeTaskSidebar */}
390
+ {openIdx !== -1 && (
391
+ <PipelineTypeTaskSidebar
392
+ open={openIdx !== -1}
393
+ title={formatStepName(items[openIdx], openIdx)}
394
+ status={items[openIdx]?.status || "definition"}
395
+ task={items[openIdx]}
396
+ pipelineSlug={pipelineSlug}
397
+ onClose={() => setOpenIdx(-1)}
398
+ />
399
+ )}
400
+ </div>
401
+ );
402
+ }
403
+
404
+ export default PipelineDAGGrid;
@@ -0,0 +1,96 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Sidebar } from "./ui/sidebar.jsx";
3
+ import { TaskAnalysisDisplay } from "./TaskAnalysisDisplay.jsx";
4
+
5
+ /**
6
+ * PipelineTypeTaskSidebar component for displaying pipeline type task details in a slide-over panel
7
+ * @param {Object} props - Component props
8
+ * @param {boolean} props.open - Whether the sidebar is open
9
+ * @param {string} props.title - Preformatted step name for the header
10
+ * @param {string} props.status - Status for styling
11
+ * @param {Object} props.task - Task object with id, title, and other metadata
12
+ * @param {string} props.pipelineSlug - Pipeline slug for fetching analysis
13
+ * @param {Function} props.onClose - Close handler
14
+ */
15
+ export function PipelineTypeTaskSidebar({
16
+ open,
17
+ title,
18
+ status,
19
+ task,
20
+ pipelineSlug,
21
+ onClose,
22
+ }) {
23
+ const [analysis, setAnalysis] = useState(null);
24
+ const [analysisLoading, setAnalysisLoading] = useState(false);
25
+ const [analysisError, setAnalysisError] = useState(null);
26
+
27
+ useEffect(() => {
28
+ if (!open) {
29
+ // Reset analysis state when sidebar closes to prevent stale data
30
+ setAnalysis(null);
31
+ setAnalysisLoading(false);
32
+ setAnalysisError(null);
33
+ return;
34
+ }
35
+
36
+ if (!task?.id || !pipelineSlug) {
37
+ return;
38
+ }
39
+
40
+ const fetchAnalysis = async () => {
41
+ setAnalysisLoading(true);
42
+ setAnalysisError(null);
43
+
44
+ try {
45
+ const response = await fetch(
46
+ `/api/pipelines/${pipelineSlug}/tasks/${task.id}/analysis`
47
+ );
48
+ const data = await response.json();
49
+
50
+ if (!response.ok) {
51
+ throw new Error(data.message || "Failed to fetch analysis");
52
+ }
53
+
54
+ setAnalysis(data.data);
55
+ } catch (err) {
56
+ setAnalysisError(err.message);
57
+ } finally {
58
+ setAnalysisLoading(false);
59
+ }
60
+ };
61
+
62
+ fetchAnalysis();
63
+ }, [open, task?.id, pipelineSlug]);
64
+
65
+ // Get CSS classes for card header based on status
66
+ const getHeaderClasses = (status) => {
67
+ switch (status) {
68
+ case "definition":
69
+ return "bg-blue-50 border-blue-200 text-blue-700";
70
+ default:
71
+ return "bg-muted/50 border-input text-foreground";
72
+ }
73
+ };
74
+
75
+ if (!open) {
76
+ return null;
77
+ }
78
+
79
+ return (
80
+ <Sidebar
81
+ open={open}
82
+ onOpenChange={(isOpen) => !isOpen && onClose()}
83
+ title={title}
84
+ headerClassName={getHeaderClasses(status)}
85
+ >
86
+ <TaskAnalysisDisplay
87
+ analysis={analysis}
88
+ loading={analysisLoading}
89
+ error={analysisError}
90
+ pipelineSlug={pipelineSlug}
91
+ />
92
+ </Sidebar>
93
+ );
94
+ }
95
+
96
+ export default React.memo(PipelineTypeTaskSidebar);