vzcode 1.32.0 → 1.34.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 (44) hide show
  1. package/README.md +0 -1
  2. package/dist/assets/index-ajiBaTmx.js +263 -0
  3. package/dist/assets/index-lvLw6dCW.css +1 -0
  4. package/dist/assets/worker-BrwN8AXx.js +330 -0
  5. package/dist/assets/worker-CkHcCP1n.js +136 -0
  6. package/dist/assets/worker-DkYJN5WQ.js +453 -0
  7. package/dist/index.html +2 -2
  8. package/package.json +42 -37
  9. package/src/client/CodeEditor/getOrCreateEditor.ts +18 -5
  10. package/src/client/Icons/MicSVG.tsx +1 -1
  11. package/src/client/VZCodeContext.tsx +18 -2
  12. package/src/client/VZSidebar/AIChat/ChatInput.tsx +65 -0
  13. package/src/client/VZSidebar/AIChat/Message.tsx +36 -0
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +62 -0
  15. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +25 -0
  16. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +13 -0
  17. package/src/client/VZSidebar/AIChat/index.tsx +75 -0
  18. package/src/client/VZSidebar/AIChat/styles.scss +256 -0
  19. package/src/client/VZSidebar/index.tsx +53 -8
  20. package/src/client/VZSidebar/styles.scss +8 -1
  21. package/src/client/diff.js +1 -1
  22. package/src/client/featureFlags.ts +2 -0
  23. package/src/client/useActions.ts +20 -0
  24. package/src/client/useEditorCache.ts +1 -0
  25. package/src/client/useFileCRUD.ts +47 -0
  26. package/src/client/useOpenDirectories.ts +43 -12
  27. package/src/client/utils/fileExtension.ts +10 -0
  28. package/src/client/vzReducer/aiChatReducer.ts +31 -0
  29. package/src/client/vzReducer/createInitialState.ts +2 -0
  30. package/src/client/vzReducer/index.ts +20 -0
  31. package/src/ot.js +10 -2
  32. package/src/server/aiChatHandler/aiEditing.js +42 -0
  33. package/src/server/aiChatHandler/chatOperations.js +195 -0
  34. package/src/server/aiChatHandler/errorHandling.js +53 -0
  35. package/src/server/aiChatHandler/index.js +69 -0
  36. package/src/server/aiChatHandler/llmStreaming.js +105 -0
  37. package/src/server/aiChatHandler/validation.js +24 -0
  38. package/src/server/handleAIChatMessage.js +2 -0
  39. package/src/server/index.js +8 -0
  40. package/dist/assets/index-DTtcN2MP.css +0 -1
  41. package/dist/assets/index-sreAszZh.js +0 -240
  42. package/dist/assets/worker-BKfYWklR.js +0 -136
  43. package/dist/assets/worker-COyDRdqW.js +0 -453
  44. package/dist/assets/worker-CWsWXMyk.js +0 -335
@@ -30,6 +30,10 @@ import {
30
30
  setSearchFocusedIndexReducer,
31
31
  toggleSearchFocusedReducer,
32
32
  } from './searchReducer';
33
+ import {
34
+ setIsAIChatOpenReducer,
35
+ toggleAIChatFocusedReducer,
36
+ } from './aiChatReducer';
33
37
  import { toggleAutoFollowReducer } from './toggleAutoFollowReducer';
34
38
  import { updatePresenceIndicatorReducer } from './updatePresenceIndicatorReducer';
35
39
  import { splitCurrentPaneReducer } from './splitCurrentPaneReducer';
@@ -52,6 +56,12 @@ export type VZState = {
52
56
  // True to show the search instead of files
53
57
  isSearchOpen: boolean;
54
58
 
59
+ // True to show the AI chat instead of files
60
+ isAIChatOpen: boolean;
61
+
62
+ // True if the AI chat input should focus on the next render.
63
+ aiChatFocused: boolean;
64
+
55
65
  // True to show the settings modal.
56
66
  isSettingsOpen: boolean;
57
67
 
@@ -115,6 +125,14 @@ export type VZAction =
115
125
  // * Sets whether the search tab is open.
116
126
  | { type: 'set_is_search_open'; value: boolean }
117
127
 
128
+ // `set_is_ai_chat_open`
129
+ // * Sets whether the AI chat tab is open.
130
+ | { type: 'set_is_ai_chat_open'; value: boolean }
131
+
132
+ // `toggle_ai_chat_focused`
133
+ // * Toggles focused variable to trigger AI chat input focus
134
+ | { type: 'toggle_ai_chat_focused' }
135
+
118
136
  // `set_search`
119
137
  // * Sets the current search pattern
120
138
  | { type: 'set_search'; value: string }
@@ -192,6 +210,8 @@ const reducers = [
192
210
  setSearchFocusedIndexReducer,
193
211
  toggleSearchFocusedReducer,
194
212
  setIsSearchOpenReducer,
213
+ setIsAIChatOpenReducer,
214
+ toggleAIChatFocusedReducer,
195
215
  setIsSettingsOpenReducer,
196
216
  setIsDocOpenReducer,
197
217
  editorNoLongerWantsFocusReducer,
package/src/ot.js CHANGED
@@ -1,5 +1,13 @@
1
+ // Operational Transformation (OT) utilities.
1
2
  // This file is the central point where the OT types are imported.
2
3
  // Localized to one file so it's easy to change it in future.
3
4
  import OTJSON1Presence from 'sharedb-client-browser/dist/ot-json1-presence-umd.cjs';
4
- const { json1Presence, textUnicode } = OTJSON1Presence;
5
- export { json1Presence, textUnicode };
5
+
6
+ export const { json1Presence, textUnicode } =
7
+ OTJSON1Presence;
8
+
9
+ // The OT type used throughout the codebase.
10
+ export const otType = json1Presence.type;
11
+
12
+ // Applies an OT op to an object.
13
+ export const apply = otType.apply;
@@ -0,0 +1,42 @@
1
+ import { performAiEdit } from 'editcodewithai';
2
+ import {
3
+ clearAIScratchpadAndStatus,
4
+ updateFiles,
5
+ setIsInteracting,
6
+ } from './chatOperations.js';
7
+
8
+ /**
9
+ * Performs AI editing operations on the files
10
+ */
11
+ export const performAIEditing = async (
12
+ shareDBDoc,
13
+ chatId,
14
+ content,
15
+ files,
16
+ llmFunction,
17
+ ) => {
18
+ const editResult = await performAiEdit({
19
+ prompt: content,
20
+ files,
21
+ llmFunction,
22
+ apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
23
+ });
24
+
25
+ // Clear the scratchpad and update status
26
+ clearAIScratchpadAndStatus(
27
+ shareDBDoc,
28
+ chatId,
29
+ 'Done editing with AI.',
30
+ );
31
+
32
+ // Apply AI edits to files
33
+ updateFiles(shareDBDoc, editResult.changedFiles);
34
+
35
+ // Wait for propagation
36
+ await new Promise((resolve) => setTimeout(resolve, 100));
37
+
38
+ // Unset isInteracting
39
+ setIsInteracting(shareDBDoc, false);
40
+
41
+ return editResult;
42
+ };
@@ -0,0 +1,195 @@
1
+ import { dateToTimestamp } from '@vizhub/viz-utils';
2
+ import { diff } from '../../client/diff.js';
3
+
4
+ /**
5
+ * Ensures the chats object exists in the ShareDB document
6
+ */
7
+ export const ensureChatsExist = (shareDBDoc) => {
8
+ if (!shareDBDoc.data.chats) {
9
+ const op = diff(shareDBDoc.data, {
10
+ ...shareDBDoc.data,
11
+ chats: {},
12
+ });
13
+ shareDBDoc.submitOp(op);
14
+ }
15
+ };
16
+
17
+ /**
18
+ * Ensures a specific chat exists in the ShareDB document
19
+ */
20
+ export const ensureChatExists = (shareDBDoc, chatId) => {
21
+ if (!shareDBDoc.data.chats[chatId]) {
22
+ const op = diff(shareDBDoc.data, {
23
+ ...shareDBDoc.data,
24
+ chats: {
25
+ ...shareDBDoc.data.chats,
26
+ [chatId]: {
27
+ id: chatId,
28
+ messages: [],
29
+ createdAt: dateToTimestamp(new Date()),
30
+ updatedAt: dateToTimestamp(new Date()),
31
+ },
32
+ },
33
+ });
34
+ shareDBDoc.submitOp(op);
35
+ }
36
+ };
37
+
38
+ /**
39
+ * Adds a user message to the chat
40
+ */
41
+ export const addUserMessage = (
42
+ shareDBDoc,
43
+ chatId,
44
+ content,
45
+ ) => {
46
+ const userMessage = {
47
+ id: `user-${Date.now()}`,
48
+ role: 'user',
49
+ content: content,
50
+ timestamp: dateToTimestamp(new Date()),
51
+ };
52
+
53
+ const userMessageOp = diff(shareDBDoc.data, {
54
+ ...shareDBDoc.data,
55
+ chats: {
56
+ ...shareDBDoc.data.chats,
57
+ [chatId]: {
58
+ ...shareDBDoc.data.chats[chatId],
59
+ messages: [
60
+ ...shareDBDoc.data.chats[chatId].messages,
61
+ userMessage,
62
+ ],
63
+ updatedAt: dateToTimestamp(new Date()),
64
+ },
65
+ },
66
+ });
67
+ shareDBDoc.submitOp(userMessageOp);
68
+
69
+ return userMessage;
70
+ };
71
+
72
+ /**
73
+ * Updates AI status in the chat
74
+ */
75
+ export const updateAIStatus = (
76
+ shareDBDoc,
77
+ chatId,
78
+ status,
79
+ ) => {
80
+ const op = diff(shareDBDoc.data, {
81
+ ...shareDBDoc.data,
82
+ chats: {
83
+ ...shareDBDoc.data.chats,
84
+ [chatId]: {
85
+ ...shareDBDoc.data.chats[chatId],
86
+ aiStatus: status,
87
+ },
88
+ },
89
+ });
90
+ shareDBDoc.submitOp(op);
91
+ };
92
+
93
+ /**
94
+ * Updates AI scratchpad content
95
+ */
96
+ export const updateAIScratchpad = (
97
+ shareDBDoc,
98
+ chatId,
99
+ content,
100
+ ) => {
101
+ const op = diff(shareDBDoc.data, {
102
+ ...shareDBDoc.data,
103
+ chats: {
104
+ ...shareDBDoc.data.chats,
105
+ [chatId]: {
106
+ ...shareDBDoc.data.chats[chatId],
107
+ aiScratchpad: content,
108
+ },
109
+ },
110
+ });
111
+ shareDBDoc.submitOp(op);
112
+ };
113
+
114
+ /**
115
+ * Clears AI scratchpad and updates status
116
+ */
117
+ export const clearAIScratchpadAndStatus = (
118
+ shareDBDoc,
119
+ chatId,
120
+ status,
121
+ ) => {
122
+ const op = diff(shareDBDoc.data, {
123
+ ...shareDBDoc.data,
124
+ chats: {
125
+ ...shareDBDoc.data.chats,
126
+ [chatId]: {
127
+ ...shareDBDoc.data.chats[chatId],
128
+ aiScratchpad: undefined,
129
+ aiStatus: status,
130
+ },
131
+ },
132
+ });
133
+ shareDBDoc.submitOp(op);
134
+ };
135
+
136
+ /**
137
+ * Adds an AI response message to the chat
138
+ */
139
+ export const addAIMessage = (
140
+ shareDBDoc,
141
+ chatId,
142
+ content,
143
+ ) => {
144
+ const aiResponse = {
145
+ id: Date.now() + 1,
146
+ role: 'assistant',
147
+ content: content || 'AI edit completed successfully.',
148
+ timestamp: dateToTimestamp(new Date()),
149
+ };
150
+
151
+ const messageOp = diff(shareDBDoc.data, {
152
+ ...shareDBDoc.data,
153
+ chats: {
154
+ ...shareDBDoc.data.chats,
155
+ [chatId]: {
156
+ ...shareDBDoc.data.chats[chatId],
157
+ messages: [
158
+ ...shareDBDoc.data.chats[chatId].messages,
159
+ aiResponse,
160
+ ],
161
+ updatedAt: dateToTimestamp(new Date()),
162
+ aiStatus: undefined,
163
+ },
164
+ },
165
+ });
166
+ shareDBDoc.submitOp(messageOp);
167
+
168
+ return aiResponse;
169
+ };
170
+
171
+ /**
172
+ * Updates files in the ShareDB document
173
+ */
174
+ export const updateFiles = (shareDBDoc, files) => {
175
+ const filesOp = diff(shareDBDoc.data, {
176
+ ...shareDBDoc.data,
177
+ files: files,
178
+ isInteracting: true,
179
+ });
180
+ shareDBDoc.submitOp(filesOp);
181
+ };
182
+
183
+ /**
184
+ * Sets isInteracting flag
185
+ */
186
+ export const setIsInteracting = (
187
+ shareDBDoc,
188
+ isInteracting,
189
+ ) => {
190
+ const interactingOp = diff(
191
+ { isInteracting: !isInteracting },
192
+ { isInteracting: isInteracting },
193
+ );
194
+ shareDBDoc.submitOp(interactingOp);
195
+ };
@@ -0,0 +1,53 @@
1
+ import { dateToTimestamp } from '@vizhub/viz-utils';
2
+ import { diff } from '../../client/diff.js';
3
+
4
+ /**
5
+ * Handles errors by adding an error message to the chat and clearing AI state
6
+ */
7
+ export const handleError = (
8
+ shareDBDoc,
9
+ chatId,
10
+ error,
11
+ res,
12
+ ) => {
13
+ console.error('[handleAIChatMessage] error:', error);
14
+
15
+ // Clear scratchpad and add error message on error
16
+ try {
17
+ const errorResponse = {
18
+ id: `error-${Date.now()}`,
19
+ role: 'assistant',
20
+ content:
21
+ 'Sorry, I encountered an error while processing your message. Please try again.',
22
+ timestamp: dateToTimestamp(new Date()),
23
+ };
24
+
25
+ const errorOp = diff(shareDBDoc.data, {
26
+ ...shareDBDoc.data,
27
+ chats: {
28
+ ...shareDBDoc.data.chats,
29
+ [chatId]: {
30
+ ...shareDBDoc.data.chats[chatId],
31
+ messages: [
32
+ ...shareDBDoc.data.chats[chatId].messages,
33
+ errorResponse,
34
+ ],
35
+ aiScratchpad: undefined,
36
+ aiStatus: undefined,
37
+ updatedAt: dateToTimestamp(new Date()),
38
+ },
39
+ },
40
+ });
41
+ shareDBDoc.submitOp(errorOp);
42
+ } catch (opError) {
43
+ console.error(
44
+ '[handleAIChatMessage] error handling error state:',
45
+ opError,
46
+ );
47
+ }
48
+
49
+ res.status(500).json({
50
+ error: 'Internal server error',
51
+ message: error.message,
52
+ });
53
+ };
@@ -0,0 +1,69 @@
1
+ import { validateRequest } from './validation.js';
2
+ import {
3
+ ensureChatsExist,
4
+ ensureChatExists,
5
+ addUserMessage,
6
+ addAIMessage,
7
+ } from './chatOperations.js';
8
+ import { createLLMFunction } from './llmStreaming.js';
9
+ import { performAIEditing } from './aiEditing.js';
10
+ import { handleError } from './errorHandling.js';
11
+
12
+ const debug = false;
13
+
14
+ export const handleAIChatMessage =
15
+ (shareDBDoc) => async (req, res) => {
16
+ const { content, chatId } = req.body;
17
+
18
+ if (debug) {
19
+ console.log(
20
+ '[handleAIChatMessage] content:',
21
+ content,
22
+ 'chatId:',
23
+ chatId,
24
+ );
25
+ }
26
+
27
+ // Validate request
28
+ if (!validateRequest(req, res)) {
29
+ return;
30
+ }
31
+
32
+ try {
33
+ // Get existing files from ShareDB doc
34
+ const files = shareDBDoc.data.files;
35
+
36
+ // Ensure chats structure exists
37
+ ensureChatsExist(shareDBDoc);
38
+ ensureChatExists(shareDBDoc, chatId);
39
+
40
+ // Add user message to chat
41
+ addUserMessage(shareDBDoc, chatId, content);
42
+
43
+ // Create LLM function for streaming
44
+ const llmFunction = createLLMFunction(
45
+ shareDBDoc,
46
+ chatId,
47
+ );
48
+
49
+ // Perform AI editing
50
+ const editResult = await performAIEditing(
51
+ shareDBDoc,
52
+ chatId,
53
+ content,
54
+ files,
55
+ llmFunction,
56
+ );
57
+
58
+ // Add AI response message
59
+ const aiResponse = addAIMessage(
60
+ shareDBDoc,
61
+ chatId,
62
+ editResult.content,
63
+ );
64
+
65
+ res.status(200).json(aiResponse);
66
+ } catch (error) {
67
+ handleError(shareDBDoc, chatId, error, res);
68
+ }
69
+ };
@@ -0,0 +1,105 @@
1
+ import { StreamingMarkdownParser } from 'llm-code-format';
2
+ import { ChatOpenAI } from '@langchain/openai';
3
+ import {
4
+ updateAIStatus,
5
+ updateAIScratchpad,
6
+ } from './chatOperations.js';
7
+
8
+ const debug = false;
9
+
10
+ /**
11
+ * Creates and configures the LLM function for streaming
12
+ */
13
+ export const createLLMFunction = (shareDBDoc, chatId) => {
14
+ return async (fullPrompt) => {
15
+ const chatModel = new ChatOpenAI({
16
+ modelName:
17
+ process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
18
+ 'gpt-4o-mini',
19
+ configuration: {
20
+ apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
21
+ baseURL: process.env.VIZHUB_EDIT_WITH_AI_BASE_URL,
22
+ defaultHeaders: {
23
+ 'HTTP-Referer': 'https://vizhub.com',
24
+ 'X-Title': 'VizHub',
25
+ },
26
+ },
27
+ streaming: true,
28
+ });
29
+
30
+ let fullContent = '';
31
+ let generationId = '';
32
+
33
+ // Track expected state to avoid race conditions
34
+ let expectedAiScratchpad =
35
+ shareDBDoc.data.chats[chatId]?.aiScratchpad || '';
36
+
37
+ // Throttle updates to avoid "op too long" errors
38
+ let lastUpdateTime = 0;
39
+ const UPDATE_THROTTLE_MS = 100;
40
+
41
+ // Define callbacks for streaming parser
42
+ const callbacks = {
43
+ onFileNameChange: (fileName, format) => {
44
+ debug &&
45
+ console.log(
46
+ `File changed to: ${fileName} (${format})`,
47
+ );
48
+ updateAIStatus(
49
+ shareDBDoc,
50
+ chatId,
51
+ 'Editing ' + fileName,
52
+ );
53
+ },
54
+ onCodeLine: (line) => {
55
+ debug && console.log(`Code line: ${line}`);
56
+ },
57
+ onNonCodeLine: (line) => {
58
+ debug && console.log(`Comment/text: ${line}`);
59
+ },
60
+ };
61
+
62
+ const parser = new StreamingMarkdownParser(callbacks);
63
+
64
+ // Stream the response
65
+ const stream = await chatModel.stream(fullPrompt);
66
+ for await (const chunk of stream) {
67
+ if (chunk.content) {
68
+ const chunkContent = String(chunk.content);
69
+ fullContent += chunkContent;
70
+
71
+ // Throttle updates
72
+ const now = Date.now();
73
+ const shouldUpdate =
74
+ now - lastUpdateTime >= UPDATE_THROTTLE_MS;
75
+
76
+ if (shouldUpdate) {
77
+ updateAIScratchpad(
78
+ shareDBDoc,
79
+ chatId,
80
+ fullContent,
81
+ );
82
+ expectedAiScratchpad = fullContent;
83
+ lastUpdateTime = now;
84
+ }
85
+
86
+ parser.processChunk(chunkContent);
87
+ }
88
+
89
+ if (!generationId && chunk.lc_kwargs?.id) {
90
+ generationId = chunk.lc_kwargs.id;
91
+ }
92
+ }
93
+ parser.flushRemaining();
94
+
95
+ // Submit final update if there's pending content
96
+ if (expectedAiScratchpad !== fullContent) {
97
+ updateAIScratchpad(shareDBDoc, chatId, fullContent);
98
+ }
99
+
100
+ return {
101
+ content: fullContent,
102
+ generationId: generationId,
103
+ };
104
+ };
105
+ };
@@ -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,2 @@
1
+ // Re-export the refactored handleAIChatMessage from the new modular structure
2
+ export { handleAIChatMessage } from './aiChatHandler/index.js';
@@ -14,6 +14,7 @@ import { json1Presence } from '../ot.js';
14
14
  import { computeInitialDocument } from './computeInitialDocument.js';
15
15
  import { handleAIAssist } from './handleAIAssist.js';
16
16
  import { handleAICopilot } from './handleAICopilot.js';
17
+ import { handleAIChatMessage } from './handleAIChatMessage.js';
17
18
  import { isDirectory } from './isDirectory.js';
18
19
  import { createToken } from './livekit.js';
19
20
  import './setupEnv.js';
@@ -112,6 +113,13 @@ app.post(
112
113
  handleAICopilot(),
113
114
  );
114
115
 
116
+ // Handle AI Chat Message requests.
117
+ app.post(
118
+ '/ai-chat-message',
119
+ bodyParser.json(),
120
+ handleAIChatMessage(shareDBDoc),
121
+ );
122
+
115
123
  // Livekit Token Generator
116
124
  app.get('/livekit-token', async (req, res) => {
117
125
  const { room, username } = req.query;