feat: Multiple reference uploads per category
- Enable multi-file selection for Subject, Scene, Style inputs - Show stacked thumbnails with count badge - Add reference preview panel with individual delete buttons - Add '+' button to add more refs per category
This commit is contained in:
parent
2a8125f6d7
commit
32359cc2d1
1 changed files with 109 additions and 47 deletions
|
|
@ -11,7 +11,7 @@ export function PromptHero() {
|
||||||
const {
|
const {
|
||||||
prompt, setPrompt, addToGallery,
|
prompt, setPrompt, addToGallery,
|
||||||
settings, setSettings,
|
settings, setSettings,
|
||||||
references, setReference, addReference, clearReferences,
|
references, setReference, addReference, removeReference, clearReferences,
|
||||||
setSelectionMode, setCurrentView,
|
setSelectionMode, setCurrentView,
|
||||||
history, setHistory
|
history, setHistory
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
@ -280,11 +280,16 @@ export function PromptHero() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle file input change for click-to-upload
|
// Handle file input change for click-to-upload (supports multiple files)
|
||||||
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>, category: ReferenceCategory) => {
|
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>, category: ReferenceCategory) => {
|
||||||
const file = e.target.files?.[0];
|
const files = e.target.files;
|
||||||
if (file && file.type.startsWith('image/')) {
|
if (files && files.length > 0) {
|
||||||
uploadReference(file, category);
|
// Upload each selected file
|
||||||
|
Array.from(files).forEach(file => {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
uploadReference(file, category);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Reset input value so same file can be selected again
|
// Reset input value so same file can be selected again
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
|
|
@ -299,14 +304,9 @@ export function PromptHero() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleReference = (category: ReferenceCategory) => {
|
const toggleReference = (category: ReferenceCategory) => {
|
||||||
const hasRefs = references[category] && references[category]!.length > 0;
|
// Always open file picker to add more references
|
||||||
if (hasRefs) {
|
// Users can remove individual refs or clear all via the X button
|
||||||
// If already has references, clear them
|
openFilePicker(category);
|
||||||
clearReferences(category);
|
|
||||||
} else {
|
|
||||||
// If no references, open file picker for upload
|
|
||||||
openFilePicker(category);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAspectRatio = () => {
|
const nextAspectRatio = () => {
|
||||||
|
|
@ -473,46 +473,59 @@ export function PromptHero() {
|
||||||
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
|
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
|
||||||
const refs = references[cat] || [];
|
const refs = references[cat] || [];
|
||||||
const hasRefs = refs.length > 0;
|
const hasRefs = refs.length > 0;
|
||||||
const firstRef = refs[0];
|
|
||||||
const isUploading = uploadingRefs[cat];
|
const isUploading = uploadingRefs[cat];
|
||||||
return (
|
return (
|
||||||
<button
|
<div key={cat} className="relative group">
|
||||||
key={cat}
|
<button
|
||||||
onClick={() => toggleReference(cat)}
|
onClick={() => toggleReference(cat)}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleDrop(e, cat)}
|
onDrop={(e) => handleDrop(e, cat)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex items-center gap-2 rounded-full px-4 py-2 text-xs font-medium transition-all border relative overflow-hidden",
|
"flex items-center gap-2 rounded-full px-4 py-2 text-xs font-medium transition-all border relative overflow-hidden",
|
||||||
hasRefs
|
hasRefs
|
||||||
? "bg-purple-500/10 text-purple-200 border-purple-500/30 hover:bg-purple-500/20"
|
? "bg-purple-500/10 text-purple-200 border-purple-500/30 hover:bg-purple-500/20"
|
||||||
: "bg-white/5 text-white/40 border-white/5 hover:bg-white/10 hover:text-white/70 hover:border-white/10",
|
: "bg-white/5 text-white/40 border-white/5 hover:bg-white/10 hover:text-white/70 hover:border-white/10",
|
||||||
isUploading && "animate-pulse cursor-wait"
|
isUploading && "animate-pulse cursor-wait"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
) : firstRef?.thumbnail ? (
|
) : hasRefs ? (
|
||||||
<img
|
<div className="flex -space-x-2">
|
||||||
src={firstRef.thumbnail}
|
{refs.slice(0, 4).map((ref, idx) => (
|
||||||
alt={cat}
|
<img
|
||||||
className="h-5 w-5 rounded-sm object-cover ring-1 ring-white/20"
|
key={ref.id}
|
||||||
/>
|
src={ref.thumbnail}
|
||||||
) : (
|
alt=""
|
||||||
<Upload className="h-4 w-4" />
|
className="h-5 w-5 rounded-sm object-cover ring-1 ring-white/20"
|
||||||
)}
|
style={{ zIndex: 10 - idx }}
|
||||||
<span className="capitalize tracking-wide">{cat}</span>
|
/>
|
||||||
{refs.length > 1 && (
|
))}
|
||||||
<span className="text-[10px] bg-purple-500/30 text-purple-100 rounded-full px-1.5 h-4 flex items-center">{refs.length}</span>
|
{refs.length > 4 && (
|
||||||
)}
|
<div className="h-5 w-5 rounded-sm bg-purple-500/50 flex items-center justify-center text-[9px] font-bold ring-1 ring-white/20">
|
||||||
|
+{refs.length - 4}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span className="capitalize tracking-wide">{cat}</span>
|
||||||
|
{refs.length > 0 && (
|
||||||
|
<span className="text-[10px] bg-purple-500/30 text-purple-100 rounded-full px-1.5 h-4 flex items-center">{refs.length}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{/* Clear all button */}
|
||||||
{hasRefs && !isUploading && (
|
{hasRefs && !isUploading && (
|
||||||
<div
|
<button
|
||||||
className="ml-1.5 -mr-1 p-0.5 rounded-full hover:bg-black/20 text-current/70 hover:text-current"
|
className="absolute -top-1 -right-1 p-1 rounded-full bg-red-500/80 text-white opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500"
|
||||||
onClick={(e) => { e.stopPropagation(); clearReferences(cat); }}
|
onClick={(e) => { e.stopPropagation(); clearReferences(cat); }}
|
||||||
|
title={`Clear all ${cat} references`}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-2.5 w-2.5" />
|
||||||
</div>
|
</button>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -522,6 +535,7 @@ export function PromptHero() {
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRefs.subject}
|
ref={fileInputRefs.subject}
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleFileInputChange(e, 'subject')}
|
onChange={(e) => handleFileInputChange(e, 'subject')}
|
||||||
/>
|
/>
|
||||||
|
|
@ -529,6 +543,7 @@ export function PromptHero() {
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRefs.scene}
|
ref={fileInputRefs.scene}
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleFileInputChange(e, 'scene')}
|
onChange={(e) => handleFileInputChange(e, 'scene')}
|
||||||
/>
|
/>
|
||||||
|
|
@ -536,6 +551,7 @@ export function PromptHero() {
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRefs.style}
|
ref={fileInputRefs.style}
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleFileInputChange(e, 'style')}
|
onChange={(e) => handleFileInputChange(e, 'style')}
|
||||||
/>
|
/>
|
||||||
|
|
@ -604,6 +620,52 @@ export function PromptHero() {
|
||||||
</GradientButton>
|
</GradientButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Reference Preview Panel - shows when any references exist */}
|
||||||
|
{(references.subject?.length || references.scene?.length || references.style?.length) ? (
|
||||||
|
<div className="mt-4 p-3 rounded-xl bg-white/5 border border-white/10">
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
|
||||||
|
const refs = references[cat] || [];
|
||||||
|
if (refs.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div key={cat} className="flex-1 min-w-[120px]">
|
||||||
|
<div className="text-[10px] uppercase tracking-wider text-white/40 mb-2 flex items-center justify-between">
|
||||||
|
<span>{cat}</span>
|
||||||
|
<span className="text-purple-300">{refs.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{refs.map((ref) => (
|
||||||
|
<div key={ref.id} className="relative group/thumb">
|
||||||
|
<img
|
||||||
|
src={ref.thumbnail}
|
||||||
|
alt=""
|
||||||
|
className="h-10 w-10 rounded object-cover ring-1 ring-white/10 group-hover/thumb:ring-purple-500/50 transition-all"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => removeReference(cat, ref.id)}
|
||||||
|
className="absolute -top-1 -right-1 p-0.5 rounded-full bg-red-500 text-white opacity-0 group-hover/thumb:opacity-100 transition-opacity hover:bg-red-600"
|
||||||
|
title="Remove this reference"
|
||||||
|
>
|
||||||
|
<X className="h-2.5 w-2.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* Add more button */}
|
||||||
|
<button
|
||||||
|
onClick={() => openFilePicker(cat)}
|
||||||
|
className="h-10 w-10 rounded border border-dashed border-white/20 flex items-center justify-center text-white/30 hover:text-white/60 hover:border-white/40 transition-colors"
|
||||||
|
title={`Add more ${cat} references`}
|
||||||
|
>
|
||||||
|
<span className="text-lg">+</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue