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/README.md +1 -1
- package/dist/assets/{index-70b6d947.js → index-a9585cbc.js} +35 -35
- package/dist/assets/{index-5f9f376e.css → index-d30c9f1b.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/client/AIAssist.ts +46 -0
- package/src/client/CodeEditor/getOrCreateEditor.ts +16 -1
- package/src/client/CodeEditor/index.tsx +15 -0
- package/src/client/CodeErrorOverlay/index.tsx +17 -7
- package/src/client/CodeErrorOverlay/style.scss +18 -7
- package/src/client/VZSidebar/index.tsx +51 -28
- package/src/client/VZSidebar/styles.scss +21 -1
- package/src/server/handleAIAssist.js +69 -0
- package/src/server/index.js +9 -0
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-
|
|
28
|
-
<link rel="stylesheet" href="/assets/index-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
// Set the visibility state to false when the button is clicked
|
|
23
|
+
setIsOverlayVisible(false);
|
|
14
24
|
}, []);
|
|
15
25
|
|
|
16
|
-
return errorMessage !== null ? (
|
|
17
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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:
|
|
21
|
-
right:
|
|
22
|
-
margin:
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
};
|
package/src/server/index.js
CHANGED
|
@@ -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
|
|