vzcode 1.43.0 → 1.45.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-C_tGhWXG.js +682 -0
- package/dist/assets/index-C1lTrzOZ.js +457 -0
- package/dist/assets/{index-CP9o_hN5.css → index-DeeOcIzw.css} +1 -1
- package/dist/assets/worker-CekYspLa.js +453 -0
- package/dist/index.html +2 -2
- package/package.json +20 -18
- package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
- package/src/client/CodeEditor/index.tsx +4 -0
- package/src/client/Icons/SparklesSVG.tsx +9 -3
- package/src/client/ImageViewer/index.tsx +91 -0
- package/src/client/ImageViewer/style.scss +41 -0
- package/src/client/VZCodeContext.tsx +34 -6
- package/src/client/VZMiddle.tsx +22 -7
- package/src/client/VZRight.tsx +7 -2
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +8 -8
- package/src/client/VZSidebar/AIChat/DiffView.scss +125 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +11 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +1 -0
- package/src/client/VZSidebar/AIChat/index.tsx +9 -7
- package/src/client/VZSidebar/aiCopyPaste.ts +64 -1
- 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/featureFlags.ts +2 -0
- package/src/client/useRuntimeError.ts +32 -0
- package/src/client/utils/isImageFile.ts +12 -0
- package/src/server/aiChatHandler/aiEditing.ts +19 -0
- package/src/server/aiChatHandler/chatOperations.ts +40 -0
- package/src/server/aiChatHandler/index.ts +13 -0
- package/src/server/computeInitialDocument.ts +25 -6
- package/src/server/index.ts +31 -2
- package/src/utils/fileDiff.ts +114 -0
- package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
- package/dist/assets/buildWorker-B-vXZ9gA.js +0 -579
- package/dist/assets/index-B83lKfVG.js +0 -329
- package/dist/assets/worker-DkYJN5WQ.js +0 -453
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
.diff-view {
|
|
2
|
+
margin: 12px 0;
|
|
3
|
+
|
|
4
|
+
.diff-summary {
|
|
5
|
+
margin-bottom: 8px;
|
|
6
|
+
|
|
7
|
+
.diff-stats {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 8px;
|
|
11
|
+
font-size: 12px;
|
|
12
|
+
color: var(--bs-secondary-color);
|
|
13
|
+
|
|
14
|
+
.files-changed {
|
|
15
|
+
color: white;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.additions {
|
|
20
|
+
color: #28a745;
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.deletions {
|
|
25
|
+
color: #dc3545;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.diff-files {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 4px;
|
|
35
|
+
|
|
36
|
+
// Fix diff2html styling issues for dark theme and layout
|
|
37
|
+
.d2h-wrapper {
|
|
38
|
+
// Override diff2html variables for dark theme
|
|
39
|
+
--d2h-bg-color: rgb(41, 44, 52);
|
|
40
|
+
--d2h-color: #e6edf3;
|
|
41
|
+
--d2h-border-color: #30363d;
|
|
42
|
+
--d2h-dim-color: #6e7681;
|
|
43
|
+
--d2h-info-bg-color: #161b22;
|
|
44
|
+
--d2h-file-header-bg-color: #161b22;
|
|
45
|
+
--d2h-file-header-border-color: #30363d;
|
|
46
|
+
--d2h-line-border-color: #21262d;
|
|
47
|
+
--d2h-ins-bg-color: rgba(46, 160, 67, 0.15);
|
|
48
|
+
--d2h-ins-border-color: rgba(46, 160, 67, 0.4);
|
|
49
|
+
--d2h-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
|
|
50
|
+
--d2h-ins-label-color: #3fb950;
|
|
51
|
+
--d2h-del-bg-color: rgba(248, 81, 73, 0.1);
|
|
52
|
+
--d2h-del-border-color: rgba(248, 81, 73, 0.4);
|
|
53
|
+
--d2h-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
|
|
54
|
+
--d2h-del-label-color: #f85149;
|
|
55
|
+
|
|
56
|
+
background-color: var(--d2h-bg-color);
|
|
57
|
+
color: var(--d2h-color);
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fix layout issues with line numbers
|
|
63
|
+
.d2h-diff-table {
|
|
64
|
+
position: relative;
|
|
65
|
+
background-color: var(--d2h-bg-color);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.d2h-code-linenumber {
|
|
69
|
+
position: relative !important;
|
|
70
|
+
float: none !important;
|
|
71
|
+
display: table-cell !important;
|
|
72
|
+
width: auto !important;
|
|
73
|
+
background-color: var(--d2h-bg-color) !important;
|
|
74
|
+
border-color: var(--d2h-line-border-color) !important;
|
|
75
|
+
color: var(--d2h-dim-color) !important;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.d2h-code-side-linenumber {
|
|
79
|
+
position: relative !important;
|
|
80
|
+
float: none !important;
|
|
81
|
+
display: table-cell !important;
|
|
82
|
+
width: auto !important;
|
|
83
|
+
background-color: var(--d2h-bg-color) !important;
|
|
84
|
+
border-color: var(--d2h-line-border-color) !important;
|
|
85
|
+
color: var(--d2h-dim-color) !important;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Ensure proper table layout
|
|
89
|
+
.d2h-diff-tbody tr {
|
|
90
|
+
display: table-row;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.d2h-diff-tbody td {
|
|
94
|
+
display: table-cell;
|
|
95
|
+
vertical-align: top;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fix file header styling
|
|
99
|
+
.d2h-file-header {
|
|
100
|
+
background-color: var(
|
|
101
|
+
--d2h-file-header-bg-color
|
|
102
|
+
) !important;
|
|
103
|
+
border-bottom: 1px solid
|
|
104
|
+
var(--d2h-file-header-border-color) !important;
|
|
105
|
+
color: var(--d2h-color);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Ensure proper line styling
|
|
109
|
+
.d2h-del {
|
|
110
|
+
background-color: var(--d2h-del-bg-color) !important;
|
|
111
|
+
border-color: var(--d2h-del-border-color) !important;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.d2h-ins {
|
|
115
|
+
background-color: var(--d2h-ins-bg-color) !important;
|
|
116
|
+
border-color: var(--d2h-ins-border-color) !important;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fix wrapper border
|
|
120
|
+
.d2h-file-wrapper {
|
|
121
|
+
border: 1px solid var(--d2h-border-color) !important;
|
|
122
|
+
background-color: var(--d2h-bg-color);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
UnifiedFilesDiff,
|
|
4
|
+
parseUnifiedDiffStats,
|
|
5
|
+
combineUnifiedDiffs,
|
|
6
|
+
} from '../../../utils/fileDiff';
|
|
7
|
+
import * as Diff2Html from 'diff2html';
|
|
8
|
+
import 'diff2html/bundles/css/diff2html.min.css';
|
|
9
|
+
import './DiffView.scss';
|
|
10
|
+
|
|
11
|
+
interface DiffViewProps {
|
|
12
|
+
diffData: UnifiedFilesDiff;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DiffView: React.FC<DiffViewProps> = ({
|
|
16
|
+
diffData,
|
|
17
|
+
}) => {
|
|
18
|
+
const unifiedDiffs = Object.values(diffData).filter(
|
|
19
|
+
(diff) => diff.length > 0,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (unifiedDiffs.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Calculate statistics from all unified diffs
|
|
27
|
+
let totalAdditions = 0;
|
|
28
|
+
let totalDeletions = 0;
|
|
29
|
+
|
|
30
|
+
for (const unifiedDiff of unifiedDiffs) {
|
|
31
|
+
const stats = parseUnifiedDiffStats(unifiedDiff);
|
|
32
|
+
totalAdditions += stats.additions;
|
|
33
|
+
totalDeletions += stats.deletions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Combine all unified diffs and convert to HTML using diff2html
|
|
37
|
+
const combinedUnifiedDiff = combineUnifiedDiffs(diffData);
|
|
38
|
+
const diffHtml = Diff2Html.html(combinedUnifiedDiff, {
|
|
39
|
+
drawFileList: false,
|
|
40
|
+
matching: 'words',
|
|
41
|
+
diffStyle: 'word',
|
|
42
|
+
outputFormat: 'line-by-line',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="diff-view">
|
|
47
|
+
<div className="diff-summary">
|
|
48
|
+
<div className="diff-stats">
|
|
49
|
+
<span className="files-changed">
|
|
50
|
+
{unifiedDiffs.length} file
|
|
51
|
+
{unifiedDiffs.length !== 1 ? 's' : ''} changed
|
|
52
|
+
</span>
|
|
53
|
+
{totalAdditions > 0 && (
|
|
54
|
+
<span className="additions">
|
|
55
|
+
+{totalAdditions}
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
58
|
+
{totalDeletions > 0 && (
|
|
59
|
+
<span className="deletions">
|
|
60
|
+
-{totalDeletions}
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div
|
|
67
|
+
className="diff-files"
|
|
68
|
+
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -2,6 +2,9 @@ import { timestampToDate } from '@vizhub/viz-utils';
|
|
|
2
2
|
import Markdown from 'react-markdown';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import { useMemo, memo } from 'react';
|
|
5
|
+
import { DiffView } from './DiffView';
|
|
6
|
+
import { UnifiedFilesDiff } from '../../../utils/fileDiff';
|
|
7
|
+
import { enableDiffView } from '../../featureFlags';
|
|
5
8
|
|
|
6
9
|
interface MessageProps {
|
|
7
10
|
id: string;
|
|
@@ -9,6 +12,7 @@ interface MessageProps {
|
|
|
9
12
|
content: string;
|
|
10
13
|
timestamp: number;
|
|
11
14
|
isStreaming?: boolean;
|
|
15
|
+
diffData?: UnifiedFilesDiff;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
const MessageComponent = ({
|
|
@@ -16,6 +20,7 @@ const MessageComponent = ({
|
|
|
16
20
|
content,
|
|
17
21
|
timestamp,
|
|
18
22
|
isStreaming,
|
|
23
|
+
diffData,
|
|
19
24
|
}: MessageProps) => {
|
|
20
25
|
// Memoize date formatting to avoid repeated computation
|
|
21
26
|
const formattedTime = useMemo(() => {
|
|
@@ -32,12 +37,18 @@ const MessageComponent = ({
|
|
|
32
37
|
const messageClassName = useMemo(() => {
|
|
33
38
|
return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
|
|
34
39
|
}, [role, isStreaming]);
|
|
40
|
+
|
|
35
41
|
return (
|
|
36
42
|
<div className={messageClassName}>
|
|
37
43
|
<div className="ai-chat-message-content">
|
|
38
44
|
<Markdown remarkPlugins={[remarkGfm]}>
|
|
39
45
|
{content}
|
|
40
46
|
</Markdown>
|
|
47
|
+
{enableDiffView &&
|
|
48
|
+
diffData &&
|
|
49
|
+
Object.keys(diffData).length > 0 && (
|
|
50
|
+
<DiffView diffData={diffData} />
|
|
51
|
+
)}
|
|
41
52
|
</div>
|
|
42
53
|
<div className="ai-chat-message-time">
|
|
43
54
|
{formattedTime}
|
|
@@ -13,7 +13,9 @@ import './styles.scss';
|
|
|
13
13
|
const defaultAIChatEndpoint = '/ai-chat-message';
|
|
14
14
|
|
|
15
15
|
export const AIChat = () => {
|
|
16
|
-
const
|
|
16
|
+
const { aiChatMessage, setAIChatMessage } =
|
|
17
|
+
useContext(VZCodeContext);
|
|
18
|
+
|
|
17
19
|
const [isLoading, setIsLoading] = useState(false);
|
|
18
20
|
const [currentChatId] = useState(() => uuidv4());
|
|
19
21
|
|
|
@@ -40,9 +42,9 @@ export const AIChat = () => {
|
|
|
40
42
|
);
|
|
41
43
|
|
|
42
44
|
const handleSendMessage = useCallback(async () => {
|
|
43
|
-
if (!
|
|
45
|
+
if (!aiChatMessage.trim() || isLoading) return;
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
setAIChatMessage('');
|
|
46
48
|
setIsLoading(true);
|
|
47
49
|
|
|
48
50
|
// Call backend endpoint for AI response
|
|
@@ -55,7 +57,7 @@ export const AIChat = () => {
|
|
|
55
57
|
},
|
|
56
58
|
body: JSON.stringify({
|
|
57
59
|
...aiChatOptions,
|
|
58
|
-
content:
|
|
60
|
+
content: aiChatMessage.trim(),
|
|
59
61
|
chatId: currentChatId,
|
|
60
62
|
}),
|
|
61
63
|
});
|
|
@@ -75,7 +77,7 @@ export const AIChat = () => {
|
|
|
75
77
|
setIsLoading(false);
|
|
76
78
|
}
|
|
77
79
|
}, [
|
|
78
|
-
|
|
80
|
+
aiChatMessage,
|
|
79
81
|
isLoading,
|
|
80
82
|
aiChatEndpoint,
|
|
81
83
|
aiChatOptions,
|
|
@@ -90,8 +92,8 @@ export const AIChat = () => {
|
|
|
90
92
|
isLoading={isLoading}
|
|
91
93
|
/>
|
|
92
94
|
<ChatInput
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
aiChatMessage={aiChatMessage}
|
|
96
|
+
setAIChatMessage={setAIChatMessage}
|
|
95
97
|
onSendMessage={handleSendMessage}
|
|
96
98
|
isLoading={isLoading}
|
|
97
99
|
focused={aiChatFocused}
|
|
@@ -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 () => {
|
|
@@ -79,8 +81,16 @@ export const createAICopyPasteHandlers = (
|
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
// Preprocess to remove formatting instructions section to avoid creating extra files
|
|
85
|
+
const preprocessed = normalized
|
|
86
|
+
.split('## Formatting Instructions')[0]
|
|
87
|
+
.trim();
|
|
88
|
+
|
|
82
89
|
// Parse the markdown files format
|
|
83
|
-
const parsed = parseMarkdownFiles(
|
|
90
|
+
const parsed = parseMarkdownFiles(
|
|
91
|
+
preprocessed,
|
|
92
|
+
'bold',
|
|
93
|
+
);
|
|
84
94
|
|
|
85
95
|
if (
|
|
86
96
|
parsed.files &&
|
|
@@ -124,8 +134,61 @@ export const createAICopyPasteHandlers = (
|
|
|
124
134
|
}
|
|
125
135
|
};
|
|
126
136
|
|
|
137
|
+
// Export to ZIP - creates and downloads a ZIP file with all viz files
|
|
138
|
+
const handleExportToZip = async () => {
|
|
139
|
+
if (!files || Object.keys(files).length === 0) {
|
|
140
|
+
setExportButtonText('No files to export');
|
|
141
|
+
setTimeout(
|
|
142
|
+
() => setExportButtonText('Export to ZIP'),
|
|
143
|
+
2000,
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Create the ZIP
|
|
150
|
+
const zip = new JSZip();
|
|
151
|
+
|
|
152
|
+
// Add each file to the ZIP
|
|
153
|
+
Object.entries(files).forEach(([fileId, file]) => {
|
|
154
|
+
const fileName = file.name || fileId;
|
|
155
|
+
const fileContent = file.text || '';
|
|
156
|
+
zip.file(fileName, fileContent);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Generate and download
|
|
160
|
+
const blob = await zip.generateAsync({
|
|
161
|
+
type: 'blob',
|
|
162
|
+
});
|
|
163
|
+
const url = URL.createObjectURL(blob);
|
|
164
|
+
const a = document.createElement('a');
|
|
165
|
+
a.href = url;
|
|
166
|
+
a.download = 'viz-files.zip';
|
|
167
|
+
a.click();
|
|
168
|
+
URL.revokeObjectURL(url);
|
|
169
|
+
|
|
170
|
+
// Show success feedback
|
|
171
|
+
setExportButtonText('Exported!');
|
|
172
|
+
setTimeout(
|
|
173
|
+
() => setExportButtonText('Export to ZIP'),
|
|
174
|
+
2000,
|
|
175
|
+
);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(
|
|
178
|
+
'Failed to export files to ZIP:',
|
|
179
|
+
error,
|
|
180
|
+
);
|
|
181
|
+
setExportButtonText('Error');
|
|
182
|
+
setTimeout(
|
|
183
|
+
() => setExportButtonText('Export to ZIP'),
|
|
184
|
+
2000,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
127
189
|
return {
|
|
128
190
|
handleCopyForAI,
|
|
129
191
|
handlePasteForAI,
|
|
192
|
+
handleExportToZip,
|
|
130
193
|
};
|
|
131
194
|
};
|
|
@@ -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}:`,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
// Custom hook to manage runtime error state
|
|
4
|
+
export const useRuntimeError = () => {
|
|
5
|
+
// State to hold the runtime error message
|
|
6
|
+
// `null` means no errors
|
|
7
|
+
// If this is a string, it's the
|
|
8
|
+
// error message from runtime execution.
|
|
9
|
+
const [runtimeError, setRuntimeError] = useState<
|
|
10
|
+
string | null
|
|
11
|
+
>(null);
|
|
12
|
+
|
|
13
|
+
// Callback function to handle runtime errors
|
|
14
|
+
// This will be passed to the createRuntime function
|
|
15
|
+
const handleRuntimeError = useCallback(
|
|
16
|
+
(formattedErrorMessage: string) => {
|
|
17
|
+
setRuntimeError(formattedErrorMessage);
|
|
18
|
+
},
|
|
19
|
+
[],
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Function to clear runtime errors
|
|
23
|
+
const clearRuntimeError = useCallback(() => {
|
|
24
|
+
setRuntimeError(null);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
runtimeError,
|
|
29
|
+
handleRuntimeError,
|
|
30
|
+
clearRuntimeError,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function to check if a file is an image based on its name.
|
|
3
|
+
* @param fileName - The name of the file (e.g., "image.png", "photo.jpg")
|
|
4
|
+
* @returns true if the file is an image, false otherwise
|
|
5
|
+
*/
|
|
6
|
+
export const isImageFile = (fileName: string): boolean => {
|
|
7
|
+
return (
|
|
8
|
+
fileName.match(
|
|
9
|
+
/\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
|
|
10
|
+
) !== null
|
|
11
|
+
);
|
|
12
|
+
};
|