vzcode 0.45.0 → 0.47.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.
@@ -4,7 +4,7 @@ import { TabList } from './TabList';
4
4
  import { CodeEditor } from './CodeEditor';
5
5
  import { CodeErrorOverlay } from './CodeErrorOverlay';
6
6
  import { PresenceNotifications } from './PresenceNotifications';
7
- import { AIAssistWidget } from './AIAssistWidget';
7
+ import { AIAssistWidget } from './AIAssist/AIAssistWidget';
8
8
  import { VZCodeContext } from './VZCodeContext';
9
9
 
10
10
  // The middle portion of the VZCode environment, containing:
@@ -13,7 +13,15 @@ import { VZCodeContext } from './VZCodeContext';
13
13
  // * The code error overlay
14
14
  // * The presence notifications
15
15
  // * The UI for AI Assist
16
- export const VZMiddle = ({ enableAIAssist = true }) => {
16
+ export const VZMiddle = ({
17
+ enableAIAssist = true,
18
+ aiAssistEndpoint,
19
+ aiAssistOptions,
20
+ }: {
21
+ enableAIAssist?: boolean;
22
+ aiAssistEndpoint: string;
23
+ aiAssistOptions: { [key: string]: string };
24
+ }) => {
17
25
  const { codeEditorWidth } = useContext(
18
26
  SplitPaneResizeContext,
19
27
  );
@@ -70,22 +78,26 @@ export const VZMiddle = ({ enableAIAssist = true }) => {
70
78
  }
71
79
  username={username}
72
80
  typeScriptWorker={typeScriptWorker}
73
- tabList={tabList}
74
81
  />
75
82
  ) : null}
76
- <CodeErrorOverlay errorMessage={errorMessage} />
77
- <PresenceNotifications
78
- docPresence={docPresence}
79
- localPresence={localPresence}
80
- />
81
83
  {enableAIAssist && content && activeFileId ? (
82
84
  <AIAssistWidget
83
85
  activeFileId={activeFileId}
84
86
  shareDBDoc={shareDBDoc}
85
87
  editorCache={editorCache}
86
88
  tabList={tabList}
89
+ aiAssistEndpoint={aiAssistEndpoint}
90
+ aiAssistOptions={aiAssistOptions}
87
91
  />
88
92
  ) : null}
93
+ <CodeErrorOverlay
94
+ errorMessage={errorMessage}
95
+ content={content}
96
+ />
97
+ <PresenceNotifications
98
+ docPresence={docPresence}
99
+ localPresence={localPresence}
100
+ />
89
101
  </div>
90
102
  ) : null;
91
103
  };
@@ -0,0 +1,6 @@
1
+ export type RequestId = string;
2
+
3
+ // Generates a random string of digits.
4
+ // e.g. "6891835662438279"
5
+ export const generateRequestId = (): RequestId =>
6
+ (Math.random() + '').slice(2);
@@ -27,6 +27,10 @@ export const useTypeScript = ({
27
27
 
28
28
  const debounceUpdateContent = useCallback(
29
29
  (content: VZCodeContent) => {
30
+ // Handle the case where the content has not yet been loaded.
31
+ if (content === null) {
32
+ return;
33
+ }
30
34
  clearTimeout(debounceTimeoutId.current);
31
35
  debounceTimeoutId.current = window.setTimeout(() => {
32
36
  typeScriptWorker.postMessage({
@@ -0,0 +1,111 @@
1
+ import OpenAI from 'openai';
2
+ import { json1Presence } from '../ot.js';
3
+
4
+ const { editOp, type } = json1Presence;
5
+
6
+ const debug = false;
7
+
8
+ // Feature flag to slow down AI for development/testing
9
+ const slowdown = false;
10
+
11
+ let openai;
12
+ if (process.env.OPENAI_API_KEY !== undefined) {
13
+ openai = new OpenAI();
14
+ }
15
+
16
+ const AISourceName = 'AIAssist';
17
+
18
+ function opComesFromAIAssist(ops, source) {
19
+ return source === AISourceName;
20
+ }
21
+
22
+ const streams = {};
23
+
24
+ export const generateAIResponse = async ({
25
+ inputText,
26
+ insertionCursor,
27
+ fileId,
28
+ streamId,
29
+ shareDBDoc,
30
+ }) => {
31
+ if (debug) {
32
+ console.log(
33
+ '[generateAIResponse] inputText:',
34
+ inputText,
35
+ );
36
+ console.log(
37
+ '[generateAIResponse] insertionCursor:',
38
+ insertionCursor,
39
+ );
40
+ console.log('[generateAIResponse] fileId:', fileId);
41
+ console.log('[generateAIResponse] streamId:', streamId);
42
+ console.log(
43
+ '[generateAIResponse] shareDBDoc:',
44
+ shareDBDoc,
45
+ );
46
+ }
47
+ function accomodateDocChanges(op, source) {
48
+ if (!opComesFromAIAssist(op, source)) {
49
+ if (op !== null) {
50
+ insertionCursor = type
51
+ .transformPosition(
52
+ ['files', fileId, 'text', insertionCursor],
53
+ op,
54
+ )
55
+ .slice(-1)[0];
56
+ }
57
+ }
58
+ }
59
+
60
+ shareDBDoc.on('op', accomodateDocChanges);
61
+
62
+ const messages = [
63
+ {
64
+ role: 'system',
65
+ content: [
66
+ 'You are an expert programmer.',
67
+ 'Your task is to output ONLY the code that replaces <FILL_ME> correctly.',
68
+ 'Do not add any markdown.',
69
+ 'Do not duplicate the code before or after <FILL_ME>.',
70
+ 'Do not make any changes outside of <FILL_ME>.',
71
+ ].join(' '),
72
+ },
73
+ { role: 'user', content: inputText },
74
+ ];
75
+
76
+ console.log('messages');
77
+ console.log(messages);
78
+
79
+ streams[streamId] = await openai.chat.completions.create({
80
+ // model: 'gpt-3.5-turbo',
81
+ model: 'gpt-4',
82
+
83
+ messages,
84
+ stream: true,
85
+ });
86
+
87
+ for await (const part of streams[streamId]) {
88
+ const op = editOp(
89
+ ['files', fileId, 'text'],
90
+ 'text-unicode',
91
+ [
92
+ insertionCursor,
93
+ part.choices[0]?.delta?.content || '',
94
+ ],
95
+ );
96
+
97
+ shareDBDoc.submitOp(op, { source: AISourceName });
98
+
99
+ // Wait for 500ms
100
+ if (slowdown) {
101
+ await new Promise((resolve) => {
102
+ setTimeout(resolve, 1000);
103
+ });
104
+ }
105
+
106
+ insertionCursor += (
107
+ part.choices[0]?.delta?.content || ''
108
+ ).length;
109
+ }
110
+ shareDBDoc.off('op', accomodateDocChanges);
111
+ };
@@ -1,109 +1,20 @@
1
- import OpenAI from 'openai';
2
- import { json1Presence } from '../ot.js';
1
+ import { generateAIResponse } from './generateAIResponse.js';
3
2
 
4
- const { editOp, type, replaceOp } = json1Presence;
3
+ const debug = false;
5
4
 
6
- // Feature flag to slow down AI for development/testing
7
- const slowdown = false;
8
-
9
- let openai;
10
- if (process.env.OPENAI_API_KEY !== undefined) {
11
- openai = new OpenAI();
12
- }
13
-
14
- export async function generateAIResponse({
15
- inputText,
16
- insertionCursor,
17
- fileId,
18
- streamId,
19
- shareDBDoc,
20
- }) {
21
- function accomodateDocChanges(op, source) {
22
- if (!opComesFromAIAssist(op, source)) {
23
- if (op !== null) {
24
- insertionCursor = type
25
- .transformPosition(
26
- ['files', fileId, 'text', insertionCursor],
27
- op,
28
- )
29
- .slice(-1)[0];
30
- }
31
- }
32
- }
33
-
34
- shareDBDoc.on('op', accomodateDocChanges);
35
-
36
- streams[streamId] = await openai.chat.completions.create({
37
- // model: 'gpt-3.5-turbo',
38
- model: 'gpt-4',
39
-
40
- messages: [
41
- {
42
- role: 'system',
43
- content:
44
- 'Write typescript or javascript code to continue the current file, given the other files for context. Use // for comments.',
45
- },
46
- { role: 'user', content: inputText },
47
- ],
48
- stream: true,
49
- });
5
+ export const handleAIAssist =
6
+ (shareDBDoc) => async (req, res) => {
7
+ const { inputText, insertionCursor, fileId } = req.body;
50
8
 
51
- for await (const part of streams[streamId]) {
52
- const op = editOp(
53
- ['files', fileId, 'text'],
54
- 'text-unicode',
55
- [
9
+ if (debug) {
10
+ console.log('[handleAIAssist] inputText:', inputText);
11
+ console.log(
12
+ '[handleAIAssist] insertionCursor:',
56
13
  insertionCursor,
57
- part.choices[0]?.delta?.content || '',
58
- ],
59
- );
60
-
61
- shareDBDoc.submitOp(op, { source: AISourceName });
62
-
63
- // Wait for 500ms
64
- if (slowdown) {
65
- await new Promise((resolve) => {
66
- setTimeout(resolve, 1000);
67
- });
14
+ );
15
+ console.log('[handleAIAssist] fileId:', fileId);
68
16
  }
69
17
 
70
- insertionCursor += (
71
- part.choices[0]?.delta?.content || ''
72
- ).length;
73
- }
74
- shareDBDoc.off('op', accomodateDocChanges);
75
-
76
- const confirmCompletedOperation = replaceOp(
77
- [
78
- 'aiStreams',
79
- streamId,
80
- 'AIStreamStatus',
81
- 'serverIsRunning',
82
- ],
83
- true,
84
- false,
85
- );
86
- shareDBDoc.submitOp(confirmCompletedOperation, {
87
- source: 'AIServer',
88
- });
89
- }
90
-
91
- const AISourceName = 'AIAssist';
92
-
93
- function opComesFromAIAssist(ops, source) {
94
- return source === AISourceName;
95
- }
96
-
97
- const streams = {};
98
-
99
- export const handleAIAssist =
100
- (shareDBDoc) => async (req, res) => {
101
- const {
102
- text: inputText,
103
- cursorLocation: insertionCursor,
104
- fileId,
105
- } = req.body;
106
-
107
18
  try {
108
19
  await generateAIResponse({
109
20
  inputText,
@@ -13,13 +13,7 @@ import ngrok from 'ngrok';
13
13
  import bodyParser from 'body-parser';
14
14
  import { json1Presence } from '../ot.js';
15
15
  import { computeInitialDocument } from './computeInitialDocument.js';
16
- import {
17
- generateAIResponse,
18
- haltGeneration,
19
- handleAIAssist,
20
- } from './handleAIAssist.js';
21
-
22
- const { replaceOp } = json1Presence;
16
+ import { handleAIAssist } from './handleAIAssist.js';
23
17
 
24
18
  // The time in milliseconds by which auto-saving is debounced.
25
19
  const autoSaveDebounceTimeMS = 800;
@@ -103,7 +97,7 @@ shareDBDoc.create(initialDocument, json1Presence.type.uri);
103
97
 
104
98
  // Handle AI Assist requests.
105
99
  app.post(
106
- '/AIAssist',
100
+ '/ai-assist',
107
101
  bodyParser.json(),
108
102
  handleAIAssist(shareDBDoc),
109
103
  );
@@ -219,53 +213,53 @@ shareDBDoc.subscribe(() => {
219
213
  debounceSave();
220
214
  }
221
215
 
222
- if (
223
- op !== null &&
224
- op[0] == 'aiStreams' &&
225
- source !== 'AIServer' &&
226
- source !== 'AIAssist'
227
- ) {
228
- //This is an insert operation (a new request)
229
- if (
230
- op[op.length - 1]['i'] !== undefined &&
231
- op[op.length - 1]['i']['AIStreamStatus'] !==
232
- undefined
233
- ) {
234
- const input =
235
- op[op.length - 1]['i']['AIStreamStatus'];
236
-
237
- generateAIResponse({
238
- inputText: input.text,
239
- insertionCursor: input.insertionCursor,
240
- shareDBDoc: shareDBDoc,
241
- streamId: op[op.length - 2],
242
- fileId: input.fileId,
243
- });
244
-
245
- const confirmStartOperation = replaceOp(
246
- [
247
- ...op.filter(
248
- (value) => typeof value === 'string',
249
- ),
250
- 'AIStreamStatus',
251
- 'serverIsRunning',
252
- ],
253
- true,
254
- true,
255
- );
256
-
257
- shareDBDoc.submitOp(confirmStartOperation, {
258
- source: 'AIServer',
259
- });
260
- }
261
- if (
262
- op[op.length - 1]['r'] !== null &&
263
- op[op.length - 2] == 'clientWantsToStart'
264
- ) {
265
- //client wants to halt generation
266
- haltGeneration(op[1]);
267
- }
268
- }
216
+ // if (
217
+ // op !== null &&
218
+ // op[0] == 'aiStreams' &&
219
+ // source !== 'AIServer' &&
220
+ // source !== 'AIAssist'
221
+ // ) {
222
+ // //This is an insert operation (a new request)
223
+ // if (
224
+ // op[op.length - 1]['i'] !== undefined &&
225
+ // op[op.length - 1]['i']['AIStreamStatus'] !==
226
+ // undefined
227
+ // ) {
228
+ // const input =
229
+ // op[op.length - 1]['i']['AIStreamStatus'];
230
+
231
+ // generateAIResponse({
232
+ // inputText: input.text,
233
+ // insertionCursor: input.insertionCursor,
234
+ // shareDBDoc: shareDBDoc,
235
+ // streamId: op[op.length - 2],
236
+ // fileId: input.fileId,
237
+ // });
238
+
239
+ // const confirmStartOperation = replaceOp(
240
+ // [
241
+ // ...op.filter(
242
+ // (value) => typeof value === 'string',
243
+ // ),
244
+ // 'AIStreamStatus',
245
+ // 'serverIsRunning',
246
+ // ],
247
+ // true,
248
+ // true,
249
+ // );
250
+
251
+ // shareDBDoc.submitOp(confirmStartOperation, {
252
+ // source: 'AIServer',
253
+ // });
254
+ // }
255
+ // if (
256
+ // op[op.length - 1]['r'] !== null &&
257
+ // op[op.length - 2] == 'clientWantsToStart'
258
+ // ) {
259
+ // //client wants to halt generation
260
+ // haltGeneration(op[1]);
261
+ // }
262
+ // }
269
263
  });
270
264
  });
271
265
 
package/src/types.ts CHANGED
@@ -87,23 +87,23 @@ export type ShareDBDoc<T> = {
87
87
  // Generated by the client.
88
88
  export type AIStreamId = string;
89
89
 
90
- // TODO define this interface.
91
- export type AIStreamStatus = {
92
- // This field is managed by the client.
93
- // * `true` if the client wants the server to start the generation.
94
- // * `false` if the client does not want the server to start the generation.
95
- clientWantsToStart: boolean;
96
-
97
- // This field is managed by the server.
98
- // * `true` if the server is running the generation.
99
- // * `false` if the server is not running the generation.
100
- serverIsRunning: boolean;
90
+ // // TODO define this interface.
91
+ // export type AIStreamStatus = {
92
+ // // This field is managed by the client.
93
+ // // * `true` if the client wants the server to start the generation.
94
+ // // * `false` if the client does not want the server to start the generation.
95
+ // clientWantsToStart: boolean;
101
96
 
102
- text: string;
97
+ // // This field is managed by the server.
98
+ // // * `true` if the server is running the generation.
99
+ // // * `false` if the server is not running the generation.
100
+ // serverIsRunning: boolean;
103
101
 
104
- startingInsertionCursor: number;
105
- fileId: FileId;
106
- };
102
+ // text: string;
103
+
104
+ // startingInsertionCursor: number;
105
+ // fileId: FileId;
106
+ // };
107
107
 
108
108
  /// Alternative universe
109
109
 
@@ -161,12 +161,12 @@ export type VZCodeContent = {
161
161
  // client and server.
162
162
  // * This can power the transition of the icon from
163
163
  // lightning bolt to stop sign and back (when stream finishes)
164
- aiStreams?: {
165
- // These need to be removed at some point?
166
- [aiStreamId: AIStreamId]: {
167
- AIStreamStatus: AIStreamStatus;
168
- };
169
- };
164
+ // aiStreams?: {
165
+ // // These need to be removed at some point?
166
+ // [aiStreamId: AIStreamId]: {
167
+ // AIStreamStatus: AIStreamStatus;
168
+ // };
169
+ // };
170
170
 
171
171
  // TODO consider is there a way to replace the HTTP request to
172
172
  // the AIAssist endpoint with a manipulation of the ShareDB document?
@@ -1,133 +0,0 @@
1
- import { EditorView, keymap } from '@codemirror/view';
2
- import { json1Presence } from '../ot';
3
- import {
4
- FileId,
5
- File,
6
- ShareDBDoc,
7
- VZCodeContent,
8
- } from '../types';
9
- import { generateRequestId } from './CodeEditor/typeScriptCompletions';
10
-
11
- import { TabState } from './vzReducer';
12
-
13
- const { insertOp, replaceOp } = json1Presence;
14
-
15
- export const AIAssist = ({
16
- shareDBDoc,
17
- // The file id of the file the AI should assist with.
18
- fileId,
19
- tabList,
20
- }: {
21
- shareDBDoc: ShareDBDoc<VZCodeContent>;
22
- fileId: string;
23
- aiAssistEndpoint: string;
24
- aiAssistOptions?: {
25
- [key: string]: any;
26
- };
27
- tabList: Array<TabState>;
28
- }) =>
29
- keymap.of([
30
- {
31
- key: 'control-m',
32
- run: (view: EditorView) => {
33
- if (
34
- shareDBDoc.data.aiStreams === undefined ||
35
- shareDBDoc.data.aiStreams[mostRecentStreamId] ===
36
- null ||
37
- shareDBDoc.data.aiStreams[mostRecentStreamId]
38
- ?.AIStreamStatus.serverIsRunning !== true
39
- ) {
40
- startAIAssist(view, shareDBDoc, fileId, tabList);
41
- } else {
42
- haltAIAssist(shareDBDoc);
43
- }
44
-
45
- return true;
46
- },
47
- },
48
- ]);
49
-
50
- // TODO use ref forwarding instead of `export let`
51
- export let mostRecentStreamId = null;
52
-
53
- export const startAIAssist = async (
54
- view: EditorView,
55
- shareDBDoc: ShareDBDoc<VZCodeContent>,
56
- fileId: FileId,
57
- tabList: Array<TabState>,
58
- // TODO mostRecentStreamIdRef: React.MutableRefObject<string>,
59
- ) => {
60
- const textToSend =
61
- (await generateFilesContext(
62
- tabList.map(
63
- (tabState) =>
64
- shareDBDoc.data.files[tabState.fileId],
65
- ),
66
- )) +
67
- 'Current File:\n' +
68
- view.state.sliceDoc(0, view.state.selection.main.to);
69
-
70
- console.log(textToSend);
71
-
72
- // TODO mostRecentStreamIdRef.current = generateRequestId();
73
- mostRecentStreamId = generateRequestId();
74
-
75
- if (shareDBDoc.data['aiStreams'] === undefined) {
76
- shareDBDoc.submitOp(insertOp(['aiStreams'], {}), {
77
- source: 'AIClient',
78
- });
79
- }
80
-
81
- shareDBDoc.submitOp(
82
- insertOp(['aiStreams', mostRecentStreamId], {
83
- AIStreamStatus: {
84
- clientWantsToStart: true,
85
- serverIsRunning: false,
86
- text: textToSend,
87
- insertionCursor: view.state.selection.main.to,
88
- fileId: fileId,
89
- },
90
- }),
91
- { source: 'AIClient' },
92
- );
93
- };
94
-
95
- export const haltAIAssist = (
96
- shareDBDoc: ShareDBDoc<VZCodeContent>,
97
- ) => {
98
- const haltGenerationOp = replaceOp(
99
- [
100
- 'aiStreams',
101
- mostRecentStreamId,
102
- 'AIStreamStatus',
103
- 'clientWantsToStart',
104
- ],
105
- true,
106
- false,
107
- );
108
-
109
- shareDBDoc.submitOp(haltGenerationOp, {
110
- source: 'AIClient',
111
- });
112
- };
113
-
114
- export const generateFilesContext = async (
115
- goodFiles: File[],
116
- ): Promise<string> => {
117
- const input = goodFiles
118
- .map((file) => {
119
- const nameSubstring = file.name
120
- // ?.substring(0, maxFileNameLength)
121
- .trim();
122
-
123
- const textSubstring = file.text
124
- // ?.substring(0, maxFileTextLength)
125
- .trim();
126
-
127
- // Generate Markdown that AI will understand.
128
- return `File \`${nameSubstring}\`:\n\`\`\`${textSubstring}\`\`\``;
129
- })
130
- .join('\n\n');
131
-
132
- return input;
133
- };