vzcode 0.27.0 → 0.28.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/index.html CHANGED
@@ -24,8 +24,8 @@
24
24
  href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css"
25
25
  rel="stylesheet"
26
26
  />
27
- <script type="module" crossorigin src="/assets/index-70b6d947.js"></script>
28
- <link rel="stylesheet" href="/assets/index-5f9f376e.css">
27
+ <script type="module" crossorigin src="/assets/index-a9585cbc.js"></script>
28
+ <link rel="stylesheet" href="/assets/index-d30c9f1b.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -64,6 +64,7 @@
64
64
  "@uiw/codemirror-theme-okaidia": "^4.21.20",
65
65
  "@uiw/codemirror-theme-xcode": "^4.21.20",
66
66
  "@uiw/codemirror-themes": "^4.21.20",
67
+ "body-parser": "^1.20.2",
67
68
  "codemirror": "^6.0.1",
68
69
  "codemirror-ot": "^4.1.0",
69
70
  "d3-array": "^3.2.4",
@@ -73,6 +74,7 @@
73
74
  "json0-ot-diff": "^1.1.2",
74
75
  "ngrok": "^4.3.3",
75
76
  "open": "^9.1.0",
77
+ "openai": "^4.14.1",
76
78
  "ot-json1": "^1.0.2",
77
79
  "react": "^18.2.0",
78
80
  "react-bootstrap": "^2.9.1",
@@ -0,0 +1,46 @@
1
+ import { EditorView, keymap } from '@codemirror/view';
2
+
3
+ export const AIAssist = ({
4
+ // The file id of the file the AI should assist with.
5
+ fileId,
6
+
7
+ // Optional endpoint override.
8
+ aiAssistEndpoint = '/AIAssist',
9
+
10
+ // Optional additional options to pass to the endpoint.
11
+ aiAssistOptions = {},
12
+ }: {
13
+ fileId: string;
14
+ aiAssistEndpoint: string;
15
+ aiAssistOptions?: {
16
+ [key: string]: any;
17
+ };
18
+ }) =>
19
+ keymap.of([
20
+ {
21
+ key: 'control-m',
22
+ run: (view: EditorView) => {
23
+ const textToSend = view.state.sliceDoc(
24
+ 0,
25
+ view.state.selection.main.to,
26
+ );
27
+
28
+ fetch(aiAssistEndpoint, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: JSON.stringify({
34
+ // Pass additional options to the AI Assist endpoint.
35
+ ...aiAssistOptions,
36
+
37
+ text: textToSend,
38
+ fileId,
39
+ cursorLocation: view.state.selection.main.to,
40
+ }),
41
+ });
42
+
43
+ return true;
44
+ },
45
+ },
46
+ ]);
@@ -28,6 +28,7 @@ import {
28
28
  EditorCacheValue,
29
29
  } from '../useEditorCache';
30
30
  import { ThemeLabel, themeOptionsByLabel } from '../themes';
31
+ import { AIAssist } from '../AIAssist';
31
32
 
32
33
  // Language extensions for CodeMirror.
33
34
  // Keys are file extensions.
@@ -66,6 +67,8 @@ export const getOrCreateEditor = ({
66
67
  onInteract,
67
68
  editorCache,
68
69
  usernameRef,
70
+ aiAssistEndpoint,
71
+ aiAssistOptions,
69
72
  }: {
70
73
  fileId: FileId;
71
74
 
@@ -85,6 +88,10 @@ export const getOrCreateEditor = ({
85
88
  onInteract?: () => void;
86
89
  editorCache: EditorCache;
87
90
  usernameRef: React.MutableRefObject<Username>;
91
+ aiAssistEndpoint?: string;
92
+ aiAssistOptions?: {
93
+ [key: string]: any;
94
+ };
88
95
  }): EditorCacheValue => {
89
96
  // Cache hit
90
97
  if (editorCache.has(fileId)) {
@@ -157,7 +164,7 @@ export const getOrCreateEditor = ({
157
164
  // using a Compartment.
158
165
  const languageCompartment = new Compartment();
159
166
  // See https://github.com/vizhub-core/vzcode/issues/55
160
- const getLanguageExtension = (fileExtension) => {
167
+ const getLanguageExtension = (fileExtension: string) => {
161
168
  const languageFunc = languageExtensions[fileExtension];
162
169
  return languageFunc ? languageFunc() : undefined;
163
170
  };
@@ -200,6 +207,14 @@ export const getOrCreateEditor = ({
200
207
 
201
208
  extensions.push(rotationIndicator);
202
209
 
210
+ extensions.push(
211
+ AIAssist({
212
+ fileId,
213
+ aiAssistEndpoint,
214
+ aiAssistOptions,
215
+ }),
216
+ );
217
+
203
218
  const editor = new EditorView({
204
219
  state: EditorState.create({
205
220
  doc: text,
@@ -30,6 +30,8 @@ export const CodeEditor = ({
30
30
  editorWantsFocus,
31
31
  editorNoLongerWantsFocus,
32
32
  username,
33
+ aiAssistEndpoint,
34
+ aiAssistOptions,
33
35
  }: {
34
36
  activeFileId: FileId;
35
37
  shareDBDoc: ShareDBDoc<VZCodeContent> | null;
@@ -51,6 +53,15 @@ export const CodeEditor = ({
51
53
  // Signals that the editor no longer wants focus.
52
54
  editorNoLongerWantsFocus: () => void;
53
55
  username: Username;
56
+
57
+ // The server endpoint for the AI Assist service.
58
+ aiAssistEndpoint?: string;
59
+
60
+ // Additional options to pass to the AI Assist service,
61
+ // an object with string values.
62
+ aiAssistOptions?: {
63
+ [key: string]: string;
64
+ };
54
65
  }) => {
55
66
  const ref = useRef<HTMLDivElement>(null);
56
67
 
@@ -98,6 +109,8 @@ export const CodeEditor = ({
98
109
  onInteract,
99
110
  editorCache,
100
111
  usernameRef,
112
+ aiAssistEndpoint,
113
+ aiAssistOptions,
101
114
  }),
102
115
  [
103
116
  activeFileId,
@@ -109,6 +122,8 @@ export const CodeEditor = ({
109
122
  onInteract,
110
123
  editorCache,
111
124
  usernameRef,
125
+ aiAssistEndpoint,
126
+ aiAssistOptions,
112
127
  ],
113
128
  );
114
129
 
@@ -1,20 +1,30 @@
1
- import { useCallback } from 'react';
1
+ import { useState, useCallback, useEffect } from 'react';
2
2
  import './style.scss';
3
3
 
4
- // Feature flag for the button that dismisses the error.
5
- const enableErrorDismiss = false;
4
+ const enableErrorDismiss = true;
6
5
 
7
6
  export const CodeErrorOverlay = ({
8
7
  errorMessage,
9
8
  }: {
10
9
  errorMessage: string | null;
11
10
  }) => {
11
+ const [isOverlayVisible, setIsOverlayVisible] =
12
+ useState(true);
13
+
14
+ // If errorMessage changes, set the overlay to be visible
15
+ useEffect(() => {
16
+ if (errorMessage !== null) {
17
+ setIsOverlayVisible(true);
18
+ }
19
+ }, [errorMessage]);
20
+
12
21
  const handleCloseClick = useCallback(() => {
13
- console.log('TODO dismiss the error');
22
+ // Set the visibility state to false when the button is clicked
23
+ setIsOverlayVisible(false);
14
24
  }, []);
15
25
 
16
- return errorMessage !== null ? (
17
- <pre className="vz-code-error-overlay">
26
+ return isOverlayVisible && errorMessage !== null ? (
27
+ <div className="vz-code-error-overlay">
18
28
  {errorMessage}
19
29
  {enableErrorDismiss ? (
20
30
  <div
@@ -22,6 +32,6 @@ export const CodeErrorOverlay = ({
22
32
  onClick={handleCloseClick}
23
33
  ></div>
24
34
  ) : null}
25
- </pre>
35
+ </div>
26
36
  ) : null;
27
37
  };
@@ -10,21 +10,32 @@
10
10
  // pointer-events: none;
11
11
  margin: 0;
12
12
  padding: 8px;
13
-
14
13
  border-top-left-radius: 12px;
15
14
  border-top-right-radius: 12px;
16
15
 
17
- // TODO unify with styles in tab close button
16
+ /* Define your custom styles here */
17
+ /* For example, change the font size and color */
18
+ font-size: 16px;
19
+ color: #FF5733;
20
+
21
+ /* Add a box shadow for a subtle effect */
22
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
23
+
24
+ /* Customize the dismiss button style */
18
25
  .error-dismiss-button {
19
26
  position: absolute;
20
- top: 0;
21
- right: 0;
22
- margin: 5px;
27
+ top: 5px; /* Adjust the top position */
28
+ right: 5px; /* Adjust the right position */
29
+ margin: 0;
30
+ padding: 5px; /* Adjust padding for the button */
23
31
  cursor: pointer;
24
32
  color: white;
33
+ background-color: #FF5733; /* Change the button's background color */
34
+ border-radius: 50%; /* Make it circular */
25
35
  }
36
+
37
+ /* Style the button on hover */
26
38
  .error-dismiss-button:hover {
27
- border-radius: 50%;
28
- background-color: rgba(255, 255, 255, 0.2);
39
+ background-color: #FF826D; /* Change the background color on hover */
29
40
  }
30
41
  }
@@ -13,6 +13,17 @@ import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
13
13
  import { Listing } from './Listing';
14
14
  import './styles.scss';
15
15
 
16
+ const CreateFileButton = ({ createFile }) => {
17
+ return (
18
+ <i
19
+ className="bx bxs-file-plus new-btn"
20
+ style={{ color: '#dbdde1' }}
21
+ onClick={createFile}
22
+ // TODO better tooltip
23
+ title="Create file"
24
+ ></i>
25
+ );
26
+ };
16
27
  export const VZSidebar = ({
17
28
  files,
18
29
  createFile,
@@ -71,6 +82,12 @@ export const VZSidebar = ({
71
82
  [openTab],
72
83
  );
73
84
 
85
+ // True if files exist.
86
+ const filesExist =
87
+ fileTree &&
88
+ fileTree.children &&
89
+ fileTree.children.length > 0;
90
+
74
91
  return (
75
92
  <div
76
93
  className="vz-sidebar"
@@ -80,36 +97,42 @@ export const VZSidebar = ({
80
97
  <div className="full-box">
81
98
  <div className="sidebar-section-hint">Files</div>
82
99
  <div>
83
- <i
84
- className="bx bxs-file-plus new-btn"
85
- style={{ color: '#dbdde1' }}
86
- onClick={createFile}
87
- ></i>
100
+ <CreateFileButton createFile={createFile} />
88
101
  </div>
89
102
  </div>
90
- {fileTree
91
- ? fileTree.children.map((entity) => {
92
- const { fileId } = entity as FileTreeFile;
93
- const { path } = entity as FileTree;
94
- const key = fileId ? fileId : path;
95
- return (
96
- <Listing
97
- key={key}
98
- entity={entity}
99
- renameFile={renameFile}
100
- deleteFile={deleteFile}
101
- deleteDirectory={deleteDirectory}
102
- handleFileClick={handleFileClick}
103
- handleFileDoubleClick={
104
- handleFileDoubleClick
105
- }
106
- isDirectoryOpen={isDirectoryOpen}
107
- toggleDirectory={toggleDirectory}
108
- activeFileId={activeFileId}
109
- />
110
- );
111
- })
112
- : null}
103
+ {filesExist ? (
104
+ fileTree.children.map((entity) => {
105
+ const { fileId } = entity as FileTreeFile;
106
+ const { path } = entity as FileTree;
107
+ const key = fileId ? fileId : path;
108
+ return (
109
+ <Listing
110
+ key={key}
111
+ entity={entity}
112
+ renameFile={renameFile}
113
+ deleteFile={deleteFile}
114
+ deleteDirectory={deleteDirectory}
115
+ handleFileClick={handleFileClick}
116
+ handleFileDoubleClick={
117
+ handleFileDoubleClick
118
+ }
119
+ isDirectoryOpen={isDirectoryOpen}
120
+ toggleDirectory={toggleDirectory}
121
+ activeFileId={activeFileId}
122
+ />
123
+ );
124
+ })
125
+ ) : (
126
+ <div className="empty">
127
+ <div className="empty-text">
128
+ It looks like you don't have any files yet!
129
+ Click the "Create file" button below to create
130
+ your first file.
131
+ </div>
132
+
133
+ <CreateFileButton createFile={createFile} />
134
+ </div>
135
+ )}
113
136
  </div>
114
137
  {disableSettings ? null : (
115
138
  <div
@@ -16,6 +16,9 @@
16
16
 
17
17
  .files {
18
18
  overflow: auto;
19
+ display: flex;
20
+ flex-direction: column;
21
+ flex: 1;
19
22
  }
20
23
 
21
24
  .file-or-directory {
@@ -46,7 +49,6 @@
46
49
  gap: 8px;
47
50
  }
48
51
  .utilities {
49
- // margin-right: 3px;
50
52
  transform: scale(1.2);
51
53
  }
52
54
 
@@ -85,4 +87,22 @@
85
87
  margin: 10px;
86
88
  border-radius: 4px;
87
89
  }
90
+
91
+ /* Empty state for when there are no files */
92
+ .empty {
93
+ display: flex;
94
+ flex: 1;
95
+ flex-direction: column;
96
+ align-items: center;
97
+ justify-content: space-evenly;
98
+ margin: 10px;
99
+ padding: 20px;
100
+ border: 1px dashed rgba(255, 255, 255, 0.5); /* White dashed outline */
101
+ border-radius: 10px;
102
+ }
103
+
104
+ .empty-text {
105
+ margin-bottom: 10px;
106
+ font-size: 14px;
107
+ }
88
108
  }
@@ -0,0 +1,69 @@
1
+ import OpenAI from 'openai';
2
+ import { editOp } from 'ot-json1';
3
+
4
+ const openai = new OpenAI();
5
+
6
+ export async function generateAIResponse({
7
+ inputText,
8
+ insertionCursor,
9
+ fileId,
10
+ shareDBDoc,
11
+ }) {
12
+ const stream = await openai.chat.completions.create({
13
+ model: 'gpt-3.5-turbo',
14
+ messages: [
15
+ {
16
+ role: 'system',
17
+ content:
18
+ 'Write typescript or javascript code that would follow the prompt',
19
+ },
20
+ { role: 'user', content: inputText },
21
+ ],
22
+ stream: true,
23
+ });
24
+
25
+ let currentCursor = insertionCursor;
26
+
27
+ for await (const part of stream) {
28
+ const op = editOp(
29
+ ['files', fileId, 'text'],
30
+ 'text-unicode',
31
+ [
32
+ currentCursor,
33
+ part.choices[0]?.delta?.content || '',
34
+ ],
35
+ );
36
+
37
+ shareDBDoc.submitOp(op);
38
+ currentCursor += (part.choices[0]?.delta?.content || '')
39
+ .length;
40
+ }
41
+ }
42
+
43
+ export const handleAIAssist =
44
+ (shareDBDoc) => async (req, res) => {
45
+ const {
46
+ text: inputText,
47
+ cursorLocation: insertionCursor,
48
+ fileId,
49
+ } = req.body;
50
+
51
+ try {
52
+ await generateAIResponse({
53
+ inputText,
54
+ insertionCursor,
55
+ fileId,
56
+ shareDBDoc,
57
+ });
58
+
59
+ res
60
+ .status(200)
61
+ .send({ message: 'Operation successful!' });
62
+ } catch (error) {
63
+ console.error('handleAIAssist error:', error);
64
+ res.status(500).send({
65
+ message: 'Internal Server Error',
66
+ error: error.message,
67
+ });
68
+ }
69
+ };
@@ -12,6 +12,8 @@ import ngrok from 'ngrok';
12
12
  import dotenv from 'dotenv';
13
13
  import { computeInitialDocument } from './computeInitialDocument.js';
14
14
  import { json1Presence } from '../ot.js';
15
+ import bodyParser from 'body-parser';
16
+ import { handleAIAssist } from './handleAIAssist.js';
15
17
 
16
18
  dotenv.config({ path: '../../.env' });
17
19
 
@@ -95,6 +97,13 @@ const shareDBConnection = shareDBBackend.connect();
95
97
  const shareDBDoc = shareDBConnection.get('documents', '1');
96
98
  shareDBDoc.create(initialDocument, json1Presence.type.uri);
97
99
 
100
+ // Handle AI Assist requests.
101
+ app.post(
102
+ '/AIAssist',
103
+ bodyParser.json(),
104
+ handleAIAssist(shareDBDoc),
105
+ );
106
+
98
107
  // The state of the document when files were last auto-saved.
99
108
  let previousDocument = initialDocument;
100
109