vzcode 1.33.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/dist/assets/index-ajiBaTmx.js +263 -0
- package/dist/assets/{index-DnunmWjy.css → index-lvLw6dCW.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +7 -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 +1 -1
- package/src/client/VZSidebar/styles.scss +0 -170
- package/src/client/diff.js +1 -1
- package/src/client/featureFlags.ts +1 -1
- 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-DPIrco_g.js +0 -235
- package/src/client/VZSidebar/AIChat.tsx +0 -142
|
@@ -81,176 +81,6 @@
|
|
|
81
81
|
height: 100%;
|
|
82
82
|
display: flex;
|
|
83
83
|
flex-direction: column;
|
|
84
|
-
|
|
85
|
-
.ai-chat-container {
|
|
86
|
-
height: 100%;
|
|
87
|
-
display: flex;
|
|
88
|
-
flex-direction: column;
|
|
89
|
-
padding: 10px;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.ai-chat-messages {
|
|
93
|
-
flex: 1;
|
|
94
|
-
overflow-y: auto;
|
|
95
|
-
margin-bottom: 10px;
|
|
96
|
-
padding-right: 5px;
|
|
97
|
-
|
|
98
|
-
&::-webkit-scrollbar {
|
|
99
|
-
width: 6px;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
&::-webkit-scrollbar-track {
|
|
103
|
-
background: var(--vh-color-neutral-02);
|
|
104
|
-
border-radius: 3px;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
&::-webkit-scrollbar-thumb {
|
|
108
|
-
background: var(--vh-color-neutral-04);
|
|
109
|
-
border-radius: 3px;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.ai-chat-message {
|
|
114
|
-
margin-bottom: 15px;
|
|
115
|
-
|
|
116
|
-
&.user {
|
|
117
|
-
.ai-chat-message-content {
|
|
118
|
-
background: var(--vh-color-primary-01);
|
|
119
|
-
color: white;
|
|
120
|
-
margin-left: 20px;
|
|
121
|
-
border-radius: 12px 12px 4px 12px;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.ai-chat-message-time {
|
|
125
|
-
text-align: right;
|
|
126
|
-
margin-right: 5px;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
&.assistant {
|
|
131
|
-
.ai-chat-message-content {
|
|
132
|
-
background: var(--vh-color-neutral-02);
|
|
133
|
-
color: var(--vh-color-neutral-04);
|
|
134
|
-
margin-right: 20px;
|
|
135
|
-
border-radius: 12px 12px 12px 4px;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.ai-chat-message-time {
|
|
139
|
-
text-align: left;
|
|
140
|
-
margin-left: 5px;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.ai-chat-message-content {
|
|
146
|
-
padding: 10px 12px;
|
|
147
|
-
font-size: 14px;
|
|
148
|
-
line-height: 1.4;
|
|
149
|
-
word-wrap: break-word;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.ai-chat-message-time {
|
|
153
|
-
font-size: 11px;
|
|
154
|
-
color: var(--vh-color-neutral-03);
|
|
155
|
-
margin-top: 4px;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.ai-chat-typing {
|
|
159
|
-
display: flex;
|
|
160
|
-
gap: 4px;
|
|
161
|
-
align-items: center;
|
|
162
|
-
|
|
163
|
-
span {
|
|
164
|
-
width: 6px;
|
|
165
|
-
height: 6px;
|
|
166
|
-
background: var(--vh-color-neutral-03);
|
|
167
|
-
border-radius: 50%;
|
|
168
|
-
animation: typing 1.4s infinite ease-in-out;
|
|
169
|
-
|
|
170
|
-
&:nth-child(1) {
|
|
171
|
-
animation-delay: -0.32s;
|
|
172
|
-
}
|
|
173
|
-
&:nth-child(2) {
|
|
174
|
-
animation-delay: -0.16s;
|
|
175
|
-
}
|
|
176
|
-
&:nth-child(3) {
|
|
177
|
-
animation-delay: 0s;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
.ai-chat-input-container {
|
|
183
|
-
border-top: 1px solid var(--vh-color-neutral-02);
|
|
184
|
-
padding-top: 10px;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.ai-chat-input-group {
|
|
188
|
-
display: flex;
|
|
189
|
-
gap: 8px;
|
|
190
|
-
margin-bottom: 0;
|
|
191
|
-
|
|
192
|
-
textarea {
|
|
193
|
-
flex: 1;
|
|
194
|
-
background: var(--vh-color-neutral-02);
|
|
195
|
-
border: 1px solid var(--vh-color-neutral-03);
|
|
196
|
-
color: var(--vh-color-neutral-04);
|
|
197
|
-
border-radius: 8px;
|
|
198
|
-
padding: 8px 10px;
|
|
199
|
-
font-size: 14px;
|
|
200
|
-
font-family: var(--vzcode-font-family);
|
|
201
|
-
resize: none;
|
|
202
|
-
min-height: 36px;
|
|
203
|
-
|
|
204
|
-
&:focus {
|
|
205
|
-
outline: none;
|
|
206
|
-
border-color: var(--vh-color-primary-01);
|
|
207
|
-
box-shadow: 0 0 0 2px rgba(38, 67, 153, 0.2);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
&::placeholder {
|
|
211
|
-
color: var(--vh-color-neutral-03);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
&:disabled {
|
|
215
|
-
opacity: 0.6;
|
|
216
|
-
cursor: not-allowed;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.ai-chat-send-button {
|
|
221
|
-
background: var(--vh-color-primary-01);
|
|
222
|
-
border: none;
|
|
223
|
-
color: white;
|
|
224
|
-
border-radius: 8px;
|
|
225
|
-
padding: 8px 16px;
|
|
226
|
-
font-size: 14px;
|
|
227
|
-
font-weight: 500;
|
|
228
|
-
cursor: pointer;
|
|
229
|
-
transition: background-color 0.2s ease;
|
|
230
|
-
|
|
231
|
-
&:hover:not(:disabled) {
|
|
232
|
-
background: var(--vh-color-primary-02);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
&:disabled {
|
|
236
|
-
background: var(--vh-color-neutral-03);
|
|
237
|
-
cursor: not-allowed;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
@keyframes typing {
|
|
244
|
-
0%,
|
|
245
|
-
80%,
|
|
246
|
-
100% {
|
|
247
|
-
transform: scale(0.8);
|
|
248
|
-
opacity: 0.5;
|
|
249
|
-
}
|
|
250
|
-
40% {
|
|
251
|
-
transform: scale(1);
|
|
252
|
-
opacity: 1;
|
|
253
|
-
}
|
|
254
84
|
}
|
|
255
85
|
|
|
256
86
|
.sidebar-search .arrow-wrapper {
|
package/src/client/diff.js
CHANGED
package/src/ot.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
// Operational Transformation (OT) utilities.
|
|
1
2
|
// This file is the central point where the OT types are imported.
|
|
2
3
|
// Localized to one file so it's easy to change it in future.
|
|
3
4
|
import OTJSON1Presence from 'sharedb-client-browser/dist/ot-json1-presence-umd.cjs';
|
|
4
|
-
|
|
5
|
-
export { json1Presence, textUnicode }
|
|
5
|
+
|
|
6
|
+
export const { json1Presence, textUnicode } =
|
|
7
|
+
OTJSON1Presence;
|
|
8
|
+
|
|
9
|
+
// The OT type used throughout the codebase.
|
|
10
|
+
export const otType = json1Presence.type;
|
|
11
|
+
|
|
12
|
+
// Applies an OT op to an object.
|
|
13
|
+
export const apply = otType.apply;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { performAiEdit } from 'editcodewithai';
|
|
2
|
+
import {
|
|
3
|
+
clearAIScratchpadAndStatus,
|
|
4
|
+
updateFiles,
|
|
5
|
+
setIsInteracting,
|
|
6
|
+
} from './chatOperations.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Performs AI editing operations on the files
|
|
10
|
+
*/
|
|
11
|
+
export const performAIEditing = async (
|
|
12
|
+
shareDBDoc,
|
|
13
|
+
chatId,
|
|
14
|
+
content,
|
|
15
|
+
files,
|
|
16
|
+
llmFunction,
|
|
17
|
+
) => {
|
|
18
|
+
const editResult = await performAiEdit({
|
|
19
|
+
prompt: content,
|
|
20
|
+
files,
|
|
21
|
+
llmFunction,
|
|
22
|
+
apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Clear the scratchpad and update status
|
|
26
|
+
clearAIScratchpadAndStatus(
|
|
27
|
+
shareDBDoc,
|
|
28
|
+
chatId,
|
|
29
|
+
'Done editing with AI.',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Apply AI edits to files
|
|
33
|
+
updateFiles(shareDBDoc, editResult.changedFiles);
|
|
34
|
+
|
|
35
|
+
// Wait for propagation
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
37
|
+
|
|
38
|
+
// Unset isInteracting
|
|
39
|
+
setIsInteracting(shareDBDoc, false);
|
|
40
|
+
|
|
41
|
+
return editResult;
|
|
42
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { dateToTimestamp } from '@vizhub/viz-utils';
|
|
2
|
+
import { diff } from '../../client/diff.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensures the chats object exists in the ShareDB document
|
|
6
|
+
*/
|
|
7
|
+
export const ensureChatsExist = (shareDBDoc) => {
|
|
8
|
+
if (!shareDBDoc.data.chats) {
|
|
9
|
+
const op = diff(shareDBDoc.data, {
|
|
10
|
+
...shareDBDoc.data,
|
|
11
|
+
chats: {},
|
|
12
|
+
});
|
|
13
|
+
shareDBDoc.submitOp(op);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensures a specific chat exists in the ShareDB document
|
|
19
|
+
*/
|
|
20
|
+
export const ensureChatExists = (shareDBDoc, chatId) => {
|
|
21
|
+
if (!shareDBDoc.data.chats[chatId]) {
|
|
22
|
+
const op = diff(shareDBDoc.data, {
|
|
23
|
+
...shareDBDoc.data,
|
|
24
|
+
chats: {
|
|
25
|
+
...shareDBDoc.data.chats,
|
|
26
|
+
[chatId]: {
|
|
27
|
+
id: chatId,
|
|
28
|
+
messages: [],
|
|
29
|
+
createdAt: dateToTimestamp(new Date()),
|
|
30
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
shareDBDoc.submitOp(op);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adds a user message to the chat
|
|
40
|
+
*/
|
|
41
|
+
export const addUserMessage = (
|
|
42
|
+
shareDBDoc,
|
|
43
|
+
chatId,
|
|
44
|
+
content,
|
|
45
|
+
) => {
|
|
46
|
+
const userMessage = {
|
|
47
|
+
id: `user-${Date.now()}`,
|
|
48
|
+
role: 'user',
|
|
49
|
+
content: content,
|
|
50
|
+
timestamp: dateToTimestamp(new Date()),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const userMessageOp = diff(shareDBDoc.data, {
|
|
54
|
+
...shareDBDoc.data,
|
|
55
|
+
chats: {
|
|
56
|
+
...shareDBDoc.data.chats,
|
|
57
|
+
[chatId]: {
|
|
58
|
+
...shareDBDoc.data.chats[chatId],
|
|
59
|
+
messages: [
|
|
60
|
+
...shareDBDoc.data.chats[chatId].messages,
|
|
61
|
+
userMessage,
|
|
62
|
+
],
|
|
63
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
shareDBDoc.submitOp(userMessageOp);
|
|
68
|
+
|
|
69
|
+
return userMessage;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Updates AI status in the chat
|
|
74
|
+
*/
|
|
75
|
+
export const updateAIStatus = (
|
|
76
|
+
shareDBDoc,
|
|
77
|
+
chatId,
|
|
78
|
+
status,
|
|
79
|
+
) => {
|
|
80
|
+
const op = diff(shareDBDoc.data, {
|
|
81
|
+
...shareDBDoc.data,
|
|
82
|
+
chats: {
|
|
83
|
+
...shareDBDoc.data.chats,
|
|
84
|
+
[chatId]: {
|
|
85
|
+
...shareDBDoc.data.chats[chatId],
|
|
86
|
+
aiStatus: status,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
shareDBDoc.submitOp(op);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Updates AI scratchpad content
|
|
95
|
+
*/
|
|
96
|
+
export const updateAIScratchpad = (
|
|
97
|
+
shareDBDoc,
|
|
98
|
+
chatId,
|
|
99
|
+
content,
|
|
100
|
+
) => {
|
|
101
|
+
const op = diff(shareDBDoc.data, {
|
|
102
|
+
...shareDBDoc.data,
|
|
103
|
+
chats: {
|
|
104
|
+
...shareDBDoc.data.chats,
|
|
105
|
+
[chatId]: {
|
|
106
|
+
...shareDBDoc.data.chats[chatId],
|
|
107
|
+
aiScratchpad: content,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
shareDBDoc.submitOp(op);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Clears AI scratchpad and updates status
|
|
116
|
+
*/
|
|
117
|
+
export const clearAIScratchpadAndStatus = (
|
|
118
|
+
shareDBDoc,
|
|
119
|
+
chatId,
|
|
120
|
+
status,
|
|
121
|
+
) => {
|
|
122
|
+
const op = diff(shareDBDoc.data, {
|
|
123
|
+
...shareDBDoc.data,
|
|
124
|
+
chats: {
|
|
125
|
+
...shareDBDoc.data.chats,
|
|
126
|
+
[chatId]: {
|
|
127
|
+
...shareDBDoc.data.chats[chatId],
|
|
128
|
+
aiScratchpad: undefined,
|
|
129
|
+
aiStatus: status,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
shareDBDoc.submitOp(op);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Adds an AI response message to the chat
|
|
138
|
+
*/
|
|
139
|
+
export const addAIMessage = (
|
|
140
|
+
shareDBDoc,
|
|
141
|
+
chatId,
|
|
142
|
+
content,
|
|
143
|
+
) => {
|
|
144
|
+
const aiResponse = {
|
|
145
|
+
id: Date.now() + 1,
|
|
146
|
+
role: 'assistant',
|
|
147
|
+
content: content || 'AI edit completed successfully.',
|
|
148
|
+
timestamp: dateToTimestamp(new Date()),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const messageOp = diff(shareDBDoc.data, {
|
|
152
|
+
...shareDBDoc.data,
|
|
153
|
+
chats: {
|
|
154
|
+
...shareDBDoc.data.chats,
|
|
155
|
+
[chatId]: {
|
|
156
|
+
...shareDBDoc.data.chats[chatId],
|
|
157
|
+
messages: [
|
|
158
|
+
...shareDBDoc.data.chats[chatId].messages,
|
|
159
|
+
aiResponse,
|
|
160
|
+
],
|
|
161
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
162
|
+
aiStatus: undefined,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
shareDBDoc.submitOp(messageOp);
|
|
167
|
+
|
|
168
|
+
return aiResponse;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Updates files in the ShareDB document
|
|
173
|
+
*/
|
|
174
|
+
export const updateFiles = (shareDBDoc, files) => {
|
|
175
|
+
const filesOp = diff(shareDBDoc.data, {
|
|
176
|
+
...shareDBDoc.data,
|
|
177
|
+
files: files,
|
|
178
|
+
isInteracting: true,
|
|
179
|
+
});
|
|
180
|
+
shareDBDoc.submitOp(filesOp);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sets isInteracting flag
|
|
185
|
+
*/
|
|
186
|
+
export const setIsInteracting = (
|
|
187
|
+
shareDBDoc,
|
|
188
|
+
isInteracting,
|
|
189
|
+
) => {
|
|
190
|
+
const interactingOp = diff(
|
|
191
|
+
{ isInteracting: !isInteracting },
|
|
192
|
+
{ isInteracting: isInteracting },
|
|
193
|
+
);
|
|
194
|
+
shareDBDoc.submitOp(interactingOp);
|
|
195
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { dateToTimestamp } from '@vizhub/viz-utils';
|
|
2
|
+
import { diff } from '../../client/diff.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles errors by adding an error message to the chat and clearing AI state
|
|
6
|
+
*/
|
|
7
|
+
export const handleError = (
|
|
8
|
+
shareDBDoc,
|
|
9
|
+
chatId,
|
|
10
|
+
error,
|
|
11
|
+
res,
|
|
12
|
+
) => {
|
|
13
|
+
console.error('[handleAIChatMessage] error:', error);
|
|
14
|
+
|
|
15
|
+
// Clear scratchpad and add error message on error
|
|
16
|
+
try {
|
|
17
|
+
const errorResponse = {
|
|
18
|
+
id: `error-${Date.now()}`,
|
|
19
|
+
role: 'assistant',
|
|
20
|
+
content:
|
|
21
|
+
'Sorry, I encountered an error while processing your message. Please try again.',
|
|
22
|
+
timestamp: dateToTimestamp(new Date()),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const errorOp = diff(shareDBDoc.data, {
|
|
26
|
+
...shareDBDoc.data,
|
|
27
|
+
chats: {
|
|
28
|
+
...shareDBDoc.data.chats,
|
|
29
|
+
[chatId]: {
|
|
30
|
+
...shareDBDoc.data.chats[chatId],
|
|
31
|
+
messages: [
|
|
32
|
+
...shareDBDoc.data.chats[chatId].messages,
|
|
33
|
+
errorResponse,
|
|
34
|
+
],
|
|
35
|
+
aiScratchpad: undefined,
|
|
36
|
+
aiStatus: undefined,
|
|
37
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
shareDBDoc.submitOp(errorOp);
|
|
42
|
+
} catch (opError) {
|
|
43
|
+
console.error(
|
|
44
|
+
'[handleAIChatMessage] error handling error state:',
|
|
45
|
+
opError,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
res.status(500).json({
|
|
50
|
+
error: 'Internal server error',
|
|
51
|
+
message: error.message,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { validateRequest } from './validation.js';
|
|
2
|
+
import {
|
|
3
|
+
ensureChatsExist,
|
|
4
|
+
ensureChatExists,
|
|
5
|
+
addUserMessage,
|
|
6
|
+
addAIMessage,
|
|
7
|
+
} from './chatOperations.js';
|
|
8
|
+
import { createLLMFunction } from './llmStreaming.js';
|
|
9
|
+
import { performAIEditing } from './aiEditing.js';
|
|
10
|
+
import { handleError } from './errorHandling.js';
|
|
11
|
+
|
|
12
|
+
const debug = false;
|
|
13
|
+
|
|
14
|
+
export const handleAIChatMessage =
|
|
15
|
+
(shareDBDoc) => async (req, res) => {
|
|
16
|
+
const { content, chatId } = req.body;
|
|
17
|
+
|
|
18
|
+
if (debug) {
|
|
19
|
+
console.log(
|
|
20
|
+
'[handleAIChatMessage] content:',
|
|
21
|
+
content,
|
|
22
|
+
'chatId:',
|
|
23
|
+
chatId,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Validate request
|
|
28
|
+
if (!validateRequest(req, res)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Get existing files from ShareDB doc
|
|
34
|
+
const files = shareDBDoc.data.files;
|
|
35
|
+
|
|
36
|
+
// Ensure chats structure exists
|
|
37
|
+
ensureChatsExist(shareDBDoc);
|
|
38
|
+
ensureChatExists(shareDBDoc, chatId);
|
|
39
|
+
|
|
40
|
+
// Add user message to chat
|
|
41
|
+
addUserMessage(shareDBDoc, chatId, content);
|
|
42
|
+
|
|
43
|
+
// Create LLM function for streaming
|
|
44
|
+
const llmFunction = createLLMFunction(
|
|
45
|
+
shareDBDoc,
|
|
46
|
+
chatId,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Perform AI editing
|
|
50
|
+
const editResult = await performAIEditing(
|
|
51
|
+
shareDBDoc,
|
|
52
|
+
chatId,
|
|
53
|
+
content,
|
|
54
|
+
files,
|
|
55
|
+
llmFunction,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Add AI response message
|
|
59
|
+
const aiResponse = addAIMessage(
|
|
60
|
+
shareDBDoc,
|
|
61
|
+
chatId,
|
|
62
|
+
editResult.content,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
res.status(200).json(aiResponse);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
handleError(shareDBDoc, chatId, error, res);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { StreamingMarkdownParser } from 'llm-code-format';
|
|
2
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
3
|
+
import {
|
|
4
|
+
updateAIStatus,
|
|
5
|
+
updateAIScratchpad,
|
|
6
|
+
} from './chatOperations.js';
|
|
7
|
+
|
|
8
|
+
const debug = false;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates and configures the LLM function for streaming
|
|
12
|
+
*/
|
|
13
|
+
export const createLLMFunction = (shareDBDoc, chatId) => {
|
|
14
|
+
return async (fullPrompt) => {
|
|
15
|
+
const chatModel = new ChatOpenAI({
|
|
16
|
+
modelName:
|
|
17
|
+
process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
|
|
18
|
+
'gpt-4o-mini',
|
|
19
|
+
configuration: {
|
|
20
|
+
apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
|
|
21
|
+
baseURL: process.env.VIZHUB_EDIT_WITH_AI_BASE_URL,
|
|
22
|
+
defaultHeaders: {
|
|
23
|
+
'HTTP-Referer': 'https://vizhub.com',
|
|
24
|
+
'X-Title': 'VizHub',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
streaming: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let fullContent = '';
|
|
31
|
+
let generationId = '';
|
|
32
|
+
|
|
33
|
+
// Track expected state to avoid race conditions
|
|
34
|
+
let expectedAiScratchpad =
|
|
35
|
+
shareDBDoc.data.chats[chatId]?.aiScratchpad || '';
|
|
36
|
+
|
|
37
|
+
// Throttle updates to avoid "op too long" errors
|
|
38
|
+
let lastUpdateTime = 0;
|
|
39
|
+
const UPDATE_THROTTLE_MS = 100;
|
|
40
|
+
|
|
41
|
+
// Define callbacks for streaming parser
|
|
42
|
+
const callbacks = {
|
|
43
|
+
onFileNameChange: (fileName, format) => {
|
|
44
|
+
debug &&
|
|
45
|
+
console.log(
|
|
46
|
+
`File changed to: ${fileName} (${format})`,
|
|
47
|
+
);
|
|
48
|
+
updateAIStatus(
|
|
49
|
+
shareDBDoc,
|
|
50
|
+
chatId,
|
|
51
|
+
'Editing ' + fileName,
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
onCodeLine: (line) => {
|
|
55
|
+
debug && console.log(`Code line: ${line}`);
|
|
56
|
+
},
|
|
57
|
+
onNonCodeLine: (line) => {
|
|
58
|
+
debug && console.log(`Comment/text: ${line}`);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const parser = new StreamingMarkdownParser(callbacks);
|
|
63
|
+
|
|
64
|
+
// Stream the response
|
|
65
|
+
const stream = await chatModel.stream(fullPrompt);
|
|
66
|
+
for await (const chunk of stream) {
|
|
67
|
+
if (chunk.content) {
|
|
68
|
+
const chunkContent = String(chunk.content);
|
|
69
|
+
fullContent += chunkContent;
|
|
70
|
+
|
|
71
|
+
// Throttle updates
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const shouldUpdate =
|
|
74
|
+
now - lastUpdateTime >= UPDATE_THROTTLE_MS;
|
|
75
|
+
|
|
76
|
+
if (shouldUpdate) {
|
|
77
|
+
updateAIScratchpad(
|
|
78
|
+
shareDBDoc,
|
|
79
|
+
chatId,
|
|
80
|
+
fullContent,
|
|
81
|
+
);
|
|
82
|
+
expectedAiScratchpad = fullContent;
|
|
83
|
+
lastUpdateTime = now;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
parser.processChunk(chunkContent);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!generationId && chunk.lc_kwargs?.id) {
|
|
90
|
+
generationId = chunk.lc_kwargs.id;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
parser.flushRemaining();
|
|
94
|
+
|
|
95
|
+
// Submit final update if there's pending content
|
|
96
|
+
if (expectedAiScratchpad !== fullContent) {
|
|
97
|
+
updateAIScratchpad(shareDBDoc, chatId, fullContent);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
content: fullContent,
|
|
102
|
+
generationId: generationId,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates incoming request data for AI chat messages
|
|
3
|
+
*/
|
|
4
|
+
export const validateRequest = (req, res) => {
|
|
5
|
+
const { content, chatId } = req.body;
|
|
6
|
+
|
|
7
|
+
if (!content || typeof content !== 'string') {
|
|
8
|
+
res.status(400).json({
|
|
9
|
+
error:
|
|
10
|
+
'Invalid request: content is required and must be a string',
|
|
11
|
+
});
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!chatId || typeof chatId !== 'string') {
|
|
16
|
+
res.status(400).json({
|
|
17
|
+
error:
|
|
18
|
+
'Invalid request: chatId is required and must be a string',
|
|
19
|
+
});
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
};
|