@ryanfw/prompt-orchestration-pipeline 0.5.0 → 0.7.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 (67) hide show
  1. package/README.md +1 -2
  2. package/package.json +1 -2
  3. package/src/api/validators/json.js +39 -0
  4. package/src/components/DAGGrid.jsx +392 -303
  5. package/src/components/JobCard.jsx +14 -12
  6. package/src/components/JobDetail.jsx +54 -51
  7. package/src/components/JobTable.jsx +72 -23
  8. package/src/components/Layout.jsx +145 -42
  9. package/src/components/LiveText.jsx +47 -0
  10. package/src/components/PageSubheader.jsx +75 -0
  11. package/src/components/TaskDetailSidebar.jsx +216 -0
  12. package/src/components/TimerText.jsx +82 -0
  13. package/src/components/UploadSeed.jsx +0 -70
  14. package/src/components/ui/Logo.jsx +16 -0
  15. package/src/components/ui/RestartJobModal.jsx +140 -0
  16. package/src/components/ui/toast.jsx +138 -0
  17. package/src/config/models.js +322 -0
  18. package/src/config/statuses.js +119 -0
  19. package/src/core/config.js +4 -34
  20. package/src/core/file-io.js +13 -28
  21. package/src/core/module-loader.js +54 -40
  22. package/src/core/pipeline-runner.js +65 -26
  23. package/src/core/status-writer.js +213 -58
  24. package/src/core/symlink-bridge.js +57 -0
  25. package/src/core/symlink-utils.js +94 -0
  26. package/src/core/task-runner.js +321 -437
  27. package/src/llm/index.js +258 -86
  28. package/src/pages/Code.jsx +351 -0
  29. package/src/pages/PipelineDetail.jsx +124 -15
  30. package/src/pages/PromptPipelineDashboard.jsx +20 -88
  31. package/src/providers/anthropic.js +83 -69
  32. package/src/providers/base.js +52 -0
  33. package/src/providers/deepseek.js +20 -21
  34. package/src/providers/gemini.js +226 -0
  35. package/src/providers/openai.js +36 -106
  36. package/src/providers/zhipu.js +136 -0
  37. package/src/ui/client/adapters/job-adapter.js +42 -28
  38. package/src/ui/client/api.js +134 -0
  39. package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -179
  40. package/src/ui/client/index.css +15 -0
  41. package/src/ui/client/index.html +2 -1
  42. package/src/ui/client/main.jsx +19 -14
  43. package/src/ui/client/time-store.js +161 -0
  44. package/src/ui/config-bridge.js +15 -24
  45. package/src/ui/config-bridge.node.js +15 -24
  46. package/src/ui/dist/assets/{index-CxcrauYR.js → index-DqkbzXZ1.js} +2132 -1086
  47. package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
  48. package/src/ui/dist/index.html +4 -3
  49. package/src/ui/job-reader.js +0 -108
  50. package/src/ui/public/favicon.svg +12 -0
  51. package/src/ui/server.js +252 -0
  52. package/src/ui/sse-enhancer.js +0 -1
  53. package/src/ui/transformers/list-transformer.js +32 -12
  54. package/src/ui/transformers/status-transformer.js +29 -42
  55. package/src/utils/dag.js +8 -4
  56. package/src/utils/duration.js +13 -19
  57. package/src/utils/formatters.js +27 -0
  58. package/src/utils/geometry-equality.js +83 -0
  59. package/src/utils/pipelines.js +5 -1
  60. package/src/utils/time-utils.js +40 -0
  61. package/src/utils/token-cost-calculator.js +294 -0
  62. package/src/utils/ui.jsx +18 -20
  63. package/src/components/ui/select.jsx +0 -27
  64. package/src/lib/utils.js +0 -6
  65. package/src/ui/client/hooks/useTicker.js +0 -26
  66. package/src/ui/config-bridge.browser.js +0 -149
  67. package/src/ui/dist/assets/style-D6K_oQ12.css +0 -62
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Box, Flex, Text } from "@radix-ui/themes";
4
+ import { ChevronRight } from "lucide-react";
5
+
6
+ /**
7
+ * PageSubheader renders a secondary header with breadcrumbs and optional right-side content.
8
+ * Intended to be placed directly below main navigation header.
9
+ */
10
+ export default function PageSubheader({
11
+ breadcrumbs = [],
12
+ children,
13
+ maxWidth = "max-w-7xl",
14
+ }) {
15
+ return (
16
+ <Box
17
+ role="region"
18
+ aria-label="Page header"
19
+ className="border-b border-gray-300 bg-gray-1/60 backdrop-blur supports-[backdrop-filter]:bg-gray-1/40 mb-4"
20
+ >
21
+ <Flex
22
+ align="center"
23
+ justify="between"
24
+ className={`mx-auto w-full ${maxWidth} px-1.5 py-3`}
25
+ gap="4"
26
+ wrap="wrap"
27
+ >
28
+ <Flex align="center" gap="3" className="min-w-0 flex-1">
29
+ {breadcrumbs.length > 0 && (
30
+ <nav aria-label="Breadcrumb" className="shrink-0">
31
+ <ol className="flex items-center gap-2 text-sm text-gray-11">
32
+ {breadcrumbs.map((crumb, index) => {
33
+ const isLast = index === breadcrumbs.length - 1;
34
+ return (
35
+ <React.Fragment key={index}>
36
+ {index > 0 && (
37
+ <ChevronRight
38
+ className="h-4 w-4 text-gray-9"
39
+ aria-hidden="true"
40
+ />
41
+ )}
42
+ {crumb.href ? (
43
+ <Link
44
+ to={crumb.href}
45
+ className="hover:text-gray-12 transition-colors underline-offset-4 hover:underline"
46
+ >
47
+ {crumb.label}
48
+ </Link>
49
+ ) : (
50
+ <Text
51
+ as="span"
52
+ aria-current={isLast ? "page" : undefined}
53
+ className={isLast ? "text-gray-12 font-medium" : ""}
54
+ >
55
+ {crumb.label}
56
+ </Text>
57
+ )}
58
+ </React.Fragment>
59
+ );
60
+ })}
61
+ </ol>
62
+ </nav>
63
+ )}
64
+ </Flex>
65
+
66
+ {/* Right side content */}
67
+ {children && (
68
+ <Flex align="center" gap="3" className="shrink-0">
69
+ {children}
70
+ </Flex>
71
+ )}
72
+ </Flex>
73
+ </Box>
74
+ );
75
+ }
@@ -0,0 +1,216 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { Callout } from "@radix-ui/themes";
3
+ import { TaskFilePane } from "./TaskFilePane.jsx";
4
+ import { TaskState } from "../config/statuses.js";
5
+
6
+ /**
7
+ * TaskDetailSidebar component for displaying task details in a slide-over panel
8
+ * @param {Object} props - Component props
9
+ * @param {boolean} props.open - Whether the sidebar is open
10
+ * @param {string} props.title - Preformatted step name for the header
11
+ * @param {string} props.status - TaskState for styling
12
+ * @param {string} props.jobId - Job ID for file operations
13
+ * @param {string} props.taskId - Task ID for file operations
14
+ * @param {string|null} props.taskBody - Task body for error callout when status is FAILED
15
+ * @param {Function} props.filesByTypeForItem - Selector returning { artifacts, logs, tmp }
16
+ * @param {Object} props.task - Original task item, passed for filesByTypeForItem
17
+ * @param {Function} props.onClose - Close handler
18
+ */
19
+ export function TaskDetailSidebar({
20
+ open,
21
+ title,
22
+ status,
23
+ jobId,
24
+ taskId,
25
+ taskBody,
26
+ filesByTypeForItem = () => ({ artifacts: [], logs: [], tmp: [] }),
27
+ task,
28
+ onClose,
29
+ taskIndex, // Add taskIndex for ID compatibility
30
+ }) {
31
+ // Internal state
32
+ const [filePaneType, setFilePaneType] = useState("artifacts");
33
+ const [filePaneOpen, setFilePaneOpen] = useState(false);
34
+ const [filePaneFilename, setFilePaneFilename] = useState(null);
35
+ const closeButtonRef = useRef(null);
36
+
37
+ // Get CSS classes for card header based on status (mirrored from DAGGrid)
38
+ const getHeaderClasses = (status) => {
39
+ switch (status) {
40
+ case TaskState.DONE:
41
+ return "bg-green-50 border-green-200 text-green-700";
42
+ case TaskState.RUNNING:
43
+ return "bg-amber-50 border-amber-200 text-amber-700";
44
+ case TaskState.FAILED:
45
+ return "bg-pink-50 border-pink-200 text-pink-700";
46
+ default:
47
+ return "bg-gray-100 border-gray-200 text-gray-700";
48
+ }
49
+ };
50
+
51
+ // Focus close button when sidebar opens
52
+ useEffect(() => {
53
+ if (open && closeButtonRef.current) {
54
+ closeButtonRef.current.focus();
55
+ }
56
+ }, [open]);
57
+
58
+ // Reset internal state when open changes
59
+ useEffect(() => {
60
+ if (open) {
61
+ setFilePaneType("artifacts");
62
+ setFilePaneOpen(false);
63
+ setFilePaneFilename(null);
64
+ }
65
+ }, [open]);
66
+
67
+ // Reset file pane when type changes
68
+ useEffect(() => {
69
+ setFilePaneFilename(null);
70
+ setFilePaneOpen(false);
71
+ }, [filePaneType]);
72
+
73
+ // Handle file click
74
+ const handleFileClick = (filename) => {
75
+ setFilePaneFilename(filename);
76
+ setFilePaneOpen(true);
77
+ };
78
+
79
+ // Handle TaskFilePane close
80
+ const handleFilePaneClose = () => {
81
+ setFilePaneOpen(false);
82
+ setFilePaneFilename(null);
83
+ };
84
+
85
+ if (!open) {
86
+ return null;
87
+ }
88
+
89
+ // Get files for the current task
90
+ const filesForStep = filesByTypeForItem(task);
91
+ const filesForTab = filesForStep[filePaneType] ?? [];
92
+
93
+ return (
94
+ <aside
95
+ role="dialog"
96
+ aria-modal="true"
97
+ aria-labelledby={`slide-over-title-${taskIndex}`}
98
+ aria-hidden={false}
99
+ className={`fixed inset-y-0 right-0 z-[2000] w-full max-w-4xl bg-white border-l border-gray-200 transform transition-transform duration-300 ease-out translate-x-0`}
100
+ >
101
+ {/* Header */}
102
+ <div
103
+ className={`px-6 py-4 border-b flex items-center justify-between ${getHeaderClasses(status)}`}
104
+ >
105
+ <div
106
+ id={`slide-over-title-${taskIndex}`}
107
+ className="text-lg font-semibold truncate"
108
+ >
109
+ {title}
110
+ </div>
111
+ <button
112
+ ref={closeButtonRef}
113
+ type="button"
114
+ aria-label="Close details"
115
+ onClick={onClose}
116
+ className="rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 text-base"
117
+ >
118
+ ×
119
+ </button>
120
+ </div>
121
+
122
+ <div className="p-6 space-y-8 overflow-y-auto h-full">
123
+ {/* Error Callout - shown when task has error status and body */}
124
+ {status === TaskState.FAILED && taskBody && (
125
+ <section aria-label="Error">
126
+ <Callout.Root role="alert" aria-live="assertive">
127
+ <Callout.Text className="whitespace-pre-wrap break-words">
128
+ {taskBody}
129
+ </Callout.Text>
130
+ </Callout.Root>
131
+ </section>
132
+ )}
133
+
134
+ {/* File Display Area with Type Tabs */}
135
+ <section className="mt-6">
136
+ <div className="flex items-center justify-between mb-4">
137
+ <h3 className="text-base font-semibold text-gray-900">Files</h3>
138
+ <div className="flex items-center space-x-2">
139
+ <div className="flex rounded-lg border border-gray-200 bg-gray-50 p-1">
140
+ <button
141
+ onClick={() => setFilePaneType("artifacts")}
142
+ className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
143
+ filePaneType === "artifacts"
144
+ ? "bg-white text-gray-900 shadow-sm"
145
+ : "text-gray-600 hover:text-gray-900"
146
+ }`}
147
+ >
148
+ Artifacts
149
+ </button>
150
+ <button
151
+ onClick={() => setFilePaneType("logs")}
152
+ className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
153
+ filePaneType === "logs"
154
+ ? "bg-white text-gray-900 shadow-sm"
155
+ : "text-gray-600 hover:text-gray-900"
156
+ }`}
157
+ >
158
+ Logs
159
+ </button>
160
+ <button
161
+ onClick={() => setFilePaneType("tmp")}
162
+ className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
163
+ filePaneType === "tmp"
164
+ ? "bg-white text-gray-900 shadow-sm"
165
+ : "text-gray-600 hover:text-gray-900"
166
+ }`}
167
+ >
168
+ Temp
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </section>
174
+
175
+ {/* File List */}
176
+ <div className="space-y-2">
177
+ <div className="text-sm text-gray-600">
178
+ {filePaneType.charAt(0).toUpperCase() + filePaneType.slice(1)} files
179
+ for {taskId}
180
+ </div>
181
+ <div className="space-y-1">
182
+ {filesForTab.length === 0 ? (
183
+ <div className="text-sm text-gray-500 italic py-4 text-center">
184
+ No {filePaneType} files available for this task
185
+ </div>
186
+ ) : (
187
+ filesForTab.map((name) => (
188
+ <div
189
+ key={`${filePaneType}-${name}`}
190
+ className="flex items-center justify-between p-2 rounded border border-gray-200 hover:border-gray-300 hover:bg-gray-50 cursor-pointer transition-colors"
191
+ onClick={() => handleFileClick(name)}
192
+ >
193
+ <div className="flex items-center space-x-2">
194
+ <span className="text-sm text-gray-700">{name}</span>
195
+ </div>
196
+ </div>
197
+ ))
198
+ )}
199
+ </div>
200
+ </div>
201
+
202
+ {/* TaskFilePane Modal */}
203
+ <TaskFilePane
204
+ isOpen={filePaneOpen}
205
+ jobId={jobId}
206
+ taskId={taskId}
207
+ type={filePaneType}
208
+ filename={filePaneFilename}
209
+ onClose={handleFilePaneClose}
210
+ />
211
+ </div>
212
+ </aside>
213
+ );
214
+ }
215
+
216
+ export default React.memo(TaskDetailSidebar);
@@ -0,0 +1,82 @@
1
+ import React, { useSyncExternalStore, useId, useState, useEffect } from "react";
2
+ import {
3
+ subscribe,
4
+ getSnapshot,
5
+ getServerSnapshot,
6
+ addCadenceHint,
7
+ removeCadenceHint,
8
+ } from "../ui/client/time-store.js";
9
+ import { fmtDuration } from "../utils/duration.js";
10
+
11
+ /**
12
+ * TimerText component for displaying live-updating durations without parent re-renders
13
+ *
14
+ * @param {Object} props
15
+ * @param {number} props.startMs - Start timestamp in milliseconds
16
+ * @param {number|null} props.endMs - End timestamp in milliseconds (null for ongoing timers)
17
+ * @param {"second"|"minute"} props.granularity - Update granularity
18
+ * @param {Function} props.format - Duration formatting function (defaults to fmtDuration)
19
+ * @param {string} props.className - CSS className for styling
20
+ */
21
+ export default function TimerText({
22
+ startMs,
23
+ endMs = null,
24
+ granularity = "second",
25
+ format = fmtDuration,
26
+ className,
27
+ }) {
28
+ const id = useId();
29
+
30
+ // Get current time from the global time store
31
+ const now = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
32
+
33
+ // Local state for the formatted text to avoid re-renders of parent
34
+ const [displayText, setDisplayText] = useState(() => {
35
+ // Initial text for SSR safety
36
+ if (!startMs) return "—";
37
+ const initialEnd = endMs ?? Date.now();
38
+ const elapsed = Math.max(0, initialEnd - startMs);
39
+ return format(elapsed);
40
+ });
41
+
42
+ // Register cadence hint and handle subscription
43
+ useEffect(() => {
44
+ if (!startMs) return;
45
+
46
+ // Register cadence hint based on granularity
47
+ const cadenceMs = granularity === "second" ? 1000 : 60_000;
48
+ addCadenceHint(id, cadenceMs);
49
+
50
+ // Cleanup function
51
+ return () => {
52
+ removeCadenceHint(id);
53
+ };
54
+ }, [id, granularity, startMs]);
55
+
56
+ // Update display text when time changes (only for ongoing timers)
57
+ useEffect(() => {
58
+ if (!startMs) return;
59
+
60
+ // If endMs is present, this is a completed timer - no further updates needed
61
+ if (endMs !== null) return;
62
+
63
+ // For ongoing timers, update the display text
64
+ const elapsed = Math.max(0, now - startMs);
65
+ setDisplayText(format(elapsed));
66
+ }, [now, startMs, endMs, format]);
67
+
68
+ // For completed timers, ensure the final duration is displayed
69
+ useEffect(() => {
70
+ if (!startMs || endMs === null) return;
71
+
72
+ const elapsed = Math.max(0, endMs - startMs);
73
+ setDisplayText(format(elapsed));
74
+ }, [startMs, endMs, format]);
75
+
76
+ // If no start time, show placeholder
77
+ if (!startMs) {
78
+ return <span className={className}>—</span>;
79
+ }
80
+
81
+ return <span className={className}>{displayText}</span>;
82
+ }
@@ -24,23 +24,8 @@ export const normalizeUploadError = (err) => {
24
24
  */
25
25
  export default function UploadSeed({ onUploadSuccess }) {
26
26
  const fileInputRef = React.useRef(null);
27
- const [showSample, setShowSample] = useState(false);
28
27
  const [error, setError] = useState(null);
29
28
 
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
29
  const handleFileChange = async (event) => {
45
30
  const files = event.target.files;
46
31
  if (!files || files.length === 0) return;
@@ -179,61 +164,6 @@ export default function UploadSeed({ onUploadSuccess }) {
179
164
  onChange={handleFileChange}
180
165
  data-testid="file-input"
181
166
  />
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
167
  </div>
238
168
  );
239
169
  }
@@ -0,0 +1,16 @@
1
+ export const Logo = () => (
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ width="100%"
5
+ height="100%"
6
+ viewBox="0 0 1200 1200"
7
+ >
8
+ <path
9
+ fill="#009966"
10
+ d="M406.13 988.31c-17.297 75.047-84.562 131.11-164.86 131.11-93.375 0-169.18-75.797-169.18-169.18s75.797-169.18 169.18-169.18 169.18 75.797 169.18 169.18v1.266h447.74v-167.9H671.63c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.703-16.406-39.562v-37.312h-317.16c-10.312 0-18.656-8.344-18.656-18.656V355.78h-147.94c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.75-16.406-39.562v-111.94c0-14.859 5.906-29.109 16.406-39.562 10.5-10.5 24.75-16.406 39.562-16.406h391.78c14.859 0 29.062 5.906 39.562 16.406s16.406 24.75 16.406 39.562v37.312h202.4c9.281-84.609 81.094-150.52 168.14-150.52 93.375 0 169.18 75.797 169.18 169.18s-75.797 169.18-169.18 169.18c-87.047 0-158.86-65.906-168.14-150.52h-202.4v37.312c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-206.53v297.24h298.5v-37.312c0-14.859 5.906-29.062 16.406-39.562s24.703-16.406 39.562-16.406h392.63c14.859 0 29.062 5.906 39.562 16.406s16.406 24.703 16.406 39.562v111.94c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-168.74v186.56c0 10.312-8.344 18.656-18.656 18.656h-466.4c-1.5 0-2.906-.187-4.312-.516zM225.19 262.45h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H225.19c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H411.75c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm616.18 0h85.5c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-85.5l29.062-22.594c8.109-6.328 9.609-18.047 3.281-26.156s-18.047-9.609-26.156-3.281l-71.953 55.969a18.61 18.61 0 0 0 0 29.438l71.953 55.969c8.109 6.328 19.875 4.875 26.156-3.281 6.328-8.109 4.875-19.875-3.281-26.203l-29.062-22.594zm-779.95 696.66l50.391 50.391c7.266 7.313 19.078 7.313 26.391 0l100.73-100.73c7.266-7.266 7.266-19.078 0-26.391-7.266-7.266-19.078-7.266-26.391 0l-87.562 87.562-37.172-37.172c-7.266-7.266-19.078-7.266-26.391 0-7.266 7.266-7.266 19.078 0 26.391zm797.21-268.78h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H858.63c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656z"
11
+ fillRule="evenodd"
12
+ />
13
+ </svg>
14
+ );
15
+
16
+ export default Logo;
@@ -0,0 +1,140 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Box, Flex, Text, Heading } from "@radix-ui/themes";
3
+ import { Button } from "./button.jsx";
4
+
5
+ /**
6
+ * RestartJobModal component for confirming job restart from clean slate
7
+ * @param {Object} props
8
+ * @param {boolean} props.open - Whether the modal is open
9
+ * @param {Function} props.onClose - Function to call when modal is closed
10
+ * @param {Function} props.onConfirm - Function to call when restart is confirmed
11
+ * @param {string} props.jobId - The ID of the job to restart
12
+ * @param {string} props.taskId - The ID of the task that triggered the restart (optional)
13
+ * @param {boolean} props.isSubmitting - Whether the restart action is in progress
14
+ */
15
+ export function RestartJobModal({
16
+ open,
17
+ onClose,
18
+ onConfirm,
19
+ jobId,
20
+ taskId,
21
+ isSubmitting = false,
22
+ }) {
23
+ const modalRef = useRef(null);
24
+
25
+ // Handle Escape key to close modal
26
+ useEffect(() => {
27
+ const handleKeyDown = (e) => {
28
+ if (e.key === "Escape" && open) {
29
+ e.preventDefault();
30
+ onClose();
31
+ }
32
+ };
33
+
34
+ if (open) {
35
+ document.addEventListener("keydown", handleKeyDown);
36
+ // Focus the modal for accessibility
37
+ if (modalRef.current) {
38
+ modalRef.current.focus();
39
+ }
40
+ return () => {
41
+ document.removeEventListener("keydown", handleKeyDown);
42
+ };
43
+ }
44
+ }, [open, onClose]);
45
+
46
+ // Handle Enter key to confirm when modal is focused
47
+ const handleKeyDown = (e) => {
48
+ if (e.key === "Enter" && !isSubmitting && open) {
49
+ e.preventDefault();
50
+ onConfirm();
51
+ }
52
+ };
53
+
54
+ if (!open) return null;
55
+
56
+ return (
57
+ <>
58
+ <div
59
+ className="fixed inset-0 z-50 flex items-center justify-center"
60
+ aria-hidden={!open}
61
+ >
62
+ {/* Backdrop */}
63
+ <div
64
+ className="absolute inset-0 bg-black/50"
65
+ onClick={onClose}
66
+ aria-hidden="true"
67
+ />
68
+
69
+ {/* Modal */}
70
+ <div
71
+ ref={modalRef}
72
+ role="dialog"
73
+ aria-modal="true"
74
+ aria-labelledby="restart-modal-title"
75
+ aria-describedby="restart-modal-description"
76
+ className="relative bg-white rounded-lg shadow-2xl border border-gray-200 max-w-lg w-full mx-4 outline-none"
77
+ style={{ minWidth: "320px", maxWidth: "560px" }}
78
+ tabIndex={-1}
79
+ onKeyDown={handleKeyDown}
80
+ >
81
+ <div className="p-6">
82
+ {/* Header */}
83
+ <Heading
84
+ id="restart-modal-title"
85
+ as="h2"
86
+ size="5"
87
+ className="mb-4 text-gray-900"
88
+ >
89
+ {taskId
90
+ ? `Restart from ${taskId}`
91
+ : "Restart job (reset progress)"}
92
+ </Heading>
93
+
94
+ {/* Body */}
95
+ <Box id="restart-modal-description" className="mb-6">
96
+ <Text as="p" className="text-gray-700 mb-4">
97
+ {taskId
98
+ ? `This will restart the job from the "${taskId}" task. Tasks before ${taskId} will remain completed, while ${taskId} and all subsequent tasks will be reset to pending. Files and artifacts are preserved. A new background run will start automatically. This cannot be undone.`
99
+ : "This will clear the job's progress and active stage and reset all tasks to pending. Files and artifacts are preserved. A new background run will start automatically. This cannot be undone."}
100
+ </Text>
101
+
102
+ {taskId && (
103
+ <Text as="p" className="text-sm text-gray-600 mb-3">
104
+ <strong>Triggered from task:</strong> {taskId}
105
+ </Text>
106
+ )}
107
+
108
+ <Text as="p" className="text-sm text-gray-500 italic">
109
+ Note: Job must be in current lifecycle and not running.
110
+ </Text>
111
+ </Box>
112
+
113
+ {/* Actions */}
114
+ <Flex gap="3" justify="end">
115
+ <Button
116
+ variant="outline"
117
+ onClick={onClose}
118
+ disabled={isSubmitting}
119
+ className="min-w-[80px]"
120
+ >
121
+ Cancel
122
+ </Button>
123
+
124
+ <Button
125
+ variant="destructive"
126
+ onClick={onConfirm}
127
+ disabled={isSubmitting}
128
+ className="min-w-[80px]"
129
+ >
130
+ {isSubmitting ? "Restarting..." : "Restart"}
131
+ </Button>
132
+ </Flex>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </>
137
+ );
138
+ }
139
+
140
+ export default RestartJobModal;