vzcode 0.45.0 → 0.46.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.
@@ -0,0 +1,101 @@
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
+ streams[streamId] = await openai.chat.completions.create({
63
+ // model: 'gpt-3.5-turbo',
64
+ model: 'gpt-4',
65
+
66
+ messages: [
67
+ {
68
+ role: 'system',
69
+ content:
70
+ 'Write typescript or javascript code to continue the current file, given the other files for context. Use // for comments.',
71
+ },
72
+ { role: 'user', content: inputText },
73
+ ],
74
+ stream: true,
75
+ });
76
+
77
+ for await (const part of streams[streamId]) {
78
+ const op = editOp(
79
+ ['files', fileId, 'text'],
80
+ 'text-unicode',
81
+ [
82
+ insertionCursor,
83
+ part.choices[0]?.delta?.content || '',
84
+ ],
85
+ );
86
+
87
+ shareDBDoc.submitOp(op, { source: AISourceName });
88
+
89
+ // Wait for 500ms
90
+ if (slowdown) {
91
+ await new Promise((resolve) => {
92
+ setTimeout(resolve, 1000);
93
+ });
94
+ }
95
+
96
+ insertionCursor += (
97
+ part.choices[0]?.delta?.content || ''
98
+ ).length;
99
+ }
100
+ shareDBDoc.off('op', accomodateDocChanges);
101
+ };
@@ -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
- };
@@ -1,101 +0,0 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
- import { Button } from '../bootstrap';
3
- import './style.scss';
4
- import {
5
- mostRecentStreamId,
6
- haltAIAssist,
7
- startAIAssist,
8
- } from '../AIAssist';
9
- import {
10
- FileId,
11
- ShareDBDoc,
12
- VZCodeContent,
13
- } from '../../types';
14
- import { EditorCache } from '../useEditorCache';
15
- import { TabState } from '../vzReducer';
16
- // From
17
- // https://primer.style/foundations/icons/zap-24
18
- const ZapSVG = () => (
19
- <svg
20
- xmlns="http://www.w3.org/2000/svg"
21
- viewBox="0 0 24 24"
22
- width="24"
23
- height="24"
24
- >
25
- <path d="M15.716 1.329a1.341 1.341 0 0 1 2.109 1.55L15.147 9h4.161c1.623 0 2.372 2.016 1.143 3.075L8.102 22.721a1.148 1.148 0 0 1-1.81-1.317L8.996 15H4.674c-1.619 0-2.37-2.008-1.148-3.07l12.19-10.6Zm.452 1.595L4.51 13.061a.25.25 0 0 0 .164.439h5.45a.749.749 0 0 1 .692 1.041l-2.559 6.066 11.215-9.668a.25.25 0 0 0-.164-.439H14a.75.75 0 0 1-.687-1.05Z"></path>
26
- </svg>
27
- );
28
-
29
- // From Wikimedia
30
- // https://commons.wikimedia.org/wiki/File:Stopsign.svg
31
- const StopSVG = () => (
32
- <svg
33
- width="15pt"
34
- height="15pt"
35
- version="1.1"
36
- viewBox="0 0 15 15"
37
- xmlns="http://www.w3.org/2000/svg"
38
- >
39
- <g transform="scale(.75)" fill="#fff" stroke="#000">
40
- <path d="m17.5 0.5c1.1055 0 2 0.89453 2 2v15c0 1.1055-0.89453 2-2 2h-15c-1.1055 0-2-0.89453-2-2v-15c0-1.1055 0.89453-2 2-2z" />
41
- </g>
42
- </svg>
43
- );
44
-
45
- export const AIAssistWidget = ({
46
- activeFileId,
47
- shareDBDoc,
48
- editorCache,
49
- tabList,
50
- }: {
51
- activeFileId: FileId;
52
- shareDBDoc: ShareDBDoc<VZCodeContent>;
53
- editorCache: EditorCache;
54
- tabList: Array<TabState>;
55
- }) => {
56
- const [AIAssistRunning, setAIAssistRunning] =
57
- useState(false);
58
-
59
- useEffect(() => {
60
- const handleOp = (op, source) => {
61
- if (op !== null && op[0] === 'aiStreams') {
62
- // The check if the value is true is required
63
- // because the value could be null rather than false.
64
- setAIAssistRunning(
65
- shareDBDoc.data.aiStreams[mostRecentStreamId]
66
- ?.AIStreamStatus.serverIsRunning === true,
67
- );
68
- }
69
- };
70
- shareDBDoc.on('op', handleOp);
71
-
72
- return () => {
73
- shareDBDoc.off('op', handleOp);
74
- };
75
- }, [shareDBDoc]);
76
-
77
- const handleClick = useCallback(() => {
78
- if (!AIAssistRunning) {
79
- startAIAssist(
80
- editorCache.get(activeFileId).editor,
81
- shareDBDoc,
82
- activeFileId,
83
- tabList,
84
- );
85
- } else {
86
- haltAIAssist(shareDBDoc);
87
- }
88
- }, [activeFileId, AIAssistRunning]);
89
-
90
- return (
91
- <div className="vz-code-ai-assist-widget">
92
- <Button
93
- variant="light"
94
- title="Start AI Assist"
95
- onClick={handleClick}
96
- >
97
- {AIAssistRunning ? <StopSVG /> : <ZapSVG />}
98
- </Button>
99
- </div>
100
- );
101
- };
@@ -1,10 +0,0 @@
1
- .vz-code-ai-assist-widget {
2
- position: absolute;
3
- bottom: 20px;
4
- right: 20px;
5
-
6
- .btn {
7
- /* Center the icon */
8
- padding: 6px 6px 7px 6px;
9
- }
10
- }