Files
igny8/frontend/src/components/common/ImageResultCard.tsx
2025-11-09 10:27:02 +00:00

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>
);
}