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.
Files changed (196) hide show
  1. package/package.json +3 -2
  2. package/src/cli.ts +208 -0
  3. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
  4. package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
  5. package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
  6. package/src/client/AIAssist/Spinner.jsx +76 -0
  7. package/src/client/AIAssist/formatFile.ts +27 -0
  8. package/src/client/AIAssist/generateFilesContext.ts +25 -0
  9. package/src/client/AIAssist/index.ts +2 -0
  10. package/src/client/AIAssist/startAIAssist.ts +95 -0
  11. package/src/client/App/index.tsx +152 -0
  12. package/src/client/App/style.scss +82 -0
  13. package/src/client/App/usePending.ts +51 -0
  14. package/src/client/App/useShareDB.ts +161 -0
  15. package/src/client/CodeEditor/Copilot/index.ts +74 -0
  16. package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
  17. package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
  18. package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
  19. package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
  20. package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
  21. package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
  22. package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
  23. package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
  24. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
  25. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
  26. package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
  27. package/src/client/CodeEditor/basicSetup.ts +73 -0
  28. package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
  29. package/src/client/CodeEditor/index.tsx +202 -0
  30. package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
  31. package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
  32. package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
  33. package/src/client/CodeEditor/style.scss +156 -0
  34. package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
  35. package/src/client/CodeErrorOverlay/index.tsx +77 -0
  36. package/src/client/CodeErrorOverlay/style.scss +33 -0
  37. package/src/client/Fonts/fonts.ts +8 -0
  38. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  39. package/src/client/Icons/BugSVG.tsx +20 -0
  40. package/src/client/Icons/CloseSVG.tsx +19 -0
  41. package/src/client/Icons/CssSVG.tsx +16 -0
  42. package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
  43. package/src/client/Icons/EditSVG.tsx +19 -0
  44. package/src/client/Icons/FileSVG.tsx +19 -0
  45. package/src/client/Icons/FolderSVG.tsx +20 -0
  46. package/src/client/Icons/GearSVG.tsx +16 -0
  47. package/src/client/Icons/HtmlSVG.tsx +16 -0
  48. package/src/client/Icons/JavaScriptSVG.tsx +16 -0
  49. package/src/client/Icons/JsonSVG.tsx +16 -0
  50. package/src/client/Icons/MarkdownSVG.tsx +16 -0
  51. package/src/client/Icons/MicOffSVG.tsx +35 -0
  52. package/src/client/Icons/MicSVG.tsx +25 -0
  53. package/src/client/Icons/NewSVG.tsx +19 -0
  54. package/src/client/Icons/PinSVG.tsx +14 -0
  55. package/src/client/Icons/PlaySVG.tsx +16 -0
  56. package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
  57. package/src/client/Icons/ReactSVG.tsx +16 -0
  58. package/src/client/Icons/SearchSVG.tsx +20 -0
  59. package/src/client/Icons/SparklesSVG.tsx +23 -0
  60. package/src/client/Icons/SplitEditorSVG.tsx +34 -0
  61. package/src/client/Icons/StopSVG.tsx +15 -0
  62. package/src/client/Icons/SvelteSVG.tsx +16 -0
  63. package/src/client/Icons/TrashSVG.tsx +19 -0
  64. package/src/client/Icons/TypeScriptSVG.tsx +16 -0
  65. package/src/client/Icons/index.tsx +26 -0
  66. package/src/client/ImageViewer/index.tsx +91 -0
  67. package/src/client/ImageViewer/style.scss +41 -0
  68. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  69. package/src/client/PresenceNotifications/index.tsx +158 -0
  70. package/src/client/PresenceNotifications/style.scss +17 -0
  71. package/src/client/RunCodeWidget/index.tsx +117 -0
  72. package/src/client/RunCodeWidget/style.scss +24 -0
  73. package/src/client/SplitPaneResizeContext.tsx +159 -0
  74. package/src/client/TabList/Tab.tsx +73 -0
  75. package/src/client/TabList/index.tsx +35 -0
  76. package/src/client/TabList/style.scss +51 -0
  77. package/src/client/VZCodeContext/index.tsx +29 -0
  78. package/src/client/VZCodeContext/types.ts +248 -0
  79. package/src/client/VZCodeContext/useVZCodeState.ts +602 -0
  80. package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
  81. package/src/client/VZLeft.tsx +28 -0
  82. package/src/client/VZMiddle.tsx +172 -0
  83. package/src/client/VZResizer/index.tsx +128 -0
  84. package/src/client/VZResizer/styles.scss +33 -0
  85. package/src/client/VZRight.tsx +113 -0
  86. package/src/client/VZSettings.tsx +275 -0
  87. package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
  88. package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
  89. package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
  90. package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
  91. package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
  92. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
  93. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
  94. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
  95. package/src/client/VZSidebar/AIChat/index.tsx +357 -0
  96. package/src/client/VZSidebar/AIChat/styles.scss +646 -0
  97. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
  98. package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
  99. package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
  100. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
  101. package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
  102. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  103. package/src/client/VZSidebar/FileListing.tsx +91 -0
  104. package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
  105. package/src/client/VZSidebar/Item.tsx +237 -0
  106. package/src/client/VZSidebar/Listing.tsx +47 -0
  107. package/src/client/VZSidebar/Search.tsx +527 -0
  108. package/src/client/VZSidebar/VisualEditor.tsx +282 -0
  109. package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
  110. package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
  111. package/src/client/VZSidebar/index.tsx +728 -0
  112. package/src/client/VZSidebar/styles.scss +612 -0
  113. package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
  114. package/src/client/bootstrap.ts +26 -0
  115. package/src/client/buildWorker.ts +3 -0
  116. package/src/client/featureFlags.ts +8 -0
  117. package/src/client/generateRequestId.ts +6 -0
  118. package/src/client/getFileTree.ts +53 -0
  119. package/src/client/index.ts +19 -0
  120. package/src/client/main.jsx +10 -0
  121. package/src/client/presenceColor.ts +8 -0
  122. package/src/client/shouldTriggerRun.ts +31 -0
  123. package/src/client/sortFileTree.ts +21 -0
  124. package/src/client/tabsSearchParameters.test.ts +79 -0
  125. package/src/client/tabsSearchParameters.ts +137 -0
  126. package/src/client/themes/index.ts +7 -0
  127. package/src/client/themes/themes.ts +77 -0
  128. package/src/client/themes/uiwImports.ts +32 -0
  129. package/src/client/themes/useDynamicTheme.ts +29 -0
  130. package/src/client/themes/vizhubTheme/colors.ts +80 -0
  131. package/src/client/themes/vizhubTheme/index.ts +193 -0
  132. package/src/client/useActions.ts +288 -0
  133. package/src/client/useESLint/index.ts +72 -0
  134. package/src/client/useESLint/worker.ts +113 -0
  135. package/src/client/useEditorCache.ts +35 -0
  136. package/src/client/useFileCRUD.ts +228 -0
  137. package/src/client/useKeyboardShortcuts.ts +451 -0
  138. package/src/client/useOpenDirectories.ts +99 -0
  139. package/src/client/usePresenceAutoFollow.ts +85 -0
  140. package/src/client/usePrettier/index.ts +178 -0
  141. package/src/client/usePrettier/worker.ts +109 -0
  142. package/src/client/useRunCode.tsx +15 -0
  143. package/src/client/useRuntimeError.ts +32 -0
  144. package/src/client/useSubmitOperation.ts +15 -0
  145. package/src/client/useURLSync.ts +137 -0
  146. package/src/client/usernameLocalStorage.ts +43 -0
  147. package/src/client/utils/fileExtension.ts +10 -0
  148. package/src/client/utils/isImageFile.ts +12 -0
  149. package/src/client/vzReducer/aiChatReducer.ts +44 -0
  150. package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
  151. package/src/client/vzReducer/closeTabsReducer.ts +96 -0
  152. package/src/client/vzReducer/createInitialState.ts +38 -0
  153. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  154. package/src/client/vzReducer/findPane.ts +19 -0
  155. package/src/client/vzReducer/index.ts +250 -0
  156. package/src/client/vzReducer/openTabReducer.test.ts +127 -0
  157. package/src/client/vzReducer/openTabReducer.ts +74 -0
  158. package/src/client/vzReducer/searchReducer.test.ts +292 -0
  159. package/src/client/vzReducer/searchReducer.ts +181 -0
  160. package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
  161. package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
  162. package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
  163. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  164. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  165. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  166. package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
  167. package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
  168. package/src/client/vzReducer/updatePane.ts +40 -0
  169. package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
  170. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  171. package/src/ot.ts +24 -0
  172. package/src/randomId.ts +15 -0
  173. package/src/reactRouterExports.ts +15 -0
  174. package/src/runCode.ts +27 -0
  175. package/src/server/aiChatHandler/aiEditing.ts +151 -0
  176. package/src/server/aiChatHandler/chatOperations.ts +476 -0
  177. package/src/server/aiChatHandler/errorHandling.ts +66 -0
  178. package/src/server/aiChatHandler/index.ts +199 -0
  179. package/src/server/aiChatHandler/llmStreaming.ts +419 -0
  180. package/src/server/aiChatHandler/validation.ts +24 -0
  181. package/src/server/computeInitialDocument.ts +220 -0
  182. package/src/server/config.ts +24 -0
  183. package/src/server/featureFlags.ts +8 -0
  184. package/src/server/generateAIResponse.ts +188 -0
  185. package/src/server/handleAIAssist.ts +53 -0
  186. package/src/server/handleAIChatMessage.ts +2 -0
  187. package/src/server/handleAICopilot.ts +97 -0
  188. package/src/server/index.ts +379 -0
  189. package/src/server/isDirectory.ts +1 -0
  190. package/src/server/livekit.ts +24 -0
  191. package/src/server/prettier.ts +115 -0
  192. package/src/server/setupEnv.ts +8 -0
  193. package/src/submitOperation.ts +25 -0
  194. package/src/types.ts +205 -0
  195. package/src/utils/fileDiff.ts +117 -0
  196. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,220 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { randomId } from '../randomId.js';
4
+ import {
5
+ enableDirectories,
6
+ debugDirectories,
7
+ debugIgnore,
8
+ } from './featureFlags.js';
9
+ import createIgnore from 'ignore';
10
+ import { ignoreFilePattern, baseIgnore } from './config.js';
11
+ import { isDirectory } from './isDirectory.js';
12
+
13
+ // Import the image file utility
14
+ const isImageFile = (fileName) => {
15
+ return (
16
+ fileName.match(
17
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
18
+ ) !== null
19
+ );
20
+ };
21
+
22
+ /**
23
+ * @param {string} fullPath - absolut path of the workspace root
24
+ * @param {string} currentDirectoryPath - path where the ignore file is found, relative to fullPath
25
+ * @param {string} fileName - name of the ignore file
26
+ * @returns {string[]} parsed lines
27
+ */
28
+ const parseIgnoreFile = (
29
+ fullPath,
30
+ currentDirectory,
31
+ fileName,
32
+ ) => {
33
+ const filePath = path.join(
34
+ fullPath,
35
+ currentDirectory,
36
+ fileName,
37
+ );
38
+ const content = fs.readFileSync(filePath, 'utf8');
39
+ const globs = content
40
+ .split(/[\n\r]+/)
41
+ .filter(
42
+ // remove blank line and comments
43
+ (line) => line.length > 0 && !line.startsWith('#'),
44
+ )
45
+ .map((line) => {
46
+ const { bang, slash, glob } = line.match(
47
+ /^(?<bang>!?)(?<slash>\/?)(?<glob>.*)$/,
48
+ ).groups;
49
+ const hasSlash =
50
+ Boolean(slash) || /\/.*\S/.test(glob);
51
+ const relativeGlob = path.posix.join(
52
+ currentDirectory.replace(
53
+ // escape characters with special meaning in glob expressions
54
+ /[*?!# \[\]\\]/g,
55
+ (char) => '\\' + char,
56
+ ),
57
+ // a pattern that doesn't include a slash (not counting a trailing one) matches files in any descendant directory od the current one
58
+ hasSlash ? '' : '**',
59
+ glob,
60
+ );
61
+ // preserve leading `!` and `/` characters
62
+ return bang + slash + relativeGlob;
63
+ });
64
+ if (debugIgnore) {
65
+ console.debug(
66
+ 'at',
67
+ currentDirectory,
68
+ 'parsing',
69
+ fileName,
70
+ 'obtained globs',
71
+ globs,
72
+ );
73
+ }
74
+ return globs;
75
+ };
76
+
77
+ // Lists files from the file system,
78
+ // converts them into the VZCode internal
79
+ // ShareDB-compatible data structure.
80
+ export const computeInitialDocument = ({ fullPath }) => {
81
+ // Isolate files, not directories.
82
+ // Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
83
+
84
+ // Initialize the document using our data structure for representing files.
85
+ const initialDocument = {
86
+ // `files`
87
+ // * Keys are file ids, which are random numbers.
88
+ // * Values are objects with properties:
89
+ // * text - the text content of the file
90
+ // * name - the file name
91
+ files: {},
92
+
93
+ // `isInteracting`
94
+ // * Whether the user is currently interacting with the document
95
+ // using an interactive code widget such as number dragger.
96
+ // * When true, the auto-save to the file system is changed to be
97
+ // more frequent (throttled not debounced).
98
+ // isInteracting: false,
99
+ // This is `undefined` initially.
100
+ };
101
+
102
+ /**
103
+ * Stack for recursively traversing directories.
104
+ * @type {string[]}
105
+ */
106
+ let files = [];
107
+
108
+ const ignoreFileMatcher = createIgnore().add(
109
+ ignoreFilePattern,
110
+ );
111
+ const isIgnoreFile = (fileName) =>
112
+ ignoreFileMatcher.ignores(fileName);
113
+
114
+ const unsearchedDirectories = [
115
+ {
116
+ currentDirectory: '.',
117
+ ignore: createIgnore().add(baseIgnore),
118
+ },
119
+ ];
120
+
121
+ while (unsearchedDirectories.length !== 0) {
122
+ const { currentDirectory, ignore: parentIgnore } =
123
+ unsearchedDirectories.pop();
124
+ const currentDirectoryPath = path.join(
125
+ fullPath,
126
+ currentDirectory,
127
+ );
128
+ const dirEntries = fs
129
+ .readdirSync(currentDirectoryPath, {
130
+ withFileTypes: true,
131
+ })
132
+ .filter((dirent) =>
133
+ enableDirectories ? true : dirent.isFile(),
134
+ );
135
+ // find .ignore or .gitignore files in the current directory
136
+ const ignoreFiles = dirEntries
137
+ .filter(
138
+ (dirent) =>
139
+ dirent.isFile() && isIgnoreFile(dirent.name),
140
+ )
141
+ .map((file) => file.name);
142
+ let ignore = parentIgnore;
143
+ if (ignoreFiles.length > 0) {
144
+ const globs = ignoreFiles.flatMap((fileName) =>
145
+ parseIgnoreFile(
146
+ fullPath,
147
+ currentDirectory,
148
+ fileName,
149
+ ),
150
+ );
151
+ ignore = createIgnore().add(parentIgnore).add(globs);
152
+ }
153
+ const newFiles = dirEntries
154
+ .filter((dirent) => {
155
+ const relativePath =
156
+ path.posix.join(currentDirectory, dirent.name) +
157
+ (dirent.isDirectory() ? '/' : '');
158
+ const keep = !ignore.ignores(relativePath);
159
+ if (debugIgnore && !keep) {
160
+ console.debug(
161
+ 'at',
162
+ currentDirectory,
163
+ 'ignoring',
164
+ relativePath,
165
+ );
166
+ }
167
+ return keep;
168
+ })
169
+ // Add a trailing slash for directories
170
+ .map((dirent) => {
171
+ const relativePath = path.posix.join(
172
+ currentDirectory,
173
+ dirent.name,
174
+ );
175
+
176
+ if (!dirent.isDirectory()) {
177
+ return relativePath;
178
+ }
179
+ unsearchedDirectories.push({
180
+ currentDirectory: relativePath,
181
+ ignore,
182
+ });
183
+ return relativePath + '/';
184
+ });
185
+ // console.log(currentDirectory);
186
+
187
+ files.push(...newFiles);
188
+ }
189
+
190
+ files.forEach((file) => {
191
+ const id = randomId();
192
+ let text = null;
193
+
194
+ if (!isDirectory(file)) {
195
+ const filePath = path.join(fullPath, file);
196
+
197
+ if (isImageFile(file)) {
198
+ // Read image files as binary and convert to base64
199
+ const buffer = fs.readFileSync(filePath);
200
+ text = buffer.toString('base64');
201
+ } else {
202
+ // Read non-image files as UTF-8 text
203
+ text = fs.readFileSync(filePath, 'utf-8');
204
+ }
205
+ }
206
+
207
+ initialDocument.files[id] = {
208
+ text,
209
+ name: file,
210
+ };
211
+ });
212
+
213
+ if (debugDirectories) {
214
+ console.log('files:');
215
+ console.log(files);
216
+ console.log('initialDocument:');
217
+ console.log(JSON.stringify(initialDocument, null, 2));
218
+ }
219
+ return initialDocument;
220
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ *patterns describing which files should e comsidered as ignore files when creating the file tree.
3
+ *
4
+ * Format: one pattern per line, in gitignore format as defimed by https://git-scm.com/docs/gitignore#_pattern_format
5
+ */
6
+ export const ignoreFilePattern =
7
+ process.env.IGNORE_FILE_PATTERN ??
8
+ `
9
+ .vzignore
10
+ .ignore
11
+ .gitignore
12
+ `;
13
+
14
+ /**
15
+ * the base ignore patterns, that should be applied before any ignore file found when creating the file tree.
16
+ *
17
+ * These patterns apply even in the absence of an ignore file.
18
+ */
19
+ export const baseIgnore =
20
+ process.env.BASE_IGNORE ??
21
+ `
22
+ .git/
23
+ node_modules/
24
+ `;
@@ -0,0 +1,8 @@
1
+ // Feature flag for directories, disabled until it's fully working.
2
+ export const enableDirectories = true;
3
+
4
+ export const debugDirectories = false;
5
+
6
+ export const debugIgnore = false;
7
+
8
+ export const enableESLint = true;
@@ -0,0 +1,188 @@
1
+ import OpenAI from 'openai';
2
+ import { json1Presence } from '../ot.js';
3
+
4
+ // This module implements the generation of the AI response,
5
+ // using the OpenAI client.
6
+
7
+ // These environment variables are used to configure the OpenAI client.
8
+ // See `.env.sample` for the expected values.
9
+ const { VZCODE_AI_API_KEY, VZCODE_AI_BASE_URL } =
10
+ process.env;
11
+
12
+ // If neither of these are not set, the OpenAI client will not be initialized,
13
+ // e.g. for local development where no AI is needed.
14
+ const isAIEnabled =
15
+ VZCODE_AI_API_KEY !== undefined &&
16
+ VZCODE_AI_BASE_URL !== undefined;
17
+
18
+ const { editOp, type } = json1Presence;
19
+
20
+ // Debug flag to log more information during development.
21
+ const debug = false;
22
+
23
+ // Feature flag to slow down AI for development/testing.
24
+ // Particularly relevant for debugging ShareDB and CodeMirror sync issues.
25
+ // This allows you to test the case of editing the document at the
26
+ // same time as the AI generation is happening.
27
+ const slowdown = false;
28
+
29
+ // The options passed into the OpenAI client
30
+ // new OpenAI(openAIOptions)
31
+ const openAIOptions: { apiKey?: string; baseURL?: string } =
32
+ {};
33
+
34
+ // Support specifying the API key via an environment variable
35
+ // If VZCODE_AI_API_KEY is not set, note that the OpenAI client
36
+ // will look for the OPENAI_API_KEY environment variable instead.
37
+ if (process.env.VZCODE_AI_API_KEY !== undefined) {
38
+ openAIOptions.apiKey = process.env.VZCODE_AI_API_KEY;
39
+ }
40
+
41
+ // Support for local AI server
42
+ if (process.env.VZCODE_AI_BASE_URL !== undefined) {
43
+ // The OpenAI client errors if the API key is not set,
44
+ // so in the case where we don't need an API key,
45
+ // e.g. for testing with a local AI server like LM Studio or LocalAI,
46
+ // we populate the option with a fake API key, so it doesn't error.
47
+ if (!openAIOptions.apiKey) {
48
+ openAIOptions.apiKey = 'Fake API Key';
49
+ }
50
+
51
+ openAIOptions.baseURL = process.env.VZCODE_AI_BASE_URL;
52
+ }
53
+
54
+ debug &&
55
+ console.log(
56
+ 'openAIOptions: ' +
57
+ JSON.stringify(openAIOptions, null, 2),
58
+ );
59
+ let openai;
60
+
61
+ if (isAIEnabled) {
62
+ openai = new OpenAI(openAIOptions);
63
+ }
64
+
65
+ // The name of the source that the AI responses
66
+ // will be attributed to in ShareDB operations.
67
+ const AIShareDBSourceName = 'AIAssist';
68
+
69
+ // Returns trie if the operation comes from AI.
70
+ const opComesFromAIAssist = (ops, source) =>
71
+ source === AIShareDBSourceName;
72
+
73
+ // Keeps track of the currently ongoing AI streams.
74
+ // There could be many streams at the same time.
75
+ export const streams: Record<string, any> = {};
76
+
77
+ export const generateAIResponse = async ({
78
+ inputText,
79
+ insertionCursor,
80
+ fileId,
81
+ streamId,
82
+ shareDBDoc,
83
+ }) => {
84
+ if (!isAIEnabled) {
85
+ console.log(
86
+ '[generateAIResponse] AI is not enabled. Skipping AI generation.',
87
+ );
88
+ console.log(
89
+ '[generateAIResponse] To enable AI, see .env.sample for the required environment variables.',
90
+ );
91
+ return;
92
+ }
93
+
94
+ if (debug) {
95
+ console.log(
96
+ '[generateAIResponse] inputText:',
97
+ inputText,
98
+ );
99
+ console.log(
100
+ '[generateAIResponse] insertionCursor:',
101
+ insertionCursor,
102
+ );
103
+ console.log('[generateAIResponse] fileId:', fileId);
104
+ console.log('[generateAIResponse] streamId:', streamId);
105
+ console.log(
106
+ '[generateAIResponse] shareDBDoc:',
107
+ shareDBDoc,
108
+ );
109
+ }
110
+
111
+ // Handle the case that a user edits the text in the document
112
+ // that comes becofore the insertion cursor.
113
+ const accomodateDocChanges = (op, source) => {
114
+ if (!opComesFromAIAssist(op, source)) {
115
+ if (op !== null) {
116
+ insertionCursor = type
117
+ .transformPosition(
118
+ ['files', fileId, 'text', insertionCursor],
119
+ op,
120
+ )
121
+ .slice(-1)[0];
122
+ }
123
+ }
124
+ };
125
+ shareDBDoc.on('op', accomodateDocChanges);
126
+
127
+ // The prompt!
128
+ const messages = [
129
+ {
130
+ role: 'system',
131
+ content: [
132
+ 'You are an expert programmer.',
133
+ 'Your task is to output ONLY the code that replaces <FILL_ME> correctly.',
134
+ 'Do not add any markdown around.',
135
+ 'Do not duplicate the code before or after <FILL_ME>.',
136
+ 'Do not make any changes outside of <FILL_ME>.',
137
+ 'Do not enclose the output with backticks.',
138
+ 'If any additional instructions are required, they will be provided in comments.',
139
+ ].join(' '),
140
+ },
141
+ { role: 'user', content: inputText },
142
+ ];
143
+
144
+ if (debug) {
145
+ console.log('[generateAIResponse] messages:');
146
+ console.log(JSON.stringify(messages, null, 2));
147
+ }
148
+
149
+ streams[streamId] = await openai.chat.completions.create({
150
+ // model: 'gpt-3.5-turbo',
151
+ model: 'gpt-4o',
152
+ messages,
153
+ reasoning: {
154
+ effort: 'medium',
155
+ exclude: false,
156
+ } as any, // Type assertion for OpenRouter-specific reasoning parameter
157
+ provider: {
158
+ sort: 'throughput',
159
+ } as any, // Type assertion for OpenRouter-specific provider parameter
160
+ stream: true,
161
+ });
162
+
163
+ for await (const part of streams[streamId]) {
164
+ const op = editOp(
165
+ ['files', fileId, 'text'],
166
+ 'text-unicode',
167
+ [
168
+ insertionCursor,
169
+ part.choices[0]?.delta?.content || '',
170
+ ],
171
+ );
172
+
173
+ shareDBDoc.submitOp(op, {
174
+ source: AIShareDBSourceName,
175
+ });
176
+
177
+ if (slowdown) {
178
+ await new Promise((resolve) => {
179
+ setTimeout(resolve, 2000);
180
+ });
181
+ }
182
+
183
+ insertionCursor += (
184
+ part.choices[0]?.delta?.content || ''
185
+ ).length;
186
+ }
187
+ shareDBDoc.off('op', accomodateDocChanges);
188
+ };
@@ -0,0 +1,53 @@
1
+ import {
2
+ generateAIResponse,
3
+ streams,
4
+ } from './generateAIResponse.js';
5
+
6
+ const debug = false;
7
+
8
+ export const handleAIAssist =
9
+ (shareDBDoc: any) => async (req: any, res: any) => {
10
+ const { inputText, insertionCursor, fileId } = req.body;
11
+
12
+ if (debug) {
13
+ console.log('[handleAIAssist] inputText:', inputText);
14
+ console.log(
15
+ '[handleAIAssist] insertionCursor:',
16
+ insertionCursor,
17
+ );
18
+ console.log('[handleAIAssist] fileId:', fileId);
19
+ }
20
+
21
+ try {
22
+ // Generate a unique streamId for this request
23
+ const streamId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
24
+
25
+ await generateAIResponse({
26
+ inputText,
27
+ insertionCursor,
28
+ fileId,
29
+ streamId,
30
+ shareDBDoc,
31
+ });
32
+
33
+ res
34
+ .status(200)
35
+ .send({ message: 'Operation successful!' });
36
+ } catch (error: any) {
37
+ console.error('handleAIAssist error:', error);
38
+ res.status(500).send({
39
+ message: 'Internal Server Error',
40
+ error: error.message,
41
+ });
42
+ }
43
+ };
44
+
45
+ export function haltGeneration(streamId: string) {
46
+ const stream = streams[streamId];
47
+
48
+ // Stream can be undefined here if the user
49
+ // clicks start and stop very quickly.
50
+ if (stream) {
51
+ stream.controller.abort();
52
+ }
53
+ }
@@ -0,0 +1,2 @@
1
+ // Re-export the refactored handleAIChatMessage from the new modular structure
2
+ export { handleAIChatMessage } from './aiChatHandler/index.js';
@@ -0,0 +1,97 @@
1
+ import { ChatOpenAI } from '@langchain/openai';
2
+ import { StringOutputParser } from '@langchain/core/output_parsers';
3
+
4
+ // This module implements the generation of the copilot response.
5
+
6
+ // Debug flag to log more information during development.
7
+ const debug = false;
8
+
9
+ export const handleAICopilot = () => {
10
+ // These environment variables are used to configure the OpenAI client.
11
+ // See `.env.sample` for the expected values.
12
+ const {
13
+ VZCODE_AI_COPILOT_API_KEY,
14
+ VZCODE_AI_COPILOT_BASE_URL,
15
+ VZCODE_AI_COPILOT_MODEL,
16
+ } = process.env;
17
+
18
+ // If neither of these are not set, the OpenAI client will not be initialized,
19
+ // e.g. for local development where no AI is needed.
20
+ const isCopilotEnabled =
21
+ VZCODE_AI_COPILOT_API_KEY !== undefined &&
22
+ VZCODE_AI_COPILOT_BASE_URL !== undefined &&
23
+ VZCODE_AI_COPILOT_MODEL !== undefined;
24
+
25
+ debug &&
26
+ console.log('isCopilotEnabled: ' + isCopilotEnabled);
27
+
28
+ debug &&
29
+ console.log({
30
+ VZCODE_AI_COPILOT_API_KEY,
31
+ VZCODE_AI_COPILOT_BASE_URL,
32
+ VZCODE_AI_COPILOT_MODEL,
33
+ });
34
+
35
+ let chatModel;
36
+ if (isCopilotEnabled) {
37
+ const options = {
38
+ modelName: VZCODE_AI_COPILOT_MODEL,
39
+ configuration: {
40
+ apiKey: VZCODE_AI_COPILOT_API_KEY,
41
+ baseURL: VZCODE_AI_COPILOT_BASE_URL,
42
+ },
43
+ additionalParameters: {
44
+ provider: { sort: 'throughput' },
45
+ },
46
+ streaming: false,
47
+ };
48
+ debug && console.log('chatModel options:', options);
49
+ chatModel = new ChatOpenAI(options);
50
+ }
51
+
52
+ return async (req, res) => {
53
+ // Don't break if the AI is not enabled.
54
+ // This is useful for local development.
55
+ if (!isCopilotEnabled) {
56
+ res.status(200).send({ text: '' });
57
+ return;
58
+ }
59
+ const { prefix, suffix } = req.body;
60
+
61
+ const messages = [
62
+ {
63
+ role: 'user',
64
+ content: [
65
+ 'Complete the missing part of the text below:\n\n',
66
+ '\`\`\`\n',
67
+ prefix,
68
+ '<MISSING_PART>',
69
+ suffix,
70
+ '\`\`\`\n\n',
71
+ 'ONLY output the text that replaces <MISSING_PART>.\n\n',
72
+ 'NEVER include \`\`\`javascript or similar code blocks.\n\n',
73
+ ].join(''),
74
+ },
75
+ ];
76
+ if (debug) {
77
+ console.log(
78
+ '[generateAIResponse] prompt:\n```\n' +
79
+ messages[0].content +
80
+ '\n```\n',
81
+ );
82
+ }
83
+ try {
84
+ const result = await chatModel.invoke(messages);
85
+ const parser = new StringOutputParser();
86
+ const text = await parser.invoke(result);
87
+
88
+ res.status(200).send({ text });
89
+ } catch (error) {
90
+ console.error('handleAICopilot error:', error);
91
+ res.status(500).send({
92
+ message: 'Internal Server Error',
93
+ error: error.message,
94
+ });
95
+ }
96
+ };
97
+ };