@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.
- package/README.md +1 -2
- package/package.json +1 -2
- package/src/api/validators/json.js +39 -0
- package/src/components/DAGGrid.jsx +392 -303
- package/src/components/JobCard.jsx +14 -12
- package/src/components/JobDetail.jsx +54 -51
- package/src/components/JobTable.jsx +72 -23
- package/src/components/Layout.jsx +145 -42
- package/src/components/LiveText.jsx +47 -0
- package/src/components/PageSubheader.jsx +75 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -0
- package/src/components/UploadSeed.jsx +0 -70
- package/src/components/ui/Logo.jsx +16 -0
- package/src/components/ui/RestartJobModal.jsx +140 -0
- package/src/components/ui/toast.jsx +138 -0
- package/src/config/models.js +322 -0
- package/src/config/statuses.js +119 -0
- package/src/core/config.js +4 -34
- package/src/core/file-io.js +13 -28
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +65 -26
- package/src/core/status-writer.js +213 -58
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +321 -437
- package/src/llm/index.js +258 -86
- package/src/pages/Code.jsx +351 -0
- package/src/pages/PipelineDetail.jsx +124 -15
- package/src/pages/PromptPipelineDashboard.jsx +20 -88
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +20 -21
- package/src/providers/gemini.js +226 -0
- package/src/providers/openai.js +36 -106
- package/src/providers/zhipu.js +136 -0
- package/src/ui/client/adapters/job-adapter.js +42 -28
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -179
- package/src/ui/client/index.css +15 -0
- package/src/ui/client/index.html +2 -1
- package/src/ui/client/main.jsx +19 -14
- package/src/ui/client/time-store.js +161 -0
- package/src/ui/config-bridge.js +15 -24
- package/src/ui/config-bridge.node.js +15 -24
- package/src/ui/dist/assets/{index-CxcrauYR.js → index-DqkbzXZ1.js} +2132 -1086
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +4 -3
- package/src/ui/job-reader.js +0 -108
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +252 -0
- package/src/ui/sse-enhancer.js +0 -1
- package/src/ui/transformers/list-transformer.js +32 -12
- package/src/ui/transformers/status-transformer.js +29 -42
- package/src/utils/dag.js +8 -4
- package/src/utils/duration.js +13 -19
- package/src/utils/formatters.js +27 -0
- package/src/utils/geometry-equality.js +83 -0
- package/src/utils/pipelines.js +5 -1
- package/src/utils/time-utils.js +40 -0
- package/src/utils/token-cost-calculator.js +294 -0
- package/src/utils/ui.jsx +18 -20
- package/src/components/ui/select.jsx +0 -27
- package/src/lib/utils.js +0 -6
- package/src/ui/client/hooks/useTicker.js +0 -26
- package/src/ui/config-bridge.browser.js +0 -149
- 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;
|