@lokvis/ui-react 0.2.1 → 0.4.1
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/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +18 -62
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +4 -4
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +9 -9
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +7 -7
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +9 -8
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +10 -9
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts +3 -3
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +10 -17
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBanner.d.ts +2 -2
- package/dist/components/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner.js +3 -3
- package/dist/components/ErrorBanner.js.map +1 -1
- package/dist/components/ErrorBoundary.d.ts +40 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +52 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/ExifPanel.d.ts +1 -1
- package/dist/components/ExifPanel.d.ts.map +1 -1
- package/dist/components/ExifPanel.js +2 -2
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts +5 -5
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +15 -8
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/HistoryPanel.d.ts +2 -2
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +23 -23
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +13 -10
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +10 -7
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +19 -28
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +4 -4
- package/dist/components/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar.js +8 -8
- package/dist/components/ProgressBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts +3 -3
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +12 -21
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/StatusDot.d.ts +12 -0
- package/dist/components/StatusDot.d.ts.map +1 -0
- package/dist/components/StatusDot.js +15 -0
- package/dist/components/StatusDot.js.map +1 -0
- package/dist/components/ThemeToggle.d.ts +4 -4
- package/dist/components/ThemeToggle.d.ts.map +1 -1
- package/dist/components/ThemeToggle.js +7 -7
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/ToolRunner.d.ts +53 -0
- package/dist/components/ToolRunner.d.ts.map +1 -0
- package/dist/components/ToolRunner.js +142 -0
- package/dist/components/ToolRunner.js.map +1 -0
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +3 -3
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts +6 -6
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +40 -49
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts +2 -2
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +17 -7
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +45 -23
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +87 -40
- package/dist/components/Workspace.js.map +1 -1
- package/dist/hooks/useCustomPresets.d.ts +98 -0
- package/dist/hooks/useCustomPresets.d.ts.map +1 -0
- package/dist/hooks/useCustomPresets.js +210 -0
- package/dist/hooks/useCustomPresets.js.map +1 -0
- package/dist/hooks/useLokvis.d.ts +19 -1
- package/dist/hooks/useLokvis.d.ts.map +1 -1
- package/dist/hooks/useLokvis.js +30 -9
- package/dist/hooks/useLokvis.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/store/assets-slice.d.ts +8 -1
- package/dist/store/assets-slice.d.ts.map +1 -1
- package/dist/store/assets-slice.js +101 -54
- package/dist/store/assets-slice.js.map +1 -1
- package/dist/store/capabilities-slice.d.ts +1 -1
- package/dist/store/capabilities-slice.d.ts.map +1 -1
- package/dist/store/capabilities-slice.js +8 -1
- package/dist/store/capabilities-slice.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/runtime-slice.d.ts.map +1 -1
- package/dist/store/runtime-slice.js +22 -16
- package/dist/store/runtime-slice.js.map +1 -1
- package/dist/store/subscribe-utils.d.ts +45 -0
- package/dist/store/subscribe-utils.d.ts.map +1 -0
- package/dist/store/subscribe-utils.js +69 -0
- package/dist/store/subscribe-utils.js.map +1 -0
- package/dist/store/types.d.ts +19 -11
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/types.js +0 -10
- package/dist/store/types.js.map +1 -1
- package/dist/store/workflow-slice.d.ts +1 -1
- package/dist/store/workflow-slice.d.ts.map +1 -1
- package/dist/store/workflow-slice.js +19 -51
- package/dist/store/workflow-slice.js.map +1 -1
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +31 -0
- package/dist/utils.js.map +1 -0
- package/package.json +7 -6
- package/src/components/AssetPanel.tsx +224 -264
- package/src/components/Canvas.tsx +178 -174
- package/src/components/CommandPalette.tsx +242 -242
- package/src/components/CompareSlider.tsx +168 -165
- package/src/components/DownloadPanel.tsx +201 -207
- package/src/components/ErrorBanner.tsx +64 -64
- package/src/components/ErrorBoundary.tsx +101 -0
- package/src/components/ExifPanel.tsx +154 -154
- package/src/components/GlobalDropzone.tsx +175 -170
- package/src/components/HistoryPanel.tsx +219 -219
- package/src/components/Inspector.tsx +145 -137
- package/src/components/ParamForm.tsx +140 -137
- package/src/components/PipelineBar.tsx +97 -104
- package/src/components/ProgressBar.tsx +78 -78
- package/src/components/StatusBar.tsx +145 -152
- package/src/components/StatusDot.tsx +27 -0
- package/src/components/ThemeToggle.tsx +98 -98
- package/src/components/ToolRunner.tsx +352 -0
- package/src/components/Toolbar.tsx +65 -65
- package/src/components/WorkflowEditor.tsx +333 -326
- package/src/components/WorkflowTemplates.tsx +101 -78
- package/src/components/Workspace.tsx +310 -223
- package/src/css-modules.d.ts +3 -0
- package/src/hooks/useCustomPresets.ts +284 -0
- package/src/hooks/useLokvis.ts +43 -10
- package/src/index.ts +7 -0
- package/src/store/assets-slice.ts +106 -52
- package/src/store/capabilities-slice.ts +10 -2
- package/src/store/index.ts +1 -1
- package/src/store/runtime-slice.ts +20 -15
- package/src/store/subscribe-utils.ts +82 -0
- package/src/store/types.ts +19 -12
- package/src/store/workflow-slice.ts +19 -53
- package/src/utils.ts +41 -0
- package/dist/store.d.ts +0 -76
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -217
- package/dist/store.js.map +0 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolRunner - 单工具页组合组件
|
|
3
|
+
*
|
|
4
|
+
* 为单工具操作场景(压缩、裁剪、格式转换等)提供一站式 UI:
|
|
5
|
+
* 上传 → 调参 → 处理 → 对比 → 下载。
|
|
6
|
+
*
|
|
7
|
+
* 不依赖 Workspace Store(zustand),通过 engine prop 注入处理函数,
|
|
8
|
+
* 可在任何 React 应用中独立使用。
|
|
9
|
+
*
|
|
10
|
+
* 五层架构:UI 层不直接依赖 Engine 层,engine 函数由消费方传入。
|
|
11
|
+
* 消费方需自行实现按 capability.name 路由到对应 Blob↔Blob 操作的 wrapper
|
|
12
|
+
* (engine-image 已不再提供 runTool 统一入口,各 operation 需直接组合调用)。
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* import { ToolRunner } from '@lokvis/ui-react';
|
|
17
|
+
* import { IMAGE_COMPRESS, compress } from '@lokvis/engine-image';
|
|
18
|
+
*
|
|
19
|
+
* <ToolRunner
|
|
20
|
+
* capability={IMAGE_COMPRESS}
|
|
21
|
+
* engine={(name, input, params, opts) => compress(input, params, opts?.signal)}
|
|
22
|
+
* onOpenInWorkspace={(state) => { ... }}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as React from 'react';
|
|
28
|
+
import type { Capability } from '@lokvis/schema';
|
|
29
|
+
import { Button, Card } from '@lokvis/ui-core';
|
|
30
|
+
import { ParamForm } from './ParamForm.js';
|
|
31
|
+
|
|
32
|
+
type EngineFn = (
|
|
33
|
+
toolName: string,
|
|
34
|
+
input: Blob,
|
|
35
|
+
params: Record<string, unknown>,
|
|
36
|
+
options?: { signal?: AbortSignal; onProgress?: (p: number) => void },
|
|
37
|
+
) => Promise<Blob>;
|
|
38
|
+
|
|
39
|
+
export interface ToolRunnerProps {
|
|
40
|
+
/** 工具能力定义(决定参数表单和输入类型) */
|
|
41
|
+
capability: Capability;
|
|
42
|
+
/** 处理函数(按 capability.name 路由到对应 Blob↔Blob 操作的 wrapper) */
|
|
43
|
+
engine: EngineFn;
|
|
44
|
+
/** 在 Workspace 中打开的回调(携带当前状态) */
|
|
45
|
+
onOpenInWorkspace?: (state: {
|
|
46
|
+
inputBlob: Blob;
|
|
47
|
+
outputBlob: Blob | null;
|
|
48
|
+
params: Record<string, unknown>;
|
|
49
|
+
}) => void;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* ToolRunner - 单工具操作组件。
|
|
55
|
+
*
|
|
56
|
+
* 内部组合:上传区 + 参数表单 + 预览/对比 + 进度 + 下载。
|
|
57
|
+
* 所有状态自管理,不依赖外部 store。
|
|
58
|
+
*/
|
|
59
|
+
export function ToolRunner({
|
|
60
|
+
capability,
|
|
61
|
+
engine,
|
|
62
|
+
onOpenInWorkspace,
|
|
63
|
+
className = '',
|
|
64
|
+
}: ToolRunnerProps) {
|
|
65
|
+
const [inputBlob, setInputBlob] = React.useState<Blob | null>(null);
|
|
66
|
+
const [inputUrl, setInputUrl] = React.useState<string | null>(null);
|
|
67
|
+
const [outputBlob, setOutputBlob] = React.useState<Blob | null>(null);
|
|
68
|
+
const [outputUrl, setOutputUrl] = React.useState<string | null>(null);
|
|
69
|
+
const [params, setParams] = React.useState<Record<string, unknown>>(() => {
|
|
70
|
+
const defaults: Record<string, unknown> = {};
|
|
71
|
+
for (const p of capability.params) {
|
|
72
|
+
if (p.default !== undefined) defaults[p.name] = p.default;
|
|
73
|
+
}
|
|
74
|
+
return defaults;
|
|
75
|
+
});
|
|
76
|
+
const [progress, setProgress] = React.useState(0);
|
|
77
|
+
const [processing, setProcessing] = React.useState(false);
|
|
78
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
79
|
+
const [dragging, setDragging] = React.useState(false);
|
|
80
|
+
const abortRef = React.useRef<AbortController | null>(null);
|
|
81
|
+
|
|
82
|
+
// 清理 ObjectURL
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
return () => {
|
|
85
|
+
if (inputUrl) URL.revokeObjectURL(inputUrl);
|
|
86
|
+
if (outputUrl) URL.revokeObjectURL(outputUrl);
|
|
87
|
+
};
|
|
88
|
+
}, [inputUrl, outputUrl]);
|
|
89
|
+
|
|
90
|
+
// W21.6: unmount 时 abort 进行中的处理,防止后台泄漏
|
|
91
|
+
// (engine 函数持有 Worker/Canvas 资源,卸载后继续运行是浪费 + 潜在内存泄漏)
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
return () => {
|
|
94
|
+
if (abortRef.current) {
|
|
95
|
+
abortRef.current.abort();
|
|
96
|
+
abortRef.current = null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const accept = capability.inputTypes.join(',');
|
|
102
|
+
|
|
103
|
+
function handleFiles(files: FileList | File[] | null) {
|
|
104
|
+
if (!files || files.length === 0) return;
|
|
105
|
+
const file = files[0];
|
|
106
|
+
if (!file) return;
|
|
107
|
+
|
|
108
|
+
setInputBlob(file);
|
|
109
|
+
if (inputUrl) URL.revokeObjectURL(inputUrl);
|
|
110
|
+
setInputUrl(URL.createObjectURL(file));
|
|
111
|
+
|
|
112
|
+
// 重置输出
|
|
113
|
+
setOutputBlob(null);
|
|
114
|
+
if (outputUrl) {
|
|
115
|
+
URL.revokeObjectURL(outputUrl);
|
|
116
|
+
setOutputUrl(null);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleProcess() {
|
|
121
|
+
if (!inputBlob) return;
|
|
122
|
+
setProcessing(true);
|
|
123
|
+
setProgress(0);
|
|
124
|
+
setError(null);
|
|
125
|
+
|
|
126
|
+
abortRef.current = new AbortController();
|
|
127
|
+
try {
|
|
128
|
+
const result = await engine(capability.name, inputBlob, params, {
|
|
129
|
+
signal: abortRef.current.signal,
|
|
130
|
+
onProgress: setProgress,
|
|
131
|
+
});
|
|
132
|
+
setOutputBlob(result);
|
|
133
|
+
if (outputUrl) URL.revokeObjectURL(outputUrl);
|
|
134
|
+
setOutputUrl(URL.createObjectURL(result));
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err instanceof Error && err.name === 'AbortError') return;
|
|
137
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
138
|
+
} finally {
|
|
139
|
+
setProcessing(false);
|
|
140
|
+
abortRef.current = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleCancel() {
|
|
145
|
+
abortRef.current?.abort();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const downloadName = outputBlob
|
|
149
|
+
? `output.${outputBlob.type.split('/')[1] ?? 'bin'}`
|
|
150
|
+
: 'output';
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className={`flex flex-col gap-6 ${className}`}>
|
|
154
|
+
{/* Upload zone */}
|
|
155
|
+
{!inputBlob && (
|
|
156
|
+
<label
|
|
157
|
+
className={`block cursor-pointer rounded-xl border-2 border-dashed p-10 text-center transition-all ${
|
|
158
|
+
dragging
|
|
159
|
+
? 'border-[var(--lokvis-primary)] bg-[var(--lokvis-primary)]/10'
|
|
160
|
+
: 'border-[var(--lokvis-border)] hover:border-[var(--lokvis-primary)]/50 hover:bg-[var(--lokvis-surface-muted)]'
|
|
161
|
+
}`}
|
|
162
|
+
onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
|
|
163
|
+
onDragLeave={() => setDragging(false)}
|
|
164
|
+
onDrop={(e) => {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
setDragging(false);
|
|
167
|
+
handleFiles(e.dataTransfer.files);
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<input
|
|
171
|
+
type="file"
|
|
172
|
+
accept={accept}
|
|
173
|
+
className="hidden"
|
|
174
|
+
onChange={(e) => handleFiles(e.target.files)}
|
|
175
|
+
/>
|
|
176
|
+
<div className="flex flex-col items-center gap-3">
|
|
177
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-[var(--lokvis-surface-muted)]">
|
|
178
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-[var(--lokvis-fg-subtle)]">
|
|
179
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
180
|
+
<polyline points="17 8 12 3 7 8" />
|
|
181
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
182
|
+
</svg>
|
|
183
|
+
</div>
|
|
184
|
+
<div>
|
|
185
|
+
<p className="text-sm font-medium text-[var(--lokvis-fg)]">
|
|
186
|
+
Drop file here or click to upload
|
|
187
|
+
</p>
|
|
188
|
+
<p className="mt-1 text-xs text-[var(--lokvis-fg-subtle)]">
|
|
189
|
+
Accepted: {accept || 'any file'}
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</label>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Processing area */}
|
|
197
|
+
{inputBlob && (
|
|
198
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
199
|
+
{/* Left: Parameters */}
|
|
200
|
+
<div className="space-y-4">
|
|
201
|
+
<Card>
|
|
202
|
+
<div className="mb-3 flex items-center justify-between">
|
|
203
|
+
<h3 className="text-sm font-semibold text-[var(--lokvis-fg)]">
|
|
204
|
+
Parameters
|
|
205
|
+
</h3>
|
|
206
|
+
<span className="rounded bg-[var(--lokvis-surface-muted)] px-2 py-0.5 font-mono text-xs text-[var(--lokvis-primary)]">
|
|
207
|
+
{capability.name}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
<ParamForm
|
|
211
|
+
capability={capability}
|
|
212
|
+
values={params}
|
|
213
|
+
onChange={setParams}
|
|
214
|
+
/>
|
|
215
|
+
</Card>
|
|
216
|
+
|
|
217
|
+
{/* Actions */}
|
|
218
|
+
<div className="flex gap-3">
|
|
219
|
+
<Button
|
|
220
|
+
variant="primary"
|
|
221
|
+
size="md"
|
|
222
|
+
onClick={handleProcess}
|
|
223
|
+
loading={processing}
|
|
224
|
+
disabled={!inputBlob}
|
|
225
|
+
>
|
|
226
|
+
{processing ? 'Processing...' : 'Process'}
|
|
227
|
+
</Button>
|
|
228
|
+
<Button
|
|
229
|
+
variant="secondary"
|
|
230
|
+
size="md"
|
|
231
|
+
onClick={() => {
|
|
232
|
+
setInputBlob(null);
|
|
233
|
+
if (inputUrl) URL.revokeObjectURL(inputUrl);
|
|
234
|
+
setInputUrl(null);
|
|
235
|
+
setOutputBlob(null);
|
|
236
|
+
if (outputUrl) {
|
|
237
|
+
URL.revokeObjectURL(outputUrl);
|
|
238
|
+
setOutputUrl(null);
|
|
239
|
+
}
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
Change file
|
|
243
|
+
</Button>
|
|
244
|
+
{onOpenInWorkspace && (
|
|
245
|
+
<Button
|
|
246
|
+
variant="ghost"
|
|
247
|
+
size="md"
|
|
248
|
+
onClick={() => onOpenInWorkspace({ inputBlob, outputBlob, params })}
|
|
249
|
+
>
|
|
250
|
+
Open in Workspace →
|
|
251
|
+
</Button>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Progress */}
|
|
256
|
+
{processing && (
|
|
257
|
+
<div className="space-y-2">
|
|
258
|
+
<div className="flex items-center justify-between text-xs text-[var(--lokvis-fg-muted)]">
|
|
259
|
+
<span>Processing...</span>
|
|
260
|
+
<span className="tabular-nums">{Math.round(progress)}%</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-[var(--lokvis-border)]">
|
|
263
|
+
<div
|
|
264
|
+
className="h-full rounded-full bg-[var(--lokvis-primary)] transition-all duration-200"
|
|
265
|
+
style={{ width: `${progress}%` }}
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={handleCancel}
|
|
271
|
+
className="text-xs text-[var(--lokvis-danger)] hover:underline"
|
|
272
|
+
>
|
|
273
|
+
Cancel
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* Error */}
|
|
279
|
+
{error && (
|
|
280
|
+
<div className="rounded-lg border border-[var(--lokvis-danger)]/30 bg-[var(--lokvis-danger)]/10 px-4 py-3 text-sm text-[var(--lokvis-danger)]">
|
|
281
|
+
{error}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* Download */}
|
|
286
|
+
{outputBlob && outputUrl && (
|
|
287
|
+
<Card>
|
|
288
|
+
<div className="flex items-center justify-between">
|
|
289
|
+
<div>
|
|
290
|
+
<p className="text-sm font-medium text-[var(--lokvis-fg)]">Ready to download</p>
|
|
291
|
+
<p className="mt-1 text-xs text-[var(--lokvis-fg-muted)]">
|
|
292
|
+
{(outputBlob.size / 1024).toFixed(1)} KB · {outputBlob.type.split('/')[1] ?? 'file'}
|
|
293
|
+
</p>
|
|
294
|
+
</div>
|
|
295
|
+
<a
|
|
296
|
+
href={outputUrl}
|
|
297
|
+
download={downloadName}
|
|
298
|
+
className="inline-flex items-center gap-1.5 rounded-lg bg-[var(--lokvis-primary)] px-4 py-2 text-sm font-medium text-[var(--lokvis-primary-fg)] transition-colors hover:bg-[var(--lokvis-primary-hover)]"
|
|
299
|
+
>
|
|
300
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
301
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
302
|
+
<polyline points="7 10 12 15 17 10" />
|
|
303
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
304
|
+
</svg>
|
|
305
|
+
Download
|
|
306
|
+
</a>
|
|
307
|
+
</div>
|
|
308
|
+
</Card>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{/* Right: Preview */}
|
|
313
|
+
<div className="space-y-4">
|
|
314
|
+
{/* Input preview */}
|
|
315
|
+
{inputUrl && (
|
|
316
|
+
<Card>
|
|
317
|
+
<h4 className="mb-2 text-xs font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
|
|
318
|
+
Input
|
|
319
|
+
</h4>
|
|
320
|
+
<img
|
|
321
|
+
src={inputUrl}
|
|
322
|
+
alt="Input preview"
|
|
323
|
+
className="max-h-80 w-full rounded-lg object-contain"
|
|
324
|
+
/>
|
|
325
|
+
<p className="mt-2 text-xs text-[var(--lokvis-fg-muted)]">
|
|
326
|
+
{(inputBlob.size / 1024).toFixed(1)} KB · {inputBlob.type.split('/')[1] ?? 'file'}
|
|
327
|
+
</p>
|
|
328
|
+
</Card>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{/* Output preview */}
|
|
332
|
+
{outputUrl && (
|
|
333
|
+
<Card>
|
|
334
|
+
<h4 className="mb-2 text-xs font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
|
|
335
|
+
Output
|
|
336
|
+
</h4>
|
|
337
|
+
<img
|
|
338
|
+
src={outputUrl}
|
|
339
|
+
alt="Output preview"
|
|
340
|
+
className="max-h-80 w-full rounded-lg object-contain"
|
|
341
|
+
/>
|
|
342
|
+
<p className="mt-2 text-xs text-[var(--lokvis-fg-muted)]">
|
|
343
|
+
{(outputBlob!.size / 1024).toFixed(1)} KB · {outputBlob!.type.split('/')[1] ?? 'file'}
|
|
344
|
+
</p>
|
|
345
|
+
</Card>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
@@ -11,77 +11,77 @@ import { Button } from '@lokvis/ui-core';
|
|
|
11
11
|
import { useWorkspaceStore } from '../store/index.js';
|
|
12
12
|
|
|
13
13
|
export interface ToolbarProps {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
title?: string;
|
|
15
|
+
rightExtra?: React.ReactNode;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function Toolbar({ title = 'Lokvis Workspace', rightExtra }: ToolbarProps) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const running = useWorkspaceStore((s) => s.running);
|
|
20
|
+
const statusMessage = useWorkspaceStore((s) => s.statusMessage);
|
|
21
|
+
const error = useWorkspaceStore((s) => s.error);
|
|
22
|
+
const nodes = useWorkspaceStore((s) => s.nodes);
|
|
23
|
+
const selectedAssetId = useWorkspaceStore((s) => s.selectedAssetId);
|
|
24
|
+
const run = useWorkspaceStore((s) => s.run);
|
|
25
|
+
const clearWorkflow = useWorkspaceStore((s) => s.clearWorkflow);
|
|
26
|
+
const setError = useWorkspaceStore((s) => s.setError);
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const canRun = !running && nodes.length > 0 && !!selectedAssetId;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
async function handleRun() {
|
|
31
|
+
try {
|
|
32
|
+
await run();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
return (
|
|
39
|
+
<header className="flex h-[var(--lokvis-toolbar-h)] shrink-0 items-center justify-between border-b border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] px-4">
|
|
40
|
+
{/* Left: Brand */}
|
|
41
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
42
|
+
<span
|
|
43
|
+
className="shrink-0 text-base font-bold leading-none"
|
|
44
|
+
style={{
|
|
45
|
+
background: 'linear-gradient(135deg, var(--lokvis-primary), var(--lokvis-info))',
|
|
46
|
+
WebkitBackgroundClip: 'text',
|
|
47
|
+
WebkitTextFillColor: 'transparent',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
◆
|
|
51
|
+
</span>
|
|
52
|
+
<span className="truncate text-sm font-semibold tracking-tight">{title}</span>
|
|
53
|
+
</div>
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
{/* Center: Status */}
|
|
56
|
+
<div className="flex flex-1 items-center justify-center px-4 min-w-0">
|
|
57
|
+
<span className={`truncate text-xs ${error ? 'text-[var(--lokvis-danger)]' : 'text-[var(--lokvis-fg-subtle)]'}`}>
|
|
58
|
+
{statusMessage}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
62
|
+
{/* Right: Actions */}
|
|
63
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
64
|
+
{rightExtra}
|
|
65
|
+
{nodes.length > 0 && (
|
|
66
|
+
<Button
|
|
67
|
+
variant="ghost"
|
|
68
|
+
size="sm"
|
|
69
|
+
onClick={clearWorkflow}
|
|
70
|
+
disabled={running}
|
|
71
|
+
>
|
|
72
|
+
Clear
|
|
73
|
+
</Button>
|
|
74
|
+
)}
|
|
75
|
+
<Button
|
|
76
|
+
variant="primary"
|
|
77
|
+
size="sm"
|
|
78
|
+
onClick={handleRun}
|
|
79
|
+
loading={running}
|
|
80
|
+
disabled={!canRun}
|
|
81
|
+
>
|
|
82
|
+
Run
|
|
83
|
+
</Button>
|
|
84
|
+
</div>
|
|
85
|
+
</header>
|
|
86
|
+
);
|
|
87
87
|
}
|