vzcode 1.33.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.
@@ -81,176 +81,6 @@
81
81
  height: 100%;
82
82
  display: flex;
83
83
  flex-direction: column;
84
-
85
- .ai-chat-container {
86
- height: 100%;
87
- display: flex;
88
- flex-direction: column;
89
- padding: 10px;
90
- }
91
-
92
- .ai-chat-messages {
93
- flex: 1;
94
- overflow-y: auto;
95
- margin-bottom: 10px;
96
- padding-right: 5px;
97
-
98
- &::-webkit-scrollbar {
99
- width: 6px;
100
- }
101
-
102
- &::-webkit-scrollbar-track {
103
- background: var(--vh-color-neutral-02);
104
- border-radius: 3px;
105
- }
106
-
107
- &::-webkit-scrollbar-thumb {
108
- background: var(--vh-color-neutral-04);
109
- border-radius: 3px;
110
- }
111
- }
112
-
113
- .ai-chat-message {
114
- margin-bottom: 15px;
115
-
116
- &.user {
117
- .ai-chat-message-content {
118
- background: var(--vh-color-primary-01);
119
- color: white;
120
- margin-left: 20px;
121
- border-radius: 12px 12px 4px 12px;
122
- }
123
-
124
- .ai-chat-message-time {
125
- text-align: right;
126
- margin-right: 5px;
127
- }
128
- }
129
-
130
- &.assistant {
131
- .ai-chat-message-content {
132
- background: var(--vh-color-neutral-02);
133
- color: var(--vh-color-neutral-04);
134
- margin-right: 20px;
135
- border-radius: 12px 12px 12px 4px;
136
- }
137
-
138
- .ai-chat-message-time {
139
- text-align: left;
140
- margin-left: 5px;
141
- }
142
- }
143
- }
144
-
145
- .ai-chat-message-content {
146
- padding: 10px 12px;
147
- font-size: 14px;
148
- line-height: 1.4;
149
- word-wrap: break-word;
150
- }
151
-
152
- .ai-chat-message-time {
153
- font-size: 11px;
154
- color: var(--vh-color-neutral-03);
155
- margin-top: 4px;
156
- }
157
-
158
- .ai-chat-typing {
159
- display: flex;
160
- gap: 4px;
161
- align-items: center;
162
-
163
- span {
164
- width: 6px;
165
- height: 6px;
166
- background: var(--vh-color-neutral-03);
167
- border-radius: 50%;
168
- animation: typing 1.4s infinite ease-in-out;
169
-
170
- &:nth-child(1) {
171
- animation-delay: -0.32s;
172
- }
173
- &:nth-child(2) {
174
- animation-delay: -0.16s;
175
- }
176
- &:nth-child(3) {
177
- animation-delay: 0s;
178
- }
179
- }
180
- }
181
-
182
- .ai-chat-input-container {
183
- border-top: 1px solid var(--vh-color-neutral-02);
184
- padding-top: 10px;
185
- }
186
-
187
- .ai-chat-input-group {
188
- display: flex;
189
- gap: 8px;
190
- margin-bottom: 0;
191
-
192
- textarea {
193
- flex: 1;
194
- background: var(--vh-color-neutral-02);
195
- border: 1px solid var(--vh-color-neutral-03);
196
- color: var(--vh-color-neutral-04);
197
- border-radius: 8px;
198
- padding: 8px 10px;
199
- font-size: 14px;
200
- font-family: var(--vzcode-font-family);
201
- resize: none;
202
- min-height: 36px;
203
-
204
- &:focus {
205
- outline: none;
206
- border-color: var(--vh-color-primary-01);
207
- box-shadow: 0 0 0 2px rgba(38, 67, 153, 0.2);
208
- }
209
-
210
- &::placeholder {
211
- color: var(--vh-color-neutral-03);
212
- }
213
-
214
- &:disabled {
215
- opacity: 0.6;
216
- cursor: not-allowed;
217
- }
218
- }
219
-
220
- .ai-chat-send-button {
221
- background: var(--vh-color-primary-01);
222
- border: none;
223
- color: white;
224
- border-radius: 8px;
225
- padding: 8px 16px;
226
- font-size: 14px;
227
- font-weight: 500;
228
- cursor: pointer;
229
- transition: background-color 0.2s ease;
230
-
231
- &:hover:not(:disabled) {
232
- background: var(--vh-color-primary-02);
233
- }
234
-
235
- &:disabled {
236
- background: var(--vh-color-neutral-03);
237
- cursor: not-allowed;
238
- }
239
- }
240
- }
241
- }
242
-
243
- @keyframes typing {
244
- 0%,
245
- 80%,
246
- 100% {
247
- transform: scale(0.8);
248
- opacity: 0.5;
249
- }
250
- 40% {
251
- transform: scale(1);
252
- opacity: 1;
253
- }
254
84
  }
255
85
 
256
86
  .sidebar-search .arrow-wrapper {
@@ -1,6 +1,6 @@
1
1
  import jsondiff from 'json0-ot-diff';
2
2
  import diffMatchPatch from 'diff-match-patch';
3
- import { json1Presence, textUnicode } from '../ot';
3
+ import { json1Presence, textUnicode } from '../ot.js';
4
4
 
5
5
  export const diff = (a, b) =>
6
6
  jsondiff(
@@ -1,4 +1,4 @@
1
1
  export const enableLiveKit =
2
2
  import.meta.env.VITE_ENABLE_LIVEKIT === 'true';
3
3
 
4
- export const enableAIChat = false;
4
+ export const enableAIChat = true;
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';