vzcode 2.3.0 → 2.4.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.
- package/package.json +3 -2
- package/src/cli.ts +208 -0
- package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
- package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
- package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
- package/src/client/AIAssist/Spinner.jsx +76 -0
- package/src/client/AIAssist/formatFile.ts +27 -0
- package/src/client/AIAssist/generateFilesContext.ts +25 -0
- package/src/client/AIAssist/index.ts +2 -0
- package/src/client/AIAssist/startAIAssist.ts +95 -0
- package/src/client/App/index.tsx +152 -0
- package/src/client/App/style.scss +82 -0
- package/src/client/App/usePending.ts +51 -0
- package/src/client/App/useShareDB.ts +161 -0
- package/src/client/CodeEditor/Copilot/index.ts +74 -0
- package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
- package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
- package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
- package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
- package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
- package/src/client/CodeEditor/basicSetup.ts +73 -0
- package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
- package/src/client/CodeEditor/index.tsx +202 -0
- package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
- package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
- package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
- package/src/client/CodeEditor/style.scss +156 -0
- package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
- package/src/client/CodeErrorOverlay/index.tsx +77 -0
- package/src/client/CodeErrorOverlay/style.scss +33 -0
- package/src/client/Fonts/fonts.ts +8 -0
- package/src/client/Icons/AdjustmentSVG.tsx +19 -0
- package/src/client/Icons/BugSVG.tsx +20 -0
- package/src/client/Icons/CloseSVG.tsx +19 -0
- package/src/client/Icons/CssSVG.tsx +16 -0
- package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
- package/src/client/Icons/EditSVG.tsx +19 -0
- package/src/client/Icons/FileSVG.tsx +19 -0
- package/src/client/Icons/FolderSVG.tsx +20 -0
- package/src/client/Icons/GearSVG.tsx +16 -0
- package/src/client/Icons/HtmlSVG.tsx +16 -0
- package/src/client/Icons/JavaScriptSVG.tsx +16 -0
- package/src/client/Icons/JsonSVG.tsx +16 -0
- package/src/client/Icons/MarkdownSVG.tsx +16 -0
- package/src/client/Icons/MicOffSVG.tsx +35 -0
- package/src/client/Icons/MicSVG.tsx +25 -0
- package/src/client/Icons/NewSVG.tsx +19 -0
- package/src/client/Icons/PinSVG.tsx +14 -0
- package/src/client/Icons/PlaySVG.tsx +16 -0
- package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
- package/src/client/Icons/ReactSVG.tsx +16 -0
- package/src/client/Icons/SearchSVG.tsx +20 -0
- package/src/client/Icons/SparklesSVG.tsx +23 -0
- package/src/client/Icons/SplitEditorSVG.tsx +34 -0
- package/src/client/Icons/StopSVG.tsx +15 -0
- package/src/client/Icons/SvelteSVG.tsx +16 -0
- package/src/client/Icons/TrashSVG.tsx +19 -0
- package/src/client/Icons/TypeScriptSVG.tsx +16 -0
- package/src/client/Icons/index.tsx +26 -0
- package/src/client/ImageViewer/index.tsx +91 -0
- package/src/client/ImageViewer/style.scss +41 -0
- package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
- package/src/client/PresenceNotifications/index.tsx +158 -0
- package/src/client/PresenceNotifications/style.scss +17 -0
- package/src/client/RunCodeWidget/index.tsx +117 -0
- package/src/client/RunCodeWidget/style.scss +24 -0
- package/src/client/SplitPaneResizeContext.tsx +159 -0
- package/src/client/TabList/Tab.tsx +73 -0
- package/src/client/TabList/index.tsx +35 -0
- package/src/client/TabList/style.scss +51 -0
- package/src/client/VZCodeContext/index.tsx +29 -0
- package/src/client/VZCodeContext/types.ts +248 -0
- package/src/client/VZCodeContext/useVZCodeState.ts +602 -0
- package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
- package/src/client/VZLeft.tsx +28 -0
- package/src/client/VZMiddle.tsx +172 -0
- package/src/client/VZResizer/index.tsx +128 -0
- package/src/client/VZResizer/styles.scss +33 -0
- package/src/client/VZRight.tsx +113 -0
- package/src/client/VZSettings.tsx +275 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
- package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
- package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
- package/src/client/VZSidebar/AIChat/index.tsx +357 -0
- package/src/client/VZSidebar/AIChat/styles.scss +646 -0
- package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
- package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
- package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
- package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
- package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
- package/src/client/VZSidebar/EmptyState.tsx +11 -0
- package/src/client/VZSidebar/FileListing.tsx +91 -0
- package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
- package/src/client/VZSidebar/Item.tsx +237 -0
- package/src/client/VZSidebar/Listing.tsx +47 -0
- package/src/client/VZSidebar/Search.tsx +527 -0
- package/src/client/VZSidebar/VisualEditor.tsx +282 -0
- package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
- package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
- package/src/client/VZSidebar/index.tsx +728 -0
- package/src/client/VZSidebar/styles.scss +612 -0
- package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
- package/src/client/bootstrap.ts +26 -0
- package/src/client/buildWorker.ts +3 -0
- package/src/client/featureFlags.ts +8 -0
- package/src/client/generateRequestId.ts +6 -0
- package/src/client/getFileTree.ts +53 -0
- package/src/client/index.ts +19 -0
- package/src/client/main.jsx +10 -0
- package/src/client/presenceColor.ts +8 -0
- package/src/client/shouldTriggerRun.ts +31 -0
- package/src/client/sortFileTree.ts +21 -0
- package/src/client/tabsSearchParameters.test.ts +79 -0
- package/src/client/tabsSearchParameters.ts +137 -0
- package/src/client/themes/index.ts +7 -0
- package/src/client/themes/themes.ts +77 -0
- package/src/client/themes/uiwImports.ts +32 -0
- package/src/client/themes/useDynamicTheme.ts +29 -0
- package/src/client/themes/vizhubTheme/colors.ts +80 -0
- package/src/client/themes/vizhubTheme/index.ts +193 -0
- package/src/client/useActions.ts +288 -0
- package/src/client/useESLint/index.ts +72 -0
- package/src/client/useESLint/worker.ts +113 -0
- package/src/client/useEditorCache.ts +35 -0
- package/src/client/useFileCRUD.ts +228 -0
- package/src/client/useKeyboardShortcuts.ts +451 -0
- package/src/client/useOpenDirectories.ts +99 -0
- package/src/client/usePresenceAutoFollow.ts +85 -0
- package/src/client/usePrettier/index.ts +178 -0
- package/src/client/usePrettier/worker.ts +109 -0
- package/src/client/useRunCode.tsx +15 -0
- package/src/client/useRuntimeError.ts +32 -0
- package/src/client/useSubmitOperation.ts +15 -0
- package/src/client/useURLSync.ts +137 -0
- package/src/client/usernameLocalStorage.ts +43 -0
- package/src/client/utils/fileExtension.ts +10 -0
- package/src/client/utils/isImageFile.ts +12 -0
- package/src/client/vzReducer/aiChatReducer.ts +44 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
- package/src/client/vzReducer/closeTabsReducer.ts +96 -0
- package/src/client/vzReducer/createInitialState.ts +38 -0
- package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
- package/src/client/vzReducer/findPane.ts +19 -0
- package/src/client/vzReducer/index.ts +250 -0
- package/src/client/vzReducer/openTabReducer.test.ts +127 -0
- package/src/client/vzReducer/openTabReducer.ts +74 -0
- package/src/client/vzReducer/searchReducer.test.ts +292 -0
- package/src/client/vzReducer/searchReducer.ts +181 -0
- package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
- package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
- package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
- package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
- package/src/client/vzReducer/setThemeReducer.ts +9 -0
- package/src/client/vzReducer/setUsernameReducer.ts +9 -0
- package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
- package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
- package/src/client/vzReducer/updatePane.ts +40 -0
- package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
- package/src/client/vzReducer/visualEditorReducer.ts +9 -0
- package/src/ot.ts +24 -0
- package/src/randomId.ts +15 -0
- package/src/reactRouterExports.ts +15 -0
- package/src/runCode.ts +27 -0
- package/src/server/aiChatHandler/aiEditing.ts +151 -0
- package/src/server/aiChatHandler/chatOperations.ts +476 -0
- package/src/server/aiChatHandler/errorHandling.ts +66 -0
- package/src/server/aiChatHandler/index.ts +199 -0
- package/src/server/aiChatHandler/llmStreaming.ts +419 -0
- package/src/server/aiChatHandler/validation.ts +24 -0
- package/src/server/computeInitialDocument.ts +220 -0
- package/src/server/config.ts +24 -0
- package/src/server/featureFlags.ts +8 -0
- package/src/server/generateAIResponse.ts +188 -0
- package/src/server/handleAIAssist.ts +53 -0
- package/src/server/handleAIChatMessage.ts +2 -0
- package/src/server/handleAICopilot.ts +97 -0
- package/src/server/index.ts +379 -0
- package/src/server/isDirectory.ts +1 -0
- package/src/server/livekit.ts +24 -0
- package/src/server/prettier.ts +115 -0
- package/src/server/setupEnv.ts +8 -0
- package/src/submitOperation.ts +25 -0
- package/src/types.ts +205 -0
- package/src/utils/fileDiff.ts +117 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { validateRequest } from './validation.js';
|
|
2
|
+
import {
|
|
3
|
+
ensureChatsExist,
|
|
4
|
+
ensureChatExists,
|
|
5
|
+
addUserMessage,
|
|
6
|
+
addDiffToAIMessage,
|
|
7
|
+
setAIStatus,
|
|
8
|
+
} from './chatOperations.js';
|
|
9
|
+
import { createLLMFunction } from './llmStreaming.js';
|
|
10
|
+
import {
|
|
11
|
+
performAIEditing,
|
|
12
|
+
performAIChat,
|
|
13
|
+
} from './aiEditing.js';
|
|
14
|
+
import {
|
|
15
|
+
handleError,
|
|
16
|
+
handleBackgroundError,
|
|
17
|
+
} from './errorHandling.js';
|
|
18
|
+
import { createRunCodeFunction } from '../../runCode.js';
|
|
19
|
+
import { ShareDBDoc } from '../../types.js';
|
|
20
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
21
|
+
import { createSubmitOperation } from '../../submitOperation.js';
|
|
22
|
+
|
|
23
|
+
const DEBUG = false;
|
|
24
|
+
|
|
25
|
+
export const handleAIChatMessage =
|
|
26
|
+
({
|
|
27
|
+
shareDBDoc,
|
|
28
|
+
createAIEditLocalPresence,
|
|
29
|
+
onCreditDeduction,
|
|
30
|
+
getCurrentCommitId,
|
|
31
|
+
model,
|
|
32
|
+
aiRequestOptions,
|
|
33
|
+
}: {
|
|
34
|
+
shareDBDoc: ShareDBDoc<VizContent>;
|
|
35
|
+
createAIEditLocalPresence: () => any;
|
|
36
|
+
onCreditDeduction?: any;
|
|
37
|
+
getCurrentCommitId?: () => string | null;
|
|
38
|
+
model?: string;
|
|
39
|
+
aiRequestOptions?: any;
|
|
40
|
+
}) =>
|
|
41
|
+
async (req: any, res: any) => {
|
|
42
|
+
const { content, chatId, mode = 'edit' } = req.body;
|
|
43
|
+
|
|
44
|
+
if (DEBUG) {
|
|
45
|
+
console.log(
|
|
46
|
+
'[handleAIChatMessage] content:',
|
|
47
|
+
content,
|
|
48
|
+
'chatId:',
|
|
49
|
+
chatId,
|
|
50
|
+
'shareDBDoc:',
|
|
51
|
+
shareDBDoc,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate request
|
|
56
|
+
if (!validateRequest(req, res)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Ensure chats structure exists
|
|
62
|
+
ensureChatsExist(shareDBDoc);
|
|
63
|
+
ensureChatExists(shareDBDoc, chatId);
|
|
64
|
+
|
|
65
|
+
// Add user message to chat
|
|
66
|
+
addUserMessage(shareDBDoc, chatId, content);
|
|
67
|
+
|
|
68
|
+
// Return success immediately - AI generation continues in background
|
|
69
|
+
res.status(200).json('success');
|
|
70
|
+
|
|
71
|
+
// Set AI status to indicate generation is starting
|
|
72
|
+
setAIStatus(shareDBDoc, chatId, 'generating');
|
|
73
|
+
|
|
74
|
+
// Continue AI processing in background (don't await)
|
|
75
|
+
processAIRequestAsync({
|
|
76
|
+
shareDBDoc,
|
|
77
|
+
chatId,
|
|
78
|
+
content,
|
|
79
|
+
mode,
|
|
80
|
+
createAIEditLocalPresence,
|
|
81
|
+
getCurrentCommitId,
|
|
82
|
+
model,
|
|
83
|
+
aiRequestOptions,
|
|
84
|
+
onCreditDeduction,
|
|
85
|
+
}).catch((error) => {
|
|
86
|
+
console.error(
|
|
87
|
+
'Background AI processing error:',
|
|
88
|
+
error,
|
|
89
|
+
);
|
|
90
|
+
// Handle error without HTTP response
|
|
91
|
+
handleBackgroundError(shareDBDoc, chatId, error);
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
handleError(shareDBDoc, chatId, error, res);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Processes the AI request asynchronously in the background
|
|
100
|
+
*/
|
|
101
|
+
const processAIRequestAsync = async ({
|
|
102
|
+
shareDBDoc,
|
|
103
|
+
chatId,
|
|
104
|
+
content,
|
|
105
|
+
mode,
|
|
106
|
+
createAIEditLocalPresence,
|
|
107
|
+
getCurrentCommitId,
|
|
108
|
+
model,
|
|
109
|
+
aiRequestOptions,
|
|
110
|
+
onCreditDeduction,
|
|
111
|
+
}: {
|
|
112
|
+
shareDBDoc: ShareDBDoc<VizContent>;
|
|
113
|
+
chatId: string;
|
|
114
|
+
content: string;
|
|
115
|
+
mode: string;
|
|
116
|
+
createAIEditLocalPresence: () => any;
|
|
117
|
+
getCurrentCommitId?: () => string | null;
|
|
118
|
+
model?: string;
|
|
119
|
+
aiRequestOptions?: any;
|
|
120
|
+
onCreditDeduction?: any;
|
|
121
|
+
}) => {
|
|
122
|
+
try {
|
|
123
|
+
// Capture the current commit ID before making changes (for VizHub integration)
|
|
124
|
+
const beforeCommitId = getCurrentCommitId
|
|
125
|
+
? getCurrentCommitId()
|
|
126
|
+
: null;
|
|
127
|
+
|
|
128
|
+
// Create LLM function for streaming
|
|
129
|
+
const llmFunction = createLLMFunction({
|
|
130
|
+
shareDBDoc,
|
|
131
|
+
createAIEditLocalPresence,
|
|
132
|
+
chatId,
|
|
133
|
+
model,
|
|
134
|
+
aiRequestOptions,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Create server-side runCode function using shareDBDoc
|
|
138
|
+
const submitOperation =
|
|
139
|
+
createSubmitOperation(shareDBDoc);
|
|
140
|
+
const runCode = createRunCodeFunction(submitOperation);
|
|
141
|
+
|
|
142
|
+
// Perform AI editing or chat based on mode
|
|
143
|
+
const editResult =
|
|
144
|
+
mode === 'ask'
|
|
145
|
+
? await performAIChat({
|
|
146
|
+
prompt: content,
|
|
147
|
+
shareDBDoc,
|
|
148
|
+
llmFunction,
|
|
149
|
+
})
|
|
150
|
+
: await performAIEditing({
|
|
151
|
+
prompt: content,
|
|
152
|
+
shareDBDoc,
|
|
153
|
+
llmFunction,
|
|
154
|
+
runCode,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Add diff data to the AI message if there are changes
|
|
158
|
+
if (
|
|
159
|
+
editResult.diffData &&
|
|
160
|
+
Object.keys(editResult.diffData).length > 0
|
|
161
|
+
) {
|
|
162
|
+
addDiffToAIMessage(
|
|
163
|
+
shareDBDoc,
|
|
164
|
+
chatId,
|
|
165
|
+
editResult.diffData,
|
|
166
|
+
beforeCommitId, // Pass the commit ID before AI changes (for VizHub integration)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle credit deduction if callback is provided
|
|
171
|
+
if (
|
|
172
|
+
onCreditDeduction &&
|
|
173
|
+
(editResult as any).upstreamCostCents
|
|
174
|
+
) {
|
|
175
|
+
try {
|
|
176
|
+
await onCreditDeduction({
|
|
177
|
+
upstreamCostCents: (editResult as any)
|
|
178
|
+
.upstreamCostCents,
|
|
179
|
+
provider: (editResult as any).provider,
|
|
180
|
+
inputTokens: (editResult as any).inputTokens,
|
|
181
|
+
outputTokens: (editResult as any).outputTokens,
|
|
182
|
+
});
|
|
183
|
+
} catch (creditError) {
|
|
184
|
+
console.error(
|
|
185
|
+
'Credit deduction error:',
|
|
186
|
+
creditError,
|
|
187
|
+
);
|
|
188
|
+
// Don't fail the request if credit deduction fails
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Clear the AI status to indicate completion
|
|
193
|
+
setAIStatus(shareDBDoc, chatId, undefined);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Set error status and add error message to chat
|
|
196
|
+
setAIStatus(shareDBDoc, chatId, 'error');
|
|
197
|
+
handleBackgroundError(shareDBDoc, chatId, error);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseMarkdownFiles,
|
|
3
|
+
StreamingMarkdownParser,
|
|
4
|
+
} from 'llm-code-format';
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { generateRunId } from '@vizhub/viz-utils';
|
|
8
|
+
import {
|
|
9
|
+
updateAIStatus,
|
|
10
|
+
createAIMessage,
|
|
11
|
+
updateAIMessageContent,
|
|
12
|
+
finalizeAIMessage,
|
|
13
|
+
ensureFileExists,
|
|
14
|
+
clearFileContent,
|
|
15
|
+
appendLineToFile,
|
|
16
|
+
updateFiles,
|
|
17
|
+
updateAIScratchpad,
|
|
18
|
+
} from './chatOperations.js';
|
|
19
|
+
import { mergeFileChanges } from 'editcodewithai';
|
|
20
|
+
import { diff } from '../../ot.js';
|
|
21
|
+
import { VizChatId, VizContent } from '@vizhub/viz-types';
|
|
22
|
+
import { ShareDBDoc } from '../../types.js';
|
|
23
|
+
|
|
24
|
+
const DEBUG = false;
|
|
25
|
+
|
|
26
|
+
// Useful for testing/debugging the streaming behavior
|
|
27
|
+
const slowMode = false;
|
|
28
|
+
|
|
29
|
+
// Throttle the streaming updates, so that we don't
|
|
30
|
+
// overwhelm the ShareDB server with too many updates.
|
|
31
|
+
// It happened actually, before adding this.
|
|
32
|
+
// MongoDB VizHub server got in fact overloaded with
|
|
33
|
+
// too many updates from the AI streaming response, with
|
|
34
|
+
// warning: "Replication Oplog Window has gone below 1 hour"
|
|
35
|
+
const THROTTLE_INTERVAL_MS = 100;
|
|
36
|
+
|
|
37
|
+
// Feature flag to enable/disable streaming editing.
|
|
38
|
+
// * If `true`, the AI streaming response will be used to
|
|
39
|
+
// edit files in real-time by submitting ShareDB ops.
|
|
40
|
+
// * If `false`, the updates to code files will be applied
|
|
41
|
+
// only after the AI has finished generating the entire response.
|
|
42
|
+
//
|
|
43
|
+
// Current status: there's a tricky bug with the streaming
|
|
44
|
+
// where the AI edits sometimes don't apply correctly in CodeMirror.
|
|
45
|
+
// It seems that sometimes the op that clears the file content
|
|
46
|
+
// is not applied correctly in the front end, leading to
|
|
47
|
+
// a situation where the AI streaming edits are concatenated into the middle
|
|
48
|
+
// of the file instead of replacing it.
|
|
49
|
+
// See https://github.com/codemirror/codemirror.next/issues/1234
|
|
50
|
+
const enableStreamingEditing = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates and configures the LLM function for streaming with reasoning tokens
|
|
54
|
+
*/
|
|
55
|
+
export const createLLMFunction = ({
|
|
56
|
+
shareDBDoc,
|
|
57
|
+
createAIEditLocalPresence,
|
|
58
|
+
chatId,
|
|
59
|
+
// Feature flag to enable/disable reasoning tokens.
|
|
60
|
+
// When false, reasoning tokens are not requested from the API
|
|
61
|
+
// and reasoning content is not processed in the streaming response.
|
|
62
|
+
enableReasoningTokens = false,
|
|
63
|
+
model,
|
|
64
|
+
aiRequestOptions,
|
|
65
|
+
}: {
|
|
66
|
+
shareDBDoc: ShareDBDoc<VizContent>;
|
|
67
|
+
createAIEditLocalPresence: () => any;
|
|
68
|
+
chatId: VizChatId;
|
|
69
|
+
enableReasoningTokens?: boolean;
|
|
70
|
+
model?: string;
|
|
71
|
+
aiRequestOptions?: any;
|
|
72
|
+
}) => {
|
|
73
|
+
return async (fullPrompt: string) => {
|
|
74
|
+
const localPresence = enableStreamingEditing
|
|
75
|
+
? createAIEditLocalPresence()
|
|
76
|
+
: null;
|
|
77
|
+
|
|
78
|
+
// Create OpenRouter client for reasoning token support
|
|
79
|
+
const openRouterClient = new OpenAI({
|
|
80
|
+
apiKey: process.env.VZCODE_EDIT_WITH_AI_API_KEY,
|
|
81
|
+
baseURL:
|
|
82
|
+
process.env.VZCODE_EDIT_WITH_AI_BASE_URL ||
|
|
83
|
+
'https://openrouter.ai/api/v1',
|
|
84
|
+
defaultHeaders: {
|
|
85
|
+
'HTTP-Referer': 'https://vizhub.com',
|
|
86
|
+
'X-Title': 'VizHub',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let fullContent = '';
|
|
91
|
+
let generationId = '';
|
|
92
|
+
let currentEditingFileId = null;
|
|
93
|
+
let currentEditingFileName = null;
|
|
94
|
+
|
|
95
|
+
// Create initial AI message for streaming
|
|
96
|
+
const aiMessageId = createAIMessage(shareDBDoc, chatId);
|
|
97
|
+
|
|
98
|
+
// --- throttle wrapper ---
|
|
99
|
+
function makeThrottledUpdater() {
|
|
100
|
+
let lastCall = 0;
|
|
101
|
+
let latestContent = '';
|
|
102
|
+
let timer: NodeJS.Timeout | null = null;
|
|
103
|
+
|
|
104
|
+
function invoke() {
|
|
105
|
+
updateAIMessageContent(
|
|
106
|
+
shareDBDoc,
|
|
107
|
+
chatId,
|
|
108
|
+
aiMessageId,
|
|
109
|
+
latestContent,
|
|
110
|
+
);
|
|
111
|
+
lastCall = Date.now();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fn = (content: string) => {
|
|
115
|
+
latestContent = content;
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
|
|
118
|
+
if (now - lastCall >= THROTTLE_INTERVAL_MS) {
|
|
119
|
+
// safe to call immediately
|
|
120
|
+
invoke();
|
|
121
|
+
} else if (!timer) {
|
|
122
|
+
// schedule for later
|
|
123
|
+
timer = setTimeout(
|
|
124
|
+
() => {
|
|
125
|
+
timer = null;
|
|
126
|
+
invoke();
|
|
127
|
+
},
|
|
128
|
+
THROTTLE_INTERVAL_MS - (now - lastCall),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// expose a flush() helper to force an immediate write
|
|
134
|
+
fn.flush = () => {
|
|
135
|
+
if (timer) {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
timer = null;
|
|
138
|
+
}
|
|
139
|
+
invoke();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return fn;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const throttledUpdateAIMessageContent =
|
|
146
|
+
makeThrottledUpdater();
|
|
147
|
+
|
|
148
|
+
// Function to report file edited
|
|
149
|
+
// This is called when the AI has finished editing a file
|
|
150
|
+
// and we want to update the message content with the file name.
|
|
151
|
+
const reportFileEdited = () => {
|
|
152
|
+
if (currentEditingFileName) {
|
|
153
|
+
fullContent += ` * Edited ${currentEditingFileName}\n`;
|
|
154
|
+
throttledUpdateAIMessageContent(fullContent);
|
|
155
|
+
currentEditingFileName = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Define callbacks for streaming parser
|
|
160
|
+
const callbacks = {
|
|
161
|
+
onFileNameChange: async (
|
|
162
|
+
fileName: string,
|
|
163
|
+
format: string,
|
|
164
|
+
) => {
|
|
165
|
+
DEBUG &&
|
|
166
|
+
console.log(
|
|
167
|
+
`File changed to: ${fileName} (${format})`,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Find existing file or create new one
|
|
171
|
+
currentEditingFileId = ensureFileExists(
|
|
172
|
+
shareDBDoc,
|
|
173
|
+
fileName,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
reportFileEdited();
|
|
177
|
+
currentEditingFileName = fileName;
|
|
178
|
+
|
|
179
|
+
// Clear the file content to start fresh
|
|
180
|
+
// (AI will regenerate the entire file content)
|
|
181
|
+
clearFileContent(shareDBDoc, currentEditingFileId);
|
|
182
|
+
|
|
183
|
+
// Update AI status
|
|
184
|
+
updateAIStatus(
|
|
185
|
+
shareDBDoc,
|
|
186
|
+
chatId,
|
|
187
|
+
'Editing ' + fileName,
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
onCodeLine: async (line: string) => {
|
|
191
|
+
DEBUG && console.log(`Code line: ${line}`);
|
|
192
|
+
|
|
193
|
+
// If streaming is enabled, we apply the line immediately
|
|
194
|
+
if (currentEditingFileId) {
|
|
195
|
+
// Apply OT operation for this line immediately
|
|
196
|
+
appendLineToFile(
|
|
197
|
+
shareDBDoc,
|
|
198
|
+
currentEditingFileId,
|
|
199
|
+
line,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Update AI presence to show cursor at the end of the file
|
|
203
|
+
const currentFile =
|
|
204
|
+
shareDBDoc.data.files[currentEditingFileId];
|
|
205
|
+
if (currentFile && currentFile.text) {
|
|
206
|
+
const textLength = currentFile.text.length;
|
|
207
|
+
const filePresence = {
|
|
208
|
+
username: 'AI Editor',
|
|
209
|
+
start: [
|
|
210
|
+
'files',
|
|
211
|
+
currentEditingFileId,
|
|
212
|
+
'text',
|
|
213
|
+
textLength,
|
|
214
|
+
],
|
|
215
|
+
end: [
|
|
216
|
+
'files',
|
|
217
|
+
currentEditingFileId,
|
|
218
|
+
'text',
|
|
219
|
+
textLength,
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (localPresence) {
|
|
224
|
+
localPresence.submit(
|
|
225
|
+
filePresence,
|
|
226
|
+
(error) => {
|
|
227
|
+
if (error) {
|
|
228
|
+
console.warn(
|
|
229
|
+
'AI Editor line presence submission error:',
|
|
230
|
+
error,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
onNonCodeLine: async (line: string) => {
|
|
240
|
+
// We want to report a file edited only if the line is not empty,
|
|
241
|
+
// because sometimes the LLMs leave a newline between the file name
|
|
242
|
+
// declaration and th
|
|
243
|
+
if (line.trim() !== '') {
|
|
244
|
+
reportFileEdited();
|
|
245
|
+
}
|
|
246
|
+
fullContent += line + '\n';
|
|
247
|
+
throttledUpdateAIMessageContent(fullContent);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const parser = new StreamingMarkdownParser(callbacks);
|
|
252
|
+
|
|
253
|
+
const chunks = [];
|
|
254
|
+
let reasoningContent = '';
|
|
255
|
+
|
|
256
|
+
// Stream the response with reasoning tokens
|
|
257
|
+
const modelName =
|
|
258
|
+
model ||
|
|
259
|
+
process.env.VZCODE_EDIT_WITH_AI_MODEL_NAME ||
|
|
260
|
+
'anthropic/claude-3.5-sonnet';
|
|
261
|
+
|
|
262
|
+
// Configure reasoning tokens based on enableReasoningTokens flag
|
|
263
|
+
const requestConfig: any = {
|
|
264
|
+
model: modelName,
|
|
265
|
+
messages: [{ role: 'user', content: fullPrompt }],
|
|
266
|
+
usage: { include: true },
|
|
267
|
+
stream: true,
|
|
268
|
+
...aiRequestOptions,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Only include reasoning configuration if reasoning tokens are enabled
|
|
272
|
+
if (enableReasoningTokens) {
|
|
273
|
+
requestConfig.reasoning = {
|
|
274
|
+
effort: 'low',
|
|
275
|
+
exclude: false,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const stream = await (
|
|
280
|
+
openRouterClient.chat.completions.create as any
|
|
281
|
+
)(requestConfig);
|
|
282
|
+
|
|
283
|
+
let reasoningStarted = false;
|
|
284
|
+
let contentStarted = false;
|
|
285
|
+
|
|
286
|
+
for await (const chunk of stream) {
|
|
287
|
+
if (slowMode) {
|
|
288
|
+
await new Promise((resolve) =>
|
|
289
|
+
setTimeout(resolve, 500),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
|
|
293
|
+
|
|
294
|
+
if (delta?.reasoning && enableReasoningTokens) {
|
|
295
|
+
// Handle reasoning tokens (thinking) - only if enabled
|
|
296
|
+
if (!reasoningStarted) {
|
|
297
|
+
reasoningStarted = true;
|
|
298
|
+
updateAIStatus(shareDBDoc, chatId, 'Thinking...');
|
|
299
|
+
}
|
|
300
|
+
reasoningContent += delta.reasoning;
|
|
301
|
+
updateAIScratchpad(
|
|
302
|
+
shareDBDoc,
|
|
303
|
+
chatId,
|
|
304
|
+
reasoningContent,
|
|
305
|
+
);
|
|
306
|
+
} else if (delta?.content) {
|
|
307
|
+
// Handle regular content tokens
|
|
308
|
+
if (reasoningStarted && !contentStarted) {
|
|
309
|
+
// Clear reasoning when content starts
|
|
310
|
+
contentStarted = true;
|
|
311
|
+
updateAIScratchpad(shareDBDoc, chatId, '');
|
|
312
|
+
updateAIStatus(
|
|
313
|
+
shareDBDoc,
|
|
314
|
+
chatId,
|
|
315
|
+
'Generating response...',
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const chunkContent = delta.content;
|
|
320
|
+
chunks.push(chunkContent);
|
|
321
|
+
|
|
322
|
+
if (enableStreamingEditing) {
|
|
323
|
+
await parser.processChunk(chunkContent);
|
|
324
|
+
} else {
|
|
325
|
+
fullContent += chunkContent;
|
|
326
|
+
throttledUpdateAIMessageContent(fullContent);
|
|
327
|
+
}
|
|
328
|
+
} else if (chunk.usage) {
|
|
329
|
+
// Handle usage information
|
|
330
|
+
DEBUG && console.log('Usage:', chunk.usage);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!generationId && chunk.id) {
|
|
334
|
+
generationId = chunk.id;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await parser.flushRemaining();
|
|
338
|
+
reportFileEdited();
|
|
339
|
+
|
|
340
|
+
// Final cleanup - clear scratchpad and set final status
|
|
341
|
+
updateAIScratchpad(shareDBDoc, chatId, '');
|
|
342
|
+
updateAIStatus(shareDBDoc, chatId, 'Done editing.');
|
|
343
|
+
throttledUpdateAIMessageContent(fullContent);
|
|
344
|
+
|
|
345
|
+
// Flush to ensure the final content is written immediately
|
|
346
|
+
throttledUpdateAIMessageContent.flush();
|
|
347
|
+
|
|
348
|
+
// Finalize the AI message by clearing temporary fields
|
|
349
|
+
finalizeAIMessage(shareDBDoc, chatId);
|
|
350
|
+
|
|
351
|
+
// Clear AI Editor presence when done
|
|
352
|
+
DEBUG &&
|
|
353
|
+
console.log(
|
|
354
|
+
'AI editing done, clearing AI Editor presence',
|
|
355
|
+
);
|
|
356
|
+
if (localPresence) {
|
|
357
|
+
localPresence.submit(null, (error) => {
|
|
358
|
+
DEBUG && console.log('AI Editor presence cleared');
|
|
359
|
+
if (error) {
|
|
360
|
+
console.warn(
|
|
361
|
+
'AI Editor presence cleanup error:',
|
|
362
|
+
error,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// If streaming editing is not enabled, we need to
|
|
369
|
+
// apply all the edits at once
|
|
370
|
+
if (!enableStreamingEditing) {
|
|
371
|
+
updateFiles(
|
|
372
|
+
shareDBDoc,
|
|
373
|
+
mergeFileChanges(
|
|
374
|
+
shareDBDoc.data.files,
|
|
375
|
+
parseMarkdownFiles(fullContent, 'bold').files,
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Generate a new runId to trigger a run when AI finishes editing
|
|
381
|
+
// This will trigger a re-run without hot reloading
|
|
382
|
+
const newRunId = generateRunId();
|
|
383
|
+
const runIdOp = diff(shareDBDoc.data, {
|
|
384
|
+
...shareDBDoc.data,
|
|
385
|
+
runId: newRunId,
|
|
386
|
+
});
|
|
387
|
+
shareDBDoc.submitOp(runIdOp, (error) => {
|
|
388
|
+
if (error) {
|
|
389
|
+
console.warn(
|
|
390
|
+
'Error setting runId after AI editing:',
|
|
391
|
+
error,
|
|
392
|
+
);
|
|
393
|
+
} else {
|
|
394
|
+
DEBUG &&
|
|
395
|
+
console.log(
|
|
396
|
+
'Set new runId after AI editing:',
|
|
397
|
+
newRunId,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Write chunks file for debugging
|
|
403
|
+
if (DEBUG) {
|
|
404
|
+
const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
|
|
405
|
+
fs.writeFileSync(
|
|
406
|
+
chunksFileJSONpath,
|
|
407
|
+
JSON.stringify(chunks, null, 2),
|
|
408
|
+
);
|
|
409
|
+
console.log(
|
|
410
|
+
`AI chunks written to ${chunksFileJSONpath}`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
content: fullContent,
|
|
416
|
+
generationId: generationId,
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
};
|
|
@@ -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
|
+
};
|