@lokvis/ui-react 0.2.1 → 0.2.2

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 (97) hide show
  1. package/dist/components/AssetPanel.d.ts.map +1 -1
  2. package/dist/components/AssetPanel.js +14 -14
  3. package/dist/components/AssetPanel.js.map +1 -1
  4. package/dist/components/Canvas.d.ts +4 -4
  5. package/dist/components/Canvas.d.ts.map +1 -1
  6. package/dist/components/Canvas.js +8 -8
  7. package/dist/components/Canvas.js.map +1 -1
  8. package/dist/components/CommandPalette.d.ts +7 -7
  9. package/dist/components/CommandPalette.d.ts.map +1 -1
  10. package/dist/components/CommandPalette.js +10 -10
  11. package/dist/components/CommandPalette.js.map +1 -1
  12. package/dist/components/CompareSlider.d.ts +6 -6
  13. package/dist/components/CompareSlider.d.ts.map +1 -1
  14. package/dist/components/CompareSlider.js +7 -7
  15. package/dist/components/CompareSlider.js.map +1 -1
  16. package/dist/components/DownloadPanel.d.ts +3 -3
  17. package/dist/components/DownloadPanel.d.ts.map +1 -1
  18. package/dist/components/DownloadPanel.js +9 -9
  19. package/dist/components/DownloadPanel.js.map +1 -1
  20. package/dist/components/ErrorBanner.d.ts +2 -2
  21. package/dist/components/ErrorBanner.d.ts.map +1 -1
  22. package/dist/components/ErrorBanner.js +3 -3
  23. package/dist/components/ErrorBanner.js.map +1 -1
  24. package/dist/components/ExifPanel.d.ts +1 -1
  25. package/dist/components/ExifPanel.d.ts.map +1 -1
  26. package/dist/components/ExifPanel.js +2 -2
  27. package/dist/components/ExifPanel.js.map +1 -1
  28. package/dist/components/GlobalDropzone.d.ts +5 -5
  29. package/dist/components/GlobalDropzone.d.ts.map +1 -1
  30. package/dist/components/GlobalDropzone.js +6 -6
  31. package/dist/components/GlobalDropzone.js.map +1 -1
  32. package/dist/components/HistoryPanel.d.ts +2 -2
  33. package/dist/components/HistoryPanel.d.ts.map +1 -1
  34. package/dist/components/HistoryPanel.js +23 -23
  35. package/dist/components/HistoryPanel.js.map +1 -1
  36. package/dist/components/Inspector.d.ts.map +1 -1
  37. package/dist/components/Inspector.js +6 -6
  38. package/dist/components/Inspector.js.map +1 -1
  39. package/dist/components/ParamForm.d.ts.map +1 -1
  40. package/dist/components/ParamForm.js +7 -7
  41. package/dist/components/ParamForm.js.map +1 -1
  42. package/dist/components/PipelineBar.d.ts.map +1 -1
  43. package/dist/components/PipelineBar.js +13 -13
  44. package/dist/components/PipelineBar.js.map +1 -1
  45. package/dist/components/ProgressBar.d.ts +4 -4
  46. package/dist/components/ProgressBar.d.ts.map +1 -1
  47. package/dist/components/ProgressBar.js +8 -8
  48. package/dist/components/ProgressBar.js.map +1 -1
  49. package/dist/components/StatusBar.d.ts +3 -3
  50. package/dist/components/StatusBar.d.ts.map +1 -1
  51. package/dist/components/StatusBar.js +11 -11
  52. package/dist/components/StatusBar.js.map +1 -1
  53. package/dist/components/ThemeToggle.d.ts +4 -4
  54. package/dist/components/ThemeToggle.d.ts.map +1 -1
  55. package/dist/components/ThemeToggle.js +7 -7
  56. package/dist/components/ThemeToggle.js.map +1 -1
  57. package/dist/components/Toolbar.d.ts.map +1 -1
  58. package/dist/components/Toolbar.js +2 -2
  59. package/dist/components/Toolbar.js.map +1 -1
  60. package/dist/components/WorkflowEditor.d.ts +6 -6
  61. package/dist/components/WorkflowEditor.d.ts.map +1 -1
  62. package/dist/components/WorkflowEditor.js +25 -25
  63. package/dist/components/WorkflowEditor.js.map +1 -1
  64. package/dist/components/WorkflowTemplates.d.ts +2 -2
  65. package/dist/components/WorkflowTemplates.d.ts.map +1 -1
  66. package/dist/components/WorkflowTemplates.js +3 -3
  67. package/dist/components/WorkflowTemplates.js.map +1 -1
  68. package/dist/components/Workspace.d.ts +22 -22
  69. package/dist/components/Workspace.d.ts.map +1 -1
  70. package/dist/components/Workspace.js +30 -30
  71. package/dist/components/Workspace.js.map +1 -1
  72. package/dist/index.d.ts +1 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +3 -0
  75. package/dist/index.js.map +1 -1
  76. package/package.json +6 -6
  77. package/src/components/AssetPanel.tsx +263 -263
  78. package/src/components/Canvas.tsx +174 -174
  79. package/src/components/CommandPalette.tsx +242 -242
  80. package/src/components/CompareSlider.tsx +163 -163
  81. package/src/components/DownloadPanel.tsx +203 -203
  82. package/src/components/ErrorBanner.tsx +64 -64
  83. package/src/components/ExifPanel.tsx +154 -154
  84. package/src/components/GlobalDropzone.tsx +170 -170
  85. package/src/components/HistoryPanel.tsx +219 -219
  86. package/src/components/Inspector.tsx +136 -136
  87. package/src/components/ParamForm.tsx +136 -136
  88. package/src/components/PipelineBar.tsx +98 -98
  89. package/src/components/ProgressBar.tsx +78 -78
  90. package/src/components/StatusBar.tsx +148 -148
  91. package/src/components/ThemeToggle.tsx +98 -98
  92. package/src/components/Toolbar.tsx +65 -65
  93. package/src/components/WorkflowEditor.tsx +321 -321
  94. package/src/components/WorkflowTemplates.tsx +77 -77
  95. package/src/components/Workspace.tsx +223 -223
  96. package/src/css-modules.d.ts +3 -0
  97. package/src/index.ts +4 -0
@@ -2,9 +2,9 @@
2
2
  * DownloadPanel - 工作流输出下载面板(W9.5)
3
3
  *
4
4
  * 展示上次工作流执行的输出资产,提供:
5
- * - 单项下载(从 runtime.exportAsset 取 blob,触发浏览器下载)
6
- * - 全部下载(逐项触发,BATCH_DOWNLOAD_DELAY_MS 间隔,避免浏览器拦截;长期方案见该常量注释)
7
- * - 清空输出列表
5
+ * - 单项下载(从 runtime.exportAsset 取 blob,触发浏览器下载)
6
+ * - 全部下载(逐项触发,BATCH_DOWNLOAD_DELAY_MS 间隔,避免浏览器拦截;长期方案见该常量注释)
7
+ * - 清空输出列表
8
8
  *
9
9
  * 与 apps/playground 的 DownloadManager(独立工具页)不同:本组件是 Workspace
10
10
  * 内嵌的轻量面板,数据来自 store.lastOutputIds(由 workflow-slice.run 写入)。
@@ -18,14 +18,14 @@ import { Icon } from '@lokvis/ui-core';
18
18
  import { useWorkspaceStore } from '../store/index.js';
19
19
 
20
20
  export interface DownloadPanelProps {
21
- className?: string;
21
+ className?: string;
22
22
  }
23
23
 
24
24
  /** 格式化字节数 */
25
25
  function formatBytes(bytes: number): string {
26
- if (bytes < 1024) return `${bytes} B`;
27
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
28
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
26
+ if (bytes < 1024) return `${bytes} B`;
27
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
28
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
29
29
  }
30
30
 
31
31
  /**
@@ -46,207 +46,207 @@ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms
46
46
 
47
47
  /** 从扩展名或 mime 推断下载扩展名 */
48
48
  function extFromMime(mime: string): string {
49
- const sub = mime.split('/')[1] ?? 'bin';
50
- // image/svg+xml → svg+xml → svg(取 + 前部分)
51
- return sub.split('+')[0] ?? sub;
49
+ const sub = mime.split('/')[1] ?? 'bin';
50
+ // image/svg+xml → svg+xml → svg(取 + 前部分)
51
+ return sub.split('+')[0] ?? sub;
52
52
  }
53
53
 
54
54
  /** 触发浏览器下载 */
55
55
  function downloadBlob(blob: Blob, filename: string): void {
56
- const url = URL.createObjectURL(blob);
57
- const a = document.createElement('a');
58
- a.href = url;
59
- a.download = filename;
60
- document.body.appendChild(a);
61
- a.click();
62
- document.body.removeChild(a);
63
- setTimeout(() => URL.revokeObjectURL(url), 1000);
56
+ const url = URL.createObjectURL(blob);
57
+ const a = document.createElement('a');
58
+ a.href = url;
59
+ a.download = filename;
60
+ document.body.appendChild(a);
61
+ a.click();
62
+ document.body.removeChild(a);
63
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
64
64
  }
65
65
 
66
66
  export function DownloadPanel({ className = '' }: DownloadPanelProps) {
67
- const assets = useWorkspaceStore((s) => s.assets);
68
- const lastOutputIds = useWorkspaceStore((s) => s.lastOutputIds);
69
- const runtime = useWorkspaceStore((s) => s.runtime);
70
- const clearOutputs = useWorkspaceStore((s) => s.clearOutputs);
71
- const setStatus = useWorkspaceStore((s) => s.setStatus);
72
-
73
- const [downloaded, setDownloaded] = React.useState<Set<string>>(new Set());
74
- const [batchDownloading, setBatchDownloading] = React.useState(false);
75
- const [error, setError] = React.useState<string | null>(null);
76
-
77
- // 卸载标志:批量下载是异步循环,卸载后不应再 setState
78
- const mountedRef = React.useRef(true);
79
- React.useEffect(() => {
80
- mountedRef.current = true;
81
- return () => {
82
- mountedRef.current = false;
83
- };
84
- }, []);
85
-
86
- // 输出资产列表(从 store.assets 中按 lastOutputIds 顺序取)
87
- const outputAssets = React.useMemo(() => {
88
- return lastOutputIds
89
- .map((id) => assets.find((a) => a.id === id))
90
- .filter((a): a is NonNullable<typeof a> => a !== undefined);
91
- }, [lastOutputIds, assets]);
92
-
93
- // outputs 变化时清空 downloaded 状态
94
- React.useEffect(() => {
95
- setDownloaded(new Set());
96
- setError(null);
97
- }, [lastOutputIds]);
98
-
99
- const totalSize = outputAssets.reduce((sum, a) => sum + a.metadata.size, 0);
100
- const downloadedCount = outputAssets.filter((a) => downloaded.has(a.id)).length;
101
- const allDownloaded = outputAssets.length > 0 && downloadedCount === outputAssets.length;
102
-
103
- async function handleDownloadOne(id: string) {
104
- if (!runtime) return;
105
- const asset = outputAssets.find((a) => a.id === id);
106
- if (!asset) return;
107
- try {
108
- const blob = await runtime.exportAsset(id);
109
- const ext = extFromMime(asset.metadata.mimeType);
110
- const filename = `lokvis-output-${asset.id.slice(0, 8)}.${ext}`;
111
- downloadBlob(blob, filename);
112
- if (mountedRef.current) {
113
- setDownloaded((prev) => new Set(prev).add(id));
114
- }
115
- } catch (err) {
116
- if (mountedRef.current) {
117
- setError(err instanceof Error ? err.message : String(err));
118
- }
119
- }
120
- }
121
-
122
- async function handleDownloadAll() {
123
- if (!runtime || batchDownloading) return;
124
- setBatchDownloading(true);
125
- setError(null);
126
- setStatus('Downloading outputs...');
127
- // 用本地 Set 跟踪本次循环已下载 id,避免 downloaded 闭包快照与 state 不同步
128
- // (review 反馈:循环中 setDownloaded 更新 state 但闭包 downloaded 不变,
129
- // 虽有 batchDownloading guard 阻止并发,逻辑上仍应读取最新已下载集合)
130
- const localDownloaded = new Set(downloaded);
131
- try {
132
- for (const asset of outputAssets) {
133
- // 卸载后立即停止循环(避免对已卸载组件 setState)
134
- if (!mountedRef.current) return;
135
- if (localDownloaded.has(asset.id)) continue;
136
- try {
137
- const blob = await runtime.exportAsset(asset.id);
138
- const ext = extFromMime(asset.metadata.mimeType);
139
- const filename = `lokvis-output-${asset.id.slice(0, 8)}.${ext}`;
140
- downloadBlob(blob, filename);
141
- localDownloaded.add(asset.id);
142
- if (mountedRef.current) {
143
- setDownloaded((prev) => new Set(prev).add(asset.id));
144
- }
145
- await sleep(BATCH_DOWNLOAD_DELAY_MS);
146
- } catch (err) {
147
- if (mountedRef.current) {
148
- setError(err instanceof Error ? err.message : String(err));
149
- }
150
- // 单项失败不阻断后续
151
- }
152
- }
153
- if (mountedRef.current) {
154
- setStatus('Downloads complete');
155
- }
156
- } finally {
157
- if (mountedRef.current) {
158
- setBatchDownloading(false);
159
- }
160
- }
161
- }
162
-
163
- function handleClear() {
164
- clearOutputs();
165
- setDownloaded(new Set());
166
- }
167
-
168
- if (outputAssets.length === 0) return null;
169
-
170
- return (
171
- <div
172
- className={`flex flex-col border-t border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900 ${className}`}
173
- aria-label="Outputs panel"
174
- >
175
- <div className="flex items-center justify-between px-3 h-9 shrink-0 border-b border-zinc-200 dark:border-zinc-800">
176
- <span className="text-[11px] font-semibold uppercase tracking-wider text-zinc-400">
177
- Outputs
178
- </span>
179
- <span className="text-[11px] tabular-nums text-zinc-400">
180
- {downloadedCount}/{outputAssets.length} · {formatBytes(totalSize)}
181
- </span>
182
- </div>
183
-
184
- <div className="flex items-center gap-2 px-3 py-2 overflow-x-auto">
185
- {outputAssets.map((asset) => {
186
- const isDownloaded = downloaded.has(asset.id);
187
- const dims = asset.metadata.dimensions;
188
- return (
189
- <div
190
- key={asset.id}
191
- className="flex shrink-0 flex-col gap-1 rounded-md border border-zinc-200 p-2 dark:border-zinc-700"
192
- >
193
- <div className="flex items-center gap-2">
194
- <span className="rounded bg-zinc-100 px-1 py-0.5 text-[9px] font-medium text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
195
- {asset.metadata.format.toUpperCase()}
196
- </span>
197
- {dims && (
198
- <span className="text-[10px] text-zinc-500 dark:text-zinc-400 tabular-nums">
199
- {dims.width}×{dims.height}
200
- </span>
201
- )}
202
- <span className="text-[10px] text-zinc-400 tabular-nums">
203
- {formatBytes(asset.metadata.size)}
204
- </span>
205
- </div>
206
- <div className="flex items-center gap-1">
207
- <button
208
- type="button"
209
- onClick={() => void handleDownloadOne(asset.id)}
210
- className={`flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-medium transition-colors ${
211
- isDownloaded
212
- ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400'
213
- : 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-950/30 dark:text-indigo-300 dark:hover:bg-indigo-900/40'
214
- }`}
215
- >
216
- <Icon size={10} strokeWidth={2}>
217
- <path d="M12 3v12m0 0l-4-4m4 4l4-4M3 17v2a2 2 0 002 2h14a2 2 0 002-2v-2" />
218
- </Icon>
219
- {isDownloaded ? 'Downloaded' : 'Download'}
220
- </button>
221
- </div>
222
- </div>
223
- );
224
- })}
225
-
226
- <div className="flex shrink-0 items-center gap-1">
227
- <button
228
- type="button"
229
- onClick={() => void handleDownloadAll()}
230
- disabled={batchDownloading || allDownloaded}
231
- className="rounded-md bg-indigo-600 px-2.5 py-1 text-[10px] font-medium text-white hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
232
- >
233
- {batchDownloading ? 'Downloading...' : 'Download All'}
234
- </button>
235
- <button
236
- type="button"
237
- onClick={handleClear}
238
- className="rounded-md px-2 py-1 text-[10px] text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
239
- >
240
- Clear
241
- </button>
242
- </div>
243
- </div>
244
-
245
- {error && (
246
- <p className="px-3 pb-2 text-[10px] text-red-500" role="alert">
247
- {error}
248
- </p>
249
- )}
250
- </div>
251
- );
67
+ const assets = useWorkspaceStore((s) => s.assets);
68
+ const lastOutputIds = useWorkspaceStore((s) => s.lastOutputIds);
69
+ const runtime = useWorkspaceStore((s) => s.runtime);
70
+ const clearOutputs = useWorkspaceStore((s) => s.clearOutputs);
71
+ const setStatus = useWorkspaceStore((s) => s.setStatus);
72
+
73
+ const [downloaded, setDownloaded] = React.useState<Set<string>>(new Set());
74
+ const [batchDownloading, setBatchDownloading] = React.useState(false);
75
+ const [error, setError] = React.useState<string | null>(null);
76
+
77
+ // 卸载标志:批量下载是异步循环,卸载后不应再 setState
78
+ const mountedRef = React.useRef(true);
79
+ React.useEffect(() => {
80
+ mountedRef.current = true;
81
+ return () => {
82
+ mountedRef.current = false;
83
+ };
84
+ }, []);
85
+
86
+ // 输出资产列表(从 store.assets 中按 lastOutputIds 顺序取)
87
+ const outputAssets = React.useMemo(() => {
88
+ return lastOutputIds
89
+ .map((id) => assets.find((a) => a.id === id))
90
+ .filter((a): a is NonNullable<typeof a> => a !== undefined);
91
+ }, [lastOutputIds, assets]);
92
+
93
+ // outputs 变化时清空 downloaded 状态
94
+ React.useEffect(() => {
95
+ setDownloaded(new Set());
96
+ setError(null);
97
+ }, [lastOutputIds]);
98
+
99
+ const totalSize = outputAssets.reduce((sum, a) => sum + a.metadata.size, 0);
100
+ const downloadedCount = outputAssets.filter((a) => downloaded.has(a.id)).length;
101
+ const allDownloaded = outputAssets.length > 0 && downloadedCount === outputAssets.length;
102
+
103
+ async function handleDownloadOne(id: string) {
104
+ if (!runtime) return;
105
+ const asset = outputAssets.find((a) => a.id === id);
106
+ if (!asset) return;
107
+ try {
108
+ const blob = await runtime.exportAsset(id);
109
+ const ext = extFromMime(asset.metadata.mimeType);
110
+ const filename = `lokvis-output-${asset.id.slice(0, 8)}.${ext}`;
111
+ downloadBlob(blob, filename);
112
+ if (mountedRef.current) {
113
+ setDownloaded((prev) => new Set(prev).add(id));
114
+ }
115
+ } catch (err) {
116
+ if (mountedRef.current) {
117
+ setError(err instanceof Error ? err.message : String(err));
118
+ }
119
+ }
120
+ }
121
+
122
+ async function handleDownloadAll() {
123
+ if (!runtime || batchDownloading) return;
124
+ setBatchDownloading(true);
125
+ setError(null);
126
+ setStatus('Downloading outputs...');
127
+ // 用本地 Set 跟踪本次循环已下载 id,避免 downloaded 闭包快照与 state 不同步
128
+ // (review 反馈:循环中 setDownloaded 更新 state 但闭包 downloaded 不变,
129
+ // 虽有 batchDownloading guard 阻止并发,逻辑上仍应读取最新已下载集合)
130
+ const localDownloaded = new Set(downloaded);
131
+ try {
132
+ for (const asset of outputAssets) {
133
+ // 卸载后立即停止循环(避免对已卸载组件 setState)
134
+ if (!mountedRef.current) return;
135
+ if (localDownloaded.has(asset.id)) continue;
136
+ try {
137
+ const blob = await runtime.exportAsset(asset.id);
138
+ const ext = extFromMime(asset.metadata.mimeType);
139
+ const filename = `lokvis-output-${asset.id.slice(0, 8)}.${ext}`;
140
+ downloadBlob(blob, filename);
141
+ localDownloaded.add(asset.id);
142
+ if (mountedRef.current) {
143
+ setDownloaded((prev) => new Set(prev).add(asset.id));
144
+ }
145
+ await sleep(BATCH_DOWNLOAD_DELAY_MS);
146
+ } catch (err) {
147
+ if (mountedRef.current) {
148
+ setError(err instanceof Error ? err.message : String(err));
149
+ }
150
+ // 单项失败不阻断后续
151
+ }
152
+ }
153
+ if (mountedRef.current) {
154
+ setStatus('Downloads complete');
155
+ }
156
+ } finally {
157
+ if (mountedRef.current) {
158
+ setBatchDownloading(false);
159
+ }
160
+ }
161
+ }
162
+
163
+ function handleClear() {
164
+ clearOutputs();
165
+ setDownloaded(new Set());
166
+ }
167
+
168
+ if (outputAssets.length === 0) return null;
169
+
170
+ return (
171
+ <div
172
+ className={`flex flex-col border-t border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] ${className}`}
173
+ aria-label="Outputs panel"
174
+ >
175
+ <div className="flex items-center justify-between px-3 h-9 shrink-0 border-b border-[var(--lokvis-border)]">
176
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
177
+ Outputs
178
+ </span>
179
+ <span className="text-[11px] tabular-nums text-[var(--lokvis-fg-subtle)]">
180
+ {downloadedCount}/{outputAssets.length} · {formatBytes(totalSize)}
181
+ </span>
182
+ </div>
183
+
184
+ <div className="flex items-center gap-2 px-3 py-2 overflow-x-auto">
185
+ {outputAssets.map((asset) => {
186
+ const isDownloaded = downloaded.has(asset.id);
187
+ const dims = asset.metadata.dimensions;
188
+ return (
189
+ <div
190
+ key={asset.id}
191
+ className="flex shrink-0 flex-col gap-1 rounded-md border border-[var(--lokvis-border)] p-2"
192
+ >
193
+ <div className="flex items-center gap-2">
194
+ <span className="rounded bg-[var(--lokvis-surface-muted)] px-1 py-0.5 text-[9px] font-medium text-[var(--lokvis-fg-muted)]">
195
+ {asset.metadata.format.toUpperCase()}
196
+ </span>
197
+ {dims && (
198
+ <span className="text-[10px] text-[var(--lokvis-fg-muted)] tabular-nums">
199
+ {dims.width}×{dims.height}
200
+ </span>
201
+ )}
202
+ <span className="text-[10px] text-[var(--lokvis-fg-subtle)] tabular-nums">
203
+ {formatBytes(asset.metadata.size)}
204
+ </span>
205
+ </div>
206
+ <div className="flex items-center gap-1">
207
+ <button
208
+ type="button"
209
+ onClick={() => void handleDownloadOne(asset.id)}
210
+ className={`flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-medium transition-colors ${
211
+ isDownloaded
212
+ ? 'bg-[var(--lokvis-success)]/15 text-[var(--lokvis-success)]'
213
+ : 'bg-[var(--lokvis-primary)]/15 text-[var(--lokvis-primary)] hover:bg-[var(--lokvis-primary)]/25'
214
+ }`}
215
+ >
216
+ <Icon size={10} strokeWidth={2}>
217
+ <path d="M12 3v12m0 0l-4-4m4 4l4-4M3 17v2a2 2 0 002 2h14a2 2 0 002-2v-2" />
218
+ </Icon>
219
+ {isDownloaded ? 'Downloaded' : 'Download'}
220
+ </button>
221
+ </div>
222
+ </div>
223
+ );
224
+ })}
225
+
226
+ <div className="flex shrink-0 items-center gap-1">
227
+ <button
228
+ type="button"
229
+ onClick={() => void handleDownloadAll()}
230
+ disabled={batchDownloading || allDownloaded}
231
+ className="rounded-md bg-[var(--lokvis-primary-hover)] px-2.5 py-1 text-[10px] font-medium text-[var(--lokvis-primary-fg)] hover:bg-[var(--lokvis-primary)] disabled:cursor-not-allowed disabled:opacity-50"
232
+ >
233
+ {batchDownloading ? 'Downloading...' : 'Download All'}
234
+ </button>
235
+ <button
236
+ type="button"
237
+ onClick={handleClear}
238
+ className="rounded-md px-2 py-1 text-[10px] text-[var(--lokvis-fg-muted)] hover:bg-[var(--lokvis-surface-muted)] hover:text-[var(--lokvis-fg-muted)]"
239
+ >
240
+ Clear
241
+ </button>
242
+ </div>
243
+ </div>
244
+
245
+ {error && (
246
+ <p className="px-3 pb-2 text-[10px] text-[var(--lokvis-danger)]" role="alert">
247
+ {error}
248
+ </p>
249
+ )}
250
+ </div>
251
+ );
252
252
  }
@@ -2,8 +2,8 @@
2
2
  * ErrorBanner - 错误信息横幅(W11.3)
3
3
  *
4
4
  * 当 store.error 非空时显示,展示错误信息并提供:
5
- * - 关闭按钮(清除 error)
6
- * - 重试按钮(重新执行工作流)
5
+ * - 关闭按钮(清除 error)
6
+ * - 重试按钮(重新执行工作流)
7
7
  *
8
8
  * 节点失败高亮由 WorkflowEditor 的节点状态颜色处理(已实现);
9
9
  * 本组件负责全局错误信息的可见性。
@@ -20,76 +20,76 @@ import { Icon } from '@lokvis/ui-core';
20
20
  import { useWorkspaceStore } from '../store/index.js';
21
21
 
22
22
  export interface ErrorBannerProps {
23
- className?: string;
23
+ className?: string;
24
24
  }
25
25
 
26
26
  export function ErrorBanner({ className = '' }: ErrorBannerProps) {
27
- const error = useWorkspaceStore((s) => s.error);
28
- const errorSeq = useWorkspaceStore((s) => s.errorSeq);
29
- const setError = useWorkspaceStore((s) => s.setError);
30
- const run = useWorkspaceStore((s) => s.run);
31
- const nodes = useWorkspaceStore((s) => s.nodes);
32
- const running = useWorkspaceStore((s) => s.running);
27
+ const error = useWorkspaceStore((s) => s.error);
28
+ const errorSeq = useWorkspaceStore((s) => s.errorSeq);
29
+ const setError = useWorkspaceStore((s) => s.setError);
30
+ const run = useWorkspaceStore((s) => s.run);
31
+ const nodes = useWorkspaceStore((s) => s.nodes);
32
+ const running = useWorkspaceStore((s) => s.running);
33
33
 
34
- // 用户关闭后隐藏;新错误(errorSeq 变化)时重新显示。
35
- // 用 errorSeq 而非 error 字符串本身:store.run() 在 setError(null) 之后才设新 error,
36
- // errorSeq 在 setError(非 null) 时递增,即使错误消息相同也能感知到"新的错误事件"。
37
- const [dismissedSeq, setDismissedSeq] = React.useState<number | null>(null);
34
+ // 用户关闭后隐藏;新错误(errorSeq 变化)时重新显示。
35
+ // 用 errorSeq 而非 error 字符串本身:store.run() 在 setError(null) 之后才设新 error,
36
+ // errorSeq 在 setError(非 null) 时递增,即使错误消息相同也能感知到"新的错误事件"。
37
+ const [dismissedSeq, setDismissedSeq] = React.useState<number | null>(null);
38
38
 
39
- const visible = error !== null && dismissedSeq !== errorSeq;
39
+ const visible = error !== null && dismissedSeq !== errorSeq;
40
40
 
41
- const handleDismiss = React.useCallback(() => {
42
- setDismissedSeq(errorSeq);
43
- setError(null);
44
- }, [errorSeq, setError]);
41
+ const handleDismiss = React.useCallback(() => {
42
+ setDismissedSeq(errorSeq);
43
+ setError(null);
44
+ }, [errorSeq, setError]);
45
45
 
46
- const handleRetry = React.useCallback(() => {
47
- // 先清 error 再 run,避免 run 同步抛错时 banner 已隐藏且 errorSeq 未变化
48
- setError(null);
49
- void run();
50
- }, [setError, run]);
46
+ const handleRetry = React.useCallback(() => {
47
+ // 先清 error 再 run,避免 run 同步抛错时 banner 已隐藏且 errorSeq 未变化
48
+ setError(null);
49
+ void run();
50
+ }, [setError, run]);
51
51
 
52
- if (!visible || !error) return null;
52
+ if (!visible || !error) return null;
53
53
 
54
- const canRetry = nodes.length > 0 && !running;
54
+ const canRetry = nodes.length > 0 && !running;
55
55
 
56
- return (
57
- <div
58
- className={`flex items-start gap-2 border-b border-red-200 bg-red-50 px-3 py-2 dark:border-red-800/50 dark:bg-red-950/20 ${className}`}
59
- role="alert"
60
- >
61
- <Icon size={14} className="mt-0.5 shrink-0 text-red-500">
62
- <circle cx="12" cy="12" r="10" />
63
- <line x1="12" y1="8" x2="12" y2="12" />
64
- <line x1="12" y1="16" x2="12.01" y2="16" />
65
- </Icon>
66
- <div className="flex-1 min-w-0">
67
- <p className="text-[11px] font-medium text-red-700 dark:text-red-400">
68
- 执行出错
69
- </p>
70
- <p className="mt-0.5 break-words text-[10px] text-red-600/80 dark:text-red-400/70">
71
- {error}
72
- </p>
73
- </div>
74
- <div className="flex shrink-0 items-center gap-1">
75
- {canRetry && (
76
- <button
77
- type="button"
78
- onClick={handleRetry}
79
- className="rounded bg-red-100 px-2 py-0.5 text-[10px] font-medium text-red-700 transition-colors hover:bg-red-200 dark:bg-red-900/40 dark:text-red-400 dark:hover:bg-red-900/60"
80
- >
81
- 重试
82
- </button>
83
- )}
84
- <button
85
- type="button"
86
- onClick={handleDismiss}
87
- aria-label="关闭错误信息"
88
- className="rounded p-0.5 text-red-400 transition-colors hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/40"
89
- >
90
- <Icon size={12} strokeWidth={3}><path d="M6 18L18 6M6 6l12 12" /></Icon>
91
- </button>
92
- </div>
93
- </div>
94
- );
56
+ return (
57
+ <div
58
+ className={`flex items-start gap-2 border-b border-[var(--lokvis-danger)]/30 bg-[var(--lokvis-danger)]/10 px-3 py-2 ${className}`}
59
+ role="alert"
60
+ >
61
+ <Icon size={14} className="mt-0.5 shrink-0 text-[var(--lokvis-danger)]">
62
+ <circle cx="12" cy="12" r="10" />
63
+ <line x1="12" y1="8" x2="12" y2="12" />
64
+ <line x1="12" y1="16" x2="12.01" y2="16" />
65
+ </Icon>
66
+ <div className="flex-1 min-w-0">
67
+ <p className="text-[11px] font-medium text-[var(--lokvis-danger)]">
68
+ 执行出错
69
+ </p>
70
+ <p className="mt-0.5 break-words text-[10px] text-[var(--lokvis-danger)]/80">
71
+ {error}
72
+ </p>
73
+ </div>
74
+ <div className="flex shrink-0 items-center gap-1">
75
+ {canRetry && (
76
+ <button
77
+ type="button"
78
+ onClick={handleRetry}
79
+ className="rounded bg-[var(--lokvis-danger)]/15 px-2 py-0.5 text-[10px] font-medium text-[var(--lokvis-danger)] transition-colors hover:bg-[var(--lokvis-danger)]/30"
80
+ >
81
+ 重试
82
+ </button>
83
+ )}
84
+ <button
85
+ type="button"
86
+ onClick={handleDismiss}
87
+ aria-label="关闭错误信息"
88
+ className="rounded p-0.5 text-[var(--lokvis-danger)] transition-colors hover:bg-[var(--lokvis-danger)]/15 hover:text-[var(--lokvis-danger)]"
89
+ >
90
+ <Icon size={12} strokeWidth={3}><path d="M6 18L18 6M6 6l12 12" /></Icon>
91
+ </button>
92
+ </div>
93
+ </div>
94
+ );
95
95
  }