vzcode 1.16.0 → 1.17.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/index-BQgRA0SD.js +181 -0
- package/dist/assets/index-CSSpCKM0.js +447 -0
- package/dist/assets/index-zzK6rRiK.css +1 -0
- package/dist/assets/worker-8af7bTHW.js +293 -0
- package/dist/index.html +2 -2
- package/package.json +40 -34
- package/src/client/App/index.tsx +52 -20
- package/src/client/App/usePending.ts +50 -0
- package/src/client/App/useShareDB.ts +40 -1
- package/src/client/CodeEditor/Copilot/index.ts +28 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +129 -33
- package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +36 -1
- package/src/client/CodeEditor/getOrCreateEditor.ts +43 -6
- package/src/client/CodeEditor/index.tsx +4 -1
- package/src/client/CodeEditor/rainbowBrackets.ts +14 -2
- package/src/client/CodeEditor/style.scss +106 -1
- package/src/client/Icons/MicSVG.tsx +25 -0
- package/src/client/RunCodeWidget/index.tsx +20 -3
- package/src/client/VZCodeContext.tsx +52 -14
- package/src/client/VZKeyboardShortcutsDoc.tsx +8 -8
- package/src/client/VZLeft.tsx +4 -2
- package/src/client/VZMiddle.tsx +5 -0
- package/src/client/VZSettings.tsx +54 -3
- package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
- package/src/client/VZSidebar/index.tsx +80 -15
- package/src/client/VZSidebar/styles.scss +6 -1
- package/src/client/useTypeScript/worker/constants.ts +6 -0
- package/src/server/generateAIResponse.js +79 -13
- package/src/server/handleAICopilot.js +77 -0
- package/src/server/index.js +23 -8
- package/src/server/livekit.js +24 -0
- package/src/server/setupEnv.js +1 -1
- package/src/types.ts +1 -0
- package/dist/assets/index-B6yTvvah.css +0 -1
- package/dist/assets/index-BlwLvENZ.js +0 -159
- package/dist/assets/index-Bm65AzMP.js +0 -425
- package/dist/assets/worker-CJWrLBN2.js +0 -286
|
@@ -9,8 +9,9 @@ import { Button, Modal, Form } from './bootstrap';
|
|
|
9
9
|
import { ThemeLabel, themes } from './themes';
|
|
10
10
|
import { VZCodeContext } from './VZCodeContext';
|
|
11
11
|
import { fonts } from './Fonts/fonts';
|
|
12
|
+
import { toggleRainbowBrackets } from './CodeEditor/rainbowBrackets';
|
|
12
13
|
|
|
13
|
-
const fontSizes = ['16px', '18px', '20px', '24px'];
|
|
14
|
+
const fontSizes = ['10px','12px','14px','16px', '18px', '20px', '24px'];
|
|
14
15
|
|
|
15
16
|
export const VZSettings = ({
|
|
16
17
|
enableUsernameField = true,
|
|
@@ -29,11 +30,21 @@ export const VZSettings = ({
|
|
|
29
30
|
|
|
30
31
|
const handleThemeChange = useCallback(
|
|
31
32
|
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
32
|
-
|
|
33
|
+
const selectedTheme = event.target.value as ThemeLabel;
|
|
34
|
+
setTheme(selectedTheme);
|
|
35
|
+
localStorage.setItem('vzcodeSelectedTheme', selectedTheme);
|
|
33
36
|
},
|
|
34
37
|
[],
|
|
35
38
|
);
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const savedTheme = localStorage.getItem('vzcodeSelectedTheme') as ThemeLabel | null;
|
|
42
|
+
if (savedTheme) {
|
|
43
|
+
setTheme(savedTheme);
|
|
44
|
+
} else {
|
|
45
|
+
setTheme('vizhub');
|
|
46
|
+
}
|
|
47
|
+
}, [setTheme]);
|
|
37
48
|
// Initialize font and size from localStorage or defaults
|
|
38
49
|
// const [selectedFont, setSelectedFont] = useState(
|
|
39
50
|
// localStorage.getItem('vzcodeSelectedFont') ||
|
|
@@ -48,6 +59,10 @@ export const VZSettings = ({
|
|
|
48
59
|
const [selectedFontSize, setSelectedFontSize] =
|
|
49
60
|
useState('16px');
|
|
50
61
|
|
|
62
|
+
// Initialize rainbowBrackets setting from localStorage or default to 'on'
|
|
63
|
+
const [rainbowBrackets, setRainbowBrackets] =
|
|
64
|
+
useState('on');
|
|
65
|
+
|
|
51
66
|
useEffect(() => {
|
|
52
67
|
// If we're in the browser,
|
|
53
68
|
if (typeof window !== 'undefined') {
|
|
@@ -60,6 +75,12 @@ export const VZSettings = ({
|
|
|
60
75
|
'vzcodeSelectedFontSize',
|
|
61
76
|
);
|
|
62
77
|
|
|
78
|
+
const rainbowBracketsFromLocalStorage:
|
|
79
|
+
| string
|
|
80
|
+
| null = window.localStorage.getItem(
|
|
81
|
+
'vzcodeRainbowBrackets',
|
|
82
|
+
);
|
|
83
|
+
|
|
63
84
|
if (selectedFontFromLocalStorage !== null) {
|
|
64
85
|
setSelectedFont(selectedFontFromLocalStorage);
|
|
65
86
|
}
|
|
@@ -68,6 +89,9 @@ export const VZSettings = ({
|
|
|
68
89
|
selectedFontSizeFromLocalStorage,
|
|
69
90
|
);
|
|
70
91
|
}
|
|
92
|
+
if (rainbowBracketsFromLocalStorage !== null) {
|
|
93
|
+
setRainbowBrackets(rainbowBracketsFromLocalStorage);
|
|
94
|
+
}
|
|
71
95
|
} else {
|
|
72
96
|
// If we're not in the browser, use the default initial width.
|
|
73
97
|
}
|
|
@@ -96,6 +120,17 @@ export const VZSettings = ({
|
|
|
96
120
|
[],
|
|
97
121
|
);
|
|
98
122
|
|
|
123
|
+
// Called when the user changes the Rainbow Brackets setting
|
|
124
|
+
const handleRainbowBracketsChange = useCallback(
|
|
125
|
+
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
126
|
+
const newSetting = event.target.value;
|
|
127
|
+
localStorage.setItem('vzcodeRainbowBrackets', newSetting);
|
|
128
|
+
setRainbowBrackets(newSetting);
|
|
129
|
+
toggleRainbowBrackets(newSetting === 'on');
|
|
130
|
+
},
|
|
131
|
+
[],
|
|
132
|
+
);
|
|
133
|
+
|
|
99
134
|
useEffect(() => {
|
|
100
135
|
document.body.style.setProperty(
|
|
101
136
|
'--vzcode-font-family',
|
|
@@ -220,6 +255,22 @@ export const VZSettings = ({
|
|
|
220
255
|
Select a font size for the editor
|
|
221
256
|
</Form.Text>
|
|
222
257
|
</Form.Group>
|
|
258
|
+
|
|
259
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
260
|
+
<Form.Label>Rainbow Brackets</Form.Label>
|
|
261
|
+
<select
|
|
262
|
+
className="form-select"
|
|
263
|
+
onChange={handleRainbowBracketsChange}
|
|
264
|
+
value={rainbowBrackets}
|
|
265
|
+
>
|
|
266
|
+
<option value="on">On</option>
|
|
267
|
+
<option value="off">Off</option>
|
|
268
|
+
</select>
|
|
269
|
+
<Form.Text className="text-muted">
|
|
270
|
+
Toggle Rainbow Brackets
|
|
271
|
+
</Form.Text>
|
|
272
|
+
</Form.Group>
|
|
273
|
+
|
|
223
274
|
</Modal.Body>
|
|
224
275
|
<Modal.Footer>
|
|
225
276
|
<Button variant="primary" onClick={closeSettings}>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ParticipantLoop,
|
|
3
|
+
ParticipantName,
|
|
4
|
+
useParticipants,
|
|
5
|
+
useRoomContext,
|
|
6
|
+
} from '@livekit/components-react';
|
|
7
|
+
import { useContext, useState } from 'react';
|
|
8
|
+
import { Button, Form, Modal } from 'react-bootstrap';
|
|
9
|
+
import { v4 } from 'uuid';
|
|
10
|
+
import { VZCodeContext } from '../VZCodeContext';
|
|
11
|
+
|
|
12
|
+
export function VoiceChatModal() {
|
|
13
|
+
const {
|
|
14
|
+
liveKitRoomName,
|
|
15
|
+
setLiveKitRoom,
|
|
16
|
+
liveKitToken,
|
|
17
|
+
setLiveKitToken,
|
|
18
|
+
liveKitConnection,
|
|
19
|
+
setLiveKitConnection,
|
|
20
|
+
voiceChatModalOpen,
|
|
21
|
+
setVoiceChatModalOpen,
|
|
22
|
+
} = useContext(VZCodeContext);
|
|
23
|
+
const participants = useParticipants();
|
|
24
|
+
|
|
25
|
+
const [roomName, setRoomName] = useState(
|
|
26
|
+
liveKitRoomName || '',
|
|
27
|
+
);
|
|
28
|
+
const [username, setUsername] = useState('');
|
|
29
|
+
const room = useRoomContext();
|
|
30
|
+
|
|
31
|
+
const handleConnect = () => {
|
|
32
|
+
if (liveKitConnection) {
|
|
33
|
+
setLiveKitConnection(false);
|
|
34
|
+
setLiveKitRoom(`${v4()}`);
|
|
35
|
+
setLiveKitToken('');
|
|
36
|
+
room.disconnect(true);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setLiveKitRoom(roomName);
|
|
40
|
+
const fetchToken = async () => {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(
|
|
43
|
+
`/livekit-token?room=${roomName}&username=${username}`,
|
|
44
|
+
{
|
|
45
|
+
method: 'GET',
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
const tokenResponse = await res.text();
|
|
49
|
+
setLiveKitToken(tokenResponse);
|
|
50
|
+
setLiveKitConnection(true);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error fetching token:', error);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
fetchToken();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleClose = () => {
|
|
60
|
+
setVoiceChatModalOpen(false);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Modal
|
|
65
|
+
className="vz-settings"
|
|
66
|
+
show={voiceChatModalOpen}
|
|
67
|
+
onHide={handleClose}
|
|
68
|
+
animation={false}
|
|
69
|
+
>
|
|
70
|
+
<Modal.Header closeButton>
|
|
71
|
+
<Modal.Title>Connect to LiveKit Room</Modal.Title>
|
|
72
|
+
</Modal.Header>
|
|
73
|
+
<Modal.Body>
|
|
74
|
+
<Form>
|
|
75
|
+
<Form.Group controlId="formRoomName">
|
|
76
|
+
<Form.Label>Room Name</Form.Label>
|
|
77
|
+
<Form.Control
|
|
78
|
+
type="text"
|
|
79
|
+
placeholder="Enter room name"
|
|
80
|
+
value={roomName}
|
|
81
|
+
onChange={(e) => setRoomName(e.target.value)}
|
|
82
|
+
/>
|
|
83
|
+
</Form.Group>
|
|
84
|
+
</Form>
|
|
85
|
+
<Form>
|
|
86
|
+
<Form.Group controlId="formUserName">
|
|
87
|
+
<Form.Label>Username</Form.Label>
|
|
88
|
+
<Form.Control
|
|
89
|
+
type="text"
|
|
90
|
+
placeholder="Enter Username"
|
|
91
|
+
value={username}
|
|
92
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
93
|
+
/>
|
|
94
|
+
</Form.Group>
|
|
95
|
+
</Form>
|
|
96
|
+
<div
|
|
97
|
+
style={{
|
|
98
|
+
display: 'flex',
|
|
99
|
+
flexDirection: 'column',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<b>Users in Room: </b>
|
|
103
|
+
<ParticipantLoop participants={participants}>
|
|
104
|
+
<ParticipantName />
|
|
105
|
+
</ParticipantLoop>
|
|
106
|
+
</div>
|
|
107
|
+
</Modal.Body>
|
|
108
|
+
<Modal.Footer>
|
|
109
|
+
<Button variant="secondary" onClick={handleClose}>
|
|
110
|
+
Cancel
|
|
111
|
+
</Button>
|
|
112
|
+
<Button
|
|
113
|
+
variant={liveKitConnection ? 'danger' : 'primary'}
|
|
114
|
+
onClick={handleConnect}
|
|
115
|
+
>
|
|
116
|
+
{liveKitConnection ? 'Disconnect' : 'Connect'}
|
|
117
|
+
</Button>
|
|
118
|
+
</Modal.Footer>
|
|
119
|
+
</Modal>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useEffect,
|
|
5
5
|
useMemo,
|
|
6
6
|
useState,
|
|
7
|
+
useRef
|
|
7
8
|
} from 'react';
|
|
8
9
|
import {
|
|
9
10
|
FileId,
|
|
@@ -13,25 +14,26 @@ import {
|
|
|
13
14
|
PresenceId,
|
|
14
15
|
PresenceIndicator,
|
|
15
16
|
} from '../../types';
|
|
16
|
-
import {
|
|
17
|
-
import { Search } from './Search';
|
|
17
|
+
import { OverlayTrigger, Tooltip } from '../bootstrap';
|
|
18
18
|
import { getFileTree } from '../getFileTree';
|
|
19
|
-
import { sortFileTree } from '../sortFileTree';
|
|
20
|
-
import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
|
|
21
19
|
import {
|
|
22
|
-
FolderSVG,
|
|
23
|
-
SearchSVG,
|
|
24
20
|
BugSVG,
|
|
21
|
+
FileSVG,
|
|
22
|
+
FolderSVG,
|
|
25
23
|
GearSVG,
|
|
26
24
|
NewSVG,
|
|
27
|
-
FileSVG,
|
|
28
|
-
QuestionMarkSVG,
|
|
29
25
|
PinSVG,
|
|
26
|
+
QuestionMarkSVG,
|
|
27
|
+
SearchSVG,
|
|
30
28
|
} from '../Icons';
|
|
29
|
+
import { MicSVG } from '../Icons/MicSVG';
|
|
30
|
+
import { sortFileTree } from '../sortFileTree';
|
|
31
|
+
import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
|
|
31
32
|
import { VZCodeContext } from '../VZCodeContext';
|
|
32
33
|
import { Listing } from './Listing';
|
|
33
|
-
import {
|
|
34
|
+
import { Search } from './Search';
|
|
34
35
|
import './styles.scss';
|
|
36
|
+
import { useDragAndDrop } from './useDragAndDrop';
|
|
35
37
|
|
|
36
38
|
// TODO turn this UI back on when we are actually detecting
|
|
37
39
|
// the connection status.
|
|
@@ -97,6 +99,11 @@ export const VZSidebar = ({
|
|
|
97
99
|
<div>(Ctrl + Shift + A)</div>
|
|
98
100
|
</div>
|
|
99
101
|
),
|
|
102
|
+
voiceChatToolTipText = (
|
|
103
|
+
<div>
|
|
104
|
+
<strong>Open Voice Chat Menu</strong>
|
|
105
|
+
</div>
|
|
106
|
+
),
|
|
100
107
|
}: {
|
|
101
108
|
createFileTooltipText?: React.ReactNode;
|
|
102
109
|
createDirTooltipText?: React.ReactNode;
|
|
@@ -107,6 +114,7 @@ export const VZSidebar = ({
|
|
|
107
114
|
filesToolTipText?: React.ReactNode;
|
|
108
115
|
enableAutoFollowTooltipText?: React.ReactNode;
|
|
109
116
|
disableAutoFollowTooltipText?: React.ReactNode;
|
|
117
|
+
voiceChatToolTipText?: React.ReactNode;
|
|
110
118
|
}) => {
|
|
111
119
|
const {
|
|
112
120
|
files,
|
|
@@ -118,12 +126,16 @@ export const VZSidebar = ({
|
|
|
118
126
|
handleOpenCreateFileModal,
|
|
119
127
|
handleOpenCreateDirModal,
|
|
120
128
|
connected,
|
|
129
|
+
pending,
|
|
121
130
|
sidebarRef,
|
|
122
131
|
enableAutoFollow,
|
|
123
132
|
toggleAutoFollow,
|
|
124
133
|
docPresence,
|
|
125
134
|
updatePresenceIndicator,
|
|
126
135
|
sidebarPresenceIndicators,
|
|
136
|
+
liveKitConnection,
|
|
137
|
+
setLiveKitConnection,
|
|
138
|
+
setVoiceChatModalOpen,
|
|
127
139
|
} = useContext(VZCodeContext);
|
|
128
140
|
|
|
129
141
|
const fileTree = useMemo(
|
|
@@ -205,7 +217,31 @@ export const VZSidebar = ({
|
|
|
205
217
|
}
|
|
206
218
|
}, [docPresence]);
|
|
207
219
|
|
|
208
|
-
|
|
220
|
+
const [saved, setSaved] = useState(false);
|
|
221
|
+
const [isConnecting, setIsConnecting] = useState(true);
|
|
222
|
+
const previousPendingRef = useRef<boolean>(pending);
|
|
223
|
+
|
|
224
|
+
// Handle connection status transitions
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (isConnecting && connected) {
|
|
227
|
+
setIsConnecting(false);
|
|
228
|
+
}
|
|
229
|
+
}, [connected, isConnecting]);
|
|
230
|
+
|
|
231
|
+
// Handle 'saved' state based on 'pending' transition
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
// Check if 'pending' transitioned from true to false
|
|
234
|
+
if (previousPendingRef.current && !pending && connected && !isConnecting) {
|
|
235
|
+
setSaved(true);
|
|
236
|
+
const timer = setTimeout(() => {
|
|
237
|
+
setSaved(false);
|
|
238
|
+
}, 1500); // Reset after 2 seconds
|
|
239
|
+
return () => clearTimeout(timer);
|
|
240
|
+
}
|
|
241
|
+
// Update the ref with the current 'pending' state
|
|
242
|
+
previousPendingRef.current = pending;
|
|
243
|
+
}, [pending, connected, isConnecting]);
|
|
244
|
+
|
|
209
245
|
|
|
210
246
|
return (
|
|
211
247
|
<div
|
|
@@ -367,11 +403,31 @@ export const VZSidebar = ({
|
|
|
367
403
|
}`}
|
|
368
404
|
onClick={toggleAutoFollow}
|
|
369
405
|
>
|
|
406
|
+
<i></i>
|
|
370
407
|
<PinSVG />
|
|
371
408
|
</i>
|
|
372
409
|
</OverlayTrigger>
|
|
410
|
+
{/* Start Voice Chat */}
|
|
411
|
+
<OverlayTrigger
|
|
412
|
+
placement="right"
|
|
413
|
+
overlay={
|
|
414
|
+
<Tooltip id="voice-chat">
|
|
415
|
+
{voiceChatToolTipText}
|
|
416
|
+
</Tooltip>
|
|
417
|
+
}
|
|
418
|
+
>
|
|
419
|
+
<i
|
|
420
|
+
id="mic-icon"
|
|
421
|
+
className="icon-button icon-button-dark"
|
|
422
|
+
onClick={() => {
|
|
423
|
+
console.log('clicking', liveKitConnection);
|
|
424
|
+
setVoiceChatModalOpen(true);
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
<MicSVG />
|
|
428
|
+
</i>
|
|
429
|
+
</OverlayTrigger>
|
|
373
430
|
</div>
|
|
374
|
-
|
|
375
431
|
<div className="files" id="sidebar-view-container">
|
|
376
432
|
{!isSearchOpen ? (
|
|
377
433
|
<div className="sidebar-files">
|
|
@@ -414,15 +470,24 @@ export const VZSidebar = ({
|
|
|
414
470
|
)}
|
|
415
471
|
</div>
|
|
416
472
|
</div>
|
|
417
|
-
|
|
418
473
|
{enableConnectionStatus && (
|
|
419
474
|
<div className="connection-status">
|
|
420
|
-
{
|
|
475
|
+
{isConnecting ? 'Connecting...' :
|
|
476
|
+
!connected ? 'Connection Lost' :
|
|
477
|
+
pending ? 'Saving...' :
|
|
478
|
+
saved ? 'Saved.' :
|
|
479
|
+
'Connected'}
|
|
421
480
|
<div className="connection">
|
|
422
481
|
<div
|
|
423
482
|
className={`connection-status-indicator ${
|
|
424
|
-
|
|
425
|
-
|
|
483
|
+
isConnecting
|
|
484
|
+
? 'pending'
|
|
485
|
+
: !connected
|
|
486
|
+
? 'disconnected'
|
|
487
|
+
: pending
|
|
488
|
+
? 'pending'
|
|
489
|
+
: 'connected'
|
|
490
|
+
}`}
|
|
426
491
|
/>
|
|
427
492
|
</div>
|
|
428
493
|
</div>
|
|
@@ -261,6 +261,10 @@
|
|
|
261
261
|
background-color: var(--vh-color-success-01);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
&.pending {
|
|
265
|
+
background-color: var(--vh-color-caution-01);
|
|
266
|
+
}
|
|
267
|
+
|
|
264
268
|
&.disconnected {
|
|
265
269
|
background-color: var(--vh-color-warning-01);
|
|
266
270
|
}
|
|
@@ -290,6 +294,7 @@
|
|
|
290
294
|
align-items: center;
|
|
291
295
|
justify-content: center;
|
|
292
296
|
font-size: 8px;
|
|
293
|
-
color: #
|
|
297
|
+
color: #000000;
|
|
298
|
+
z-index: 1;
|
|
294
299
|
}
|
|
295
300
|
}
|
|
@@ -69,9 +69,13 @@ const LINT_ERROR_CODE_OBJ_REASSINGMENT = 2739;
|
|
|
69
69
|
// Type 'any' is not assignable to type 'never'.
|
|
70
70
|
const LINT_ERROR_CODE_ANY_NOT_ASSIGNABLE_TO_NEVER = 2322;
|
|
71
71
|
|
|
72
|
+
// Ignore potential undefined error for variables
|
|
73
|
+
const LINT_ERROR_CODE_POSSIBLY_UNDEFINED = 18048;
|
|
74
|
+
|
|
72
75
|
// This code is for errors like:
|
|
73
76
|
// "Object is of type 'unknown'."
|
|
74
77
|
const LINT_ERROR_CODE_UNKNOWN = 18046;
|
|
78
|
+
const LINT_ERROR_CODE_UNKNOWN_SYMBOL_ITERATOR = 2488;
|
|
75
79
|
|
|
76
80
|
export const excludedErrorCodes = new Set([
|
|
77
81
|
LINT_ERROR_CODE_ANY,
|
|
@@ -85,4 +89,6 @@ export const excludedErrorCodes = new Set([
|
|
|
85
89
|
LINT_ERROR_CODE_OBJ_REASSINGMENT,
|
|
86
90
|
LINT_ERROR_CODE_ANY_NOT_ASSIGNABLE_TO_NEVER,
|
|
87
91
|
LINT_ERROR_CODE_UNKNOWN,
|
|
92
|
+
LINT_ERROR_CODE_UNKNOWN_SYMBOL_ITERATOR,
|
|
93
|
+
LINT_ERROR_CODE_POSSIBLY_UNDEFINED,
|
|
88
94
|
]);
|
|
@@ -1,24 +1,76 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { json1Presence } from '../ot.js';
|
|
3
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
|
+
|
|
4
18
|
const { editOp, type } = json1Presence;
|
|
5
19
|
|
|
20
|
+
// Debug flag to log more information during development.
|
|
6
21
|
const debug = false;
|
|
7
22
|
|
|
8
|
-
// Feature flag to slow down AI for development/testing
|
|
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.
|
|
9
27
|
const slowdown = false;
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
// The options passed into the OpenAI client
|
|
30
|
+
// new OpenAI(openAIOptions)
|
|
31
|
+
const openAIOptions = {};
|
|
32
|
+
|
|
33
|
+
// Support specifying the API key via an environment variable
|
|
34
|
+
// If VZCODE_AI_API_KEY is not set, note that the OpenAI client
|
|
35
|
+
// will look for the OPENAI_API_KEY environment variable instead.
|
|
36
|
+
if (process.env.VZCODE_AI_API_KEY !== undefined) {
|
|
37
|
+
openAIOptions.apiKey = process.env.VZCODE_AI_API_KEY;
|
|
14
38
|
}
|
|
15
39
|
|
|
16
|
-
|
|
40
|
+
// Support for local AI server
|
|
41
|
+
if (process.env.VZCODE_AI_BASE_URL !== undefined) {
|
|
42
|
+
// The OpenAI client errors if the API key is not set,
|
|
43
|
+
// so in the case where we don't need an API key,
|
|
44
|
+
// e.g. for testing with a local AI server like LM Studio or LocalAI,
|
|
45
|
+
// we populate the option with a fake API key, so it doesn't error.
|
|
46
|
+
if (!openAIOptions.apiKey) {
|
|
47
|
+
openAIOptions.apiKey = 'Fake API Key';
|
|
48
|
+
}
|
|
17
49
|
|
|
18
|
-
|
|
19
|
-
return source === AISourceName;
|
|
50
|
+
openAIOptions.baseURL = process.env.VZCODE_AI_BASE_URL;
|
|
20
51
|
}
|
|
21
52
|
|
|
53
|
+
debug &&
|
|
54
|
+
console.log(
|
|
55
|
+
'openAIOptions: ' +
|
|
56
|
+
JSON.stringify(openAIOptions, null, 2),
|
|
57
|
+
);
|
|
58
|
+
let openai;
|
|
59
|
+
|
|
60
|
+
if (isAIEnabled) {
|
|
61
|
+
openai = new OpenAI(openAIOptions);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The name of the source that the AI responses
|
|
65
|
+
// will be attributed to in ShareDB operations.
|
|
66
|
+
const AIShareDBSourceName = 'AIAssist';
|
|
67
|
+
|
|
68
|
+
// Returns trie if the operation comes from AI.
|
|
69
|
+
const opComesFromAIAssist = (ops, source) =>
|
|
70
|
+
source === AIShareDBSourceName;
|
|
71
|
+
|
|
72
|
+
// Keeps track of the currently ongoing AI streams.
|
|
73
|
+
// There could be many streams at the same time.
|
|
22
74
|
const streams = {};
|
|
23
75
|
|
|
24
76
|
export const generateAIResponse = async ({
|
|
@@ -28,6 +80,16 @@ export const generateAIResponse = async ({
|
|
|
28
80
|
streamId,
|
|
29
81
|
shareDBDoc,
|
|
30
82
|
}) => {
|
|
83
|
+
if (!isAIEnabled) {
|
|
84
|
+
console.log(
|
|
85
|
+
'[generateAIResponse] AI is not enabled. Skipping AI generation.',
|
|
86
|
+
);
|
|
87
|
+
console.log(
|
|
88
|
+
'[generateAIResponse] To enable AI, see .env.sample for the required environment variables.',
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
if (debug) {
|
|
32
94
|
console.log(
|
|
33
95
|
'[generateAIResponse] inputText:',
|
|
@@ -44,7 +106,10 @@ export const generateAIResponse = async ({
|
|
|
44
106
|
shareDBDoc,
|
|
45
107
|
);
|
|
46
108
|
}
|
|
47
|
-
|
|
109
|
+
|
|
110
|
+
// Handle the case that a user edits the text in the document
|
|
111
|
+
// that comes becofore the insertion cursor.
|
|
112
|
+
const accomodateDocChanges = (op, source) => {
|
|
48
113
|
if (!opComesFromAIAssist(op, source)) {
|
|
49
114
|
if (op !== null) {
|
|
50
115
|
insertionCursor = type
|
|
@@ -55,10 +120,10 @@ export const generateAIResponse = async ({
|
|
|
55
120
|
.slice(-1)[0];
|
|
56
121
|
}
|
|
57
122
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
123
|
+
};
|
|
60
124
|
shareDBDoc.on('op', accomodateDocChanges);
|
|
61
125
|
|
|
126
|
+
// The prompt!
|
|
62
127
|
const messages = [
|
|
63
128
|
{
|
|
64
129
|
role: 'system',
|
|
@@ -82,8 +147,7 @@ export const generateAIResponse = async ({
|
|
|
82
147
|
|
|
83
148
|
streams[streamId] = await openai.chat.completions.create({
|
|
84
149
|
// model: 'gpt-3.5-turbo',
|
|
85
|
-
model: 'gpt-
|
|
86
|
-
|
|
150
|
+
model: 'gpt-4o',
|
|
87
151
|
messages,
|
|
88
152
|
stream: true,
|
|
89
153
|
});
|
|
@@ -98,7 +162,9 @@ export const generateAIResponse = async ({
|
|
|
98
162
|
],
|
|
99
163
|
);
|
|
100
164
|
|
|
101
|
-
shareDBDoc.submitOp(op, {
|
|
165
|
+
shareDBDoc.submitOp(op, {
|
|
166
|
+
source: AIShareDBSourceName,
|
|
167
|
+
});
|
|
102
168
|
|
|
103
169
|
if (slowdown) {
|
|
104
170
|
await new Promise((resolve) => {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
|
|
3
|
+
// This module implements the generation of the copilot response,
|
|
4
|
+
// using the OpenAI client (but not necessarily the OpenAI service).
|
|
5
|
+
|
|
6
|
+
// Debug flag to log more information during development.
|
|
7
|
+
const debug = false;
|
|
8
|
+
|
|
9
|
+
// These environment variables are used to configure the OpenAI client.
|
|
10
|
+
// See `.env.sample` for the expected values.
|
|
11
|
+
const {
|
|
12
|
+
VZCODE_AI_COPILOT_API_KEY,
|
|
13
|
+
VZCODE_AI_COLIPOT_BASE_URL,
|
|
14
|
+
VZCODE_AI_COLIPOT_MODEL,
|
|
15
|
+
} = process.env;
|
|
16
|
+
|
|
17
|
+
// If neither of these are not set, the OpenAI client will not be initialized,
|
|
18
|
+
// e.g. for local development where no AI is needed.
|
|
19
|
+
const isCopilotEnabled =
|
|
20
|
+
VZCODE_AI_COPILOT_API_KEY !== undefined &&
|
|
21
|
+
VZCODE_AI_COLIPOT_BASE_URL !== undefined &&
|
|
22
|
+
VZCODE_AI_COLIPOT_MODEL !== undefined;
|
|
23
|
+
|
|
24
|
+
debug &&
|
|
25
|
+
console.log('isCopilotEnabled: ' + isCopilotEnabled);
|
|
26
|
+
|
|
27
|
+
let openai;
|
|
28
|
+
if (isCopilotEnabled) {
|
|
29
|
+
const openAIOptions = {
|
|
30
|
+
apiKey: VZCODE_AI_COPILOT_API_KEY,
|
|
31
|
+
baseURL: VZCODE_AI_COLIPOT_BASE_URL,
|
|
32
|
+
};
|
|
33
|
+
debug &&
|
|
34
|
+
console.log(
|
|
35
|
+
'openAIOptions: ' +
|
|
36
|
+
JSON.stringify(openAIOptions, null, 2),
|
|
37
|
+
);
|
|
38
|
+
openai = new OpenAI(openAIOptions);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const handleAICopilot =
|
|
42
|
+
(/*TODO shareDBDoc*/) => async (req, res) => {
|
|
43
|
+
// Don't break if the AI is not enabled.
|
|
44
|
+
// This is useful for local development.
|
|
45
|
+
if (!isCopilotEnabled) {
|
|
46
|
+
res.status(200).send({ text: '' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const { prefix, suffix } = req.body;
|
|
50
|
+
|
|
51
|
+
const prompt = `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`;
|
|
52
|
+
|
|
53
|
+
if (debug) {
|
|
54
|
+
console.log(
|
|
55
|
+
'[generateAIResponse] prompt:\n```\n' +
|
|
56
|
+
prompt +
|
|
57
|
+
'\n```\n',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const completion = await openai.completions.create({
|
|
62
|
+
model: VZCODE_AI_COLIPOT_MODEL,
|
|
63
|
+
prompt,
|
|
64
|
+
max_tokens: 150,
|
|
65
|
+
stop: ['<|fim_pad|>', '<|file_sep|>'],
|
|
66
|
+
});
|
|
67
|
+
const text = completion.choices[0].text;
|
|
68
|
+
|
|
69
|
+
res.status(200).send({ text });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('handleAICopilot error:', error);
|
|
72
|
+
res.status(500).send({
|
|
73
|
+
message: 'Internal Server Error',
|
|
74
|
+
error: error.message,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|