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.
@@ -15,28 +15,27 @@ const defaultAIChatEndpoint = '/api/ai-chat/';
15
15
 
16
16
  const DEBUG = false;
17
17
 
18
- const showSuggestedRequests = false;
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
- style={{
219
- padding: '10px',
220
- flex: 1,
221
- display: 'flex',
222
- flexDirection: 'column',
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
- {aiChatMode === 'ask'
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
- </div>
344
- ) : (
345
- <MessageList
346
- messages={messages}
347
- aiStatus={aiStatus}
348
- isLoading={isLoading}
349
- chatId={currentChatId}
350
- aiScratchpad={aiScratchpad}
351
- />
352
- )}
353
- {errorMessage && (
354
- <div className="ai-chat-error">
355
- <div className="ai-chat-error-content">
356
- <span className="ai-chat-error-icon">⚠️</span>
357
- <span className="ai-chat-error-message">
358
- {errorMessage}
359
- </span>
360
- <button
361
- className="ai-chat-error-dismiss"
362
- onClick={() => setErrorMessage(null)}
363
- aria-label="Dismiss error"
364
- >
365
- ×
366
- </button>
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
- </div>
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: 8px 10px;
451
+ padding: 10px 12px;
431
452
  font-size: 14px;
432
453
  font-family: var(--vzcode-font-family);
433
454
  resize: none;
434
- min-height: 36px;
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-info {
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
- margin-bottom: 6px;
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: background-color 0.2s ease;
475
- align-self: flex-end;
495
+ transition: all 0.2s ease;
476
496
  min-width: 80px;
497
+ flex-shrink: 0;
477
498
 
478
- &:hover:not(:disabled) {
479
- background: var(--vh-color-primary-02);
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 (only available for edit mode)
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
- const stream = await (
201
- openRouterClient.chat.completions.create as any
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: 'throughput',
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...');