vzcode 0.26.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-a9585cbc.js +111 -0
- package/dist/assets/index-d30c9f1b.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/client/AIAssist.ts +46 -0
- package/src/client/App.tsx +86 -43
- package/src/client/CodeEditor/getOrCreateEditor.ts +22 -2
- package/src/client/CodeEditor/index.tsx +75 -12
- package/src/client/CodeEditor/json1PresenceBroadcast.ts +6 -2
- package/src/client/CodeEditor/json1PresenceDisplay.ts +0 -45
- package/src/client/CodeEditor/style.scss +5 -1
- package/src/client/CodeErrorOverlay/index.tsx +37 -0
- package/src/client/CodeErrorOverlay/style.scss +41 -0
- package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
- package/src/client/PresenceNotifications/index.tsx +118 -103
- package/src/client/SplitPaneResizeContext.tsx +12 -7
- package/src/client/TabList/Tab.tsx +20 -6
- package/src/client/TabList/index.tsx +18 -8
- package/src/client/TabList/style.scss +5 -0
- package/src/client/VZSettings.tsx +33 -20
- package/src/client/VZSidebar/{ConfirmationBox.tsx → DeleteConfirmationModal.tsx} +17 -14
- package/src/client/VZSidebar/DirectoryListing.tsx +27 -26
- package/src/client/VZSidebar/FileListing.tsx +16 -19
- package/src/client/VZSidebar/Item.tsx +83 -37
- package/src/client/VZSidebar/Listing.tsx +17 -14
- package/src/client/VZSidebar/index.tsx +87 -45
- package/src/client/VZSidebar/styles.scss +21 -1
- package/src/client/style.scss +3 -0
- package/src/client/useActions.ts +25 -6
- package/src/client/useEditorCache.ts +2 -1
- package/src/client/useFileCRUD.ts +29 -27
- package/src/client/usernameLocalStorage.ts +43 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
- package/src/client/vzReducer/closeTabsReducer.ts +68 -0
- package/src/client/vzReducer/createInitialState.ts +19 -0
- package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
- package/src/client/vzReducer/index.ts +107 -0
- package/src/client/vzReducer/openTabReducer.test.ts +110 -0
- package/src/client/vzReducer/openTabReducer.ts +57 -0
- package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
- package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
- package/src/client/vzReducer/setThemeReducer.ts +9 -0
- package/src/client/vzReducer/setUsernameReducer.ts +9 -0
- package/src/index.ts +9 -3
- package/src/server/handleAIAssist.js +69 -0
- package/src/server/index.js +9 -0
- package/src/types.ts +12 -5
- package/dist/assets/index-284b7339.js +0 -111
- package/dist/assets/index-e160f758.css +0 -1
- package/src/client/PrettierErrorOverlay/index.tsx +0 -9
- package/src/client/PrettierErrorOverlay/style.scss +0 -9
- package/src/client/vzReducer.ts +0 -118
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { openTabReducer } from './openTabReducer';
|
|
3
|
+
import { VZAction, VZState, createInitialState } from '.';
|
|
4
|
+
import { defaultTheme } from '../themes';
|
|
5
|
+
|
|
6
|
+
const initialState: VZState = createInitialState({
|
|
7
|
+
defaultTheme,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('openTabReducer', () => {
|
|
11
|
+
it('Opens a new tab', () => {
|
|
12
|
+
const action: VZAction = {
|
|
13
|
+
type: 'open_tab',
|
|
14
|
+
fileId: 'file1',
|
|
15
|
+
isTransient: false,
|
|
16
|
+
};
|
|
17
|
+
const newState = openTabReducer(initialState, action);
|
|
18
|
+
|
|
19
|
+
expect(newState.tabList.length).toBe(1);
|
|
20
|
+
expect(newState.tabList[0].fileId).toBe('file1');
|
|
21
|
+
expect(newState.tabList[0].isTransient).toBe(false);
|
|
22
|
+
expect(newState.activeFileId).toBe('file1');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('Replaces a transient tab', () => {
|
|
26
|
+
const stateWithTransientTab = {
|
|
27
|
+
...initialState,
|
|
28
|
+
tabList: [{ fileId: 'file2', isTransient: true }],
|
|
29
|
+
};
|
|
30
|
+
const action: VZAction = {
|
|
31
|
+
type: 'open_tab',
|
|
32
|
+
fileId: 'file1',
|
|
33
|
+
isTransient: true,
|
|
34
|
+
};
|
|
35
|
+
const newState = openTabReducer(
|
|
36
|
+
stateWithTransientTab,
|
|
37
|
+
action,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(newState.tabList.length).toBe(1);
|
|
41
|
+
expect(newState.tabList[0].fileId).toBe('file1');
|
|
42
|
+
expect(newState.tabList[0].isTransient).toBe(true);
|
|
43
|
+
expect(newState.activeFileId).toBe('file1');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('Does not modify a non-transient tab', () => {
|
|
47
|
+
const stateWithNonTransientTab = {
|
|
48
|
+
...initialState,
|
|
49
|
+
tabList: [{ fileId: 'file1', isTransient: false }],
|
|
50
|
+
};
|
|
51
|
+
const action: VZAction = {
|
|
52
|
+
type: 'open_tab',
|
|
53
|
+
fileId: 'file1',
|
|
54
|
+
isTransient: true,
|
|
55
|
+
};
|
|
56
|
+
const newState = openTabReducer(
|
|
57
|
+
stateWithNonTransientTab,
|
|
58
|
+
action,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(newState.tabList.length).toBe(1);
|
|
62
|
+
expect(newState.tabList[0].fileId).toBe('file1');
|
|
63
|
+
expect(newState.tabList[0].isTransient).toBe(false); // should remain non-transient
|
|
64
|
+
expect(newState.activeFileId).toBe('file1');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('Activates an already open tab', () => {
|
|
68
|
+
const stateWithMultipleTabs = {
|
|
69
|
+
...initialState,
|
|
70
|
+
tabList: [
|
|
71
|
+
{ fileId: 'file1', isTransient: false },
|
|
72
|
+
{ fileId: 'file2', isTransient: false },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const action: VZAction = {
|
|
76
|
+
type: 'open_tab',
|
|
77
|
+
fileId: 'file1',
|
|
78
|
+
isTransient: false,
|
|
79
|
+
};
|
|
80
|
+
const newState = openTabReducer(
|
|
81
|
+
stateWithMultipleTabs,
|
|
82
|
+
action,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(newState.tabList.length).toBe(2);
|
|
86
|
+
expect(newState.activeFileId).toBe('file1');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('Sets editorWantsFocus to true for non-transient tabs', () => {
|
|
90
|
+
const action: VZAction = {
|
|
91
|
+
type: 'open_tab',
|
|
92
|
+
fileId: 'file3',
|
|
93
|
+
isTransient: false, // This tab is persistent
|
|
94
|
+
};
|
|
95
|
+
const newState = openTabReducer(initialState, action);
|
|
96
|
+
|
|
97
|
+
expect(newState.editorWantsFocus).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('Sets editorWantsFocus to false for transient tabs', () => {
|
|
101
|
+
const action: VZAction = {
|
|
102
|
+
type: 'open_tab',
|
|
103
|
+
fileId: 'file4',
|
|
104
|
+
isTransient: true, // This tab is transient
|
|
105
|
+
};
|
|
106
|
+
const newState = openTabReducer(initialState, action);
|
|
107
|
+
|
|
108
|
+
expect(newState.editorWantsFocus).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { TabState, VZAction, VZState } from '.';
|
|
2
|
+
|
|
3
|
+
export const openTabReducer = (
|
|
4
|
+
state: VZState,
|
|
5
|
+
action: VZAction,
|
|
6
|
+
): VZState => {
|
|
7
|
+
if (action.type !== 'open_tab') return state;
|
|
8
|
+
|
|
9
|
+
// Check if the tab is already open and if it's transient.
|
|
10
|
+
const existingTabIndex = state.tabList.findIndex(
|
|
11
|
+
(tab) => tab.fileId === action.fileId,
|
|
12
|
+
);
|
|
13
|
+
const tabExists = existingTabIndex !== -1;
|
|
14
|
+
const existingTabIsTransient =
|
|
15
|
+
tabExists &&
|
|
16
|
+
state.tabList[existingTabIndex].isTransient;
|
|
17
|
+
|
|
18
|
+
// Find if there's any other transient tab.
|
|
19
|
+
const transientTabIndex = state.tabList.findIndex(
|
|
20
|
+
(tab) =>
|
|
21
|
+
tab.isTransient && tab.fileId !== action.fileId,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// The new tab state.
|
|
25
|
+
const newTabState: TabState = {
|
|
26
|
+
fileId: action.fileId,
|
|
27
|
+
isTransient: action.isTransient,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let newTabList: Array<TabState>;
|
|
31
|
+
|
|
32
|
+
if (tabExists) {
|
|
33
|
+
if (existingTabIsTransient) {
|
|
34
|
+
newTabList = state.tabList.map((tabState) =>
|
|
35
|
+
tabState.fileId === action.fileId
|
|
36
|
+
? newTabState
|
|
37
|
+
: tabState,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
newTabList = [...state.tabList];
|
|
41
|
+
}
|
|
42
|
+
} else if (transientTabIndex !== -1) {
|
|
43
|
+
newTabList = [...state.tabList];
|
|
44
|
+
newTabList[transientTabIndex] = newTabState;
|
|
45
|
+
} else {
|
|
46
|
+
newTabList = [...state.tabList, newTabState];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
activeFileId: action.fileId,
|
|
52
|
+
tabList: newTabList,
|
|
53
|
+
|
|
54
|
+
// If the tab is persistent, the editor should focus on the next render.
|
|
55
|
+
editorWantsFocus: !action.isTransient,
|
|
56
|
+
};
|
|
57
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -15,16 +15,22 @@ export type {
|
|
|
15
15
|
|
|
16
16
|
export { VZSidebar } from './client/VZSidebar';
|
|
17
17
|
export { VZSettings } from './client/VZSettings';
|
|
18
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
SplitPaneResizeProvider,
|
|
20
|
+
SplitPaneResizeContext,
|
|
21
|
+
} from './client/SplitPaneResizeContext';
|
|
19
22
|
export { Resizer } from './client/Resizer';
|
|
20
23
|
export { TabList } from './client/TabList';
|
|
21
24
|
export { CodeEditor } from './client/CodeEditor';
|
|
22
25
|
export { usePrettier } from './client/usePrettier';
|
|
23
|
-
export {
|
|
26
|
+
export { CodeErrorOverlay } from './client/CodeErrorOverlay';
|
|
24
27
|
export { PresenceNotifications } from './client/PresenceNotifications';
|
|
25
28
|
export { useFileCRUD } from './client/useFileCRUD';
|
|
26
29
|
export { useSubmitOperation } from './client/useSubmitOperation';
|
|
27
|
-
export {
|
|
30
|
+
export {
|
|
31
|
+
vzReducer,
|
|
32
|
+
createInitialState,
|
|
33
|
+
} from './client/vzReducer';
|
|
28
34
|
export { useActions } from './client/useActions';
|
|
29
35
|
export { useOpenDirectories } from './client/useOpenDirectories';
|
|
30
36
|
export {
|
|
@@ -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
|
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface File {
|
|
|
30
30
|
text: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// FileTreePath
|
|
34
|
+
// * The path to the directory, e.g. "src/components", OR
|
|
35
|
+
// * The path to the file e.g. "src/components/HelloWorld.js".
|
|
36
|
+
export type FileTreePath = string;
|
|
37
|
+
|
|
33
38
|
// FileTree
|
|
34
39
|
// * A tree of files.
|
|
35
40
|
// * Each node in the tree is either a directory or a file.
|
|
@@ -38,13 +43,9 @@ export interface File {
|
|
|
38
43
|
// * Each file has a `fileId` string.
|
|
39
44
|
// * Each directory has a `name` string.
|
|
40
45
|
// * Each directory has a `path` string.
|
|
41
|
-
// * The `path` string is the path to the directory.
|
|
42
|
-
// e.g. "src/components".
|
|
43
|
-
// * The `path` string is the path to the file.
|
|
44
|
-
// e.g. "src/components/HelloWorld.js".
|
|
45
46
|
export interface FileTree {
|
|
46
47
|
name: string;
|
|
47
|
-
path?:
|
|
48
|
+
path?: FileTreePath;
|
|
48
49
|
children?: Array<FileTree | FileTreeFile>;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -93,3 +94,9 @@ export type VZCodeContent = {
|
|
|
93
94
|
// * Hot reloading is debounced when this is `false`.
|
|
94
95
|
isInteracting?: boolean;
|
|
95
96
|
};
|
|
97
|
+
|
|
98
|
+
// An id used for presence.
|
|
99
|
+
export type PresenceId = string;
|
|
100
|
+
|
|
101
|
+
// A user name for display in presence.
|
|
102
|
+
export type Username = string;
|