Skip to content

Commit 2ecf2b5

Browse files
authored
Merge pull request #10 from deapi-ai/feat/file-upload-improvements
Feat/file upload improvements
2 parents c536fbc + 193b221 commit 2ecf2b5

2 files changed

Lines changed: 127 additions & 53 deletions

File tree

src/components/EndpointForm.tsx

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -181,34 +181,50 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }:
181181
}, [models]);
182182

183183
const handleFileChange = useCallback(async (name: string, file: File | File[] | null) => {
184-
setImagePreviews((prev) => {
185-
const oldPreviews = prev[name];
186-
if (oldPreviews) {
187-
oldPreviews.forEach((p) => URL.revokeObjectURL(p.url));
188-
}
189-
const newPreviews = { ...prev };
190-
delete newPreviews[name];
191-
return newPreviews;
192-
});
193-
194184
if (file) {
195-
setFiles((prev) => ({ ...prev, [name]: file }));
185+
const incomingFiles = Array.isArray(file) ? file : [file];
186+
const isMulti = Array.isArray(file);
187+
188+
// In multi mode, append to existing files
189+
const mergedFiles = isMulti
190+
? [...(Array.isArray(files[name]) ? files[name] as File[] : files[name] ? [files[name] as File] : []), ...incomingFiles]
191+
: incomingFiles;
196192

197-
const fileArray = Array.isArray(file) ? file : [file];
198-
const imageFiles = fileArray.filter((f) => f.type.startsWith('image/'));
193+
setFiles((prev) => ({ ...prev, [name]: isMulti ? mergedFiles : mergedFiles[0] }));
199194

195+
const imageFiles = incomingFiles.filter((f) => f.type.startsWith('image/'));
200196
if (imageFiles.length > 0) {
201-
const previews = await Promise.all(imageFiles.map(generateImagePreview));
202-
setImagePreviews((prev) => ({ ...prev, [name]: previews }));
197+
const newPreviews = await Promise.all(imageFiles.map(generateImagePreview));
198+
setImagePreviews((prev) => ({
199+
...prev,
200+
[name]: isMulti ? [...(prev[name] || []), ...newPreviews] : newPreviews,
201+
}));
202+
} else if (!isMulti) {
203+
// Single mode non-image: clear old previews
204+
setImagePreviews((prev) => {
205+
const old = prev[name];
206+
if (old) old.forEach((p) => URL.revokeObjectURL(p.url));
207+
const next = { ...prev };
208+
delete next[name];
209+
return next;
210+
});
203211
}
204212
} else {
213+
// Clear all
214+
setImagePreviews((prev) => {
215+
const old = prev[name];
216+
if (old) old.forEach((p) => URL.revokeObjectURL(p.url));
217+
const next = { ...prev };
218+
delete next[name];
219+
return next;
220+
});
205221
setFiles((prev) => {
206222
const newFiles = { ...prev };
207223
delete newFiles[name];
208224
return newFiles;
209225
});
210226
}
211-
}, []);
227+
}, [files]);
212228

213229
const toggleNullable = useCallback((name: string, disabled: boolean) => {
214230
setNullableDisabled((prev) => ({ ...prev, [name]: disabled }));

src/components/form/FileUploadField.tsx

Lines changed: 95 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useState, useRef, DragEvent } from 'react';
44
import { Upload, X, FolderOpen } from 'lucide-react';
55
import { formatFileSize } from '@/lib/format-utils';
66
import { OutputPicker } from './OutputPicker';
@@ -41,8 +41,49 @@ export function FileUploadField({
4141
onModeChange,
4242
}: FileUploadFieldProps) {
4343
const [pickerOpen, setPickerOpen] = useState(false);
44+
const [isDragging, setIsDragging] = useState(false);
45+
const dragCounter = useRef(0);
4446
const isImageAccept = accept?.includes('image/') ?? false;
4547

48+
const handleDragEnter = (e: DragEvent) => {
49+
e.preventDefault();
50+
e.stopPropagation();
51+
dragCounter.current++;
52+
if (e.dataTransfer.types.includes('Files')) {
53+
setIsDragging(true);
54+
}
55+
};
56+
57+
const handleDragLeave = (e: DragEvent) => {
58+
e.preventDefault();
59+
e.stopPropagation();
60+
dragCounter.current--;
61+
if (dragCounter.current === 0) {
62+
setIsDragging(false);
63+
}
64+
};
65+
66+
const handleDragOver = (e: DragEvent) => {
67+
e.preventDefault();
68+
e.stopPropagation();
69+
};
70+
71+
const handleDrop = (e: DragEvent) => {
72+
e.preventDefault();
73+
e.stopPropagation();
74+
setIsDragging(false);
75+
dragCounter.current = 0;
76+
77+
const droppedFiles = e.dataTransfer.files;
78+
if (!droppedFiles || droppedFiles.length === 0) return;
79+
80+
if (isMultiMode) {
81+
onFileChange(name, Array.from(droppedFiles));
82+
} else {
83+
onFileChange(name, droppedFiles[0]);
84+
}
85+
};
86+
4687
const fileArray = files ? (Array.isArray(files) ? files : [files]) : [];
4788
const fileCount = fileArray.length;
4889
const fileLabel =
@@ -118,43 +159,60 @@ export function FileUploadField({
118159
</div>
119160
)}
120161

121-
<div className="flex items-center gap-1">
122-
<label className="flex items-center gap-2 px-2 py-1.5 bg-[var(--surface-2)] border border-[var(--border)] rounded cursor-pointer hover:border-[var(--border-strong)] transition-colors flex-1 min-w-0">
123-
<Upload className="w-3 h-3 text-[var(--muted)] flex-shrink-0" />
124-
<span className="text-xs text-[var(--text-secondary)] truncate flex-1">{fileLabel}</span>
125-
{isMultiMode && fileCount > 0 && (
126-
<span className="text-[10px] px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded">
127-
{fileCount}
128-
</span>
129-
)}
130-
<input
131-
type="file"
132-
accept={accept}
133-
multiple={isMultiMode}
134-
onChange={(e) => {
135-
const selectedFiles = e.target.files;
136-
if (!selectedFiles || selectedFiles.length === 0) {
137-
onFileChange(name, null);
138-
} else if (isMultiMode) {
139-
onFileChange(name, Array.from(selectedFiles));
140-
} else {
141-
onFileChange(name, selectedFiles[0]);
142-
}
143-
}}
144-
className="hidden"
145-
/>
146-
</label>
147-
148-
{isImageAccept && (
149-
<button
150-
type="button"
151-
onClick={() => setPickerOpen(true)}
152-
className="flex items-center justify-center w-8 h-8 bg-[var(--surface-2)] border border-[var(--border)] rounded hover:border-[var(--border-strong)] hover:bg-[var(--hover)] transition-colors flex-shrink-0"
153-
title="Pick from output"
154-
>
155-
<FolderOpen className="w-3.5 h-3.5 text-[var(--muted)]" />
156-
</button>
162+
<div
163+
onDragEnter={handleDragEnter}
164+
onDragLeave={handleDragLeave}
165+
onDragOver={handleDragOver}
166+
onDrop={handleDrop}
167+
className={`relative rounded border-2 border-dashed transition-colors ${
168+
isDragging
169+
? 'border-blue-500 bg-blue-500/10'
170+
: 'border-transparent'
171+
}`}
172+
>
173+
{isDragging && (
174+
<div className="absolute inset-0 flex items-center justify-center z-10 rounded pointer-events-none">
175+
<span className="text-xs font-medium text-blue-400">Drop {isMultiMode ? 'file(s)' : 'file'} here</span>
176+
</div>
157177
)}
178+
<div className={`flex items-center gap-1 ${isDragging ? 'opacity-30' : ''}`}>
179+
<label className="flex items-center gap-2 px-2 py-1.5 bg-[var(--surface-2)] border border-[var(--border)] rounded cursor-pointer hover:border-[var(--border-strong)] transition-colors flex-1 min-w-0">
180+
<Upload className="w-3 h-3 text-[var(--muted)] flex-shrink-0" />
181+
<span className="text-xs text-[var(--text-secondary)] truncate flex-1">{fileLabel}</span>
182+
{isMultiMode && fileCount > 0 && (
183+
<span className="text-[10px] px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded">
184+
{fileCount}
185+
</span>
186+
)}
187+
<input
188+
type="file"
189+
accept={accept}
190+
multiple={isMultiMode}
191+
onChange={(e) => {
192+
const selectedFiles = e.target.files;
193+
if (!selectedFiles || selectedFiles.length === 0) {
194+
onFileChange(name, null);
195+
} else if (isMultiMode) {
196+
onFileChange(name, Array.from(selectedFiles));
197+
} else {
198+
onFileChange(name, selectedFiles[0]);
199+
}
200+
}}
201+
className="hidden"
202+
/>
203+
</label>
204+
205+
{isImageAccept && (
206+
<button
207+
type="button"
208+
onClick={() => setPickerOpen(true)}
209+
className="flex items-center justify-center w-8 h-8 bg-[var(--surface-2)] border border-[var(--border)] rounded hover:border-[var(--border-strong)] hover:bg-[var(--hover)] transition-colors flex-shrink-0"
210+
title="Pick from output"
211+
>
212+
<FolderOpen className="w-3.5 h-3.5 text-[var(--muted)]" />
213+
</button>
214+
)}
215+
</div>
158216
</div>
159217

160218
{isImageAccept && (

0 commit comments

Comments
 (0)