vzcode 1.39.0 → 1.41.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 (35) hide show
  1. package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
  2. package/dist/assets/buildWorker-B-vXZ9gA.js +579 -0
  3. package/dist/assets/index-DPAeYEiH.js +329 -0
  4. package/dist/index.html +1 -1
  5. package/package.json +16 -14
  6. package/src/client/CodeEditor/getOrCreateEditor.ts +2 -5
  7. package/src/client/CodeEditor/index.tsx +1 -1
  8. package/src/client/CodeEditor/json1PresenceDisplay.ts +28 -44
  9. package/src/client/VZRight.tsx +67 -6
  10. package/src/client/VZSidebar/AIChat/ChatInput.tsx +17 -5
  11. package/src/client/VZSidebar/AIChat/Message.tsx +21 -8
  12. package/src/client/VZSidebar/AIChat/MessageList.tsx +11 -4
  13. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +6 -1
  14. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +7 -1
  15. package/src/client/VZSidebar/AIChat/index.tsx +22 -7
  16. package/src/client/buildWorker.ts +3 -0
  17. package/src/client/usePresenceAutoFollow.ts +3 -4
  18. package/src/server/aiChatHandler/{chatOperations.js → chatOperations.ts} +5 -2
  19. package/src/server/aiChatHandler/{index.js → index.ts} +19 -9
  20. package/src/server/aiChatHandler/{llmStreaming.js → llmStreaming.ts} +16 -7
  21. package/src/server/{generateAIResponse.js → generateAIResponse.ts} +3 -2
  22. package/src/server/{handleAIAssist.js → handleAIAssist.ts} +11 -4
  23. package/src/server/{index.js → index.ts} +16 -22
  24. package/dist/assets/index-0y6ZxgSv.js +0 -327
  25. /package/src/server/aiChatHandler/{aiEditing.js → aiEditing.ts} +0 -0
  26. /package/src/server/aiChatHandler/{errorHandling.js → errorHandling.ts} +0 -0
  27. /package/src/server/aiChatHandler/{validation.js → validation.ts} +0 -0
  28. /package/src/server/{computeInitialDocument.js → computeInitialDocument.ts} +0 -0
  29. /package/src/server/{config.js → config.ts} +0 -0
  30. /package/src/server/{featureFlags.js → featureFlags.ts} +0 -0
  31. /package/src/server/{handleAIChatMessage.js → handleAIChatMessage.ts} +0 -0
  32. /package/src/server/{handleAICopilot.js → handleAICopilot.ts} +0 -0
  33. /package/src/server/{isDirectory.js → isDirectory.ts} +0 -0
  34. /package/src/server/{livekit.js → livekit.ts} +0 -0
  35. /package/src/server/{setupEnv.js → setupEnv.ts} +0 -0
package/dist/index.html CHANGED
@@ -20,7 +20,7 @@
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-0y6ZxgSv.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-DPAeYEiH.js"></script>
24
24
  <link rel="stylesheet" crossorigin href="/assets/index-DKLiE98a.css">
25
25
  </head>
26
26
  <body>
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
7
- "bin": "src/server/index.js",
7
+ "bin": "src/server/index.ts",
8
8
  "files": [
9
9
  "dist/**",
10
10
  "src/**"
11
11
  ],
12
12
  "scripts": {
13
13
  "test": "vitest run",
14
- "test-interactive": "cd test/sampleDirectories/kitchenSink && node ../../../src/server/index.js",
14
+ "test-interactive": "cd test/sampleDirectories/kitchenSink && node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' ../../../src/server/index.ts",
15
15
  "prettier": "prettier {*.*,**/*.*} --write",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "dev": "cross-env EDITOR_PORT=5173 concurrently \"npm run test-interactive\" \"vite\"",
@@ -115,7 +115,7 @@
115
115
  "@codemirror/theme-one-dark": "^6.1.3",
116
116
  "@codemirror/view": "^6.38.1",
117
117
  "@langchain/core": "^0.3.66",
118
- "@langchain/openai": "^0.6.2",
118
+ "@langchain/openai": "^0.6.3",
119
119
  "@lezer/highlight": "^1.2.1",
120
120
  "@livekit/components-react": "^2.9.14",
121
121
  "@replit/codemirror-indentation-markers": "^6.5.3",
@@ -134,33 +134,34 @@
134
134
  "@uiw/codemirror-theme-xcode": "^4.24.1",
135
135
  "@uiw/codemirror-themes": "^4.24.1",
136
136
  "@valtown/codemirror-ts": "^2.3.1",
137
+ "@vizhub/runtime": "^4.1.0",
137
138
  "@vizhub/viz-types": "^0.3.0",
138
139
  "@vizhub/viz-utils": "^1.3.0",
139
140
  "body-parser": "^2.2.0",
140
141
  "codemirror": "^6.0.2",
141
142
  "codemirror-copilot": "^0.0.7",
142
- "codemirror-ot": "^4.5.0",
143
+ "codemirror-ot": "^4.6.0",
143
144
  "color-hash": "^2.0.2",
144
145
  "comlink": "^4.4.2",
145
146
  "d3-array": "^3.2.4",
146
147
  "diff-match-patch": "^1.0.5",
147
- "dotenv": "^17.2.0",
148
- "editcodewithai": "^2.2.0",
148
+ "dotenv": "^17.2.1",
149
+ "editcodewithai": "^2.3.0",
149
150
  "eslint-linter-browserify": "^9.31.0",
150
151
  "express": "^5.1.0",
151
152
  "ignore": "^7.0.5",
152
153
  "json0-ot-diff": "^1.1.2",
153
154
  "livekit-server-sdk": "^2.13.1",
154
- "llm-code-format": "^2.1.0",
155
+ "llm-code-format": "^3.0.0",
155
156
  "ngrok": "^5.0.0-beta.2",
156
- "npm": "^11.4.2",
157
+ "npm": "^11.5.1",
157
158
  "open": "^10.2.0",
158
159
  "prettier-plugin-svelte": "^3.4.0",
159
160
  "react": "^18",
160
161
  "react-bootstrap": "^2.10.10",
161
162
  "react-dom": "^18",
162
163
  "react-markdown": "^10.1.0",
163
- "react-router-dom": "^7.7.0",
164
+ "react-router-dom": "^7.7.1",
164
165
  "remark-gfm": "^4.0.1",
165
166
  "sharedb": "^5.2.2",
166
167
  "sharedb-client-browser": "^5.2.2",
@@ -169,19 +170,20 @@
169
170
  "ws": "^8.18.3"
170
171
  },
171
172
  "devDependencies": {
172
- "@eslint/js": "^9.31.0",
173
+ "@eslint/js": "^9.32.0",
173
174
  "@types/react": "^18",
174
175
  "@types/react-dom": "^18",
175
176
  "@vitejs/plugin-react": "^4.7.0",
176
177
  "concurrently": "^9.2.0",
177
178
  "cross-env": "^7.0.3",
178
- "eslint": "^9.31.0",
179
+ "eslint": "^9.32.0",
179
180
  "globals": "^16.3.0",
180
- "npm-check-updates": "^18.0.1",
181
+ "npm-check-updates": "^18.0.2",
181
182
  "prettier": "^3.6.2",
182
183
  "sass": "^1.89.2",
184
+ "ts-node": "^10.9.2",
183
185
  "typescript": "^5.8.3",
184
- "vite": "^7.0.5",
186
+ "vite": "^7.0.6",
185
187
  "vitest": "^3.2.4"
186
188
  },
187
189
  "optionalDependencies": {
@@ -166,7 +166,6 @@ export const getOrCreateEditor = async ({
166
166
  paneId = 'root',
167
167
  fileId,
168
168
  shareDBDoc,
169
- content,
170
169
  filesPath,
171
170
  localPresence,
172
171
  docPresence,
@@ -194,10 +193,7 @@ export const getOrCreateEditor = async ({
194
193
  // This can be `undefined` in the case where we are
195
194
  // viewing files read-only, in which case multiplayer
196
195
  // editing is not enabled.
197
- shareDBDoc: ShareDBDoc<VizContent> | undefined;
198
-
199
- // The initial content to present.
200
- content: VizContent;
196
+ shareDBDoc: ShareDBDoc<VizContent>;
201
197
 
202
198
  filesPath: string[];
203
199
  localPresence: any;
@@ -233,6 +229,7 @@ export const getOrCreateEditor = async ({
233
229
  // Compute `text` and `fileExtension` from the ShareDB document.
234
230
  const textPath = [...filesPath, fileId, 'text'];
235
231
  const namePath = [...filesPath, fileId, 'name'];
232
+ const content = shareDBDoc.data;
236
233
  const text = getAtPath(content, textPath);
237
234
  const name = getAtPath(content, namePath);
238
235
 
@@ -91,13 +91,13 @@ export const CodeEditor = ({
91
91
  // Get the editor corresponding to the active file.
92
92
  // Looks in `editorCache` first, and if not found, creates a new editor.
93
93
  useEffect(() => {
94
+ if (!shareDBDoc) return;
94
95
  let isMounted = true;
95
96
 
96
97
  const initEditor = async () => {
97
98
  const editor = await getOrCreateEditor({
98
99
  fileId: activePane.activeFileId,
99
100
  shareDBDoc,
100
- content,
101
101
  filesPath,
102
102
  localPresence,
103
103
  docPresence,
@@ -12,7 +12,7 @@ import {
12
12
  Username,
13
13
  } from '../../types';
14
14
 
15
- const DEBUG = true;
15
+ const DEBUG = false;
16
16
 
17
17
  // Deals with receiving the broadcasted presence cursor locations
18
18
  // from other clients and displaying them.
@@ -57,6 +57,7 @@ export const json1PresenceDisplay = ({
57
57
  // this.scrollToCursor(view);
58
58
  // });
59
59
  // Receive remote presence changes.
60
+ // Receive remote presence changes.
60
61
  docPresence.on(
61
62
  'receive',
62
63
  (id: PresenceId, presence: Presence) => {
@@ -64,37 +65,35 @@ export const json1PresenceDisplay = ({
64
65
  console.log(
65
66
  `Received presence for id ${id}`,
66
67
  presence,
67
- ); // Debug statement
68
+ );
68
69
  }
69
70
 
70
- // If presence === null, the user has disconnected / exited
71
+ // If presence is null, the user has disconnected.
72
+ // We must remove them from our local state.
71
73
  if (!presence) {
72
74
  delete presenceState[id];
73
- return;
74
- }
75
-
76
- // Check if the presence is for the current file or not.
77
- const isPresenceInCurrentFile = pathMatches(
78
- path,
79
- presence,
80
- );
81
-
82
- // If the presence is in the current file, update the presence state.
83
- if (isPresenceInCurrentFile) {
84
- presenceState[id] = presence;
85
- } else if (presence) {
86
- // Otherwise, delete the presence state.
87
- delete presenceState[id];
75
+ // Also remove their cursor position to prevent errors.
76
+ delete this.cursorPosition[id];
77
+ } else {
78
+ // Otherwise, the user is active. Check if the presence
79
+ // is for the current file.
80
+ const isPresenceInCurrentFile = pathMatches(
81
+ path,
82
+ presence,
83
+ );
88
84
 
89
- // Note: Auto-follow tab opening is now handled by usePresenceAutoFollow hook
90
- // in VZCodeContext, which works independently of CodeMirror extensions
85
+ // If it's the current file, add/update their state.
86
+ if (isPresenceInCurrentFile) {
87
+ presenceState[id] = presence;
88
+ } else {
89
+ // If it's for another file, remove them from this view's state.
90
+ delete presenceState[id];
91
+ delete this.cursorPosition[id];
92
+ }
91
93
  }
92
- // Update decorations to reflect new presence state.
93
- // TODO consider mutating this rather than recomputing it on each change.
94
94
 
95
95
  const presenceDecorations = [];
96
96
 
97
- // Object.keys(presenceState).map((id) => {
98
97
  for (const id of Object.keys(presenceState)) {
99
98
  const presence: Presence = presenceState[id];
100
99
  const { start, end } = presence;
@@ -104,7 +103,7 @@ export const json1PresenceDisplay = ({
104
103
  presence.username,
105
104
  );
106
105
  const { username } = presence;
107
- //console.log("File User Color:" + assignUserColor(presence.username));
106
+
108
107
  presenceDecorations.push({
109
108
  from,
110
109
  to: from,
@@ -112,10 +111,6 @@ export const json1PresenceDisplay = ({
112
111
  side: -1,
113
112
  block: false,
114
113
  widget: new PresenceWidget(
115
- // TODO see if we can figure out why
116
- // updateDOM was not being called when passing
117
- // the presence id as the id
118
- // id,
119
114
  '' + Math.random(),
120
115
  userColor,
121
116
  username,
@@ -123,10 +118,7 @@ export const json1PresenceDisplay = ({
123
118
  }),
124
119
  });
125
120
 
126
- // This is `true` when the presence is a cursor,
127
- // with no selection.
128
121
  if (from !== to) {
129
- // This is the case when the presence is a selection.
130
122
  presenceDecorations.push({
131
123
  from,
132
124
  to,
@@ -142,34 +134,26 @@ export const json1PresenceDisplay = ({
142
134
  });
143
135
  }
144
136
  if (view.state.doc.length >= from) {
145
- // Ensure position is valid
146
- this.cursorPosition[id] = from; // Store the cursor position, important to run if we cant get the regular scroll to work
147
- // console.log(`Stored cursor position for id ${id}: ${from}`); // Debug statement
137
+ this.cursorPosition[id] = from;
148
138
  } else {
149
- // console.warn(`Invalid cursor position for id ${id}: ${from}`); // Debug statement
139
+ // The cursor position is invalid, so remove it.
140
+ delete this.cursorPosition[id];
150
141
  }
151
142
  }
152
143
 
153
144
  this.decorations = Decoration.set(
154
145
  presenceDecorations,
155
- // Without this argument, we get the following error:
156
- // Uncaught Error: Ranges must be added sorted by `from` position and `startSide`
157
146
  true,
158
147
  );
159
148
 
160
- // Somehow this triggers re-rendering of the Decorations.
161
- // Not sure if this is the correct usage of the API.
162
- // Inspired by https://github.com/yjs/y-codemirror.next/blob/main/src/y-remote-selections.js
163
- // Set timeout so that the current CodeMirror update finishes
164
- // before the next ones that render presence begin.
149
+ // This dispatch triggers the re-rendering of decorations.
150
+ // It now correctly runs after a deletion.
165
151
  setTimeout(() => {
166
152
  view.dispatch({
167
153
  annotations: [presenceAnnotation.of(true)],
168
154
  });
169
155
  }, 0);
170
156
 
171
- // Auto-follow all users when their presence is broadcast
172
- // by scrolling them into view.
173
157
  if (enableAutoFollowRef.current) {
174
158
  this.scrollToCursor(view);
175
159
  }
@@ -1,14 +1,75 @@
1
- // Displays the Vite devserver iframe.
2
- // TODO test this out, think this through.
3
- // Purpose: support folks using VZCode as the editor
4
- // for their Vite-based projects.
5
- const enableIframe = false;
1
+ import { useContext, useEffect, useMemo } from 'react';
2
+ import {
3
+ createRuntime,
4
+ VizHubRuntime,
5
+ } from '@vizhub/runtime';
6
+ import BuildWorker from './buildWorker?worker';
7
+ import { VZCodeContext } from './VZCodeContext';
8
+ import { vizFilesToFileCollection } from '@vizhub/viz-utils';
9
+
10
+ const enableIframe = true;
11
+
12
+ // Singleton runtime instance
13
+ // TODO use refs, and cleanup on unmount
14
+ let runtime: VizHubRuntime = null;
15
+ let isFirstRun = true;
6
16
 
7
17
  export const VZRight = () => {
18
+ // Get access to the current files.
19
+ const { content } = useContext(VZCodeContext);
20
+
21
+ const files = useMemo(
22
+ () =>
23
+ content
24
+ ? vizFilesToFileCollection(content.files)
25
+ : null,
26
+ [content?.files],
27
+ );
28
+
29
+ const isInteracting = content?.isInteracting || false;
30
+
31
+ useEffect(() => {
32
+ if (!files) return;
33
+
34
+ // Initialize the runtime only once
35
+ if (!runtime) {
36
+ const worker = new BuildWorker();
37
+ const iframe = document.getElementById(
38
+ 'viz-iframe',
39
+ ) as HTMLIFrameElement;
40
+ runtime = createRuntime({
41
+ iframe,
42
+ worker,
43
+ setBuildErrorMessage: (error) => {
44
+ if (error) {
45
+ console.error('Build error:', error);
46
+ }
47
+ },
48
+ });
49
+ }
50
+
51
+ // Run code in the iframe
52
+ if (isFirstRun || isInteracting) {
53
+ runtime.run({
54
+ files,
55
+ enableHotReloading: true,
56
+ enableSourcemap: true,
57
+ vizId: 'example-viz',
58
+ });
59
+ isFirstRun = false;
60
+ }
61
+
62
+ // TODO use refs, and cleanup on unmount
63
+ // return () => {
64
+ // runtime.cleanup();
65
+ // worker.terminate();
66
+ // };
67
+ }, [files, isInteracting]);
68
+
8
69
  return (
9
70
  <div className="right">
10
71
  {enableIframe ? (
11
- <iframe src="http://localhost:5173/"></iframe>
72
+ <iframe id="viz-iframe"></iframe>
12
73
  ) : null}
13
74
  </div>
14
75
  );
@@ -1,4 +1,9 @@
1
- import { useRef, useEffect, useCallback } from 'react';
1
+ import {
2
+ useRef,
3
+ useEffect,
4
+ useCallback,
5
+ memo,
6
+ } from 'react';
2
7
  import { Form, Button } from '../../bootstrap';
3
8
 
4
9
  interface ChatInputProps {
@@ -9,7 +14,7 @@ interface ChatInputProps {
9
14
  focused: boolean;
10
15
  }
11
16
 
12
- export const ChatInput = ({
17
+ const ChatInputComponent = ({
13
18
  message,
14
19
  setMessage,
15
20
  onSendMessage,
@@ -35,6 +40,13 @@ export const ChatInput = ({
35
40
  [onSendMessage],
36
41
  );
37
42
 
43
+ const handleChange = useCallback(
44
+ (event: React.ChangeEvent<HTMLTextAreaElement>) => {
45
+ setMessage(event.target.value);
46
+ },
47
+ [setMessage],
48
+ );
49
+
38
50
  return (
39
51
  <div className="ai-chat-input-container">
40
52
  <Form.Group className="ai-chat-input-group">
@@ -42,9 +54,7 @@ export const ChatInput = ({
42
54
  as="textarea"
43
55
  rows={2}
44
56
  value={message}
45
- onChange={(event) =>
46
- setMessage(event.target.value)
47
- }
57
+ onChange={handleChange}
48
58
  onKeyDown={handleKeyDown}
49
59
  ref={inputRef}
50
60
  placeholder="Describe what you'd like me to change in your code..."
@@ -63,3 +73,5 @@ export const ChatInput = ({
63
73
  </div>
64
74
  );
65
75
  };
76
+
77
+ export const ChatInput = memo(ChatInputComponent);
@@ -1,6 +1,7 @@
1
1
  import { timestampToDate } from '@vizhub/viz-utils';
2
2
  import Markdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
+ import { useMemo, memo } from 'react';
4
5
 
5
6
  interface MessageProps {
6
7
  id: string;
@@ -10,27 +11,39 @@ interface MessageProps {
10
11
  isStreaming?: boolean;
11
12
  }
12
13
 
13
- export const Message = ({
14
+ const MessageComponent = ({
14
15
  role,
15
16
  content,
16
17
  timestamp,
17
18
  isStreaming,
18
19
  }: MessageProps) => {
20
+ // Memoize date formatting to avoid repeated computation
21
+ const formattedTime = useMemo(() => {
22
+ return timestampToDate(timestamp).toLocaleTimeString(
23
+ [],
24
+ {
25
+ hour: '2-digit',
26
+ minute: '2-digit',
27
+ },
28
+ );
29
+ }, [timestamp]);
30
+
31
+ // Memoize the className string to avoid recreation
32
+ const messageClassName = useMemo(() => {
33
+ return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
34
+ }, [role, isStreaming]);
19
35
  return (
20
- <div
21
- className={`ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`}
22
- >
36
+ <div className={messageClassName}>
23
37
  <div className="ai-chat-message-content">
24
38
  <Markdown remarkPlugins={[remarkGfm]}>
25
39
  {content}
26
40
  </Markdown>
27
41
  </div>
28
42
  <div className="ai-chat-message-time">
29
- {timestampToDate(timestamp).toLocaleTimeString([], {
30
- hour: '2-digit',
31
- minute: '2-digit',
32
- })}
43
+ {formattedTime}
33
44
  </div>
34
45
  </div>
35
46
  );
36
47
  };
48
+
49
+ export const Message = memo(MessageComponent);
@@ -1,4 +1,9 @@
1
- import { useRef, useEffect } from 'react';
1
+ import {
2
+ useRef,
3
+ useEffect,
4
+ useCallback,
5
+ memo,
6
+ } from 'react';
2
7
  import { Message } from './Message';
3
8
  import { StreamingMessage } from './StreamingMessage';
4
9
  import { TypingIndicator } from './TypingIndicator';
@@ -17,7 +22,7 @@ interface MessageListProps {
17
22
  isLoading: boolean;
18
23
  }
19
24
 
20
- export const MessageList = ({
25
+ const MessageListComponent = ({
21
26
  messages,
22
27
  aiScratchpad,
23
28
  aiStatus,
@@ -25,11 +30,11 @@ export const MessageList = ({
25
30
  }: MessageListProps) => {
26
31
  const messagesEndRef = useRef<HTMLDivElement>(null);
27
32
 
28
- const scrollToBottom = () => {
33
+ const scrollToBottom = useCallback(() => {
29
34
  messagesEndRef.current?.scrollIntoView({
30
35
  behavior: 'smooth',
31
36
  });
32
- };
37
+ }, []);
33
38
 
34
39
  useEffect(() => {
35
40
  scrollToBottom();
@@ -60,3 +65,5 @@ export const MessageList = ({
60
65
  </div>
61
66
  );
62
67
  };
68
+
69
+ export const MessageList = memo(MessageListComponent);
@@ -1,12 +1,13 @@
1
1
  import Markdown from 'react-markdown';
2
2
  import remarkGfm from 'remark-gfm';
3
+ import { memo } from 'react';
3
4
 
4
5
  interface StreamingMessageProps {
5
6
  content: string;
6
7
  status?: string;
7
8
  }
8
9
 
9
- export const StreamingMessage = ({
10
+ const StreamingMessageComponent = ({
10
11
  content,
11
12
  status,
12
13
  }: StreamingMessageProps) => {
@@ -23,3 +24,7 @@ export const StreamingMessage = ({
23
24
  </div>
24
25
  );
25
26
  };
27
+
28
+ export const StreamingMessage = memo(
29
+ StreamingMessageComponent,
30
+ );
@@ -1,4 +1,6 @@
1
- export const TypingIndicator = () => {
1
+ import { memo } from 'react';
2
+
3
+ const TypingIndicatorComponent = () => {
2
4
  return (
3
5
  <div className="ai-chat-message assistant">
4
6
  <div className="ai-chat-message-content">
@@ -11,3 +13,7 @@ export const TypingIndicator = () => {
11
13
  </div>
12
14
  );
13
15
  };
16
+
17
+ export const TypingIndicator = memo(
18
+ TypingIndicatorComponent,
19
+ );
@@ -1,4 +1,9 @@
1
- import { useContext, useState, useCallback } from 'react';
1
+ import {
2
+ useContext,
3
+ useState,
4
+ useCallback,
5
+ useMemo,
6
+ } from 'react';
2
7
  import { VZCodeContext } from '../../VZCodeContext';
3
8
  import { v4 as uuidv4 } from 'uuid';
4
9
  import { MessageList } from './MessageList';
@@ -25,11 +30,15 @@ export const AIChat = () => {
25
30
  const aiScratchpad = currentChat?.aiScratchpad;
26
31
  const aiStatus = currentChat?.aiStatus;
27
32
 
28
- // Transform messages to ensure they have required id field
29
- const messages = rawMessages.map((msg, index) => ({
30
- ...msg,
31
- id: msg.id || `msg-${index}`,
32
- }));
33
+ // Transform messages to ensure they have required id field - memoized to avoid recreation
34
+ const messages = useMemo(
35
+ () =>
36
+ rawMessages.map((msg, index) => ({
37
+ ...msg,
38
+ id: msg.id || `msg-${index}`,
39
+ })),
40
+ [rawMessages],
41
+ );
33
42
 
34
43
  const handleSendMessage = useCallback(async () => {
35
44
  if (!message.trim() || isLoading) return;
@@ -66,7 +75,13 @@ export const AIChat = () => {
66
75
  } finally {
67
76
  setIsLoading(false);
68
77
  }
69
- }, [message, isLoading, currentChatId]);
78
+ }, [
79
+ message,
80
+ isLoading,
81
+ aiChatEndpoint,
82
+ aiChatOptions,
83
+ currentChatId,
84
+ ]);
70
85
 
71
86
  return (
72
87
  <div className="ai-chat-container">
@@ -0,0 +1,3 @@
1
+ // @ts-ignore TODO fix this type error
2
+ import { initWorker } from '@vizhub/runtime/worker';
3
+ initWorker();
@@ -1,9 +1,8 @@
1
- import { useContext, useEffect, useRef } from 'react';
2
- import { VZCodeContext } from './VZCodeContext';
3
- import { Presence, PresenceId, TabState } from '../types';
1
+ import { useEffect, useRef } from 'react';
2
+ import { Presence, PresenceId } from '../types';
4
3
  import { VizFileId } from '@vizhub/viz-types';
5
4
 
6
- const DEBUG = true;
5
+ const DEBUG = false;
7
6
 
8
7
  /**
9
8
  * Custom hook that handles auto-following users by opening tabs when presence
@@ -213,12 +213,15 @@ export const setIsInteracting = (
213
213
  /**
214
214
  * Finds a file ID by searching for a matching file name
215
215
  */
216
- export const resolveFileId = (fileName, shareDBDoc) => {
216
+ export const resolveFileId = (
217
+ fileName: string,
218
+ shareDBDoc: any,
219
+ ) => {
217
220
  const files = shareDBDoc.data.files;
218
221
 
219
222
  // Search through all files to find matching name
220
223
  for (const [fileId, file] of Object.entries(files)) {
221
- if (file.name === fileName) {
224
+ if ((file as any).name === fileName) {
222
225
  return fileId;
223
226
  }
224
227
  }