vzcode 1.61.0 → 2.0.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/dist/assets/{index-Br76WttW.css → index-BM8tDh6M.css} +1 -1
- package/dist/assets/{index-EWyCE9d7.js → index-BgqNq-ww.js} +173 -173
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/client/VZCodeContext/types.ts +2 -0
- package/src/client/VZCodeContext/useVZCodeState.ts +5 -1
- package/src/client/VZSidebar/AIChat/MessageList.tsx +23 -0
- package/src/client/VZSidebar/DeleteConfirmationModal.tsx +15 -4
- package/src/client/VZSidebar/VisualEditor.tsx +11 -0
- package/src/client/VZSidebar/aiCopyPaste.ts +1 -1
- package/src/client/VZSidebar/index.tsx +42 -0
- package/src/client/VZSidebar/styles.scss +15 -0
- package/src/client/useFileCRUD.ts +14 -0
- package/src/server/aiChatHandler/aiEditing.ts +50 -1
- package/src/server/prettier.ts +115 -0
package/dist/index.html
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-BgqNq-ww.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BM8tDh6M.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
27
27
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -41,6 +41,7 @@ export type VZCodeContextValue = {
|
|
|
41
41
|
directoryName: string,
|
|
42
42
|
) => void;
|
|
43
43
|
deleteDirectory: (directoryId: string) => void;
|
|
44
|
+
deleteAllFiles: () => void;
|
|
44
45
|
|
|
45
46
|
setActiveFileId: (fileId: string | null) => void;
|
|
46
47
|
setActiveFileLeft: () => void;
|
|
@@ -244,4 +245,5 @@ export interface VZCodeProviderProps {
|
|
|
244
245
|
chatId: string;
|
|
245
246
|
canUndo: boolean;
|
|
246
247
|
}>;
|
|
248
|
+
iframeRef?: React.MutableRefObject<HTMLIFrameElement>;
|
|
247
249
|
}
|
|
@@ -52,6 +52,7 @@ export const useVZCodeState = ({
|
|
|
52
52
|
clearStoredAIPrompt,
|
|
53
53
|
getStoredAIPrompt,
|
|
54
54
|
additionalWidgets,
|
|
55
|
+
iframeRef: externalIframeRef,
|
|
55
56
|
}: Omit<
|
|
56
57
|
VZCodeProviderProps,
|
|
57
58
|
'children'
|
|
@@ -76,7 +77,8 @@ export const useVZCodeState = ({
|
|
|
76
77
|
|
|
77
78
|
const codeEditorRef = useRef(null);
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
// Use external iframeRef if provided, otherwise create our own
|
|
81
|
+
const iframeRef = externalIframeRef || useRef(null);
|
|
80
82
|
|
|
81
83
|
// The error message shows errors in order of priority:
|
|
82
84
|
// * `runtimeError` - errors from runtime execution, highest priority
|
|
@@ -182,6 +184,7 @@ export const useVZCodeState = ({
|
|
|
182
184
|
createDirectory,
|
|
183
185
|
renameDirectory,
|
|
184
186
|
deleteDirectory,
|
|
187
|
+
deleteAllFiles,
|
|
185
188
|
} = useFileCRUD({
|
|
186
189
|
submitOperation,
|
|
187
190
|
closeTabs,
|
|
@@ -459,6 +462,7 @@ export const useVZCodeState = ({
|
|
|
459
462
|
createDirectory,
|
|
460
463
|
renameDirectory,
|
|
461
464
|
deleteDirectory,
|
|
465
|
+
deleteAllFiles,
|
|
462
466
|
|
|
463
467
|
setActiveFileId,
|
|
464
468
|
setActiveFileLeft,
|
|
@@ -107,6 +107,29 @@ const MessageListComponent = ({
|
|
|
107
107
|
scrollToBottom,
|
|
108
108
|
]);
|
|
109
109
|
|
|
110
|
+
// Track previous loading state to detect when AI generation completes
|
|
111
|
+
const [prevIsLoading, setPrevIsLoading] =
|
|
112
|
+
useState(isLoading);
|
|
113
|
+
|
|
114
|
+
// Scroll to bottom when AI generation completes (loading changes from true to false)
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
// If AI was generating and now it's finished, scroll to bottom
|
|
117
|
+
if (prevIsLoading && !isLoading) {
|
|
118
|
+
// Force scroll to bottom regardless of user scroll state
|
|
119
|
+
// This ensures we always scroll to bottom when generation completes
|
|
120
|
+
messagesEndRef.current?.scrollIntoView({
|
|
121
|
+
behavior: 'smooth',
|
|
122
|
+
block: 'end',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Re-enable auto-scroll for future messages
|
|
126
|
+
setIsUserScrolled(false);
|
|
127
|
+
setAutoScrollEnabled(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setPrevIsLoading(isLoading);
|
|
131
|
+
}, [isLoading, prevIsLoading]);
|
|
132
|
+
|
|
110
133
|
// Clean up timeout on unmount
|
|
111
134
|
useEffect(() => {
|
|
112
135
|
return () => {
|
|
@@ -36,15 +36,26 @@ export const DeleteConfirmationModal = ({
|
|
|
36
36
|
<Modal show={show} onHide={onClose} centered>
|
|
37
37
|
<Modal.Header closeButton onClick={onClose}>
|
|
38
38
|
<Modal.Title>
|
|
39
|
-
Delete
|
|
39
|
+
Delete{' '}
|
|
40
|
+
{name === 'all files'
|
|
41
|
+
? 'All Files'
|
|
42
|
+
: isDirectory
|
|
43
|
+
? 'Directory'
|
|
44
|
+
: 'File'}
|
|
40
45
|
</Modal.Title>
|
|
41
46
|
</Modal.Header>
|
|
42
47
|
|
|
43
48
|
<Modal.Body>
|
|
44
49
|
<p>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
{name === 'all files' ? (
|
|
51
|
+
'This will delete all files in the project.'
|
|
52
|
+
) : (
|
|
53
|
+
<>
|
|
54
|
+
This will delete the{' '}
|
|
55
|
+
{isDirectory ? 'directory' : 'file'}{' '}
|
|
56
|
+
<strong>{name}</strong>.
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
48
59
|
</p>
|
|
49
60
|
</Modal.Body>
|
|
50
61
|
|
|
@@ -59,6 +59,17 @@ export const VisualEditor = () => {
|
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
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
|
+
|
|
62
73
|
const onInputUpdate = useCallback(
|
|
63
74
|
(
|
|
64
75
|
property: string,
|
|
@@ -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
|
-
//
|
|
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
|
+
};
|