vzcode 2.2.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/dist/assets/{buildWorker-Dsx6rOdK.js → buildWorker-wLkQ0yz1.js} +64 -64
- package/dist/assets/{index-BM8tDh6M.css → index-BoFeK4GL.css} +1 -1
- package/dist/assets/{index-CpxqGhXj.js → index-DuUjtrt6.js} +150 -150
- package/dist/assets/{worker-BeBZGG1n.js → worker-DHxSPM7N.js} +78 -78
- package/dist/cli.js +113 -0
- package/dist/index.html +2 -2
- package/dist/ot.js +12 -0
- package/dist/randomId.js +14 -0
- package/dist/runCode.js +20 -0
- package/dist/server/aiChatHandler/aiEditing.js +89 -0
- package/dist/server/aiChatHandler/chatOperations.js +360 -0
- package/dist/server/aiChatHandler/errorHandling.js +49 -0
- package/dist/server/aiChatHandler/index.js +112 -0
- package/dist/server/aiChatHandler/llmStreaming.js +284 -0
- package/dist/server/aiChatHandler/validation.js +19 -0
- package/dist/server/computeInitialDocument.js +146 -0
- package/dist/server/config.js +21 -0
- package/dist/server/featureFlags.js +5 -0
- package/dist/server/generateAIResponse.js +129 -0
- package/dist/server/handleAIAssist.js +39 -0
- package/dist/server/handleAIChatMessage.js +2 -0
- package/dist/server/handleAICopilot.js +81 -0
- package/dist/server/index.js +297 -0
- package/dist/server/isDirectory.js +1 -0
- package/dist/server/livekit.js +17 -0
- package/dist/server/prettier.js +90 -0
- package/dist/server/setupEnv.js +7 -0
- package/dist/submitOperation.js +14 -0
- package/dist/types.js +1 -0
- package/dist/utils/fileDiff.js +66 -0
- package/package.json +21 -20
- package/src/cli.ts +208 -0
- package/src/client/App/index.tsx +0 -1
- package/src/client/VZCodeContext/types.ts +3 -4
- package/src/client/VZCodeContext/useVZCodeState.ts +24 -6
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +4 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +2 -66
- package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -15
- package/src/client/VZSidebar/AIChat/index.tsx +91 -7
- package/src/client/VZSidebar/AIChat/styles.scss +19 -0
- package/src/server/aiChatHandler/aiEditing.ts +0 -1
- package/src/server/aiChatHandler/chatOperations.ts +0 -34
- package/src/server/aiChatHandler/index.ts +0 -1
- package/src/server/aiChatHandler/llmStreaming.ts +3 -3
- package/src/server/index.ts +1 -10
- package/src/server/aiChatHandler/undoHandler.ts +0 -97
- package/src/server/handleAIChatUndo.ts +0 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { diff } from './ot.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
|
|
4
|
+
* This is the core logic extracted from useSubmitOperation for reuse across client and server.
|
|
5
|
+
*/
|
|
6
|
+
export const createSubmitOperation = (shareDBDoc) => {
|
|
7
|
+
return (next) => {
|
|
8
|
+
const data = shareDBDoc.data;
|
|
9
|
+
const op = diff(data, next(data));
|
|
10
|
+
if (op && shareDBDoc) {
|
|
11
|
+
shareDBDoc.submitOp(op);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createTwoFilesPatch } from 'diff';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a unified diff for a single file using the diff library
|
|
4
|
+
*/
|
|
5
|
+
export function generateFileUnifiedDiff(fileId, fileName, beforeContent, afterContent) {
|
|
6
|
+
// Only generate diff if there are actual changes
|
|
7
|
+
if (beforeContent === afterContent) {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
// Use the diff library's createTwoFilesPatch function to generate unified diff
|
|
11
|
+
const unifiedDiff = createTwoFilesPatch(fileName, fileName, beforeContent, afterContent, '', '', { context: 3 });
|
|
12
|
+
return unifiedDiff;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Generate unified diffs for multiple files
|
|
16
|
+
*/
|
|
17
|
+
export function generateFilesUnifiedDiff(beforeFiles, afterFiles) {
|
|
18
|
+
const result = {};
|
|
19
|
+
const allFileIds = new Set([
|
|
20
|
+
...Object.keys(beforeFiles),
|
|
21
|
+
...Object.keys(afterFiles),
|
|
22
|
+
]);
|
|
23
|
+
for (const fileId of allFileIds) {
|
|
24
|
+
const beforeFile = beforeFiles[fileId];
|
|
25
|
+
const afterFile = afterFiles[fileId];
|
|
26
|
+
const beforeContent = beforeFile?.text || '';
|
|
27
|
+
const afterContent = afterFile?.text || '';
|
|
28
|
+
const fileName = afterFile?.name || beforeFile?.name || fileId;
|
|
29
|
+
const unifiedDiff = generateFileUnifiedDiff(fileId, fileName, beforeContent, afterContent);
|
|
30
|
+
// Only include files that have changes
|
|
31
|
+
if (unifiedDiff) {
|
|
32
|
+
result[fileId] = unifiedDiff;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a snapshot of current files for diff comparison
|
|
39
|
+
*/
|
|
40
|
+
export function createFilesSnapshot(files) {
|
|
41
|
+
return JSON.parse(JSON.stringify(files));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse unified diff to extract basic statistics
|
|
45
|
+
*/
|
|
46
|
+
export function parseUnifiedDiffStats(unifiedDiff) {
|
|
47
|
+
const lines = unifiedDiff.split('\n');
|
|
48
|
+
let additions = 0;
|
|
49
|
+
let deletions = 0;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
52
|
+
additions++;
|
|
53
|
+
}
|
|
54
|
+
else if (line.startsWith('-') &&
|
|
55
|
+
!line.startsWith('---')) {
|
|
56
|
+
deletions++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { additions, deletions };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Combine multiple unified diffs into a single diff string
|
|
63
|
+
*/
|
|
64
|
+
export function combineUnifiedDiffs(unifiedDiffs) {
|
|
65
|
+
return Object.values(unifiedDiffs).join('\n');
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
"bin": "
|
|
7
|
+
"bin": "dist/cli.js",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/**",
|
|
10
10
|
"src/**"
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"prettier": "prettier {*.*,**/*.*} --write",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"dev": "cross-env EDITOR_PORT=5173 concurrently \"npm run test-interactive\" \"vite\"",
|
|
18
|
-
"build": "vite build",
|
|
19
|
-
"build
|
|
18
|
+
"build": "vite build && npm run build:server",
|
|
19
|
+
"build:server": "tsc --project tsconfig.server.json",
|
|
20
|
+
"build-release": "vite build --mode release && npm run build:server",
|
|
20
21
|
"preview": "concurrently \"npm run test-interactive\" \"vite preview\"",
|
|
21
22
|
"tauri": "tauri",
|
|
22
23
|
"tauri:dev": "tauri dev",
|
|
@@ -117,7 +118,7 @@
|
|
|
117
118
|
"@codemirror/state": "^6.5.2",
|
|
118
119
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
119
120
|
"@codemirror/view": "^6.38.1",
|
|
120
|
-
"@langchain/core": "^0.3.
|
|
121
|
+
"@langchain/core": "^0.3.70",
|
|
121
122
|
"@langchain/openai": "^0.6.7",
|
|
122
123
|
"@lezer/highlight": "^1.2.1",
|
|
123
124
|
"@livekit/components-react": "^2.9.14",
|
|
@@ -127,17 +128,17 @@
|
|
|
127
128
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
128
129
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
129
130
|
"@typescript/vfs": "^1.6.1",
|
|
130
|
-
"@uiw/codemirror-theme-abcdef": "^4.
|
|
131
|
-
"@uiw/codemirror-theme-dracula": "^4.
|
|
132
|
-
"@uiw/codemirror-theme-eclipse": "^4.
|
|
133
|
-
"@uiw/codemirror-theme-github": "^4.
|
|
134
|
-
"@uiw/codemirror-theme-material": "^4.
|
|
135
|
-
"@uiw/codemirror-theme-nord": "^4.
|
|
136
|
-
"@uiw/codemirror-theme-okaidia": "^4.
|
|
137
|
-
"@uiw/codemirror-theme-xcode": "^4.
|
|
138
|
-
"@uiw/codemirror-themes": "^4.
|
|
131
|
+
"@uiw/codemirror-theme-abcdef": "^4.25.1",
|
|
132
|
+
"@uiw/codemirror-theme-dracula": "^4.25.1",
|
|
133
|
+
"@uiw/codemirror-theme-eclipse": "^4.25.1",
|
|
134
|
+
"@uiw/codemirror-theme-github": "^4.25.1",
|
|
135
|
+
"@uiw/codemirror-theme-material": "^4.25.1",
|
|
136
|
+
"@uiw/codemirror-theme-nord": "^4.25.1",
|
|
137
|
+
"@uiw/codemirror-theme-okaidia": "^4.25.1",
|
|
138
|
+
"@uiw/codemirror-theme-xcode": "^4.25.1",
|
|
139
|
+
"@uiw/codemirror-themes": "^4.25.1",
|
|
139
140
|
"@valtown/codemirror-ts": "^2.3.1",
|
|
140
|
-
"@vizhub/runtime": "^4.
|
|
141
|
+
"@vizhub/runtime": "^4.4.0",
|
|
141
142
|
"@vizhub/viz-types": "^0.4.0",
|
|
142
143
|
"@vizhub/viz-utils": "^1.4.0",
|
|
143
144
|
"body-parser": "^2.2.0",
|
|
@@ -153,12 +154,12 @@
|
|
|
153
154
|
"diff2html": "^3.4.52",
|
|
154
155
|
"dotenv": "^17.2.1",
|
|
155
156
|
"editcodewithai": "^2.3.0",
|
|
156
|
-
"eslint-linter-browserify": "^9.
|
|
157
|
+
"eslint-linter-browserify": "^9.33.0",
|
|
157
158
|
"express": "^5.1.0",
|
|
158
159
|
"ignore": "^7.0.5",
|
|
159
160
|
"json0-ot-diff": "^1.1.2",
|
|
160
161
|
"jszip": "^3.10.1",
|
|
161
|
-
"livekit-server-sdk": "^2.13.
|
|
162
|
+
"livekit-server-sdk": "^2.13.2",
|
|
162
163
|
"llm-code-format": "^3.0.0",
|
|
163
164
|
"lucide-react": "^0.539.0",
|
|
164
165
|
"npm": "^11.5.2",
|
|
@@ -177,21 +178,21 @@
|
|
|
177
178
|
"ws": "^8.18.3"
|
|
178
179
|
},
|
|
179
180
|
"devDependencies": {
|
|
180
|
-
"@eslint/js": "^9.
|
|
181
|
+
"@eslint/js": "^9.33.0",
|
|
181
182
|
"@tauri-apps/cli": "^2.7.1",
|
|
182
183
|
"@types/react": "^18",
|
|
183
184
|
"@types/react-dom": "^18",
|
|
184
185
|
"@vitejs/plugin-react": "^5.0.0",
|
|
185
186
|
"concurrently": "^9.2.0",
|
|
186
187
|
"cross-env": "^10.0.0",
|
|
187
|
-
"eslint": "^9.
|
|
188
|
+
"eslint": "^9.33.0",
|
|
188
189
|
"globals": "^16.3.0",
|
|
189
190
|
"npm-check-updates": "^18.0.2",
|
|
190
191
|
"prettier": "^3.6.2",
|
|
191
192
|
"sass": "^1.90.0",
|
|
192
193
|
"ts-node": "^10.9.2",
|
|
193
194
|
"typescript": "^5.9.2",
|
|
194
|
-
"vite": "^7.1.
|
|
195
|
+
"vite": "^7.1.2",
|
|
195
196
|
"vitest": "^3.2.4"
|
|
196
197
|
},
|
|
197
198
|
"optionalDependencies": {
|
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();
|
package/src/client/App/index.tsx
CHANGED
|
@@ -103,7 +103,6 @@ export type VZCodeContextValue = {
|
|
|
103
103
|
aiChatMode: 'ask' | 'edit';
|
|
104
104
|
setAIChatMode: (mode: 'ask' | 'edit') => void;
|
|
105
105
|
aiChatEndpoint?: string;
|
|
106
|
-
aiChatUndoEndpoint?: string;
|
|
107
106
|
aiChatOptions?: { [key: string]: any };
|
|
108
107
|
setSearchResults: (files: ShareDBDoc<VizContent>) => void;
|
|
109
108
|
setSearchFileVisibility: (
|
|
@@ -173,6 +172,8 @@ export type VZCodeContextValue = {
|
|
|
173
172
|
setAIChatMessage: (message: string) => void;
|
|
174
173
|
isLoading: boolean;
|
|
175
174
|
currentChatId: string;
|
|
175
|
+
selectedChatId: string | null;
|
|
176
|
+
setSelectedChatId: (chatId: string | null) => void;
|
|
176
177
|
aiErrorMessage: string | null;
|
|
177
178
|
setAIErrorMessage: (state: string | null) => void;
|
|
178
179
|
handleSendMessage: (
|
|
@@ -201,7 +202,6 @@ export type VZCodeContextValue = {
|
|
|
201
202
|
additionalWidgets?: React.ComponentType<{
|
|
202
203
|
messageId: string;
|
|
203
204
|
chatId: string;
|
|
204
|
-
canUndo: boolean;
|
|
205
205
|
handleSendMessage?: (
|
|
206
206
|
messageToSend?: string,
|
|
207
207
|
options?: Record<string, string>,
|
|
@@ -228,7 +228,6 @@ export interface VZCodeProviderProps {
|
|
|
228
228
|
liveKitConnection?: boolean;
|
|
229
229
|
setLiveKitConnection?: (state: boolean) => void;
|
|
230
230
|
aiChatEndpoint?: string;
|
|
231
|
-
aiChatUndoEndpoint?: string;
|
|
232
231
|
aiChatOptions?: { [key: string]: any };
|
|
233
232
|
autoForkAndRetryAI?: (
|
|
234
233
|
prompt: string,
|
|
@@ -243,7 +242,7 @@ export interface VZCodeProviderProps {
|
|
|
243
242
|
additionalWidgets?: React.ComponentType<{
|
|
244
243
|
messageId: string;
|
|
245
244
|
chatId: string;
|
|
246
|
-
canUndo: boolean;
|
|
247
245
|
}>;
|
|
248
246
|
iframeRef?: React.MutableRefObject<HTMLIFrameElement>;
|
|
247
|
+
handleChatError?: (error: string) => void;
|
|
249
248
|
}
|
|
@@ -46,13 +46,13 @@ export const useVZCodeState = ({
|
|
|
46
46
|
liveKitConnection,
|
|
47
47
|
setLiveKitConnection,
|
|
48
48
|
aiChatEndpoint,
|
|
49
|
-
aiChatUndoEndpoint,
|
|
50
49
|
aiChatOptions,
|
|
51
50
|
autoForkAndRetryAI,
|
|
52
51
|
clearStoredAIPrompt,
|
|
53
52
|
getStoredAIPrompt,
|
|
54
53
|
additionalWidgets,
|
|
55
54
|
iframeRef: externalIframeRef,
|
|
55
|
+
handleChatError,
|
|
56
56
|
}: Omit<
|
|
57
57
|
VZCodeProviderProps,
|
|
58
58
|
'children'
|
|
@@ -272,7 +272,13 @@ export const useVZCodeState = ({
|
|
|
272
272
|
|
|
273
273
|
// Compute isLoading based on the current chat's aiStatus
|
|
274
274
|
const [currentChatId] = useState(() => uuidv4());
|
|
275
|
-
const
|
|
275
|
+
const [selectedChatId, setSelectedChatId] = useState<
|
|
276
|
+
string | null
|
|
277
|
+
>(null);
|
|
278
|
+
|
|
279
|
+
// Use selectedChatId if available, otherwise use currentChatId for backward compatibility
|
|
280
|
+
const activeChatId = selectedChatId || currentChatId;
|
|
281
|
+
const currentChat = content?.chats?.[activeChatId];
|
|
276
282
|
const isLoading = currentChat?.aiStatus === 'generating';
|
|
277
283
|
|
|
278
284
|
// Message history for up/down arrow navigation - using ShareDB document data
|
|
@@ -287,10 +293,21 @@ export const useVZCodeState = ({
|
|
|
287
293
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
288
294
|
const [currentDraft, setCurrentDraft] = useState('');
|
|
289
295
|
|
|
290
|
-
const [aiErrorMessage,
|
|
296
|
+
const [aiErrorMessage, setAIErrorMessageState] = useState<
|
|
291
297
|
string | null
|
|
292
298
|
>(null);
|
|
293
299
|
|
|
300
|
+
// Create the combined setAIErrorMessage function that calls both state setter and external callback
|
|
301
|
+
const setAIErrorMessage = useCallback(
|
|
302
|
+
(error: string | null) => {
|
|
303
|
+
setAIErrorMessageState(error);
|
|
304
|
+
if (error && handleChatError) {
|
|
305
|
+
handleChatError(error);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
[handleChatError],
|
|
309
|
+
);
|
|
310
|
+
|
|
294
311
|
// Message history navigation functions
|
|
295
312
|
const navigateMessageHistoryUp = useCallback(() => {
|
|
296
313
|
if (messageHistory.length === 0) return;
|
|
@@ -378,7 +395,7 @@ export const useVZCodeState = ({
|
|
|
378
395
|
body: JSON.stringify({
|
|
379
396
|
...aiChatOptions,
|
|
380
397
|
content: messageContent.trim(),
|
|
381
|
-
chatId:
|
|
398
|
+
chatId: activeChatId,
|
|
382
399
|
mode: aiChatMode,
|
|
383
400
|
...options,
|
|
384
401
|
}),
|
|
@@ -441,7 +458,7 @@ export const useVZCodeState = ({
|
|
|
441
458
|
isLoading,
|
|
442
459
|
aiChatEndpoint,
|
|
443
460
|
aiChatOptions,
|
|
444
|
-
|
|
461
|
+
activeChatId,
|
|
445
462
|
aiChatMode,
|
|
446
463
|
autoForkAndRetryAI,
|
|
447
464
|
],
|
|
@@ -497,7 +514,6 @@ export const useVZCodeState = ({
|
|
|
497
514
|
aiChatMode: state.aiChatMode,
|
|
498
515
|
setAIChatMode,
|
|
499
516
|
aiChatEndpoint,
|
|
500
|
-
aiChatUndoEndpoint,
|
|
501
517
|
aiChatOptions,
|
|
502
518
|
|
|
503
519
|
isSettingsOpen,
|
|
@@ -564,6 +580,8 @@ export const useVZCodeState = ({
|
|
|
564
580
|
setAIChatMessage,
|
|
565
581
|
isLoading,
|
|
566
582
|
currentChatId,
|
|
583
|
+
selectedChatId,
|
|
584
|
+
setSelectedChatId,
|
|
567
585
|
aiErrorMessage,
|
|
568
586
|
setAIErrorMessage,
|
|
569
587
|
handleSendMessage,
|
|
@@ -60,6 +60,10 @@ const ChatInputComponent = ({
|
|
|
60
60
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
61
61
|
event.preventDefault();
|
|
62
62
|
onSendMessage();
|
|
63
|
+
} else if (event.key === 'Enter' && event.shiftKey) {
|
|
64
|
+
// Shift+Enter should add a newline, not trigger run code
|
|
65
|
+
// We prevent the event from bubbling up to the global keyboard handler
|
|
66
|
+
event.stopPropagation();
|
|
63
67
|
} else if (event.key === 'ArrowUp') {
|
|
64
68
|
// Only navigate history if cursor is at the beginning of the first line
|
|
65
69
|
const textarea =
|
|
@@ -19,9 +19,7 @@ interface MessageProps {
|
|
|
19
19
|
timestamp: number;
|
|
20
20
|
isStreaming?: boolean;
|
|
21
21
|
diffData?: UnifiedFilesDiff;
|
|
22
|
-
beforeFiles?: any;
|
|
23
22
|
chatId?: string;
|
|
24
|
-
canUndo?: boolean;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
const MessageComponent = ({
|
|
@@ -31,17 +29,10 @@ const MessageComponent = ({
|
|
|
31
29
|
timestamp,
|
|
32
30
|
isStreaming,
|
|
33
31
|
diffData,
|
|
34
|
-
beforeFiles,
|
|
35
32
|
chatId,
|
|
36
|
-
canUndo,
|
|
37
33
|
}: MessageProps) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
aiChatUndoEndpoint,
|
|
41
|
-
aiChatOptions = {},
|
|
42
|
-
additionalWidgets,
|
|
43
|
-
handleSendMessage,
|
|
44
|
-
} = useContext(VZCodeContext);
|
|
34
|
+
const { additionalWidgets, handleSendMessage } =
|
|
35
|
+
useContext(VZCodeContext);
|
|
45
36
|
|
|
46
37
|
// Memoize date formatting to avoid repeated computation
|
|
47
38
|
const formattedTime = useMemo(() => {
|
|
@@ -59,47 +50,6 @@ const MessageComponent = ({
|
|
|
59
50
|
return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
|
|
60
51
|
}, [role, isStreaming]);
|
|
61
52
|
|
|
62
|
-
const handleUndo = async () => {
|
|
63
|
-
if (
|
|
64
|
-
!id ||
|
|
65
|
-
!chatId ||
|
|
66
|
-
!beforeFiles ||
|
|
67
|
-
isUndoing ||
|
|
68
|
-
!aiChatUndoEndpoint
|
|
69
|
-
) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
setIsUndoing(true);
|
|
74
|
-
try {
|
|
75
|
-
const response = await fetch(aiChatUndoEndpoint, {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: {
|
|
78
|
-
'Content-Type': 'application/json',
|
|
79
|
-
},
|
|
80
|
-
body: JSON.stringify({
|
|
81
|
-
vizId: aiChatOptions.vizId,
|
|
82
|
-
chatId,
|
|
83
|
-
messageId: id,
|
|
84
|
-
}),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
`HTTP error! status: ${response.status}`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// The server will handle the ShareDB operations to undo the changes
|
|
94
|
-
// The UI will update automatically via ShareDB
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error('Error undoing AI edit:', error);
|
|
97
|
-
// TODO: Show user-friendly error message
|
|
98
|
-
} finally {
|
|
99
|
-
setIsUndoing(false);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
53
|
return (
|
|
104
54
|
<div className={messageClassName}>
|
|
105
55
|
<div className="ai-chat-message-content">
|
|
@@ -112,26 +62,12 @@ const MessageComponent = ({
|
|
|
112
62
|
<DiffView diffData={diffData} />
|
|
113
63
|
)}
|
|
114
64
|
{additionalWidgets &&
|
|
115
|
-
canUndo &&
|
|
116
65
|
chatId &&
|
|
117
66
|
React.createElement(additionalWidgets, {
|
|
118
67
|
messageId: id,
|
|
119
68
|
chatId: chatId,
|
|
120
|
-
canUndo: canUndo,
|
|
121
69
|
handleSendMessage,
|
|
122
70
|
})}
|
|
123
|
-
{!additionalWidgets && canUndo && beforeFiles && (
|
|
124
|
-
<div className="undo-button-container">
|
|
125
|
-
<button
|
|
126
|
-
className="undo-button"
|
|
127
|
-
onClick={handleUndo}
|
|
128
|
-
disabled={isUndoing}
|
|
129
|
-
title="Undo this AI edit"
|
|
130
|
-
>
|
|
131
|
-
{isUndoing ? 'Undoing...' : 'Undo'}
|
|
132
|
-
</button>
|
|
133
|
-
</div>
|
|
134
|
-
)}
|
|
135
71
|
</div>
|
|
136
72
|
<div className="ai-chat-message-time">
|
|
137
73
|
{formattedTime}
|
|
@@ -161,19 +161,6 @@ const MessageListComponent = ({
|
|
|
161
161
|
onScroll={handleScroll}
|
|
162
162
|
>
|
|
163
163
|
{messages.map((msg, index) => {
|
|
164
|
-
// Only the most recent assistant message with diffData can be undone
|
|
165
|
-
const isLastAssistantMessage =
|
|
166
|
-
msg.role === 'assistant' &&
|
|
167
|
-
index === messages.length - 1 &&
|
|
168
|
-
!isLoading; // Can't undo while AI is still generating
|
|
169
|
-
|
|
170
|
-
const canUndo =
|
|
171
|
-
isLastAssistantMessage &&
|
|
172
|
-
(msg as any).diffData &&
|
|
173
|
-
(msg as any).beforeFiles &&
|
|
174
|
-
Object.keys((msg as any).diffData || {}).length >
|
|
175
|
-
0;
|
|
176
|
-
|
|
177
164
|
return (
|
|
178
165
|
<Message
|
|
179
166
|
key={msg.id}
|
|
@@ -182,9 +169,7 @@ const MessageListComponent = ({
|
|
|
182
169
|
content={msg.content}
|
|
183
170
|
timestamp={msg.timestamp}
|
|
184
171
|
diffData={(msg as any).diffData}
|
|
185
|
-
beforeFiles={(msg as any).beforeFiles}
|
|
186
172
|
chatId={chatId}
|
|
187
|
-
canUndo={canUndo}
|
|
188
173
|
/>
|
|
189
174
|
);
|
|
190
175
|
})}
|