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.
- package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
- package/dist/assets/buildWorker-B-vXZ9gA.js +579 -0
- package/dist/assets/{index-DKLiE98a.css → index-CP9o_hN5.css} +1 -1
- package/dist/assets/index-kPLgjbcb.js +329 -0
- package/dist/index.html +2 -2
- package/package.json +17 -16
- package/src/client/App/style.scss +2 -2
- package/src/client/CodeEditor/getOrCreateEditor.ts +2 -5
- package/src/client/CodeEditor/index.tsx +1 -1
- package/src/client/CodeEditor/json1PresenceDisplay.ts +40 -12
- package/src/client/VZRight.tsx +68 -6
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +17 -5
- package/src/client/VZSidebar/AIChat/Message.tsx +21 -8
- package/src/client/VZSidebar/AIChat/MessageList.tsx +29 -31
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +6 -1
- package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +7 -1
- package/src/client/VZSidebar/AIChat/index.tsx +22 -9
- package/src/client/VZSidebar/aiCopyPaste.ts +8 -1
- package/src/client/VZSidebar/index.tsx +10 -8
- package/src/client/buildWorker.ts +3 -0
- package/src/client/vzReducer/searchReducer.test.ts +250 -0
- package/src/client/vzReducer/searchReducer.ts +3 -1
- package/src/{ot.js → ot.ts} +11 -0
- package/src/runCode.ts +4 -23
- package/src/server/aiChatHandler/aiEditing.ts +10 -7
- package/src/server/aiChatHandler/chatOperations.ts +144 -58
- package/src/server/aiChatHandler/errorHandling.ts +1 -1
- package/src/server/aiChatHandler/index.ts +8 -2
- package/src/server/aiChatHandler/llmStreaming.ts +83 -44
- package/src/submitOperation.ts +1 -1
- package/dist/assets/index-ubSLcdJf.js +0 -327
- package/src/client/diff.js +0 -12
- package/src/runCode.js +0 -48
- package/src/submitOperation.js +0 -16
- /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-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
148
|
-
"editcodewithai": "^2.
|
|
149
|
-
"eslint-linter-browserify": "^9.
|
|
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": "^
|
|
155
|
+
"llm-code-format": "^3.0.0",
|
|
155
156
|
"ngrok": "^5.0.0-beta.2",
|
|
156
|
-
"npm": "^11.
|
|
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.
|
|
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.
|
|
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": "^
|
|
178
|
-
"eslint": "^9.
|
|
178
|
+
"cross-env": "^10.0.0",
|
|
179
|
+
"eslint": "^9.32.0",
|
|
179
180
|
"globals": "^16.3.0",
|
|
180
|
-
"npm-check-updates": "^18.0.
|
|
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.
|
|
186
|
+
"vite": "^7.0.6",
|
|
186
187
|
"vitest": "^3.2.4"
|
|
187
188
|
},
|
|
188
189
|
"optionalDependencies": {
|
|
189
|
-
"@rollup/rollup-darwin-arm64": "^4.
|
|
190
|
-
"@rollup/rollup-win32-x64-msvc": "^4.
|
|
190
|
+
"@rollup/rollup-darwin-arm64": "^4.46.1",
|
|
191
|
+
"@rollup/rollup-win32-x64-msvc": "^4.46.1"
|
|
191
192
|
}
|
|
192
193
|
}
|
|
@@ -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
|
|
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
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
}
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -1,14 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
73
|
+
<iframe ref={iframeRef}></iframe>
|
|
12
74
|
) : null}
|
|
13
75
|
</div>
|
|
14
76
|
);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
{
|
|
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 {
|
|
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
|
-
|
|
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
|
-
}:
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}, [
|
|
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)
|
|
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 &&
|
|
549
|
+
{!isAIChatOpen && !isSearchOpen && (
|
|
550
550
|
<div className="ai-buttons">
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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}
|