254 lines
9.4 KiB
TypeScript
254 lines
9.4 KiB
TypeScript
import { ReactNode, useEffect, useState } from 'react';
|
|
|
|
interface ImageResultCardProps {
|
|
title: string;
|
|
description?: string;
|
|
icon?: ReactNode;
|
|
generatedImage?: {
|
|
url: string;
|
|
revised_prompt?: string;
|
|
model?: string;
|
|
provider?: string;
|
|
size?: string;
|
|
format?: string;
|
|
cost?: string;
|
|
} | null;
|
|
error?: string | null;
|
|
}
|
|
|
|
/**
|
|
* Image Result Display Card Component
|
|
* Displays the generated image with details
|
|
*/
|
|
export default function ImageResultCard({
|
|
title,
|
|
description,
|
|
icon,
|
|
generatedImage,
|
|
error,
|
|
}: ImageResultCardProps) {
|
|
const [imageData, setImageData] = useState(generatedImage);
|
|
const [errorState, setErrorState] = useState(error);
|
|
|
|
// Listen for image generation events from ImageGenerationCard
|
|
useEffect(() => {
|
|
const handleImageGenerated = (event: CustomEvent) => {
|
|
setImageData(event.detail);
|
|
setErrorState(null);
|
|
};
|
|
|
|
const handleImageError = (event: CustomEvent) => {
|
|
setErrorState(event.detail);
|
|
setImageData(null);
|
|
};
|
|
|
|
window.addEventListener('imageGenerated', handleImageGenerated as EventListener);
|
|
window.addEventListener('imageGenerationError', handleImageError as EventListener);
|
|
|
|
return () => {
|
|
window.removeEventListener('imageGenerated', handleImageGenerated as EventListener);
|
|
window.removeEventListener('imageGenerationError', handleImageError as EventListener);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setImageData(generatedImage);
|
|
}, [generatedImage]);
|
|
|
|
useEffect(() => {
|
|
setErrorState(error);
|
|
}, [error]);
|
|
|
|
return (
|
|
<article className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/3">
|
|
<div className="relative p-5 pb-6">
|
|
{icon && (
|
|
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center">
|
|
{icon}
|
|
</div>
|
|
)}
|
|
<h3 className="mb-2 text-base font-semibold text-gray-800 dark:text-white/90">
|
|
{title}
|
|
</h3>
|
|
{description && (
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="border-t border-gray-200 p-5 dark:border-gray-800">
|
|
{errorState ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="mb-4 rounded-full bg-red-100 p-4 dark:bg-red-900/20">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-red-600 dark:text-red-400"
|
|
>
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
<line x1="12" y1="9" x2="12" y2="13" />
|
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
</svg>
|
|
</div>
|
|
<h4 className="mb-2 text-lg font-semibold text-gray-800 dark:text-white">
|
|
Generation Failed
|
|
</h4>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">{errorState}</p>
|
|
</div>
|
|
) : imageData?.url ? (
|
|
<div className="space-y-5">
|
|
{/* Generated Image */}
|
|
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
|
<img
|
|
src={imageData.url}
|
|
alt="Generated image"
|
|
className="w-full object-contain"
|
|
style={{ maxHeight: '400px' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Image Details */}
|
|
<div className="space-y-3 rounded-lg bg-gray-50 p-4 dark:bg-gray-800/50">
|
|
<h4 className="text-sm font-semibold text-gray-800 dark:text-white">
|
|
Image Details
|
|
</h4>
|
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
<div>
|
|
<span className="font-medium text-gray-600 dark:text-gray-400">Size:</span>
|
|
<span className="ml-2 text-gray-800 dark:text-white">
|
|
{imageData.size || '1024x1024'} pixels
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium text-gray-600 dark:text-gray-400">Format:</span>
|
|
<span className="ml-2 text-gray-800 dark:text-white">
|
|
{imageData.format || 'WEBP'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium text-gray-600 dark:text-gray-400">Model:</span>
|
|
<span className="ml-2 text-gray-800 dark:text-white">
|
|
{imageData.model || 'DALL·E 3'}
|
|
</span>
|
|
</div>
|
|
{imageData.cost && (
|
|
<div>
|
|
<span className="font-medium text-gray-600 dark:text-gray-400">Cost:</span>
|
|
<span className="ml-2 text-gray-800 dark:text-white">{imageData.cost}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Revised Prompt */}
|
|
{imageData.revised_prompt && (
|
|
<div className="mt-3 border-t border-gray-200 pt-3 dark:border-gray-700">
|
|
<p className="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
Revised Prompt:
|
|
</p>
|
|
<p className="text-xs text-gray-700 dark:text-gray-300">
|
|
{imageData.revised_prompt}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Negative Prompt (if available) */}
|
|
{imageData.negative_prompt && (
|
|
<div className="mt-3 border-t border-gray-200 pt-3 dark:border-gray-700">
|
|
<p className="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
Negative Prompt:
|
|
</p>
|
|
<p className="text-xs text-gray-700 dark:text-gray-300">
|
|
{imageData.negative_prompt}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-3">
|
|
<a
|
|
href={imageData.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
<polyline points="15 3 21 3 21 9" />
|
|
<line x1="10" y1="14" x2="21" y2="3" />
|
|
</svg>
|
|
View Original
|
|
</a>
|
|
<button
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(imageData.url);
|
|
}}
|
|
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
</svg>
|
|
Copy URL
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="mb-4 rounded-full bg-gray-100 p-4 dark:bg-gray-800">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-gray-400 dark:text-gray-500"
|
|
>
|
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
<circle cx="9" cy="9" r="2" />
|
|
<path d="M21 15l-3.086-3.086a2 2 0 00-2.828 0L6 21" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-sm text-gray-400 dark:text-gray-500">
|
|
No image generated yet. Fill out the form and click "Generate Image" to create your
|
|
first AI image.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|