vzcode 1.32.0 → 1.34.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 (44) hide show
  1. package/README.md +0 -1
  2. package/dist/assets/index-ajiBaTmx.js +263 -0
  3. package/dist/assets/index-lvLw6dCW.css +1 -0
  4. package/dist/assets/worker-BrwN8AXx.js +330 -0
  5. package/dist/assets/worker-CkHcCP1n.js +136 -0
  6. package/dist/assets/worker-DkYJN5WQ.js +453 -0
  7. package/dist/index.html +2 -2
  8. package/package.json +42 -37
  9. package/src/client/CodeEditor/getOrCreateEditor.ts +18 -5
  10. package/src/client/Icons/MicSVG.tsx +1 -1
  11. package/src/client/VZCodeContext.tsx +18 -2
  12. package/src/client/VZSidebar/AIChat/ChatInput.tsx +65 -0
  13. package/src/client/VZSidebar/AIChat/Message.tsx +36 -0
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +62 -0
  15. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +25 -0
  16. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +13 -0
  17. package/src/client/VZSidebar/AIChat/index.tsx +75 -0
  18. package/src/client/VZSidebar/AIChat/styles.scss +256 -0
  19. package/src/client/VZSidebar/index.tsx +53 -8
  20. package/src/client/VZSidebar/styles.scss +8 -1
  21. package/src/client/diff.js +1 -1
  22. package/src/client/featureFlags.ts +2 -0
  23. package/src/client/useActions.ts +20 -0
  24. package/src/client/useEditorCache.ts +1 -0
  25. package/src/client/useFileCRUD.ts +47 -0
  26. package/src/client/useOpenDirectories.ts +43 -12
  27. package/src/client/utils/fileExtension.ts +10 -0
  28. package/src/client/vzReducer/aiChatReducer.ts +31 -0
  29. package/src/client/vzReducer/createInitialState.ts +2 -0
  30. package/src/client/vzReducer/index.ts +20 -0
  31. package/src/ot.js +10 -2
  32. package/src/server/aiChatHandler/aiEditing.js +42 -0
  33. package/src/server/aiChatHandler/chatOperations.js +195 -0
  34. package/src/server/aiChatHandler/errorHandling.js +53 -0
  35. package/src/server/aiChatHandler/index.js +69 -0
  36. package/src/server/aiChatHandler/llmStreaming.js +105 -0
  37. package/src/server/aiChatHandler/validation.js +24 -0
  38. package/src/server/handleAIChatMessage.js +2 -0
  39. package/src/server/index.js +8 -0
  40. package/dist/assets/index-DTtcN2MP.css +0 -1
  41. package/dist/assets/index-sreAszZh.js +0 -240
  42. package/dist/assets/worker-BKfYWklR.js +0 -136
  43. package/dist/assets/worker-COyDRdqW.js +0 -453
  44. package/dist/assets/worker-CWsWXMyk.js +0 -335
@@ -0,0 +1,256 @@
1
+ .ai-chat-container {
2
+ height: 100%;
3
+ display: flex;
4
+ flex-direction: column;
5
+ padding: 10px;
6
+ }
7
+
8
+ .ai-chat-messages {
9
+ flex: 1;
10
+ overflow-y: auto;
11
+ margin-bottom: 10px;
12
+ padding-right: 5px;
13
+
14
+ &::-webkit-scrollbar {
15
+ width: 6px;
16
+ }
17
+
18
+ &::-webkit-scrollbar-track {
19
+ background: var(--vh-color-neutral-02);
20
+ border-radius: 3px;
21
+ }
22
+
23
+ &::-webkit-scrollbar-thumb {
24
+ background: var(--vh-color-neutral-04);
25
+ border-radius: 3px;
26
+ }
27
+ }
28
+
29
+ .ai-chat-message {
30
+ margin-bottom: 15px;
31
+
32
+ &.user {
33
+ .ai-chat-message-content {
34
+ background: var(--vh-color-primary-01);
35
+ color: white;
36
+ margin-left: 20px;
37
+ border-radius: 12px 12px 4px 12px;
38
+ }
39
+
40
+ .ai-chat-message-time {
41
+ text-align: right;
42
+ margin-right: 5px;
43
+ }
44
+ }
45
+
46
+ &.assistant {
47
+ .ai-chat-message-content {
48
+ background: var(--vh-color-neutral-02);
49
+ color: white;
50
+ margin-right: 20px;
51
+ border-radius: 12px 12px 12px 4px;
52
+ }
53
+
54
+ .ai-chat-message-time {
55
+ text-align: left;
56
+ margin-left: 5px;
57
+ }
58
+
59
+ &.streaming {
60
+ .ai-chat-message-content {
61
+ background: var(--vh-color-neutral-02);
62
+ color: white;
63
+ border-radius: 12px 12px 12px 4px;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ .ai-chat-message-content {
70
+ padding: 10px 12px;
71
+ font-size: 14px;
72
+ line-height: 1.4;
73
+ word-wrap: break-word;
74
+
75
+ // Markdown styling for dark backgrounds
76
+ pre {
77
+ background-color: rgba(0, 0, 0, 0.3);
78
+ color: #e6e6e6;
79
+ padding: 0.5rem;
80
+ border-radius: 0.25rem;
81
+ overflow-x: auto;
82
+ font-size: 0.875rem;
83
+ border: 1px solid rgba(255, 255, 255, 0.1);
84
+ }
85
+
86
+ code {
87
+ background-color: rgba(0, 0, 0, 0.3);
88
+ color: #e6e6e6;
89
+ padding: 0.125rem 0.25rem;
90
+ border-radius: 0.25rem;
91
+ font-size: 0.875rem;
92
+ border: 1px solid rgba(255, 255, 255, 0.1);
93
+ }
94
+
95
+ pre code {
96
+ background-color: transparent;
97
+ padding: 0;
98
+ border: none;
99
+ }
100
+
101
+ blockquote {
102
+ border-left: 3px solid var(--vh-color-neutral-03);
103
+ padding-left: 1rem;
104
+ margin: 0.5rem 0;
105
+ color: var(--vh-color-neutral-03);
106
+ font-style: italic;
107
+ }
108
+
109
+ h1,
110
+ h2,
111
+ h3,
112
+ h4,
113
+ h5,
114
+ h6 {
115
+ margin-top: 1rem;
116
+ margin-bottom: 0.5rem;
117
+ color: inherit;
118
+ }
119
+
120
+ ul,
121
+ ol {
122
+ padding-left: 1.5rem;
123
+ }
124
+
125
+ p {
126
+ margin-bottom: 0.5rem;
127
+ }
128
+
129
+ // Ensure good contrast for assistant messages
130
+ .ai-chat-message.assistant & {
131
+ color: white;
132
+ }
133
+
134
+ // Ensure user message text stays white
135
+ .ai-chat-message.user & {
136
+ color: white;
137
+ }
138
+ }
139
+
140
+ .ai-chat-message-time {
141
+ font-size: 11px;
142
+ color: var(--vh-color-neutral-03);
143
+ margin-top: 4px;
144
+ }
145
+
146
+ .ai-chat-status {
147
+ font-size: 0.75rem;
148
+ font-style: italic;
149
+ color: var(--vh-color-neutral-03);
150
+ margin-bottom: 0.5rem;
151
+ margin-left: 20px;
152
+ margin-right: 20px;
153
+ padding: 0.25rem 0.5rem;
154
+ background-color: var(--vh-color-neutral-02);
155
+ border-radius: 0.25rem;
156
+ border: 1px solid var(--vh-color-neutral-03);
157
+ text-align: left;
158
+ }
159
+
160
+ .ai-chat-typing {
161
+ display: flex;
162
+ gap: 4px;
163
+ align-items: center;
164
+
165
+ span {
166
+ width: 6px;
167
+ height: 6px;
168
+ background: var(--vh-color-neutral-03);
169
+ border-radius: 50%;
170
+ animation: typing 1.4s infinite ease-in-out;
171
+
172
+ &:nth-child(1) {
173
+ animation-delay: -0.32s;
174
+ }
175
+ &:nth-child(2) {
176
+ animation-delay: -0.16s;
177
+ }
178
+ &:nth-child(3) {
179
+ animation-delay: 0s;
180
+ }
181
+ }
182
+ }
183
+
184
+ @keyframes typing {
185
+ 0%,
186
+ 80%,
187
+ 100% {
188
+ transform: scale(0.8);
189
+ opacity: 0.5;
190
+ }
191
+ 40% {
192
+ transform: scale(1);
193
+ opacity: 1;
194
+ }
195
+ }
196
+
197
+ .ai-chat-input-container {
198
+ border-top: 1px solid var(--vh-color-neutral-02);
199
+ padding-top: 10px;
200
+ }
201
+
202
+ .ai-chat-input-group {
203
+ display: flex;
204
+ gap: 8px;
205
+ margin-bottom: 0;
206
+
207
+ textarea {
208
+ flex: 1;
209
+ background: var(--vh-color-neutral-02) !important;
210
+ border: 1px solid var(--vh-color-neutral-03) !important;
211
+ color: var(--vh-color-neutral-04) !important;
212
+ border-radius: 8px;
213
+ padding: 8px 10px;
214
+ font-size: 14px;
215
+ font-family: var(--vzcode-font-family);
216
+ resize: none;
217
+ min-height: 36px;
218
+
219
+ &:focus {
220
+ outline: none !important;
221
+ border-color: var(--vh-color-primary-01) !important;
222
+ box-shadow: 0 0 0 2px rgba(38, 67, 153, 0.2) !important;
223
+ color: var(--vh-color-neutral-04) !important;
224
+ }
225
+
226
+ &::placeholder {
227
+ color: var(--vh-color-neutral-03) !important;
228
+ }
229
+
230
+ &:disabled {
231
+ opacity: 0.6;
232
+ cursor: not-allowed;
233
+ }
234
+ }
235
+
236
+ .ai-chat-send-button {
237
+ background: var(--vh-color-primary-01);
238
+ border: none;
239
+ color: white;
240
+ border-radius: 8px;
241
+ padding: 8px 16px;
242
+ font-size: 14px;
243
+ font-weight: 500;
244
+ cursor: pointer;
245
+ transition: background-color 0.2s ease;
246
+
247
+ &:hover:not(:disabled) {
248
+ background: var(--vh-color-primary-02);
249
+ }
250
+
251
+ &:disabled {
252
+ background: var(--vh-color-neutral-03);
253
+ cursor: not-allowed;
254
+ }
255
+ }
256
+ }
@@ -25,15 +25,20 @@ import {
25
25
  PinSVG,
26
26
  QuestionMarkSVG,
27
27
  SearchSVG,
28
+ SparklesSVG,
28
29
  } from '../Icons';
29
30
  import { MicSVG } from '../Icons/MicSVG';
30
31
  import { sortFileTree } from '../sortFileTree';
31
32
  import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
32
33
  import { VZCodeContext } from '../VZCodeContext';
34
+ import { AIChat } from './AIChat';
33
35
  import { Listing } from './Listing';
34
36
  import { Search } from './Search';
35
37
  import { useDragAndDrop } from './useDragAndDrop';
36
- import { enableLiveKit } from '../featureFlags';
38
+ import {
39
+ enableLiveKit,
40
+ enableAIChat,
41
+ } from '../featureFlags';
37
42
  import './styles.scss';
38
43
 
39
44
  // TODO turn this UI back on when we are actually detecting
@@ -105,6 +110,11 @@ export const VZSidebar = ({
105
110
  <strong>Open Voice Chat Menu</strong>
106
111
  </div>
107
112
  ),
113
+ aiChatToolTipText = (
114
+ <div>
115
+ <strong>AI Chat (beta)</strong>
116
+ </div>
117
+ ),
108
118
  }: {
109
119
  createFileTooltipText?: React.ReactNode;
110
120
  createDirTooltipText?: React.ReactNode;
@@ -116,6 +126,7 @@ export const VZSidebar = ({
116
126
  enableAutoFollowTooltipText?: React.ReactNode;
117
127
  disableAutoFollowTooltipText?: React.ReactNode;
118
128
  voiceChatToolTipText?: React.ReactNode;
129
+ aiChatToolTipText?: React.ReactNode;
119
130
  }) => {
120
131
  const {
121
132
  files,
@@ -124,6 +135,8 @@ export const VZSidebar = ({
124
135
  setIsDocOpen,
125
136
  isSearchOpen,
126
137
  setIsSearchOpen,
138
+ isAIChatOpen,
139
+ setIsAIChatOpen,
127
140
  handleOpenCreateFileModal,
128
141
  handleOpenCreateDirModal,
129
142
  connected,
@@ -270,7 +283,10 @@ export const VZSidebar = ({
270
283
  <i
271
284
  id="files-icon"
272
285
  className="icon-button icon-button-dark"
273
- onClick={() => setIsSearchOpen(false)}
286
+ onClick={() => {
287
+ setIsSearchOpen(false);
288
+ setIsAIChatOpen(false);
289
+ }}
274
290
  >
275
291
  <FolderSVG />
276
292
  </i>
@@ -287,12 +303,37 @@ export const VZSidebar = ({
287
303
  <i
288
304
  id="search-icon"
289
305
  className="icon-button icon-button-dark"
290
- onClick={() => setIsSearchOpen(true)}
306
+ onClick={() => {
307
+ setIsSearchOpen(true);
308
+ setIsAIChatOpen(false);
309
+ }}
291
310
  >
292
311
  <SearchSVG />
293
312
  </i>
294
313
  </OverlayTrigger>
295
314
 
315
+ {enableAIChat && (
316
+ <OverlayTrigger
317
+ placement="right"
318
+ overlay={
319
+ <Tooltip id="ai-chat-tooltip">
320
+ {aiChatToolTipText}
321
+ </Tooltip>
322
+ }
323
+ >
324
+ <i
325
+ id="ai-chat-icon"
326
+ className="icon-button icon-button-dark"
327
+ onClick={() => {
328
+ setIsAIChatOpen(true);
329
+ setIsSearchOpen(false);
330
+ }}
331
+ >
332
+ <SparklesSVG />
333
+ </i>
334
+ </OverlayTrigger>
335
+ )}
336
+
296
337
  <OverlayTrigger
297
338
  placement="right"
298
339
  overlay={
@@ -438,7 +479,15 @@ export const VZSidebar = ({
438
479
  onDragLeave={handleDragLeave}
439
480
  onDrop={handleDrop}
440
481
  >
441
- {!isSearchOpen ? (
482
+ {isAIChatOpen ? (
483
+ <div className="sidebar-ai-chat">
484
+ <AIChat />
485
+ </div>
486
+ ) : isSearchOpen ? (
487
+ <div className="sidebar-search">
488
+ <Search />
489
+ </div>
490
+ ) : (
442
491
  <div className="sidebar-files">
443
492
  {isDragOver ? (
444
493
  <div className="empty drag-over">
@@ -480,10 +529,6 @@ export const VZSidebar = ({
480
529
  </div>
481
530
  )}
482
531
  </div>
483
- ) : (
484
- <div className="sidebar-search">
485
- <Search />
486
- </div>
487
532
  )}
488
533
  </div>
489
534
  </div>
@@ -66,7 +66,8 @@
66
66
  }
67
67
 
68
68
  .sidebar-files,
69
- .sidebar-search {
69
+ .sidebar-search,
70
+ .sidebar-ai-chat {
70
71
  width: 100%;
71
72
  }
72
73
 
@@ -76,6 +77,12 @@
76
77
  }
77
78
  }
78
79
 
80
+ .sidebar-ai-chat {
81
+ height: 100%;
82
+ display: flex;
83
+ flex-direction: column;
84
+ }
85
+
79
86
  .sidebar-search .arrow-wrapper {
80
87
  cursor: pointer;
81
88
  }
@@ -1,6 +1,6 @@
1
1
  import jsondiff from 'json0-ot-diff';
2
2
  import diffMatchPatch from 'diff-match-patch';
3
- import { json1Presence, textUnicode } from '../ot';
3
+ import { json1Presence, textUnicode } from '../ot.js';
4
4
 
5
5
  export const diff = (a, b) =>
6
6
  jsondiff(
@@ -1,2 +1,4 @@
1
1
  export const enableLiveKit =
2
2
  import.meta.env.VITE_ENABLE_LIVEKIT === 'true';
3
+
4
+ export const enableAIChat = true;
@@ -150,6 +150,24 @@ export const useActions = (
150
150
  });
151
151
  }, [dispatch]);
152
152
 
153
+ // True to show the AI chat instead of files
154
+ const setIsAIChatOpen = useCallback(
155
+ (value: boolean) => {
156
+ dispatch({
157
+ type: 'set_is_ai_chat_open',
158
+ value: value,
159
+ });
160
+ },
161
+ [dispatch],
162
+ );
163
+
164
+ // Toggle the focused variable, which should focus the AI chat input
165
+ const toggleAIChatFocused = useCallback(() => {
166
+ dispatch({
167
+ type: 'toggle_ai_chat_focused',
168
+ });
169
+ }, [dispatch]);
170
+
153
171
  // True to show the settings modal.
154
172
  const setIsSettingsOpen = useCallback(
155
173
  (value: boolean) => {
@@ -231,6 +249,8 @@ export const useActions = (
231
249
  setSearchLineVisibility,
232
250
  setSearchFocusedIndex,
233
251
  toggleSearchFocused,
252
+ setIsAIChatOpen,
253
+ toggleAIChatFocused,
234
254
  setIsSettingsOpen,
235
255
  setIsDocOpen,
236
256
  closeSettings,
@@ -6,6 +6,7 @@ import { EditorView } from 'codemirror';
6
6
  export type EditorCacheValue = {
7
7
  editor: EditorView;
8
8
  themeCompartment: any;
9
+ languageCompartment?: any;
9
10
  scrollPosition?: number;
10
11
  };
11
12
 
@@ -2,6 +2,10 @@ import { useCallback } from 'react';
2
2
  import { FileTreePath } from '../types';
3
3
  import { VizFileId, VizContent } from '@vizhub/viz-types';
4
4
  import { randomId } from '../randomId';
5
+ import { EditorCache } from './useEditorCache';
6
+ import { getFileExtension } from './utils/fileExtension';
7
+ import { getLanguageExtension } from './CodeEditor/getOrCreateEditor';
8
+ import { editorCacheKey } from './useEditorCache';
5
9
 
6
10
  // CRUD operations for files and directories
7
11
  // CRUD = Create, Read, Update, Delete
@@ -9,6 +13,8 @@ export const useFileCRUD = ({
9
13
  submitOperation,
10
14
  closeTabs,
11
15
  openTab,
16
+ editorCache,
17
+ content,
12
18
  }: {
13
19
  submitOperation: (
14
20
  operation: (document: VizContent) => VizContent,
@@ -21,6 +27,8 @@ export const useFileCRUD = ({
21
27
  fileId: VizFileId;
22
28
  isTransient: boolean;
23
29
  }) => void;
30
+ editorCache: EditorCache;
31
+ content: VizContent;
24
32
  }) => {
25
33
  // Create a new file
26
34
  const createFile = useCallback(
@@ -78,6 +86,45 @@ export const useFileCRUD = ({
78
86
  },
79
87
  },
80
88
  }));
89
+
90
+ const oldName = content.files[fileId].name;
91
+ const oldExtension = getFileExtension(oldName);
92
+
93
+ const newExtension = getFileExtension(newName);
94
+
95
+ const didExtensionChange =
96
+ newExtension !== oldExtension;
97
+ if (didExtensionChange) {
98
+ // Update the language compartment of the corresponding CodeMirror editor
99
+ // with the new language, based on the new extension, only if it changed.
100
+ // Search through all cached editors for this file ID
101
+ for (const [
102
+ cacheKey,
103
+ cachedEditor,
104
+ ] of editorCache.entries()) {
105
+ // Check if this cache entry is for the current file
106
+ if (cacheKey.startsWith(fileId + '|')) {
107
+ if (
108
+ cachedEditor &&
109
+ cachedEditor.languageCompartment
110
+ ) {
111
+ const newLanguageExtension =
112
+ getLanguageExtension(newExtension);
113
+
114
+ // Reconfigure the language compartment with the new language extension
115
+ cachedEditor.editor.dispatch({
116
+ effects:
117
+ cachedEditor.languageCompartment.reconfigure(
118
+ newLanguageExtension
119
+ ? [newLanguageExtension]
120
+ : [],
121
+ ),
122
+ });
123
+ // Continue to update all instances of this file in different panes
124
+ }
125
+ }
126
+ }
127
+ }
81
128
  },
82
129
  [submitOperation],
83
130
  );
@@ -1,26 +1,36 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import type { Pane } from '../types';
3
+ import { VizContent } from '@vizhub/viz-types';
2
4
 
3
5
  // TODO bring this feature back
4
6
  // When a page is opened with an active file,
5
7
  // make sure all the directories leading to that file
6
8
  // are opened automatically.
7
- //export const initialOpenDirectories = (activeFile) => {
8
- // const openDirectories = {};
9
- // if (activeFile) {
10
- // const path = activeFile.split('/');
11
- // for (let i = 1; i < path.length; i++) {
12
- // openDirectories[path.slice(0, i).join('/')] = true;
13
- // }
14
- // }
15
- // return openDirectories;
16
- //};
9
+ export const initialOpenDirectories = (
10
+ activeFile: string | null,
11
+ ): Set<VZPath> => {
12
+ const openDirectories = new Set<VZPath>();
13
+ if (activeFile) {
14
+ const path = activeFile.split('/');
15
+ for (let i = 1; i < path.length; i++) {
16
+ openDirectories.add(path.slice(0, i).join('/'));
17
+ }
18
+ }
19
+ return openDirectories;
20
+ };
17
21
 
18
22
  type VZPath = string;
19
23
 
20
24
  // Inspired by
21
25
  // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/useOpenDirectories.js
22
26
  // TODO move this into reducer
23
- export const useOpenDirectories = (): {
27
+ export const useOpenDirectories = ({
28
+ activePane,
29
+ content,
30
+ }: {
31
+ activePane: Pane;
32
+ content: VizContent;
33
+ }): {
24
34
  isDirectoryOpen: (path: VZPath) => boolean;
25
35
  toggleDirectory: (path: VZPath) => void;
26
36
  } => {
@@ -29,6 +39,27 @@ export const useOpenDirectories = (): {
29
39
  Set<VZPath>
30
40
  >(new Set());
31
41
 
42
+ // (client-side only) initialize the open directories
43
+ // based on the open files from the URL params.
44
+ useEffect(() => {
45
+ if (
46
+ activePane.type === 'leafPane' &&
47
+ activePane?.activeFileId &&
48
+ content.files[activePane.activeFileId]
49
+ ) {
50
+ const activeFileId = activePane.activeFileId;
51
+ const fileName = content.files[activeFileId].name;
52
+ const dirsToOpen = initialOpenDirectories(fileName);
53
+ setOpenDirectories((prev) => {
54
+ const updated = new Set(prev);
55
+ for (const dir of dirsToOpen) {
56
+ updated.add(dir);
57
+ }
58
+ return updated;
59
+ });
60
+ }
61
+ }, [activePane]);
62
+
32
63
  // Whether a directory is open.
33
64
  const isDirectoryOpen: (path: VZPath) => boolean =
34
65
  useCallback(
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Utility function to extract file extension from a file name.
3
+ * @param fileName - The name of the file (e.g., "index.html", "script.js")
4
+ * @returns The file extension without the dot (e.g., "html", "js") or undefined if no extension
5
+ */
6
+ export const getFileExtension = (
7
+ fileName: string,
8
+ ): string | undefined => {
9
+ return fileName.split('.').pop();
10
+ };
@@ -0,0 +1,31 @@
1
+ import { VZState, VZAction } from '.';
2
+
3
+ export const setIsAIChatOpenReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState => {
7
+ if (action.type === 'set_is_ai_chat_open') {
8
+ return {
9
+ ...state,
10
+ isAIChatOpen: action.value,
11
+ // When opening AI chat, close search
12
+ isSearchOpen: action.value
13
+ ? false
14
+ : state.isSearchOpen,
15
+ };
16
+ }
17
+ return state;
18
+ };
19
+
20
+ export const toggleAIChatFocusedReducer = (
21
+ state: VZState,
22
+ action: VZAction,
23
+ ): VZState => {
24
+ if (action.type === 'toggle_ai_chat_focused') {
25
+ return {
26
+ ...state,
27
+ aiChatFocused: !state.aiChatFocused,
28
+ };
29
+ }
30
+ return state;
31
+ };
@@ -25,6 +25,8 @@ export const createInitialState = ({
25
25
  focusedChildIndex: null,
26
26
  },
27
27
  isSearchOpen: false,
28
+ isAIChatOpen: false,
29
+ aiChatFocused: false,
28
30
  isSettingsOpen: false,
29
31
  isDocOpen: false,
30
32
  editorWantsFocus: false,