vzcode 1.16.0 → 1.18.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.
Files changed (39) hide show
  1. package/dist/assets/index-CSSpCKM0.js +447 -0
  2. package/dist/assets/index-eifYAxsF.js +181 -0
  3. package/dist/assets/index-zzK6rRiK.css +1 -0
  4. package/dist/assets/worker-8af7bTHW.js +293 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +40 -34
  7. package/src/client/App/index.tsx +52 -20
  8. package/src/client/App/usePending.ts +50 -0
  9. package/src/client/App/useShareDB.ts +40 -1
  10. package/src/client/CodeEditor/Copilot/index.ts +28 -0
  11. package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +129 -33
  12. package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +36 -1
  13. package/src/client/CodeEditor/getOrCreateEditor.ts +43 -6
  14. package/src/client/CodeEditor/index.tsx +4 -1
  15. package/src/client/CodeEditor/rainbowBrackets.ts +14 -2
  16. package/src/client/CodeEditor/style.scss +106 -1
  17. package/src/client/Icons/MicSVG.tsx +25 -0
  18. package/src/client/RunCodeWidget/index.tsx +20 -3
  19. package/src/client/VZCodeContext.tsx +52 -14
  20. package/src/client/VZKeyboardShortcutsDoc.tsx +8 -8
  21. package/src/client/VZLeft.tsx +5 -2
  22. package/src/client/VZMiddle.tsx +5 -0
  23. package/src/client/VZSettings.tsx +54 -3
  24. package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
  25. package/src/client/VZSidebar/index.tsx +93 -14
  26. package/src/client/VZSidebar/styles.scss +6 -1
  27. package/src/client/featureFlags.ts +2 -0
  28. package/src/client/useTypeScript/worker/constants.ts +6 -0
  29. package/src/server/generateAIResponse.js +79 -13
  30. package/src/server/handleAICopilot.js +77 -0
  31. package/src/server/index.js +23 -8
  32. package/src/server/livekit.js +24 -0
  33. package/src/server/setupEnv.js +1 -1
  34. package/src/types.ts +1 -0
  35. package/src/vite-env.d.ts +10 -0
  36. package/dist/assets/index-B6yTvvah.css +0 -1
  37. package/dist/assets/index-BlwLvENZ.js +0 -159
  38. package/dist/assets/index-Bm65AzMP.js +0 -425
  39. 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
- setTheme(event.target.value as ThemeLabel);
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,27 @@ import {
13
14
  PresenceId,
14
15
  PresenceIndicator,
15
16
  } from '../../types';
16
- import { Tooltip, OverlayTrigger } from '../bootstrap';
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 { useDragAndDrop } from './useDragAndDrop';
34
+ import { Search } from './Search';
34
35
  import './styles.scss';
36
+ import { useDragAndDrop } from './useDragAndDrop';
37
+ import { enableLiveKit } from '../featureFlags';
35
38
 
36
39
  // TODO turn this UI back on when we are actually detecting
37
40
  // the connection status.
@@ -97,6 +100,11 @@ export const VZSidebar = ({
97
100
  <div>(Ctrl + Shift + A)</div>
98
101
  </div>
99
102
  ),
103
+ voiceChatToolTipText = (
104
+ <div>
105
+ <strong>Open Voice Chat Menu</strong>
106
+ </div>
107
+ ),
100
108
  }: {
101
109
  createFileTooltipText?: React.ReactNode;
102
110
  createDirTooltipText?: React.ReactNode;
@@ -107,6 +115,7 @@ export const VZSidebar = ({
107
115
  filesToolTipText?: React.ReactNode;
108
116
  enableAutoFollowTooltipText?: React.ReactNode;
109
117
  disableAutoFollowTooltipText?: React.ReactNode;
118
+ voiceChatToolTipText?: React.ReactNode;
110
119
  }) => {
111
120
  const {
112
121
  files,
@@ -118,12 +127,16 @@ export const VZSidebar = ({
118
127
  handleOpenCreateFileModal,
119
128
  handleOpenCreateDirModal,
120
129
  connected,
130
+ pending,
121
131
  sidebarRef,
122
132
  enableAutoFollow,
123
133
  toggleAutoFollow,
124
134
  docPresence,
125
135
  updatePresenceIndicator,
126
136
  sidebarPresenceIndicators,
137
+ liveKitConnection,
138
+ setLiveKitConnection,
139
+ setVoiceChatModalOpen,
127
140
  } = useContext(VZCodeContext);
128
141
 
129
142
  const fileTree = useMemo(
@@ -205,7 +218,35 @@ export const VZSidebar = ({
205
218
  }
206
219
  }, [docPresence]);
207
220
 
208
- // console.log(sidebarPresenceIndicators);
221
+ const [saved, setSaved] = useState(false);
222
+ const [isConnecting, setIsConnecting] = useState(true);
223
+ const previousPendingRef = useRef<boolean>(pending);
224
+
225
+ // Handle connection status transitions
226
+ useEffect(() => {
227
+ if (isConnecting && connected) {
228
+ setIsConnecting(false);
229
+ }
230
+ }, [connected, isConnecting]);
231
+
232
+ // Handle 'saved' state based on 'pending' transition
233
+ useEffect(() => {
234
+ // Check if 'pending' transitioned from true to false
235
+ if (
236
+ previousPendingRef.current &&
237
+ !pending &&
238
+ connected &&
239
+ !isConnecting
240
+ ) {
241
+ setSaved(true);
242
+ const timer = setTimeout(() => {
243
+ setSaved(false);
244
+ }, 1500); // Reset after 2 seconds
245
+ return () => clearTimeout(timer);
246
+ }
247
+ // Update the ref with the current 'pending' state
248
+ previousPendingRef.current = pending;
249
+ }, [pending, connected, isConnecting]);
209
250
 
210
251
  return (
211
252
  <div
@@ -367,11 +408,36 @@ export const VZSidebar = ({
367
408
  }`}
368
409
  onClick={toggleAutoFollow}
369
410
  >
411
+ <i></i>
370
412
  <PinSVG />
371
413
  </i>
372
414
  </OverlayTrigger>
415
+ {/* Start Voice Chat */}
416
+ {enableLiveKit && (
417
+ <OverlayTrigger
418
+ placement="right"
419
+ overlay={
420
+ <Tooltip id="voice-chat">
421
+ {voiceChatToolTipText}
422
+ </Tooltip>
423
+ }
424
+ >
425
+ <i
426
+ id="mic-icon"
427
+ className="icon-button icon-button-dark"
428
+ onClick={() => {
429
+ console.log(
430
+ 'clicking',
431
+ liveKitConnection,
432
+ );
433
+ setVoiceChatModalOpen(true);
434
+ }}
435
+ >
436
+ <MicSVG />
437
+ </i>
438
+ </OverlayTrigger>
439
+ )}
373
440
  </div>
374
-
375
441
  <div className="files" id="sidebar-view-container">
376
442
  {!isSearchOpen ? (
377
443
  <div className="sidebar-files">
@@ -414,14 +480,27 @@ export const VZSidebar = ({
414
480
  )}
415
481
  </div>
416
482
  </div>
417
-
418
483
  {enableConnectionStatus && (
419
484
  <div className="connection-status">
420
- {connected ? 'Connected' : 'Connection Lost'}
485
+ {isConnecting
486
+ ? 'Connecting...'
487
+ : !connected
488
+ ? 'Connection Lost'
489
+ : pending
490
+ ? 'Saving...'
491
+ : saved
492
+ ? 'Saved.'
493
+ : 'Connected'}
421
494
  <div className="connection">
422
495
  <div
423
496
  className={`connection-status-indicator ${
424
- connected ? 'connected' : 'disconnected'
497
+ isConnecting
498
+ ? 'pending'
499
+ : !connected
500
+ ? 'disconnected'
501
+ : pending
502
+ ? 'pending'
503
+ : 'connected'
425
504
  }`}
426
505
  />
427
506
  </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: #ffffff;
297
+ color: #000000;
298
+ z-index: 1;
294
299
  }
295
300
  }
@@ -0,0 +1,2 @@
1
+ export const enableLiveKit =
2
+ import.meta.env.VITE_ENABLE_LIVEKIT === 'true';
@@ -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
- let openai;
12
- if (process.env.OPENAI_API_KEY !== undefined) {
13
- openai = new OpenAI();
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
- const AISourceName = 'AIAssist';
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
- function opComesFromAIAssist(ops, source) {
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
- function accomodateDocChanges(op, source) {
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-4',
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, { source: AISourceName });
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
+ };