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.
- package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
- package/dist/assets/buildWorker-B-vXZ9gA.js +579 -0
- package/dist/assets/index-DPAeYEiH.js +329 -0
- package/dist/index.html +1 -1
- package/package.json +16 -14
- package/src/client/CodeEditor/getOrCreateEditor.ts +2 -5
- package/src/client/CodeEditor/index.tsx +1 -1
- package/src/client/CodeEditor/json1PresenceDisplay.ts +28 -44
- package/src/client/VZRight.tsx +67 -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 +11 -4
- 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 -7
- package/src/client/buildWorker.ts +3 -0
- package/src/client/usePresenceAutoFollow.ts +3 -4
- package/src/server/aiChatHandler/{chatOperations.js → chatOperations.ts} +5 -2
- package/src/server/aiChatHandler/{index.js → index.ts} +19 -9
- package/src/server/aiChatHandler/{llmStreaming.js → llmStreaming.ts} +16 -7
- package/src/server/{generateAIResponse.js → generateAIResponse.ts} +3 -2
- package/src/server/{handleAIAssist.js → handleAIAssist.ts} +11 -4
- package/src/server/{index.js → index.ts} +16 -22
- package/dist/assets/index-0y6ZxgSv.js +0 -327
- /package/src/server/aiChatHandler/{aiEditing.js → aiEditing.ts} +0 -0
- /package/src/server/aiChatHandler/{errorHandling.js → errorHandling.ts} +0 -0
- /package/src/server/aiChatHandler/{validation.js → validation.ts} +0 -0
- /package/src/server/{computeInitialDocument.js → computeInitialDocument.ts} +0 -0
- /package/src/server/{config.js → config.ts} +0 -0
- /package/src/server/{featureFlags.js → featureFlags.ts} +0 -0
- /package/src/server/{handleAIChatMessage.js → handleAIChatMessage.ts} +0 -0
- /package/src/server/{handleAICopilot.js → handleAICopilot.ts} +0 -0
- /package/src/server/{isDirectory.js → isDirectory.ts} +0 -0
- /package/src/server/{livekit.js → livekit.ts} +0 -0
- /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-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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,19 +170,20 @@
|
|
|
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
178
|
"cross-env": "^7.0.3",
|
|
178
|
-
"eslint": "^9.
|
|
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",
|
|
184
|
+
"ts-node": "^10.9.2",
|
|
183
185
|
"typescript": "^5.8.3",
|
|
184
|
-
"vite": "^7.0.
|
|
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
|
|
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 =
|
|
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
|
-
);
|
|
68
|
+
);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
// If presence
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
161
|
-
//
|
|
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
|
}
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -1,14 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
72
|
+
<iframe id="viz-iframe"></iframe>
|
|
12
73
|
) : null}
|
|
13
74
|
</div>
|
|
14
75
|
);
|
|
@@ -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,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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';
|
|
@@ -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 =
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}, [
|
|
78
|
+
}, [
|
|
79
|
+
message,
|
|
80
|
+
isLoading,
|
|
81
|
+
aiChatEndpoint,
|
|
82
|
+
aiChatOptions,
|
|
83
|
+
currentChatId,
|
|
84
|
+
]);
|
|
70
85
|
|
|
71
86
|
return (
|
|
72
87
|
<div className="ai-chat-container">
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 =
|
|
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 = (
|
|
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
|
}
|