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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-a9585cbc.js +111 -0
  3. package/dist/assets/index-d30c9f1b.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +6 -4
  6. package/src/client/AIAssist.ts +46 -0
  7. package/src/client/App.tsx +86 -43
  8. package/src/client/CodeEditor/getOrCreateEditor.ts +22 -2
  9. package/src/client/CodeEditor/index.tsx +75 -12
  10. package/src/client/CodeEditor/json1PresenceBroadcast.ts +6 -2
  11. package/src/client/CodeEditor/json1PresenceDisplay.ts +0 -45
  12. package/src/client/CodeEditor/style.scss +5 -1
  13. package/src/client/CodeErrorOverlay/index.tsx +37 -0
  14. package/src/client/CodeErrorOverlay/style.scss +41 -0
  15. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  16. package/src/client/PresenceNotifications/index.tsx +118 -103
  17. package/src/client/SplitPaneResizeContext.tsx +12 -7
  18. package/src/client/TabList/Tab.tsx +20 -6
  19. package/src/client/TabList/index.tsx +18 -8
  20. package/src/client/TabList/style.scss +5 -0
  21. package/src/client/VZSettings.tsx +33 -20
  22. package/src/client/VZSidebar/{ConfirmationBox.tsx → DeleteConfirmationModal.tsx} +17 -14
  23. package/src/client/VZSidebar/DirectoryListing.tsx +27 -26
  24. package/src/client/VZSidebar/FileListing.tsx +16 -19
  25. package/src/client/VZSidebar/Item.tsx +83 -37
  26. package/src/client/VZSidebar/Listing.tsx +17 -14
  27. package/src/client/VZSidebar/index.tsx +87 -45
  28. package/src/client/VZSidebar/styles.scss +21 -1
  29. package/src/client/style.scss +3 -0
  30. package/src/client/useActions.ts +25 -6
  31. package/src/client/useEditorCache.ts +2 -1
  32. package/src/client/useFileCRUD.ts +29 -27
  33. package/src/client/usernameLocalStorage.ts +43 -0
  34. package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
  35. package/src/client/vzReducer/closeTabsReducer.ts +68 -0
  36. package/src/client/vzReducer/createInitialState.ts +19 -0
  37. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  38. package/src/client/vzReducer/index.ts +107 -0
  39. package/src/client/vzReducer/openTabReducer.test.ts +110 -0
  40. package/src/client/vzReducer/openTabReducer.ts +57 -0
  41. package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
  42. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  43. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  44. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  45. package/src/index.ts +9 -3
  46. package/src/server/handleAIAssist.js +69 -0
  47. package/src/server/index.js +9 -0
  48. package/src/types.ts +12 -5
  49. package/dist/assets/index-284b7339.js +0 -111
  50. package/dist/assets/index-e160f758.css +0 -1
  51. package/src/client/PrettierErrorOverlay/index.tsx +0 -9
  52. package/src/client/PrettierErrorOverlay/style.scss +0 -9
  53. 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
+ };
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const setActiveFileIdReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'set_active_file_id'
8
+ ? { ...state, activeFileId: action.activeFileId }
9
+ : state;
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const setIsSettingsOpenReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'set_is_settings_open'
8
+ ? { ...state, isSettingsOpen: action.value }
9
+ : state;
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const setThemeReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'set_theme'
8
+ ? { ...state, theme: action.themeLabel }
9
+ : state;
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const setUsernameReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'set_username'
8
+ ? { ...state, username: action.username }
9
+ : state;
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 { SplitPaneResizeContext } from './client/SplitPaneResizeContext';
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 { PrettierErrorOverlay } from './client/PrettierErrorOverlay';
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 { vzReducer } from './client/vzReducer';
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
+ };
@@ -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?: string;
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;