vzcode 1.32.0 → 1.34.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 (44) hide show
  1. package/README.md +0 -1
  2. package/dist/assets/index-ajiBaTmx.js +263 -0
  3. package/dist/assets/index-lvLw6dCW.css +1 -0
  4. package/dist/assets/worker-BrwN8AXx.js +330 -0
  5. package/dist/assets/worker-CkHcCP1n.js +136 -0
  6. package/dist/assets/worker-DkYJN5WQ.js +453 -0
  7. package/dist/index.html +2 -2
  8. package/package.json +42 -37
  9. package/src/client/CodeEditor/getOrCreateEditor.ts +18 -5
  10. package/src/client/Icons/MicSVG.tsx +1 -1
  11. package/src/client/VZCodeContext.tsx +18 -2
  12. package/src/client/VZSidebar/AIChat/ChatInput.tsx +65 -0
  13. package/src/client/VZSidebar/AIChat/Message.tsx +36 -0
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +62 -0
  15. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +25 -0
  16. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +13 -0
  17. package/src/client/VZSidebar/AIChat/index.tsx +75 -0
  18. package/src/client/VZSidebar/AIChat/styles.scss +256 -0
  19. package/src/client/VZSidebar/index.tsx +53 -8
  20. package/src/client/VZSidebar/styles.scss +8 -1
  21. package/src/client/diff.js +1 -1
  22. package/src/client/featureFlags.ts +2 -0
  23. package/src/client/useActions.ts +20 -0
  24. package/src/client/useEditorCache.ts +1 -0
  25. package/src/client/useFileCRUD.ts +47 -0
  26. package/src/client/useOpenDirectories.ts +43 -12
  27. package/src/client/utils/fileExtension.ts +10 -0
  28. package/src/client/vzReducer/aiChatReducer.ts +31 -0
  29. package/src/client/vzReducer/createInitialState.ts +2 -0
  30. package/src/client/vzReducer/index.ts +20 -0
  31. package/src/ot.js +10 -2
  32. package/src/server/aiChatHandler/aiEditing.js +42 -0
  33. package/src/server/aiChatHandler/chatOperations.js +195 -0
  34. package/src/server/aiChatHandler/errorHandling.js +53 -0
  35. package/src/server/aiChatHandler/index.js +69 -0
  36. package/src/server/aiChatHandler/llmStreaming.js +105 -0
  37. package/src/server/aiChatHandler/validation.js +24 -0
  38. package/src/server/handleAIChatMessage.js +2 -0
  39. package/src/server/index.js +8 -0
  40. package/dist/assets/index-DTtcN2MP.css +0 -1
  41. package/dist/assets/index-sreAszZh.js +0 -240
  42. package/dist/assets/worker-BKfYWklR.js +0 -136
  43. package/dist/assets/worker-COyDRdqW.js +0 -453
  44. package/dist/assets/worker-CWsWXMyk.js +0 -335
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-sreAszZh.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-DTtcN2MP.css">
23
+ <script type="module" crossorigin src="/assets/index-ajiBaTmx.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-lvLw6dCW.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.32.0",
3
+ "version": "1.34.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -108,78 +108,83 @@
108
108
  "@codemirror/lang-css": "^6.3.1",
109
109
  "@codemirror/lang-html": "^6.4.9",
110
110
  "@codemirror/lang-javascript": "^6.2.4",
111
- "@codemirror/lang-json": "^6.0.1",
112
- "@codemirror/lang-markdown": "^6.3.2",
111
+ "@codemirror/lang-json": "^6.0.2",
112
+ "@codemirror/lang-markdown": "^6.3.3",
113
113
  "@codemirror/lint": "^6.8.5",
114
114
  "@codemirror/state": "^6.5.2",
115
- "@codemirror/theme-one-dark": "^6.1.2",
116
- "@codemirror/view": "^6.37.1",
117
- "@langchain/core": "^0.3.57",
118
- "@langchain/openai": "^0.5.12",
115
+ "@codemirror/theme-one-dark": "^6.1.3",
116
+ "@codemirror/view": "^6.38.1",
117
+ "@langchain/core": "^0.3.65",
118
+ "@langchain/openai": "^0.6.2",
119
119
  "@lezer/highlight": "^1.2.1",
120
- "@livekit/components-react": "^2.9.9",
120
+ "@livekit/components-react": "^2.9.14",
121
121
  "@replit/codemirror-indentation-markers": "^6.5.3",
122
122
  "@replit/codemirror-interact": "^6.3.1",
123
123
  "@replit/codemirror-lang-svelte": "^6.0.0",
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.23.12",
128
- "@uiw/codemirror-theme-dracula": "^4.23.12",
129
- "@uiw/codemirror-theme-eclipse": "^4.23.12",
130
- "@uiw/codemirror-theme-github": "^4.23.12",
131
- "@uiw/codemirror-theme-material": "^4.23.12",
132
- "@uiw/codemirror-theme-nord": "^4.23.12",
133
- "@uiw/codemirror-theme-okaidia": "^4.23.12",
134
- "@uiw/codemirror-theme-xcode": "^4.23.12",
135
- "@uiw/codemirror-themes": "^4.23.12",
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",
136
136
  "@valtown/codemirror-ts": "^2.3.1",
137
- "@vizhub/viz-types": "^0.1.0",
137
+ "@vizhub/viz-types": "^0.3.0",
138
+ "@vizhub/viz-utils": "^1.3.0",
138
139
  "body-parser": "^2.2.0",
139
- "codemirror": "^6.0.1",
140
+ "codemirror": "^6.0.2",
140
141
  "codemirror-copilot": "^0.0.7",
141
142
  "codemirror-ot": "^4.4.0",
142
143
  "color-hash": "^2.0.2",
143
144
  "comlink": "^4.4.2",
144
145
  "d3-array": "^3.2.4",
145
146
  "diff-match-patch": "^1.0.5",
146
- "dotenv": "^16.5.0",
147
- "eslint": "^9.28.0",
148
- "eslint-linter-browserify": "^9.28.0",
147
+ "dotenv": "^17.2.0",
148
+ "editcodewithai": "^2.0.0",
149
+ "eslint": "^9.31.0",
150
+ "eslint-linter-browserify": "^9.31.0",
149
151
  "express": "^5.1.0",
150
- "globals": "^16.2.0",
152
+ "globals": "^16.3.0",
151
153
  "ignore": "^7.0.5",
152
154
  "json0-ot-diff": "^1.1.2",
153
- "livekit-server-sdk": "^2.13.0",
155
+ "livekit-server-sdk": "^2.13.1",
156
+ "llm-code-format": "^2.0.1",
154
157
  "ngrok": "^5.0.0-beta.2",
155
- "npm": "^11.4.1",
156
- "open": "^10.1.2",
158
+ "npm": "^11.4.2",
159
+ "open": "^10.2.0",
157
160
  "prettier-plugin-svelte": "^3.4.0",
158
161
  "react": "^18",
159
162
  "react-bootstrap": "^2.10.10",
160
163
  "react-dom": "^18",
161
- "react-router-dom": "^7.6.2",
164
+ "react-markdown": "^10.1.0",
165
+ "react-router-dom": "^7.7.0",
166
+ "remark-gfm": "^4.0.1",
162
167
  "sharedb": "^5.2.2",
163
168
  "sharedb-client-browser": "^5.2.2",
164
169
  "uuid": "^11.1.0",
165
- "vizhub-ui": "^3.23.0",
166
- "ws": "^8.18.2"
170
+ "vizhub-ui": "^3.24.0",
171
+ "ws": "^8.18.3"
167
172
  },
168
173
  "devDependencies": {
169
174
  "@types/react": "^18",
170
175
  "@types/react-dom": "^18",
171
- "@vitejs/plugin-react": "^4.5.1",
172
- "concurrently": "^9.1.2",
176
+ "@vitejs/plugin-react": "^4.7.0",
177
+ "concurrently": "^9.2.0",
173
178
  "cross-env": "^7.0.3",
174
179
  "npm-check-updates": "^18.0.1",
175
- "prettier": "^3.5.3",
176
- "sass": "^1.89.1",
180
+ "prettier": "^3.6.2",
181
+ "sass": "^1.89.2",
177
182
  "typescript": "^5.8.3",
178
- "vite": "^6.3.5",
179
- "vitest": "^3.2.2"
183
+ "vite": "^7.0.5",
184
+ "vitest": "^3.2.4"
180
185
  },
181
186
  "optionalDependencies": {
182
- "@rollup/rollup-darwin-arm64": "^4.42.0",
183
- "@rollup/rollup-win32-x64-msvc": "^4.42.0"
187
+ "@rollup/rollup-darwin-arm64": "^4.45.1",
188
+ "@rollup/rollup-win32-x64-msvc": "^4.45.1"
184
189
  }
185
190
  }
@@ -59,6 +59,7 @@ import {
59
59
  tsLinterWorker,
60
60
  tsSyncWorker,
61
61
  } from '@valtown/codemirror-ts';
62
+ import { getFileExtension } from '../utils/fileExtension';
62
63
 
63
64
  const DEBUG = false;
64
65
 
@@ -131,6 +132,14 @@ const languageExtensions = {
131
132
  svelte,
132
133
  };
133
134
 
135
+ // Exported utility function to get language extension for a file extension
136
+ export const getLanguageExtension = (
137
+ fileExtension: string,
138
+ ) => {
139
+ const languageFunc = languageExtensions[fileExtension];
140
+ return languageFunc ? languageFunc() : undefined;
141
+ };
142
+
134
143
  // Gets a value at a path in an object.
135
144
  // e.g. getAtPath({a: {b: 1}}, ['a', 'b']) === 1
136
145
  const getAtPath = (obj, path) => {
@@ -146,6 +155,7 @@ const getAtPath = (obj, path) => {
146
155
  interface ExtendedEditorCacheValue
147
156
  extends EditorCacheValue {
148
157
  themeCompartment: Compartment;
158
+ languageCompartment: Compartment;
149
159
  rainbowBracketsCompartment: Compartment;
150
160
  updateRainbowBrackets: (enabled: boolean) => void;
151
161
  }
@@ -227,7 +237,8 @@ export const getOrCreateEditor = async ({
227
237
  const namePath = [...filesPath, fileId, 'name'];
228
238
  const text = getAtPath(content, textPath);
229
239
  const name = getAtPath(content, namePath);
230
- const fileExtension = name.split('.').pop();
240
+
241
+ const fileExtension = getFileExtension(name);
231
242
 
232
243
  // Create a compartment for the theme so that it can be changed dynamically.
233
244
  // Inspired by: https://github.com/craftzdog/cm6-themes/blob/main/example/index.ts
@@ -321,10 +332,6 @@ export const getOrCreateEditor = async ({
321
332
  // using a Compartment.
322
333
  const languageCompartment = new Compartment();
323
334
  // See https://github.com/vizhub-core/vzcode/issues/55
324
- const getLanguageExtension = (fileExtension: string) => {
325
- const languageFunc = languageExtensions[fileExtension];
326
- return languageFunc ? languageFunc() : undefined;
327
- };
328
335
 
329
336
  const languageExtension =
330
337
  getLanguageExtension(fileExtension);
@@ -346,6 +353,11 @@ export const getOrCreateEditor = async ({
346
353
  extensions.push(languageCompartment.of([]));
347
354
  }
348
355
 
356
+ // Enable line wrapping for Markdown files
357
+ if (fileExtension === 'md') {
358
+ extensions.push(EditorView.lineWrapping);
359
+ }
360
+
349
361
  // Add interactive widgets.
350
362
  // Includes the Alt+drag functionality for numbers.
351
363
  // Calls `onInteract` when one of those widgets is interacted with.
@@ -458,6 +470,7 @@ export const getOrCreateEditor = async ({
458
470
  const editorCacheValue: ExtendedEditorCacheValue = {
459
471
  editor,
460
472
  themeCompartment,
473
+ languageCompartment,
461
474
  rainbowBracketsCompartment,
462
475
  updateRainbowBrackets: (enabled) => {
463
476
  editor.dispatch({
@@ -7,7 +7,7 @@ export const MicSVG = () => (
7
7
  version="1.1"
8
8
  xmlns="http://www.w3.org/2000/svg"
9
9
  viewBox="0 0 512 512"
10
- enable-background="new 0 0 512 512"
10
+ enableBackground="new 0 0 512 512"
11
11
  >
12
12
  <g>
13
13
  <g>
@@ -113,6 +113,11 @@ export type VZCodeContextValue = {
113
113
  isSearchOpen: boolean;
114
114
  setIsSearchOpen: (isSearchOpen: boolean) => void;
115
115
  setSearch: (pattern: string) => void;
116
+
117
+ isAIChatOpen: boolean;
118
+ setIsAIChatOpen: (isAIChatOpen: boolean) => void;
119
+ aiChatFocused: boolean;
120
+ toggleAIChatFocused: () => void;
116
121
  setSearchResults: (files: ShareDBDoc<VizContent>) => void;
117
122
  setSearchFileVisibility: (
118
123
  files: ShareDBDoc<VizContent>,
@@ -252,6 +257,8 @@ export const VZCodeProvider = ({
252
257
  theme,
253
258
  search,
254
259
  isSearchOpen,
260
+ isAIChatOpen,
261
+ aiChatFocused,
255
262
  isSettingsOpen,
256
263
  isDocOpen,
257
264
  editorWantsFocus,
@@ -260,7 +267,7 @@ export const VZCodeProvider = ({
260
267
  sidebarPresenceIndicators,
261
268
  } = state;
262
269
 
263
- const activePane = findPane(pane, activePaneId);
270
+ const activePane: Pane = findPane(pane, activePaneId);
264
271
  if (activePane.type !== 'leafPane') {
265
272
  // Should never happen
266
273
  throw new Error('Expected leafPane');
@@ -281,6 +288,8 @@ export const VZCodeProvider = ({
281
288
  setSearchLineVisibility,
282
289
  setSearchFocusedIndex,
283
290
  toggleSearchFocused,
291
+ setIsAIChatOpen,
292
+ toggleAIChatFocused,
284
293
  setIsSettingsOpen,
285
294
  setIsDocOpen,
286
295
  closeSettings,
@@ -304,7 +313,7 @@ export const VZCodeProvider = ({
304
313
  // The set of open directories.
305
314
  // TODO move this into reducer/useActions
306
315
  const { isDirectoryOpen, toggleDirectory } =
307
- useOpenDirectories();
316
+ useOpenDirectories({ activePane, content });
308
317
 
309
318
  // Cache of CodeMirror editors by file id.
310
319
  const editorCache: EditorCache = useEditorCache();
@@ -324,6 +333,8 @@ export const VZCodeProvider = ({
324
333
  submitOperation,
325
334
  closeTabs,
326
335
  openTab,
336
+ editorCache,
337
+ content,
327
338
  });
328
339
 
329
340
  // State to control the create file modal's visibility
@@ -435,6 +446,11 @@ export const VZCodeProvider = ({
435
446
  setSearchFocusedIndex,
436
447
  toggleSearchFocused,
437
448
 
449
+ isAIChatOpen,
450
+ setIsAIChatOpen,
451
+ aiChatFocused,
452
+ toggleAIChatFocused,
453
+
438
454
  isSettingsOpen,
439
455
  setIsSettingsOpen,
440
456
  closeSettings,
@@ -0,0 +1,65 @@
1
+ import { useRef, useEffect, useCallback } from 'react';
2
+ import { Form, Button } from '../../bootstrap';
3
+
4
+ interface ChatInputProps {
5
+ message: string;
6
+ setMessage: (message: string) => void;
7
+ onSendMessage: () => void;
8
+ isLoading: boolean;
9
+ focused: boolean;
10
+ }
11
+
12
+ export const ChatInput = ({
13
+ message,
14
+ setMessage,
15
+ onSendMessage,
16
+ isLoading,
17
+ focused,
18
+ }: ChatInputProps) => {
19
+ const inputRef = useRef<HTMLTextAreaElement>(null);
20
+
21
+ useEffect(() => {
22
+ // Focus the input when the AI chat is focused
23
+ if (focused && inputRef.current) {
24
+ inputRef.current.focus();
25
+ }
26
+ }, [focused]);
27
+
28
+ const handleKeyDown = useCallback(
29
+ (event: React.KeyboardEvent) => {
30
+ if (event.key === 'Enter' && !event.shiftKey) {
31
+ event.preventDefault();
32
+ onSendMessage();
33
+ }
34
+ },
35
+ [onSendMessage],
36
+ );
37
+
38
+ return (
39
+ <div className="ai-chat-input-container">
40
+ <Form.Group className="ai-chat-input-group">
41
+ <Form.Control
42
+ as="textarea"
43
+ rows={2}
44
+ value={message}
45
+ onChange={(event) =>
46
+ setMessage(event.target.value)
47
+ }
48
+ onKeyDown={handleKeyDown}
49
+ ref={inputRef}
50
+ placeholder="Describe what you'd like me to change in your code..."
51
+ spellCheck="false"
52
+ disabled={isLoading}
53
+ />
54
+ <Button
55
+ variant="primary"
56
+ onClick={onSendMessage}
57
+ disabled={!message.trim() || isLoading}
58
+ className="ai-chat-send-button"
59
+ >
60
+ Send
61
+ </Button>
62
+ </Form.Group>
63
+ </div>
64
+ );
65
+ };
@@ -0,0 +1,36 @@
1
+ import { timestampToDate } from '@vizhub/viz-utils';
2
+ import Markdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+
5
+ interface MessageProps {
6
+ id: string;
7
+ role: 'user' | 'assistant';
8
+ content: string;
9
+ timestamp: number;
10
+ isStreaming?: boolean;
11
+ }
12
+
13
+ export const Message = ({
14
+ role,
15
+ content,
16
+ timestamp,
17
+ isStreaming,
18
+ }: MessageProps) => {
19
+ return (
20
+ <div
21
+ className={`ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`}
22
+ >
23
+ <div className="ai-chat-message-content">
24
+ <Markdown remarkPlugins={[remarkGfm]}>
25
+ {content}
26
+ </Markdown>
27
+ </div>
28
+ <div className="ai-chat-message-time">
29
+ {timestampToDate(timestamp).toLocaleTimeString([], {
30
+ hour: '2-digit',
31
+ minute: '2-digit',
32
+ })}
33
+ </div>
34
+ </div>
35
+ );
36
+ };
@@ -0,0 +1,62 @@
1
+ import { useRef, useEffect } from 'react';
2
+ import { Message } from './Message';
3
+ import { StreamingMessage } from './StreamingMessage';
4
+ import { TypingIndicator } from './TypingIndicator';
5
+
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 = ({
21
+ messages,
22
+ aiScratchpad,
23
+ aiStatus,
24
+ isLoading,
25
+ }: MessageListProps) => {
26
+ const messagesEndRef = useRef<HTMLDivElement>(null);
27
+
28
+ const scrollToBottom = () => {
29
+ messagesEndRef.current?.scrollIntoView({
30
+ behavior: 'smooth',
31
+ });
32
+ };
33
+
34
+ useEffect(() => {
35
+ scrollToBottom();
36
+ }, [messages, aiScratchpad]);
37
+
38
+ return (
39
+ <div className="ai-chat-messages">
40
+ {messages.map((msg) => (
41
+ <Message
42
+ key={msg.id}
43
+ id={msg.id}
44
+ role={msg.role}
45
+ content={msg.content}
46
+ timestamp={msg.timestamp}
47
+ />
48
+ ))}
49
+
50
+ {/* Show streaming content if available */}
51
+ {aiScratchpad && (
52
+ <StreamingMessage
53
+ content={aiScratchpad}
54
+ status={aiStatus}
55
+ />
56
+ )}
57
+
58
+ {isLoading && !aiScratchpad && <TypingIndicator />}
59
+ <div ref={messagesEndRef} />
60
+ </div>
61
+ );
62
+ };
@@ -0,0 +1,25 @@
1
+ import Markdown from 'react-markdown';
2
+ import remarkGfm from 'remark-gfm';
3
+
4
+ interface StreamingMessageProps {
5
+ content: string;
6
+ status?: string;
7
+ }
8
+
9
+ export const StreamingMessage = ({
10
+ content,
11
+ status,
12
+ }: StreamingMessageProps) => {
13
+ return (
14
+ <div className="ai-chat-message assistant streaming">
15
+ {status && (
16
+ <div className="ai-chat-status">{status}</div>
17
+ )}
18
+ <div className="ai-chat-message-content">
19
+ <Markdown remarkPlugins={[remarkGfm]}>
20
+ {content}
21
+ </Markdown>
22
+ </div>
23
+ </div>
24
+ );
25
+ };
@@ -0,0 +1,13 @@
1
+ export const TypingIndicator = () => {
2
+ return (
3
+ <div className="ai-chat-message assistant">
4
+ <div className="ai-chat-message-content">
5
+ <div className="ai-chat-typing">
6
+ <span></span>
7
+ <span></span>
8
+ <span></span>
9
+ </div>
10
+ </div>
11
+ </div>
12
+ );
13
+ };
@@ -0,0 +1,75 @@
1
+ import { useContext, useState, useCallback } from 'react';
2
+ import { VZCodeContext } from '../../VZCodeContext';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { MessageList } from './MessageList';
5
+ import { ChatInput } from './ChatInput';
6
+ import './styles.scss';
7
+
8
+ export const AIChat = () => {
9
+ const [message, setMessage] = useState('');
10
+ const [isLoading, setIsLoading] = useState(false);
11
+ const [currentChatId] = useState(() => uuidv4());
12
+
13
+ const { aiChatFocused, content } =
14
+ useContext(VZCodeContext);
15
+
16
+ // Get current chat data from content
17
+ const currentChat = content?.chats?.[currentChatId];
18
+ const messages = currentChat?.messages || [];
19
+ const aiScratchpad = currentChat?.aiScratchpad;
20
+ const aiStatus = currentChat?.aiStatus;
21
+
22
+ const handleSendMessage = useCallback(async () => {
23
+ if (!message.trim() || isLoading) return;
24
+
25
+ setMessage('');
26
+ setIsLoading(true);
27
+
28
+ // Call backend endpoint for AI response
29
+ // The server will handle all ShareDB operations including adding the user message
30
+ try {
31
+ const response = await fetch('/ai-chat-message', {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ },
36
+ body: JSON.stringify({
37
+ content: message.trim(),
38
+ chatId: currentChatId,
39
+ }),
40
+ });
41
+
42
+ if (!response.ok) {
43
+ throw new Error(
44
+ `HTTP error! status: ${response.status}`,
45
+ );
46
+ }
47
+
48
+ // The backend handles all ShareDB operations
49
+ await response.json();
50
+ } catch (error) {
51
+ console.error('Error getting AI response:', error);
52
+ // Server will handle error messages too
53
+ } finally {
54
+ setIsLoading(false);
55
+ }
56
+ }, [message, isLoading, currentChatId]);
57
+
58
+ return (
59
+ <div className="ai-chat-container">
60
+ <MessageList
61
+ messages={messages}
62
+ aiScratchpad={aiScratchpad}
63
+ aiStatus={aiStatus}
64
+ isLoading={isLoading}
65
+ />
66
+ <ChatInput
67
+ message={message}
68
+ setMessage={setMessage}
69
+ onSendMessage={handleSendMessage}
70
+ isLoading={isLoading}
71
+ focused={aiChatFocused}
72
+ />
73
+ </div>
74
+ );
75
+ };