vzcode 1.44.0 → 1.46.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 (42) hide show
  1. package/dist/assets/{buildWorker-CRLuRnCr.js → buildWorker-C_tGhWXG.js} +135 -74
  2. package/dist/assets/index-CC0CUUDi.js +457 -0
  3. package/dist/assets/{index-Beqaj63x.css → index-CQyHWTWE.css} +1 -1
  4. package/dist/assets/worker-CekYspLa.js +453 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +13 -7
  7. package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
  8. package/src/client/CodeEditor/index.tsx +11 -4
  9. package/src/client/Icons/SparklesSVG.tsx +9 -3
  10. package/src/client/RunCodeWidget/index.tsx +18 -15
  11. package/src/client/SplitPaneResizeContext.tsx +26 -108
  12. package/src/client/VZCodeContext.tsx +15 -1
  13. package/src/client/VZRight.tsx +32 -4
  14. package/src/client/VZSidebar/AIChat/ChatInput.tsx +80 -10
  15. package/src/client/VZSidebar/AIChat/DiffView.scss +154 -0
  16. package/src/client/VZSidebar/AIChat/DiffView.tsx +133 -0
  17. package/src/client/VZSidebar/AIChat/Message.tsx +24 -0
  18. package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -12
  19. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
  20. package/src/client/VZSidebar/AIChat/index.tsx +155 -17
  21. package/src/client/VZSidebar/AIChat/styles.scss +223 -1
  22. package/src/client/VZSidebar/aiCopyPaste.ts +15 -2
  23. package/src/client/VZSidebar/index.tsx +10 -2
  24. package/src/client/bootstrap.ts +11 -1
  25. package/src/client/featureFlags.ts +2 -0
  26. package/src/client/useActions.ts +12 -0
  27. package/src/client/usePrettier/index.ts +1 -3
  28. package/src/client/vzReducer/aiChatReducer.ts +13 -0
  29. package/src/client/vzReducer/createInitialState.ts +1 -0
  30. package/src/client/vzReducer/index.ts +9 -0
  31. package/src/client/vzReducer/searchReducer.test.ts +44 -2
  32. package/src/runCode.ts +5 -17
  33. package/src/server/aiChatHandler/aiEditing.ts +56 -0
  34. package/src/server/aiChatHandler/chatOperations.ts +78 -1
  35. package/src/server/aiChatHandler/index.ts +33 -9
  36. package/src/server/aiChatHandler/llmStreaming.ts +101 -19
  37. package/src/server/computeInitialDocument.ts +25 -6
  38. package/src/server/generateAIResponse.ts +4 -0
  39. package/src/server/index.ts +83 -2
  40. package/src/utils/fileDiff.ts +114 -0
  41. package/dist/assets/index-C46Hp5ym.js +0 -340
  42. package/dist/assets/worker-DkYJN5WQ.js +0 -453
@@ -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`;
@@ -10,6 +10,15 @@ import createIgnore from 'ignore';
10
10
  import { ignoreFilePattern, baseIgnore } from './config.js';
11
11
  import { isDirectory } from './isDirectory.js';
12
12
 
13
+ // Import the image file utility
14
+ const isImageFile = (fileName) => {
15
+ return (
16
+ fileName.match(
17
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
18
+ ) !== null
19
+ );
20
+ };
21
+
13
22
  /**
14
23
  * @param {string} fullPath - absolut path of the workspace root
15
24
  * @param {string} currentDirectoryPath - path where the ignore file is found, relative to fullPath
@@ -180,13 +189,23 @@ export const computeInitialDocument = ({ fullPath }) => {
180
189
 
181
190
  files.forEach((file) => {
182
191
  const id = randomId();
192
+ let text = null;
193
+
194
+ if (!isDirectory(file)) {
195
+ const filePath = path.join(fullPath, file);
196
+
197
+ if (isImageFile(file)) {
198
+ // Read image files as binary and convert to base64
199
+ const buffer = fs.readFileSync(filePath);
200
+ text = buffer.toString('base64');
201
+ } else {
202
+ // Read non-image files as UTF-8 text
203
+ text = fs.readFileSync(filePath, 'utf-8');
204
+ }
205
+ }
206
+
183
207
  initialDocument.files[id] = {
184
- text: isDirectory(file)
185
- ? null
186
- : fs.readFileSync(
187
- path.join(fullPath, file),
188
- 'utf-8',
189
- ),
208
+ text,
190
209
  name: file,
191
210
  };
192
211
  });
@@ -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,10 +14,20 @@ 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';
20
21
 
22
+ // Import the image file utility
23
+ const isImageFile = (fileName) => {
24
+ return (
25
+ fileName.match(
26
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
27
+ ) !== null
28
+ );
29
+ };
30
+
21
31
  // The time in milliseconds by which auto-saving is debounced.
22
32
  const autoSaveDebounceTimeMS = 800;
23
33
 
@@ -132,6 +142,57 @@ app.post(
132
142
  }),
133
143
  );
134
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
+
135
196
  // Livekit Token Generator
136
197
  app.get('/livekit-token', async (req, res) => {
137
198
  const { room, username } = req.query;
@@ -161,7 +222,17 @@ const save = () => {
161
222
  if (previous && current) {
162
223
  // Handle changing of text content.
163
224
  if (previous.text !== current.text) {
164
- fs.writeFileSync(current.name, current.text);
225
+ if (isImageFile(current.name)) {
226
+ // Write image files as binary from base64
227
+ const buffer = Buffer.from(
228
+ current.text,
229
+ 'base64',
230
+ );
231
+ fs.writeFileSync(current.name, buffer);
232
+ } else {
233
+ // Write non-image files as text
234
+ fs.writeFileSync(current.name, current.text);
235
+ }
165
236
  }
166
237
 
167
238
  // // Handle renaming files.
@@ -256,7 +327,17 @@ const save = () => {
256
327
  if (!previous && current) {
257
328
  //File Creation
258
329
  if (!isDirectory(current.name)) {
259
- fs.writeFileSync(current.name, current.text);
330
+ if (isImageFile(current.name)) {
331
+ // Write image files as binary from base64
332
+ const buffer = Buffer.from(
333
+ current.text,
334
+ 'base64',
335
+ );
336
+ fs.writeFileSync(current.name, buffer);
337
+ } else {
338
+ // Write non-image files as text
339
+ fs.writeFileSync(current.name, current.text);
340
+ }
260
341
  } else {
261
342
  fs.mkdirSync(current.name, { recursive: true });
262
343
  }
@@ -0,0 +1,114 @@
1
+ import { createTwoFilesPatch } from 'diff';
2
+ import { VizFiles, VizFileId } from '@vizhub/viz-types';
3
+
4
+ export interface UnifiedFilesDiff {
5
+ [fileId: VizFileId]: string; // Unified diff string
6
+ }
7
+
8
+ /**
9
+ * Generate a unified diff for a single file using the diff library
10
+ */
11
+ export function generateFileUnifiedDiff(
12
+ fileId: VizFileId,
13
+ fileName: string,
14
+ beforeContent: string,
15
+ afterContent: string,
16
+ ): string {
17
+ // Only generate diff if there are actual changes
18
+ if (beforeContent === afterContent) {
19
+ return '';
20
+ }
21
+
22
+ // Use the diff library's createTwoFilesPatch function to generate unified diff
23
+ const unifiedDiff = createTwoFilesPatch(
24
+ fileName,
25
+ fileName,
26
+ beforeContent,
27
+ afterContent,
28
+ );
29
+
30
+ return unifiedDiff;
31
+ }
32
+
33
+ /**
34
+ * Generate unified diffs for multiple files
35
+ */
36
+ export function generateFilesUnifiedDiff(
37
+ beforeFiles: VizFiles,
38
+ afterFiles: VizFiles,
39
+ ): UnifiedFilesDiff {
40
+ const result: UnifiedFilesDiff = {};
41
+ const allFileIds = new Set([
42
+ ...Object.keys(beforeFiles),
43
+ ...Object.keys(afterFiles),
44
+ ]);
45
+
46
+ for (const fileId of allFileIds) {
47
+ const beforeFile = beforeFiles[fileId];
48
+ const afterFile = afterFiles[fileId];
49
+
50
+ const beforeContent = beforeFile?.text || '';
51
+ const afterContent = afterFile?.text || '';
52
+ const fileName =
53
+ afterFile?.name || beforeFile?.name || fileId;
54
+
55
+ const unifiedDiff = generateFileUnifiedDiff(
56
+ fileId,
57
+ fileName,
58
+ beforeContent,
59
+ afterContent,
60
+ );
61
+
62
+ // Only include files that have changes
63
+ if (unifiedDiff) {
64
+ result[fileId] = unifiedDiff;
65
+ }
66
+ }
67
+
68
+ return result;
69
+ }
70
+
71
+ /**
72
+ * Create a snapshot of current files for diff comparison
73
+ */
74
+ export function createFilesSnapshot(
75
+ files: VizFiles,
76
+ ): VizFiles {
77
+ return JSON.parse(JSON.stringify(files));
78
+ }
79
+
80
+ /**
81
+ * Parse unified diff to extract basic statistics
82
+ */
83
+ export function parseUnifiedDiffStats(
84
+ unifiedDiff: string,
85
+ ): {
86
+ additions: number;
87
+ deletions: number;
88
+ } {
89
+ const lines = unifiedDiff.split('\n');
90
+ let additions = 0;
91
+ let deletions = 0;
92
+
93
+ for (const line of lines) {
94
+ if (line.startsWith('+') && !line.startsWith('+++')) {
95
+ additions++;
96
+ } else if (
97
+ line.startsWith('-') &&
98
+ !line.startsWith('---')
99
+ ) {
100
+ deletions++;
101
+ }
102
+ }
103
+
104
+ return { additions, deletions };
105
+ }
106
+
107
+ /**
108
+ * Combine multiple unified diffs into a single diff string
109
+ */
110
+ export function combineUnifiedDiffs(
111
+ unifiedDiffs: UnifiedFilesDiff,
112
+ ): string {
113
+ return Object.values(unifiedDiffs).join('\n');
114
+ }