vzcode 1.60.0 → 1.62.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 (30) hide show
  1. package/dist/assets/bootstrap-icons-BeopsB42.woff +0 -0
  2. package/dist/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  3. package/dist/assets/{index-DMIy-XE4.css → index-Br76WttW.css} +5 -1
  4. package/dist/assets/{index-S-R8oY72.js → index-vbF7Aejt.js} +174 -174
  5. package/dist/index.html +2 -2
  6. package/package.json +7 -6
  7. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  8. package/src/client/Icons/index.tsx +1 -0
  9. package/src/client/VZCodeContext/types.ts +13 -1
  10. package/src/client/VZCodeContext/useVZCodeState.ts +83 -5
  11. package/src/client/VZRight.tsx +6 -3
  12. package/src/client/VZSidebar/AIChat/ChatInput.tsx +45 -2
  13. package/src/client/VZSidebar/AIChat/MessageList.tsx +2 -0
  14. package/src/client/VZSidebar/AIChat/index.tsx +12 -0
  15. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  16. package/src/client/VZSidebar/VisualEditor.tsx +295 -0
  17. package/src/client/VZSidebar/index.tsx +64 -0
  18. package/src/client/VZSidebar/styles.scss +213 -1
  19. package/src/client/bootstrap.ts +3 -0
  20. package/src/client/useActions.ts +12 -0
  21. package/src/client/vzReducer/closeTabReducer.test.ts +33 -21
  22. package/src/client/vzReducer/createInitialState.ts +1 -0
  23. package/src/client/vzReducer/index.ts +9 -0
  24. package/src/client/vzReducer/searchReducer.test.ts +1 -1
  25. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  26. package/src/server/aiChatHandler/chatOperations.ts +22 -0
  27. package/src/server/aiChatHandler/errorHandling.ts +17 -4
  28. package/src/server/aiChatHandler/index.ts +131 -67
  29. package/src/server/aiChatHandler/llmStreaming.ts +76 -27
  30. package/src/types.ts +12 -0
@@ -0,0 +1,295 @@
1
+ import {
2
+ useCallback,
3
+ useContext,
4
+ useState,
5
+ useRef,
6
+ useEffect,
7
+ } from 'react';
8
+ import { VZCodeContext } from '../VZCodeContext';
9
+ import { VizContent, VizFileId } from '@vizhub/viz-types';
10
+ import { VisualEditorConfigEntry } from '../../types';
11
+ import { EmptyState } from './EmptyState';
12
+
13
+ const CONFIG_FILE_NAME = 'config.json';
14
+
15
+ export const VisualEditor = () => {
16
+ const {
17
+ files,
18
+ submitOperation,
19
+ runPrettierRef,
20
+ iframeRef,
21
+ } = useContext(VZCodeContext);
22
+
23
+ let configFileId: VizFileId | null = null;
24
+ for (const fileId in files) {
25
+ if (files[fileId].name === CONFIG_FILE_NAME) {
26
+ configFileId = fileId;
27
+ break;
28
+ }
29
+ }
30
+
31
+ if (configFileId === null) {
32
+ return (
33
+ <EmptyState>
34
+ To begin using the visual editor, create a
35
+ config.json.
36
+ </EmptyState>
37
+ );
38
+ }
39
+
40
+ let configData;
41
+
42
+ try {
43
+ configData = JSON.parse(files[configFileId].text);
44
+ } catch (error) {
45
+ return (
46
+ <EmptyState>
47
+ Your config.json file is not valid json.
48
+ </EmptyState>
49
+ );
50
+ }
51
+
52
+ if (!('visualEditorWidgets' in configData)) {
53
+ return (
54
+ <EmptyState>
55
+ To begin using the visual editor, make sure your
56
+ config.json has a key called "visualEditorWidgets",
57
+ whose value is the config for the visual editor.
58
+ </EmptyState>
59
+ );
60
+ }
61
+
62
+ if (!Array.isArray(configData.visualEditorWidgets)) {
63
+ return (
64
+ <EmptyState>
65
+ Your config.json file has "visualEditorWidgets" but
66
+ it is not an array. Please make sure
67
+ "visualEditorWidgets" is an array of widget
68
+ configurations.
69
+ </EmptyState>
70
+ );
71
+ }
72
+
73
+ const onInputUpdate = useCallback(
74
+ (
75
+ property: string,
76
+ previousValue: any,
77
+ ): React.FormEventHandler<HTMLInputElement> =>
78
+ (event) => {
79
+ //TODO: test race condition in which someone is editing the config file as another user uses the visual editor
80
+
81
+ const newValueOfConsistentType =
82
+ typeof previousValue === 'number'
83
+ ? parseFloat(event.currentTarget.value)
84
+ : event.currentTarget.value;
85
+
86
+ const newConfigData = {
87
+ ...configData,
88
+ [property]: newValueOfConsistentType,
89
+ };
90
+
91
+ submitOperation((document: VizContent) => ({
92
+ ...document,
93
+ files: {
94
+ ...files,
95
+ [configFileId]: {
96
+ name: 'config.json',
97
+ text: JSON.stringify(newConfigData, null, 2),
98
+ },
99
+ },
100
+ }));
101
+
102
+ iframeRef.current.contentWindow.postMessage({
103
+ [property]: newValueOfConsistentType,
104
+ });
105
+ },
106
+ [configData, files, configFileId],
107
+ );
108
+
109
+ const visualEditorWidgets: VisualEditorConfigEntry[] =
110
+ configData.visualEditorWidgets;
111
+
112
+ // State for advanced interactions
113
+ const [activeSlider, setActiveSlider] = useState<
114
+ string | null
115
+ >(null);
116
+ const [isDragging, setIsDragging] = useState<
117
+ string | null
118
+ >(null);
119
+ const [hoverValue, setHoverValue] = useState<
120
+ number | null
121
+ >(null);
122
+ const sliderRefs = useRef<{
123
+ [key: string]: HTMLInputElement | null;
124
+ }>({});
125
+
126
+ // Helper function to format values nicely
127
+ const formatValue = (
128
+ value: number,
129
+ min: number,
130
+ max: number,
131
+ ) => {
132
+ // Determine decimal places based on the range
133
+ const range = max - min;
134
+ const decimalPlaces =
135
+ range < 10 ? 2 : range < 100 ? 1 : 0;
136
+ return Number(value).toFixed(decimalPlaces);
137
+ };
138
+
139
+ // Helper function to calculate percentage for progress fill
140
+ const calculatePercentage = (
141
+ value: number,
142
+ min: number,
143
+ max: number,
144
+ ) => {
145
+ return ((value - min) / (max - min)) * 100;
146
+ };
147
+
148
+ // Enhanced input handler with animations
149
+ const onInputUpdateEnhanced = useCallback(
150
+ (
151
+ property: string,
152
+ previousValue: any,
153
+ widgetConfig: VisualEditorConfigEntry,
154
+ ): React.FormEventHandler<HTMLInputElement> =>
155
+ (event) => {
156
+ const newValueOfConsistentType =
157
+ typeof previousValue === 'number'
158
+ ? parseFloat(event.currentTarget.value)
159
+ : event.currentTarget.value;
160
+
161
+ const newConfigData = {
162
+ ...configData,
163
+ [property]: newValueOfConsistentType,
164
+ };
165
+
166
+ submitOperation((document: VizContent) => ({
167
+ ...document,
168
+ files: {
169
+ ...files,
170
+ [configFileId]: {
171
+ name: 'config.json',
172
+ text: JSON.stringify(newConfigData, null, 2),
173
+ },
174
+ },
175
+ }));
176
+
177
+ iframeRef.current.contentWindow.postMessage({
178
+ [property]: newValueOfConsistentType,
179
+ });
180
+
181
+ // Trigger value change animation
182
+ const sliderElement = sliderRefs.current[property];
183
+ if (sliderElement) {
184
+ sliderElement.classList.add('value-changed');
185
+ setTimeout(() => {
186
+ sliderElement.classList.remove('value-changed');
187
+ }, 300);
188
+ }
189
+ },
190
+ [configData, files, configFileId],
191
+ );
192
+
193
+ // Mouse event handlers for enhanced interactions
194
+ const handleMouseDown = (property: string) => {
195
+ setIsDragging(property);
196
+ setActiveSlider(property);
197
+ };
198
+
199
+ const handleMouseUp = () => {
200
+ setIsDragging(null);
201
+ };
202
+
203
+ const handleMouseEnter = (property: string) => {
204
+ setActiveSlider(property);
205
+ };
206
+
207
+ const handleMouseLeave = () => {
208
+ setActiveSlider(null);
209
+ setHoverValue(null);
210
+ };
211
+
212
+ // Add global mouse up listener
213
+ useEffect(() => {
214
+ const handleGlobalMouseUp = () => {
215
+ setIsDragging(null);
216
+ };
217
+
218
+ document.addEventListener(
219
+ 'mouseup',
220
+ handleGlobalMouseUp,
221
+ );
222
+ return () => {
223
+ document.removeEventListener(
224
+ 'mouseup',
225
+ handleGlobalMouseUp,
226
+ );
227
+ };
228
+ }, []);
229
+
230
+ return (
231
+ <div className="visual-editor">
232
+ {visualEditorWidgets.map((widgetConfig, index) => {
233
+ if (widgetConfig.type === 'number') {
234
+ const currentValue =
235
+ configData[widgetConfig.property];
236
+ const percentage = calculatePercentage(
237
+ currentValue,
238
+ widgetConfig.min,
239
+ widgetConfig.max,
240
+ );
241
+
242
+ return (
243
+ <div
244
+ key={widgetConfig.property}
245
+ className="visual-editor-slider"
246
+ >
247
+ <div className="slider-header">
248
+ <label
249
+ htmlFor={widgetConfig.property}
250
+ className="slider-label"
251
+ >
252
+ {widgetConfig.label}
253
+ </label>
254
+ <span className="slider-value">
255
+ {formatValue(
256
+ currentValue,
257
+ widgetConfig.min,
258
+ widgetConfig.max,
259
+ )}
260
+ </span>
261
+ </div>
262
+ <div className="slider-container">
263
+ <input
264
+ type="range"
265
+ id={widgetConfig.property}
266
+ className="slider-input"
267
+ min={widgetConfig.min}
268
+ max={widgetConfig.max}
269
+ step="any"
270
+ onInput={onInputUpdate(
271
+ widgetConfig.property,
272
+ configData[widgetConfig.property],
273
+ )}
274
+ defaultValue={currentValue}
275
+ />
276
+ <div
277
+ className="slider-track-fill"
278
+ style={{ width: `${percentage}%` }}
279
+ />
280
+ </div>
281
+ <div className="slider-bounds">
282
+ <span className="min-value">
283
+ {widgetConfig.min}
284
+ </span>
285
+ <span className="max-value">
286
+ {widgetConfig.max}
287
+ </span>
288
+ </div>
289
+ </div>
290
+ );
291
+ }
292
+ })}
293
+ </div>
294
+ );
295
+ };
@@ -26,6 +26,7 @@ import {
26
26
  QuestionMarkSVG,
27
27
  SearchSVG,
28
28
  SparklesSVG,
29
+ AdjustmentSVG,
29
30
  } from '../Icons';
30
31
  import { MicSVG } from '../Icons/MicSVG';
31
32
  import { sortFileTree } from '../sortFileTree';
@@ -41,6 +42,7 @@ import {
41
42
  enableAIChat,
42
43
  } from '../featureFlags';
43
44
  import './styles.scss';
45
+ import { VisualEditor } from './VisualEditor';
44
46
 
45
47
  const enableConnectionStatus = true;
46
48
 
@@ -113,6 +115,12 @@ export const VZSidebar = ({
113
115
  <strong>Edit with AI</strong>
114
116
  </div>
115
117
  ),
118
+
119
+ visualEditorToolTipText = (
120
+ <div>
121
+ <strong>Visual Editor</strong>
122
+ </div>
123
+ ),
116
124
  }: {
117
125
  createFileTooltipText?: React.ReactNode;
118
126
  createDirTooltipText?: React.ReactNode;
@@ -125,6 +133,7 @@ export const VZSidebar = ({
125
133
  disableAutoFollowTooltipText?: React.ReactNode;
126
134
  voiceChatToolTipText?: React.ReactNode;
127
135
  aiChatToolTipText?: React.ReactNode;
136
+ visualEditorToolTipText?: React.ReactNode;
128
137
  }) => {
129
138
  const {
130
139
  files,
@@ -133,6 +142,8 @@ export const VZSidebar = ({
133
142
  setIsDocOpen,
134
143
  isSearchOpen,
135
144
  setIsSearchOpen,
145
+ isVisualEditorOpen,
146
+ setIsVisualEditorOpen,
136
147
  isAIChatOpen,
137
148
  setIsAIChatOpen,
138
149
  handleOpenCreateFileModal,
@@ -335,6 +346,7 @@ export const VZSidebar = ({
335
346
  onClick={() => {
336
347
  setIsSearchOpen(false);
337
348
  setIsAIChatOpen(false);
349
+ setIsVisualEditorOpen(false);
338
350
  setSidebarView(false); // Switch to files view
339
351
  }}
340
352
  >
@@ -356,6 +368,7 @@ export const VZSidebar = ({
356
368
  onClick={() => {
357
369
  setIsSearchOpen(true);
358
370
  setIsAIChatOpen(false);
371
+ setIsVisualEditorOpen(false);
359
372
  setSidebarView(false); // Switch to files view (search uses same width as files)
360
373
  }}
361
374
  >
@@ -363,6 +376,50 @@ export const VZSidebar = ({
363
376
  </i>
364
377
  </OverlayTrigger>
365
378
 
379
+ {enableAIChat && (
380
+ <OverlayTrigger
381
+ placement="right"
382
+ overlay={
383
+ <Tooltip id="ai-chat-tooltip">
384
+ {aiChatToolTipText}
385
+ </Tooltip>
386
+ }
387
+ >
388
+ <i
389
+ id="ai-chat-icon"
390
+ className="icon-button icon-button-dark"
391
+ onClick={() => {
392
+ setIsAIChatOpen(true);
393
+ setIsSearchOpen(false);
394
+ setIsVisualEditorOpen(false);
395
+ }}
396
+ >
397
+ <SparklesSVG />
398
+ </i>
399
+ </OverlayTrigger>
400
+ )}
401
+
402
+ <OverlayTrigger
403
+ placement="right"
404
+ overlay={
405
+ <Tooltip id="visual-editor-tooltip">
406
+ {visualEditorToolTipText}
407
+ </Tooltip>
408
+ }
409
+ >
410
+ <i
411
+ id="visual-editor-icon"
412
+ className="icon-button icon-button-dark"
413
+ onClick={() => {
414
+ setIsVisualEditorOpen(true);
415
+ setIsAIChatOpen(false);
416
+ setIsSearchOpen(false);
417
+ }}
418
+ >
419
+ <AdjustmentSVG />
420
+ </i>
421
+ </OverlayTrigger>
422
+
366
423
  <OverlayTrigger
367
424
  placement="right"
368
425
  overlay={
@@ -516,6 +573,10 @@ export const VZSidebar = ({
516
573
  <div className="sidebar-search">
517
574
  <Search />
518
575
  </div>
576
+ ) : isVisualEditorOpen ? (
577
+ <div className="sidebar-visual-editor">
578
+ <VisualEditor />
579
+ </div>
519
580
  ) : (
520
581
  <div className="sidebar-files">
521
582
  {isDragOver ? (
@@ -569,6 +630,7 @@ export const VZSidebar = ({
569
630
  onClick={handleExportToZip}
570
631
  title="Export files to ZIP"
571
632
  >
633
+ <i className="bi bi-download"></i>
572
634
  {exportButtonText}
573
635
  </button>
574
636
  )}
@@ -578,6 +640,7 @@ export const VZSidebar = ({
578
640
  onClick={handleCopyForAI}
579
641
  title="Copy files formatted for AI"
580
642
  >
643
+ <i className="bi bi-clipboard"></i>
581
644
  {copyButtonText}
582
645
  </button>
583
646
  )}
@@ -587,6 +650,7 @@ export const VZSidebar = ({
587
650
  onClick={handlePasteForAI}
588
651
  title="Paste files from AI"
589
652
  >
653
+ <i className="bi bi-clipboard-plus"></i>
590
654
  {pasteButtonText}
591
655
  </button>
592
656
  </div>
@@ -67,7 +67,8 @@
67
67
 
68
68
  .sidebar-files,
69
69
  .sidebar-search,
70
- .sidebar-ai-chat {
70
+ .sidebar-ai-chat,
71
+ .sidebar-visual-editor {
71
72
  width: 100%;
72
73
  }
73
74
 
@@ -317,6 +318,10 @@
317
318
  cursor: pointer;
318
319
  transition: all 0.2s ease;
319
320
 
321
+ i {
322
+ margin-right: 8px;
323
+ }
324
+
320
325
  &:hover {
321
326
  background: var(--vh-color-hover-dark);
322
327
  border-color: var(--vh-color-neutral-04);
@@ -382,4 +387,211 @@
382
387
  color: #000000;
383
388
  z-index: 1;
384
389
  }
390
+
391
+ // Visual Editor Slider Styles
392
+ .visual-editor {
393
+ display: flex;
394
+ flex-direction: column;
395
+ gap: 16px;
396
+ width: 100%;
397
+ padding: 10px;
398
+ box-sizing: border-box;
399
+ }
400
+
401
+ .visual-editor-slider {
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: 8px;
405
+ padding: 12px 10px;
406
+ background: var(--vh-color-neutral-01);
407
+ border: 1px solid var(--vh-color-neutral-02);
408
+ border-radius: 8px;
409
+ transition: all 0.2s ease;
410
+ width: 100%;
411
+ box-sizing: border-box;
412
+
413
+ &:hover {
414
+ border-color: var(--vh-color-neutral-03);
415
+ background: rgba(255, 255, 255, 0.02);
416
+ }
417
+ }
418
+
419
+ .slider-header {
420
+ display: flex;
421
+ justify-content: space-between;
422
+ align-items: center;
423
+ margin-bottom: 4px;
424
+ }
425
+
426
+ .slider-label {
427
+ font-family: var(--vzcode-font-family);
428
+ font-size: 13px;
429
+ font-weight: 500;
430
+ color: var(--vh-color-neutral-04);
431
+ margin: 0;
432
+ }
433
+
434
+ .slider-value {
435
+ font-family: var(--vzcode-font-family);
436
+ font-size: 13px;
437
+ font-weight: 600;
438
+ color: #66ecff; // SKY color from theme
439
+ background: rgba(102, 236, 255, 0.1);
440
+ padding: 2px 6px;
441
+ border-radius: 4px;
442
+ min-width: 40px;
443
+ text-align: center;
444
+ }
445
+
446
+ .slider-container {
447
+ position: relative;
448
+ height: 20px;
449
+ display: flex;
450
+ align-items: center;
451
+ }
452
+
453
+ .slider-input {
454
+ width: 100%;
455
+ height: 6px;
456
+ background: transparent;
457
+ outline: none;
458
+ border: none;
459
+ cursor: pointer;
460
+ position: relative;
461
+ z-index: 2;
462
+
463
+ // Remove default styling
464
+ -webkit-appearance: none;
465
+ appearance: none;
466
+
467
+ // Custom track
468
+ &::-webkit-slider-track {
469
+ width: 100%;
470
+ height: 6px;
471
+ background: var(--vh-color-neutral-02);
472
+ border-radius: 3px;
473
+ border: none;
474
+ }
475
+
476
+ &::-moz-range-track {
477
+ width: 100%;
478
+ height: 6px;
479
+ background: var(--vh-color-neutral-02);
480
+ border-radius: 3px;
481
+ border: none;
482
+ }
483
+
484
+ // Custom thumb
485
+ &::-webkit-slider-thumb {
486
+ -webkit-appearance: none;
487
+ appearance: none;
488
+ width: 18px;
489
+ height: 18px;
490
+ background: linear-gradient(135deg, #66ecff, #00ffff);
491
+ border-radius: 50%;
492
+ cursor: pointer;
493
+ border: 2px solid var(--vh-color-neutral-01);
494
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
495
+ transition: all 0.2s ease;
496
+ position: relative;
497
+ z-index: 3;
498
+
499
+ &:hover {
500
+ transform: scale(1.1);
501
+ box-shadow: 0 3px 8px rgba(102, 236, 255, 0.4);
502
+ }
503
+
504
+ &:active {
505
+ transform: scale(1.05);
506
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
507
+ }
508
+ }
509
+
510
+ &::-moz-range-thumb {
511
+ width: 18px;
512
+ height: 18px;
513
+ background: linear-gradient(135deg, #66ecff, #00ffff);
514
+ border-radius: 50%;
515
+ cursor: pointer;
516
+ border: 2px solid var(--vh-color-neutral-01);
517
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
518
+ transition: all 0.2s ease;
519
+
520
+ &:hover {
521
+ transform: scale(1.1);
522
+ box-shadow: 0 3px 8px rgba(102, 236, 255, 0.4);
523
+ }
524
+
525
+ &:active {
526
+ transform: scale(1.05);
527
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
528
+ }
529
+ }
530
+
531
+ // Focus styles
532
+ &:focus {
533
+ outline: none;
534
+
535
+ &::-webkit-slider-thumb {
536
+ box-shadow: 0 0 0 3px rgba(102, 236, 255, 0.3);
537
+ }
538
+
539
+ &::-moz-range-thumb {
540
+ box-shadow: 0 0 0 3px rgba(102, 236, 255, 0.3);
541
+ }
542
+ }
543
+ }
544
+
545
+ .slider-track-fill {
546
+ position: absolute;
547
+ top: 50%;
548
+ left: 0;
549
+ height: 6px;
550
+ background: linear-gradient(90deg, #77fd8c, #66ecff);
551
+ border-radius: 3px;
552
+ transform: translateY(-50%);
553
+ transition: width 0.1s ease;
554
+ pointer-events: none;
555
+ z-index: 1;
556
+ }
557
+
558
+ .slider-bounds {
559
+ display: flex;
560
+ justify-content: space-between;
561
+ align-items: center;
562
+ margin-top: 2px;
563
+ }
564
+
565
+ .min-value,
566
+ .max-value {
567
+ font-family: var(--vzcode-font-family);
568
+ font-size: 11px;
569
+ color: var(--vh-color-neutral-03);
570
+ font-weight: 400;
571
+ }
572
+
573
+ // Empty State Component Styles
574
+ .empty-state {
575
+ display: flex;
576
+ flex-direction: column;
577
+ align-items: center;
578
+ justify-content: center;
579
+ padding: 24px 16px;
580
+ margin: 16px;
581
+ background: var(--vh-color-neutral-01);
582
+ border: 1px solid var(--vh-color-neutral-02);
583
+ border-radius: 8px;
584
+ text-align: center;
585
+ font-family: var(--vzcode-font-family);
586
+ font-size: 14px;
587
+ font-weight: 400;
588
+ color: var(--vh-color-neutral-04);
589
+ line-height: 1.5;
590
+ min-height: 80px;
591
+
592
+ &:hover {
593
+ border-color: var(--vh-color-neutral-03);
594
+ background: rgba(255, 255, 255, 0.02);
595
+ }
596
+ }
385
597
  }
@@ -12,6 +12,9 @@ import Tooltip from 'react-bootstrap/cjs/Tooltip.js';
12
12
  // Pull in custom CSS from vizhub-ui.
13
13
  import 'vizhub-ui/dist/vizhub-ui.css';
14
14
 
15
+ // Pull in Bootstrap Icons CSS.
16
+ import 'bootstrap-icons/font/bootstrap-icons.css';
17
+
15
18
  export {
16
19
  Form,
17
20
  Button,
@@ -64,6 +64,17 @@ export const useActions = (
64
64
  [dispatch],
65
65
  );
66
66
 
67
+ // True to show the visual editor
68
+ const setIsVisualEditorOpen = useCallback(
69
+ (value: boolean) => {
70
+ dispatch({
71
+ type: 'set_is_visual_editor_open',
72
+ value: value,
73
+ });
74
+ },
75
+ [dispatch],
76
+ );
77
+
67
78
  // True to show the settings modal.
68
79
  const setIsSearchOpen = useCallback(
69
80
  (value: boolean) => {
@@ -253,6 +264,7 @@ export const useActions = (
253
264
  openTab,
254
265
  closeTabs,
255
266
  setTheme,
267
+ setIsVisualEditorOpen,
256
268
  setIsSearchOpen,
257
269
  setSearch,
258
270
  setSearchResults,