vzcode 2.18.0 → 2.22.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 (57) hide show
  1. package/README.md +359 -0
  2. package/dist/assets/index-CfDs-dAu.js +476 -0
  3. package/dist/assets/{index-BvGPtSrr.css → index-fSroLVgi.css} +1 -1
  4. package/dist/assets/{worker-y5jhqCTR.js → worker-Cm3I3tnR.js} +57 -57
  5. package/dist/assets/{worker-ClF0pBYr.js → worker-KiuLM-X4.js} +71 -71
  6. package/dist/index.html +2 -2
  7. package/dist/llm-streaming-server/aiEditing.js +58 -0
  8. package/dist/llm-streaming-server/chatOperations.js +494 -0
  9. package/dist/llm-streaming-server/errorHandling.js +49 -0
  10. package/dist/llm-streaming-server/index.js +20 -0
  11. package/dist/llm-streaming-server/llmStreaming.js +265 -0
  12. package/dist/llm-streaming-server/validation.js +19 -0
  13. package/dist/server/aiChatHandler/chatOperations.js +3 -4
  14. package/dist/server/aiChatHandler/index.js +5 -5
  15. package/dist/server/aiChatHandler/llmStreaming.js +57 -4
  16. package/dist/server/prettier.js +18 -16
  17. package/dist/utils/fileDiff.js +25 -1
  18. package/package.json +45 -45
  19. package/src/client/CodeEditor/getOrCreateEditor.tsx +272 -258
  20. package/src/client/CodeEditor/index.tsx +3 -3
  21. package/src/client/VZRight.tsx +4 -0
  22. package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -1
  23. package/src/client/VZSidebar/AIChat/DiffView.scss +37 -1
  24. package/src/client/VZSidebar/AIChat/DiffView.tsx +55 -16
  25. package/src/client/VZSidebar/AIChat/styles.scss +38 -0
  26. package/src/client/VZSidebar/FileTypeIcon.tsx +1 -0
  27. package/src/client/VZSidebar/index.tsx +1 -1
  28. package/src/client/featureFlags.ts +7 -0
  29. package/src/llm-streaming-server/README.md +71 -0
  30. package/src/llm-streaming-server/aiEditing.ts +102 -0
  31. package/src/llm-streaming-server/chatOperations.ts +655 -0
  32. package/src/llm-streaming-server/errorHandling.ts +66 -0
  33. package/src/llm-streaming-server/index.ts +51 -0
  34. package/src/llm-streaming-server/llmStreaming.ts +403 -0
  35. package/src/llm-streaming-server/validation.ts +24 -0
  36. package/src/llm-streaming-ui/README.md +103 -0
  37. package/src/llm-streaming-ui/components/ChatInput.tsx +237 -0
  38. package/src/llm-streaming-ui/components/DiffView.scss +173 -0
  39. package/src/llm-streaming-ui/components/DiffView.tsx +236 -0
  40. package/src/llm-streaming-ui/components/FileEditingIndicator.tsx +89 -0
  41. package/src/llm-streaming-ui/components/IndividualFileDiff.tsx +86 -0
  42. package/src/llm-streaming-ui/components/JumpToLatestButton.tsx +64 -0
  43. package/src/llm-streaming-ui/components/Message.tsx +145 -0
  44. package/src/llm-streaming-ui/components/MessageList.tsx +241 -0
  45. package/src/llm-streaming-ui/components/ThinkingScratchpad.tsx +42 -0
  46. package/src/llm-streaming-ui/components/TypingIndicator.tsx +19 -0
  47. package/src/llm-streaming-ui/components/index.tsx +388 -0
  48. package/src/llm-streaming-ui/components/styles.scss +831 -0
  49. package/src/llm-streaming-ui/components/useSpeechRecognition.ts +116 -0
  50. package/src/llm-streaming-ui/index.ts +34 -0
  51. package/src/server/aiChatHandler/chatOperations.ts +3 -4
  52. package/src/server/aiChatHandler/index.ts +5 -5
  53. package/src/server/aiChatHandler/llmStreaming.ts +87 -4
  54. package/src/server/prettier.ts +30 -31
  55. package/src/types.ts +5 -0
  56. package/src/utils/fileDiff.ts +36 -2
  57. package/dist/assets/index-DuDXdJWC.js +0 -477
@@ -0,0 +1,116 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useCallback,
5
+ Dispatch,
6
+ SetStateAction,
7
+ } from 'react';
8
+
9
+ export interface UseSpeechRecognitionResult {
10
+ isSpeaking: boolean;
11
+ toggleSpeechRecognition: () => void;
12
+ stopSpeaking: () => void;
13
+ }
14
+
15
+ export const useSpeechRecognition = (
16
+ onTranscriptChange: Dispatch<SetStateAction<string>>,
17
+ ): UseSpeechRecognitionResult => {
18
+ const [isSpeaking, setIsSpeaking] =
19
+ useState<boolean>(false);
20
+ const [recognition, setRecognition] =
21
+ // @ts-ignore
22
+ useState<SpeechRecognition | null>(null);
23
+ const [, setFinalTranscript] = useState<string>('');
24
+
25
+ // Speech recognition setup
26
+ useEffect(() => {
27
+ // Check if browser supports SpeechRecognition
28
+ const SpeechRecognition =
29
+ // @ts-ignore
30
+ window.SpeechRecognition ||
31
+ // @ts-ignore
32
+ window.webkitSpeechRecognition;
33
+
34
+ if (SpeechRecognition) {
35
+ const recognitionInstance = new SpeechRecognition();
36
+ recognitionInstance.continuous = true;
37
+ recognitionInstance.interimResults = true;
38
+
39
+ recognitionInstance.onresult = (event) => {
40
+ let finalText = '';
41
+ let interimText = '';
42
+
43
+ // Process all results to separate final and interim text
44
+ for (let i = 0; i < event.results.length; i++) {
45
+ const result = event.results[i];
46
+ if (result.isFinal) {
47
+ finalText += result[0].transcript;
48
+ } else {
49
+ interimText += result[0].transcript;
50
+ }
51
+ }
52
+
53
+ // Update our final transcript state
54
+ setFinalTranscript(finalText);
55
+
56
+ // Combine final and interim text and update the prompt
57
+ const fullTranscript = finalText + interimText;
58
+ onTranscriptChange(fullTranscript);
59
+ };
60
+
61
+ recognitionInstance.onerror = (event) => {
62
+ console.error(
63
+ 'Speech recognition error',
64
+ event.error,
65
+ );
66
+ setIsSpeaking(false);
67
+ };
68
+
69
+ recognitionInstance.onend = () => {
70
+ setIsSpeaking(false);
71
+ // Reset final transcript when recognition ends
72
+ setFinalTranscript('');
73
+ };
74
+
75
+ setRecognition(recognitionInstance);
76
+
77
+ return () => {
78
+ recognitionInstance.abort();
79
+ };
80
+ }
81
+ }, [onTranscriptChange]);
82
+
83
+ const toggleSpeechRecognition = useCallback(() => {
84
+ if (!recognition) {
85
+ console.error(
86
+ 'Speech recognition not supported in this browser',
87
+ );
88
+ return;
89
+ }
90
+
91
+ if (isSpeaking) {
92
+ recognition.stop();
93
+ setIsSpeaking(false);
94
+ } else {
95
+ // Reset final transcript when starting new recognition
96
+ setFinalTranscript('');
97
+ recognition.start();
98
+ setIsSpeaking(true);
99
+ }
100
+ }, [isSpeaking, recognition]);
101
+
102
+ const stopSpeaking = useCallback(() => {
103
+ if (recognition && isSpeaking) {
104
+ recognition.stop();
105
+ setIsSpeaking(false);
106
+ // Reset final transcript when manually stopping
107
+ setFinalTranscript('');
108
+ }
109
+ }, [isSpeaking, recognition]);
110
+
111
+ return {
112
+ isSpeaking,
113
+ toggleSpeechRecognition,
114
+ stopSpeaking,
115
+ };
116
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @vizhub/llm-streaming-ui
3
+ *
4
+ * Client-side React UI components for displaying streaming AI edits and chat.
5
+ * This module provides functionality for:
6
+ * - Displaying AI chat messages with streaming support
7
+ * - Showing real-time diffs and code changes
8
+ * - Rendering thinking/reasoning scratchpads
9
+ * - Managing file editing indicators
10
+ * - Voice input support
11
+ */
12
+
13
+ // Main AI Chat component
14
+ export { AIChat } from './components/index.js';
15
+
16
+ // Message display components
17
+ export { MessageList } from './components/MessageList.js';
18
+ export { Message } from './components/Message.js';
19
+
20
+ // Diff visualization components
21
+ export { DiffView } from './components/DiffView.js';
22
+ export { IndividualFileDiff } from './components/IndividualFileDiff.js';
23
+
24
+ // Status and indicator components
25
+ export { TypingIndicator } from './components/TypingIndicator.js';
26
+ export { ThinkingScratchpad } from './components/ThinkingScratchpad.js';
27
+ export { AIEditingStatusIndicator as FileEditingIndicator } from './components/FileEditingIndicator';
28
+ export { JumpToLatestButton } from './components/JumpToLatestButton.js';
29
+
30
+ // Input components
31
+ export { ChatInput } from './components/ChatInput.js';
32
+
33
+ // Custom hooks
34
+ export { useSpeechRecognition } from './components/useSpeechRecognition.js';
@@ -51,6 +51,7 @@ export const ensureChatExists = (
51
51
 
52
52
  /**
53
53
  * Adds a user message to the chat
54
+ * Clears old messages to reflect that each prompt is a self-contained code transformation
54
55
  */
55
56
  export const addUserMessage = (
56
57
  shareDBDoc: ShareDBDoc<VizContent>,
@@ -70,10 +71,7 @@ export const addUserMessage = (
70
71
  ...shareDBDoc.data.chats,
71
72
  [chatId]: {
72
73
  ...shareDBDoc.data.chats[chatId],
73
- messages: [
74
- ...shareDBDoc.data.chats[chatId].messages,
75
- userMessage,
76
- ],
74
+ messages: [userMessage], // Replace old messages with just the new user message
77
75
  updatedAt: dateToTimestamp(new Date()),
78
76
  },
79
77
  },
@@ -379,6 +377,7 @@ export const updateFiles = (
379
377
  DEBUG && console.log('updateFiles op:');
380
378
  DEBUG && console.log(JSON.stringify(filesOp, null, 2));
381
379
  shareDBDoc.submitOp(filesOp);
380
+ return filesOp;
382
381
  };
383
382
 
384
383
  /**
@@ -1,16 +1,16 @@
1
- import { validateRequest } from './validation.js';
1
+ import { validateRequest } from '../../llm-streaming-server/validation.js';
2
2
  import {
3
3
  ensureChatsExist,
4
4
  ensureChatExists,
5
5
  addUserMessage,
6
6
  setAIStatus,
7
- } from './chatOperations.js';
8
- import { createLLMFunction } from './llmStreaming.js';
9
- import { performAIEditing } from './aiEditing.js';
7
+ } from '../../llm-streaming-server/chatOperations.js';
8
+ import { createLLMFunction } from '../../llm-streaming-server/llmStreaming.js';
9
+ import { performAIEditing } from '../../llm-streaming-server/aiEditing.js';
10
10
  import {
11
11
  handleError,
12
12
  handleBackgroundError,
13
- } from './errorHandling.js';
13
+ } from '../../llm-streaming-server/errorHandling.js';
14
14
  import { createRunCodeFunction } from '../../runCode.js';
15
15
  import { ShareDBDoc } from '../../types.js';
16
16
  import { VizContent } from '@vizhub/viz-types';
@@ -7,6 +7,7 @@ import { mergeFileChanges } from 'editcodewithai';
7
7
  import {
8
8
  FileCollection,
9
9
  VizChatId,
10
+ VizFiles,
10
11
  } from '@vizhub/viz-types';
11
12
  import {
12
13
  updateFiles,
@@ -22,11 +23,18 @@ import {
22
23
  } from '../../types.js';
23
24
  import { formatFiles } from '../prettier.js';
24
25
 
26
+ // Verbose logs
25
27
  const DEBUG = false;
26
28
 
27
29
  // Useful for testing/debugging the streaming behavior
28
30
  const slowMode = false;
29
31
 
32
+ // If the `EMIT_FIXTURES` variable is true,
33
+ // then an output file in the `test/fixtures` folder
34
+ // with the before and after file states for testing purposes.
35
+ // This feeds into tests in codemirror-ot.
36
+ const EMIT_FIXTURES = false;
37
+
30
38
  /**
31
39
  * Creates and configures the LLM function for streaming with reasoning tokens
32
40
  */
@@ -109,7 +117,7 @@ export const createLLMFunction = ({
109
117
  const completeFileEditing = async (
110
118
  fileName: string,
111
119
  ) => {
112
- if (fileName && currentFileContent) {
120
+ if (fileName) {
113
121
  DEBUG &&
114
122
  console.log(
115
123
  `LLMStreaming: Completing file editing for ${fileName}`,
@@ -185,6 +193,38 @@ export const createLLMFunction = ({
185
193
  }
186
194
  }
187
195
  },
196
+ onFileDelete: async (fileName: string) => {
197
+ DEBUG &&
198
+ console.log(
199
+ `LLMStreaming: File marked for deletion: ${fileName}`,
200
+ );
201
+
202
+ // Emit any accumulated text chunk first
203
+ await emitTextChunk();
204
+
205
+ // Complete previous file if any
206
+ if (currentEditingFileName) {
207
+ await completeFileEditing(currentEditingFileName);
208
+ }
209
+
210
+ // Reset current editing state
211
+ currentEditingFileName = null;
212
+ currentFileContent = '';
213
+
214
+ // Emit file delete event
215
+ await addStreamingEvent(shareDBDoc, chatId, {
216
+ type: 'file_delete',
217
+ fileName,
218
+ timestamp: Date.now(),
219
+ });
220
+
221
+ // Update status
222
+ updateStreamingStatus(
223
+ shareDBDoc,
224
+ chatId,
225
+ `Deleting ${fileName}...`,
226
+ );
227
+ },
188
228
  };
189
229
 
190
230
  const parser = new StreamingMarkdownParser(callbacks);
@@ -297,17 +337,60 @@ export const createLLMFunction = ({
297
337
  const newFilesUnformatted: FileCollection =
298
338
  parseMarkdownFiles(fullContent, 'bold').files;
299
339
 
300
- // Run Prettier on `newFiles` before applying them
340
+ // Run Prettier on `newFiles` before applying them,
341
+ // preserving empty files as empty
342
+ // since that is the cue to delete a file.
301
343
  const newFilesFormatted = await formatFiles(
302
344
  newFilesUnformatted,
303
345
  );
304
346
 
347
+ // Capture the current state of files before applying changes
348
+ let vizFilesBefore: VizFiles;
349
+ if (EMIT_FIXTURES) {
350
+ vizFilesBefore = JSON.parse(
351
+ JSON.stringify(shareDBDoc.data.files),
352
+ );
353
+ }
354
+
305
355
  // Apply all the edits at once
306
- const mergedChanges = mergeFileChanges(
356
+ const vizFilesAfter: VizFiles = mergeFileChanges(
307
357
  shareDBDoc.data.files,
308
358
  newFilesFormatted,
309
359
  );
310
- updateFiles(shareDBDoc, mergedChanges);
360
+
361
+ const filesOp = updateFiles(shareDBDoc, vizFilesAfter);
362
+
363
+ if (EMIT_FIXTURES) {
364
+ const fs = await import('fs');
365
+ const path = await import('path');
366
+ const testCasesDir = path.resolve(
367
+ process.cwd(),
368
+ '../',
369
+ 'fixtures',
370
+ );
371
+ if (!fs.existsSync(testCasesDir)) {
372
+ fs.mkdirSync(testCasesDir, { recursive: true });
373
+ }
374
+ const timestamp = new Date()
375
+ .toISOString()
376
+ .replace(/[:.]/g, '-');
377
+ const testCasePath = path.join(
378
+ testCasesDir,
379
+ `ai-chat-${timestamp}.json`,
380
+ );
381
+ const testCaseData = {
382
+ vizFilesBefore,
383
+ vizFilesAfter,
384
+ filesOp,
385
+ };
386
+ fs.writeFileSync(
387
+ testCasePath,
388
+ JSON.stringify(testCaseData, null, 2),
389
+ );
390
+ console.log(
391
+ `AI chat test case written to ${testCasePath}`,
392
+ );
393
+ }
311
394
 
312
395
  // Finalize streaming message
313
396
  finalizeStreamingMessage(shareDBDoc, chatId);
@@ -5,10 +5,7 @@ import * as prettierPluginHtml from 'prettier/plugins/html';
5
5
  import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
6
6
  import * as prettierPluginCSS from 'prettier/plugins/postcss';
7
7
  import * as prettierPluginTypescript from 'prettier/plugins/typescript';
8
- import {
9
- VizFileId,
10
- FileCollection,
11
- } from '@vizhub/viz-types';
8
+ import { FileCollection } from '@vizhub/viz-types';
12
9
 
13
10
  // Parser mappings - matches client-side implementation
14
11
  const parsers = {
@@ -85,43 +82,45 @@ export const formatFile = async (
85
82
  }
86
83
  };
87
84
 
85
+ // Keys are file names, values are file text contents
86
+ //export type FileCollection = Record<string, string>;
87
+
88
88
  /**
89
89
  * Formats multiple files using Prettier
90
90
  * Only formats files that have supported extensions
91
91
  * Returns a map of fileId -> formatted text for successfully formatted files
92
92
  */
93
93
  export const formatFiles = async (
94
- fileCollection:
95
- | FileCollection
96
- | { [key: string]: { name: string; text: string } },
97
- ): Promise<{ [fileId: VizFileId]: string }> => {
98
- const results: { [fileId: VizFileId]: string } = {};
99
- const targetFileIds = Object.keys(fileCollection);
94
+ fileCollection: FileCollection,
95
+ ): Promise<FileCollection> => {
96
+ const results: FileCollection = {};
97
+ const targetFileNames = Object.keys(fileCollection);
100
98
 
101
99
  // Process files in parallel for better performance
102
- const formatPromises = targetFileIds.map(
103
- async (fileId) => {
104
- const fileData = fileCollection[fileId];
105
- if (!fileData) return;
106
-
107
- // Handle both FileCollection format and test format
108
- const fileName =
109
- typeof fileData === 'string'
110
- ? fileId
111
- : fileData.name;
112
- const fileText =
113
- typeof fileData === 'string'
114
- ? fileData
115
- : fileData.text;
100
+ const formatPromises = targetFileNames.map(
101
+ async (fileName: string) => {
102
+ const fileText = fileCollection[fileName];
103
+ results[fileName] = fileText;
116
104
 
117
- if (!fileText) return;
105
+ // Preserve empty files as empty,
106
+ // since this is the cue to delete a file.
107
+ if (!fileText || fileText.trim() === '') {
108
+ return;
109
+ }
118
110
 
119
- const formatted = await formatFile(
120
- fileText,
121
- fileName,
122
- );
123
- if (formatted !== null && formatted !== fileText) {
124
- results[fileId] = formatted;
111
+ try {
112
+ const formatted = await formatFile(
113
+ fileText,
114
+ fileName,
115
+ );
116
+ if (formatted !== null && formatted !== fileText) {
117
+ results[fileName] = formatted;
118
+ }
119
+ } catch (error) {
120
+ console.error(
121
+ `Error formatting ${fileName}:`,
122
+ error,
123
+ );
125
124
  }
126
125
  },
127
126
  );
package/src/types.ts CHANGED
@@ -245,6 +245,11 @@ export type StreamingEvent =
245
245
  afterContent: string;
246
246
  timestamp: number;
247
247
  }
248
+ | {
249
+ type: 'file_delete';
250
+ fileName: string;
251
+ timestamp: number;
252
+ }
248
253
  | {
249
254
  type: 'status_update';
250
255
  status: string;
@@ -2,7 +2,33 @@ import { createTwoFilesPatch } from 'diff';
2
2
  import { VizFiles, VizFileId } from '@vizhub/viz-types';
3
3
 
4
4
  export interface UnifiedFilesDiff {
5
- [fileId: VizFileId]: string; // Unified diff string
5
+ [fileId: VizFileId]: string; // Unified diff string or deletion marker
6
+ }
7
+
8
+ // Special marker to indicate a file deletion
9
+ export const FILE_DELETION_MARKER = '__FILE_DELETED__';
10
+
11
+ /**
12
+ * Check if a diff string represents a file deletion
13
+ */
14
+ export function isFileDeletion(
15
+ diffString: string,
16
+ ): boolean {
17
+ return diffString.startsWith(FILE_DELETION_MARKER);
18
+ }
19
+
20
+ /**
21
+ * Extract file name from a deletion marker
22
+ */
23
+ export function getDeletedFileName(
24
+ diffString: string,
25
+ ): string {
26
+ if (!isFileDeletion(diffString)) {
27
+ return '';
28
+ }
29
+ return diffString.substring(
30
+ FILE_DELETION_MARKER.length + 1,
31
+ );
6
32
  }
7
33
 
8
34
  /**
@@ -19,6 +45,11 @@ export function generateFileUnifiedDiff(
19
45
  return '';
20
46
  }
21
47
 
48
+ // Handle file deletion case - when file existed before but is now empty/deleted
49
+ if (beforeContent && !afterContent) {
50
+ return `${FILE_DELETION_MARKER}:${fileName}`;
51
+ }
52
+
22
53
  // Use the diff library's createTwoFilesPatch function to generate unified diff
23
54
  const unifiedDiff = createTwoFilesPatch(
24
55
  fileName,
@@ -109,9 +140,12 @@ export function parseUnifiedDiffStats(
109
140
 
110
141
  /**
111
142
  * Combine multiple unified diffs into a single diff string
143
+ * Note: File deletion markers are excluded from combination
112
144
  */
113
145
  export function combineUnifiedDiffs(
114
146
  unifiedDiffs: UnifiedFilesDiff,
115
147
  ): string {
116
- return Object.values(unifiedDiffs).join('\n');
148
+ return Object.values(unifiedDiffs)
149
+ .filter((diff) => !isFileDeletion(diff))
150
+ .join('\n');
117
151
  }