@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
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { Callout } from "@radix-ui/themes";
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Callout, Table, Text, Box } from "@radix-ui/themes";
|
|
3
3
|
import { TaskFilePane } from "./TaskFilePane.jsx";
|
|
4
4
|
import { TaskState } from "../config/statuses.js";
|
|
5
|
+
import { Sidebar, SidebarSection } from "./ui/sidebar.jsx";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* TaskDetailSidebar component for displaying task details in a slide-over panel
|
|
@@ -12,9 +13,11 @@ import { TaskState } from "../config/statuses.js";
|
|
|
12
13
|
* @param {string} props.jobId - Job ID for file operations
|
|
13
14
|
* @param {string} props.taskId - Task ID for file operations
|
|
14
15
|
* @param {string|null} props.taskBody - Task body for error callout when status is FAILED
|
|
16
|
+
* @param {Object} props.taskError - Error object with message and stack
|
|
15
17
|
* @param {Function} props.filesByTypeForItem - Selector returning { artifacts, logs, tmp }
|
|
16
18
|
* @param {Object} props.task - Original task item, passed for filesByTypeForItem
|
|
17
19
|
* @param {Function} props.onClose - Close handler
|
|
20
|
+
* @param {number} props.taskIndex - Task index for ID compatibility
|
|
18
21
|
*/
|
|
19
22
|
export function TaskDetailSidebar({
|
|
20
23
|
open,
|
|
@@ -27,36 +30,28 @@ export function TaskDetailSidebar({
|
|
|
27
30
|
filesByTypeForItem = () => ({ artifacts: [], logs: [], tmp: [] }),
|
|
28
31
|
task,
|
|
29
32
|
onClose,
|
|
30
|
-
taskIndex, //
|
|
33
|
+
taskIndex: _taskIndex, // eslint-disable-line no-unused-vars
|
|
31
34
|
}) {
|
|
32
35
|
// Internal state
|
|
33
36
|
const [filePaneType, setFilePaneType] = useState("artifacts");
|
|
34
37
|
const [filePaneOpen, setFilePaneOpen] = useState(false);
|
|
35
38
|
const [filePaneFilename, setFilePaneFilename] = useState(null);
|
|
36
39
|
const [showStack, setShowStack] = useState(false);
|
|
37
|
-
const closeButtonRef = useRef(null);
|
|
38
40
|
|
|
39
41
|
// Get CSS classes for card header based on status (mirrored from DAGGrid)
|
|
40
42
|
const getHeaderClasses = (status) => {
|
|
41
43
|
switch (status) {
|
|
42
44
|
case TaskState.DONE:
|
|
43
|
-
return "bg-
|
|
45
|
+
return "bg-success/10 border-success/30 text-success-foreground";
|
|
44
46
|
case TaskState.RUNNING:
|
|
45
|
-
return "bg-
|
|
47
|
+
return "bg-warning/10 border-warning/30 text-warning-foreground";
|
|
46
48
|
case TaskState.FAILED:
|
|
47
|
-
return "bg-
|
|
49
|
+
return "bg-error/10 border-error/30 text-error-foreground";
|
|
48
50
|
default:
|
|
49
|
-
return "bg-
|
|
51
|
+
return "bg-muted/50 border-input text-foreground";
|
|
50
52
|
}
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
// Focus close button when sidebar opens
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (open && closeButtonRef.current) {
|
|
56
|
-
closeButtonRef.current.focus();
|
|
57
|
-
}
|
|
58
|
-
}, [open]);
|
|
59
|
-
|
|
60
55
|
// Reset internal state when open changes
|
|
61
56
|
useEffect(() => {
|
|
62
57
|
if (open) {
|
|
@@ -93,100 +88,80 @@ export function TaskDetailSidebar({
|
|
|
93
88
|
const filesForTab = filesForStep[filePaneType] ?? [];
|
|
94
89
|
|
|
95
90
|
return (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
>
|
|
103
|
-
{/* Header */}
|
|
104
|
-
<div
|
|
105
|
-
className={`px-6 py-4 border-b flex items-center justify-between ${getHeaderClasses(status)}`}
|
|
91
|
+
<>
|
|
92
|
+
<Sidebar
|
|
93
|
+
open={open}
|
|
94
|
+
onOpenChange={(isOpen) => !isOpen && onClose()}
|
|
95
|
+
title={title}
|
|
96
|
+
headerClassName={getHeaderClasses(status)}
|
|
106
97
|
>
|
|
107
|
-
<div
|
|
108
|
-
id={`slide-over-title-${taskIndex}`}
|
|
109
|
-
className="text-lg font-semibold truncate"
|
|
110
|
-
>
|
|
111
|
-
{title}
|
|
112
|
-
</div>
|
|
113
|
-
<button
|
|
114
|
-
ref={closeButtonRef}
|
|
115
|
-
type="button"
|
|
116
|
-
aria-label="Close details"
|
|
117
|
-
onClick={onClose}
|
|
118
|
-
className="rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 text-base"
|
|
119
|
-
>
|
|
120
|
-
×
|
|
121
|
-
</button>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div className="p-6 space-y-8 overflow-y-auto h-full">
|
|
125
98
|
{/* Error Callout - shown when task has error status */}
|
|
126
99
|
{status === TaskState.FAILED && (taskError?.message || taskBody) && (
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
<Callout.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{showStack ? "Hide stack" : "Show stack"}
|
|
144
|
-
</button>
|
|
145
|
-
{showStack && (
|
|
146
|
-
<pre
|
|
147
|
-
id="error-stack"
|
|
148
|
-
className="mt-2 p-2 bg-gray-50 border rounded text-xs font-mono max-h-64 overflow-auto whitespace-pre-wrap"
|
|
100
|
+
<SidebarSection className="bg-destructive/5 border-b">
|
|
101
|
+
<section aria-label="Error">
|
|
102
|
+
<Callout.Root role="alert" aria-live="assertive">
|
|
103
|
+
<Callout.Text className="whitespace-pre-wrap break-words">
|
|
104
|
+
{taskError?.message || taskBody}
|
|
105
|
+
</Callout.Text>
|
|
106
|
+
</Callout.Root>
|
|
107
|
+
|
|
108
|
+
{/* Stack trace toggle */}
|
|
109
|
+
{taskError?.stack && (
|
|
110
|
+
<div className="mt-3">
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => setShowStack(!showStack)}
|
|
113
|
+
className="text-sm text-primary hover:text-primary/80 underline"
|
|
114
|
+
aria-expanded={showStack}
|
|
115
|
+
aria-controls="error-stack"
|
|
149
116
|
>
|
|
150
|
-
{
|
|
151
|
-
</
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
117
|
+
{showStack ? "Hide stack" : "Show stack"}
|
|
118
|
+
</button>
|
|
119
|
+
{showStack && (
|
|
120
|
+
<pre
|
|
121
|
+
id="error-stack"
|
|
122
|
+
className="mt-2 p-2 bg-muted border rounded text-xs font-mono max-h-64 overflow-auto whitespace-pre-wrap"
|
|
123
|
+
>
|
|
124
|
+
{taskError.stack}
|
|
125
|
+
</pre>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</section>
|
|
130
|
+
</SidebarSection>
|
|
156
131
|
)}
|
|
157
132
|
|
|
158
133
|
{/* File Display Area with Type Tabs */}
|
|
159
|
-
<
|
|
134
|
+
<SidebarSection>
|
|
160
135
|
<div className="flex items-center justify-between mb-4">
|
|
161
|
-
<h3 className="text-base font-semibold text-
|
|
136
|
+
<h3 className="text-base font-semibold text-foreground">Files</h3>
|
|
162
137
|
<div className="flex items-center space-x-2">
|
|
163
|
-
<div className="flex rounded-lg border border-
|
|
138
|
+
<div className="flex rounded-lg border border-input bg-muted p-1">
|
|
164
139
|
<button
|
|
165
140
|
onClick={() => setFilePaneType("artifacts")}
|
|
166
|
-
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
|
141
|
+
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors border-l-2 ${
|
|
167
142
|
filePaneType === "artifacts"
|
|
168
|
-
? "bg-
|
|
169
|
-
: "text-
|
|
143
|
+
? "bg-background text-foreground shadow-sm border-indigo-400"
|
|
144
|
+
: "text-muted-foreground hover:text-foreground border-transparent"
|
|
170
145
|
}`}
|
|
171
146
|
>
|
|
172
147
|
Artifacts
|
|
173
148
|
</button>
|
|
174
149
|
<button
|
|
175
150
|
onClick={() => setFilePaneType("logs")}
|
|
176
|
-
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
|
151
|
+
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors border-l-2 ${
|
|
177
152
|
filePaneType === "logs"
|
|
178
|
-
? "bg-
|
|
179
|
-
: "text-
|
|
153
|
+
? "bg-background text-foreground shadow-sm border-indigo-400"
|
|
154
|
+
: "text-muted-foreground hover:text-foreground border-transparent"
|
|
180
155
|
}`}
|
|
181
156
|
>
|
|
182
157
|
Logs
|
|
183
158
|
</button>
|
|
184
159
|
<button
|
|
185
160
|
onClick={() => setFilePaneType("tmp")}
|
|
186
|
-
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
|
161
|
+
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors border-l-2 ${
|
|
187
162
|
filePaneType === "tmp"
|
|
188
|
-
? "bg-
|
|
189
|
-
: "text-
|
|
163
|
+
? "bg-background text-foreground shadow-sm border-indigo-400"
|
|
164
|
+
: "text-muted-foreground hover:text-foreground border-transparent"
|
|
190
165
|
}`}
|
|
191
166
|
>
|
|
192
167
|
Temp
|
|
@@ -194,46 +169,73 @@ export function TaskDetailSidebar({
|
|
|
194
169
|
</div>
|
|
195
170
|
</div>
|
|
196
171
|
</div>
|
|
197
|
-
</section>
|
|
198
172
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<div className="space-y-1">
|
|
173
|
+
{/* File List Table */}
|
|
174
|
+
<div className="space-y-2">
|
|
175
|
+
<Text size="2" className="text-muted-foreground">
|
|
176
|
+
{filePaneType.charAt(0).toUpperCase() + filePaneType.slice(1)}{" "}
|
|
177
|
+
files for {taskId}
|
|
178
|
+
</Text>
|
|
206
179
|
{filesForTab.length === 0 ? (
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
180
|
+
<Box className="py-4 text-center">
|
|
181
|
+
<Text size="2" className="text-muted-foreground italic">
|
|
182
|
+
No {filePaneType} files available for this task
|
|
183
|
+
</Text>
|
|
184
|
+
</Box>
|
|
210
185
|
) : (
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
186
|
+
<Table.Root radius="none">
|
|
187
|
+
<Table.Header>
|
|
188
|
+
<Table.Row>
|
|
189
|
+
<Table.ColumnHeaderCell>File Name</Table.ColumnHeaderCell>
|
|
190
|
+
</Table.Row>
|
|
191
|
+
</Table.Header>
|
|
192
|
+
<Table.Body>
|
|
193
|
+
{filesForTab.map((name) => (
|
|
194
|
+
<Table.Row
|
|
195
|
+
key={`${filePaneType}-${name}`}
|
|
196
|
+
className="cursor-pointer hover:bg-slate-50/50"
|
|
197
|
+
onClick={() => handleFileClick(name)}
|
|
198
|
+
>
|
|
199
|
+
<Table.Cell>
|
|
200
|
+
<Text size="2">{name}</Text>
|
|
201
|
+
</Table.Cell>
|
|
202
|
+
</Table.Row>
|
|
203
|
+
))}
|
|
204
|
+
</Table.Body>
|
|
205
|
+
</Table.Root>
|
|
222
206
|
)}
|
|
223
207
|
</div>
|
|
224
|
-
</div>
|
|
225
208
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
209
|
+
{/* Inline File Preview */}
|
|
210
|
+
{filePaneOpen && (
|
|
211
|
+
<div className="mt-4 border-t pt-4">
|
|
212
|
+
<div className="flex items-center justify-between mb-2">
|
|
213
|
+
<Text size="2" weight="medium" className="text-foreground">
|
|
214
|
+
File Preview
|
|
215
|
+
</Text>
|
|
216
|
+
<button
|
|
217
|
+
onClick={handleFilePaneClose}
|
|
218
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
219
|
+
>
|
|
220
|
+
Close Preview
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="h-96 overflow-auto">
|
|
224
|
+
<TaskFilePane
|
|
225
|
+
isOpen={filePaneOpen}
|
|
226
|
+
jobId={jobId}
|
|
227
|
+
taskId={taskId}
|
|
228
|
+
type={filePaneType}
|
|
229
|
+
filename={filePaneFilename}
|
|
230
|
+
onClose={handleFilePaneClose}
|
|
231
|
+
inline={true}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</SidebarSection>
|
|
237
|
+
</Sidebar>
|
|
238
|
+
</>
|
|
237
239
|
);
|
|
238
240
|
}
|
|
239
241
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Button } from "./ui/button.jsx";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* TaskFilePane component for displaying a single task file with preview
|
|
@@ -9,6 +10,7 @@ import React, { useState, useEffect, useRef } from "react";
|
|
|
9
10
|
* @param {string} props.type - File type (artifacts|logs|tmp)
|
|
10
11
|
* @param {string} props.filename - File name to display
|
|
11
12
|
* @param {Function} props.onClose - Close handler
|
|
13
|
+
* @param {boolean} props.inline - Whether to render inline (vs modal)
|
|
12
14
|
*/
|
|
13
15
|
export function TaskFilePane({
|
|
14
16
|
isOpen,
|
|
@@ -17,6 +19,7 @@ export function TaskFilePane({
|
|
|
17
19
|
type,
|
|
18
20
|
filename,
|
|
19
21
|
onClose,
|
|
22
|
+
inline = false,
|
|
20
23
|
}) {
|
|
21
24
|
const [copyNotice, setCopyNotice] = useState(null);
|
|
22
25
|
const [loading, setLoading] = useState(false);
|
|
@@ -364,12 +367,14 @@ export function TaskFilePane({
|
|
|
364
367
|
<p className="mt-1 text-sm text-red-700">
|
|
365
368
|
{error.error?.message || "Unknown error"}
|
|
366
369
|
</p>
|
|
367
|
-
<
|
|
370
|
+
<Button
|
|
371
|
+
variant="ghost"
|
|
372
|
+
size="sm"
|
|
368
373
|
onClick={handleRetry}
|
|
369
|
-
className="mt-2 text-
|
|
374
|
+
className="mt-2 text-red-600 hover:text-red-800"
|
|
370
375
|
>
|
|
371
376
|
Retry
|
|
372
|
-
</
|
|
377
|
+
</Button>
|
|
373
378
|
</div>
|
|
374
379
|
</div>
|
|
375
380
|
</div>
|
|
@@ -493,12 +498,18 @@ export function TaskFilePane({
|
|
|
493
498
|
return null;
|
|
494
499
|
}
|
|
495
500
|
|
|
501
|
+
const containerClassName = inline
|
|
502
|
+
? "flex flex-col h-full"
|
|
503
|
+
: "bg-white rounded-lg shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col";
|
|
504
|
+
|
|
496
505
|
return (
|
|
497
|
-
<div className=
|
|
506
|
+
<div className={containerClassName}>
|
|
498
507
|
{/* Header */}
|
|
499
|
-
<div className="flex items-center justify-between p-4 border-b">
|
|
508
|
+
<div className="flex items-center justify-between p-4 border-b bg-white">
|
|
500
509
|
<div>
|
|
501
|
-
<h2 className="text-lg font-semibold">
|
|
510
|
+
<h2 className="text-lg font-semibold">
|
|
511
|
+
{inline ? "File Preview" : filename}
|
|
512
|
+
</h2>
|
|
502
513
|
</div>
|
|
503
514
|
<button
|
|
504
515
|
ref={closeButtonRef}
|
|
@@ -523,44 +534,88 @@ export function TaskFilePane({
|
|
|
523
534
|
</div>
|
|
524
535
|
|
|
525
536
|
{/* Preview */}
|
|
526
|
-
<div className="flex-1 flex flex-col bg-gray-50">
|
|
527
|
-
{/* Preview Header */}
|
|
528
|
-
|
|
529
|
-
<div className="
|
|
530
|
-
<div>
|
|
531
|
-
<
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
537
|
+
<div className="flex-1 flex flex-col bg-gray-50 overflow-hidden">
|
|
538
|
+
{/* Preview Header - only show filename in non-inline mode */}
|
|
539
|
+
{!inline && (
|
|
540
|
+
<div className="bg-white border-b p-4">
|
|
541
|
+
<div className="flex items-center justify-between">
|
|
542
|
+
<div>
|
|
543
|
+
<h3 className="font-medium">{filename}</h3>
|
|
544
|
+
<div className="flex items-center text-sm text-gray-500 mt-1">
|
|
545
|
+
{size && <span>{formatSize(size)}</span>}
|
|
546
|
+
{size && mtime && <span className="mx-1">•</span>}
|
|
547
|
+
{mtime && <span>{formatDate(mtime)}</span>}
|
|
548
|
+
{mime && (size || mtime) && <span className="mx-1">•</span>}
|
|
549
|
+
{mime && <span>{mime}</span>}
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
<div className="flex items-center space-x-2">
|
|
553
|
+
{copyNotice && (
|
|
554
|
+
<div
|
|
555
|
+
className={`text-sm ${
|
|
556
|
+
copyNotice.type === "success"
|
|
557
|
+
? "text-green-600"
|
|
558
|
+
: "text-red-600"
|
|
559
|
+
}`}
|
|
560
|
+
>
|
|
561
|
+
{copyNotice.message}
|
|
562
|
+
</div>
|
|
563
|
+
)}
|
|
564
|
+
{content && encoding === "utf8" && (
|
|
565
|
+
<Button
|
|
566
|
+
variant="solid"
|
|
567
|
+
size="sm"
|
|
568
|
+
onClick={handleCopy}
|
|
569
|
+
aria-label="Copy content to clipboard"
|
|
570
|
+
>
|
|
571
|
+
Copy
|
|
572
|
+
</Button>
|
|
573
|
+
)}
|
|
538
574
|
</div>
|
|
539
575
|
</div>
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
>
|
|
549
|
-
|
|
576
|
+
</div>
|
|
577
|
+
)}
|
|
578
|
+
|
|
579
|
+
{/* Inline preview header with filename and actions */}
|
|
580
|
+
{inline && (
|
|
581
|
+
<div className="bg-white border-b p-3">
|
|
582
|
+
<div className="flex items-center justify-between">
|
|
583
|
+
<div>
|
|
584
|
+
<h3 className="font-medium text-sm">{filename}</h3>
|
|
585
|
+
<div className="flex items-center text-xs text-gray-500 mt-1">
|
|
586
|
+
{size && <span>{formatSize(size)}</span>}
|
|
587
|
+
{size && mtime && <span className="mx-1">•</span>}
|
|
588
|
+
{mtime && <span>{formatDate(mtime)}</span>}
|
|
589
|
+
{mime && (size || mtime) && <span className="mx-1">•</span>}
|
|
590
|
+
{mime && <span>{mime}</span>}
|
|
550
591
|
</div>
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
592
|
+
</div>
|
|
593
|
+
<div className="flex items-center space-x-2">
|
|
594
|
+
{copyNotice && (
|
|
595
|
+
<div
|
|
596
|
+
className={`text-xs ${
|
|
597
|
+
copyNotice.type === "success"
|
|
598
|
+
? "text-green-600"
|
|
599
|
+
: "text-red-600"
|
|
600
|
+
}`}
|
|
601
|
+
>
|
|
602
|
+
{copyNotice.message}
|
|
603
|
+
</div>
|
|
604
|
+
)}
|
|
605
|
+
{content && encoding === "utf8" && (
|
|
606
|
+
<Button
|
|
607
|
+
variant="solid"
|
|
608
|
+
size="sm"
|
|
609
|
+
onClick={handleCopy}
|
|
610
|
+
aria-label="Copy content to clipboard"
|
|
611
|
+
>
|
|
612
|
+
Copy
|
|
613
|
+
</Button>
|
|
614
|
+
)}
|
|
615
|
+
</div>
|
|
561
616
|
</div>
|
|
562
617
|
</div>
|
|
563
|
-
|
|
618
|
+
)}
|
|
564
619
|
|
|
565
620
|
{/* Preview Content */}
|
|
566
621
|
<div className="flex-1 overflow-auto">{renderContent()}</div>
|
|
@@ -1,43 +1,75 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Button as RadixButton } from "@radix-ui/themes";
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Button Component
|
|
5
|
+
*
|
|
6
|
+
* Standardized button component following the Steel Terminal design system.
|
|
7
|
+
* Use this component for all buttons instead of raw <button> tags.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/button-standards.md for usage guidelines
|
|
10
|
+
*
|
|
11
|
+
* @param {string} variant - Button variant: solid, soft, outline, ghost, destructive
|
|
12
|
+
* @param {string} size - Button size: sm, md, lg
|
|
13
|
+
* @param {boolean} loading - Show loading state
|
|
14
|
+
* @param {string} className - Additional CSS classes
|
|
15
|
+
*/
|
|
4
16
|
export function Button({
|
|
5
17
|
variant = "solid",
|
|
6
|
-
size = "
|
|
18
|
+
size = "md",
|
|
19
|
+
loading = false,
|
|
7
20
|
className = "",
|
|
21
|
+
disabled,
|
|
22
|
+
children,
|
|
23
|
+
type = "button",
|
|
8
24
|
...props
|
|
9
25
|
}) {
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
? "solid"
|
|
14
|
-
: variant === "outline"
|
|
15
|
-
? "outline"
|
|
16
|
-
: variant === "ghost"
|
|
17
|
-
? "ghost"
|
|
18
|
-
: variant === "secondary"
|
|
19
|
-
? "soft"
|
|
20
|
-
: variant === "destructive"
|
|
21
|
-
? "solid"
|
|
22
|
-
: "solid";
|
|
26
|
+
// Base classes for all buttons
|
|
27
|
+
const baseClasses =
|
|
28
|
+
"inline-flex items-center justify-center font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
|
23
29
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
30
|
+
// Variant styles using Steel Terminal theme colors
|
|
31
|
+
const variantClasses = {
|
|
32
|
+
solid:
|
|
33
|
+
"bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] hover:bg-[hsl(var(--primary-hover))] focus:ring-[hsl(var(--primary))]",
|
|
34
|
+
default:
|
|
35
|
+
"bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] hover:bg-[hsl(var(--primary-hover))] focus:ring-[hsl(var(--primary))]",
|
|
36
|
+
soft: "bg-[hsl(var(--primary))]/10 text-[hsl(var(--primary))] hover:bg-[hsl(var(--primary))]/15 focus:ring-[hsl(var(--primary))] border border-[hsl(var(--primary))]/20",
|
|
37
|
+
outline:
|
|
38
|
+
"border border-[hsl(var(--border))] text-[hsl(var(--secondary-foreground))] bg-transparent hover:bg-[hsl(var(--secondary))] focus:ring-[hsl(var(--ring))]",
|
|
39
|
+
ghost:
|
|
40
|
+
"text-[hsl(var(--muted-foreground))] bg-transparent hover:bg-[hsl(var(--secondary))] hover:text-[hsl(var(--secondary-foreground))] focus:ring-[hsl(var(--ring))]",
|
|
41
|
+
destructive:
|
|
42
|
+
"bg-[hsl(var(--destructive))] text-[hsl(var(--destructive-foreground))] hover:bg-[hsl(var(--destructive))]/90 focus:ring-[hsl(var(--destructive))]",
|
|
43
|
+
};
|
|
27
44
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
45
|
+
// Size styles
|
|
46
|
+
const sizeClasses = {
|
|
47
|
+
sm: "px-3 py-1.5 text-sm",
|
|
48
|
+
md: "px-4 py-2 text-base",
|
|
49
|
+
lg: "px-6 py-3 text-lg",
|
|
50
|
+
};
|
|
30
51
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
52
|
+
// Disable button when loading or explicitly disabled
|
|
53
|
+
const isDisabled = disabled || loading;
|
|
54
|
+
|
|
55
|
+
// Combine all classes
|
|
56
|
+
const combinedClassName = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`;
|
|
33
57
|
|
|
34
58
|
return (
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
size={radixSize}
|
|
38
|
-
color={color}
|
|
59
|
+
<button
|
|
60
|
+
type={type}
|
|
39
61
|
className={combinedClassName}
|
|
62
|
+
disabled={isDisabled}
|
|
40
63
|
{...props}
|
|
41
|
-
|
|
64
|
+
>
|
|
65
|
+
{loading ? (
|
|
66
|
+
<span className="inline-flex items-center gap-2">
|
|
67
|
+
<span className="animate-spin">⟳</span>
|
|
68
|
+
{children}
|
|
69
|
+
</span>
|
|
70
|
+
) : (
|
|
71
|
+
children
|
|
72
|
+
)}
|
|
73
|
+
</button>
|
|
42
74
|
);
|
|
43
75
|
}
|