vzcode 1.45.0 → 1.47.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.
Files changed (33) hide show
  1. package/dist/assets/{index-C1lTrzOZ.js → index-BMYXp1Uw.js} +114 -114
  2. package/dist/assets/{index-DeeOcIzw.css → index-BXyJ2hMf.css} +1 -1
  3. package/dist/index.html +2 -2
  4. package/package.json +7 -3
  5. package/src/client/CodeEditor/index.tsx +7 -4
  6. package/src/client/RunCodeWidget/index.tsx +18 -15
  7. package/src/client/SplitPaneResizeContext.tsx +26 -108
  8. package/src/client/VZCodeContext.tsx +8 -1
  9. package/src/client/VZRight.tsx +32 -4
  10. package/src/client/VZSidebar/AIChat/ChatInput.tsx +72 -2
  11. package/src/client/VZSidebar/AIChat/DiffView.scss +29 -0
  12. package/src/client/VZSidebar/AIChat/DiffView.tsx +62 -1
  13. package/src/client/VZSidebar/AIChat/Message.tsx +14 -1
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -13
  15. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
  16. package/src/client/VZSidebar/AIChat/index.tsx +187 -16
  17. package/src/client/VZSidebar/AIChat/styles.scss +278 -1
  18. package/src/client/VZSidebar/aiCopyPaste.ts +6 -1
  19. package/src/client/VZSidebar/index.tsx +10 -2
  20. package/src/client/bootstrap.ts +11 -1
  21. package/src/client/useActions.ts +12 -0
  22. package/src/client/usePrettier/index.ts +1 -3
  23. package/src/client/vzReducer/aiChatReducer.ts +13 -0
  24. package/src/client/vzReducer/createInitialState.ts +1 -0
  25. package/src/client/vzReducer/index.ts +9 -0
  26. package/src/client/vzReducer/searchReducer.test.ts +44 -2
  27. package/src/runCode.ts +5 -17
  28. package/src/server/aiChatHandler/aiEditing.ts +37 -0
  29. package/src/server/aiChatHandler/chatOperations.ts +38 -1
  30. package/src/server/aiChatHandler/index.ts +20 -9
  31. package/src/server/aiChatHandler/llmStreaming.ts +101 -19
  32. package/src/server/generateAIResponse.ts +4 -0
  33. package/src/server/index.ts +52 -0
@@ -1,4 +1,7 @@
1
- import { dateToTimestamp } from '@vizhub/viz-utils';
1
+ import {
2
+ dateToTimestamp,
3
+ generateRunId,
4
+ } from '@vizhub/viz-utils';
2
5
  import { randomId } from '../../randomId.js';
3
6
  import { ShareDBDoc } from '../../types.js';
4
7
  import { diff } from '../../ot.js';
@@ -253,6 +256,7 @@ export const addDiffToAIMessage = (
253
256
  shareDBDoc: ShareDBDoc<VizContent>,
254
257
  chatId: VizChatId,
255
258
  diffData: any,
259
+ beforeFiles?: VizFiles, // Optional snapshot for undo functionality
256
260
  ) => {
257
261
  const chat = shareDBDoc.data.chats[chatId];
258
262
  const messages = [...chat.messages];
@@ -266,6 +270,7 @@ export const addDiffToAIMessage = (
266
270
  const newMessage = {
267
271
  ...messages[lastAIMessageIndex],
268
272
  diffData,
273
+ ...(beforeFiles && { beforeFiles }), // Add beforeFiles only if provided
269
274
  };
270
275
  // Use type assertion to extend the message with diffData
271
276
  (messages[lastAIMessageIndex] as any) = newMessage;
@@ -447,3 +452,35 @@ export const appendLineToFile = (
447
452
 
448
453
  shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
449
454
  };
455
+
456
+ /**
457
+ * Undoes the last AI edit by restoring files and removing the AI message
458
+ */
459
+ export const undoAIEdit = (
460
+ shareDBDoc: ShareDBDoc<VizContent>,
461
+ chatId: VizChatId,
462
+ messageId: string,
463
+ beforeFiles: VizFiles,
464
+ ) => {
465
+ const chat = shareDBDoc.data.chats[chatId];
466
+ const messages = chat.messages.filter(
467
+ (msg) => msg.id !== messageId,
468
+ );
469
+
470
+ const op = diff(shareDBDoc.data, {
471
+ ...shareDBDoc.data,
472
+ files: beforeFiles,
473
+ chats: {
474
+ ...shareDBDoc.data.chats,
475
+ [chatId]: {
476
+ ...chat,
477
+ messages,
478
+ updatedAt: dateToTimestamp(new Date()),
479
+ },
480
+ },
481
+ // Trigger a re-run
482
+ runId: generateRunId(),
483
+ });
484
+
485
+ shareDBDoc.submitOp(op);
486
+ };
@@ -6,7 +6,10 @@ import {
6
6
  addDiffToAIMessage,
7
7
  } from './chatOperations.js';
8
8
  import { createLLMFunction } from './llmStreaming.js';
9
- import { performAIEditing } from './aiEditing.js';
9
+ import {
10
+ performAIEditing,
11
+ performAIChat,
12
+ } from './aiEditing.js';
10
13
  import { handleError } from './errorHandling.js';
11
14
  import { createRunCodeFunction } from '../../runCode.js';
12
15
  import { ShareDBDoc } from '../../types.js';
@@ -26,7 +29,7 @@ export const handleAIChatMessage =
26
29
  onCreditDeduction?: any;
27
30
  }) =>
28
31
  async (req: any, res: any) => {
29
- const { content, chatId } = req.body;
32
+ const { content, chatId, mode = 'edit' } = req.body;
30
33
 
31
34
  if (DEBUG) {
32
35
  console.log(
@@ -65,13 +68,20 @@ export const handleAIChatMessage =
65
68
  const runCode =
66
69
  createRunCodeFunction(submitOperation);
67
70
 
68
- // Perform AI editing
69
- const editResult = await performAIEditing({
70
- prompt: content,
71
- shareDBDoc,
72
- llmFunction,
73
- runCode,
74
- });
71
+ // Perform AI editing or chat based on mode
72
+ const editResult =
73
+ mode === 'ask'
74
+ ? await performAIChat({
75
+ prompt: content,
76
+ shareDBDoc,
77
+ llmFunction,
78
+ })
79
+ : await performAIEditing({
80
+ prompt: content,
81
+ shareDBDoc,
82
+ llmFunction,
83
+ runCode,
84
+ });
75
85
 
76
86
  // Add diff data to the AI message if there are changes
77
87
  if (
@@ -82,6 +92,7 @@ export const handleAIChatMessage =
82
92
  shareDBDoc,
83
93
  chatId,
84
94
  editResult.diffData,
95
+ (editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (only available for edit mode)
85
96
  );
86
97
  }
87
98
 
@@ -3,7 +3,9 @@ import {
3
3
  StreamingMarkdownParser,
4
4
  } from 'llm-code-format';
5
5
  import { ChatOpenAI } from '@langchain/openai';
6
+ import OpenAI from 'openai';
6
7
  import fs from 'fs';
8
+ import { generateRunId } from '@vizhub/viz-utils';
7
9
  import {
8
10
  updateAIStatus,
9
11
  createAIMessage,
@@ -13,8 +15,12 @@ import {
13
15
  clearFileContent,
14
16
  appendLineToFile,
15
17
  updateFiles,
18
+ updateAIScratchpad,
16
19
  } from './chatOperations.js';
17
20
  import { mergeFileChanges } from 'editcodewithai';
21
+ import { diff } from '../../ot.js';
22
+ import { VizChatId, VizContent } from '@vizhub/viz-types';
23
+ import { ShareDBDoc } from '../../types.js';
18
24
 
19
25
  const DEBUG = false;
20
26
 
@@ -34,28 +40,30 @@ const DEBUG = false;
34
40
  const enableStreamingEditing = false;
35
41
 
36
42
  /**
37
- * Creates and configures the LLM function for streaming
43
+ * Creates and configures the LLM function for streaming with reasoning tokens
38
44
  */
39
45
  export const createLLMFunction = ({
40
46
  shareDBDoc,
41
47
  createVizBotLocalPresence,
42
48
  chatId,
49
+ }: {
50
+ shareDBDoc: ShareDBDoc<VizContent>;
51
+ createVizBotLocalPresence: () => any;
52
+ chatId: VizChatId;
43
53
  }) => {
44
54
  return async (fullPrompt: string) => {
45
55
  const localPresence = createVizBotLocalPresence();
46
- const chatModel = new ChatOpenAI({
47
- modelName:
48
- process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
49
- 'anthropic/claude-sonnet-4',
50
- configuration: {
51
- apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
52
- baseURL: process.env.VIZHUB_EDIT_WITH_AI_BASE_URL,
53
- defaultHeaders: {
54
- 'HTTP-Referer': 'https://vizhub.com',
55
- 'X-Title': 'VizHub',
56
- },
56
+
57
+ // Create OpenRouter client for reasoning token support
58
+ const openRouterClient = new OpenAI({
59
+ apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
60
+ baseURL:
61
+ process.env.VIZHUB_EDIT_WITH_AI_BASE_URL ||
62
+ 'https://openrouter.ai/api/v1',
63
+ defaultHeaders: {
64
+ 'HTTP-Referer': 'https://vizhub.com',
65
+ 'X-Title': 'VizHub',
57
66
  },
58
- streaming: true,
59
67
  });
60
68
 
61
69
  let fullContent = '';
@@ -177,12 +185,58 @@ export const createLLMFunction = ({
177
185
  const parser = new StreamingMarkdownParser(callbacks);
178
186
 
179
187
  const chunks = [];
188
+ let reasoningContent = '';
189
+
190
+ // Stream the response with reasoning tokens
191
+ const modelName =
192
+ process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
193
+ 'anthropic/claude-3.5-sonnet';
194
+ const stream = await (
195
+ openRouterClient.chat.completions.create as any
196
+ )({
197
+ model: modelName,
198
+ messages: [{ role: 'user', content: fullPrompt }],
199
+ max_tokens: 8192,
200
+ reasoning: {
201
+ effort: 'medium',
202
+ exclude: false,
203
+ },
204
+ usage: { include: true },
205
+ stream: true,
206
+ });
207
+
208
+ let reasoningStarted = false;
209
+ let contentStarted = false;
180
210
 
181
- // Stream the response
182
- const stream = await chatModel.stream(fullPrompt);
183
211
  for await (const chunk of stream) {
184
- if (chunk && chunk.content) {
185
- const chunkContent = String(chunk.content);
212
+ const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
213
+
214
+ if (delta?.reasoning) {
215
+ // Handle reasoning tokens (thinking)
216
+ if (!reasoningStarted) {
217
+ reasoningStarted = true;
218
+ updateAIStatus(shareDBDoc, chatId, 'Thinking...');
219
+ }
220
+ reasoningContent += delta.reasoning;
221
+ updateAIScratchpad(
222
+ shareDBDoc,
223
+ chatId,
224
+ reasoningContent,
225
+ );
226
+ } else if (delta?.content) {
227
+ // Handle regular content tokens
228
+ if (reasoningStarted && !contentStarted) {
229
+ // Clear reasoning when content starts
230
+ contentStarted = true;
231
+ updateAIScratchpad(shareDBDoc, chatId, '');
232
+ updateAIStatus(
233
+ shareDBDoc,
234
+ chatId,
235
+ 'Generating response...',
236
+ );
237
+ }
238
+
239
+ const chunkContent = delta.content;
186
240
  chunks.push(chunkContent);
187
241
 
188
242
  if (enableStreamingEditing) {
@@ -196,14 +250,20 @@ export const createLLMFunction = ({
196
250
  fullContent,
197
251
  );
198
252
  }
253
+ } else if (chunk.usage) {
254
+ // Handle usage information
255
+ DEBUG && console.log('Usage:', chunk.usage);
199
256
  }
200
257
 
201
- if (!generationId && chunk.lc_kwargs?.id) {
202
- generationId = chunk.lc_kwargs.id;
258
+ if (!generationId && chunk.id) {
259
+ generationId = chunk.id;
203
260
  }
204
261
  }
205
262
  await parser.flushRemaining();
206
263
  reportFileEdited();
264
+
265
+ // Final cleanup - clear scratchpad and set final status
266
+ updateAIScratchpad(shareDBDoc, chatId, '');
207
267
  updateAIStatus(shareDBDoc, chatId, 'Done editing.');
208
268
  updateAIMessageContent(
209
269
  shareDBDoc,
@@ -242,6 +302,28 @@ export const createLLMFunction = ({
242
302
  );
243
303
  }
244
304
 
305
+ // Generate a new runId to trigger a run when AI finishes editing
306
+ // This will trigger a re-run without hot reloading
307
+ const newRunId = generateRunId();
308
+ const runIdOp = diff(shareDBDoc.data, {
309
+ ...shareDBDoc.data,
310
+ runId: newRunId,
311
+ });
312
+ shareDBDoc.submitOp(runIdOp, (error) => {
313
+ if (error) {
314
+ console.warn(
315
+ 'Error setting runId after AI editing:',
316
+ error,
317
+ );
318
+ } else {
319
+ DEBUG &&
320
+ console.log(
321
+ 'Set new runId after AI editing:',
322
+ newRunId,
323
+ );
324
+ }
325
+ });
326
+
245
327
  // Write chunks file for debugging
246
328
  if (DEBUG) {
247
329
  const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
@@ -150,6 +150,10 @@ export const generateAIResponse = async ({
150
150
  // model: 'gpt-3.5-turbo',
151
151
  model: 'gpt-4o',
152
152
  messages,
153
+ reasoning: {
154
+ effort: 'medium',
155
+ exclude: false,
156
+ } as any, // Type assertion for OpenRouter-specific reasoning parameter
153
157
  stream: true,
154
158
  });
155
159
 
@@ -14,6 +14,7 @@ import { computeInitialDocument } from './computeInitialDocument.js';
14
14
  import { handleAIAssist } from './handleAIAssist.js';
15
15
  import { handleAICopilot } from './handleAICopilot.js';
16
16
  import { handleAIChatMessage } from './handleAIChatMessage.js';
17
+ import { undoAIEdit } from './aiChatHandler/chatOperations.js';
17
18
  import { isDirectory } from './isDirectory.js';
18
19
  import { createToken } from './livekit.js';
19
20
  import './setupEnv.js';
@@ -141,6 +142,57 @@ app.post(
141
142
  }),
142
143
  );
143
144
 
145
+ // Handle AI Chat Undo requests.
146
+ app.post(
147
+ '/ai-chat-undo',
148
+ bodyParser.json(),
149
+ async (req, res) => {
150
+ const { chatId, messageId } = req.body;
151
+
152
+ if (!chatId || !messageId) {
153
+ return res
154
+ .status(400)
155
+ .json({ error: 'Missing chatId or messageId' });
156
+ }
157
+
158
+ try {
159
+ const chat = shareDBDoc.data.chats?.[chatId];
160
+ if (!chat) {
161
+ return res
162
+ .status(404)
163
+ .json({ error: 'Chat not found' });
164
+ }
165
+
166
+ const message = chat.messages.find(
167
+ (msg) => msg.id === messageId,
168
+ );
169
+ if (
170
+ !message ||
171
+ message.role !== 'assistant' ||
172
+ !message.beforeFiles
173
+ ) {
174
+ return res
175
+ .status(400)
176
+ .json({ error: 'Invalid message for undo' });
177
+ }
178
+
179
+ undoAIEdit(
180
+ shareDBDoc,
181
+ chatId,
182
+ messageId,
183
+ message.beforeFiles,
184
+ );
185
+
186
+ res.status(200).json({ success: true });
187
+ } catch (error) {
188
+ console.error('Error undoing AI edit:', error);
189
+ res
190
+ .status(500)
191
+ .json({ error: 'Failed to undo edit' });
192
+ }
193
+ },
194
+ );
195
+
144
196
  // Livekit Token Generator
145
197
  app.get('/livekit-token', async (req, res) => {
146
198
  const { room, username } = req.query;