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
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": "dist/cli.js",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist/**"
|
|
9
|
+
"dist/**",
|
|
10
|
+
"src/**"
|
|
10
11
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"test": "vitest run",
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
// Get the directory of the current module
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
// Function to find the Tauri binary
|
|
13
|
+
function findTauriApp(): string | null {
|
|
14
|
+
// Look for the Tauri binary in common locations
|
|
15
|
+
const possiblePaths = [
|
|
16
|
+
// Development location (when running from source)
|
|
17
|
+
path.join(
|
|
18
|
+
__dirname,
|
|
19
|
+
'..',
|
|
20
|
+
'src-tauri',
|
|
21
|
+
'target',
|
|
22
|
+
'debug',
|
|
23
|
+
'vzcode',
|
|
24
|
+
),
|
|
25
|
+
path.join(
|
|
26
|
+
__dirname,
|
|
27
|
+
'..',
|
|
28
|
+
'src-tauri',
|
|
29
|
+
'target',
|
|
30
|
+
'debug',
|
|
31
|
+
'vzcode.exe',
|
|
32
|
+
),
|
|
33
|
+
// Release location (when running from built package)
|
|
34
|
+
path.join(
|
|
35
|
+
__dirname,
|
|
36
|
+
'..',
|
|
37
|
+
'src-tauri',
|
|
38
|
+
'target',
|
|
39
|
+
'release',
|
|
40
|
+
'vzcode',
|
|
41
|
+
),
|
|
42
|
+
path.join(
|
|
43
|
+
__dirname,
|
|
44
|
+
'..',
|
|
45
|
+
'src-tauri',
|
|
46
|
+
'target',
|
|
47
|
+
'release',
|
|
48
|
+
'vzcode.exe',
|
|
49
|
+
),
|
|
50
|
+
// MacOS app bundle
|
|
51
|
+
path.join(
|
|
52
|
+
__dirname,
|
|
53
|
+
'..',
|
|
54
|
+
'src-tauri',
|
|
55
|
+
'target',
|
|
56
|
+
'debug',
|
|
57
|
+
'vzcode.app',
|
|
58
|
+
'Contents',
|
|
59
|
+
'MacOS',
|
|
60
|
+
'vzcode',
|
|
61
|
+
),
|
|
62
|
+
path.join(
|
|
63
|
+
__dirname,
|
|
64
|
+
'..',
|
|
65
|
+
'src-tauri',
|
|
66
|
+
'target',
|
|
67
|
+
'release',
|
|
68
|
+
'vzcode.app',
|
|
69
|
+
'Contents',
|
|
70
|
+
'MacOS',
|
|
71
|
+
'vzcode',
|
|
72
|
+
),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const binaryPath of possiblePaths) {
|
|
76
|
+
if (fs.existsSync(binaryPath)) {
|
|
77
|
+
return binaryPath;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Function to launch the Tauri app
|
|
85
|
+
function launchTauriApp(workingDirectory: string): void {
|
|
86
|
+
const tauriBinary = findTauriApp();
|
|
87
|
+
|
|
88
|
+
if (!tauriBinary) {
|
|
89
|
+
console.error(
|
|
90
|
+
'VZCode desktop app not found. Please build it first with: npm run tauri:build',
|
|
91
|
+
);
|
|
92
|
+
console.error(
|
|
93
|
+
'Falling back to launching browser version...',
|
|
94
|
+
);
|
|
95
|
+
launchBrowserVersion(workingDirectory);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(
|
|
100
|
+
`Launching VZCode desktop app from: ${workingDirectory}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// First start the server in the background
|
|
104
|
+
startServerInBackground(workingDirectory);
|
|
105
|
+
|
|
106
|
+
// Give the server a moment to start, then launch Tauri
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
const child = spawn(tauriBinary, [], {
|
|
109
|
+
cwd: workingDirectory,
|
|
110
|
+
stdio: 'inherit',
|
|
111
|
+
detached: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Allow the parent process to exit while keeping the child running
|
|
115
|
+
child.unref();
|
|
116
|
+
}, 2000);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Function to start the server in the background
|
|
120
|
+
function startServerInBackground(
|
|
121
|
+
workingDirectory: string,
|
|
122
|
+
): void {
|
|
123
|
+
// Start the server process in the background
|
|
124
|
+
const serverPath = path.join(
|
|
125
|
+
__dirname,
|
|
126
|
+
'server',
|
|
127
|
+
'index.js',
|
|
128
|
+
);
|
|
129
|
+
const serverChild = spawn('node', [serverPath], {
|
|
130
|
+
cwd: workingDirectory,
|
|
131
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
132
|
+
detached: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Don't block on the server process
|
|
136
|
+
serverChild.unref();
|
|
137
|
+
|
|
138
|
+
// Log server output for debugging
|
|
139
|
+
if (serverChild.stdout) {
|
|
140
|
+
serverChild.stdout.on('data', (data) => {
|
|
141
|
+
console.log('Server:', data.toString());
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (serverChild.stderr) {
|
|
146
|
+
serverChild.stderr.on('data', (data) => {
|
|
147
|
+
console.error('Server error:', data.toString());
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fallback function to launch the browser version
|
|
153
|
+
async function launchBrowserVersion(
|
|
154
|
+
workingDirectory: string,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
const { default: open } = await import('open');
|
|
157
|
+
const serverModule = await import('./server/index.js');
|
|
158
|
+
|
|
159
|
+
// Note: The server/index.js will use process.cwd() automatically
|
|
160
|
+
// and we're already in the correct working directory
|
|
161
|
+
console.log(
|
|
162
|
+
`Starting VZCode server from: ${workingDirectory}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Main CLI function
|
|
167
|
+
function main(): void {
|
|
168
|
+
const workingDirectory = process.cwd();
|
|
169
|
+
|
|
170
|
+
// Parse command line arguments
|
|
171
|
+
const args = process.argv.slice(2);
|
|
172
|
+
|
|
173
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
174
|
+
console.log('VZCode - Multiplayer Code Editor');
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log('Usage: vzcode [options]');
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log('Options:');
|
|
179
|
+
console.log(' --help, -h Show this help message');
|
|
180
|
+
console.log(
|
|
181
|
+
' --browser Force browser version instead of desktop app',
|
|
182
|
+
);
|
|
183
|
+
console.log(
|
|
184
|
+
' --port=<port> Specify port for server (default: 3030)',
|
|
185
|
+
);
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log('Examples:');
|
|
188
|
+
console.log(
|
|
189
|
+
' vzcode # Launch desktop app in current directory',
|
|
190
|
+
);
|
|
191
|
+
console.log(
|
|
192
|
+
' vzcode --browser # Launch browser version',
|
|
193
|
+
);
|
|
194
|
+
console.log(
|
|
195
|
+
' vzcode --port=4000 # Start server on port 4000',
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (args.includes('--browser')) {
|
|
201
|
+
launchBrowserVersion(workingDirectory);
|
|
202
|
+
} else {
|
|
203
|
+
launchTauriApp(workingDirectory);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Run the CLI
|
|
208
|
+
main();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
2
|
+
import { ShareDBDoc, TabState } from '../../types';
|
|
3
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
4
|
+
|
|
5
|
+
export const AIAssistCodeMirrorKeyMap = ({
|
|
6
|
+
shareDBDoc,
|
|
7
|
+
fileId,
|
|
8
|
+
tabList,
|
|
9
|
+
}: {
|
|
10
|
+
shareDBDoc: ShareDBDoc<VizContent>;
|
|
11
|
+
fileId: string;
|
|
12
|
+
tabList: Array<TabState>;
|
|
13
|
+
}) =>
|
|
14
|
+
keymap.of([
|
|
15
|
+
{
|
|
16
|
+
key: 'control-m',
|
|
17
|
+
run: (view: EditorView) => {
|
|
18
|
+
// if (
|
|
19
|
+
// shareDBDoc.data.aiStreams === undefined ||
|
|
20
|
+
// shareDBDoc.data.aiStreams[mostRecentStreamId] ===
|
|
21
|
+
// null ||
|
|
22
|
+
// shareDBDoc.data.aiStreams[mostRecentStreamId]
|
|
23
|
+
// ?.AIStreamStatus.serverIsRunning !== true
|
|
24
|
+
// ) {
|
|
25
|
+
|
|
26
|
+
// TODO handle starting it
|
|
27
|
+
// startAIAssist({
|
|
28
|
+
// view,
|
|
29
|
+
// shareDBDoc,
|
|
30
|
+
// fileId,
|
|
31
|
+
// tabList,
|
|
32
|
+
// });
|
|
33
|
+
|
|
34
|
+
// TODO handle stopping it
|
|
35
|
+
// } else {
|
|
36
|
+
// haltAIAssist(shareDBDoc);
|
|
37
|
+
// }
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useCallback, useContext, useState } from 'react';
|
|
2
|
+
import { OverlayTrigger, Tooltip } from '../../bootstrap';
|
|
3
|
+
import { PaneId, TabState } from '../../../types';
|
|
4
|
+
import { VizFileId } from '@vizhub/viz-types';
|
|
5
|
+
import { VZCodeContext } from '../../VZCodeContext';
|
|
6
|
+
import {
|
|
7
|
+
RequestId,
|
|
8
|
+
generateRequestId,
|
|
9
|
+
} from '../../generateRequestId';
|
|
10
|
+
import { SparklesSVG } from '../../Icons';
|
|
11
|
+
import { startAIAssist } from '../startAIAssist';
|
|
12
|
+
import { editorCacheKey } from '../../useEditorCache';
|
|
13
|
+
import { Spinner } from '../Spinner';
|
|
14
|
+
import './style.scss';
|
|
15
|
+
|
|
16
|
+
const enableStopGeneration = false;
|
|
17
|
+
|
|
18
|
+
export const AIAssistWidget = ({
|
|
19
|
+
aiAssistEndpoint,
|
|
20
|
+
aiAssistOptions,
|
|
21
|
+
aiAssistTooltipText = 'Start AI Assist',
|
|
22
|
+
aiAssistClickOverride,
|
|
23
|
+
activeFileId,
|
|
24
|
+
tabList,
|
|
25
|
+
paneId,
|
|
26
|
+
}: {
|
|
27
|
+
aiAssistEndpoint: string;
|
|
28
|
+
aiAssistOptions: { [key: string]: string };
|
|
29
|
+
aiAssistTooltipText?: string;
|
|
30
|
+
aiAssistClickOverride?: () => void;
|
|
31
|
+
activeFileId: VizFileId;
|
|
32
|
+
tabList: Array<TabState>;
|
|
33
|
+
paneId: PaneId;
|
|
34
|
+
}) => {
|
|
35
|
+
const {
|
|
36
|
+
shareDBDoc,
|
|
37
|
+
editorCache,
|
|
38
|
+
runPrettierRef,
|
|
39
|
+
runCodeRef,
|
|
40
|
+
} = useContext(VZCodeContext);
|
|
41
|
+
|
|
42
|
+
// The stream ID of the most recent request.
|
|
43
|
+
// * If `null`, no request has been made yet.
|
|
44
|
+
// * If non-null, a request has been made, and the
|
|
45
|
+
// server is processing it.
|
|
46
|
+
const [aiStreamId, setAiStreamId] =
|
|
47
|
+
useState<RequestId | null>(null);
|
|
48
|
+
|
|
49
|
+
const handleClick = useCallback(async () => {
|
|
50
|
+
const isCurrentlyGenerationg = aiStreamId !== null;
|
|
51
|
+
if (isCurrentlyGenerationg) {
|
|
52
|
+
// TODO stop generation
|
|
53
|
+
} else {
|
|
54
|
+
const newAiStreamId = generateRequestId();
|
|
55
|
+
setAiStreamId(newAiStreamId);
|
|
56
|
+
|
|
57
|
+
// Wait for generation to finish.
|
|
58
|
+
await startAIAssist({
|
|
59
|
+
view: editorCache.get(
|
|
60
|
+
editorCacheKey(activeFileId, paneId),
|
|
61
|
+
).editor,
|
|
62
|
+
shareDBDoc,
|
|
63
|
+
fileId: activeFileId,
|
|
64
|
+
tabList,
|
|
65
|
+
aiStreamId: newAiStreamId,
|
|
66
|
+
aiAssistEndpoint,
|
|
67
|
+
aiAssistOptions,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Introduce a delay before running Prettier
|
|
71
|
+
// and the code, to give the ShareDB ops a chance
|
|
72
|
+
// to be applied and to propagate via WebSockets
|
|
73
|
+
// to the client. This is necessary because sometimes
|
|
74
|
+
// the ShareDB ops from the AI assist actually don't
|
|
75
|
+
// reach the client until AFTER the response comes back
|
|
76
|
+
// from `await startAIAssist`.
|
|
77
|
+
await new Promise((resolve) =>
|
|
78
|
+
setTimeout(resolve, 1000),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Trigger a Prettier run after the AI Assist.
|
|
82
|
+
const runPrettier = runPrettierRef.current;
|
|
83
|
+
if (runPrettier !== null) {
|
|
84
|
+
runPrettier();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Run the code after the AI Assist.
|
|
88
|
+
const runCode = runCodeRef.current;
|
|
89
|
+
if (runCode !== null) {
|
|
90
|
+
runCode();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handles the case that the user has started,
|
|
94
|
+
// stopped, and started again before the first request
|
|
95
|
+
// has finished, by comparing the current stream ID
|
|
96
|
+
// with the stream ID that was active when the request
|
|
97
|
+
// was started.
|
|
98
|
+
setAiStreamId((currentAIStreamId) =>
|
|
99
|
+
currentAIStreamId === newAiStreamId
|
|
100
|
+
? null
|
|
101
|
+
: currentAIStreamId,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}, [activeFileId, paneId, aiStreamId]);
|
|
105
|
+
|
|
106
|
+
const showWidget = enableStopGeneration
|
|
107
|
+
? true
|
|
108
|
+
: aiStreamId === null;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="vz-code-ai-assist-widget">
|
|
112
|
+
{aiStreamId ? (
|
|
113
|
+
<Spinner /> // Show spinner when aiStreamId is not null
|
|
114
|
+
) : (
|
|
115
|
+
showWidget && (
|
|
116
|
+
<OverlayTrigger
|
|
117
|
+
placement="left"
|
|
118
|
+
overlay={
|
|
119
|
+
<Tooltip id="ai-assist-widget-tooltip">
|
|
120
|
+
{aiAssistTooltipText}
|
|
121
|
+
</Tooltip>
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
<i
|
|
125
|
+
className="icon-button icon-button-dark"
|
|
126
|
+
onClick={aiAssistClickOverride || handleClick}
|
|
127
|
+
>
|
|
128
|
+
<SparklesSVG />
|
|
129
|
+
</i>
|
|
130
|
+
</OverlayTrigger>
|
|
131
|
+
)
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
.vz-code-ai-assist-widget {
|
|
2
|
+
position: absolute;
|
|
3
|
+
// bottom: 32px;
|
|
4
|
+
// right: 32px;
|
|
5
|
+
bottom: 16px;
|
|
6
|
+
right: 16px;
|
|
7
|
+
color: var(--vh-color-caution-01);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.vh-spinner {
|
|
11
|
+
display: flex;
|
|
12
|
+
height: 100%;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Fade in after a delay */
|
|
20
|
+
/* https://stackoverflow.com/questions/29846224/css-animation-with-delay-and-opacity */
|
|
21
|
+
.vh-spinner.fade-in svg {
|
|
22
|
+
animation: 1s linear 0s normal forwards 1 fadein;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes fadein {
|
|
26
|
+
0% {
|
|
27
|
+
opacity: 0;
|
|
28
|
+
}
|
|
29
|
+
50% {
|
|
30
|
+
opacity: 0;
|
|
31
|
+
}
|
|
32
|
+
100% {
|
|
33
|
+
opacity: 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Infinite rotation */
|
|
38
|
+
/* https://stackoverflow.com/questions/6410730/css-endless-rotation-animation */
|
|
39
|
+
.vh-spinner-dots {
|
|
40
|
+
animation: rotate 3s linear infinite;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@keyframes rotate {
|
|
44
|
+
to {
|
|
45
|
+
transform: rotate(360deg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// From https://github.com/vizhub-core/vizhub/commit/6ca078935b9771dc6e05a6065bcfd522a4724dcf
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import './AIAssistWidget/style.scss';
|
|
4
|
+
|
|
5
|
+
// const minRadius = 4;
|
|
6
|
+
// const maxRadius = 10;
|
|
7
|
+
// const numDots = 8;
|
|
8
|
+
// const wheelRadius = 40;
|
|
9
|
+
// const round = (number) => Math.floor(number * 100) / 100;
|
|
10
|
+
// const dots = [];
|
|
11
|
+
// for (let i = 0; i < numDots; i++) {
|
|
12
|
+
// const angle = (i / numDots) * Math.PI * 2;
|
|
13
|
+
// dots.push({
|
|
14
|
+
// x: round(Math.cos(angle) * wheelRadius),
|
|
15
|
+
// y: round(Math.sin(angle) * wheelRadius),
|
|
16
|
+
// r: round(
|
|
17
|
+
// minRadius +
|
|
18
|
+
// (i / (numDots - 1)) * (maxRadius - minRadius),
|
|
19
|
+
// ),
|
|
20
|
+
// });
|
|
21
|
+
// }
|
|
22
|
+
// console.log(JSON.stringify(dots));
|
|
23
|
+
|
|
24
|
+
// Hardcoded to avoid the computation.
|
|
25
|
+
const dots = [
|
|
26
|
+
{ x: 40, y: 0, r: 4 },
|
|
27
|
+
{ x: 28.28, y: 28.28, r: 4.85 },
|
|
28
|
+
{ x: 0, y: 40, r: 5.71 },
|
|
29
|
+
{ x: -28.29, y: 28.28, r: 6.57 },
|
|
30
|
+
{ x: -40, y: 0, r: 7.42 },
|
|
31
|
+
{ x: -28.29, y: -28.29, r: 8.28 },
|
|
32
|
+
{ x: -0.01, y: -40, r: 9.14 },
|
|
33
|
+
{ x: 28.28, y: -28.29, r: 10 },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// From https://bl.ocks.org/curran/685fa8300650c4324d571c6b0ecc55de
|
|
37
|
+
// And vizhub-v2/packages/neoFrontend/src/LoadingScreen/index.js
|
|
38
|
+
export const Spinner = ({
|
|
39
|
+
height = 40,
|
|
40
|
+
fill = 'currentcolor',
|
|
41
|
+
fadeIn = true,
|
|
42
|
+
}) => {
|
|
43
|
+
const [show, setShow] = useState(false);
|
|
44
|
+
|
|
45
|
+
// Avoid SSR of all the SVG elements.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setShow(true);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
show && (
|
|
52
|
+
<div
|
|
53
|
+
className={`vh-spinner${fadeIn ? ' fade-in' : ''}`}
|
|
54
|
+
>
|
|
55
|
+
<svg
|
|
56
|
+
height={height}
|
|
57
|
+
viewBox="0 0 100 100"
|
|
58
|
+
style={{ opacity: 1 }}
|
|
59
|
+
>
|
|
60
|
+
<g transform="translate(50, 50)" fill={fill}>
|
|
61
|
+
<g className="vh-spinner-dots">
|
|
62
|
+
{dots.map(({ x, y, r }, i) => (
|
|
63
|
+
<circle
|
|
64
|
+
key={i}
|
|
65
|
+
cx={x}
|
|
66
|
+
cy={y}
|
|
67
|
+
r={r}
|
|
68
|
+
></circle>
|
|
69
|
+
))}
|
|
70
|
+
</g>
|
|
71
|
+
</g>
|
|
72
|
+
</svg>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { VizFile } from '@vizhub/viz-types';
|
|
2
|
+
|
|
3
|
+
const maxFileNameLength = 100;
|
|
4
|
+
const maxFileTextLength = 2000;
|
|
5
|
+
|
|
6
|
+
// Formats a file for the prompt.
|
|
7
|
+
export const formatFile = (
|
|
8
|
+
file: VizFile,
|
|
9
|
+
truncateText = true,
|
|
10
|
+
) => {
|
|
11
|
+
const nameTruncated = file.name
|
|
12
|
+
?.substring(0, maxFileNameLength)
|
|
13
|
+
.trim();
|
|
14
|
+
|
|
15
|
+
const textTruncated = (
|
|
16
|
+
truncateText
|
|
17
|
+
? file.text?.substring(0, maxFileTextLength)
|
|
18
|
+
: file.text
|
|
19
|
+
).trim();
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
'`' + nameTruncated + '`:',
|
|
23
|
+
'```',
|
|
24
|
+
textTruncated,
|
|
25
|
+
'```',
|
|
26
|
+
].join('\n');
|
|
27
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// import { File } from '../../types';
|
|
2
|
+
|
|
3
|
+
// const maxFileNameLength = 100;
|
|
4
|
+
// const maxFileTextLength = 2000;
|
|
5
|
+
|
|
6
|
+
// export const generateFilesContext = (
|
|
7
|
+
// goodFiles: Array<File>,
|
|
8
|
+
// ): string => {
|
|
9
|
+
// const input = goodFiles
|
|
10
|
+
// .map((file) => {
|
|
11
|
+
// const nameSubstring = file.name
|
|
12
|
+
// ?.substring(0, maxFileNameLength)
|
|
13
|
+
// .trim();
|
|
14
|
+
|
|
15
|
+
// const textSubstring = file.text
|
|
16
|
+
// ?.substring(0, maxFileTextLength)
|
|
17
|
+
// .trim();
|
|
18
|
+
|
|
19
|
+
// // Generate Markdown that AI will understand.
|
|
20
|
+
// return `File \`${nameSubstring}\`:\n\`\`\`${textSubstring}\`\`\``;
|
|
21
|
+
// })
|
|
22
|
+
// .join('\n\n');
|
|
23
|
+
|
|
24
|
+
// return input;
|
|
25
|
+
// };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { EditorView } from 'codemirror';
|
|
2
|
+
import { ShareDBDoc, TabState } from '../../types';
|
|
3
|
+
import {
|
|
4
|
+
VizFile,
|
|
5
|
+
VizFileId,
|
|
6
|
+
VizContent,
|
|
7
|
+
} from '@vizhub/viz-types';
|
|
8
|
+
import { RequestId } from '../generateRequestId';
|
|
9
|
+
import { formatFile } from './formatFile';
|
|
10
|
+
|
|
11
|
+
const debug = false;
|
|
12
|
+
|
|
13
|
+
// Enables inclusion of code from open tabs as context.
|
|
14
|
+
const enableTabContext = false;
|
|
15
|
+
|
|
16
|
+
const defaultAIAssistEndpoint = '/ai-assist';
|
|
17
|
+
|
|
18
|
+
export const startAIAssist = async ({
|
|
19
|
+
view,
|
|
20
|
+
shareDBDoc,
|
|
21
|
+
fileId,
|
|
22
|
+
tabList,
|
|
23
|
+
aiAssistEndpoint = defaultAIAssistEndpoint,
|
|
24
|
+
aiAssistOptions = {},
|
|
25
|
+
aiStreamId,
|
|
26
|
+
}: {
|
|
27
|
+
view: EditorView;
|
|
28
|
+
shareDBDoc: ShareDBDoc<VizContent>;
|
|
29
|
+
fileId: VizFileId;
|
|
30
|
+
tabList: Array<TabState>;
|
|
31
|
+
aiAssistEndpoint?: string;
|
|
32
|
+
aiAssistOptions?: {
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
};
|
|
35
|
+
aiStreamId: RequestId;
|
|
36
|
+
}) => {
|
|
37
|
+
const currentFileName =
|
|
38
|
+
shareDBDoc.data.files[fileId].name;
|
|
39
|
+
|
|
40
|
+
// Generate a version of the current file with a
|
|
41
|
+
// <FILL_ME> in the middle.
|
|
42
|
+
const prefix = view.state.sliceDoc(
|
|
43
|
+
0,
|
|
44
|
+
view.state.selection.main.from,
|
|
45
|
+
);
|
|
46
|
+
const suffix = view.state.sliceDoc(
|
|
47
|
+
view.state.selection.main.to,
|
|
48
|
+
);
|
|
49
|
+
const combinedText = prefix + '<FILL_ME>' + suffix;
|
|
50
|
+
const currentFile = {
|
|
51
|
+
name: currentFileName,
|
|
52
|
+
text: combinedText,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const insertionCursor = view.state.selection.main.to;
|
|
56
|
+
|
|
57
|
+
// The main prompt is the current file, plus the context files.
|
|
58
|
+
let inputText = formatFile(currentFile, false);
|
|
59
|
+
|
|
60
|
+
if (enableTabContext) {
|
|
61
|
+
inputText +=
|
|
62
|
+
'\n\n' +
|
|
63
|
+
tabList
|
|
64
|
+
.filter((tabState) => tabState.fileId !== fileId)
|
|
65
|
+
.map((tabState) =>
|
|
66
|
+
formatFile(
|
|
67
|
+
shareDBDoc.data.files[tabState.fileId],
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
.join('\n\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const response = await fetch(aiAssistEndpoint, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
...aiAssistOptions,
|
|
80
|
+
inputText,
|
|
81
|
+
fileId,
|
|
82
|
+
insertionCursor,
|
|
83
|
+
aiStreamId,
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const responseJson = await response.json();
|
|
88
|
+
|
|
89
|
+
if (responseJson.error) {
|
|
90
|
+
console.error(
|
|
91
|
+
'[startAIAssist] error from server:',
|
|
92
|
+
responseJson.error,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
};
|