vzcode 1.43.0 → 1.44.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/bindings_wasm_bg-CH5ekNIA.wasm +0 -0
- package/dist/assets/{buildWorker-B-vXZ9gA.js → buildWorker-CRLuRnCr.js} +100 -58
- package/dist/assets/{index-CP9o_hN5.css → index-Beqaj63x.css} +1 -1
- package/dist/assets/index-C46Hp5ym.js +340 -0
- package/dist/index.html +2 -2
- package/package.json +15 -15
- package/src/client/ImageViewer/index.tsx +91 -0
- package/src/client/ImageViewer/style.scss +41 -0
- package/src/client/VZCodeContext.tsx +27 -6
- package/src/client/VZMiddle.tsx +22 -7
- package/src/client/VZRight.tsx +6 -1
- package/src/client/VZSidebar/aiCopyPaste.ts +55 -0
- package/src/client/VZSidebar/index.tsx +18 -1
- package/src/client/VZSidebar/styles.scss +9 -0
- package/src/client/VZSidebar/useDragAndDrop.tsx +133 -8
- package/src/client/useRuntimeError.ts +32 -0
- package/src/client/utils/isImageFile.ts +12 -0
- package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
- package/dist/assets/index-B83lKfVG.js +0 -329
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-C46Hp5ym.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Beqaj63x.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
27
27
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -124,23 +124,23 @@
|
|
|
124
124
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
125
125
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
126
126
|
"@typescript/vfs": "^1.6.1",
|
|
127
|
-
"@uiw/codemirror-theme-abcdef": "^4.24.
|
|
128
|
-
"@uiw/codemirror-theme-dracula": "^4.24.
|
|
129
|
-
"@uiw/codemirror-theme-eclipse": "^4.24.
|
|
130
|
-
"@uiw/codemirror-theme-github": "^4.24.
|
|
131
|
-
"@uiw/codemirror-theme-material": "^4.24.
|
|
132
|
-
"@uiw/codemirror-theme-nord": "^4.24.
|
|
133
|
-
"@uiw/codemirror-theme-okaidia": "^4.24.
|
|
134
|
-
"@uiw/codemirror-theme-xcode": "^4.24.
|
|
135
|
-
"@uiw/codemirror-themes": "^4.24.
|
|
127
|
+
"@uiw/codemirror-theme-abcdef": "^4.24.2",
|
|
128
|
+
"@uiw/codemirror-theme-dracula": "^4.24.2",
|
|
129
|
+
"@uiw/codemirror-theme-eclipse": "^4.24.2",
|
|
130
|
+
"@uiw/codemirror-theme-github": "^4.24.2",
|
|
131
|
+
"@uiw/codemirror-theme-material": "^4.24.2",
|
|
132
|
+
"@uiw/codemirror-theme-nord": "^4.24.2",
|
|
133
|
+
"@uiw/codemirror-theme-okaidia": "^4.24.2",
|
|
134
|
+
"@uiw/codemirror-theme-xcode": "^4.24.2",
|
|
135
|
+
"@uiw/codemirror-themes": "^4.24.2",
|
|
136
136
|
"@valtown/codemirror-ts": "^2.3.1",
|
|
137
|
-
"@vizhub/runtime": "^4.
|
|
137
|
+
"@vizhub/runtime": "^4.2.0",
|
|
138
138
|
"@vizhub/viz-types": "^0.3.0",
|
|
139
139
|
"@vizhub/viz-utils": "^1.3.0",
|
|
140
140
|
"body-parser": "^2.2.0",
|
|
141
141
|
"codemirror": "^6.0.2",
|
|
142
142
|
"codemirror-copilot": "^0.0.7",
|
|
143
|
-
"codemirror-ot": "^
|
|
143
|
+
"codemirror-ot": "^5.0.0",
|
|
144
144
|
"color-hash": "^2.0.2",
|
|
145
145
|
"comlink": "^4.4.2",
|
|
146
146
|
"d3-array": "^3.2.4",
|
|
@@ -151,9 +151,9 @@
|
|
|
151
151
|
"express": "^5.1.0",
|
|
152
152
|
"ignore": "^7.0.5",
|
|
153
153
|
"json0-ot-diff": "^1.1.2",
|
|
154
|
+
"jszip": "^3.10.1",
|
|
154
155
|
"livekit-server-sdk": "^2.13.1",
|
|
155
156
|
"llm-code-format": "^3.0.0",
|
|
156
|
-
"ngrok": "^5.0.0-beta.2",
|
|
157
157
|
"npm": "^11.5.1",
|
|
158
158
|
"open": "^10.2.0",
|
|
159
159
|
"prettier-plugin-svelte": "^3.4.0",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"vitest": "^3.2.4"
|
|
188
188
|
},
|
|
189
189
|
"optionalDependencies": {
|
|
190
|
-
"@rollup/rollup-darwin-arm64": "^4.46.
|
|
191
|
-
"@rollup/rollup-win32-x64-msvc": "^4.46.
|
|
190
|
+
"@rollup/rollup-darwin-arm64": "^4.46.2",
|
|
191
|
+
"@rollup/rollup-win32-x64-msvc": "^4.46.2"
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { VZCodeContext } from '../VZCodeContext';
|
|
3
|
+
import './style.scss';
|
|
4
|
+
|
|
5
|
+
export const ImageViewer = () => {
|
|
6
|
+
const { activePane, content } = useContext(VZCodeContext);
|
|
7
|
+
|
|
8
|
+
// Get the active file
|
|
9
|
+
const activeFileId = activePane?.activeFileId;
|
|
10
|
+
const activeFile =
|
|
11
|
+
activeFileId && content?.files?.[activeFileId];
|
|
12
|
+
|
|
13
|
+
if (!activeFile) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="image-viewer-error">
|
|
16
|
+
No file selected
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if it's an image file based on extension
|
|
22
|
+
const isImageFile = activeFile.name.match(
|
|
23
|
+
/\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (!isImageFile) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="image-viewer-error">
|
|
29
|
+
Not an image file
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get the MIME type from the file extension
|
|
35
|
+
const getImageMimeType = (fileName: string): string => {
|
|
36
|
+
const extension = fileName
|
|
37
|
+
.split('.')
|
|
38
|
+
.pop()
|
|
39
|
+
?.toLowerCase();
|
|
40
|
+
switch (extension) {
|
|
41
|
+
case 'jpg':
|
|
42
|
+
case 'jpeg':
|
|
43
|
+
return 'image/jpeg';
|
|
44
|
+
case 'png':
|
|
45
|
+
return 'image/png';
|
|
46
|
+
case 'gif':
|
|
47
|
+
return 'image/gif';
|
|
48
|
+
case 'bmp':
|
|
49
|
+
return 'image/bmp';
|
|
50
|
+
case 'svg':
|
|
51
|
+
return 'image/svg+xml';
|
|
52
|
+
case 'webp':
|
|
53
|
+
return 'image/webp';
|
|
54
|
+
default:
|
|
55
|
+
return 'image/png'; // fallback
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mimeType = getImageMimeType(activeFile.name);
|
|
60
|
+
const extension = activeFile.name
|
|
61
|
+
.split('.')
|
|
62
|
+
.pop()
|
|
63
|
+
?.toLowerCase();
|
|
64
|
+
|
|
65
|
+
// For SVG files, the content might already be the SVG text, not base64
|
|
66
|
+
const imageSrc =
|
|
67
|
+
extension === 'svg' &&
|
|
68
|
+
!activeFile.text.startsWith('PHN2Zw') // Check if it's not base64-encoded SVG
|
|
69
|
+
? `data:${mimeType};utf8,${encodeURIComponent(activeFile.text)}`
|
|
70
|
+
: `data:${mimeType};base64,${activeFile.text}`;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="image-viewer">
|
|
74
|
+
<div className="image-viewer-content">
|
|
75
|
+
<img
|
|
76
|
+
src={imageSrc}
|
|
77
|
+
alt={activeFile.name}
|
|
78
|
+
className="image-viewer-img"
|
|
79
|
+
onError={(e) => {
|
|
80
|
+
console.error(
|
|
81
|
+
'Failed to load image:',
|
|
82
|
+
activeFile.name,
|
|
83
|
+
);
|
|
84
|
+
(e.target as HTMLImageElement).style.display =
|
|
85
|
+
'none';
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.image-viewer {
|
|
2
|
+
height: 100%;
|
|
3
|
+
width: 100%;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
background-color: var(--bs-body-bg);
|
|
7
|
+
color: var(--bs-body-color);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.image-viewer-header {
|
|
11
|
+
padding: 12px 16px;
|
|
12
|
+
border-bottom: 1px solid var(--bs-border-color);
|
|
13
|
+
background-color: var(--bs-tertiary-bg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.image-viewer-content {
|
|
17
|
+
flex: 1;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
overflow: auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.image-viewer-img {
|
|
26
|
+
max-width: 100%;
|
|
27
|
+
max-height: 100%;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
30
|
+
object-fit: contain;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.image-viewer-error {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
height: 100%;
|
|
38
|
+
color: var(--bs-text-muted);
|
|
39
|
+
font-family: var(--bs-font-monospace);
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
}
|
|
@@ -34,6 +34,7 @@ import { useKeyboardShortcuts } from './useKeyboardShortcuts';
|
|
|
34
34
|
import { useOpenDirectories } from './useOpenDirectories';
|
|
35
35
|
import { usePrettier } from './usePrettier';
|
|
36
36
|
import { useRunCode } from './useRunCode';
|
|
37
|
+
import { useRuntimeError } from './useRuntimeError';
|
|
37
38
|
import { useURLSync } from './useURLSync';
|
|
38
39
|
import { createInitialState, vzReducer } from './vzReducer';
|
|
39
40
|
import { findPane } from './vzReducer/findPane';
|
|
@@ -110,6 +111,12 @@ export type VZCodeContextValue = {
|
|
|
110
111
|
|
|
111
112
|
errorMessage: string | null;
|
|
112
113
|
|
|
114
|
+
// Runtime error handling
|
|
115
|
+
handleRuntimeError: (
|
|
116
|
+
formattedErrorMessage: string,
|
|
117
|
+
) => void;
|
|
118
|
+
clearRuntimeError: () => void;
|
|
119
|
+
|
|
113
120
|
search: SearchResults;
|
|
114
121
|
isSearchOpen: boolean;
|
|
115
122
|
setIsSearchOpen: (isSearchOpen: boolean) => void;
|
|
@@ -231,21 +238,32 @@ export const VZCodeProvider = ({
|
|
|
231
238
|
prettierWorker,
|
|
232
239
|
});
|
|
233
240
|
|
|
241
|
+
// Handle runtime errors from the iframe
|
|
242
|
+
const {
|
|
243
|
+
runtimeError,
|
|
244
|
+
handleRuntimeError,
|
|
245
|
+
clearRuntimeError,
|
|
246
|
+
} = useRuntimeError();
|
|
247
|
+
|
|
234
248
|
const runCodeRef = useRunCode(submitOperation);
|
|
235
249
|
|
|
236
250
|
const sidebarRef = useRef(null);
|
|
237
251
|
|
|
238
252
|
const codeEditorRef = useRef(null);
|
|
239
253
|
|
|
240
|
-
// The error message shows
|
|
254
|
+
// The error message shows errors in order of priority:
|
|
255
|
+
// * `runtimeError` - errors from runtime execution, highest priority
|
|
241
256
|
// * `prettierError` - errors from Prettier, client-side only
|
|
242
257
|
// * `codeError` - errors from an external source, such as
|
|
243
258
|
// build-time errors or intercepted runtime errors.
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
259
|
+
// Runtime errors are prioritized since they indicate immediate execution issues.
|
|
260
|
+
// Prettier errors are next since they surface syntax errors that are likely
|
|
261
|
+
// to be useful to the user.
|
|
262
|
+
const errorMessage: string | null = runtimeError
|
|
263
|
+
? runtimeError
|
|
264
|
+
: prettierError
|
|
265
|
+
? prettierError
|
|
266
|
+
: codeError;
|
|
249
267
|
|
|
250
268
|
// Set up the reducer that manages much of the application state.
|
|
251
269
|
// See https://react.dev/reference/react/useReducer
|
|
@@ -493,6 +511,9 @@ export const VZCodeProvider = ({
|
|
|
493
511
|
|
|
494
512
|
errorMessage,
|
|
495
513
|
|
|
514
|
+
handleRuntimeError,
|
|
515
|
+
clearRuntimeError,
|
|
516
|
+
|
|
496
517
|
isCreateFileModalOpen,
|
|
497
518
|
handleOpenCreateFileModal,
|
|
498
519
|
handleCloseCreateFileModal,
|
package/src/client/VZMiddle.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from 'react';
|
|
|
2
2
|
import { SplitPaneResizeContext } from './SplitPaneResizeContext';
|
|
3
3
|
import { TabList } from './TabList';
|
|
4
4
|
import { CodeEditor } from './CodeEditor';
|
|
5
|
+
import { ImageViewer } from './ImageViewer';
|
|
5
6
|
import { CodeErrorOverlay } from './CodeErrorOverlay';
|
|
6
7
|
import { PresenceNotifications } from './PresenceNotifications';
|
|
7
8
|
import { AIAssistWidget } from './AIAssist/AIAssistWidget';
|
|
@@ -12,6 +13,7 @@ import { EditorView } from '@codemirror/view';
|
|
|
12
13
|
import { Diagnostic } from '@codemirror/lint';
|
|
13
14
|
import { VizContent } from '@vizhub/viz-types';
|
|
14
15
|
import { LeafPane } from '../types';
|
|
16
|
+
import { isImageFile } from './utils/isImageFile';
|
|
15
17
|
|
|
16
18
|
// TODO modify this to handle the SplitPane type
|
|
17
19
|
// Recursive structure?
|
|
@@ -50,13 +52,26 @@ const PaneView = ({
|
|
|
50
52
|
activeFileId={pane.activeFileId}
|
|
51
53
|
tabList={pane.tabList}
|
|
52
54
|
/>
|
|
53
|
-
{isClient &&
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
{isClient &&
|
|
56
|
+
content &&
|
|
57
|
+
pane.activeFileId &&
|
|
58
|
+
(() => {
|
|
59
|
+
// Get the active file to determine if it's an image
|
|
60
|
+
const activeFile =
|
|
61
|
+
content.files[pane.activeFileId];
|
|
62
|
+
const shouldShowImageViewer =
|
|
63
|
+
activeFile && isImageFile(activeFile.name);
|
|
64
|
+
|
|
65
|
+
return shouldShowImageViewer ? (
|
|
66
|
+
<ImageViewer />
|
|
67
|
+
) : (
|
|
68
|
+
<CodeEditor
|
|
69
|
+
customInteractRules={customInteractRules}
|
|
70
|
+
aiCopilotEndpoint={aiCopilotEndpoint}
|
|
71
|
+
esLintSource={esLintSource}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
})()}
|
|
60
75
|
{isClient &&
|
|
61
76
|
enableAIAssist &&
|
|
62
77
|
content &&
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -20,7 +20,8 @@ export const VZRight = () => {
|
|
|
20
20
|
const isFirstRunRef = useRef(true);
|
|
21
21
|
|
|
22
22
|
// Get access to the current files.
|
|
23
|
-
const { content } =
|
|
23
|
+
const { content, handleRuntimeError, clearRuntimeError } =
|
|
24
|
+
useContext(VZCodeContext);
|
|
24
25
|
|
|
25
26
|
const files = useMemo(
|
|
26
27
|
() =>
|
|
@@ -46,11 +47,15 @@ export const VZRight = () => {
|
|
|
46
47
|
console.error('Build error:', error);
|
|
47
48
|
}
|
|
48
49
|
},
|
|
50
|
+
handleRuntimeError,
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Run code in the iframe
|
|
53
55
|
if (isFirstRunRef.current || isInteracting) {
|
|
56
|
+
// Clear runtime errors when new code runs
|
|
57
|
+
clearRuntimeError();
|
|
58
|
+
|
|
54
59
|
runtimeRef.current?.run({
|
|
55
60
|
files,
|
|
56
61
|
enableHotReloading: true,
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
} from 'editcodewithai';
|
|
9
9
|
import { VizFiles } from '@vizhub/viz-types';
|
|
10
10
|
import { vizFilesToFileCollection } from '@vizhub/viz-utils';
|
|
11
|
+
import JSZip from 'jszip';
|
|
11
12
|
|
|
12
13
|
export const createAICopyPasteHandlers = (
|
|
13
14
|
files: VizFiles,
|
|
14
15
|
submitOperation: any,
|
|
15
16
|
setCopyButtonText: (text: string) => void,
|
|
16
17
|
setPasteButtonText: (text: string) => void,
|
|
18
|
+
setExportButtonText: (text: string) => void,
|
|
17
19
|
) => {
|
|
18
20
|
// Copy for AI - formats all files and copies to clipboard
|
|
19
21
|
const handleCopyForAI = async () => {
|
|
@@ -124,8 +126,61 @@ export const createAICopyPasteHandlers = (
|
|
|
124
126
|
}
|
|
125
127
|
};
|
|
126
128
|
|
|
129
|
+
// Export to ZIP - creates and downloads a ZIP file with all viz files
|
|
130
|
+
const handleExportToZip = async () => {
|
|
131
|
+
if (!files || Object.keys(files).length === 0) {
|
|
132
|
+
setExportButtonText('No files to export');
|
|
133
|
+
setTimeout(
|
|
134
|
+
() => setExportButtonText('Export to ZIP'),
|
|
135
|
+
2000,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Create the ZIP
|
|
142
|
+
const zip = new JSZip();
|
|
143
|
+
|
|
144
|
+
// Add each file to the ZIP
|
|
145
|
+
Object.entries(files).forEach(([fileId, file]) => {
|
|
146
|
+
const fileName = file.name || fileId;
|
|
147
|
+
const fileContent = file.text || '';
|
|
148
|
+
zip.file(fileName, fileContent);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Generate and download
|
|
152
|
+
const blob = await zip.generateAsync({
|
|
153
|
+
type: 'blob',
|
|
154
|
+
});
|
|
155
|
+
const url = URL.createObjectURL(blob);
|
|
156
|
+
const a = document.createElement('a');
|
|
157
|
+
a.href = url;
|
|
158
|
+
a.download = 'viz-files.zip';
|
|
159
|
+
a.click();
|
|
160
|
+
URL.revokeObjectURL(url);
|
|
161
|
+
|
|
162
|
+
// Show success feedback
|
|
163
|
+
setExportButtonText('Exported!');
|
|
164
|
+
setTimeout(
|
|
165
|
+
() => setExportButtonText('Export to ZIP'),
|
|
166
|
+
2000,
|
|
167
|
+
);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(
|
|
170
|
+
'Failed to export files to ZIP:',
|
|
171
|
+
error,
|
|
172
|
+
);
|
|
173
|
+
setExportButtonText('Error');
|
|
174
|
+
setTimeout(
|
|
175
|
+
() => setExportButtonText('Export to ZIP'),
|
|
176
|
+
2000,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
127
181
|
return {
|
|
128
182
|
handleCopyForAI,
|
|
129
183
|
handlePasteForAI,
|
|
184
|
+
handleExportToZip,
|
|
130
185
|
};
|
|
131
186
|
};
|
|
@@ -164,15 +164,22 @@ export const VZSidebar = ({
|
|
|
164
164
|
useState('Copy for AI');
|
|
165
165
|
const [pasteButtonText, setPasteButtonText] =
|
|
166
166
|
useState('Paste for AI');
|
|
167
|
+
const [exportButtonText, setExportButtonText] =
|
|
168
|
+
useState('Export to ZIP');
|
|
167
169
|
|
|
168
170
|
// Create AI copy/paste handlers
|
|
169
|
-
const {
|
|
171
|
+
const {
|
|
172
|
+
handleCopyForAI,
|
|
173
|
+
handlePasteForAI,
|
|
174
|
+
handleExportToZip,
|
|
175
|
+
} = useMemo(
|
|
170
176
|
() =>
|
|
171
177
|
createAICopyPasteHandlers(
|
|
172
178
|
files,
|
|
173
179
|
submitOperation,
|
|
174
180
|
setCopyButtonText,
|
|
175
181
|
setPasteButtonText,
|
|
182
|
+
setExportButtonText,
|
|
176
183
|
),
|
|
177
184
|
[files, submitOperation],
|
|
178
185
|
);
|
|
@@ -548,6 +555,15 @@ export const VZSidebar = ({
|
|
|
548
555
|
</div>
|
|
549
556
|
{!isAIChatOpen && !isSearchOpen && (
|
|
550
557
|
<div className="ai-buttons">
|
|
558
|
+
{filesExist && (
|
|
559
|
+
<button
|
|
560
|
+
className="ai-button export-button"
|
|
561
|
+
onClick={handleExportToZip}
|
|
562
|
+
title="Export files to ZIP"
|
|
563
|
+
>
|
|
564
|
+
{exportButtonText}
|
|
565
|
+
</button>
|
|
566
|
+
)}
|
|
551
567
|
{filesExist && (
|
|
552
568
|
<button
|
|
553
569
|
className="ai-button copy-button"
|
|
@@ -557,6 +573,7 @@ export const VZSidebar = ({
|
|
|
557
573
|
{copyButtonText}
|
|
558
574
|
</button>
|
|
559
575
|
)}
|
|
576
|
+
|
|
560
577
|
<button
|
|
561
578
|
className="ai-button paste-button"
|
|
562
579
|
onClick={handlePasteForAI}
|
|
@@ -335,6 +335,15 @@
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
&.export-button {
|
|
339
|
+
border-color: var(--vh-color-primary-03);
|
|
340
|
+
|
|
341
|
+
&:hover {
|
|
342
|
+
background: var(--vh-color-hover-dark);
|
|
343
|
+
border-color: var(--vh-color-neutral-04);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
&.paste-button {
|
|
339
348
|
border-color: var(--vh-color-success-03);
|
|
340
349
|
|
|
@@ -78,6 +78,23 @@ export const useDragAndDrop = () => {
|
|
|
78
78
|
return isText;
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
const isImageFile = (file: File): boolean => {
|
|
82
|
+
const imageTypes = ['image/'];
|
|
83
|
+
const isImage =
|
|
84
|
+
imageTypes.some((type) =>
|
|
85
|
+
file.type.startsWith(type),
|
|
86
|
+
) ||
|
|
87
|
+
file.name.match(
|
|
88
|
+
/\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
|
|
89
|
+
) !== null;
|
|
90
|
+
|
|
91
|
+
DEBUG &&
|
|
92
|
+
console.log(
|
|
93
|
+
`[useDragAndDrop] File ${file.name} is${isImage ? '' : ' not'} an image file`,
|
|
94
|
+
);
|
|
95
|
+
return isImage;
|
|
96
|
+
};
|
|
97
|
+
|
|
81
98
|
const readFileAsText = (file: File): Promise<string> => {
|
|
82
99
|
return new Promise((resolve, reject) => {
|
|
83
100
|
DEBUG &&
|
|
@@ -127,6 +144,98 @@ export const useDragAndDrop = () => {
|
|
|
127
144
|
});
|
|
128
145
|
};
|
|
129
146
|
|
|
147
|
+
const readImageAsBase64 = (
|
|
148
|
+
file: File,
|
|
149
|
+
): Promise<string> => {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
DEBUG &&
|
|
152
|
+
console.log(
|
|
153
|
+
`[useDragAndDrop] Reading image file: ${file.name}`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!isImageFile(file)) {
|
|
157
|
+
DEBUG &&
|
|
158
|
+
console.log(
|
|
159
|
+
`[useDragAndDrop] Rejected non-image file: ${file.name}`,
|
|
160
|
+
);
|
|
161
|
+
reject(
|
|
162
|
+
new Error(
|
|
163
|
+
`File ${file.name} is not an image file`,
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// For SVG files, read as text if they're text-based
|
|
170
|
+
if (
|
|
171
|
+
file.type === 'image/svg+xml' ||
|
|
172
|
+
file.name.toLowerCase().endsWith('.svg')
|
|
173
|
+
) {
|
|
174
|
+
const textReader = new FileReader();
|
|
175
|
+
textReader.onload = (e) => {
|
|
176
|
+
if (e.target?.result) {
|
|
177
|
+
DEBUG &&
|
|
178
|
+
console.log(
|
|
179
|
+
`[useDragAndDrop] Successfully read SVG file as text: ${file.name}`,
|
|
180
|
+
);
|
|
181
|
+
resolve(e.target.result as string);
|
|
182
|
+
} else {
|
|
183
|
+
reject(
|
|
184
|
+
new Error(
|
|
185
|
+
`Failed to read SVG file ${file.name}`,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
textReader.onerror = () =>
|
|
191
|
+
reject(
|
|
192
|
+
new Error(
|
|
193
|
+
`Error reading SVG file ${file.name}`,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
textReader.readAsText(file);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const reader = new FileReader();
|
|
201
|
+
reader.onload = (e) => {
|
|
202
|
+
if (e.target?.result) {
|
|
203
|
+
DEBUG &&
|
|
204
|
+
console.log(
|
|
205
|
+
`[useDragAndDrop] Successfully read image file: ${file.name}`,
|
|
206
|
+
);
|
|
207
|
+
// Return just the base64 part without the data URL prefix
|
|
208
|
+
const base64 = (e.target.result as string).split(
|
|
209
|
+
',',
|
|
210
|
+
)[1];
|
|
211
|
+
resolve(base64);
|
|
212
|
+
} else {
|
|
213
|
+
DEBUG &&
|
|
214
|
+
console.log(
|
|
215
|
+
`[useDragAndDrop] Failed to read image file: ${file.name}`,
|
|
216
|
+
);
|
|
217
|
+
reject(
|
|
218
|
+
new Error(
|
|
219
|
+
`Failed to read image file ${file.name}`,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
reader.onerror = () => {
|
|
225
|
+
DEBUG &&
|
|
226
|
+
console.log(
|
|
227
|
+
`[useDragAndDrop] Error reading image file: ${file.name}`,
|
|
228
|
+
);
|
|
229
|
+
reject(
|
|
230
|
+
new Error(
|
|
231
|
+
`Error reading image file ${file.name}`,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
reader.readAsDataURL(file);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
130
239
|
const processEntry = async (
|
|
131
240
|
entry: FileSystemEntry,
|
|
132
241
|
path: string,
|
|
@@ -144,12 +253,21 @@ export const useDragAndDrop = () => {
|
|
|
144
253
|
);
|
|
145
254
|
entry.file(async (file) => {
|
|
146
255
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
256
|
+
if (isImageFile(file)) {
|
|
257
|
+
const content = await readImageAsBase64(file);
|
|
258
|
+
DEBUG &&
|
|
259
|
+
console.log(
|
|
260
|
+
`[useDragAndDrop] Creating image file: ${path}${file.name}`,
|
|
261
|
+
);
|
|
262
|
+
createFile(`${path}${file.name}`, content);
|
|
263
|
+
} else {
|
|
264
|
+
const content = await readFileAsText(file);
|
|
265
|
+
DEBUG &&
|
|
266
|
+
console.log(
|
|
267
|
+
`[useDragAndDrop] Creating text file: ${path}${file.name}`,
|
|
268
|
+
);
|
|
269
|
+
createFile(`${path}${file.name}`, content);
|
|
270
|
+
}
|
|
153
271
|
} catch (error) {
|
|
154
272
|
console.error(
|
|
155
273
|
`Error processing file ${file.name}:`,
|
|
@@ -237,8 +355,15 @@ export const useDragAndDrop = () => {
|
|
|
237
355
|
`[useDragAndDrop] Processing dropped file: ${file.name}`,
|
|
238
356
|
);
|
|
239
357
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
358
|
+
if (isImageFile(file)) {
|
|
359
|
+
const content =
|
|
360
|
+
await readImageAsBase64(file);
|
|
361
|
+
createFile(file.name, content);
|
|
362
|
+
} else {
|
|
363
|
+
const content =
|
|
364
|
+
await readFileAsText(file);
|
|
365
|
+
createFile(file.name, content);
|
|
366
|
+
}
|
|
242
367
|
} catch (error) {
|
|
243
368
|
console.error(
|
|
244
369
|
`Error processing file ${file.name}:`,
|