vzcode 1.62.0 → 2.2.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.
@@ -12,13 +12,36 @@ import { EmptyState } from './EmptyState';
12
12
 
13
13
  const CONFIG_FILE_NAME = 'config.json';
14
14
 
15
+ // Helper function to calculate percentage for progress fill
16
+ const calculatePercentage = (
17
+ value: number,
18
+ min: number,
19
+ max: number,
20
+ ) => {
21
+ return ((value - min) / (max - min)) * 100;
22
+ };
23
+
24
+ // Helper function to format values nicely
25
+ const formatValue = (
26
+ value: number,
27
+ min: number,
28
+ max: number,
29
+ ) => {
30
+ // Determine decimal places based on the range
31
+ const range = max - min;
32
+ const decimalPlaces =
33
+ range < 10 ? 2 : range < 100 ? 1 : 0;
34
+ return Number(value).toFixed(decimalPlaces);
35
+ };
36
+
15
37
  export const VisualEditor = () => {
16
- const {
17
- files,
18
- submitOperation,
19
- runPrettierRef,
20
- iframeRef,
21
- } = useContext(VZCodeContext);
38
+ const { files, submitOperation, iframeRef } =
39
+ useContext(VZCodeContext);
40
+
41
+ // Local state to track slider values during user interaction
42
+ const [localValues, setLocalValues] = useState<{
43
+ [key: string]: number;
44
+ }>({});
22
45
 
23
46
  let configFileId: VizFileId | null = null;
24
47
  for (const fileId in files) {
@@ -70,22 +93,23 @@ export const VisualEditor = () => {
70
93
  );
71
94
  }
72
95
 
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
96
+ const onSliderChange = useCallback(
97
+ (property: string) =>
98
+ (event: React.ChangeEvent<HTMLInputElement>) => {
99
+ const newValue = parseFloat(
100
+ event.currentTarget.value,
101
+ );
80
102
 
81
- const newValueOfConsistentType =
82
- typeof previousValue === 'number'
83
- ? parseFloat(event.currentTarget.value)
84
- : event.currentTarget.value;
103
+ // Update local state immediately for responsive UI
104
+ setLocalValues((prev) => ({
105
+ ...prev,
106
+ [property]: newValue,
107
+ }));
85
108
 
109
+ // Update config.json
86
110
  const newConfigData = {
87
111
  ...configData,
88
- [property]: newValueOfConsistentType,
112
+ [property]: newValue,
89
113
  };
90
114
 
91
115
  submitOperation((document: VizContent) => ({
@@ -98,140 +122,104 @@ export const VisualEditor = () => {
98
122
  },
99
123
  },
100
124
  }));
101
-
102
- iframeRef.current.contentWindow.postMessage({
103
- [property]: newValueOfConsistentType,
104
- });
105
125
  },
106
- [configData, files, configFileId],
126
+ [configData, files, configFileId, setLocalValues],
107
127
  );
108
128
 
109
129
  const visualEditorWidgets: VisualEditorConfigEntry[] =
110
130
  configData.visualEditorWidgets;
111
131
 
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;
132
+ // Sync local values with config data when it changes (including remote updates)
133
+ useEffect(() => {
134
+ const newLocalValues: { [key: string]: number } = {};
135
+ visualEditorWidgets.forEach((widget) => {
136
+ if (widget.type === 'number') {
137
+ newLocalValues[widget.property] =
138
+ configData[widget.property];
139
+ }
140
+ });
141
+ setLocalValues(newLocalValues);
142
+ }, [configData, visualEditorWidgets]);
160
143
 
161
- const newConfigData = {
162
- ...configData,
163
- [property]: newValueOfConsistentType,
164
- };
144
+ // Track previous config state to detect changes from any source (remote clients, text editor, etc.)
145
+ const previousConfigRef = useRef<any>(null);
165
146
 
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
- }));
147
+ // Detect config.json changes and send updates to iframe
148
+ useEffect(() => {
149
+ if (!configFileId || !files || !files[configFileId]) {
150
+ return;
151
+ }
176
152
 
177
- iframeRef.current.contentWindow.postMessage({
178
- [property]: newValueOfConsistentType,
179
- });
153
+ let newConfigData;
154
+ try {
155
+ newConfigData = JSON.parse(files[configFileId].text);
156
+ } catch (error) {
157
+ // If config is invalid JSON, we can't process changes
158
+ return;
159
+ }
180
160
 
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
- );
161
+ const previousConfig = previousConfigRef.current;
192
162
 
193
- // Mouse event handlers for enhanced interactions
194
- const handleMouseDown = (property: string) => {
195
- setIsDragging(property);
196
- setActiveSlider(property);
197
- };
163
+ // Update the ref with the new config
164
+ previousConfigRef.current = newConfigData;
198
165
 
199
- const handleMouseUp = () => {
200
- setIsDragging(null);
201
- };
166
+ // Skip processing if this is the first time or if there's no previous config
167
+ if (!previousConfig) {
168
+ return;
169
+ }
202
170
 
203
- const handleMouseEnter = (property: string) => {
204
- setActiveSlider(property);
205
- };
171
+ // Find changed top-level properties
172
+ const changedProperties: { [key: string]: any } = {};
206
173
 
207
- const handleMouseLeave = () => {
208
- setActiveSlider(null);
209
- setHoverValue(null);
210
- };
174
+ // Check all properties in the new config
175
+ for (const key in newConfigData) {
176
+ if (newConfigData[key] !== previousConfig[key]) {
177
+ // Deep comparison for objects to detect actual changes
178
+ if (
179
+ typeof newConfigData[key] === 'object' &&
180
+ typeof previousConfig[key] === 'object'
181
+ ) {
182
+ if (
183
+ JSON.stringify(newConfigData[key]) !==
184
+ JSON.stringify(previousConfig[key])
185
+ ) {
186
+ changedProperties[key] = newConfigData[key];
187
+ }
188
+ } else {
189
+ changedProperties[key] = newConfigData[key];
190
+ }
191
+ }
192
+ }
211
193
 
212
- // Add global mouse up listener
213
- useEffect(() => {
214
- const handleGlobalMouseUp = () => {
215
- setIsDragging(null);
216
- };
194
+ // Check for deleted properties (properties that existed before but don't exist now)
195
+ for (const key in previousConfig) {
196
+ if (!(key in newConfigData)) {
197
+ changedProperties[key] = undefined;
198
+ }
199
+ }
217
200
 
218
- document.addEventListener(
219
- 'mouseup',
220
- handleGlobalMouseUp,
221
- );
222
- return () => {
223
- document.removeEventListener(
224
- 'mouseup',
225
- handleGlobalMouseUp,
226
- );
227
- };
228
- }, []);
201
+ // Send changed properties to iframe if any changes were detected
202
+ if (Object.keys(changedProperties).length > 0) {
203
+ try {
204
+ iframeRef.current.contentWindow.postMessage(
205
+ changedProperties,
206
+ );
207
+ } catch (error) {
208
+ console.error(
209
+ 'Failed to send config changes to iframe:',
210
+ error,
211
+ );
212
+ }
213
+ }
214
+ }, [files, configFileId, iframeRef]);
229
215
 
230
216
  return (
231
217
  <div className="visual-editor">
232
218
  {visualEditorWidgets.map((widgetConfig, index) => {
233
219
  if (widgetConfig.type === 'number') {
220
+ // Use local value if available, otherwise fall back to config value
234
221
  const currentValue =
222
+ localValues[widgetConfig.property] ??
235
223
  configData[widgetConfig.property];
236
224
  const percentage = calculatePercentage(
237
225
  currentValue,
@@ -267,11 +255,10 @@ export const VisualEditor = () => {
267
255
  min={widgetConfig.min}
268
256
  max={widgetConfig.max}
269
257
  step="any"
270
- onInput={onInputUpdate(
258
+ value={currentValue}
259
+ onChange={onSliderChange(
271
260
  widgetConfig.property,
272
- configData[widgetConfig.property],
273
261
  )}
274
- defaultValue={currentValue}
275
262
  />
276
263
  <div
277
264
  className="slider-track-fill"
@@ -102,7 +102,7 @@ export const createAICopyPasteHandlers = (
102
102
  ) {
103
103
  // Use mergeFileChanges utility from editcodewithai
104
104
  const mergedFiles = mergeFileChanges(
105
- files,
105
+ files || {},
106
106
  parsed.files,
107
107
  );
108
108
 
@@ -35,6 +35,7 @@ import { VZCodeContext } from '../VZCodeContext';
35
35
  import { AIChat } from './AIChat';
36
36
  import { Listing } from './Listing';
37
37
  import { Search } from './Search';
38
+ import { DeleteConfirmationModal } from './DeleteConfirmationModal';
38
39
  import { useDragAndDrop } from './useDragAndDrop';
39
40
  import { createAICopyPasteHandlers } from './aiCopyPaste';
40
41
  import {
@@ -157,6 +158,7 @@ export const VZSidebar = ({
157
158
  updatePresenceIndicator,
158
159
  setVoiceChatModalOpen,
159
160
  submitOperation,
161
+ deleteAllFiles,
160
162
  } = useContext(VZCodeContext);
161
163
 
162
164
  const fileTree = useMemo(
@@ -178,6 +180,10 @@ export const VZSidebar = ({
178
180
  const [exportButtonText, setExportButtonText] =
179
181
  useState('Export to ZIP');
180
182
 
183
+ // State for delete all confirmation modal
184
+ const [showDeleteAllModal, setShowDeleteAllModal] =
185
+ useState(false);
186
+
181
187
  // Create AI copy/paste handlers
182
188
  const {
183
189
  handleCopyForAI,
@@ -195,6 +201,20 @@ export const VZSidebar = ({
195
201
  [files, submitOperation],
196
202
  );
197
203
 
204
+ // Delete all handlers
205
+ const handleDeleteAllClick = useCallback(() => {
206
+ setShowDeleteAllModal(true);
207
+ }, []);
208
+
209
+ const handleDeleteAllModalClose = useCallback(() => {
210
+ setShowDeleteAllModal(false);
211
+ }, []);
212
+
213
+ const handleDeleteAllConfirm = useCallback(() => {
214
+ setShowDeleteAllModal(false);
215
+ deleteAllFiles();
216
+ }, [deleteAllFiles]);
217
+
198
218
  const { sidebarWidth, setSidebarView } = useContext(
199
219
  SplitPaneResizeContext,
200
220
  );
@@ -653,6 +673,17 @@ export const VZSidebar = ({
653
673
  <i className="bi bi-clipboard-plus"></i>
654
674
  {pasteButtonText}
655
675
  </button>
676
+
677
+ {filesExist && (
678
+ <button
679
+ className="ai-button delete-all-button"
680
+ onClick={handleDeleteAllClick}
681
+ title="Delete all files"
682
+ >
683
+ <i className="bi bi-trash"></i>
684
+ delete all
685
+ </button>
686
+ )}
656
687
  </div>
657
688
  )}
658
689
  {enableConnectionStatus && (
@@ -681,6 +712,17 @@ export const VZSidebar = ({
681
712
  </div>
682
713
  </div>
683
714
  )}
715
+
716
+ {/* Delete All Files Confirmation Modal */}
717
+ {showDeleteAllModal && (
718
+ <DeleteConfirmationModal
719
+ show={showDeleteAllModal}
720
+ onClose={handleDeleteAllModalClose}
721
+ onConfirm={handleDeleteAllConfirm}
722
+ isDirectory={false}
723
+ name="all files"
724
+ />
725
+ )}
684
726
  </div>
685
727
  );
686
728
  };
@@ -357,6 +357,21 @@
357
357
  border-color: var(--vh-color-success-04);
358
358
  }
359
359
  }
360
+
361
+ &.delete-all-button {
362
+ border-color: var(--vh-color-neutral-02);
363
+ color: var(--vh-color-neutral-03);
364
+ font-size: 11px;
365
+ font-weight: 400;
366
+ opacity: 0.7;
367
+
368
+ &:hover {
369
+ background: var(--vh-color-hover-dark);
370
+ border-color: var(--vh-color-danger-03);
371
+ color: var(--vh-color-danger-04);
372
+ opacity: 1;
373
+ }
374
+ }
360
375
  }
361
376
  }
362
377
 
@@ -203,6 +203,19 @@ export const useFileCRUD = ({
203
203
  [submitOperation, closeTabs],
204
204
  );
205
205
 
206
+ // Deletes all files
207
+ const deleteAllFiles = useCallback(() => {
208
+ const tabsToClose: Array<VizFileId> = [];
209
+ submitOperation((document: VizContent) => {
210
+ // Collect all file IDs for closing tabs
211
+ for (const fileId in document.files) {
212
+ tabsToClose.push(fileId);
213
+ }
214
+ return { ...document, files: {} };
215
+ });
216
+ closeTabs(tabsToClose);
217
+ }, [submitOperation, closeTabs]);
218
+
206
219
  return {
207
220
  createFile,
208
221
  renameFile,
@@ -210,5 +223,6 @@ export const useFileCRUD = ({
210
223
  createDirectory,
211
224
  renameDirectory,
212
225
  deleteDirectory,
226
+ deleteAllFiles,
213
227
  };
214
228
  };
@@ -7,6 +7,9 @@ import {
7
7
  createFilesSnapshot,
8
8
  generateFilesUnifiedDiff,
9
9
  } from '../../utils/fileDiff.js';
10
+ import { formatFiles } from '../prettier.js';
11
+ import { createSubmitOperation } from '../../submitOperation.js';
12
+ import { VizContent } from '@vizhub/viz-types';
10
13
 
11
14
  // Dev flag for waiting 1 second before starting the LLM function.
12
15
  // Useful for debugging and testing purposes, e.g. checking the typing indicator.
@@ -83,9 +86,55 @@ export const performAIEditing = async ({
83
86
  // Call the LLM function which will handle streaming and incremental file updates
84
87
  const result = await llmFunction(fullPrompt);
85
88
 
89
+ // 3. Run Prettier on changed files before running code
90
+ const afterLLMFiles = createFilesSnapshot(
91
+ shareDBDoc.data.files,
92
+ );
93
+
94
+ // Find files that were changed by the AI
95
+ const changedFileIds = Object.keys(afterLLMFiles).filter(
96
+ (fileId) =>
97
+ beforeFiles[fileId]?.text !==
98
+ afterLLMFiles[fileId]?.text,
99
+ );
100
+
101
+ if (changedFileIds.length > 0) {
102
+ // Format the changed files
103
+ const formattedFiles = await formatFiles(
104
+ shareDBDoc.data.files,
105
+ changedFileIds,
106
+ );
107
+
108
+ // Apply formatted versions to shareDBDoc if formatting succeeded
109
+ if (Object.keys(formattedFiles).length > 0) {
110
+ const submitOperation =
111
+ createSubmitOperation(shareDBDoc);
112
+ submitOperation((document: VizContent) => {
113
+ const updatedFiles = { ...document.files };
114
+
115
+ // Apply each formatted file
116
+ Object.entries(formattedFiles).forEach(
117
+ ([fileId, formattedText]) => {
118
+ if (updatedFiles[fileId]) {
119
+ updatedFiles[fileId] = {
120
+ ...updatedFiles[fileId],
121
+ text: formattedText,
122
+ };
123
+ }
124
+ },
125
+ );
126
+
127
+ return {
128
+ ...document,
129
+ files: updatedFiles,
130
+ };
131
+ });
132
+ }
133
+ }
134
+
86
135
  runCode();
87
136
 
88
- // 3. Capture the state of files after editing and generate diff
137
+ // 4. Capture the state of files after editing and formatting, then generate diff
89
138
  const afterFiles = createFilesSnapshot(
90
139
  shareDBDoc.data.files,
91
140
  );
@@ -0,0 +1,115 @@
1
+ import { format } from 'prettier/standalone';
2
+ import * as prettierPluginBabel from 'prettier/plugins/babel';
3
+ import * as prettierPluginEstree from 'prettier/plugins/estree';
4
+ import * as prettierPluginHtml from 'prettier/plugins/html';
5
+ import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
6
+ import * as prettierPluginCSS from 'prettier/plugins/postcss';
7
+ import * as prettierPluginTypescript from 'prettier/plugins/typescript';
8
+ import { VizFiles, VizFileId } from '@vizhub/viz-types';
9
+
10
+ // Parser mappings - matches client-side implementation
11
+ const parsers = {
12
+ js: 'babel',
13
+ jsx: 'babel',
14
+ mjs: 'babel',
15
+ cjs: 'babel',
16
+ ts: 'typescript',
17
+ tsx: 'typescript',
18
+ css: 'css',
19
+ html: 'html',
20
+ json: 'json',
21
+ json5: 'json5',
22
+ md: 'markdown',
23
+ markdown: 'markdown',
24
+ } as const;
25
+
26
+ // Prettier plugins - matches client-side implementation
27
+ const plugins = [
28
+ prettierPluginBabel,
29
+ prettierPluginEstree,
30
+ prettierPluginHtml,
31
+ prettierPluginMarkdown,
32
+ prettierPluginTypescript,
33
+ prettierPluginCSS,
34
+ ] as any;
35
+
36
+ // Prettier options - matches client-side implementation and .prettierrc
37
+ const prettierOptions = {
38
+ proseWrap: 'always' as const,
39
+ singleQuote: true,
40
+ printWidth: 60,
41
+ };
42
+
43
+ /**
44
+ * Extracts file extension from filename
45
+ * Example: 'foo.js' => 'js'
46
+ */
47
+ const getFileExtension = (fileName: string): string => {
48
+ const match = fileName.match(/\.([^.]+)$/);
49
+ return match ? match[1] : '';
50
+ };
51
+
52
+ /**
53
+ * Formats a single file using Prettier
54
+ */
55
+ export const formatFile = async (
56
+ fileText: string,
57
+ fileName: string,
58
+ ): Promise<string | null> => {
59
+ const fileExtension = getFileExtension(fileName);
60
+ const parser =
61
+ parsers[fileExtension as keyof typeof parsers];
62
+
63
+ // If no parser is found, return null (no formatting)
64
+ if (!parser) {
65
+ return null;
66
+ }
67
+
68
+ try {
69
+ const formatted = await format(fileText, {
70
+ parser,
71
+ plugins,
72
+ ...prettierOptions,
73
+ });
74
+ return formatted;
75
+ } catch (error) {
76
+ // Log error but don't throw - return original text
77
+ console.error(
78
+ `Prettier formatting error for ${fileName}:`,
79
+ error,
80
+ );
81
+ return null;
82
+ }
83
+ };
84
+
85
+ /**
86
+ * Formats multiple files using Prettier
87
+ * Only formats files that have supported extensions
88
+ * Returns a map of fileId -> formatted text for successfully formatted files
89
+ */
90
+ export const formatFiles = async (
91
+ files: VizFiles,
92
+ fileIds?: VizFileId[],
93
+ ): Promise<{ [fileId: VizFileId]: string }> => {
94
+ const results: { [fileId: VizFileId]: string } = {};
95
+ const targetFileIds = fileIds || Object.keys(files);
96
+
97
+ // Process files in parallel for better performance
98
+ const formatPromises = targetFileIds.map(
99
+ async (fileId) => {
100
+ const file = files[fileId];
101
+ if (!file) return;
102
+
103
+ const formatted = await formatFile(
104
+ file.text,
105
+ file.name,
106
+ );
107
+ if (formatted !== null && formatted !== file.text) {
108
+ results[fileId] = formatted;
109
+ }
110
+ },
111
+ );
112
+
113
+ await Promise.all(formatPromises);
114
+ return results;
115
+ };