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,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,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,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
|
+
};
|