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.
Files changed (38) hide show
  1. package/dist/assets/bindings_wasm_bg-CH5ekNIA.wasm +0 -0
  2. package/dist/assets/buildWorker-C_tGhWXG.js +682 -0
  3. package/dist/assets/index-C1lTrzOZ.js +457 -0
  4. package/dist/assets/{index-CP9o_hN5.css → index-DeeOcIzw.css} +1 -1
  5. package/dist/assets/worker-CekYspLa.js +453 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +20 -18
  8. package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
  9. package/src/client/CodeEditor/index.tsx +4 -0
  10. package/src/client/Icons/SparklesSVG.tsx +9 -3
  11. package/src/client/ImageViewer/index.tsx +91 -0
  12. package/src/client/ImageViewer/style.scss +41 -0
  13. package/src/client/VZCodeContext.tsx +34 -6
  14. package/src/client/VZMiddle.tsx +22 -7
  15. package/src/client/VZRight.tsx +7 -2
  16. package/src/client/VZSidebar/AIChat/ChatInput.tsx +8 -8
  17. package/src/client/VZSidebar/AIChat/DiffView.scss +125 -0
  18. package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -0
  19. package/src/client/VZSidebar/AIChat/Message.tsx +11 -0
  20. package/src/client/VZSidebar/AIChat/MessageList.tsx +1 -0
  21. package/src/client/VZSidebar/AIChat/index.tsx +9 -7
  22. package/src/client/VZSidebar/aiCopyPaste.ts +64 -1
  23. package/src/client/VZSidebar/index.tsx +18 -1
  24. package/src/client/VZSidebar/styles.scss +9 -0
  25. package/src/client/VZSidebar/useDragAndDrop.tsx +133 -8
  26. package/src/client/featureFlags.ts +2 -0
  27. package/src/client/useRuntimeError.ts +32 -0
  28. package/src/client/utils/isImageFile.ts +12 -0
  29. package/src/server/aiChatHandler/aiEditing.ts +19 -0
  30. package/src/server/aiChatHandler/chatOperations.ts +40 -0
  31. package/src/server/aiChatHandler/index.ts +13 -0
  32. package/src/server/computeInitialDocument.ts +25 -6
  33. package/src/server/index.ts +31 -2
  34. package/src/utils/fileDiff.ts +114 -0
  35. package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
  36. package/dist/assets/buildWorker-B-vXZ9gA.js +0 -579
  37. package/dist/assets/index-B83lKfVG.js +0 -329
  38. 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-B83lKfVG.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-CP9o_hN5.css">
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.43.0",
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.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.1",
128
- "@uiw/codemirror-theme-dracula": "^4.24.1",
129
- "@uiw/codemirror-theme-eclipse": "^4.24.1",
130
- "@uiw/codemirror-theme-github": "^4.24.1",
131
- "@uiw/codemirror-theme-material": "^4.24.1",
132
- "@uiw/codemirror-theme-nord": "^4.24.1",
133
- "@uiw/codemirror-theme-okaidia": "^4.24.1",
134
- "@uiw/codemirror-theme-xcode": "^4.24.1",
135
- "@uiw/codemirror-themes": "^4.24.1",
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.1.0",
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": "^4.7.0",
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
- "ngrok": "^5.0.0-beta.2",
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.8.3",
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.1",
191
- "@rollup/rollup-win32-x64-msvc": "^4.46.1"
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 { keymap } from '@codemirror/view';
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="32"
5
- height="32"
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 either:
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
- // Since `prettierError` surfaces syntax errors, it's more likely to be
245
- // useful to the user, so we prioritize it.
246
- const errorMessage: string | null = prettierError
247
- ? prettierError
248
- : codeError;
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 (
@@ -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 && content && pane.activeFileId && (
54
- <CodeEditor
55
- customInteractRules={customInteractRules}
56
- aiCopilotEndpoint={aiCopilotEndpoint}
57
- esLintSource={esLintSource}
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 &&
@@ -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 } = useContext(VZCodeContext);
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: true,
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
- message: string;
11
- setMessage: (message: string) => void;
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
- message,
19
- setMessage,
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
- setMessage(event.target.value);
45
+ setAIChatMessage(event.target.value);
46
46
  },
47
- [setMessage],
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={message}
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={!message.trim() || isLoading}
67
+ disabled={!aiChatMessage.trim() || isLoading}
68
68
  className="ai-chat-send-button"
69
69
  >
70
70
  Send