@ifc-lite/viewer 1.7.0 → 1.9.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/CHANGELOG.md +88 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
- package/dist/assets/browser-BXNIkE8a.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/index-6Mr3byM-.js +216 -0
- package/dist/assets/index-CGbokkQ9.css +1 -0
- package/dist/assets/index-huvR-kGC.js +98305 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +145 -84
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +25 -5
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +114 -14
- package/src/hooks/useLens.ts +40 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/hooks/useSandbox.ts +113 -0
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +10 -2
- package/src/store/index.ts +28 -2
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -42
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +2 -0
- package/src/store.ts +3 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/index-dgdgiQ9p.js +0 -75456
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ScriptPanel — Code editor + output console for BIM scripting.
|
|
7
|
+
*
|
|
8
|
+
* Uses CodeMirror 6 for the code editor with bim.* autocomplete.
|
|
9
|
+
* Connects to the QuickJS sandbox via useSandbox() and displays results
|
|
10
|
+
* in a log console.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useMemo, useState, memo } from 'react';
|
|
14
|
+
import {
|
|
15
|
+
Play,
|
|
16
|
+
Save,
|
|
17
|
+
Plus,
|
|
18
|
+
Trash2,
|
|
19
|
+
X,
|
|
20
|
+
ChevronDown,
|
|
21
|
+
FileCode2,
|
|
22
|
+
RotateCcw,
|
|
23
|
+
AlertCircle,
|
|
24
|
+
CheckCircle2,
|
|
25
|
+
Info,
|
|
26
|
+
AlertTriangle,
|
|
27
|
+
} from 'lucide-react';
|
|
28
|
+
import { Button } from '@/components/ui/button';
|
|
29
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
30
|
+
import {
|
|
31
|
+
DropdownMenu,
|
|
32
|
+
DropdownMenuContent,
|
|
33
|
+
DropdownMenuItem,
|
|
34
|
+
DropdownMenuSeparator,
|
|
35
|
+
DropdownMenuTrigger,
|
|
36
|
+
} from '@/components/ui/dropdown-menu';
|
|
37
|
+
import {
|
|
38
|
+
Dialog,
|
|
39
|
+
DialogContent,
|
|
40
|
+
DialogHeader,
|
|
41
|
+
DialogFooter,
|
|
42
|
+
DialogTitle,
|
|
43
|
+
DialogDescription,
|
|
44
|
+
} from '@/components/ui/dialog';
|
|
45
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
46
|
+
import { cn, formatDuration } from '@/lib/utils';
|
|
47
|
+
import { useViewerStore } from '@/store';
|
|
48
|
+
import { useSandbox } from '@/hooks/useSandbox';
|
|
49
|
+
import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
|
|
50
|
+
import { CodeEditor } from './CodeEditor';
|
|
51
|
+
import type { LogEntry } from '@/store/slices/scriptSlice';
|
|
52
|
+
|
|
53
|
+
interface ScriptPanelProps {
|
|
54
|
+
onClose?: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Consolidated script state selector — single subscription instead of 14 */
|
|
58
|
+
function useScriptState() {
|
|
59
|
+
const editorContent = useViewerStore((s) => s.scriptEditorContent);
|
|
60
|
+
const setEditorContent = useViewerStore((s) => s.setScriptEditorContent);
|
|
61
|
+
const executionState = useViewerStore((s) => s.scriptExecutionState);
|
|
62
|
+
const lastResult = useViewerStore((s) => s.scriptLastResult);
|
|
63
|
+
const lastError = useViewerStore((s) => s.scriptLastError);
|
|
64
|
+
const savedScripts = useViewerStore((s) => s.savedScripts);
|
|
65
|
+
const activeScriptId = useViewerStore((s) => s.activeScriptId);
|
|
66
|
+
const editorDirty = useViewerStore((s) => s.scriptEditorDirty);
|
|
67
|
+
const createScript = useViewerStore((s) => s.createScript);
|
|
68
|
+
const saveActiveScript = useViewerStore((s) => s.saveActiveScript);
|
|
69
|
+
const deleteScript = useViewerStore((s) => s.deleteScript);
|
|
70
|
+
const setActiveScriptId = useViewerStore((s) => s.setActiveScriptId);
|
|
71
|
+
const deleteConfirmId = useViewerStore((s) => s.scriptDeleteConfirmId);
|
|
72
|
+
const setDeleteConfirmId = useViewerStore((s) => s.setScriptDeleteConfirmId);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
editorContent,
|
|
76
|
+
setEditorContent,
|
|
77
|
+
executionState,
|
|
78
|
+
lastResult,
|
|
79
|
+
lastError,
|
|
80
|
+
savedScripts,
|
|
81
|
+
activeScriptId,
|
|
82
|
+
editorDirty,
|
|
83
|
+
createScript,
|
|
84
|
+
saveActiveScript,
|
|
85
|
+
deleteScript,
|
|
86
|
+
setActiveScriptId,
|
|
87
|
+
deleteConfirmId,
|
|
88
|
+
setDeleteConfirmId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
93
|
+
const {
|
|
94
|
+
editorContent,
|
|
95
|
+
setEditorContent,
|
|
96
|
+
executionState,
|
|
97
|
+
lastResult,
|
|
98
|
+
lastError,
|
|
99
|
+
savedScripts,
|
|
100
|
+
activeScriptId,
|
|
101
|
+
editorDirty,
|
|
102
|
+
createScript,
|
|
103
|
+
saveActiveScript,
|
|
104
|
+
deleteScript,
|
|
105
|
+
setActiveScriptId,
|
|
106
|
+
deleteConfirmId,
|
|
107
|
+
setDeleteConfirmId,
|
|
108
|
+
} = useScriptState();
|
|
109
|
+
|
|
110
|
+
const { execute, reset } = useSandbox();
|
|
111
|
+
const [outputCollapsed, setOutputCollapsed] = useState(false);
|
|
112
|
+
|
|
113
|
+
const activeScript = useMemo(
|
|
114
|
+
() => savedScripts.find((s) => s.id === activeScriptId),
|
|
115
|
+
[savedScripts, activeScriptId],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const deleteConfirmScript = useMemo(
|
|
119
|
+
() => (deleteConfirmId ? savedScripts.find((s) => s.id === deleteConfirmId) : null),
|
|
120
|
+
[savedScripts, deleteConfirmId],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const handleRun = useCallback(async () => {
|
|
124
|
+
if (executionState === 'running') return;
|
|
125
|
+
await execute(editorContent);
|
|
126
|
+
}, [execute, editorContent, executionState]);
|
|
127
|
+
|
|
128
|
+
const handleSave = useCallback(() => {
|
|
129
|
+
if (activeScriptId) {
|
|
130
|
+
saveActiveScript();
|
|
131
|
+
} else {
|
|
132
|
+
createScript('Untitled Script');
|
|
133
|
+
}
|
|
134
|
+
}, [activeScriptId, saveActiveScript, createScript]);
|
|
135
|
+
|
|
136
|
+
const handleNew = useCallback((name: string, code?: string) => {
|
|
137
|
+
createScript(name, code);
|
|
138
|
+
}, [createScript]);
|
|
139
|
+
|
|
140
|
+
const handleDeleteConfirm = useCallback(() => {
|
|
141
|
+
if (deleteConfirmId) {
|
|
142
|
+
deleteScript(deleteConfirmId);
|
|
143
|
+
}
|
|
144
|
+
}, [deleteConfirmId, deleteScript]);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="h-full flex flex-col bg-background">
|
|
148
|
+
{/* Header */}
|
|
149
|
+
<div className="flex items-center gap-1 px-2 py-1.5 border-b shrink-0">
|
|
150
|
+
<FileCode2 className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
151
|
+
<span className="text-sm font-medium truncate">
|
|
152
|
+
{activeScript ? activeScript.name : 'Script Editor'}
|
|
153
|
+
{editorDirty && <span className="text-muted-foreground ml-1">*</span>}
|
|
154
|
+
</span>
|
|
155
|
+
<div className="flex-1" />
|
|
156
|
+
|
|
157
|
+
{/* Script selector dropdown */}
|
|
158
|
+
{savedScripts.length > 0 && (
|
|
159
|
+
<DropdownMenu>
|
|
160
|
+
<DropdownMenuTrigger asChild>
|
|
161
|
+
<Button variant="ghost" size="icon-xs">
|
|
162
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
163
|
+
</Button>
|
|
164
|
+
</DropdownMenuTrigger>
|
|
165
|
+
<DropdownMenuContent align="end">
|
|
166
|
+
{savedScripts.map((s) => (
|
|
167
|
+
<DropdownMenuItem
|
|
168
|
+
key={s.id}
|
|
169
|
+
onClick={() => setActiveScriptId(s.id)}
|
|
170
|
+
className={cn(s.id === activeScriptId && 'bg-accent')}
|
|
171
|
+
>
|
|
172
|
+
<FileCode2 className="h-3.5 w-3.5 mr-2" />
|
|
173
|
+
{s.name}
|
|
174
|
+
</DropdownMenuItem>
|
|
175
|
+
))}
|
|
176
|
+
<DropdownMenuSeparator />
|
|
177
|
+
{activeScriptId && (
|
|
178
|
+
<DropdownMenuItem
|
|
179
|
+
onClick={() => setDeleteConfirmId(activeScriptId)}
|
|
180
|
+
className="text-destructive"
|
|
181
|
+
>
|
|
182
|
+
<Trash2 className="h-3.5 w-3.5 mr-2" />
|
|
183
|
+
Delete
|
|
184
|
+
</DropdownMenuItem>
|
|
185
|
+
)}
|
|
186
|
+
</DropdownMenuContent>
|
|
187
|
+
</DropdownMenu>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{onClose && (
|
|
191
|
+
<Button variant="ghost" size="icon-xs" onClick={onClose}>
|
|
192
|
+
<X className="h-3.5 w-3.5" />
|
|
193
|
+
</Button>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Toolbar */}
|
|
198
|
+
<div className="flex items-center gap-1 px-2 py-1 border-b shrink-0">
|
|
199
|
+
<Tooltip>
|
|
200
|
+
<TooltipTrigger asChild>
|
|
201
|
+
<Button
|
|
202
|
+
variant="default"
|
|
203
|
+
size="sm"
|
|
204
|
+
onClick={handleRun}
|
|
205
|
+
disabled={executionState === 'running'}
|
|
206
|
+
className="gap-1"
|
|
207
|
+
>
|
|
208
|
+
<Play className="h-3.5 w-3.5" />
|
|
209
|
+
Run
|
|
210
|
+
</Button>
|
|
211
|
+
</TooltipTrigger>
|
|
212
|
+
<TooltipContent>Run script (Ctrl+Enter)</TooltipContent>
|
|
213
|
+
</Tooltip>
|
|
214
|
+
|
|
215
|
+
<Tooltip>
|
|
216
|
+
<TooltipTrigger asChild>
|
|
217
|
+
<Button variant="ghost" size="icon-xs" onClick={handleSave}>
|
|
218
|
+
<Save className="h-3.5 w-3.5" />
|
|
219
|
+
</Button>
|
|
220
|
+
</TooltipTrigger>
|
|
221
|
+
<TooltipContent>Save (Ctrl+S)</TooltipContent>
|
|
222
|
+
</Tooltip>
|
|
223
|
+
|
|
224
|
+
{/* New script dropdown with templates */}
|
|
225
|
+
<DropdownMenu>
|
|
226
|
+
<Tooltip>
|
|
227
|
+
<TooltipTrigger asChild>
|
|
228
|
+
<DropdownMenuTrigger asChild>
|
|
229
|
+
<Button variant="ghost" size="icon-xs">
|
|
230
|
+
<Plus className="h-3.5 w-3.5" />
|
|
231
|
+
</Button>
|
|
232
|
+
</DropdownMenuTrigger>
|
|
233
|
+
</TooltipTrigger>
|
|
234
|
+
<TooltipContent>New script</TooltipContent>
|
|
235
|
+
</Tooltip>
|
|
236
|
+
<DropdownMenuContent align="start">
|
|
237
|
+
<DropdownMenuItem onClick={() => handleNew('Untitled Script')}>
|
|
238
|
+
<FileCode2 className="h-3.5 w-3.5 mr-2" />
|
|
239
|
+
Blank Script
|
|
240
|
+
</DropdownMenuItem>
|
|
241
|
+
<DropdownMenuSeparator />
|
|
242
|
+
{SCRIPT_TEMPLATES.map((t) => (
|
|
243
|
+
<DropdownMenuItem key={t.name} onClick={() => handleNew(t.name, t.code)}>
|
|
244
|
+
<FileCode2 className="h-3.5 w-3.5 mr-2" />
|
|
245
|
+
{t.name}
|
|
246
|
+
</DropdownMenuItem>
|
|
247
|
+
))}
|
|
248
|
+
</DropdownMenuContent>
|
|
249
|
+
</DropdownMenu>
|
|
250
|
+
|
|
251
|
+
<Tooltip>
|
|
252
|
+
<TooltipTrigger asChild>
|
|
253
|
+
<Button variant="ghost" size="icon-xs" onClick={reset}>
|
|
254
|
+
<RotateCcw className="h-3.5 w-3.5" />
|
|
255
|
+
</Button>
|
|
256
|
+
</TooltipTrigger>
|
|
257
|
+
<TooltipContent>Reset sandbox</TooltipContent>
|
|
258
|
+
</Tooltip>
|
|
259
|
+
|
|
260
|
+
{/* Status indicator */}
|
|
261
|
+
<div className="flex-1" />
|
|
262
|
+
{executionState === 'running' && (
|
|
263
|
+
<span className="text-xs text-muted-foreground animate-pulse">Running...</span>
|
|
264
|
+
)}
|
|
265
|
+
{executionState === 'success' && lastResult && (
|
|
266
|
+
<span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
|
|
267
|
+
<CheckCircle2 className="h-3 w-3" />
|
|
268
|
+
{formatDuration(lastResult.durationMs)}
|
|
269
|
+
</span>
|
|
270
|
+
)}
|
|
271
|
+
{executionState === 'error' && (
|
|
272
|
+
<span className="text-xs text-destructive flex items-center gap-1">
|
|
273
|
+
<AlertCircle className="h-3 w-3" />
|
|
274
|
+
Error
|
|
275
|
+
</span>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{/* Code Editor */}
|
|
280
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
281
|
+
<CodeEditor
|
|
282
|
+
value={editorContent}
|
|
283
|
+
onChange={setEditorContent}
|
|
284
|
+
onRun={handleRun}
|
|
285
|
+
onSave={handleSave}
|
|
286
|
+
className="h-full"
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Output Console */}
|
|
291
|
+
<div className="shrink-0 border-t">
|
|
292
|
+
{/* Output header */}
|
|
293
|
+
<button
|
|
294
|
+
className="flex items-center gap-1.5 px-2 py-1 w-full hover:bg-muted/50 transition-colors text-left"
|
|
295
|
+
onClick={() => setOutputCollapsed(!outputCollapsed)}
|
|
296
|
+
>
|
|
297
|
+
<ChevronDown
|
|
298
|
+
className={cn('h-3 w-3 transition-transform', outputCollapsed && '-rotate-90')}
|
|
299
|
+
/>
|
|
300
|
+
<span className="text-xs font-medium text-muted-foreground">Output</span>
|
|
301
|
+
{lastResult && lastResult.logs.length > 0 && (
|
|
302
|
+
<span className="text-xs text-muted-foreground">({lastResult.logs.length})</span>
|
|
303
|
+
)}
|
|
304
|
+
</button>
|
|
305
|
+
|
|
306
|
+
{!outputCollapsed && (
|
|
307
|
+
<ScrollArea className="h-[140px]">
|
|
308
|
+
<div className="px-2 pb-2 font-mono text-xs space-y-0.5">
|
|
309
|
+
{/* Error message */}
|
|
310
|
+
{lastError && (
|
|
311
|
+
<div className="flex items-start gap-1.5 text-destructive">
|
|
312
|
+
<AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
|
|
313
|
+
<span className="whitespace-pre-wrap break-all">{lastError}</span>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{/* Log entries */}
|
|
318
|
+
{lastResult?.logs.map((log, i) => (
|
|
319
|
+
<MemoizedLogLine key={i} log={log} />
|
|
320
|
+
))}
|
|
321
|
+
|
|
322
|
+
{/* Return value */}
|
|
323
|
+
{lastResult && lastResult.value !== undefined && lastResult.value !== null && (
|
|
324
|
+
<div className="text-muted-foreground mt-1 pt-1 border-t border-border/50">
|
|
325
|
+
<span className="opacity-60">Return: </span>
|
|
326
|
+
<span className="text-foreground">
|
|
327
|
+
{typeof lastResult.value === 'object'
|
|
328
|
+
? JSON.stringify(lastResult.value, null, 2)
|
|
329
|
+
: String(lastResult.value)}
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{/* Empty state */}
|
|
335
|
+
{!lastError && !lastResult && (
|
|
336
|
+
<div className="text-muted-foreground py-2 text-center">
|
|
337
|
+
Press Run or Ctrl+Enter to execute
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
</ScrollArea>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{/* Delete confirmation dialog */}
|
|
346
|
+
<Dialog open={deleteConfirmId !== null} onOpenChange={(open) => { if (!open) setDeleteConfirmId(null); }}>
|
|
347
|
+
<DialogContent className="sm:max-w-[400px]">
|
|
348
|
+
<DialogHeader>
|
|
349
|
+
<DialogTitle>Delete Script</DialogTitle>
|
|
350
|
+
<DialogDescription>
|
|
351
|
+
Are you sure you want to delete “{deleteConfirmScript?.name ?? 'this script'}”?
|
|
352
|
+
This action cannot be undone.
|
|
353
|
+
</DialogDescription>
|
|
354
|
+
</DialogHeader>
|
|
355
|
+
<DialogFooter>
|
|
356
|
+
<Button variant="ghost" onClick={() => setDeleteConfirmId(null)}>
|
|
357
|
+
Cancel
|
|
358
|
+
</Button>
|
|
359
|
+
<Button variant="destructive" onClick={handleDeleteConfirm}>
|
|
360
|
+
Delete
|
|
361
|
+
</Button>
|
|
362
|
+
</DialogFooter>
|
|
363
|
+
</DialogContent>
|
|
364
|
+
</Dialog>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Format a log entry's args into a display string */
|
|
370
|
+
function formatLogArgs(args: unknown[]): string {
|
|
371
|
+
return args.map((a) => {
|
|
372
|
+
if (typeof a === 'object' && a !== null) {
|
|
373
|
+
try {
|
|
374
|
+
return JSON.stringify(a, null, 2);
|
|
375
|
+
} catch {
|
|
376
|
+
return String(a);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return String(a);
|
|
380
|
+
}).join(' ');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Render a single log entry with appropriate icon and color — memoized */
|
|
384
|
+
const MemoizedLogLine = memo(function LogLine({ log }: { log: LogEntry }) {
|
|
385
|
+
const formatted = useMemo(() => formatLogArgs(log.args), [log.args]);
|
|
386
|
+
|
|
387
|
+
switch (log.level) {
|
|
388
|
+
case 'error':
|
|
389
|
+
return (
|
|
390
|
+
<div className="flex items-start gap-1.5 text-destructive">
|
|
391
|
+
<AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
|
|
392
|
+
<span className="whitespace-pre-wrap break-all">{formatted}</span>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
case 'warn':
|
|
396
|
+
return (
|
|
397
|
+
<div className="flex items-start gap-1.5 text-yellow-600 dark:text-yellow-400">
|
|
398
|
+
<AlertTriangle className="h-3 w-3 mt-0.5 shrink-0" />
|
|
399
|
+
<span className="whitespace-pre-wrap break-all">{formatted}</span>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
case 'info':
|
|
403
|
+
return (
|
|
404
|
+
<div className="flex items-start gap-1.5 text-blue-600 dark:text-blue-400">
|
|
405
|
+
<Info className="h-3 w-3 mt-0.5 shrink-0" />
|
|
406
|
+
<span className="whitespace-pre-wrap break-all">{formatted}</span>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
default:
|
|
410
|
+
return (
|
|
411
|
+
<div className="flex items-start gap-1.5">
|
|
412
|
+
<span className="whitespace-pre-wrap break-all">{formatted}</span>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
});
|