@ifc-lite/viewer 1.14.2 → 1.14.4
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 +35 -0
- package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
- package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
- package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
- package/dist/assets/index-CMQ_Dgkr.css +1 -0
- package/dist/assets/index-D7nEDctQ.js +229 -0
- package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
- package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
- package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +21 -20
- package/src/App.tsx +17 -1
- package/src/components/viewer/BasketPresentationDock.tsx +8 -4
- package/src/components/viewer/ChatPanel.tsx +1402 -0
- package/src/components/viewer/CodeEditor.tsx +70 -4
- package/src/components/viewer/CommandPalette.tsx +1 -0
- package/src/components/viewer/HierarchyPanel.tsx +28 -13
- package/src/components/viewer/MainToolbar.tsx +113 -95
- package/src/components/viewer/ScriptPanel.tsx +351 -184
- package/src/components/viewer/UpgradePage.tsx +69 -0
- package/src/components/viewer/Viewport.tsx +23 -0
- package/src/components/viewer/chat/ChatMessage.tsx +144 -0
- package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
- package/src/components/viewer/chat/ModelSelector.tsx +102 -0
- package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
- package/src/components/viewer/chat/renderTextContent.ts +19 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
- package/src/components/viewer/hierarchy/types.ts +6 -1
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
- package/src/hooks/useIfcCache.ts +1 -2
- package/src/hooks/useSandbox.ts +122 -6
- package/src/index.css +10 -0
- package/src/lib/attachments.ts +46 -0
- package/src/lib/llm/ClerkChatSync.tsx +74 -0
- package/src/lib/llm/clerk-auth.ts +62 -0
- package/src/lib/llm/code-extractor.ts +50 -0
- package/src/lib/llm/context-builder.test.ts +18 -0
- package/src/lib/llm/context-builder.ts +305 -0
- package/src/lib/llm/free-models.test.ts +118 -0
- package/src/lib/llm/message-capabilities.test.ts +131 -0
- package/src/lib/llm/message-capabilities.ts +94 -0
- package/src/lib/llm/models.ts +197 -0
- package/src/lib/llm/repair-loop.test.ts +91 -0
- package/src/lib/llm/repair-loop.ts +76 -0
- package/src/lib/llm/script-diagnostics.ts +445 -0
- package/src/lib/llm/script-edit-ops.test.ts +399 -0
- package/src/lib/llm/script-edit-ops.ts +954 -0
- package/src/lib/llm/script-preflight.test.ts +513 -0
- package/src/lib/llm/script-preflight.ts +990 -0
- package/src/lib/llm/script-preservation.test.ts +128 -0
- package/src/lib/llm/script-preservation.ts +152 -0
- package/src/lib/llm/stream-client.test.ts +97 -0
- package/src/lib/llm/stream-client.ts +410 -0
- package/src/lib/llm/system-prompt.test.ts +181 -0
- package/src/lib/llm/system-prompt.ts +665 -0
- package/src/lib/llm/types.ts +150 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
- package/src/lib/scripts/templates/create-building.ts +12 -12
- package/src/main.tsx +10 -1
- package/src/sdk/adapters/export-adapter.test.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +40 -16
- package/src/sdk/adapters/files-adapter.ts +39 -0
- package/src/sdk/adapters/model-compat.ts +1 -1
- package/src/sdk/adapters/mutate-adapter.ts +20 -6
- package/src/sdk/adapters/mutation-view.ts +112 -0
- package/src/sdk/adapters/query-adapter.ts +100 -4
- package/src/sdk/local-backend.ts +4 -0
- package/src/store/index.ts +15 -1
- package/src/store/slices/chatSlice.test.ts +325 -0
- package/src/store/slices/chatSlice.ts +468 -0
- package/src/store/slices/scriptSlice.test.ts +75 -0
- package/src/store/slices/scriptSlice.ts +256 -9
- package/src/vite-env.d.ts +10 -0
- package/vite.config.ts +21 -2
- package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
- package/dist/assets/index-ByrFvN5A.css +0 -1
- package/dist/assets/index-CN7qDq7G.js +0 -216
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* ScriptPanel — Code editor + output console
|
|
6
|
+
* ScriptPanel — Code editor + output console + optional AI chat side panel.
|
|
7
7
|
*
|
|
8
8
|
* Uses CodeMirror 6 for the code editor with bim.* autocomplete.
|
|
9
9
|
* Connects to the QuickJS sandbox via useSandbox() and displays results
|
|
10
|
-
* in a log console.
|
|
10
|
+
* in a log console. AI chat is integrated as a collapsible side panel.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { useCallback, useMemo, useState, memo } from 'react';
|
|
13
|
+
import { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
|
|
14
14
|
import {
|
|
15
15
|
Play,
|
|
16
16
|
Save,
|
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
CheckCircle2,
|
|
25
25
|
Info,
|
|
26
26
|
AlertTriangle,
|
|
27
|
+
Bot,
|
|
28
|
+
PanelRightClose,
|
|
29
|
+
PanelRightOpen,
|
|
30
|
+
Undo2,
|
|
31
|
+
Redo2,
|
|
27
32
|
} from 'lucide-react';
|
|
28
33
|
import { Button } from '@/components/ui/button';
|
|
29
34
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
@@ -48,6 +53,7 @@ import { useViewerStore } from '@/store';
|
|
|
48
53
|
import { useSandbox } from '@/hooks/useSandbox';
|
|
49
54
|
import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
|
|
50
55
|
import { CodeEditor } from './CodeEditor';
|
|
56
|
+
import { ChatPanel } from './ChatPanel';
|
|
51
57
|
import type { LogEntry } from '@/store/slices/scriptSlice';
|
|
52
58
|
|
|
53
59
|
interface ScriptPanelProps {
|
|
@@ -70,6 +76,14 @@ function useScriptState() {
|
|
|
70
76
|
const setActiveScriptId = useViewerStore((s) => s.setActiveScriptId);
|
|
71
77
|
const deleteConfirmId = useViewerStore((s) => s.scriptDeleteConfirmId);
|
|
72
78
|
const setDeleteConfirmId = useViewerStore((s) => s.setScriptDeleteConfirmId);
|
|
79
|
+
const setScriptCursorContext = useViewerStore((s) => s.setScriptCursorContext);
|
|
80
|
+
const registerScriptEditorApplyAdapter = useViewerStore((s) => s.registerScriptEditorApplyAdapter);
|
|
81
|
+
const scriptCanUndo = useViewerStore((s) => s.scriptCanUndo);
|
|
82
|
+
const scriptCanRedo = useViewerStore((s) => s.scriptCanRedo);
|
|
83
|
+
const setScriptHistoryState = useViewerStore((s) => s.setScriptHistoryState);
|
|
84
|
+
const undoScriptEditor = useViewerStore((s) => s.undoScriptEditor);
|
|
85
|
+
const redoScriptEditor = useViewerStore((s) => s.redoScriptEditor);
|
|
86
|
+
const queueChatRepairRequest = useViewerStore((s) => s.queueChatRepairRequest);
|
|
73
87
|
|
|
74
88
|
return {
|
|
75
89
|
editorContent,
|
|
@@ -86,6 +100,14 @@ function useScriptState() {
|
|
|
86
100
|
setActiveScriptId,
|
|
87
101
|
deleteConfirmId,
|
|
88
102
|
setDeleteConfirmId,
|
|
103
|
+
setScriptCursorContext,
|
|
104
|
+
registerScriptEditorApplyAdapter,
|
|
105
|
+
scriptCanUndo,
|
|
106
|
+
scriptCanRedo,
|
|
107
|
+
setScriptHistoryState,
|
|
108
|
+
undoScriptEditor,
|
|
109
|
+
redoScriptEditor,
|
|
110
|
+
queueChatRepairRequest,
|
|
89
111
|
};
|
|
90
112
|
}
|
|
91
113
|
|
|
@@ -105,10 +127,66 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
105
127
|
setActiveScriptId,
|
|
106
128
|
deleteConfirmId,
|
|
107
129
|
setDeleteConfirmId,
|
|
130
|
+
setScriptCursorContext,
|
|
131
|
+
registerScriptEditorApplyAdapter,
|
|
132
|
+
scriptCanUndo,
|
|
133
|
+
scriptCanRedo,
|
|
134
|
+
setScriptHistoryState,
|
|
135
|
+
undoScriptEditor,
|
|
136
|
+
redoScriptEditor,
|
|
137
|
+
queueChatRepairRequest,
|
|
108
138
|
} = useScriptState();
|
|
109
139
|
|
|
110
140
|
const { execute, reset } = useSandbox();
|
|
111
141
|
const [outputCollapsed, setOutputCollapsed] = useState(false);
|
|
142
|
+
const chatPanelVisible = useViewerStore((s) => s.chatPanelVisible);
|
|
143
|
+
const setChatPanelVisible = useViewerStore((s) => s.setChatPanelVisible);
|
|
144
|
+
|
|
145
|
+
// Chat panel width (px) — resizable via drag handle
|
|
146
|
+
const [chatWidth, setChatWidth] = useState(380);
|
|
147
|
+
const chatDragRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
|
148
|
+
const cleanupChatDragRef = useRef<(() => void) | null>(null);
|
|
149
|
+
|
|
150
|
+
// Open chat by default when script panel mounts
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
try {
|
|
153
|
+
if (localStorage.getItem('ifc-lite-chat-panel-visible') === null) {
|
|
154
|
+
setChatPanelVisible(true);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
setChatPanelVisible(true);
|
|
158
|
+
}
|
|
159
|
+
return () => { cleanupChatDragRef.current?.(); };
|
|
160
|
+
}, [setChatPanelVisible]);
|
|
161
|
+
|
|
162
|
+
const handleChatResizeStart = useCallback((e: React.MouseEvent) => {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
chatDragRef.current = { startX: e.clientX, startWidth: chatWidth };
|
|
165
|
+
|
|
166
|
+
const onMouseMove = (moveEvent: MouseEvent) => {
|
|
167
|
+
if (!chatDragRef.current) return;
|
|
168
|
+
const delta = chatDragRef.current.startX - moveEvent.clientX;
|
|
169
|
+
const newWidth = Math.min(700, Math.max(240, chatDragRef.current.startWidth + delta));
|
|
170
|
+
setChatWidth(newWidth);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const cleanup = () => {
|
|
174
|
+
chatDragRef.current = null;
|
|
175
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
176
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
177
|
+
document.body.style.cursor = '';
|
|
178
|
+
document.body.style.userSelect = '';
|
|
179
|
+
cleanupChatDragRef.current = null;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const onMouseUp = () => { cleanup(); };
|
|
183
|
+
|
|
184
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
185
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
186
|
+
document.body.style.cursor = 'col-resize';
|
|
187
|
+
document.body.style.userSelect = 'none';
|
|
188
|
+
cleanupChatDragRef.current = cleanup;
|
|
189
|
+
}, [chatWidth]);
|
|
112
190
|
|
|
113
191
|
const activeScript = useMemo(
|
|
114
192
|
() => savedScripts.find((s) => s.id === activeScriptId),
|
|
@@ -143,205 +221,294 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
|
|
|
143
221
|
}
|
|
144
222
|
}, [deleteConfirmId, deleteScript]);
|
|
145
223
|
|
|
224
|
+
const handleFixWithLlm = useCallback(() => {
|
|
225
|
+
if (!lastError) return;
|
|
226
|
+
setChatPanelVisible(true);
|
|
227
|
+
const state = useViewerStore.getState();
|
|
228
|
+
queueChatRepairRequest({
|
|
229
|
+
error: lastError,
|
|
230
|
+
diagnostics: state.scriptLastDiagnostics,
|
|
231
|
+
reason: lastError.startsWith('Preflight validation failed:') ? 'preflight' : 'runtime',
|
|
232
|
+
});
|
|
233
|
+
}, [lastError, queueChatRepairRequest, setChatPanelVisible]);
|
|
234
|
+
|
|
235
|
+
const toggleChat = useCallback(() => {
|
|
236
|
+
setChatPanelVisible(!chatPanelVisible);
|
|
237
|
+
}, [chatPanelVisible, setChatPanelVisible]);
|
|
238
|
+
|
|
146
239
|
return (
|
|
147
|
-
<div className="h-full flex
|
|
148
|
-
{/*
|
|
149
|
-
<div className=
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
240
|
+
<div className="h-full flex bg-background">
|
|
241
|
+
{/* Left side: Script editor + output */}
|
|
242
|
+
<div className={cn('flex flex-col min-w-0', chatPanelVisible ? 'flex-1' : 'w-full')}>
|
|
243
|
+
{/* Header */}
|
|
244
|
+
<div className="flex items-center gap-1 px-2 py-1.5 border-b shrink-0">
|
|
245
|
+
<FileCode2 className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
246
|
+
<span className="text-sm font-medium truncate">
|
|
247
|
+
{activeScript ? activeScript.name : 'Script Editor'}
|
|
248
|
+
{editorDirty && <span className="text-muted-foreground ml-1">*</span>}
|
|
249
|
+
</span>
|
|
250
|
+
<div className="flex-1" />
|
|
251
|
+
|
|
252
|
+
{/* Script selector dropdown */}
|
|
253
|
+
{savedScripts.length > 0 && (
|
|
254
|
+
<DropdownMenu>
|
|
255
|
+
<DropdownMenuTrigger asChild>
|
|
256
|
+
<Button variant="ghost" size="icon-xs">
|
|
257
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
258
|
+
</Button>
|
|
259
|
+
</DropdownMenuTrigger>
|
|
260
|
+
<DropdownMenuContent align="end">
|
|
261
|
+
{savedScripts.map((s) => (
|
|
262
|
+
<DropdownMenuItem
|
|
263
|
+
key={s.id}
|
|
264
|
+
onClick={() => setActiveScriptId(s.id)}
|
|
265
|
+
className={cn(s.id === activeScriptId && 'bg-accent')}
|
|
266
|
+
>
|
|
267
|
+
<FileCode2 className="h-3.5 w-3.5 mr-2" />
|
|
268
|
+
{s.name}
|
|
269
|
+
</DropdownMenuItem>
|
|
270
|
+
))}
|
|
271
|
+
<DropdownMenuSeparator />
|
|
272
|
+
{activeScriptId && (
|
|
273
|
+
<DropdownMenuItem
|
|
274
|
+
onClick={() => setDeleteConfirmId(activeScriptId)}
|
|
275
|
+
className="text-destructive"
|
|
276
|
+
>
|
|
277
|
+
<Trash2 className="h-3.5 w-3.5 mr-2" />
|
|
278
|
+
Delete
|
|
279
|
+
</DropdownMenuItem>
|
|
280
|
+
)}
|
|
281
|
+
</DropdownMenuContent>
|
|
282
|
+
</DropdownMenu>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* AI Chat toggle */}
|
|
286
|
+
<Tooltip>
|
|
287
|
+
<TooltipTrigger asChild>
|
|
288
|
+
<Button
|
|
289
|
+
variant={chatPanelVisible ? 'default' : 'ghost'}
|
|
290
|
+
size="icon-xs"
|
|
291
|
+
onClick={toggleChat}
|
|
292
|
+
className={cn(chatPanelVisible && 'bg-blue-500 hover:bg-blue-600 text-white')}
|
|
293
|
+
>
|
|
294
|
+
<Bot className="h-3.5 w-3.5" />
|
|
295
|
+
</Button>
|
|
296
|
+
</TooltipTrigger>
|
|
297
|
+
<TooltipContent>{chatPanelVisible ? 'Hide AI Chat' : 'Show AI Chat'}</TooltipContent>
|
|
298
|
+
</Tooltip>
|
|
299
|
+
|
|
300
|
+
{onClose && (
|
|
301
|
+
<Button variant="ghost" size="icon-xs" onClick={onClose}>
|
|
302
|
+
<X className="h-3.5 w-3.5" />
|
|
303
|
+
</Button>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Toolbar */}
|
|
308
|
+
<div className="flex items-center gap-1 px-2 py-1 border-b shrink-0">
|
|
309
|
+
<Tooltip>
|
|
310
|
+
<TooltipTrigger asChild>
|
|
311
|
+
<Button
|
|
312
|
+
variant="default"
|
|
313
|
+
size="sm"
|
|
314
|
+
onClick={handleRun}
|
|
315
|
+
disabled={executionState === 'running'}
|
|
316
|
+
className="gap-1"
|
|
317
|
+
>
|
|
318
|
+
<Play className="h-3.5 w-3.5" />
|
|
319
|
+
Run
|
|
163
320
|
</Button>
|
|
164
|
-
</
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
321
|
+
</TooltipTrigger>
|
|
322
|
+
<TooltipContent>Run script (Ctrl+Enter)</TooltipContent>
|
|
323
|
+
</Tooltip>
|
|
324
|
+
|
|
325
|
+
<Tooltip>
|
|
326
|
+
<TooltipTrigger asChild>
|
|
327
|
+
<Button variant="ghost" size="icon-xs" onClick={handleSave}>
|
|
328
|
+
<Save className="h-3.5 w-3.5" />
|
|
329
|
+
</Button>
|
|
330
|
+
</TooltipTrigger>
|
|
331
|
+
<TooltipContent>Save (Ctrl+S)</TooltipContent>
|
|
332
|
+
</Tooltip>
|
|
333
|
+
|
|
334
|
+
<Tooltip>
|
|
335
|
+
<TooltipTrigger asChild>
|
|
336
|
+
<Button
|
|
337
|
+
variant="ghost"
|
|
338
|
+
size="icon-xs"
|
|
339
|
+
onClick={undoScriptEditor}
|
|
340
|
+
disabled={!scriptCanUndo}
|
|
341
|
+
>
|
|
342
|
+
<Undo2 className="h-3.5 w-3.5" />
|
|
343
|
+
</Button>
|
|
344
|
+
</TooltipTrigger>
|
|
345
|
+
<TooltipContent>Undo (Ctrl+Z)</TooltipContent>
|
|
346
|
+
</Tooltip>
|
|
347
|
+
|
|
348
|
+
<Tooltip>
|
|
349
|
+
<TooltipTrigger asChild>
|
|
350
|
+
<Button
|
|
351
|
+
variant="ghost"
|
|
352
|
+
size="icon-xs"
|
|
353
|
+
onClick={redoScriptEditor}
|
|
354
|
+
disabled={!scriptCanRedo}
|
|
355
|
+
>
|
|
356
|
+
<Redo2 className="h-3.5 w-3.5" />
|
|
357
|
+
</Button>
|
|
358
|
+
</TooltipTrigger>
|
|
359
|
+
<TooltipContent>Redo (Ctrl+Shift+Z)</TooltipContent>
|
|
360
|
+
</Tooltip>
|
|
361
|
+
|
|
362
|
+
{/* New script dropdown with templates */}
|
|
363
|
+
<DropdownMenu>
|
|
364
|
+
<Tooltip>
|
|
365
|
+
<TooltipTrigger asChild>
|
|
366
|
+
<DropdownMenuTrigger asChild>
|
|
367
|
+
<Button variant="ghost" size="icon-xs">
|
|
368
|
+
<Plus className="h-3.5 w-3.5" />
|
|
369
|
+
</Button>
|
|
370
|
+
</DropdownMenuTrigger>
|
|
371
|
+
</TooltipTrigger>
|
|
372
|
+
<TooltipContent>New script</TooltipContent>
|
|
373
|
+
</Tooltip>
|
|
374
|
+
<DropdownMenuContent align="start">
|
|
375
|
+
<DropdownMenuItem onClick={() => handleNew('Untitled Script')}>
|
|
376
|
+
<FileCode2 className="h-3.5 w-3.5 mr-2" />
|
|
377
|
+
Blank Script
|
|
378
|
+
</DropdownMenuItem>
|
|
379
|
+
<DropdownMenuSeparator />
|
|
380
|
+
{SCRIPT_TEMPLATES.map((t) => (
|
|
381
|
+
<DropdownMenuItem key={t.name} onClick={() => handleNew(t.name, t.code)}>
|
|
172
382
|
<FileCode2 className="h-3.5 w-3.5 mr-2" />
|
|
173
|
-
{
|
|
383
|
+
{t.name}
|
|
174
384
|
</DropdownMenuItem>
|
|
175
385
|
))}
|
|
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
386
|
</DropdownMenuContent>
|
|
187
387
|
</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
388
|
|
|
224
|
-
{/* New script dropdown with templates */}
|
|
225
|
-
<DropdownMenu>
|
|
226
389
|
<Tooltip>
|
|
227
390
|
<TooltipTrigger asChild>
|
|
228
|
-
<
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
</Button>
|
|
232
|
-
</DropdownMenuTrigger>
|
|
391
|
+
<Button variant="ghost" size="icon-xs" onClick={reset}>
|
|
392
|
+
<RotateCcw className="h-3.5 w-3.5" />
|
|
393
|
+
</Button>
|
|
233
394
|
</TooltipTrigger>
|
|
234
|
-
<TooltipContent>
|
|
395
|
+
<TooltipContent>Reset sandbox</TooltipContent>
|
|
235
396
|
</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
397
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
398
|
+
{/* Status indicator */}
|
|
399
|
+
<div className="flex-1" />
|
|
400
|
+
{executionState === 'running' && (
|
|
401
|
+
<span className="text-xs text-muted-foreground animate-pulse">Running...</span>
|
|
402
|
+
)}
|
|
403
|
+
{executionState === 'success' && lastResult && (
|
|
404
|
+
<span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
|
|
405
|
+
<CheckCircle2 className="h-3 w-3" />
|
|
406
|
+
{formatDuration(lastResult.durationMs)}
|
|
407
|
+
</span>
|
|
408
|
+
)}
|
|
409
|
+
{executionState === 'error' && (
|
|
410
|
+
<span className="text-xs text-destructive flex items-center gap-1">
|
|
411
|
+
<AlertCircle className="h-3 w-3" />
|
|
412
|
+
Error
|
|
413
|
+
</span>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
289
416
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
417
|
+
{/* Code Editor */}
|
|
418
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
419
|
+
<CodeEditor
|
|
420
|
+
value={editorContent}
|
|
421
|
+
onChange={setEditorContent}
|
|
422
|
+
onSelectionChange={setScriptCursorContext}
|
|
423
|
+
onHistoryChange={setScriptHistoryState}
|
|
424
|
+
registerApplyAdapter={registerScriptEditorApplyAdapter}
|
|
425
|
+
onRun={handleRun}
|
|
426
|
+
onSave={handleSave}
|
|
427
|
+
className="h-full"
|
|
299
428
|
/>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* Output Console */}
|
|
432
|
+
<div className="shrink-0 border-t">
|
|
433
|
+
{/* Output header */}
|
|
434
|
+
<button
|
|
435
|
+
className="flex items-center gap-1.5 px-2 py-1 w-full hover:bg-muted/50 transition-colors text-left"
|
|
436
|
+
onClick={() => setOutputCollapsed(!outputCollapsed)}
|
|
437
|
+
>
|
|
438
|
+
<ChevronDown
|
|
439
|
+
className={cn('h-3 w-3 transition-transform', outputCollapsed && '-rotate-90')}
|
|
440
|
+
/>
|
|
441
|
+
<span className="text-xs font-medium text-muted-foreground">Output</span>
|
|
442
|
+
{lastResult && lastResult.logs.length > 0 && (
|
|
443
|
+
<span className="text-xs text-muted-foreground">({lastResult.logs.length})</span>
|
|
444
|
+
)}
|
|
445
|
+
</button>
|
|
446
|
+
|
|
447
|
+
{!outputCollapsed && (
|
|
448
|
+
<ScrollArea className="h-[140px]">
|
|
449
|
+
<div className="px-2 pb-2 font-mono text-xs space-y-0.5">
|
|
450
|
+
{/* Error message */}
|
|
451
|
+
{lastError && (
|
|
452
|
+
<div className="flex items-start gap-1.5 text-destructive">
|
|
453
|
+
<AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
|
|
454
|
+
<div className="min-w-0">
|
|
455
|
+
<span className="whitespace-pre-wrap break-all">{lastError}</span>
|
|
456
|
+
<div className="mt-1">
|
|
457
|
+
<Button
|
|
458
|
+
variant="outline"
|
|
459
|
+
size="sm"
|
|
460
|
+
className="h-6 px-2 text-xs border-destructive/40 text-destructive bg-transparent hover:bg-destructive/10"
|
|
461
|
+
onClick={handleFixWithLlm}
|
|
462
|
+
>
|
|
463
|
+
Fix with LLM
|
|
464
|
+
</Button>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
|
|
470
|
+
{/* Log entries */}
|
|
471
|
+
{lastResult?.logs.map((log, i) => (
|
|
472
|
+
<MemoizedLogLine key={i} log={log} />
|
|
473
|
+
))}
|
|
321
474
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
475
|
+
{/* Return value */}
|
|
476
|
+
{lastResult && lastResult.value !== undefined && lastResult.value !== null && (
|
|
477
|
+
<div className="text-muted-foreground mt-1 pt-1 border-t border-border/50">
|
|
478
|
+
<span className="opacity-60">Return: </span>
|
|
479
|
+
<span className="text-foreground">
|
|
480
|
+
{typeof lastResult.value === 'object'
|
|
481
|
+
? JSON.stringify(lastResult.value, null, 2)
|
|
482
|
+
: String(lastResult.value)}
|
|
483
|
+
</span>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{/* Empty state */}
|
|
488
|
+
{!lastError && !lastResult && (
|
|
489
|
+
<div className="text-muted-foreground py-2 text-center">
|
|
490
|
+
Press Run or Ctrl+Enter to execute
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
</ScrollArea>
|
|
495
|
+
)}
|
|
496
|
+
</div>
|
|
343
497
|
</div>
|
|
344
498
|
|
|
499
|
+
{/* Right side: AI Chat panel (collapsible, resizable) */}
|
|
500
|
+
{chatPanelVisible && (
|
|
501
|
+
<>
|
|
502
|
+
<div
|
|
503
|
+
className="w-1.5 bg-border hover:bg-primary/50 active:bg-primary/70 transition-colors cursor-col-resize shrink-0 h-full"
|
|
504
|
+
onMouseDown={handleChatResizeStart}
|
|
505
|
+
/>
|
|
506
|
+
<div style={{ width: chatWidth }} className="shrink-0 h-full min-w-0">
|
|
507
|
+
<ChatPanel onClose={() => setChatPanelVisible(false)} />
|
|
508
|
+
</div>
|
|
509
|
+
</>
|
|
510
|
+
)}
|
|
511
|
+
|
|
345
512
|
{/* Delete confirmation dialog */}
|
|
346
513
|
<Dialog open={deleteConfirmId !== null} onOpenChange={(open) => { if (!open) setDeleteConfirmId(null); }}>
|
|
347
514
|
<DialogContent className="sm:max-w-[400px]">
|