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.
- package/README.md +0 -1
- package/dist/assets/index-ajiBaTmx.js +263 -0
- package/dist/assets/index-lvLw6dCW.css +1 -0
- package/dist/assets/worker-BrwN8AXx.js +330 -0
- package/dist/assets/worker-CkHcCP1n.js +136 -0
- package/dist/assets/worker-DkYJN5WQ.js +453 -0
- package/dist/index.html +2 -2
- package/package.json +42 -37
- package/src/client/CodeEditor/getOrCreateEditor.ts +18 -5
- package/src/client/Icons/MicSVG.tsx +1 -1
- package/src/client/VZCodeContext.tsx +18 -2
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +65 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +36 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +62 -0
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +25 -0
- package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +13 -0
- package/src/client/VZSidebar/AIChat/index.tsx +75 -0
- package/src/client/VZSidebar/AIChat/styles.scss +256 -0
- package/src/client/VZSidebar/index.tsx +53 -8
- package/src/client/VZSidebar/styles.scss +8 -1
- package/src/client/diff.js +1 -1
- package/src/client/featureFlags.ts +2 -0
- package/src/client/useActions.ts +20 -0
- package/src/client/useEditorCache.ts +1 -0
- package/src/client/useFileCRUD.ts +47 -0
- package/src/client/useOpenDirectories.ts +43 -12
- package/src/client/utils/fileExtension.ts +10 -0
- package/src/client/vzReducer/aiChatReducer.ts +31 -0
- package/src/client/vzReducer/createInitialState.ts +2 -0
- package/src/client/vzReducer/index.ts +20 -0
- package/src/ot.js +10 -2
- package/src/server/aiChatHandler/aiEditing.js +42 -0
- package/src/server/aiChatHandler/chatOperations.js +195 -0
- package/src/server/aiChatHandler/errorHandling.js +53 -0
- package/src/server/aiChatHandler/index.js +69 -0
- package/src/server/aiChatHandler/llmStreaming.js +105 -0
- package/src/server/aiChatHandler/validation.js +24 -0
- package/src/server/handleAIChatMessage.js +2 -0
- package/src/server/index.js +8 -0
- package/dist/assets/index-DTtcN2MP.css +0 -1
- package/dist/assets/index-sreAszZh.js +0 -240
- package/dist/assets/worker-BKfYWklR.js +0 -136
- package/dist/assets/worker-COyDRdqW.js +0 -453
- 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-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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.
|
|
112
|
-
"@codemirror/lang-markdown": "^6.3.
|
|
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.
|
|
116
|
-
"@codemirror/view": "^6.
|
|
117
|
-
"@langchain/core": "^0.3.
|
|
118
|
-
"@langchain/openai": "^0.
|
|
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.
|
|
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.
|
|
128
|
-
"@uiw/codemirror-theme-dracula": "^4.
|
|
129
|
-
"@uiw/codemirror-theme-eclipse": "^4.
|
|
130
|
-
"@uiw/codemirror-theme-github": "^4.
|
|
131
|
-
"@uiw/codemirror-theme-material": "^4.
|
|
132
|
-
"@uiw/codemirror-theme-nord": "^4.
|
|
133
|
-
"@uiw/codemirror-theme-okaidia": "^4.
|
|
134
|
-
"@uiw/codemirror-theme-xcode": "^4.
|
|
135
|
-
"@uiw/codemirror-themes": "^4.
|
|
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.
|
|
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.
|
|
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": "^
|
|
147
|
-
"
|
|
148
|
-
"eslint
|
|
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.
|
|
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.
|
|
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.
|
|
156
|
-
"open": "^10.
|
|
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-
|
|
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.
|
|
166
|
-
"ws": "^8.18.
|
|
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.
|
|
172
|
-
"concurrently": "^9.
|
|
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.
|
|
176
|
-
"sass": "^1.89.
|
|
180
|
+
"prettier": "^3.6.2",
|
|
181
|
+
"sass": "^1.89.2",
|
|
177
182
|
"typescript": "^5.8.3",
|
|
178
|
-
"vite": "^
|
|
179
|
-
"vitest": "^3.2.
|
|
183
|
+
"vite": "^7.0.5",
|
|
184
|
+
"vitest": "^3.2.4"
|
|
180
185
|
},
|
|
181
186
|
"optionalDependencies": {
|
|
182
|
-
"@rollup/rollup-darwin-arm64": "^4.
|
|
183
|
-
"@rollup/rollup-win32-x64-msvc": "^4.
|
|
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
|
-
|
|
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({
|
|
@@ -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
|
+
};
|