@ryanfw/prompt-orchestration-pipeline 0.0.1 → 0.3.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 (76) hide show
  1. package/README.md +415 -24
  2. package/package.json +45 -8
  3. package/src/api/files.js +48 -0
  4. package/src/api/index.js +149 -53
  5. package/src/api/validators/seed.js +141 -0
  6. package/src/cli/index.js +456 -29
  7. package/src/cli/run-orchestrator.js +39 -0
  8. package/src/cli/update-pipeline-json.js +47 -0
  9. package/src/components/DAGGrid.jsx +649 -0
  10. package/src/components/JobCard.jsx +96 -0
  11. package/src/components/JobDetail.jsx +159 -0
  12. package/src/components/JobTable.jsx +202 -0
  13. package/src/components/Layout.jsx +134 -0
  14. package/src/components/TaskFilePane.jsx +570 -0
  15. package/src/components/UploadSeed.jsx +239 -0
  16. package/src/components/ui/badge.jsx +20 -0
  17. package/src/components/ui/button.jsx +43 -0
  18. package/src/components/ui/card.jsx +20 -0
  19. package/src/components/ui/focus-styles.css +60 -0
  20. package/src/components/ui/progress.jsx +26 -0
  21. package/src/components/ui/select.jsx +27 -0
  22. package/src/components/ui/separator.jsx +6 -0
  23. package/src/config/paths.js +99 -0
  24. package/src/core/config.js +270 -9
  25. package/src/core/file-io.js +202 -0
  26. package/src/core/module-loader.js +157 -0
  27. package/src/core/orchestrator.js +275 -294
  28. package/src/core/pipeline-runner.js +95 -41
  29. package/src/core/progress.js +66 -0
  30. package/src/core/status-writer.js +331 -0
  31. package/src/core/task-runner.js +719 -73
  32. package/src/core/validation.js +120 -1
  33. package/src/lib/utils.js +6 -0
  34. package/src/llm/README.md +139 -30
  35. package/src/llm/index.js +222 -72
  36. package/src/pages/PipelineDetail.jsx +111 -0
  37. package/src/pages/PromptPipelineDashboard.jsx +223 -0
  38. package/src/providers/deepseek.js +3 -15
  39. package/src/ui/client/adapters/job-adapter.js +258 -0
  40. package/src/ui/client/bootstrap.js +120 -0
  41. package/src/ui/client/hooks/useJobDetailWithUpdates.js +619 -0
  42. package/src/ui/client/hooks/useJobList.js +50 -0
  43. package/src/ui/client/hooks/useJobListWithUpdates.js +335 -0
  44. package/src/ui/client/hooks/useTicker.js +26 -0
  45. package/src/ui/client/index.css +31 -0
  46. package/src/ui/client/index.html +18 -0
  47. package/src/ui/client/main.jsx +38 -0
  48. package/src/ui/config-bridge.browser.js +149 -0
  49. package/src/ui/config-bridge.js +149 -0
  50. package/src/ui/config-bridge.node.js +310 -0
  51. package/src/ui/dist/assets/index-BDABnI-4.js +33399 -0
  52. package/src/ui/dist/assets/style-Ks8LY8gB.css +28496 -0
  53. package/src/ui/dist/index.html +19 -0
  54. package/src/ui/endpoints/job-endpoints.js +300 -0
  55. package/src/ui/file-reader.js +216 -0
  56. package/src/ui/job-change-detector.js +83 -0
  57. package/src/ui/job-index.js +231 -0
  58. package/src/ui/job-reader.js +274 -0
  59. package/src/ui/job-scanner.js +188 -0
  60. package/src/ui/public/app.js +3 -1
  61. package/src/ui/server.js +1636 -59
  62. package/src/ui/sse-enhancer.js +149 -0
  63. package/src/ui/sse.js +204 -0
  64. package/src/ui/state-snapshot.js +252 -0
  65. package/src/ui/transformers/list-transformer.js +347 -0
  66. package/src/ui/transformers/status-transformer.js +307 -0
  67. package/src/ui/watcher.js +61 -7
  68. package/src/utils/dag.js +101 -0
  69. package/src/utils/duration.js +126 -0
  70. package/src/utils/id-generator.js +30 -0
  71. package/src/utils/jobs.js +7 -0
  72. package/src/utils/pipelines.js +44 -0
  73. package/src/utils/task-files.js +271 -0
  74. package/src/utils/ui.jsx +76 -0
  75. package/src/ui/public/index.html +0 -53
  76. package/src/ui/public/style.css +0 -341
@@ -0,0 +1,239 @@
1
+ import React, { useState } from "react";
2
+ import { Box } from "@radix-ui/themes";
3
+ import { Button } from "./ui/button.jsx";
4
+
5
+ /**
6
+ * Normalize upload errors to a user-facing string
7
+ */
8
+ export const normalizeUploadError = (err) => {
9
+ if (!err) return "Upload failed";
10
+ if (typeof err === "string") return err;
11
+ if (err instanceof Error) return err.message;
12
+ if (typeof err === "object") {
13
+ if ("message" in err && err.message) return String(err.message);
14
+ if ("error" in err && err.error) return String(err.error);
15
+ }
16
+ return "Upload failed";
17
+ };
18
+
19
+ /**
20
+ * UploadSeed component for uploading seed files
21
+ *
22
+ * @param {Object} props
23
+ * @param {function} props.onUploadSuccess - Callback called on successful upload with { jobName }
24
+ */
25
+ export default function UploadSeed({ onUploadSuccess }) {
26
+ const fileInputRef = React.useRef(null);
27
+ const [showSample, setShowSample] = useState(false);
28
+ const [error, setError] = useState(null);
29
+
30
+ // Sample seed file structure for reference
31
+ const sampleSeed = {
32
+ name: "some-name",
33
+ pipeline: "content-generation",
34
+ data: {
35
+ type: "some-type",
36
+ contentType: "blog-post",
37
+ targetAudience: "software-developers",
38
+ tone: "professional-yet-accessible",
39
+ length: "1500-2000 words",
40
+ outputFormat: "blog-post",
41
+ },
42
+ };
43
+
44
+ const handleFileChange = async (event) => {
45
+ const files = event.target.files;
46
+ if (!files || files.length === 0) return;
47
+
48
+ const file = files[0];
49
+
50
+ try {
51
+ const formData = new FormData();
52
+ formData.append("file", file);
53
+
54
+ const response = await fetch("/api/upload/seed", {
55
+ method: "POST",
56
+ body: formData,
57
+ });
58
+
59
+ const result = await response.json();
60
+
61
+ if (result.success) {
62
+ // Emit console log as required
63
+ console.log("Seed uploaded:", result.jobName);
64
+
65
+ // Clear any prior error and call success callback
66
+ setError(null);
67
+ if (onUploadSuccess) {
68
+ onUploadSuccess({ jobName: result.jobName });
69
+ }
70
+ } else {
71
+ console.error("Upload failed:", result.message);
72
+ setError(normalizeUploadError(result));
73
+ }
74
+ } catch (error) {
75
+ console.error("Upload error:", error);
76
+ setError(normalizeUploadError(error));
77
+ } finally {
78
+ // Reset file input
79
+ if (fileInputRef.current) {
80
+ fileInputRef.current.value = "";
81
+ }
82
+ }
83
+ };
84
+
85
+ const handleDropAreaClick = () => {
86
+ if (fileInputRef.current) {
87
+ fileInputRef.current.click();
88
+ }
89
+ };
90
+
91
+ const handleDragOver = (event) => {
92
+ event.preventDefault();
93
+ event.currentTarget.classList.add("border-blue-500", "bg-blue-50");
94
+ };
95
+
96
+ const handleDragLeave = (event) => {
97
+ event.preventDefault();
98
+ event.currentTarget.classList.remove("border-blue-500", "bg-blue-50");
99
+ };
100
+
101
+ const handleDrop = (event) => {
102
+ event.preventDefault();
103
+ event.currentTarget.classList.remove("border-blue-500", "bg-blue-50");
104
+
105
+ if (event.dataTransfer.files.length > 0) {
106
+ const files = event.dataTransfer.files;
107
+ const fileInput = fileInputRef.current;
108
+ if (fileInput) {
109
+ const dataTransfer = new DataTransfer();
110
+ dataTransfer.items.add(files[0]);
111
+ fileInput.files = dataTransfer.files;
112
+ handleFileChange({ target: fileInput });
113
+ }
114
+ }
115
+ };
116
+
117
+ return (
118
+ <div data-testid="upload-seed" className="space-y-3">
119
+ {error && (
120
+ <Box
121
+ role="alert"
122
+ data-testid="upload-error"
123
+ className="rounded-md bg-red-50 p-3 border border-red-200"
124
+ >
125
+ <div className="flex items-start justify-between gap-3">
126
+ <div className="text-sm text-red-800">{error}</div>
127
+ <Button
128
+ size="1"
129
+ variant="ghost"
130
+ onClick={() => setError(null)}
131
+ data-testid="dismiss-error"
132
+ >
133
+ Dismiss
134
+ </Button>
135
+ </div>
136
+ </Box>
137
+ )}
138
+ <div
139
+ data-testid="upload-area"
140
+ className={`
141
+ border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors
142
+ border-gray-400 bg-white text-gray-600 hover:border-blue-500 hover:bg-blue-50
143
+ `}
144
+ onClick={handleDropAreaClick}
145
+ onDragOver={handleDragOver}
146
+ onDragLeave={handleDragLeave}
147
+ onDrop={handleDrop}
148
+ role="button"
149
+ tabIndex={0}
150
+ >
151
+ <div className="space-y-2">
152
+ <svg
153
+ className="mx-auto h-8 w-8 text-gray-400"
154
+ fill="none"
155
+ viewBox="0 0 24 24"
156
+ stroke="currentColor"
157
+ aria-hidden="true"
158
+ >
159
+ <path
160
+ strokeLinecap="round"
161
+ strokeLinejoin="round"
162
+ strokeWidth={2}
163
+ d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
164
+ />
165
+ </svg>
166
+ <div className="text-sm">
167
+ <span className="font-medium text-gray-900">Click to upload</span>{" "}
168
+ or drag and drop
169
+ </div>
170
+ <p className="text-xs text-gray-500">JSON files only</p>
171
+ </div>
172
+ </div>
173
+
174
+ <input
175
+ ref={fileInputRef}
176
+ type="file"
177
+ accept=".json"
178
+ className="hidden"
179
+ onChange={handleFileChange}
180
+ data-testid="file-input"
181
+ />
182
+
183
+ {/* Sample seed file section */}
184
+ <div className="border border-gray-200 rounded-lg overflow-hidden">
185
+ <button
186
+ type="button"
187
+ onClick={() => setShowSample(!showSample)}
188
+ className="w-full px-4 py-3 text-left bg-gray-50 hover:bg-gray-100 transition-colors flex items-center justify-between"
189
+ aria-expanded={showSample}
190
+ data-testid="sample-toggle"
191
+ >
192
+ <span className="text-sm font-medium text-gray-700">
193
+ Need help? View sample seed file structure
194
+ </span>
195
+ <svg
196
+ className={`w-4 h-4 text-gray-500 transition-transform ${
197
+ showSample ? "rotate-180" : ""
198
+ }`}
199
+ fill="none"
200
+ viewBox="0 0 24 24"
201
+ stroke="currentColor"
202
+ >
203
+ <path
204
+ strokeLinecap="round"
205
+ strokeLinejoin="round"
206
+ strokeWidth={2}
207
+ d="M19 9l-7 7-7-7"
208
+ />
209
+ </svg>
210
+ </button>
211
+
212
+ {showSample && (
213
+ <div className="p-4 bg-white border-t border-gray-200">
214
+ <div className="flex items-center justify-between mb-2">
215
+ <p className="text-xs text-gray-600">
216
+ Use this structure as a reference for your seed file:
217
+ </p>
218
+ <button
219
+ type="button"
220
+ onClick={() =>
221
+ navigator.clipboard.writeText(
222
+ JSON.stringify(sampleSeed, null, 2)
223
+ )
224
+ }
225
+ className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded hover:bg-blue-200 transition-colors"
226
+ data-testid="copy-sample"
227
+ >
228
+ Copy
229
+ </button>
230
+ </div>
231
+ <pre className="text-xs bg-gray-50 p-3 rounded overflow-auto max-h-60">
232
+ {JSON.stringify(sampleSeed, null, 2)}
233
+ </pre>
234
+ </div>
235
+ )}
236
+ </div>
237
+ </div>
238
+ );
239
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ export function Badge({ children, intent = "gray", className = "", ...props }) {
3
+ const intents = {
4
+ gray: "border-slate-300 text-slate-700 bg-slate-100",
5
+ blue: "border-blue-300 text-blue-800 bg-blue-100",
6
+ green: "border-green-300 text-green-800 bg-green-100",
7
+ red: "border-red-300 text-red-800 bg-red-100",
8
+ amber: "border-amber-300 text-amber-900 bg-amber-100",
9
+ };
10
+ const cls = [
11
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium",
12
+ intents[intent] || intents.gray,
13
+ className,
14
+ ].join(" ");
15
+ return (
16
+ <span className={cls} {...props}>
17
+ {children}
18
+ </span>
19
+ );
20
+ }
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+ import { Button as RadixButton } from "@radix-ui/themes";
3
+
4
+ export function Button({
5
+ variant = "solid",
6
+ size = "2",
7
+ className = "",
8
+ ...props
9
+ }) {
10
+ // Map custom variant names to Radix UI variants
11
+ const radixVariant =
12
+ variant === "solid"
13
+ ? "solid"
14
+ : variant === "outline"
15
+ ? "outline"
16
+ : variant === "ghost"
17
+ ? "ghost"
18
+ : variant === "secondary"
19
+ ? "soft"
20
+ : variant === "destructive"
21
+ ? "solid"
22
+ : "solid";
23
+
24
+ // Map custom size names to Radix UI sizes
25
+ const radixSize =
26
+ size === "sm" ? "1" : size === "md" ? "2" : size === "lg" ? "3" : "2";
27
+
28
+ // Map destructive variant to appropriate color
29
+ const color = variant === "destructive" ? "red" : undefined;
30
+
31
+ // Combine base classes with any additional className
32
+ const combinedClassName = `transition-colors duration-200 ${className}`;
33
+
34
+ return (
35
+ <RadixButton
36
+ variant={radixVariant}
37
+ size={radixSize}
38
+ color={color}
39
+ className={combinedClassName}
40
+ {...props}
41
+ />
42
+ );
43
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ export function Card({ className = "", ...p }) {
3
+ return (
4
+ <div
5
+ className={["rounded-xl border bg-white shadow-sm", className].join(" ")}
6
+ {...p}
7
+ />
8
+ );
9
+ }
10
+ export function CardHeader({ className = "", ...p }) {
11
+ return <div className={["p-4 border-b", className].join(" ")} {...p} />;
12
+ }
13
+ export function CardTitle({ className = "", ...p }) {
14
+ return (
15
+ <h3 className={["text-base font-semibold", className].join(" ")} {...p} />
16
+ );
17
+ }
18
+ export function CardContent({ className = "", ...p }) {
19
+ return <div className={["p-4", className].join(" ")} {...p} />;
20
+ }
@@ -0,0 +1,60 @@
1
+ /* Focus styles for accessibility */
2
+ .focus-visible:focus {
3
+ outline: 2px solid #2563eb;
4
+ outline-offset: 2px;
5
+ }
6
+
7
+ /* High contrast focus for better visibility */
8
+ .focus-visible:focus-visible {
9
+ outline: 2px solid #1d4ed8;
10
+ outline-offset: 2px;
11
+ }
12
+
13
+ /* Skip link styles */
14
+ .skip-link {
15
+ position: absolute;
16
+ top: -40px;
17
+ left: 6px;
18
+ background: #2563eb;
19
+ color: white;
20
+ padding: 8px;
21
+ text-decoration: none;
22
+ border-radius: 4px;
23
+ z-index: 1000;
24
+ transition: top 0.3s;
25
+ }
26
+
27
+ .skip-link:focus {
28
+ top: 6px;
29
+ }
30
+
31
+ /* Ensure good contrast for interactive elements */
32
+ .interactive-element {
33
+ min-height: 44px;
34
+ min-width: 44px;
35
+ }
36
+
37
+ /* Header navigation focus styles */
38
+ .nav-link:focus {
39
+ background-color: #f3f4f6;
40
+ border-radius: 4px;
41
+ outline: 2px solid #2563eb;
42
+ outline-offset: -2px;
43
+ }
44
+
45
+ /* Button focus styles */
46
+ button:focus-visible {
47
+ outline: 2px solid #2563eb;
48
+ outline-offset: 2px;
49
+ }
50
+
51
+ /* Link hover and focus states */
52
+ a:hover {
53
+ text-decoration: underline;
54
+ }
55
+
56
+ a:focus-visible {
57
+ outline: 2px solid #2563eb;
58
+ outline-offset: 2px;
59
+ border-radius: 2px;
60
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ export function Progress({ value = 0, variant = "default", className = "" }) {
3
+ const pct = Math.max(0, Math.min(100, Number(value)));
4
+
5
+ const variantClasses = {
6
+ default: "bg-blue-600",
7
+ running: "bg-blue-600",
8
+ error: "bg-red-600",
9
+ completed: "bg-green-600",
10
+ pending: "bg-slate-400",
11
+ };
12
+
13
+ return (
14
+ <div
15
+ className={[
16
+ "h-2 w-full overflow-hidden rounded bg-slate-200",
17
+ className,
18
+ ].join(" ")}
19
+ >
20
+ <div
21
+ className={`h-full transition-all duration-300 ${variantClasses[variant] || variantClasses.default}`}
22
+ style={{ width: `${pct}%` }}
23
+ />
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ export function Select({ value, onValueChange, children, className = "" }) {
3
+ return (
4
+ <select
5
+ className={[
6
+ "h-9 rounded-md border px-3 text-sm bg-white",
7
+ className,
8
+ ].join(" ")}
9
+ value={value}
10
+ onChange={(e) => onValueChange?.(e.target.value)}
11
+ >
12
+ {children}
13
+ </select>
14
+ );
15
+ }
16
+ export function SelectItem({ value, children }) {
17
+ return <option value={value}>{children}</option>;
18
+ }
19
+ export function SelectTrigger({ children, ...p }) {
20
+ return <>{children}</>;
21
+ } // keep API compatible
22
+ export function SelectContent({ children }) {
23
+ return <>{children}</>;
24
+ }
25
+ export function SelectValue({ placeholder }) {
26
+ return <>{placeholder}</>;
27
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ export function Separator({ className = "", ...p }) {
3
+ return (
4
+ <hr className={["my-4 border-slate-200", className].join(" ")} {...p} />
5
+ );
6
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Path resolution utilities for pipeline data directories
3
+ * @module config/paths
4
+ */
5
+
6
+ import path from "path";
7
+
8
+ /**
9
+ * Resolve pipeline data directory paths from a base directory
10
+ * @param {string} baseDir - Base data directory
11
+ * @returns {Object} Object containing resolved paths
12
+ */
13
+ function resolvePipelinePaths(baseDir) {
14
+ return {
15
+ pending: path.join(baseDir, "pipeline-data", "pending"),
16
+ current: path.join(baseDir, "pipeline-data", "current"),
17
+ complete: path.join(baseDir, "pipeline-data", "complete"),
18
+ rejected: path.join(baseDir, "pipeline-data", "rejected"),
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Get the exact pending filename for a given job ID
24
+ * @param {string} baseDir - Base data directory
25
+ * @param {string} jobId - Job ID
26
+ * @returns {string} Full path to pending seed file
27
+ */
28
+ function getPendingSeedPath(baseDir, jobId) {
29
+ const paths = resolvePipelinePaths(baseDir);
30
+ return path.join(paths.pending, `${jobId}-seed.json`);
31
+ }
32
+
33
+ /**
34
+ * Get the current seed file path for a given job ID
35
+ * @param {string} baseDir - Base data directory
36
+ * @param {string} jobId - Job ID
37
+ * @returns {string} Full path to current seed file
38
+ */
39
+ function getCurrentSeedPath(baseDir, jobId) {
40
+ const paths = resolvePipelinePaths(baseDir);
41
+ return path.join(paths.current, jobId, "seed.json");
42
+ }
43
+
44
+ /**
45
+ * Get the complete seed file path for a given job ID
46
+ * @param {string} baseDir - Base data directory
47
+ * @param {string} jobId - Job ID
48
+ * @returns {string} Full path to complete seed file
49
+ */
50
+ function getCompleteSeedPath(baseDir, jobId) {
51
+ const paths = resolvePipelinePaths(baseDir);
52
+ return path.join(paths.complete, jobId, "seed.json");
53
+ }
54
+
55
+ /**
56
+ * Get the job directory path for a given job ID and location
57
+ * @param {string} baseDir - Base data directory
58
+ * @param {string} jobId - Job ID
59
+ * @param {string} location - Job location ('current', 'complete', 'pending')
60
+ * @returns {string} Full path to job directory
61
+ */
62
+ function getJobDirectoryPath(baseDir, jobId, location) {
63
+ const paths = resolvePipelinePaths(baseDir);
64
+ return path.join(paths[location], jobId);
65
+ }
66
+
67
+ /**
68
+ * Get the job metadata file path for a given job ID
69
+ * @param {string} baseDir - Base data directory
70
+ * @param {string} jobId - Job ID
71
+ * @param {string} location - Job location ('current', 'complete')
72
+ * @returns {string} Full path to job metadata file
73
+ */
74
+ function getJobMetadataPath(baseDir, jobId, location = "current") {
75
+ const jobDir = getJobDirectoryPath(baseDir, jobId, location);
76
+ return path.join(jobDir, "job.json");
77
+ }
78
+
79
+ /**
80
+ * Get the pipeline snapshot file path for a given job ID
81
+ * @param {string} baseDir - Base data directory
82
+ * @param {string} jobId - Job ID
83
+ * @param {string} location - Job location ('current', 'complete')
84
+ * @returns {string} Full path to pipeline snapshot file
85
+ */
86
+ function getJobPipelinePath(baseDir, jobId, location = "current") {
87
+ const jobDir = getJobDirectoryPath(baseDir, jobId, location);
88
+ return path.join(jobDir, "pipeline.json");
89
+ }
90
+
91
+ export {
92
+ resolvePipelinePaths,
93
+ getPendingSeedPath,
94
+ getCurrentSeedPath,
95
+ getCompleteSeedPath,
96
+ getJobDirectoryPath,
97
+ getJobMetadataPath,
98
+ getJobPipelinePath,
99
+ };