vzcode 1.43.0 → 1.45.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/dist/assets/bindings_wasm_bg-CH5ekNIA.wasm +0 -0
- package/dist/assets/buildWorker-C_tGhWXG.js +682 -0
- package/dist/assets/index-C1lTrzOZ.js +457 -0
- package/dist/assets/{index-CP9o_hN5.css → index-DeeOcIzw.css} +1 -1
- package/dist/assets/worker-CekYspLa.js +453 -0
- package/dist/index.html +2 -2
- package/package.json +20 -18
- package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
- package/src/client/CodeEditor/index.tsx +4 -0
- package/src/client/Icons/SparklesSVG.tsx +9 -3
- package/src/client/ImageViewer/index.tsx +91 -0
- package/src/client/ImageViewer/style.scss +41 -0
- package/src/client/VZCodeContext.tsx +34 -6
- package/src/client/VZMiddle.tsx +22 -7
- package/src/client/VZRight.tsx +7 -2
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +8 -8
- package/src/client/VZSidebar/AIChat/DiffView.scss +125 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +11 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +1 -0
- package/src/client/VZSidebar/AIChat/index.tsx +9 -7
- package/src/client/VZSidebar/aiCopyPaste.ts +64 -1
- package/src/client/VZSidebar/index.tsx +18 -1
- package/src/client/VZSidebar/styles.scss +9 -0
- package/src/client/VZSidebar/useDragAndDrop.tsx +133 -8
- package/src/client/featureFlags.ts +2 -0
- package/src/client/useRuntimeError.ts +32 -0
- package/src/client/utils/isImageFile.ts +12 -0
- package/src/server/aiChatHandler/aiEditing.ts +19 -0
- package/src/server/aiChatHandler/chatOperations.ts +40 -0
- package/src/server/aiChatHandler/index.ts +13 -0
- package/src/server/computeInitialDocument.ts +25 -6
- package/src/server/index.ts +31 -2
- package/src/utils/fileDiff.ts +114 -0
- package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
- package/dist/assets/buildWorker-B-vXZ9gA.js +0 -579
- package/dist/assets/index-B83lKfVG.js +0 -329
- package/dist/assets/worker-DkYJN5WQ.js +0 -453
package/dist/index.html
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-C1lTrzOZ.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DeeOcIzw.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
27
27
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@codemirror/lang-html": "^6.4.9",
|
|
110
110
|
"@codemirror/lang-javascript": "^6.2.4",
|
|
111
111
|
"@codemirror/lang-json": "^6.0.2",
|
|
112
|
-
"@codemirror/lang-markdown": "^6.3.
|
|
112
|
+
"@codemirror/lang-markdown": "^6.3.4",
|
|
113
113
|
"@codemirror/lint": "^6.8.5",
|
|
114
114
|
"@codemirror/state": "^6.5.2",
|
|
115
115
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
@@ -124,37 +124,39 @@
|
|
|
124
124
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
125
125
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
126
126
|
"@typescript/vfs": "^1.6.1",
|
|
127
|
-
"@uiw/codemirror-theme-abcdef": "^4.24.
|
|
128
|
-
"@uiw/codemirror-theme-dracula": "^4.24.
|
|
129
|
-
"@uiw/codemirror-theme-eclipse": "^4.24.
|
|
130
|
-
"@uiw/codemirror-theme-github": "^4.24.
|
|
131
|
-
"@uiw/codemirror-theme-material": "^4.24.
|
|
132
|
-
"@uiw/codemirror-theme-nord": "^4.24.
|
|
133
|
-
"@uiw/codemirror-theme-okaidia": "^4.24.
|
|
134
|
-
"@uiw/codemirror-theme-xcode": "^4.24.
|
|
135
|
-
"@uiw/codemirror-themes": "^4.24.
|
|
127
|
+
"@uiw/codemirror-theme-abcdef": "^4.24.2",
|
|
128
|
+
"@uiw/codemirror-theme-dracula": "^4.24.2",
|
|
129
|
+
"@uiw/codemirror-theme-eclipse": "^4.24.2",
|
|
130
|
+
"@uiw/codemirror-theme-github": "^4.24.2",
|
|
131
|
+
"@uiw/codemirror-theme-material": "^4.24.2",
|
|
132
|
+
"@uiw/codemirror-theme-nord": "^4.24.2",
|
|
133
|
+
"@uiw/codemirror-theme-okaidia": "^4.24.2",
|
|
134
|
+
"@uiw/codemirror-theme-xcode": "^4.24.2",
|
|
135
|
+
"@uiw/codemirror-themes": "^4.24.2",
|
|
136
136
|
"@valtown/codemirror-ts": "^2.3.1",
|
|
137
|
-
"@vizhub/runtime": "^4.
|
|
137
|
+
"@vizhub/runtime": "^4.3.0",
|
|
138
138
|
"@vizhub/viz-types": "^0.3.0",
|
|
139
139
|
"@vizhub/viz-utils": "^1.3.0",
|
|
140
140
|
"body-parser": "^2.2.0",
|
|
141
141
|
"codemirror": "^6.0.2",
|
|
142
142
|
"codemirror-copilot": "^0.0.7",
|
|
143
|
-
"codemirror-ot": "^
|
|
143
|
+
"codemirror-ot": "^5.0.0",
|
|
144
144
|
"color-hash": "^2.0.2",
|
|
145
145
|
"comlink": "^4.4.2",
|
|
146
146
|
"d3-array": "^3.2.4",
|
|
147
|
+
"diff": "^8.0.2",
|
|
147
148
|
"diff-match-patch": "^1.0.5",
|
|
149
|
+
"diff2html": "^3.4.52",
|
|
148
150
|
"dotenv": "^17.2.1",
|
|
149
151
|
"editcodewithai": "^2.3.0",
|
|
150
152
|
"eslint-linter-browserify": "^9.32.0",
|
|
151
153
|
"express": "^5.1.0",
|
|
152
154
|
"ignore": "^7.0.5",
|
|
153
155
|
"json0-ot-diff": "^1.1.2",
|
|
156
|
+
"jszip": "^3.10.1",
|
|
154
157
|
"livekit-server-sdk": "^2.13.1",
|
|
155
158
|
"llm-code-format": "^3.0.0",
|
|
156
|
-
"
|
|
157
|
-
"npm": "^11.5.1",
|
|
159
|
+
"npm": "^11.5.2",
|
|
158
160
|
"open": "^10.2.0",
|
|
159
161
|
"prettier-plugin-svelte": "^3.4.0",
|
|
160
162
|
"react": "^18",
|
|
@@ -182,12 +184,12 @@
|
|
|
182
184
|
"prettier": "^3.6.2",
|
|
183
185
|
"sass": "^1.89.2",
|
|
184
186
|
"ts-node": "^10.9.2",
|
|
185
|
-
"typescript": "^5.
|
|
187
|
+
"typescript": "^5.9.2",
|
|
186
188
|
"vite": "^7.0.6",
|
|
187
189
|
"vitest": "^3.2.4"
|
|
188
190
|
},
|
|
189
191
|
"optionalDependencies": {
|
|
190
|
-
"@rollup/rollup-darwin-arm64": "^4.46.
|
|
191
|
-
"@rollup/rollup-win32-x64-msvc": "^4.46.
|
|
192
|
+
"@rollup/rollup-darwin-arm64": "^4.46.2",
|
|
193
|
+
"@rollup/rollup-win32-x64-msvc": "^4.46.2"
|
|
192
194
|
}
|
|
193
195
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRoot } from 'react-dom/client';
|
|
1
2
|
import { EditorView } from 'codemirror';
|
|
2
3
|
import {
|
|
3
4
|
Compartment,
|
|
@@ -43,7 +44,17 @@ import {
|
|
|
43
44
|
EditorCacheValue,
|
|
44
45
|
} from '../useEditorCache';
|
|
45
46
|
import { ThemeLabel, themeOptionsByLabel } from '../themes';
|
|
46
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
keymap,
|
|
49
|
+
Decoration,
|
|
50
|
+
WidgetType,
|
|
51
|
+
ViewUpdate,
|
|
52
|
+
ViewPlugin,
|
|
53
|
+
DecorationSet,
|
|
54
|
+
} from '@codemirror/view';
|
|
55
|
+
import { Range } from '@codemirror/state';
|
|
56
|
+
import { syntaxTree } from '@codemirror/language';
|
|
57
|
+
import { RegExpCursor } from '@codemirror/search';
|
|
47
58
|
import { basicSetup } from './basicSetup';
|
|
48
59
|
import { InteractRule } from '@replit/codemirror-interact';
|
|
49
60
|
import rainbowBrackets from './rainbowBrackets';
|
|
@@ -60,6 +71,9 @@ import {
|
|
|
60
71
|
tsSyncWorker,
|
|
61
72
|
} from '@valtown/codemirror-ts';
|
|
62
73
|
import { getFileExtension } from '../utils/fileExtension';
|
|
74
|
+
import { SparklesSVG } from '../Icons/SparklesSVG';
|
|
75
|
+
import { VZCodeContext } from '../VZCodeContext';
|
|
76
|
+
import { useContext, useMemo } from 'react';
|
|
63
77
|
|
|
64
78
|
const DEBUG = false;
|
|
65
79
|
|
|
@@ -178,6 +192,8 @@ export const getOrCreateEditor = async ({
|
|
|
178
192
|
aiCopilotEndpoint,
|
|
179
193
|
esLintSource,
|
|
180
194
|
rainbowBracketsEnabled = true,
|
|
195
|
+
setIsAIChatOpen,
|
|
196
|
+
setAIChatMessage,
|
|
181
197
|
}: {
|
|
182
198
|
// TODO pass this in from the outside
|
|
183
199
|
paneId?: PaneId;
|
|
@@ -213,6 +229,8 @@ export const getOrCreateEditor = async ({
|
|
|
213
229
|
view: EditorView,
|
|
214
230
|
) => Promise<readonly Diagnostic[]>;
|
|
215
231
|
rainbowBracketsEnabled?: boolean; // New parameter type
|
|
232
|
+
setIsAIChatOpen: any; // TODO fix types
|
|
233
|
+
setAIChatMessage: any; // TODO fix types
|
|
216
234
|
}): Promise<ExtendedEditorCacheValue> => {
|
|
217
235
|
// Cache hit
|
|
218
236
|
|
|
@@ -454,6 +472,116 @@ export const getOrCreateEditor = async ({
|
|
|
454
472
|
extensions.push(copilot({ aiCopilotEndpoint }));
|
|
455
473
|
}
|
|
456
474
|
|
|
475
|
+
// const { setIsAIChatOpen } = useContext(VZCodeContext);
|
|
476
|
+
|
|
477
|
+
// Widget appears after instances of "todo" in code editor, allowing AI to implement todo tasks when clicked
|
|
478
|
+
function createToDoPlugin() {
|
|
479
|
+
// setIsAIChatOpen: (open: boolean) => void,
|
|
480
|
+
class ToDoWidget extends WidgetType {
|
|
481
|
+
constructor() {
|
|
482
|
+
super();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
eq(other: ToDoWidget) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
toDOM() {
|
|
490
|
+
const wrap = document.createElement('i');
|
|
491
|
+
wrap.style.display = 'inline-flex'; // prevent block expansion
|
|
492
|
+
wrap.style.alignItems = 'center';
|
|
493
|
+
wrap.style.justifyContent = 'center';
|
|
494
|
+
wrap.className = 'icon-button icon-button-dark';
|
|
495
|
+
const reactContainer =
|
|
496
|
+
document.createElement('div');
|
|
497
|
+
wrap.appendChild(reactContainer);
|
|
498
|
+
|
|
499
|
+
const root = createRoot(reactContainer);
|
|
500
|
+
root.render(
|
|
501
|
+
<div
|
|
502
|
+
onClick={() => {
|
|
503
|
+
console.log(
|
|
504
|
+
'TODO set the chat prompt to "Implement the TODO"',
|
|
505
|
+
);
|
|
506
|
+
setIsAIChatOpen(true);
|
|
507
|
+
setAIChatMessage('Implement the TODO');
|
|
508
|
+
// submitAIChatMessage()
|
|
509
|
+
}}
|
|
510
|
+
>
|
|
511
|
+
<SparklesSVG width={14} height={14} />
|
|
512
|
+
</div>,
|
|
513
|
+
);
|
|
514
|
+
return wrap;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
ignoreEvent() {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function toDoWidgets(editor: EditorView) {
|
|
523
|
+
const { state } = editor;
|
|
524
|
+
const tree = syntaxTree(state);
|
|
525
|
+
const findToDo = new RegExpCursor(state.doc, 'todo', {
|
|
526
|
+
ignoreCase: true,
|
|
527
|
+
});
|
|
528
|
+
const widgets: Range<Decoration>[] = [];
|
|
529
|
+
|
|
530
|
+
while (!findToDo.next().done) {
|
|
531
|
+
const { from, to } = findToDo.value;
|
|
532
|
+
const node = tree.resolve(from);
|
|
533
|
+
let inComment = false;
|
|
534
|
+
|
|
535
|
+
// Walk up the parents until we hit the root, looking for a comment node
|
|
536
|
+
for (let cur: any = node; cur; cur = cur.parent) {
|
|
537
|
+
if (
|
|
538
|
+
cur.type.is('Comment') || // grammars that group comments
|
|
539
|
+
cur.type.is('comment') || // generic lowercase group
|
|
540
|
+
/Comment$/.test(cur.type.name) // fallback for e.g. LineComment
|
|
541
|
+
) {
|
|
542
|
+
inComment = true;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (!inComment) continue; // skip non-comment TODOs
|
|
547
|
+
|
|
548
|
+
const deco = Decoration.widget({
|
|
549
|
+
widget: new ToDoWidget(),
|
|
550
|
+
side: 1,
|
|
551
|
+
}).range(to);
|
|
552
|
+
widgets.push(deco);
|
|
553
|
+
}
|
|
554
|
+
return Decoration.set(widgets);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const todoPlugin = ViewPlugin.fromClass(
|
|
558
|
+
class {
|
|
559
|
+
decorations: DecorationSet;
|
|
560
|
+
|
|
561
|
+
constructor(view: EditorView) {
|
|
562
|
+
this.decorations = toDoWidgets(view);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
update(update: ViewUpdate) {
|
|
566
|
+
if (
|
|
567
|
+
update.docChanged ||
|
|
568
|
+
update.viewportChanged ||
|
|
569
|
+
syntaxTree(update.startState) !==
|
|
570
|
+
syntaxTree(update.state)
|
|
571
|
+
)
|
|
572
|
+
this.decorations = toDoWidgets(update.view);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
decorations: (v) => v.decorations,
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
return todoPlugin;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
extensions.push(createToDoPlugin());
|
|
584
|
+
|
|
457
585
|
const editor = new EditorView({
|
|
458
586
|
state: EditorState.create({
|
|
459
587
|
doc: text,
|
|
@@ -44,6 +44,8 @@ export const CodeEditor = ({
|
|
|
44
44
|
codeEditorRef,
|
|
45
45
|
enableAutoFollow,
|
|
46
46
|
openTab,
|
|
47
|
+
setIsAIChatOpen,
|
|
48
|
+
setAIChatMessage,
|
|
47
49
|
} = useContext(VZCodeContext);
|
|
48
50
|
|
|
49
51
|
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
@@ -109,6 +111,8 @@ export const CodeEditor = ({
|
|
|
109
111
|
enableAutoFollowRef,
|
|
110
112
|
aiCopilotEndpoint,
|
|
111
113
|
esLintSource,
|
|
114
|
+
setIsAIChatOpen,
|
|
115
|
+
setAIChatMessage,
|
|
112
116
|
});
|
|
113
117
|
|
|
114
118
|
if (isMounted) {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
export const SparklesSVG = (
|
|
1
|
+
export const SparklesSVG = ({
|
|
2
|
+
width = '32',
|
|
3
|
+
height = '32',
|
|
4
|
+
}: {
|
|
5
|
+
width?: string | number;
|
|
6
|
+
height?: string | number;
|
|
7
|
+
}) => (
|
|
2
8
|
<svg
|
|
3
9
|
xmlns="http://www.w3.org/2000/svg"
|
|
4
|
-
width=
|
|
5
|
-
height=
|
|
10
|
+
width={width}
|
|
11
|
+
height={height}
|
|
6
12
|
viewBox="0 0 24 24"
|
|
7
13
|
fill="currentColor"
|
|
8
14
|
stroke="currentColor"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { VZCodeContext } from '../VZCodeContext';
|
|
3
|
+
import './style.scss';
|
|
4
|
+
|
|
5
|
+
export const ImageViewer = () => {
|
|
6
|
+
const { activePane, content } = useContext(VZCodeContext);
|
|
7
|
+
|
|
8
|
+
// Get the active file
|
|
9
|
+
const activeFileId = activePane?.activeFileId;
|
|
10
|
+
const activeFile =
|
|
11
|
+
activeFileId && content?.files?.[activeFileId];
|
|
12
|
+
|
|
13
|
+
if (!activeFile) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="image-viewer-error">
|
|
16
|
+
No file selected
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if it's an image file based on extension
|
|
22
|
+
const isImageFile = activeFile.name.match(
|
|
23
|
+
/\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (!isImageFile) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="image-viewer-error">
|
|
29
|
+
Not an image file
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get the MIME type from the file extension
|
|
35
|
+
const getImageMimeType = (fileName: string): string => {
|
|
36
|
+
const extension = fileName
|
|
37
|
+
.split('.')
|
|
38
|
+
.pop()
|
|
39
|
+
?.toLowerCase();
|
|
40
|
+
switch (extension) {
|
|
41
|
+
case 'jpg':
|
|
42
|
+
case 'jpeg':
|
|
43
|
+
return 'image/jpeg';
|
|
44
|
+
case 'png':
|
|
45
|
+
return 'image/png';
|
|
46
|
+
case 'gif':
|
|
47
|
+
return 'image/gif';
|
|
48
|
+
case 'bmp':
|
|
49
|
+
return 'image/bmp';
|
|
50
|
+
case 'svg':
|
|
51
|
+
return 'image/svg+xml';
|
|
52
|
+
case 'webp':
|
|
53
|
+
return 'image/webp';
|
|
54
|
+
default:
|
|
55
|
+
return 'image/png'; // fallback
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mimeType = getImageMimeType(activeFile.name);
|
|
60
|
+
const extension = activeFile.name
|
|
61
|
+
.split('.')
|
|
62
|
+
.pop()
|
|
63
|
+
?.toLowerCase();
|
|
64
|
+
|
|
65
|
+
// For SVG files, the content might already be the SVG text, not base64
|
|
66
|
+
const imageSrc =
|
|
67
|
+
extension === 'svg' &&
|
|
68
|
+
!activeFile.text.startsWith('PHN2Zw') // Check if it's not base64-encoded SVG
|
|
69
|
+
? `data:${mimeType};utf8,${encodeURIComponent(activeFile.text)}`
|
|
70
|
+
: `data:${mimeType};base64,${activeFile.text}`;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="image-viewer">
|
|
74
|
+
<div className="image-viewer-content">
|
|
75
|
+
<img
|
|
76
|
+
src={imageSrc}
|
|
77
|
+
alt={activeFile.name}
|
|
78
|
+
className="image-viewer-img"
|
|
79
|
+
onError={(e) => {
|
|
80
|
+
console.error(
|
|
81
|
+
'Failed to load image:',
|
|
82
|
+
activeFile.name,
|
|
83
|
+
);
|
|
84
|
+
(e.target as HTMLImageElement).style.display =
|
|
85
|
+
'none';
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.image-viewer {
|
|
2
|
+
height: 100%;
|
|
3
|
+
width: 100%;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
background-color: var(--bs-body-bg);
|
|
7
|
+
color: var(--bs-body-color);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.image-viewer-header {
|
|
11
|
+
padding: 12px 16px;
|
|
12
|
+
border-bottom: 1px solid var(--bs-border-color);
|
|
13
|
+
background-color: var(--bs-tertiary-bg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.image-viewer-content {
|
|
17
|
+
flex: 1;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
overflow: auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.image-viewer-img {
|
|
26
|
+
max-width: 100%;
|
|
27
|
+
max-height: 100%;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
30
|
+
object-fit: contain;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.image-viewer-error {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
height: 100%;
|
|
38
|
+
color: var(--bs-text-muted);
|
|
39
|
+
font-family: var(--bs-font-monospace);
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
}
|
|
@@ -34,6 +34,7 @@ import { useKeyboardShortcuts } from './useKeyboardShortcuts';
|
|
|
34
34
|
import { useOpenDirectories } from './useOpenDirectories';
|
|
35
35
|
import { usePrettier } from './usePrettier';
|
|
36
36
|
import { useRunCode } from './useRunCode';
|
|
37
|
+
import { useRuntimeError } from './useRuntimeError';
|
|
37
38
|
import { useURLSync } from './useURLSync';
|
|
38
39
|
import { createInitialState, vzReducer } from './vzReducer';
|
|
39
40
|
import { findPane } from './vzReducer/findPane';
|
|
@@ -110,6 +111,12 @@ export type VZCodeContextValue = {
|
|
|
110
111
|
|
|
111
112
|
errorMessage: string | null;
|
|
112
113
|
|
|
114
|
+
// Runtime error handling
|
|
115
|
+
handleRuntimeError: (
|
|
116
|
+
formattedErrorMessage: string,
|
|
117
|
+
) => void;
|
|
118
|
+
clearRuntimeError: () => void;
|
|
119
|
+
|
|
113
120
|
search: SearchResults;
|
|
114
121
|
isSearchOpen: boolean;
|
|
115
122
|
setIsSearchOpen: (isSearchOpen: boolean) => void;
|
|
@@ -181,6 +188,8 @@ export type VZCodeContextValue = {
|
|
|
181
188
|
setLiveKitConnection: (state: boolean) => void;
|
|
182
189
|
voiceChatModalOpen: boolean;
|
|
183
190
|
setVoiceChatModalOpen: (state: boolean) => void;
|
|
191
|
+
aiChatMessage: string;
|
|
192
|
+
setAIChatMessage: (message: string) => void;
|
|
184
193
|
};
|
|
185
194
|
|
|
186
195
|
export const VZCodeProvider = ({
|
|
@@ -231,21 +240,32 @@ export const VZCodeProvider = ({
|
|
|
231
240
|
prettierWorker,
|
|
232
241
|
});
|
|
233
242
|
|
|
243
|
+
// Handle runtime errors from the iframe
|
|
244
|
+
const {
|
|
245
|
+
runtimeError,
|
|
246
|
+
handleRuntimeError,
|
|
247
|
+
clearRuntimeError,
|
|
248
|
+
} = useRuntimeError();
|
|
249
|
+
|
|
234
250
|
const runCodeRef = useRunCode(submitOperation);
|
|
235
251
|
|
|
236
252
|
const sidebarRef = useRef(null);
|
|
237
253
|
|
|
238
254
|
const codeEditorRef = useRef(null);
|
|
239
255
|
|
|
240
|
-
// The error message shows
|
|
256
|
+
// The error message shows errors in order of priority:
|
|
257
|
+
// * `runtimeError` - errors from runtime execution, highest priority
|
|
241
258
|
// * `prettierError` - errors from Prettier, client-side only
|
|
242
259
|
// * `codeError` - errors from an external source, such as
|
|
243
260
|
// build-time errors or intercepted runtime errors.
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
// Runtime errors are prioritized since they indicate immediate execution issues.
|
|
262
|
+
// Prettier errors are next since they surface syntax errors that are likely
|
|
263
|
+
// to be useful to the user.
|
|
264
|
+
const errorMessage: string | null = runtimeError
|
|
265
|
+
? runtimeError
|
|
266
|
+
: prettierError
|
|
267
|
+
? prettierError
|
|
268
|
+
: codeError;
|
|
249
269
|
|
|
250
270
|
// Set up the reducer that manages much of the application state.
|
|
251
271
|
// See https://react.dev/reference/react/useReducer
|
|
@@ -424,6 +444,8 @@ export const VZCodeProvider = ({
|
|
|
424
444
|
const [voiceChatModalOpen, setVoiceChatModalOpen] =
|
|
425
445
|
useState(false);
|
|
426
446
|
|
|
447
|
+
const [aiChatMessage, setAIChatMessage] = useState('');
|
|
448
|
+
|
|
427
449
|
// The value provided by this context.
|
|
428
450
|
const value: VZCodeContextValue = {
|
|
429
451
|
content,
|
|
@@ -493,6 +515,9 @@ export const VZCodeProvider = ({
|
|
|
493
515
|
|
|
494
516
|
errorMessage,
|
|
495
517
|
|
|
518
|
+
handleRuntimeError,
|
|
519
|
+
clearRuntimeError,
|
|
520
|
+
|
|
496
521
|
isCreateFileModalOpen,
|
|
497
522
|
handleOpenCreateFileModal,
|
|
498
523
|
handleCloseCreateFileModal,
|
|
@@ -525,6 +550,9 @@ export const VZCodeProvider = ({
|
|
|
525
550
|
setLiveKitConnection,
|
|
526
551
|
voiceChatModalOpen,
|
|
527
552
|
setVoiceChatModalOpen,
|
|
553
|
+
|
|
554
|
+
aiChatMessage,
|
|
555
|
+
setAIChatMessage,
|
|
528
556
|
};
|
|
529
557
|
|
|
530
558
|
return (
|
package/src/client/VZMiddle.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from 'react';
|
|
|
2
2
|
import { SplitPaneResizeContext } from './SplitPaneResizeContext';
|
|
3
3
|
import { TabList } from './TabList';
|
|
4
4
|
import { CodeEditor } from './CodeEditor';
|
|
5
|
+
import { ImageViewer } from './ImageViewer';
|
|
5
6
|
import { CodeErrorOverlay } from './CodeErrorOverlay';
|
|
6
7
|
import { PresenceNotifications } from './PresenceNotifications';
|
|
7
8
|
import { AIAssistWidget } from './AIAssist/AIAssistWidget';
|
|
@@ -12,6 +13,7 @@ import { EditorView } from '@codemirror/view';
|
|
|
12
13
|
import { Diagnostic } from '@codemirror/lint';
|
|
13
14
|
import { VizContent } from '@vizhub/viz-types';
|
|
14
15
|
import { LeafPane } from '../types';
|
|
16
|
+
import { isImageFile } from './utils/isImageFile';
|
|
15
17
|
|
|
16
18
|
// TODO modify this to handle the SplitPane type
|
|
17
19
|
// Recursive structure?
|
|
@@ -50,13 +52,26 @@ const PaneView = ({
|
|
|
50
52
|
activeFileId={pane.activeFileId}
|
|
51
53
|
tabList={pane.tabList}
|
|
52
54
|
/>
|
|
53
|
-
{isClient &&
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
{isClient &&
|
|
56
|
+
content &&
|
|
57
|
+
pane.activeFileId &&
|
|
58
|
+
(() => {
|
|
59
|
+
// Get the active file to determine if it's an image
|
|
60
|
+
const activeFile =
|
|
61
|
+
content.files[pane.activeFileId];
|
|
62
|
+
const shouldShowImageViewer =
|
|
63
|
+
activeFile && isImageFile(activeFile.name);
|
|
64
|
+
|
|
65
|
+
return shouldShowImageViewer ? (
|
|
66
|
+
<ImageViewer />
|
|
67
|
+
) : (
|
|
68
|
+
<CodeEditor
|
|
69
|
+
customInteractRules={customInteractRules}
|
|
70
|
+
aiCopilotEndpoint={aiCopilotEndpoint}
|
|
71
|
+
esLintSource={esLintSource}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
})()}
|
|
60
75
|
{isClient &&
|
|
61
76
|
enableAIAssist &&
|
|
62
77
|
content &&
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -20,7 +20,8 @@ export const VZRight = () => {
|
|
|
20
20
|
const isFirstRunRef = useRef(true);
|
|
21
21
|
|
|
22
22
|
// Get access to the current files.
|
|
23
|
-
const { content } =
|
|
23
|
+
const { content, handleRuntimeError, clearRuntimeError } =
|
|
24
|
+
useContext(VZCodeContext);
|
|
24
25
|
|
|
25
26
|
const files = useMemo(
|
|
26
27
|
() =>
|
|
@@ -46,14 +47,18 @@ export const VZRight = () => {
|
|
|
46
47
|
console.error('Build error:', error);
|
|
47
48
|
}
|
|
48
49
|
},
|
|
50
|
+
handleRuntimeError,
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Run code in the iframe
|
|
53
55
|
if (isFirstRunRef.current || isInteracting) {
|
|
56
|
+
// Clear runtime errors when new code runs
|
|
57
|
+
clearRuntimeError();
|
|
58
|
+
|
|
54
59
|
runtimeRef.current?.run({
|
|
55
60
|
files,
|
|
56
|
-
enableHotReloading:
|
|
61
|
+
enableHotReloading: !isFirstRunRef.current,
|
|
57
62
|
enableSourcemap: true,
|
|
58
63
|
vizId: 'example-viz',
|
|
59
64
|
});
|
|
@@ -7,16 +7,16 @@ import {
|
|
|
7
7
|
import { Form, Button } from '../../bootstrap';
|
|
8
8
|
|
|
9
9
|
interface ChatInputProps {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
aiChatMessage: string;
|
|
11
|
+
setAIChatMessage: (message: string) => void;
|
|
12
12
|
onSendMessage: () => void;
|
|
13
13
|
isLoading: boolean;
|
|
14
14
|
focused: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const ChatInputComponent = ({
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
aiChatMessage,
|
|
19
|
+
setAIChatMessage,
|
|
20
20
|
onSendMessage,
|
|
21
21
|
isLoading,
|
|
22
22
|
focused,
|
|
@@ -42,9 +42,9 @@ const ChatInputComponent = ({
|
|
|
42
42
|
|
|
43
43
|
const handleChange = useCallback(
|
|
44
44
|
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
45
|
-
|
|
45
|
+
setAIChatMessage(event.target.value);
|
|
46
46
|
},
|
|
47
|
-
[
|
|
47
|
+
[setAIChatMessage],
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
return (
|
|
@@ -53,7 +53,7 @@ const ChatInputComponent = ({
|
|
|
53
53
|
<Form.Control
|
|
54
54
|
as="textarea"
|
|
55
55
|
rows={2}
|
|
56
|
-
value={
|
|
56
|
+
value={aiChatMessage}
|
|
57
57
|
onChange={handleChange}
|
|
58
58
|
onKeyDown={handleKeyDown}
|
|
59
59
|
ref={inputRef}
|
|
@@ -64,7 +64,7 @@ const ChatInputComponent = ({
|
|
|
64
64
|
<Button
|
|
65
65
|
variant="primary"
|
|
66
66
|
onClick={onSendMessage}
|
|
67
|
-
disabled={!
|
|
67
|
+
disabled={!aiChatMessage.trim() || isLoading}
|
|
68
68
|
className="ai-chat-send-button"
|
|
69
69
|
>
|
|
70
70
|
Send
|