@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.
- package/package.json +10 -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/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/button.jsx +59 -27
- package/src/components/ui/sidebar.jsx +118 -0
- package/src/config/models.js +99 -67
- package/src/core/config.js +4 -1
- package/src/llm/index.js +129 -9
- package/src/pages/PipelineDetail.jsx +6 -6
- package/src/pages/PipelineList.jsx +214 -0
- package/src/pages/PipelineTypeDetail.jsx +234 -0
- 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/hooks/useAnalysisProgress.js +145 -0
- 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/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/index.html +2 -2
- package/src/ui/endpoints/create-pipeline-endpoint.js +194 -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/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/express-app.js +45 -0
- package/src/ui/lib/analysis-lock.js +67 -0
- package/src/ui/lib/sse.js +30 -0
- package/src/ui/server.js +4 -0
- package/src/ui/utils/slug.js +31 -0
- package/src/ui/watcher.js +28 -2
- package/src/ui/dist/assets/index-B320avRx.js +0 -26613
- package/src/ui/dist/assets/index-B320avRx.js.map +0 -1
- 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);
|