vzcode 1.57.0 → 1.59.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.
@@ -1,42 +1,28 @@
1
- import {
2
- useContext,
3
- useState,
4
- useCallback,
5
- useMemo,
6
- useEffect,
7
- } from 'react';
1
+ import { useContext, useMemo, useEffect } from 'react';
8
2
  import { VZCodeContext } from '../../VZCodeContext';
9
- import { v4 as uuidv4 } from 'uuid';
10
3
  import { MessageList } from './MessageList';
11
4
  import { ChatInput } from './ChatInput';
12
5
  import './styles.scss';
13
6
 
14
- const defaultAIChatEndpoint = '/api/ai-chat/';
15
-
16
7
  const DEBUG = false;
17
8
 
18
- const showSuggestedRequests = false;
9
+ const showSuggestedRequests = true;
19
10
 
20
11
  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
12
  const {
31
13
  aiChatFocused,
32
14
  content,
33
- aiChatEndpoint = defaultAIChatEndpoint,
34
- aiChatOptions = {},
35
15
  aiChatMode,
16
+ aiChatMessage,
17
+ isLoading,
18
+ currentChatId,
19
+ aiErrorMessage,
36
20
  setAIChatMode,
37
- autoForkAndRetryAI,
38
21
  clearStoredAIPrompt,
39
22
  getStoredAIPrompt,
23
+ setAIChatMessage,
24
+ handleSendMessage,
25
+ setAIErrorMessage,
40
26
  } = useContext(VZCodeContext);
41
27
 
42
28
  // Get current chat data from content
@@ -58,118 +44,6 @@ export const AIChat = () => {
58
44
  // Check if this is the first time opening the chat (no messages)
59
45
  const isEmptyState = rawMessages.length === 0;
60
46
 
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
- // Fail silently in case of timeout,
150
- // which happens frequently with the longer running LLM calls.
151
- // TODO refactor this so that the network request to the server
152
- // always returns immediately, and we track the `isLoading` state
153
- // within the ShareDB document itself.
154
- //
155
- // setErrorMessage(
156
- // 'Failed to send message. Please try again.',
157
- // );
158
- } finally {
159
- setIsLoading(false);
160
- }
161
- },
162
- [
163
- aiChatMessage,
164
- isLoading,
165
- aiChatEndpoint,
166
- aiChatOptions,
167
- currentChatId,
168
- aiChatMode,
169
- autoForkAndRetryAI,
170
- ],
171
- );
172
-
173
47
  // Check for stored AI prompt on component mount (post-fork restoration)
174
48
  useEffect(() => {
175
49
  DEBUG &&
@@ -350,18 +224,18 @@ export const AIChat = () => {
350
224
  aiScratchpad={aiScratchpad}
351
225
  />
352
226
  )}
353
- {errorMessage && (
227
+ {aiErrorMessage && (
354
228
  <div className="ai-chat-error">
355
229
  <div className="ai-chat-error-content">
356
230
  <span className="ai-chat-error-icon">
357
231
  ⚠️
358
232
  </span>
359
233
  <span className="ai-chat-error-message">
360
- {errorMessage}
234
+ {aiErrorMessage}
361
235
  </span>
362
236
  <button
363
237
  className="ai-chat-error-dismiss"
364
- onClick={() => setErrorMessage(null)}
238
+ onClick={() => setAIErrorMessage(null)}
365
239
  aria-label="Dismiss error"
366
240
  >
367
241
  ×
@@ -182,6 +182,7 @@
182
182
  .undo-button-container {
183
183
  display: flex;
184
184
  justify-content: flex-end;
185
+ gap: 8px;
185
186
  margin-top: 16px;
186
187
  }
187
188
 
@@ -560,6 +561,21 @@
560
561
  max-height: 200px;
561
562
  overflow-y: auto;
562
563
  background: var(--vh-color-neutral-02);
564
+ scroll-behavior: smooth;
565
+
566
+ &::-webkit-scrollbar {
567
+ width: 6px;
568
+ }
569
+
570
+ &::-webkit-scrollbar-track {
571
+ background: var(--vh-color-neutral-02);
572
+ border-radius: 3px;
573
+ }
574
+
575
+ &::-webkit-scrollbar-thumb {
576
+ background: var(--vh-color-neutral-04);
577
+ border-radius: 3px;
578
+ }
563
579
 
564
580
  // Style markdown content in thinking scratchpad
565
581
  p {
@@ -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,14 @@ export const handleAIChatMessage =
23
23
  shareDBDoc,
24
24
  createAIEditLocalPresence,
25
25
  onCreditDeduction,
26
+ getCurrentCommitId,
27
+ model,
26
28
  }: {
27
29
  shareDBDoc: ShareDBDoc<VizContent>;
28
30
  createAIEditLocalPresence: () => any;
29
31
  onCreditDeduction?: any;
32
+ getCurrentCommitId?: () => string | null;
33
+ model?: string;
30
34
  }) =>
31
35
  async (req: any, res: any) => {
32
36
  const { content, chatId, mode = 'edit' } = req.body;
@@ -52,6 +56,11 @@ export const handleAIChatMessage =
52
56
  ensureChatsExist(shareDBDoc);
53
57
  ensureChatExists(shareDBDoc, chatId);
54
58
 
59
+ // Capture the current commit ID before making changes (for VizHub integration)
60
+ const beforeCommitId = getCurrentCommitId
61
+ ? getCurrentCommitId()
62
+ : null;
63
+
55
64
  // Add user message to chat
56
65
  addUserMessage(shareDBDoc, chatId, content);
57
66
 
@@ -60,6 +69,7 @@ export const handleAIChatMessage =
60
69
  shareDBDoc,
61
70
  createAIEditLocalPresence,
62
71
  chatId,
72
+ model,
63
73
  });
64
74
 
65
75
  // Create server-side runCode function using shareDBDoc
@@ -92,7 +102,8 @@ export const handleAIChatMessage =
92
102
  shareDBDoc,
93
103
  chatId,
94
104
  editResult.diffData,
95
- (editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (only available for edit mode)
105
+ (editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (legacy)
106
+ beforeCommitId, // Pass the commit ID before AI changes (for VizHub integration)
96
107
  );
97
108
  }
98
109
 
@@ -45,10 +45,17 @@ 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,
52
+ model,
48
53
  }: {
49
54
  shareDBDoc: ShareDBDoc<VizContent>;
50
55
  createAIEditLocalPresence: () => any;
51
56
  chatId: VizChatId;
57
+ enableReasoningTokens?: boolean;
58
+ model?: string;
52
59
  }) => {
53
60
  return async (fullPrompt: string) => {
54
61
  const localPresence = enableStreamingEditing
@@ -195,23 +202,32 @@ export const createLLMFunction = ({
195
202
 
196
203
  // Stream the response with reasoning tokens
197
204
  const modelName =
205
+ model ||
198
206
  process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
199
207
  'anthropic/claude-3.5-sonnet';
200
- const stream = await (
201
- openRouterClient.chat.completions.create as any
202
- )({
208
+
209
+ // Configure reasoning tokens based on enableReasoningTokens flag
210
+ const requestConfig: any = {
203
211
  model: modelName,
204
212
  messages: [{ role: 'user', content: fullPrompt }],
205
- reasoning: {
206
- effort: 'low',
207
- exclude: false,
208
- },
209
213
  provider: {
210
214
  sort: 'price',
211
215
  },
212
216
  usage: { include: true },
213
217
  stream: true,
214
- });
218
+ };
219
+
220
+ // Only include reasoning configuration if reasoning tokens are enabled
221
+ if (enableReasoningTokens) {
222
+ requestConfig.reasoning = {
223
+ effort: 'low',
224
+ exclude: false,
225
+ };
226
+ }
227
+
228
+ const stream = await (
229
+ openRouterClient.chat.completions.create as any
230
+ )(requestConfig);
215
231
 
216
232
  let reasoningStarted = false;
217
233
  let contentStarted = false;
@@ -219,8 +235,8 @@ export const createLLMFunction = ({
219
235
  for await (const chunk of stream) {
220
236
  const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
221
237
 
222
- if (delta?.reasoning) {
223
- // Handle reasoning tokens (thinking)
238
+ if (delta?.reasoning && enableReasoningTokens) {
239
+ // Handle reasoning tokens (thinking) - only if enabled
224
240
  if (!reasoningStarted) {
225
241
  reasoningStarted = true;
226
242
  updateAIStatus(shareDBDoc, chatId, 'Thinking...');