vzcode 1.50.0 → 1.52.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.
@@ -3,6 +3,7 @@ import {
3
3
  useState,
4
4
  useCallback,
5
5
  useMemo,
6
+ useEffect,
6
7
  } from 'react';
7
8
  import { VZCodeContext } from '../../VZCodeContext';
8
9
  import { v4 as uuidv4 } from 'uuid';
@@ -12,6 +13,8 @@ import './styles.scss';
12
13
 
13
14
  const defaultAIChatEndpoint = '/api/ai-chat/';
14
15
 
16
+ const DEBUG = false;
17
+
15
18
  export const AIChat = () => {
16
19
  const { aiChatMessage, setAIChatMessage } =
17
20
  useContext(VZCodeContext);
@@ -29,6 +32,9 @@ export const AIChat = () => {
29
32
  aiChatOptions = {},
30
33
  aiChatMode,
31
34
  setAIChatMode,
35
+ autoForkAndRetryAI,
36
+ clearStoredAIPrompt,
37
+ getStoredAIPrompt,
32
38
  } = useContext(VZCodeContext);
33
39
 
34
40
  // Get current chat data from content
@@ -50,63 +56,154 @@ export const AIChat = () => {
50
56
  // Check if this is the first time opening the chat (no messages)
51
57
  const isEmptyState = rawMessages.length === 0;
52
58
 
53
- const handleSendMessage = useCallback(async () => {
54
- if (!aiChatMessage.trim() || isLoading) return;
59
+ const handleSendMessage = useCallback(
60
+ async (messageToSend?: string) => {
61
+ DEBUG &&
62
+ console.log(
63
+ 'AIChat: handleSendMessage called with:',
64
+ messageToSend,
65
+ 'aiChatMessage:',
66
+ aiChatMessage,
67
+ );
68
+ const messageContent = messageToSend || aiChatMessage;
69
+ if (
70
+ !messageContent ||
71
+ typeof messageContent !== 'string' ||
72
+ !messageContent.trim() ||
73
+ isLoading
74
+ )
75
+ return;
55
76
 
56
- setAIChatMessage('');
57
- setIsLoading(true);
58
- setErrorMessage(null); // Clear any previous errors
77
+ const currentPrompt = aiChatMessage.trim();
78
+ setAIChatMessage('');
79
+ setIsLoading(true);
80
+ setErrorMessage(null); // Clear any previous errors
59
81
 
60
- // Call backend endpoint for AI response
61
- // The server will handle all ShareDB operations including adding the user message
62
- try {
63
- const response = await fetch(aiChatEndpoint, {
64
- method: 'POST',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- },
68
- body: JSON.stringify({
69
- ...aiChatOptions,
70
- vizId: aiChatOptions.vizId,
71
- content: aiChatMessage.trim(),
72
- chatId: currentChatId,
73
- mode: aiChatMode,
74
- }),
75
- });
82
+ // Call backend endpoint for AI response
83
+ // The server will handle all ShareDB operations including adding the user message
84
+ try {
85
+ const response = await fetch(aiChatEndpoint, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ body: JSON.stringify({
91
+ ...aiChatOptions,
92
+ vizId: aiChatOptions.vizId,
93
+ content: messageContent.trim(),
94
+ chatId: currentChatId,
95
+ mode: aiChatMode,
96
+ }),
97
+ });
76
98
 
77
- if (!response.ok) {
78
- throw new Error(
79
- `HTTP error! status: ${response.status}`,
80
- );
81
- }
99
+ if (!response.ok) {
100
+ throw new Error(
101
+ `HTTP error! status: ${response.status}`,
102
+ );
103
+ }
82
104
 
83
- // Parse the response to check for errors
84
- const responseData = await response.json();
105
+ // Parse the response to check for errors
106
+ const responseData = await response.json();
85
107
 
86
- // Check if the response contains a VizHub error
87
- if (
88
- responseData.outcome === 'failure' &&
89
- responseData.error
90
- ) {
91
- setErrorMessage(responseData.error.message);
92
- return;
108
+ // Check if the response contains a VizHub error
109
+ if (
110
+ responseData.outcome === 'failure' &&
111
+ responseData.error
112
+ ) {
113
+ const errorMessage = responseData.error.message;
114
+
115
+ // Check if this is the specific permission error that should trigger auto-fork
116
+ if (
117
+ errorMessage ===
118
+ '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.'
119
+ ) {
120
+ // Trigger auto-fork instead of showing error
121
+ try {
122
+ await autoForkAndRetryAI?.(
123
+ currentPrompt,
124
+ aiChatMode,
125
+ );
126
+ // If we reach here, the fork was successful and redirect should happen
127
+ return;
128
+ } catch (forkError) {
129
+ console.error('Auto-fork failed:', forkError);
130
+ setErrorMessage(
131
+ 'Failed to fork visualization. Please try forking manually.',
132
+ );
133
+ return;
134
+ }
135
+ }
136
+
137
+ // For other errors, show the error message
138
+ setErrorMessage(errorMessage);
139
+ return;
140
+ }
141
+
142
+ // The backend handles all ShareDB operations for successful responses
143
+ } catch (error) {
144
+ console.error('Error getting AI response:', error);
145
+ setErrorMessage(
146
+ 'Failed to send message. Please try again.',
147
+ );
148
+ } finally {
149
+ setIsLoading(false);
93
150
  }
151
+ },
152
+ [
153
+ aiChatMessage,
154
+ isLoading,
155
+ aiChatEndpoint,
156
+ aiChatOptions,
157
+ currentChatId,
158
+ aiChatMode,
159
+ autoForkAndRetryAI,
160
+ ],
161
+ );
94
162
 
95
- // The backend handles all ShareDB operations for successful responses
96
- } catch (error) {
97
- console.error('Error getting AI response:', error);
98
- setErrorMessage(
99
- 'Failed to send message. Please try again.',
163
+ // Check for stored AI prompt on component mount (post-fork restoration)
164
+ useEffect(() => {
165
+ DEBUG &&
166
+ console.log('AIChat: Checking for stored AI prompt');
167
+ const storedPrompt = getStoredAIPrompt();
168
+ DEBUG &&
169
+ console.log(
170
+ 'AIChat: Stored prompt result:',
171
+ storedPrompt,
100
172
  );
101
- } finally {
102
- setIsLoading(false);
173
+ if (storedPrompt) {
174
+ // Restore the prompt and mode
175
+ DEBUG &&
176
+ console.log('AIChat: Restoring prompt and mode');
177
+ setAIChatMessage(storedPrompt.prompt);
178
+ setAIChatMode(
179
+ storedPrompt.modelName === 'ask' ? 'ask' : 'edit',
180
+ );
181
+
182
+ // Clear the stored prompt
183
+ DEBUG &&
184
+ console.log('AIChat: Clearing stored prompt');
185
+ clearStoredAIPrompt();
186
+
187
+ // Auto-submit the restored prompt after a short delay
188
+ DEBUG &&
189
+ console.log('AIChat: Scheduling auto-submit');
190
+ setTimeout(() => {
191
+ DEBUG &&
192
+ console.log(
193
+ 'AIChat: Auto-submitting restored prompt',
194
+ );
195
+ handleSendMessage(storedPrompt.prompt);
196
+ }, 100);
197
+ } else {
198
+ DEBUG &&
199
+ console.log('AIChat: No stored prompt found');
103
200
  }
104
201
  }, [
105
- aiChatMessage,
106
- isLoading,
107
- aiChatEndpoint,
108
- aiChatOptions,
109
- currentChatId,
202
+ getStoredAIPrompt,
203
+ clearStoredAIPrompt,
204
+ setAIChatMessage,
205
+ setAIChatMode,
206
+ handleSendMessage,
110
207
  ]);
111
208
 
112
209
  return (
@@ -123,7 +220,7 @@ export const AIChat = () => {
123
220
  <div className="ai-chat-empty">
124
221
  <div className="ai-chat-empty-icon">✨</div>
125
222
  <h3 className="ai-chat-empty-title">
126
- Hi, I'm VizBot!
223
+ Hi! I'm here to help you edit with AI.
127
224
  </h3>
128
225
  <div className="ai-chat-empty-text">
129
226
  How can I help you?
@@ -136,7 +233,7 @@ export const AIChat = () => {
136
233
  <button
137
234
  className="ai-chat-suggested-prompt"
138
235
  onClick={() =>
139
- setAIChatMessage(
236
+ handleSendMessage(
140
237
  'Explain how this works',
141
238
  )
142
239
  }
@@ -146,7 +243,7 @@ export const AIChat = () => {
146
243
  <button
147
244
  className="ai-chat-suggested-prompt"
148
245
  onClick={() =>
149
- setAIChatMessage(
246
+ handleSendMessage(
150
247
  'How could I change it so that the circles are bigger?',
151
248
  )
152
249
  }
@@ -157,7 +254,7 @@ export const AIChat = () => {
157
254
  <button
158
255
  className="ai-chat-suggested-prompt"
159
256
  onClick={() =>
160
- setAIChatMessage(
257
+ handleSendMessage(
161
258
  'What does this function do?',
162
259
  )
163
260
  }
@@ -167,7 +264,7 @@ export const AIChat = () => {
167
264
  <button
168
265
  className="ai-chat-suggested-prompt"
169
266
  onClick={() =>
170
- setAIChatMessage(
267
+ handleSendMessage(
171
268
  'How can I make this more accessible?',
172
269
  )
173
270
  }
@@ -183,7 +280,7 @@ export const AIChat = () => {
183
280
  <button
184
281
  className="ai-chat-suggested-prompt"
185
282
  onClick={() =>
186
- setAIChatMessage(
283
+ handleSendMessage(
187
284
  'Change the circles to squares',
188
285
  )
189
286
  }
@@ -193,7 +290,7 @@ export const AIChat = () => {
193
290
  <button
194
291
  className="ai-chat-suggested-prompt"
195
292
  onClick={() =>
196
- setAIChatMessage(
293
+ handleSendMessage(
197
294
  'Add a button that toggles the animation',
198
295
  )
199
296
  }
@@ -204,7 +301,7 @@ export const AIChat = () => {
204
301
  <button
205
302
  className="ai-chat-suggested-prompt"
206
303
  onClick={() =>
207
- setAIChatMessage(
304
+ handleSendMessage(
208
305
  'Fix the CSS so the layout is responsive',
209
306
  )
210
307
  }
@@ -215,7 +312,7 @@ export const AIChat = () => {
215
312
  <button
216
313
  className="ai-chat-suggested-prompt"
217
314
  onClick={() =>
218
- setAIChatMessage(
315
+ handleSendMessage(
219
316
  'Refactor this function to use async/await',
220
317
  )
221
318
  }
@@ -150,6 +150,33 @@
150
150
  line-height: 1.4;
151
151
  word-wrap: break-word;
152
152
 
153
+ // Undo button styles (moved from DiffView)
154
+ .undo-button-container {
155
+ display: flex;
156
+ justify-content: flex-end;
157
+ margin-top: 16px;
158
+ }
159
+
160
+ .undo-button {
161
+ background-color: #dc3545;
162
+ color: white;
163
+ border: none;
164
+ border-radius: 4px;
165
+ padding: 4px 8px;
166
+ font-size: 12px;
167
+ cursor: pointer;
168
+ transition: background-color 0.2s;
169
+
170
+ &:hover:not(:disabled) {
171
+ background-color: #c82333;
172
+ }
173
+
174
+ &:disabled {
175
+ background-color: #6c757d;
176
+ cursor: not-allowed;
177
+ }
178
+ }
179
+
153
180
  // Markdown styling for dark backgrounds
154
181
  pre {
155
182
  background-color: rgba(0, 0, 0, 0.3);
@@ -110,7 +110,7 @@ export const VZSidebar = ({
110
110
  ),
111
111
  aiChatToolTipText = (
112
112
  <div>
113
- <strong>VizBot - AI Code Editor</strong>
113
+ <strong>Edit with AI</strong>
114
114
  </div>
115
115
  ),
116
116
  }: {
@@ -21,11 +21,11 @@ const DEBUG = false;
21
21
  export const handleAIChatMessage =
22
22
  ({
23
23
  shareDBDoc,
24
- createVizBotLocalPresence,
24
+ createAIEditLocalPresence,
25
25
  onCreditDeduction,
26
26
  }: {
27
27
  shareDBDoc: ShareDBDoc<VizContent>;
28
- createVizBotLocalPresence: () => any;
28
+ createAIEditLocalPresence: () => any;
29
29
  onCreditDeduction?: any;
30
30
  }) =>
31
31
  async (req: any, res: any) => {
@@ -58,7 +58,7 @@ export const handleAIChatMessage =
58
58
  // Create LLM function for streaming
59
59
  const llmFunction = createLLMFunction({
60
60
  shareDBDoc,
61
- createVizBotLocalPresence,
61
+ createAIEditLocalPresence,
62
62
  chatId,
63
63
  });
64
64
 
@@ -44,15 +44,15 @@ const enableStreamingEditing = false;
44
44
  */
45
45
  export const createLLMFunction = ({
46
46
  shareDBDoc,
47
- createVizBotLocalPresence,
47
+ createAIEditLocalPresence,
48
48
  chatId,
49
49
  }: {
50
50
  shareDBDoc: ShareDBDoc<VizContent>;
51
- createVizBotLocalPresence: () => any;
51
+ createAIEditLocalPresence: () => any;
52
52
  chatId: VizChatId;
53
53
  }) => {
54
54
  return async (fullPrompt: string) => {
55
- const localPresence = createVizBotLocalPresence();
55
+ const localPresence = createAIEditLocalPresence();
56
56
 
57
57
  // Create OpenRouter client for reasoning token support
58
58
  const openRouterClient = new OpenAI({
@@ -133,13 +133,13 @@ export const createLLMFunction = ({
133
133
  line,
134
134
  );
135
135
 
136
- // Update VizBot presence to show cursor at the end of the file
136
+ // Update AI presence to show cursor at the end of the file
137
137
  const currentFile =
138
138
  shareDBDoc.data.files[currentEditingFileId];
139
139
  if (currentFile && currentFile.text) {
140
140
  const textLength = currentFile.text.length;
141
141
  const filePresence = {
142
- username: 'VizBot',
142
+ username: 'AI Editor',
143
143
  start: [
144
144
  'files',
145
145
  currentEditingFileId,
@@ -157,7 +157,7 @@ export const createLLMFunction = ({
157
157
  localPresence.submit(filePresence, (error) => {
158
158
  if (error) {
159
159
  console.warn(
160
- 'VizBot line presence submission error:',
160
+ 'AI Editor line presence submission error:',
161
161
  error,
162
162
  );
163
163
  }
@@ -201,6 +201,9 @@ export const createLLMFunction = ({
201
201
  effort: 'medium',
202
202
  exclude: false,
203
203
  },
204
+ provider: {
205
+ sort: 'throughput',
206
+ }, // New parameter for OpenRouter routing
204
207
  usage: { include: true },
205
208
  stream: true,
206
209
  });
@@ -275,16 +278,16 @@ export const createLLMFunction = ({
275
278
  // Finalize the AI message by clearing temporary fields
276
279
  finalizeAIMessage(shareDBDoc, chatId);
277
280
 
278
- // Clear VizBot presence when done
281
+ // Clear AI Editor presence when done
279
282
  DEBUG &&
280
283
  console.log(
281
- 'AI editing done, clearing VizBot presence',
284
+ 'AI editing done, clearing AI Editor presence',
282
285
  );
283
286
  localPresence.submit(null, (error) => {
284
- DEBUG && console.log('VizBot presence cleared');
287
+ DEBUG && console.log('AI Editor presence cleared');
285
288
  if (error) {
286
289
  console.warn(
287
- 'VizBot presence cleanup error:',
290
+ 'AI Editor presence cleanup error:',
288
291
  error,
289
292
  );
290
293
  }
@@ -154,6 +154,9 @@ export const generateAIResponse = async ({
154
154
  effort: 'medium',
155
155
  exclude: false,
156
156
  } as any, // Type assertion for OpenRouter-specific reasoning parameter
157
+ provider: {
158
+ sort: 'throughput',
159
+ } as any, // Type assertion for OpenRouter-specific provider parameter
157
160
  stream: true,
158
161
  });
159
162
 
@@ -40,6 +40,9 @@ export const handleAICopilot = () => {
40
40
  apiKey: VZCODE_AI_COPILOT_API_KEY,
41
41
  baseURL: VZCODE_AI_COPILOT_BASE_URL,
42
42
  },
43
+ additionalParameters: {
44
+ provider: { sort: 'throughput' },
45
+ },
43
46
  streaming: false,
44
47
  };
45
48
  debug && console.log('chatModel options:', options);
@@ -100,20 +100,20 @@ app.use(express.static(dir));
100
100
  const shareDBConnection = shareDBBackend.connect();
101
101
  const shareDBDoc = shareDBConnection.get('documents', '1');
102
102
 
103
- // Set up presence for VizBot following the same pattern as useShareDB.ts
103
+ // Set up presence for AI editing following the same pattern as useShareDB.ts
104
104
  const docPresence = shareDBConnection.getDocPresence(
105
105
  'documents',
106
106
  '1',
107
107
  );
108
108
 
109
- // Create local presence for VizBot with a unique ID
110
- const generateVizBotId = () => {
109
+ // Create local presence for AI editing with a unique ID
110
+ const generateAIEditId = () => {
111
111
  const timestamp = Date.now().toString(36);
112
- return `vizbot-${timestamp}`;
112
+ return `ai-edit-${timestamp}`;
113
113
  };
114
114
 
115
- const createVizBotLocalPresence = () =>
116
- docPresence.create(generateVizBotId());
115
+ const createAIEditLocalPresence = () =>
116
+ docPresence.create(generateAIEditId());
117
117
 
118
118
  shareDBDoc.create(initialDocument, json1Presence.type.uri);
119
119
 
@@ -137,7 +137,7 @@ app.post(
137
137
  bodyParser.json(),
138
138
  handleAIChatMessage({
139
139
  shareDBDoc,
140
- createVizBotLocalPresence,
140
+ createAIEditLocalPresence,
141
141
  onCreditDeduction: undefined,
142
142
  }),
143
143
  );