vzcode 1.56.0 → 1.58.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-COSAnnol.css → index-CE_xP06t.css} +1 -1
- package/dist/assets/{index-Cp6YlJo2.js → index-CtGus8Qk.js} +124 -123
- package/dist/index.html +2 -2
- package/package.json +3 -3
- package/src/client/CodeEditor/getOrCreateEditor.tsx +7 -7
- package/src/client/CodeEditor/index.tsx +2 -0
- package/src/client/VZCodeContext.tsx +145 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +20 -21
- package/src/client/VZSidebar/AIChat/DiffView.scss +8 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +105 -1
- package/src/client/VZSidebar/AIChat/Message.tsx +20 -4
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +1 -1
- package/src/client/VZSidebar/AIChat/index.tsx +158 -266
- package/src/client/VZSidebar/AIChat/styles.scss +52 -11
- package/src/server/aiChatHandler/chatOperations.ts +4 -2
- package/src/server/aiChatHandler/index.ts +9 -1
- package/src/server/aiChatHandler/llmStreaming.ts +24 -11
|
@@ -15,28 +15,27 @@ const defaultAIChatEndpoint = '/api/ai-chat/';
|
|
|
15
15
|
|
|
16
16
|
const DEBUG = false;
|
|
17
17
|
|
|
18
|
-
const showSuggestedRequests =
|
|
18
|
+
const showSuggestedRequests = true;
|
|
19
19
|
|
|
20
20
|
export const AIChat = () => {
|
|
21
|
-
const { aiChatMessage, setAIChatMessage } =
|
|
22
|
-
useContext(VZCodeContext);
|
|
23
|
-
|
|
24
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
-
const [currentChatId] = useState(() => uuidv4());
|
|
26
|
-
const [errorMessage, setErrorMessage] = useState<
|
|
27
|
-
string | null
|
|
28
|
-
>(null);
|
|
29
|
-
|
|
30
21
|
const {
|
|
31
22
|
aiChatFocused,
|
|
32
23
|
content,
|
|
33
24
|
aiChatEndpoint = defaultAIChatEndpoint,
|
|
34
25
|
aiChatOptions = {},
|
|
35
26
|
aiChatMode,
|
|
27
|
+
aiChatMessage,
|
|
28
|
+
isLoading,
|
|
29
|
+
currentChatId,
|
|
30
|
+
aiErrorMessage,
|
|
36
31
|
setAIChatMode,
|
|
37
32
|
autoForkAndRetryAI,
|
|
38
33
|
clearStoredAIPrompt,
|
|
39
34
|
getStoredAIPrompt,
|
|
35
|
+
setAIChatMessage,
|
|
36
|
+
handleSendMessage,
|
|
37
|
+
setIsLoading,
|
|
38
|
+
setAIErrorMessage,
|
|
40
39
|
} = useContext(VZCodeContext);
|
|
41
40
|
|
|
42
41
|
// Get current chat data from content
|
|
@@ -58,112 +57,6 @@ export const AIChat = () => {
|
|
|
58
57
|
// Check if this is the first time opening the chat (no messages)
|
|
59
58
|
const isEmptyState = rawMessages.length === 0;
|
|
60
59
|
|
|
61
|
-
const handleSendMessage = useCallback(
|
|
62
|
-
async (messageToSend?: string) => {
|
|
63
|
-
DEBUG &&
|
|
64
|
-
console.log(
|
|
65
|
-
'AIChat: handleSendMessage called with:',
|
|
66
|
-
messageToSend,
|
|
67
|
-
'aiChatMessage:',
|
|
68
|
-
aiChatMessage,
|
|
69
|
-
);
|
|
70
|
-
const messageContent = messageToSend || aiChatMessage;
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
!messageContent ||
|
|
74
|
-
typeof messageContent !== 'string' ||
|
|
75
|
-
!messageContent.trim() ||
|
|
76
|
-
isLoading
|
|
77
|
-
) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const currentPrompt = aiChatMessage.trim();
|
|
82
|
-
setAIChatMessage('');
|
|
83
|
-
setIsLoading(true);
|
|
84
|
-
setErrorMessage(null); // Clear any previous errors
|
|
85
|
-
|
|
86
|
-
// Call backend endpoint for AI response
|
|
87
|
-
// The server will handle all ShareDB operations including adding the user message
|
|
88
|
-
try {
|
|
89
|
-
const response = await fetch(aiChatEndpoint, {
|
|
90
|
-
method: 'POST',
|
|
91
|
-
headers: {
|
|
92
|
-
'Content-Type': 'application/json',
|
|
93
|
-
},
|
|
94
|
-
body: JSON.stringify({
|
|
95
|
-
...aiChatOptions,
|
|
96
|
-
vizId: aiChatOptions.vizId,
|
|
97
|
-
content: messageContent.trim(),
|
|
98
|
-
chatId: currentChatId,
|
|
99
|
-
mode: aiChatMode,
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
if (!response.ok) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`HTTP error! status: ${response.status}`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Parse the response to check for errors
|
|
110
|
-
const responseData = await response.json();
|
|
111
|
-
|
|
112
|
-
// Check if the response contains a VizHub error
|
|
113
|
-
if (
|
|
114
|
-
responseData.outcome === 'failure' &&
|
|
115
|
-
responseData.error
|
|
116
|
-
) {
|
|
117
|
-
const errorMessage = responseData.error.message;
|
|
118
|
-
|
|
119
|
-
// Check if this is the specific permission error that should trigger auto-fork
|
|
120
|
-
if (
|
|
121
|
-
errorMessage ===
|
|
122
|
-
'You do not have permission to use AI chat on this visualization. Only users with edit access can use this feature. Fork the viz to edit it.'
|
|
123
|
-
) {
|
|
124
|
-
// Trigger auto-fork instead of showing error
|
|
125
|
-
try {
|
|
126
|
-
await autoForkAndRetryAI?.(
|
|
127
|
-
currentPrompt,
|
|
128
|
-
aiChatMode,
|
|
129
|
-
);
|
|
130
|
-
// If we reach here, the fork was successful and redirect should happen
|
|
131
|
-
return;
|
|
132
|
-
} catch (forkError) {
|
|
133
|
-
console.error('Auto-fork failed:', forkError);
|
|
134
|
-
setErrorMessage(
|
|
135
|
-
'Failed to fork visualization. Please try forking manually.',
|
|
136
|
-
);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// For other errors, show the error message
|
|
142
|
-
setErrorMessage(errorMessage);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// The backend handles all ShareDB operations for successful responses
|
|
147
|
-
} catch (error) {
|
|
148
|
-
console.error('Error getting AI response:', error);
|
|
149
|
-
setErrorMessage(
|
|
150
|
-
'Failed to send message. Please try again.',
|
|
151
|
-
);
|
|
152
|
-
} finally {
|
|
153
|
-
setIsLoading(false);
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
[
|
|
157
|
-
aiChatMessage,
|
|
158
|
-
isLoading,
|
|
159
|
-
aiChatEndpoint,
|
|
160
|
-
aiChatOptions,
|
|
161
|
-
currentChatId,
|
|
162
|
-
aiChatMode,
|
|
163
|
-
autoForkAndRetryAI,
|
|
164
|
-
],
|
|
165
|
-
);
|
|
166
|
-
|
|
167
60
|
// Check for stored AI prompt on component mount (post-fork restoration)
|
|
168
61
|
useEffect(() => {
|
|
169
62
|
DEBUG &&
|
|
@@ -214,159 +107,158 @@ export const AIChat = () => {
|
|
|
214
107
|
|
|
215
108
|
return (
|
|
216
109
|
<div className="ai-chat-container">
|
|
217
|
-
<div
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{isEmptyState ? (
|
|
226
|
-
<div className="ai-chat-empty">
|
|
227
|
-
<div className="ai-chat-empty-icon">✨</div>
|
|
228
|
-
<h3 className="ai-chat-empty-title">
|
|
229
|
-
Edit with AI
|
|
230
|
-
</h3>
|
|
231
|
-
<div className="ai-chat-empty-text">
|
|
232
|
-
How can I help you?
|
|
233
|
-
</div>
|
|
234
|
-
{showSuggestedRequests && (
|
|
235
|
-
<div className="ai-chat-empty-examples">
|
|
236
|
-
{aiChatMode === 'ask' ? (
|
|
237
|
-
<>
|
|
238
|
-
<h4>Try asking questions like:</h4>
|
|
239
|
-
<div className="ai-chat-suggested-prompts">
|
|
240
|
-
<button
|
|
241
|
-
className="ai-chat-suggested-prompt"
|
|
242
|
-
onClick={() =>
|
|
243
|
-
handleSendMessage(
|
|
244
|
-
'Explain how this works',
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
>
|
|
248
|
-
"Explain how this works"
|
|
249
|
-
</button>
|
|
250
|
-
<button
|
|
251
|
-
className="ai-chat-suggested-prompt"
|
|
252
|
-
onClick={() =>
|
|
253
|
-
handleSendMessage(
|
|
254
|
-
'How could I change it so that the circles are bigger?',
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
>
|
|
258
|
-
"How could I change it so that the
|
|
259
|
-
circles are bigger?"
|
|
260
|
-
</button>
|
|
261
|
-
<button
|
|
262
|
-
className="ai-chat-suggested-prompt"
|
|
263
|
-
onClick={() =>
|
|
264
|
-
handleSendMessage(
|
|
265
|
-
'What does this function do?',
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
>
|
|
269
|
-
"What does this function do?"
|
|
270
|
-
</button>
|
|
271
|
-
<button
|
|
272
|
-
className="ai-chat-suggested-prompt"
|
|
273
|
-
onClick={() =>
|
|
274
|
-
handleSendMessage(
|
|
275
|
-
'How can I make this more accessible?',
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
>
|
|
279
|
-
"How can I make this more
|
|
280
|
-
accessible?"
|
|
281
|
-
</button>
|
|
282
|
-
</div>
|
|
283
|
-
</>
|
|
284
|
-
) : (
|
|
285
|
-
<>
|
|
286
|
-
<h4>Try edit requests like these:</h4>
|
|
287
|
-
<div className="ai-chat-suggested-prompts">
|
|
288
|
-
<button
|
|
289
|
-
className="ai-chat-suggested-prompt"
|
|
290
|
-
onClick={() =>
|
|
291
|
-
handleSendMessage(
|
|
292
|
-
'Change the circles to squares',
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
|
-
>
|
|
296
|
-
"Change the circles to squares"
|
|
297
|
-
</button>
|
|
298
|
-
<button
|
|
299
|
-
className="ai-chat-suggested-prompt"
|
|
300
|
-
onClick={() =>
|
|
301
|
-
handleSendMessage(
|
|
302
|
-
'Add a button that toggles the animation',
|
|
303
|
-
)
|
|
304
|
-
}
|
|
305
|
-
>
|
|
306
|
-
"Add a button that toggles the
|
|
307
|
-
animation"
|
|
308
|
-
</button>
|
|
309
|
-
<button
|
|
310
|
-
className="ai-chat-suggested-prompt"
|
|
311
|
-
onClick={() =>
|
|
312
|
-
handleSendMessage(
|
|
313
|
-
'Fix the CSS so the layout is responsive',
|
|
314
|
-
)
|
|
315
|
-
}
|
|
316
|
-
>
|
|
317
|
-
"Fix the CSS so the layout is
|
|
318
|
-
responsive"
|
|
319
|
-
</button>
|
|
320
|
-
<button
|
|
321
|
-
className="ai-chat-suggested-prompt"
|
|
322
|
-
onClick={() =>
|
|
323
|
-
handleSendMessage(
|
|
324
|
-
'Refactor this function to use async/await',
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
>
|
|
328
|
-
"Refactor this function to use
|
|
329
|
-
async/await"
|
|
330
|
-
</button>
|
|
331
|
-
</div>
|
|
332
|
-
</>
|
|
333
|
-
)}
|
|
334
|
-
</div>
|
|
335
|
-
)}
|
|
336
|
-
{showSuggestedRequests && (
|
|
110
|
+
<div className="ai-chat-content">
|
|
111
|
+
<div className="ai-chat-messages-container">
|
|
112
|
+
{isEmptyState ? (
|
|
113
|
+
<div className="ai-chat-empty">
|
|
114
|
+
<div className="ai-chat-empty-icon">✨</div>
|
|
115
|
+
<h3 className="ai-chat-empty-title">
|
|
116
|
+
Edit with AI
|
|
117
|
+
</h3>
|
|
337
118
|
<div className="ai-chat-empty-text">
|
|
338
|
-
|
|
339
|
-
? 'Type your question below to get started!'
|
|
340
|
-
: 'Type your edit request below to get started!'}
|
|
119
|
+
How can I help you?
|
|
341
120
|
</div>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
121
|
+
{showSuggestedRequests && (
|
|
122
|
+
<div className="ai-chat-empty-examples">
|
|
123
|
+
{aiChatMode === 'ask' ? (
|
|
124
|
+
<>
|
|
125
|
+
<h4>Try asking questions like:</h4>
|
|
126
|
+
<div className="ai-chat-suggested-prompts">
|
|
127
|
+
<button
|
|
128
|
+
className="ai-chat-suggested-prompt"
|
|
129
|
+
onClick={() =>
|
|
130
|
+
handleSendMessage(
|
|
131
|
+
'Explain how this works',
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
"Explain how this works"
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
className="ai-chat-suggested-prompt"
|
|
139
|
+
onClick={() =>
|
|
140
|
+
handleSendMessage(
|
|
141
|
+
'How could I change it so that the circles are bigger?',
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
"How could I change it so that the
|
|
146
|
+
circles are bigger?"
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
className="ai-chat-suggested-prompt"
|
|
150
|
+
onClick={() =>
|
|
151
|
+
handleSendMessage(
|
|
152
|
+
'What does this function do?',
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
>
|
|
156
|
+
"What does this function do?"
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
className="ai-chat-suggested-prompt"
|
|
160
|
+
onClick={() =>
|
|
161
|
+
handleSendMessage(
|
|
162
|
+
'How can I make this more accessible?',
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
"How can I make this more
|
|
167
|
+
accessible?"
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
</>
|
|
171
|
+
) : (
|
|
172
|
+
<>
|
|
173
|
+
<h4>Try edit requests like these:</h4>
|
|
174
|
+
<div className="ai-chat-suggested-prompts">
|
|
175
|
+
<button
|
|
176
|
+
className="ai-chat-suggested-prompt"
|
|
177
|
+
onClick={() =>
|
|
178
|
+
handleSendMessage(
|
|
179
|
+
'Change the circles to squares',
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
>
|
|
183
|
+
"Change the circles to squares"
|
|
184
|
+
</button>
|
|
185
|
+
<button
|
|
186
|
+
className="ai-chat-suggested-prompt"
|
|
187
|
+
onClick={() =>
|
|
188
|
+
handleSendMessage(
|
|
189
|
+
'Add a button that toggles the animation',
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
>
|
|
193
|
+
"Add a button that toggles the
|
|
194
|
+
animation"
|
|
195
|
+
</button>
|
|
196
|
+
<button
|
|
197
|
+
className="ai-chat-suggested-prompt"
|
|
198
|
+
onClick={() =>
|
|
199
|
+
handleSendMessage(
|
|
200
|
+
'Fix the CSS so the layout is responsive',
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
>
|
|
204
|
+
"Fix the CSS so the layout is
|
|
205
|
+
responsive"
|
|
206
|
+
</button>
|
|
207
|
+
<button
|
|
208
|
+
className="ai-chat-suggested-prompt"
|
|
209
|
+
onClick={() =>
|
|
210
|
+
handleSendMessage(
|
|
211
|
+
'Refactor this function to use async/await',
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
"Refactor this function to use
|
|
216
|
+
async/await"
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
{showSuggestedRequests && (
|
|
224
|
+
<div className="ai-chat-empty-text">
|
|
225
|
+
{aiChatMode === 'ask'
|
|
226
|
+
? 'Type your question below to get started!'
|
|
227
|
+
: 'Type your edit request below to get started!'}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
367
230
|
</div>
|
|
368
|
-
|
|
369
|
-
|
|
231
|
+
) : (
|
|
232
|
+
<MessageList
|
|
233
|
+
messages={messages}
|
|
234
|
+
aiStatus={aiStatus}
|
|
235
|
+
isLoading={isLoading}
|
|
236
|
+
chatId={currentChatId}
|
|
237
|
+
aiScratchpad={aiScratchpad}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
{aiErrorMessage && (
|
|
241
|
+
<div className="ai-chat-error">
|
|
242
|
+
<div className="ai-chat-error-content">
|
|
243
|
+
<span className="ai-chat-error-icon">
|
|
244
|
+
⚠️
|
|
245
|
+
</span>
|
|
246
|
+
<span className="ai-chat-error-message">
|
|
247
|
+
{aiErrorMessage}
|
|
248
|
+
</span>
|
|
249
|
+
<button
|
|
250
|
+
className="ai-chat-error-dismiss"
|
|
251
|
+
onClick={() => setAIErrorMessage(null)}
|
|
252
|
+
aria-label="Dismiss error"
|
|
253
|
+
>
|
|
254
|
+
×
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
<div className="ai-chat-input-fixed">
|
|
370
262
|
<ChatInput
|
|
371
263
|
aiChatMessage={aiChatMessage}
|
|
372
264
|
setAIChatMessage={setAIChatMessage}
|
|
@@ -5,6 +5,26 @@
|
|
|
5
5
|
padding: 0;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
.ai-chat-content {
|
|
9
|
+
flex: 1;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.ai-chat-messages-container {
|
|
16
|
+
flex: 1;
|
|
17
|
+
overflow-y: auto;
|
|
18
|
+
padding: 10px;
|
|
19
|
+
padding-bottom: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ai-chat-input-fixed {
|
|
23
|
+
flex-shrink: 0;
|
|
24
|
+
padding: 10px;
|
|
25
|
+
padding-top: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
.ai-chat-empty {
|
|
9
29
|
flex: 1;
|
|
10
30
|
display: flex;
|
|
@@ -365,6 +385,7 @@
|
|
|
365
385
|
.ai-chat-input-container {
|
|
366
386
|
border-top: 1px solid var(--vh-color-neutral-02);
|
|
367
387
|
padding-top: 10px;
|
|
388
|
+
background: var(--vh-color-neutral-01);
|
|
368
389
|
}
|
|
369
390
|
|
|
370
391
|
.ai-chat-mode-toggle {
|
|
@@ -427,11 +448,12 @@
|
|
|
427
448
|
border: 1px solid var(--vh-color-neutral-03) !important;
|
|
428
449
|
color: var(--vh-color-neutral-04) !important;
|
|
429
450
|
border-radius: 8px;
|
|
430
|
-
padding:
|
|
451
|
+
padding: 10px 12px;
|
|
431
452
|
font-size: 14px;
|
|
432
453
|
font-family: var(--vzcode-font-family);
|
|
433
454
|
resize: none;
|
|
434
|
-
min-height:
|
|
455
|
+
min-height: 48px;
|
|
456
|
+
line-height: 1.4;
|
|
435
457
|
|
|
436
458
|
&:focus {
|
|
437
459
|
outline: none !important;
|
|
@@ -450,37 +472,56 @@
|
|
|
450
472
|
}
|
|
451
473
|
}
|
|
452
474
|
|
|
453
|
-
.ai-chat-input-
|
|
475
|
+
.ai-chat-input-footer {
|
|
454
476
|
display: flex;
|
|
455
477
|
justify-content: space-between;
|
|
478
|
+
align-items: center;
|
|
456
479
|
font-size: 12px;
|
|
457
480
|
color: var(--vh-color-neutral-03);
|
|
458
|
-
|
|
481
|
+
min-height: 40px; // Prevent layout jumping
|
|
459
482
|
|
|
460
483
|
.ai-chat-hint {
|
|
461
484
|
font-style: italic;
|
|
485
|
+
flex: 1;
|
|
462
486
|
}
|
|
463
487
|
}
|
|
464
488
|
|
|
465
489
|
.ai-chat-send-button {
|
|
466
|
-
background: var(--vh-color-primary-01);
|
|
467
|
-
border: none;
|
|
468
|
-
color: white;
|
|
469
490
|
border-radius: 8px;
|
|
470
491
|
padding: 8px 16px;
|
|
471
492
|
font-size: 14px;
|
|
472
493
|
font-weight: 500;
|
|
473
494
|
cursor: pointer;
|
|
474
|
-
transition:
|
|
475
|
-
align-self: flex-end;
|
|
495
|
+
transition: all 0.2s ease;
|
|
476
496
|
min-width: 80px;
|
|
497
|
+
flex-shrink: 0;
|
|
477
498
|
|
|
478
|
-
|
|
479
|
-
background: var(--vh-color-primary-
|
|
499
|
+
&.btn-primary {
|
|
500
|
+
background: var(--vh-color-primary-01);
|
|
501
|
+
border-color: var(--vh-color-primary-01);
|
|
502
|
+
color: white;
|
|
503
|
+
|
|
504
|
+
&:hover:not(:disabled) {
|
|
505
|
+
background: var(--vh-color-primary-02);
|
|
506
|
+
border-color: var(--vh-color-primary-02);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
&.btn-outline-secondary {
|
|
511
|
+
background: transparent;
|
|
512
|
+
border-color: var(--vh-color-neutral-03);
|
|
513
|
+
color: var(--vh-color-neutral-03);
|
|
514
|
+
|
|
515
|
+
&:hover:not(:disabled) {
|
|
516
|
+
background: var(--vh-color-neutral-03);
|
|
517
|
+
color: var(--vh-color-neutral-01);
|
|
518
|
+
}
|
|
480
519
|
}
|
|
481
520
|
|
|
482
521
|
&:disabled {
|
|
483
522
|
background: var(--vh-color-neutral-03);
|
|
523
|
+
border-color: var(--vh-color-neutral-03);
|
|
524
|
+
color: var(--vh-color-neutral-02);
|
|
484
525
|
cursor: not-allowed;
|
|
485
526
|
}
|
|
486
527
|
}
|
|
@@ -256,7 +256,8 @@ export const addDiffToAIMessage = (
|
|
|
256
256
|
shareDBDoc: ShareDBDoc<VizContent>,
|
|
257
257
|
chatId: VizChatId,
|
|
258
258
|
diffData: any,
|
|
259
|
-
beforeFiles?: VizFiles, // Optional snapshot for undo functionality
|
|
259
|
+
beforeFiles?: VizFiles, // Optional snapshot for undo functionality (legacy)
|
|
260
|
+
beforeCommitId?: string, // Commit ID before AI changes for VizHub integration
|
|
260
261
|
) => {
|
|
261
262
|
const chat = shareDBDoc.data.chats[chatId];
|
|
262
263
|
const messages = [...chat.messages];
|
|
@@ -270,7 +271,8 @@ export const addDiffToAIMessage = (
|
|
|
270
271
|
const newMessage = {
|
|
271
272
|
...messages[lastAIMessageIndex],
|
|
272
273
|
diffData,
|
|
273
|
-
...(beforeFiles && { beforeFiles }), // Add beforeFiles only if provided
|
|
274
|
+
...(beforeFiles && { beforeFiles }), // Add beforeFiles only if provided (legacy)
|
|
275
|
+
...(beforeCommitId && { beforeCommitId }), // Add beforeCommitId for VizHub integration
|
|
274
276
|
};
|
|
275
277
|
// Use type assertion to extend the message with diffData
|
|
276
278
|
(messages[lastAIMessageIndex] as any) = newMessage;
|
|
@@ -23,10 +23,12 @@ export const handleAIChatMessage =
|
|
|
23
23
|
shareDBDoc,
|
|
24
24
|
createAIEditLocalPresence,
|
|
25
25
|
onCreditDeduction,
|
|
26
|
+
getCurrentCommitId,
|
|
26
27
|
}: {
|
|
27
28
|
shareDBDoc: ShareDBDoc<VizContent>;
|
|
28
29
|
createAIEditLocalPresence: () => any;
|
|
29
30
|
onCreditDeduction?: any;
|
|
31
|
+
getCurrentCommitId?: () => string | null;
|
|
30
32
|
}) =>
|
|
31
33
|
async (req: any, res: any) => {
|
|
32
34
|
const { content, chatId, mode = 'edit' } = req.body;
|
|
@@ -52,6 +54,11 @@ export const handleAIChatMessage =
|
|
|
52
54
|
ensureChatsExist(shareDBDoc);
|
|
53
55
|
ensureChatExists(shareDBDoc, chatId);
|
|
54
56
|
|
|
57
|
+
// Capture the current commit ID before making changes (for VizHub integration)
|
|
58
|
+
const beforeCommitId = getCurrentCommitId
|
|
59
|
+
? getCurrentCommitId()
|
|
60
|
+
: null;
|
|
61
|
+
|
|
55
62
|
// Add user message to chat
|
|
56
63
|
addUserMessage(shareDBDoc, chatId, content);
|
|
57
64
|
|
|
@@ -92,7 +99,8 @@ export const handleAIChatMessage =
|
|
|
92
99
|
shareDBDoc,
|
|
93
100
|
chatId,
|
|
94
101
|
editResult.diffData,
|
|
95
|
-
(editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (
|
|
102
|
+
(editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (legacy)
|
|
103
|
+
beforeCommitId, // Pass the commit ID before AI changes (for VizHub integration)
|
|
96
104
|
);
|
|
97
105
|
}
|
|
98
106
|
|
|
@@ -45,10 +45,15 @@ export const createLLMFunction = ({
|
|
|
45
45
|
shareDBDoc,
|
|
46
46
|
createAIEditLocalPresence,
|
|
47
47
|
chatId,
|
|
48
|
+
// Feature flag to enable/disable reasoning tokens.
|
|
49
|
+
// When false, reasoning tokens are not requested from the API
|
|
50
|
+
// and reasoning content is not processed in the streaming response.
|
|
51
|
+
enableReasoningTokens = false,
|
|
48
52
|
}: {
|
|
49
53
|
shareDBDoc: ShareDBDoc<VizContent>;
|
|
50
54
|
createAIEditLocalPresence: () => any;
|
|
51
55
|
chatId: VizChatId;
|
|
56
|
+
enableReasoningTokens?: boolean;
|
|
52
57
|
}) => {
|
|
53
58
|
return async (fullPrompt: string) => {
|
|
54
59
|
const localPresence = enableStreamingEditing
|
|
@@ -197,21 +202,29 @@ export const createLLMFunction = ({
|
|
|
197
202
|
const modelName =
|
|
198
203
|
process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
|
|
199
204
|
'anthropic/claude-3.5-sonnet';
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
|
|
206
|
+
// Configure reasoning tokens based on enableReasoningTokens flag
|
|
207
|
+
const requestConfig: any = {
|
|
203
208
|
model: modelName,
|
|
204
209
|
messages: [{ role: 'user', content: fullPrompt }],
|
|
205
|
-
reasoning: {
|
|
206
|
-
effort: 'medium',
|
|
207
|
-
exclude: false,
|
|
208
|
-
},
|
|
209
210
|
provider: {
|
|
210
|
-
sort: '
|
|
211
|
+
sort: 'price',
|
|
211
212
|
},
|
|
212
213
|
usage: { include: true },
|
|
213
214
|
stream: true,
|
|
214
|
-
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Only include reasoning configuration if reasoning tokens are enabled
|
|
218
|
+
if (enableReasoningTokens) {
|
|
219
|
+
requestConfig.reasoning = {
|
|
220
|
+
effort: 'low',
|
|
221
|
+
exclude: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const stream = await (
|
|
226
|
+
openRouterClient.chat.completions.create as any
|
|
227
|
+
)(requestConfig);
|
|
215
228
|
|
|
216
229
|
let reasoningStarted = false;
|
|
217
230
|
let contentStarted = false;
|
|
@@ -219,8 +232,8 @@ export const createLLMFunction = ({
|
|
|
219
232
|
for await (const chunk of stream) {
|
|
220
233
|
const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
|
|
221
234
|
|
|
222
|
-
if (delta?.reasoning) {
|
|
223
|
-
// Handle reasoning tokens (thinking)
|
|
235
|
+
if (delta?.reasoning && enableReasoningTokens) {
|
|
236
|
+
// Handle reasoning tokens (thinking) - only if enabled
|
|
224
237
|
if (!reasoningStarted) {
|
|
225
238
|
reasoningStarted = true;
|
|
226
239
|
updateAIStatus(shareDBDoc, chatId, 'Thinking...');
|