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,51 @@
1
+ /**
2
+ * @vizhub/llm-streaming-server
3
+ *
4
+ * Server-side library for streaming LLM responses with ShareDB integration.
5
+ * This module provides functionality for:
6
+ * - Creating and managing LLM streaming functions
7
+ * - Performing AI-assisted code editing
8
+ * - Managing chat operations with ShareDB
9
+ * - Validating requests and handling errors
10
+ */
11
+
12
+ // Core LLM streaming functionality
13
+ export { createLLMFunction } from './llmStreaming.js';
14
+
15
+ // AI editing operations
16
+ export {
17
+ performAIChat,
18
+ performAIEditing,
19
+ } from './aiEditing.js';
20
+
21
+ // ShareDB chat operations
22
+ export {
23
+ ensureChatsExist,
24
+ ensureChatExists,
25
+ addUserMessage,
26
+ updateAIStatus,
27
+ updateAIScratchpad,
28
+ clearAIScratchpadAndStatus,
29
+ createAIMessage,
30
+ updateAIMessageContent,
31
+ setAIStatus,
32
+ finalizeAIMessage,
33
+ addDiffToAIMessage,
34
+ addAIMessage,
35
+ updateFiles,
36
+ resolveFileId,
37
+ createNewFile,
38
+ createStreamingAIMessage,
39
+ addStreamingEvent,
40
+ updateStreamingStatus,
41
+ finalizeStreamingMessage,
42
+ } from './chatOperations.js';
43
+
44
+ // Request validation
45
+ export { validateRequest } from './validation.js';
46
+
47
+ // Error handling
48
+ export {
49
+ handleError,
50
+ handleBackgroundError,
51
+ } from './errorHandling.js';
@@ -0,0 +1,403 @@
1
+ import OpenAI from 'openai';
2
+ import {
3
+ parseMarkdownFiles,
4
+ StreamingMarkdownParser,
5
+ } from 'llm-code-format';
6
+ import { mergeFileChanges } from 'editcodewithai';
7
+ import {
8
+ FileCollection,
9
+ VizChatId,
10
+ VizFiles,
11
+ } from '@vizhub/viz-types';
12
+ import {
13
+ updateFiles,
14
+ updateAIScratchpad,
15
+ createStreamingAIMessage,
16
+ addStreamingEvent,
17
+ updateStreamingStatus,
18
+ finalizeStreamingMessage,
19
+ } from './chatOperations.js';
20
+ import {
21
+ ShareDBDoc,
22
+ ExtendedVizContent,
23
+ } from '../types.js';
24
+ import { formatFiles } from '../server/prettier.js';
25
+
26
+ // Verbose logs
27
+ const DEBUG = false;
28
+
29
+ // Useful for testing/debugging the streaming behavior
30
+ const slowMode = false;
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
+
38
+ /**
39
+ * Creates and configures the LLM function for streaming with reasoning tokens
40
+ */
41
+ export const createLLMFunction = ({
42
+ shareDBDoc,
43
+ chatId,
44
+ // Feature flag to enable/disable reasoning tokens.
45
+ // When false, reasoning tokens are not requested from the API
46
+ // and reasoning content is not processed in the streaming response.
47
+ enableReasoningTokens = false,
48
+ model,
49
+ aiRequestOptions,
50
+ }: {
51
+ shareDBDoc: ShareDBDoc<ExtendedVizContent>;
52
+ chatId: VizChatId;
53
+ enableReasoningTokens?: boolean;
54
+ model?: string;
55
+ aiRequestOptions?: any;
56
+ }) => {
57
+ return async (fullPrompt: string) => {
58
+ // Create OpenRouter client for reasoning token support
59
+ const openRouterClient = new OpenAI({
60
+ apiKey: process.env.VZCODE_EDIT_WITH_AI_API_KEY,
61
+ baseURL:
62
+ process.env.VZCODE_EDIT_WITH_AI_BASE_URL ||
63
+ 'https://openrouter.ai/api/v1',
64
+ defaultHeaders: {
65
+ 'HTTP-Referer': 'https://vizhub.com',
66
+ 'X-Title': 'VizHub',
67
+ },
68
+ });
69
+
70
+ let fullContent = '';
71
+ let generationId = '';
72
+ let currentEditingFileName = null;
73
+ let accumulatedTextChunk = '';
74
+ let currentFileContent = '';
75
+
76
+ // Create streaming AI message
77
+ createStreamingAIMessage(shareDBDoc, chatId);
78
+
79
+ // Set initial content generation status
80
+ updateStreamingStatus(
81
+ shareDBDoc,
82
+ chatId,
83
+ 'Formulating a plan...',
84
+ );
85
+
86
+ // Helper to get original file content
87
+ const getOriginalFileContent = (
88
+ fileName: string,
89
+ ): string => {
90
+ const files = shareDBDoc.data.files;
91
+ for (const file of Object.values(files)) {
92
+ if ((file as any).name === fileName) {
93
+ return (file as any).text || '';
94
+ }
95
+ }
96
+ return '';
97
+ };
98
+
99
+ // Helper to emit text chunk when accumulated
100
+ const emitTextChunk = async () => {
101
+ if (accumulatedTextChunk.trim()) {
102
+ DEBUG &&
103
+ console.log(
104
+ 'LLMStreaming: Emitting text chunk:',
105
+ accumulatedTextChunk.substring(0, 100) + '...',
106
+ );
107
+ await addStreamingEvent(shareDBDoc, chatId, {
108
+ type: 'text_chunk',
109
+ content: accumulatedTextChunk,
110
+ timestamp: Date.now(),
111
+ });
112
+ accumulatedTextChunk = '';
113
+ }
114
+ };
115
+
116
+ // Helper to complete file editing
117
+ const completeFileEditing = async (
118
+ fileName: string,
119
+ ) => {
120
+ if (fileName) {
121
+ DEBUG &&
122
+ console.log(
123
+ `LLMStreaming: Completing file editing for ${fileName}`,
124
+ );
125
+ await addStreamingEvent(shareDBDoc, chatId, {
126
+ type: 'file_complete',
127
+ fileName,
128
+ beforeContent: getOriginalFileContent(fileName),
129
+ afterContent: currentFileContent,
130
+ timestamp: Date.now(),
131
+ });
132
+ currentFileContent = '';
133
+ }
134
+ };
135
+
136
+ // Define callbacks for streaming parser
137
+ const callbacks = {
138
+ onFileNameChange: async (
139
+ fileName: string,
140
+ format: string,
141
+ ) => {
142
+ DEBUG &&
143
+ console.log(
144
+ `LLMStreaming: File changed to: ${fileName} (${format})`,
145
+ );
146
+
147
+ // Emit any accumulated text chunk first
148
+ await emitTextChunk();
149
+
150
+ // Complete previous file if any
151
+ if (currentEditingFileName) {
152
+ await completeFileEditing(currentEditingFileName);
153
+ }
154
+
155
+ // Start new file
156
+ currentEditingFileName = fileName;
157
+ currentFileContent = '';
158
+
159
+ // Emit file start event
160
+ await addStreamingEvent(shareDBDoc, chatId, {
161
+ type: 'file_start',
162
+ fileName,
163
+ timestamp: Date.now(),
164
+ });
165
+
166
+ // Update status
167
+ updateStreamingStatus(
168
+ shareDBDoc,
169
+ chatId,
170
+ `Editing ${fileName}...`,
171
+ );
172
+ },
173
+ onCodeLine: async (line: string) => {
174
+ DEBUG && console.log(`Code line: ${line}`);
175
+ // Accumulate code content for the current file
176
+ currentFileContent += line + '\n';
177
+ },
178
+ onNonCodeLine: async (line: string) => {
179
+ DEBUG && console.log(`Non-code line: ${line}`);
180
+ // Accumulate non-code content as text chunk
181
+ if (line.trim() !== '') {
182
+ accumulatedTextChunk += line + '\n';
183
+
184
+ // Update status for subsequent non-code chunks
185
+ if (firstNonCodeChunkProcessed) {
186
+ updateStreamingStatus(
187
+ shareDBDoc,
188
+ chatId,
189
+ 'Describing changes...',
190
+ );
191
+ } else {
192
+ firstNonCodeChunkProcessed = true;
193
+ }
194
+ }
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
+ },
228
+ };
229
+
230
+ const parser = new StreamingMarkdownParser(callbacks);
231
+
232
+ const chunks = [];
233
+ let reasoningContent = '';
234
+
235
+ // Stream the response with reasoning tokens
236
+ const modelName =
237
+ model ||
238
+ process.env.VZCODE_EDIT_WITH_AI_MODEL_NAME ||
239
+ 'anthropic/claude-3.5-sonnet';
240
+
241
+ // Configure reasoning tokens based on enableReasoningTokens flag
242
+ const requestConfig: any = {
243
+ model: modelName,
244
+ messages: [{ role: 'user', content: fullPrompt }],
245
+ usage: { include: true },
246
+ stream: true,
247
+ ...aiRequestOptions,
248
+ };
249
+
250
+ // Only include reasoning configuration if reasoning tokens are enabled
251
+ if (enableReasoningTokens) {
252
+ requestConfig.reasoning = {
253
+ effort: 'low',
254
+ exclude: false,
255
+ };
256
+ }
257
+
258
+ const stream = await (
259
+ openRouterClient.chat.completions.create as any
260
+ )(requestConfig);
261
+
262
+ let reasoningStarted = false;
263
+ let contentStarted = false;
264
+ let firstNonCodeChunkProcessed = false;
265
+
266
+ for await (const chunk of stream) {
267
+ if (slowMode) {
268
+ await new Promise((resolve) =>
269
+ setTimeout(resolve, 500),
270
+ );
271
+ }
272
+ const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
273
+
274
+ if (delta?.reasoning && enableReasoningTokens) {
275
+ // Handle reasoning tokens (thinking) - only if enabled
276
+ if (!reasoningStarted) {
277
+ reasoningStarted = true;
278
+ updateStreamingStatus(
279
+ shareDBDoc,
280
+ chatId,
281
+ 'Thinking...',
282
+ );
283
+ }
284
+ reasoningContent += delta.reasoning;
285
+ updateAIScratchpad(
286
+ shareDBDoc,
287
+ chatId,
288
+ reasoningContent,
289
+ );
290
+ } else if (delta?.content) {
291
+ // Handle regular content tokens
292
+ if (!contentStarted) {
293
+ contentStarted = true;
294
+ if (reasoningStarted) {
295
+ // Clear reasoning when content starts
296
+ updateAIScratchpad(shareDBDoc, chatId, '');
297
+ }
298
+ // // Set initial content generation status
299
+ // updateStreamingStatus(
300
+ // shareDBDoc,
301
+ // chatId,
302
+ // 'Formulating a plan...',
303
+ // );
304
+ }
305
+
306
+ const chunkContent = delta.content;
307
+ chunks.push(chunkContent);
308
+
309
+ await parser.processChunk(chunkContent);
310
+ fullContent += chunkContent;
311
+ } else if (chunk.usage) {
312
+ // Handle usage information
313
+ DEBUG && console.log('Usage:', chunk.usage);
314
+ }
315
+
316
+ if (!generationId && chunk.id) {
317
+ generationId = chunk.id;
318
+ }
319
+ }
320
+ await parser.flushRemaining();
321
+
322
+ // Emit any remaining text chunk
323
+ await emitTextChunk();
324
+
325
+ // Complete final file if any
326
+ if (currentEditingFileName) {
327
+ await completeFileEditing(currentEditingFileName);
328
+ }
329
+
330
+ // // Capture the current state of files before applying changes
331
+ // const beforeFiles = createFilesSnapshot(
332
+ // shareDBDoc.data.files,
333
+ // );
334
+
335
+ // Parse the full content to extract file changes
336
+ // export type FileCollection = Record<string, string>;
337
+ const newFilesUnformatted: FileCollection =
338
+ parseMarkdownFiles(fullContent, 'bold').files;
339
+
340
+ // Run Prettier on `newFiles` before applying them,
341
+ // preserving empty files as empty
342
+ // since that is the cue to delete a file.
343
+ const newFilesFormatted = await formatFiles(
344
+ newFilesUnformatted,
345
+ );
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
+
355
+ // Apply all the edits at once
356
+ const vizFilesAfter: VizFiles = mergeFileChanges(
357
+ shareDBDoc.data.files,
358
+ newFilesFormatted,
359
+ );
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
+ }
394
+
395
+ // Finalize streaming message
396
+ finalizeStreamingMessage(shareDBDoc, chatId);
397
+
398
+ return {
399
+ content: fullContent,
400
+ generationId: generationId,
401
+ };
402
+ };
403
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Validates incoming request data for AI chat messages
3
+ */
4
+ export const validateRequest = (req, res) => {
5
+ const { content, chatId } = req.body;
6
+
7
+ if (!content || typeof content !== 'string') {
8
+ res.status(400).json({
9
+ error:
10
+ 'Invalid request: content is required and must be a string',
11
+ });
12
+ return false;
13
+ }
14
+
15
+ if (!chatId || typeof chatId !== 'string') {
16
+ res.status(400).json({
17
+ error:
18
+ 'Invalid request: chatId is required and must be a string',
19
+ });
20
+ return false;
21
+ }
22
+
23
+ return true;
24
+ };
@@ -0,0 +1,103 @@
1
+ # LLM Streaming UI
2
+
3
+ Client-side React UI components for displaying streaming AI edits and chat.
4
+
5
+ ## Overview
6
+
7
+ This module is being extracted from VZCode's AI chat interface to become a standalone, reusable React component library. It provides client-side UI functionality for:
8
+
9
+ - **AI Chat Interface**: Main chat container component
10
+ - **Message Display**: Rendering user and AI messages with streaming support
11
+ - **Diff Visualization**: Showing code changes with syntax highlighting
12
+ - **Status Indicators**: Displaying AI thinking/reasoning and file editing status
13
+ - **Voice Input**: Speech recognition for voice-based chat input
14
+
15
+ ## Current Status
16
+
17
+ ⚠️ **Work in Progress**: This is Phase 1 of the extraction plan. The files have been copied to this directory and imports have been updated, but the components are not yet decoupled from VZCode.
18
+
19
+ ## Components
20
+
21
+ ### Main Components
22
+
23
+ - `index.tsx` (AIChat) - Main chat container with message management
24
+ - `MessageList.tsx` - Scrollable message list with auto-scroll
25
+ - `Message.tsx` - Individual message rendering (user/assistant)
26
+ - `ChatInput.tsx` - Chat input field with voice support
27
+
28
+ ### Diff/Code Display
29
+
30
+ - `DiffView.tsx` - Unified diff viewer for code changes
31
+ - `IndividualFileDiff.tsx` - Per-file diff display
32
+ - `FileEditingIndicator.tsx` - Status indicator for file editing
33
+
34
+ ### Status/Loading
35
+
36
+ - `TypingIndicator.tsx` - Animated typing/loading indicator
37
+ - `ThinkingScratchpad.tsx` - AI reasoning/thinking display
38
+ - `JumpToLatestButton.tsx` - Button to scroll to latest message
39
+
40
+ ### Hooks
41
+
42
+ - `useSpeechRecognition.ts` - Voice input functionality
43
+
44
+ ### Styles
45
+
46
+ - `styles.scss` - Main component styles
47
+ - `DiffView.scss` - Diff viewer styles
48
+
49
+ ## Dependencies
50
+
51
+ Currently depends on:
52
+
53
+ - React and React hooks
54
+ - VZCode context: `../../client/VZCodeContext`
55
+ - VZCode types: `../../types.js`
56
+ - VZCode utilities: `../../client/hooks/*`, `../../utils/*`
57
+ - Bootstrap components
58
+ - diff2html for diff rendering
59
+ - react-markdown for message rendering
60
+
61
+ ## Future Plans
62
+
63
+ This module will eventually be:
64
+
65
+ 1. Decoupled from VZCode-specific context and utilities
66
+ 2. Published as `@vizhub/llm-streaming-ui` on npm
67
+ 3. Made themeable and customizable
68
+ 4. Provided with Storybook documentation
69
+ 5. Fully typed with exported TypeScript interfaces
70
+
71
+ See the [main issue](https://github.com/vizhub-core/vzcode/issues/XXX) for the full extraction plan.
72
+
73
+ ## Usage (Current)
74
+
75
+ Currently, this module is imported by `src/client/VZSidebar/index.tsx`:
76
+
77
+ ```typescript
78
+ import { AIChat } from '../../llm-streaming-ui/components/index.js';
79
+ ```
80
+
81
+ The AIChat component expects VZCode context to be available and receives props like:
82
+
83
+ - Chat state from VZCodeContext
84
+ - Message sending handlers
85
+ - Auto-scroll configuration
86
+
87
+ ## Contributing
88
+
89
+ As this module is being extracted, please:
90
+
91
+ - Keep changes minimal and focused
92
+ - Maintain backward compatibility with VZCode
93
+ - Update component tests when modifying functionality
94
+ - Consider future consumers when adding new dependencies
95
+ - Document component props and behavior
96
+
97
+ ## Theming
98
+
99
+ The components currently use VZCode's theming system via SCSS variables. Future versions will provide:
100
+
101
+ - CSS custom properties for theming
102
+ - Light/dark theme presets
103
+ - Customizable component styling