vzcode 1.15.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/AIAssist/AIAssistWidget/index.tsx +10 -6
- 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 +7 -4
- package/src/client/CodeEditor/json1PresenceDisplay.ts +5 -5
- 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/TabList/index.tsx +8 -11
- package/src/client/VZCodeContext.tsx +66 -24
- package/src/client/VZKeyboardShortcutsDoc.tsx +8 -8
- package/src/client/VZLeft.tsx +4 -2
- package/src/client/VZMiddle.tsx +86 -27
- package/src/client/VZResizer/index.tsx +3 -3
- package/src/client/VZSettings.tsx +54 -3
- package/src/client/VZSidebar/FileListing.tsx +20 -6
- package/src/client/VZSidebar/Listing.tsx +2 -2
- package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
- package/src/client/VZSidebar/index.tsx +80 -15
- package/src/client/VZSidebar/styles.scss +6 -2
- package/src/client/presenceColor.ts +8 -0
- 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-Bm65AzMP.js +0 -425
- package/dist/assets/index-DbFdL1A1.js +0 -159
- package/dist/assets/index-Yz-ZHc0O.css +0 -1
- package/dist/assets/worker-CJWrLBN2.js +0 -286
|
@@ -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,7 +294,7 @@
|
|
|
290
294
|
align-items: center;
|
|
291
295
|
justify-content: center;
|
|
292
296
|
font-size: 8px;
|
|
293
|
-
|
|
294
|
-
|
|
297
|
+
color: #000000;
|
|
298
|
+
z-index: 1;
|
|
295
299
|
}
|
|
296
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
|
+
};
|
package/src/server/index.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import './setupEnv.js';
|
|
3
|
-
import http from 'http';
|
|
4
|
-
import express from 'express';
|
|
5
|
-
import ShareDB from 'sharedb';
|
|
6
|
-
import { WebSocketServer } from 'ws';
|
|
7
2
|
import WebSocketJSONStream from '@teamwork/websocket-json-stream';
|
|
3
|
+
import bodyParser from 'body-parser';
|
|
4
|
+
import express from 'express';
|
|
8
5
|
import fs from 'fs';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import ngrok from 'ngrok';
|
|
8
|
+
import open from 'open';
|
|
9
9
|
import path from 'path';
|
|
10
|
+
import ShareDB from 'sharedb';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import
|
|
12
|
-
import ngrok from 'ngrok';
|
|
13
|
-
import bodyParser from 'body-parser';
|
|
12
|
+
import { WebSocketServer } from 'ws';
|
|
14
13
|
import { json1Presence } from '../ot.js';
|
|
15
14
|
import { computeInitialDocument } from './computeInitialDocument.js';
|
|
16
15
|
import { handleAIAssist } from './handleAIAssist.js';
|
|
16
|
+
import { handleAICopilot } from './handleAICopilot.js';
|
|
17
17
|
import { isDirectory } from './isDirectory.js';
|
|
18
|
+
import { createToken } from './livekit.js';
|
|
19
|
+
import './setupEnv.js';
|
|
18
20
|
|
|
19
21
|
// The time in milliseconds by which auto-saving is debounced.
|
|
20
22
|
const autoSaveDebounceTimeMS = 800;
|
|
@@ -103,6 +105,19 @@ app.post(
|
|
|
103
105
|
handleAIAssist(shareDBDoc),
|
|
104
106
|
);
|
|
105
107
|
|
|
108
|
+
// Handle AI Copilot requests.
|
|
109
|
+
app.post(
|
|
110
|
+
'/ai-copilot',
|
|
111
|
+
bodyParser.json(),
|
|
112
|
+
handleAICopilot(shareDBDoc),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Livekit Token Generator
|
|
116
|
+
app.get('/livekit-token', async (req, res) => {
|
|
117
|
+
const { room, username } = req.query;
|
|
118
|
+
res.send(await createToken(room, username));
|
|
119
|
+
});
|
|
120
|
+
|
|
106
121
|
// The state of the document when files were last auto-saved.
|
|
107
122
|
let previousDocument = initialDocument;
|
|
108
123
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
2
|
+
|
|
3
|
+
const { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } = process.env;
|
|
4
|
+
|
|
5
|
+
export const createToken = async (roomName, username) => {
|
|
6
|
+
// If this room doesn't exist, it'll be automatically created when the first
|
|
7
|
+
// client joins
|
|
8
|
+
// Identifier to be used for participant.
|
|
9
|
+
// It's available as LocalParticipant.identity with livekit-client SDK
|
|
10
|
+
console.log(username);
|
|
11
|
+
const participantName = `${username}`;
|
|
12
|
+
|
|
13
|
+
const at = new AccessToken(
|
|
14
|
+
LIVEKIT_API_KEY,
|
|
15
|
+
LIVEKIT_API_SECRET,
|
|
16
|
+
{
|
|
17
|
+
identity: participantName,
|
|
18
|
+
// Token to expire after 10 minutes
|
|
19
|
+
ttl: '10m',
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
at.addGrant({ roomJoin: true, room: roomName });
|
|
23
|
+
return await at.toJwt();
|
|
24
|
+
};
|
package/src/server/setupEnv.js
CHANGED