|
import { LoaderCircle, ImageIcon, Send, RotateCw, Maximize, Wand2, Sun, Plus, Palette, Image as ImageLucide, Brush, SendToBack, Download, ArrowLeftRight } from 'lucide-react'; |
|
import { useEffect, useState, useCallback } from 'react'; |
|
import ImageRefiner from './ImageRefiner'; |
|
import HeaderButtons from './HeaderButtons'; |
|
|
|
|
|
const REFINEMENT_SUGGESTIONS = [ |
|
{ label: 'Rotate', prompt: 'Can you rotate this by ', icon: RotateCw }, |
|
{ label: 'Add light', prompt: 'Can you add a light from the ', icon: Sun }, |
|
{ label: 'Add object', prompt: 'Can you add a ', icon: Plus }, |
|
{ label: 'Background', prompt: 'Can you change the background to ', icon: ImageLucide }, |
|
{ label: 'Color', prompt: 'Can you make the color more ', icon: Palette }, |
|
{ label: 'Scale', prompt: 'Can you make this ', icon: Maximize }, |
|
{ label: 'Lighting', prompt: 'Can you make the lighting more ', icon: Sun }, |
|
{ label: 'Style', prompt: 'Can you make it look more ', icon: Wand2 }, |
|
{ label: 'Material', prompt: 'Can you change the material to ', icon: Wand2 }, |
|
|
|
]; |
|
|
|
|
|
const SURPRISE_REFINEMENTS = [ |
|
|
|
"Do something cool with this. I leave it up to you!", |
|
"Surprise me! Take this image somewhere unexpected.", |
|
"Transform this however you want. Be creative!", |
|
"Do something wild with this image. No limits!", |
|
"Make this image magical in your own way.", |
|
"Take creative freedom with this image. Surprise me!", |
|
"Show me what you can do with this. Go crazy!", |
|
"Transform this in a way I wouldn't expect.", |
|
"Have fun with this and do whatever inspires you.", |
|
"Go wild with this image! Show me something amazing.", |
|
"Put your own creative spin on this image.", |
|
"Reimagine this image however you want. Be bold!", |
|
"Do something unexpected with this. Totally up to you!", |
|
"Surprise me with your creativity. Anything goes!", |
|
"Make this extraordinary in whatever way you choose.", |
|
"Show off your creative abilities with this image!", |
|
"Take this in any direction that excites you!", |
|
"Transform this however your imagination guides you.", |
|
"Make this magical in your own unique way.", |
|
"Do something fun and unexpected with this!", |
|
"Surprise me! Show me your creativity.", |
|
"Make this more beautiful <3", |
|
"Put your artistic spin on this image!", |
|
"Let your imagination run wild with this!", |
|
"Take this image to a whole new level of awesome!", |
|
"Make this image extraordinary in your own way.", |
|
"Do something fantastic with this. Full creative freedom!", |
|
"Surprise me with a totally unexpected transformation!", |
|
"Go nuts with this! Show me something incredible!", |
|
"Add your own wild twist to this image!", |
|
"Make this image come alive however you want!", |
|
"Transform this in the most creative way possible!", |
|
"Go crazy with this. I want to be wowed :))", |
|
"Do whatever magical things you want with this image!", |
|
"Reinvent this image however inspires you!", |
|
|
|
|
|
"Can you add this to outer space with aliens having a BBQ?", |
|
"Can you add a giraffe wearing a tuxedo to this?", |
|
"Can you make tiny vikings invade this image?", |
|
"Can you turn this into an ice cream sundae being eaten by robots?", |
|
"Can you make this float in a sea of rainbow soup?", |
|
"Can you add dancing pickles to this image?", |
|
"Can you make this the centerpiece of an alien museum?", |
|
"Can you add this to a cereal bowl being eaten by a giant?", |
|
"Can you make this the star of a bizarre music video?", |
|
"Can you add tiny dinosaurs having a tea party?", |
|
"Can you turn this into something from a fever dream?", |
|
"Can you make this the main character in a surreal fairytale?", |
|
"Can you put this in the middle of a candy landscape?", |
|
"Can you add this to a world where physics works backwards?", |
|
"Can you make this the prize in a cosmic game show?", |
|
"Can you add tiny people worshipping this as a deity?", |
|
"Can you put this in the paws of a giant cosmic cat?", |
|
"Can you make this wearing sunglasses and surfing?", |
|
"Can you add this to a world made entirely of cheese?", |
|
"Can you make this the centerpiece of a goblin birthday party?", |
|
"Can you transform this into a cloud creature floating in the sky?", |
|
"Can you add this to a world where everything is made of pasta?", |
|
"Can you turn this into a piñata at a monster celebration?", |
|
"Can you add this to outer space?", |
|
"Can you add this to a landscape made of breakfast foods?", |
|
"Can you make this the conductor of an orchestra of unusual animals?", |
|
"Can you turn this into a strange plant growing in an alien garden?", |
|
"Can you add this to a world inside a snow globe?", |
|
"Can you make this the secret ingredient in a witch's cauldron?", |
|
"Can you turn this into a superhero with an unusual power?", |
|
"Can you make this swimming in a sea of jelly beans?", |
|
"Can you add this to a planet where everything is upside down?", |
|
"Can you make this the treasure in a dragon's unusual collection?", |
|
"Can you transform this into a character in a bizarre cartoon?", |
|
"Can you add this to a world where shadows come to life?" |
|
]; |
|
|
|
const DisplayCanvas = ({ |
|
displayCanvasRef, |
|
isLoading, |
|
handleSaveImage, |
|
handleRegenerate, |
|
hasGeneratedContent = false, |
|
currentDimension, |
|
onOpenHistory, |
|
onRefineImage, |
|
onSendToDoodle, |
|
hasHistory, |
|
openHistoryModal, |
|
toggleLibrary |
|
}) => { |
|
const [showPlaceholder, setShowPlaceholder] = useState(true); |
|
const [inputValue, setInputValue] = useState(''); |
|
const [showDoodleTooltip, setShowDoodleTooltip] = useState(false); |
|
const [showSaveTooltip, setShowSaveTooltip] = useState(false); |
|
const [isHoveringCanvas, setIsHoveringCanvas] = useState(false); |
|
|
|
|
|
useEffect(() => { |
|
if (hasGeneratedContent) { |
|
setShowPlaceholder(false); |
|
} else if (isLoading) { |
|
setShowPlaceholder(true); |
|
} |
|
}, [isLoading, hasGeneratedContent]); |
|
|
|
const handleSubmit = (e) => { |
|
e.preventDefault(); |
|
if (!inputValue.trim()) return; |
|
onRefineImage(inputValue); |
|
setInputValue(''); |
|
}; |
|
|
|
const handleSuggestionClick = (suggestion) => { |
|
setInputValue(suggestion.prompt); |
|
document.querySelector('input[name="refiner"]').focus(); |
|
}; |
|
|
|
const handleSurpriseMe = () => { |
|
const randomPrompt = SURPRISE_REFINEMENTS[Math.floor(Math.random() * SURPRISE_REFINEMENTS.length)]; |
|
setInputValue(randomPrompt); |
|
document.querySelector('input[name="refiner"]').focus(); |
|
}; |
|
|
|
const handleSendToDoodle = useCallback(() => { |
|
if (displayCanvasRef.current && onSendToDoodle) { |
|
const imageDataUrl = displayCanvasRef.current.toDataURL('image/png'); |
|
onSendToDoodle(imageDataUrl); |
|
} |
|
}, [displayCanvasRef, onSendToDoodle]); |
|
|
|
|
|
const handleCanvasClickForRegenerate = () => { |
|
if (hasGeneratedContent && !isLoading) { |
|
handleRegenerate(); |
|
} |
|
}; |
|
|
|
|
|
const handleFullscreen = (e) => { |
|
e.stopPropagation(); |
|
alert('Fullscreen action triggered!'); |
|
|
|
}; |
|
|
|
return ( |
|
<div className="flex flex-col"> |
|
{/* Canvas container with fixed aspect ratio */} |
|
<div |
|
className="relative w-full" |
|
style={{ aspectRatio: `${currentDimension.width} / ${currentDimension.height}` }} |
|
> |
|
<button |
|
type="button" |
|
className="w-full h-full absolute inset-0 z-10 cursor-pointer appearance-none bg-transparent border-0 group" |
|
onMouseEnter={() => setIsHoveringCanvas(true)} |
|
onMouseLeave={() => setIsHoveringCanvas(false)} |
|
onClick={handleCanvasClickForRegenerate} |
|
disabled={!hasGeneratedContent || isLoading} |
|
aria-label="Regenerate image" |
|
/> |
|
<canvas |
|
ref={displayCanvasRef} |
|
width={currentDimension.width} |
|
height={currentDimension.height} |
|
className="absolute inset-0 w-full h-full border border-gray-300 bg-white rounded-xl shadow-soft" |
|
aria-label="Generated image canvas" |
|
/> |
|
|
|
{/* Loading overlay */} |
|
{isLoading && ( |
|
<div className="absolute inset-0 flex items-center justify-center bg-black/5 rounded-xl"> |
|
<div className="bg-white/90 rounded-full p-3 shadow-medium"> |
|
<LoaderCircle className="w-8 h-8 animate-spin text-gray-700" /> |
|
</div> |
|
</div> |
|
)} |
|
|
|
{/* Placeholder overlay */} |
|
{showPlaceholder && !isLoading && !hasGeneratedContent && ( |
|
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none"> |
|
<ImageIcon className="w-7 h-7 text-gray-400 mb-2" /> |
|
<p className="text-gray-400 text-lg font-medium">Generation will appear here</p> |
|
</div> |
|
)} |
|
|
|
{/* Hover-to-Refresh Overlay */} |
|
{isHoveringCanvas && hasGeneratedContent && !isLoading && ( |
|
// Added transition classes for smoother appearance |
|
// Changed bg-black/20 to bg-black/10 for a gentler overlay |
|
<div className="absolute inset-0 flex items-center justify-center bg-black/0 rounded-xl pointer-events-none transition-opacity duration-300 ease-in-out"> |
|
{/* Container for icon - allows separate click handling */} |
|
{/* Removed Maximize icon */} |
|
<div className="flex items-center gap-4 bg-white/90 rounded-full p-3 shadow-medium pointer-events-auto"> |
|
{/* Refresh Icon (clickable via parent div onClick) */} |
|
<RotateCw className="w-8 h-8 text-gray-700" title="Click canvas to regenerate"/> |
|
</div> |
|
</div> |
|
)} |
|
</div> |
|
|
|
{/* Action bar and refiner section - Combined layout */} |
|
<div className="mt-4 flex items-stretch justify-between gap-2 max-w-full"> |
|
{/* Left side wrapper for Refiner and action buttons */} |
|
<div className="flex items-stretch gap-2 flex-1 min-w-0"> |
|
{/* Refiner input - only shown when there's generated content */} |
|
{hasGeneratedContent ? ( |
|
<> |
|
<form onSubmit={handleSubmit} className="flex-1 min-w-0"> |
|
<div className="group flex items-center bg-gray-50 focus-within:bg-white rounded-xl shadow-soft p-2 border border-gray-200 focus-within:border-gray-300 h-14 transition-colors"> |
|
<input |
|
name="refiner" |
|
type="text" |
|
value={inputValue} |
|
onChange={(e) => setInputValue(e.target.value)} |
|
placeholder="Type to refine the image..." |
|
disabled={isLoading} |
|
className="flex-1 px-2 bg-transparent border-none text-sm text-gray-400 placeholder-gray-300 group-focus-within:text-gray-600 group-focus-within:placeholder-gray-400 focus:outline-none transition-colors" |
|
/> |
|
<button |
|
type="submit" |
|
disabled={isLoading || !inputValue.trim()} |
|
className="p-2 rounded-lg text-gray-400 group-focus-within:text-gray-700 hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" |
|
aria-label="Send refinement" |
|
> |
|
<Send className="w-5 h-5" /> |
|
</button> |
|
</div> |
|
</form> |
|
{/* Action buttons wrapper */} |
|
<div className="flex gap-2"> |
|
{/* "Send to Doodle" button with tooltip */} |
|
<div className="relative"> |
|
<button |
|
type="button" |
|
onClick={handleSendToDoodle} |
|
disabled={isLoading} |
|
onMouseEnter={() => setShowDoodleTooltip(true)} |
|
onMouseLeave={() => setShowDoodleTooltip(false)} |
|
className="group w-14 h-14 p-2 rounded-lg bg-gray-50 border border-gray-200 shadow-soft hover:bg-white hover:border-gray-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center" |
|
aria-label="Send image back to doodle canvas" |
|
> |
|
<ArrowLeftRight className="w-5 h-5 text-gray-400 group-hover:text-gray-700 transition-colors" /> |
|
</button> |
|
{/* Custom Tooltip - Right aligned */} |
|
{showDoodleTooltip && ( |
|
<div className="absolute bottom-full right-0 mb-2 px-3 py-1.5 bg-white border border-gray-200 text-gray-700 text-xs rounded-lg shadow-soft whitespace-nowrap pointer-events-none"> |
|
Send Back to Doodle Canvas |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</> |
|
) : ( |
|
// Placeholder div to maintain layout when no content |
|
<div className="flex-1 h-0" /> |
|
)} |
|
</div> |
|
</div> |
|
|
|
{} |
|
{hasGeneratedContent && ( |
|
<div className="mt-4 flex flex-wrap gap-2 text-xs mb-4"> |
|
{/* Surprise Me button with updated styling */} |
|
<button |
|
type="button" |
|
onClick={handleSurpriseMe} |
|
className="group flex items-center gap-2 px-3 py-1 bg-gray-50 hover:bg-white rounded-full border border-gray-200 text-gray-400 hover:border-gray-300 transition-all focus:outline-none focus:ring-2 focus:ring-gray-200" |
|
> |
|
<Wand2 className="w-4 h-4 group-hover:text-gray-600" /> |
|
<span className="group-hover:text-gray-600">Surprise Me</span> |
|
</button> |
|
|
|
{/* Regular suggestion buttons with updated styling */} |
|
{REFINEMENT_SUGGESTIONS.map((suggestion) => ( |
|
<button |
|
key={`suggestion-${suggestion.label}`} |
|
type="button" |
|
onClick={() => handleSuggestionClick(suggestion)} |
|
className="group flex items-center gap-2 px-3 py-1 bg-gray-50 hover:bg-white rounded-full border border-gray-200 text-gray-400 hover:border-gray-300 transition-all focus:outline-none focus:ring-2 focus:ring-gray-200" |
|
> |
|
<suggestion.icon className="w-4 h-4 group-hover:text-gray-600" /> |
|
<span className="group-hover:text-gray-600">{suggestion.label}</span> |
|
</button> |
|
))} |
|
</div> |
|
)} |
|
|
|
{} |
|
<div className="md:hidden flex flex-wrap justify-between items-center gap-2 mt-6 mb-2 w-full"> |
|
<div className="grid grid-cols-3 w-full gap-2"> |
|
<HeaderButtons |
|
hasHistory={hasHistory} |
|
openHistoryModal={openHistoryModal} |
|
toggleLibrary={toggleLibrary} |
|
handleSaveImage={handleSaveImage} |
|
isLoading={isLoading} |
|
hasGeneratedContent={hasGeneratedContent} |
|
/> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default DisplayCanvas; |