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.
Files changed (37) hide show
  1. package/dist/assets/index-BQgRA0SD.js +181 -0
  2. package/dist/assets/index-CSSpCKM0.js +447 -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 +4 -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 +80 -15
  26. package/src/client/VZSidebar/styles.scss +6 -1
  27. package/src/client/useTypeScript/worker/constants.ts +6 -0
  28. package/src/server/generateAIResponse.js +79 -13
  29. package/src/server/handleAICopilot.js +77 -0
  30. package/src/server/index.js +23 -8
  31. package/src/server/livekit.js +24 -0
  32. package/src/server/setupEnv.js +1 -1
  33. package/src/types.ts +1 -0
  34. package/dist/assets/index-B6yTvvah.css +0 -1
  35. package/dist/assets/index-BlwLvENZ.js +0 -159
  36. package/dist/assets/index-Bm65AzMP.js +0 -425
  37. 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,26 @@ 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';
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
- // console.log(sidebarPresenceIndicators);
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
- {connected ? 'Connected' : 'Connection Lost'}
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
- connected ? 'connected' : 'disconnected'
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: #ffffff;
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
- 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
+ };