vzcode 1.40.0 → 1.42.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-DKLiE98a.css → index-CP9o_hN5.css} +1 -1
  4. package/dist/assets/index-kPLgjbcb.js +329 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +17 -16
  7. package/src/client/App/style.scss +2 -2
  8. package/src/client/CodeEditor/getOrCreateEditor.ts +2 -5
  9. package/src/client/CodeEditor/index.tsx +1 -1
  10. package/src/client/CodeEditor/json1PresenceDisplay.ts +40 -12
  11. package/src/client/VZRight.tsx +68 -6
  12. package/src/client/VZSidebar/AIChat/ChatInput.tsx +17 -5
  13. package/src/client/VZSidebar/AIChat/Message.tsx +21 -8
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +29 -31
  15. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +6 -1
  16. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +7 -1
  17. package/src/client/VZSidebar/AIChat/index.tsx +22 -9
  18. package/src/client/VZSidebar/aiCopyPaste.ts +8 -1
  19. package/src/client/VZSidebar/index.tsx +10 -8
  20. package/src/client/buildWorker.ts +3 -0
  21. package/src/client/vzReducer/searchReducer.test.ts +250 -0
  22. package/src/client/vzReducer/searchReducer.ts +3 -1
  23. package/src/{ot.js → ot.ts} +11 -0
  24. package/src/runCode.ts +4 -23
  25. package/src/server/aiChatHandler/aiEditing.ts +10 -7
  26. package/src/server/aiChatHandler/chatOperations.ts +144 -58
  27. package/src/server/aiChatHandler/errorHandling.ts +1 -1
  28. package/src/server/aiChatHandler/index.ts +8 -2
  29. package/src/server/aiChatHandler/llmStreaming.ts +83 -44
  30. package/src/submitOperation.ts +1 -1
  31. package/dist/assets/index-ubSLcdJf.js +0 -327
  32. package/src/client/diff.js +0 -12
  33. package/src/runCode.js +0 -48
  34. package/src/submitOperation.js +0 -16
  35. /package/src/{randomId.js → randomId.ts} +0 -0
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-ubSLcdJf.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-DKLiE98a.css">
23
+ <script type="module" crossorigin src="/assets/index-kPLgjbcb.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-CP9o_hN5.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.40.0",
3
+ "version": "1.42.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -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",
149
- "eslint-linter-browserify": "^9.31.0",
148
+ "dotenv": "^17.2.1",
149
+ "editcodewithai": "^2.3.0",
150
+ "eslint-linter-browserify": "^9.32.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,24 +170,24 @@
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
- "cross-env": "^7.0.3",
178
- "eslint": "^9.31.0",
178
+ "cross-env": "^10.0.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",
183
184
  "ts-node": "^10.9.2",
184
185
  "typescript": "^5.8.3",
185
- "vite": "^7.0.5",
186
+ "vite": "^7.0.6",
186
187
  "vitest": "^3.2.4"
187
188
  },
188
189
  "optionalDependencies": {
189
- "@rollup/rollup-darwin-arm64": "^4.45.1",
190
- "@rollup/rollup-win32-x64-msvc": "^4.45.1"
190
+ "@rollup/rollup-darwin-arm64": "^4.46.1",
191
+ "@rollup/rollup-win32-x64-msvc": "^4.46.1"
191
192
  }
192
193
  }
@@ -70,12 +70,12 @@ body,
70
70
  }
71
71
 
72
72
  .right {
73
+ display: flex;
73
74
  flex: 1;
74
75
  background-color: white;
75
76
 
76
77
  iframe {
77
- width: 100%;
78
- height: 100%;
78
+ flex: 1;
79
79
  border: none;
80
80
  }
81
81
  }
@@ -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,
@@ -41,6 +41,12 @@ export const json1PresenceDisplay = ({
41
41
  //Added variable for cursor position
42
42
  cursorPosition = {};
43
43
 
44
+ // Flag to prevent multiple pending updates
45
+ pendingUpdate = false;
46
+
47
+ // Flag to prevent multiple pending scroll updates
48
+ pendingScrollUpdate = false;
49
+
44
50
  constructor(view: EditorView) {
45
51
  // Initialize decorations to empty array so CodeMirror doesn't crash.
46
52
  this.decorations = RangeSet.of([]);
@@ -146,13 +152,22 @@ export const json1PresenceDisplay = ({
146
152
  true,
147
153
  );
148
154
 
149
- // This dispatch triggers the re-rendering of decorations.
150
- // It now correctly runs after a deletion.
151
- setTimeout(() => {
152
- view.dispatch({
153
- annotations: [presenceAnnotation.of(true)],
155
+ // Safely dispatch decoration updates without causing race conditions
156
+ // Use requestAnimationFrame to ensure we're not in the middle of an update
157
+ if (!this.pendingUpdate) {
158
+ this.pendingUpdate = true;
159
+ requestAnimationFrame(() => {
160
+ this.pendingUpdate = false;
161
+ // Check if view is still valid and not currently updating
162
+ if (view.state && view.dom.isConnected) {
163
+ view.dispatch({
164
+ annotations: [
165
+ presenceAnnotation.of(true),
166
+ ],
167
+ });
168
+ }
154
169
  });
155
- }, 0);
170
+ }
156
171
 
157
172
  if (enableAutoFollowRef.current) {
158
173
  this.scrollToCursor(view);
@@ -162,12 +177,25 @@ export const json1PresenceDisplay = ({
162
177
  }
163
178
  // Method to scroll the view to keep the cursor in view
164
179
  scrollToCursor(view) {
165
- for (const id in this.cursorPosition) {
166
- //getting the cursor position of the other cursor
167
- const cursorPos = this.cursorPosition[id];
168
- view.dispatch({
169
- //if the other person's cursor has jumped off screen, we will follow it by scrolling there directly.
170
- effects: EditorView.scrollIntoView(cursorPos),
180
+ // Debounce scroll updates to prevent conflicts with typing
181
+ if (!this.pendingScrollUpdate) {
182
+ this.pendingScrollUpdate = true;
183
+ requestAnimationFrame(() => {
184
+ this.pendingScrollUpdate = false;
185
+ // Check if view is still valid
186
+ if (view.state && view.dom.isConnected) {
187
+ for (const id in this.cursorPosition) {
188
+ //getting the cursor position of the other cursor
189
+ const cursorPos = this.cursorPosition[id];
190
+ view.dispatch({
191
+ //if the other person's cursor has jumped off screen, we will follow it by scrolling there directly.
192
+ effects:
193
+ EditorView.scrollIntoView(cursorPos),
194
+ });
195
+ // Only scroll to the first cursor to avoid multiple dispatches
196
+ break;
197
+ }
198
+ }
171
199
  });
172
200
  }
173
201
  }
@@ -1,14 +1,76 @@
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 {
2
+ useContext,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ } from 'react';
7
+ import {
8
+ createRuntime,
9
+ VizHubRuntime,
10
+ } from '@vizhub/runtime';
11
+ import BuildWorker from './buildWorker?worker';
12
+ import { VZCodeContext } from './VZCodeContext';
13
+ import { vizFilesToFileCollection } from '@vizhub/viz-utils';
14
+
15
+ const enableIframe = true;
6
16
 
7
17
  export const VZRight = () => {
18
+ const iframeRef = useRef<HTMLIFrameElement>(null);
19
+ const runtimeRef = useRef<VizHubRuntime | null>(null);
20
+ const isFirstRunRef = useRef(true);
21
+
22
+ // Get access to the current files.
23
+ const { content } = useContext(VZCodeContext);
24
+
25
+ const files = useMemo(
26
+ () =>
27
+ content
28
+ ? vizFilesToFileCollection(content.files)
29
+ : null,
30
+ [content?.files],
31
+ );
32
+
33
+ const isInteracting = content?.isInteracting || false;
34
+
35
+ useEffect(() => {
36
+ if (!files) return;
37
+
38
+ // Initialize the runtime only once
39
+ if (!runtimeRef.current && iframeRef.current) {
40
+ const worker = new BuildWorker();
41
+ runtimeRef.current = createRuntime({
42
+ iframe: iframeRef.current,
43
+ worker,
44
+ setBuildErrorMessage: (error) => {
45
+ if (error) {
46
+ console.error('Build error:', error);
47
+ }
48
+ },
49
+ });
50
+ }
51
+
52
+ // Run code in the iframe
53
+ if (isFirstRunRef.current || isInteracting) {
54
+ runtimeRef.current?.run({
55
+ files,
56
+ enableHotReloading: true,
57
+ enableSourcemap: true,
58
+ vizId: 'example-viz',
59
+ });
60
+ isFirstRunRef.current = false;
61
+ }
62
+
63
+ // TODO use refs, and cleanup on unmount
64
+ // return () => {
65
+ // runtime.cleanup();
66
+ // worker.terminate();
67
+ // };
68
+ }, [files, isInteracting]);
69
+
8
70
  return (
9
71
  <div className="right">
10
72
  {enableIframe ? (
11
- <iframe src="http://localhost:5173/"></iframe>
73
+ <iframe ref={iframeRef}></iframe>
12
74
  ) : null}
13
75
  </div>
14
76
  );
@@ -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,39 +1,43 @@
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
- import { StreamingMessage } from './StreamingMessage';
4
8
  import { TypingIndicator } from './TypingIndicator';
9
+ import { VizChatMessage } from '@vizhub/viz-types';
5
10
 
6
- interface MessageData {
7
- id: string;
8
- role: 'user' | 'assistant';
9
- content: string;
10
- timestamp: number;
11
- }
12
-
13
- interface MessageListProps {
14
- messages: MessageData[];
15
- aiScratchpad?: string;
16
- aiStatus?: string;
17
- isLoading: boolean;
18
- }
19
-
20
- export const MessageList = ({
11
+ const MessageListComponent = ({
21
12
  messages,
22
- aiScratchpad,
23
13
  aiStatus,
24
14
  isLoading,
25
- }: MessageListProps) => {
15
+ }: {
16
+ messages: VizChatMessage[];
17
+ aiStatus?: string;
18
+ isLoading: boolean;
19
+ }) => {
26
20
  const messagesEndRef = useRef<HTMLDivElement>(null);
27
21
 
28
- const scrollToBottom = () => {
22
+ const scrollToBottom = useCallback(() => {
29
23
  messagesEndRef.current?.scrollIntoView({
30
24
  behavior: 'smooth',
31
25
  });
32
- };
26
+ }, []);
33
27
 
34
28
  useEffect(() => {
35
29
  scrollToBottom();
36
- }, [messages, aiScratchpad]);
30
+ }, [messages]);
31
+
32
+ // Check if AI generation has started (last message is from assistant)
33
+ const lastMessage = messages[messages.length - 1];
34
+ const aiGenerationStarted =
35
+ lastMessage?.role === 'assistant' &&
36
+ lastMessage.content !== '';
37
+
38
+ // Show typing indicator only when loading and AI generation hasn't started yet
39
+ const showTypingIndicator =
40
+ isLoading && !aiGenerationStarted;
37
41
 
38
42
  return (
39
43
  <div className="ai-chat-messages">
@@ -47,16 +51,10 @@ export const MessageList = ({
47
51
  />
48
52
  ))}
49
53
 
50
- {/* Show streaming content if available */}
51
- {aiScratchpad && (
52
- <StreamingMessage
53
- content={aiScratchpad}
54
- status={aiStatus}
55
- />
56
- )}
57
-
58
- {isLoading && !aiScratchpad && <TypingIndicator />}
54
+ {showTypingIndicator && <TypingIndicator />}
59
55
  <div ref={messagesEndRef} />
60
56
  </div>
61
57
  );
62
58
  };
59
+
60
+ 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';
@@ -22,14 +27,17 @@ export const AIChat = () => {
22
27
  // Get current chat data from content
23
28
  const currentChat = content?.chats?.[currentChatId];
24
29
  const rawMessages = currentChat?.messages || [];
25
- const aiScratchpad = currentChat?.aiScratchpad;
26
30
  const aiStatus = currentChat?.aiStatus;
27
31
 
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
- }));
32
+ // Transform messages to ensure they have required id field - memoized to avoid recreation
33
+ const messages = useMemo(
34
+ () =>
35
+ rawMessages.map((msg, index) => ({
36
+ ...msg,
37
+ id: msg.id || `msg-${index}`,
38
+ })),
39
+ [rawMessages],
40
+ );
33
41
 
34
42
  const handleSendMessage = useCallback(async () => {
35
43
  if (!message.trim() || isLoading) return;
@@ -66,13 +74,18 @@ export const AIChat = () => {
66
74
  } finally {
67
75
  setIsLoading(false);
68
76
  }
69
- }, [message, isLoading, currentChatId]);
77
+ }, [
78
+ message,
79
+ isLoading,
80
+ aiChatEndpoint,
81
+ aiChatOptions,
82
+ currentChatId,
83
+ ]);
70
84
 
71
85
  return (
72
86
  <div className="ai-chat-container">
73
87
  <MessageList
74
88
  messages={messages}
75
- aiScratchpad={aiScratchpad}
76
89
  aiStatus={aiStatus}
77
90
  isLoading={isLoading}
78
91
  />
@@ -17,7 +17,14 @@ export const createAICopyPasteHandlers = (
17
17
  ) => {
18
18
  // Copy for AI - formats all files and copies to clipboard
19
19
  const handleCopyForAI = async () => {
20
- if (!files) return;
20
+ if (!files || Object.keys(files).length === 0) {
21
+ setCopyButtonText('No files to copy');
22
+ setTimeout(
23
+ () => setCopyButtonText('Copy for AI'),
24
+ 2000,
25
+ );
26
+ return;
27
+ }
21
28
 
22
29
  try {
23
30
  const fileCollection =
@@ -546,15 +546,17 @@ export const VZSidebar = ({
546
546
  )}
547
547
  </div>
548
548
  </div>
549
- {!isAIChatOpen && !isSearchOpen && filesExist && (
549
+ {!isAIChatOpen && !isSearchOpen && (
550
550
  <div className="ai-buttons">
551
- <button
552
- className="ai-button copy-button"
553
- onClick={handleCopyForAI}
554
- title="Copy files formatted for AI"
555
- >
556
- {copyButtonText}
557
- </button>
551
+ {filesExist && (
552
+ <button
553
+ className="ai-button copy-button"
554
+ onClick={handleCopyForAI}
555
+ title="Copy files formatted for AI"
556
+ >
557
+ {copyButtonText}
558
+ </button>
559
+ )}
558
560
  <button
559
561
  className="ai-button paste-button"
560
562
  onClick={handlePasteForAI}
@@ -0,0 +1,3 @@
1
+ // @ts-ignore TODO fix this type error
2
+ import { initWorker } from '@vizhub/runtime/worker';
3
+ initWorker();