banatie-service/apps/landing/src/components/demo/ResultCard.tsx

377 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { InspectMode } from './InspectMode';
import { PromptReuseButton } from './PromptReuseButton';
import { CompletedTimerBadge } from './GenerationTimer';
import { CodeExamplesWidget } from './CodeExamplesWidget';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
interface EnhancementOptionsData {
imageStyle?: string;
aspectRatio?: string;
mood?: string;
lighting?: string;
cameraAngle?: string;
negativePrompts?: string[];
}
interface GenerationResult {
id: string;
timestamp: Date;
originalPrompt: string;
enhancedPrompt?: string;
leftImage: {
url: string;
width: number;
height: number;
error?: string;
} | null;
rightImage: {
url: string;
width: number;
height: number;
error?: string;
} | null;
durationMs?: number;
leftData?: {
request: object;
response: object;
geminiParams: object;
};
rightData?: {
request: object;
response: object;
geminiParams: object;
};
enhancementOptions?: EnhancementOptionsData;
}
interface ResultCardProps {
result: GenerationResult;
apiKey: string;
onZoom: (url: string) => void;
onCopy: (text: string) => void;
onDownload: (url: string, filename: string) => void;
onReusePrompt: (prompt: string) => void;
}
type ViewMode = 'preview' | 'inspect';
export function ResultCard({
result,
apiKey,
onZoom,
onCopy,
onDownload,
onReusePrompt,
}: ResultCardProps) {
const [viewMode, setViewMode] = useState<ViewMode>('preview');
// Build enhancement options JSON for code examples
const buildEnhancementOptionsJson = () => {
if (!result.enhancementOptions) return '';
const opts: any = {};
if (result.enhancementOptions.imageStyle)
opts.imageStyle = result.enhancementOptions.imageStyle;
if (result.enhancementOptions.aspectRatio)
opts.aspectRatio = result.enhancementOptions.aspectRatio;
if (result.enhancementOptions.mood) opts.mood = result.enhancementOptions.mood;
if (result.enhancementOptions.lighting) opts.lighting = result.enhancementOptions.lighting;
if (result.enhancementOptions.cameraAngle)
opts.cameraAngle = result.enhancementOptions.cameraAngle;
if (result.enhancementOptions.negativePrompts)
opts.negativePrompts = result.enhancementOptions.negativePrompts;
if (Object.keys(opts).length === 0) return '';
return JSON.stringify(opts, null, 2)
.split('\n')
.map((line, i) => (i === 0 ? line : ` ${line}`))
.join('\n');
};
const enhancementOptionsJson = buildEnhancementOptionsJson();
const hasEnhancementOptions = !!result.enhancementOptions;
const curlCode = hasEnhancementOptions
? `# Right image (with enhancement options)
curl -X POST ${API_BASE_URL}/api/text-to-image \\
-H "Content-Type: application/json" \\
-H "X-API-Key: ${apiKey}" \\
-d '{
"prompt": "${result.originalPrompt.replace(/"/g, '\\"')}",
"filename": "generated_image",
"enhancementOptions": ${enhancementOptionsJson.replace(/\n/g, '\n ')}
}'`
: `curl -X POST ${API_BASE_URL}/api/text-to-image \\
-H "Content-Type: application/json" \\
-H "X-API-Key: ${apiKey}" \\
-d '{
"prompt": "${result.originalPrompt.replace(/"/g, '\\"')}",
"filename": "generated_image"
}'`;
const fetchCode = hasEnhancementOptions
? `// Right image (with enhancement options)
fetch('${API_BASE_URL}/api/text-to-image', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': '${apiKey}'
},
body: JSON.stringify({
prompt: '${result.originalPrompt.replace(/'/g, "\\'")}',
filename: 'generated_image',
enhancementOptions: ${enhancementOptionsJson}
})
})
.then(res => res.json())
.then(data => console.log(data));`
: `fetch('${API_BASE_URL}/api/text-to-image', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': '${apiKey}'
},
body: JSON.stringify({
prompt: '${result.originalPrompt.replace(/'/g, "\\'")}',
filename: 'generated_image'
})
})
.then(res => res.json())
.then(data => console.log(data));`;
const restCode = hasEnhancementOptions
? `### Right Image - With Enhancement Options
POST ${API_BASE_URL}/api/text-to-image
Content-Type: application/json
X-API-Key: ${apiKey}
{
"prompt": "${result.originalPrompt.replace(/"/g, '\\"')}",
"filename": "generated_image",
"enhancementOptions": ${enhancementOptionsJson}
}`
: `### Generate Image - Text to Image
POST ${API_BASE_URL}/api/text-to-image
Content-Type: application/json
X-API-Key: ${apiKey}
{
"prompt": "${result.originalPrompt.replace(/"/g, '\\"')}",
"filename": "generated_image"
}`;
const codeExamples = {
curl: curlCode,
fetch: fetchCode,
rest: restCode,
};
return (
<div className="p-5 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl animate-fade-in">
{/* Header */}
<div className="mb-4 flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500">{result.timestamp.toLocaleString()}</span>
{result.durationMs && <CompletedTimerBadge durationMs={result.durationMs} />}
</div>
{/* View Mode Toggle */}
<div className="flex gap-1 p-1 bg-slate-950/50 border border-slate-700 rounded-lg">
<button
onClick={() => setViewMode('preview')}
className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
viewMode === 'preview' ? 'bg-amber-600 text-white' : 'text-gray-400 hover:text-white'
}`}
aria-pressed={viewMode === 'preview'}
>
Preview
</button>
<button
onClick={() => setViewMode('inspect')}
className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
viewMode === 'inspect' ? 'bg-amber-600 text-white' : 'text-gray-400 hover:text-white'
}`}
aria-pressed={viewMode === 'inspect'}
>
Inspect
</button>
</div>
</div>
{/* Content */}
{viewMode === 'preview' ? (
<>
{/* Image Comparison */}
<div className="mb-5 overflow-x-auto scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-slate-800">
<div className="flex gap-4 pb-4">
{/* Left Image */}
<ImagePreview
image={result.leftImage}
label="Original Prompt"
prompt={result.originalPrompt}
onZoom={onZoom}
onDownload={onDownload}
onReusePrompt={onReusePrompt}
filename="original.png"
/>
{/* Right Image */}
<ImagePreview
image={result.rightImage}
label="Enhanced Prompt"
prompt={result.enhancedPrompt || result.originalPrompt}
onZoom={onZoom}
onDownload={onDownload}
onReusePrompt={onReusePrompt}
filename="enhanced.png"
hasEnhancementOptions={!!result.enhancementOptions}
/>
</div>
</div>
{/* API Code Examples */}
<CodeExamplesWidget codeExamples={codeExamples} onCopy={onCopy} />
</>
) : (
<InspectMode
leftData={
result.leftData || {
request: { prompt: result.originalPrompt },
response: { success: true, url: result.leftImage?.url },
geminiParams: {},
}
}
rightData={
result.rightData || {
request: { prompt: result.enhancedPrompt || result.originalPrompt },
response: { success: true, url: result.rightImage?.url },
geminiParams: {},
}
}
onCopy={onCopy}
/>
)}
</div>
);
}
// Image Preview Component
function ImagePreview({
image,
label,
prompt,
onZoom,
onDownload,
onReusePrompt,
filename,
hasEnhancementOptions = false,
}: {
image: GenerationResult['leftImage'];
label: string;
prompt: string;
onZoom: (url: string) => void;
onDownload: (url: string, filename: string) => void;
onReusePrompt: (prompt: string) => void;
filename: string;
hasEnhancementOptions?: boolean;
}) {
const [promptExpanded, setPromptExpanded] = useState(false);
const [urlCopied, setUrlCopied] = useState(false);
const copyImageUrl = () => {
if (image?.url) {
navigator.clipboard.writeText(image.url);
setUrlCopied(true);
setTimeout(() => setUrlCopied(false), 2000);
}
};
return (
<div className="flex-shrink-0">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-400">{label}</span>
{hasEnhancementOptions && (
<span className="px-2 py-0.5 text-xs bg-amber-600/20 text-amber-400 rounded-full border border-amber-600/30">
+ Options
</span>
)}
</div>
</div>
{image?.error ? (
<div className="h-96 w-96 flex items-center justify-center bg-red-900/20 border border-red-700 rounded-lg">
<div className="text-center p-4">
<div className="text-4xl mb-2"></div>
<div className="text-sm text-red-400">{image.error}</div>
</div>
</div>
) : (
image && (
<>
<div className="relative group cursor-pointer">
<img
src={image.url}
alt={label}
className="h-96 w-auto object-contain rounded-lg border border-slate-700"
onClick={() => onZoom(image.url)}
/>
<button
onClick={(e) => {
e.stopPropagation();
onDownload(image.url, filename);
}}
className="absolute top-2 right-2 px-3 py-1.5 bg-black/70 hover:bg-black/90 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity"
aria-label={`Download ${label}`}
>
Download
</button>
</div>
{/* Image URL with Copy Button */}
<div className="mt-2 flex items-center gap-2 max-w-sm">
<div className="flex-1 px-2 py-1 bg-slate-800/50 border border-slate-700 rounded text-xs text-gray-400 font-mono truncate">
{image.url}
</div>
<button
onClick={copyImageUrl}
className="px-2 py-1 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded text-xs text-gray-300 hover:text-white transition-colors whitespace-nowrap"
aria-label="Copy image URL"
>
{urlCopied ? '✓ Copied' : 'Copy URL'}
</button>
</div>
</>
)
)}
{/* Prompt with Truncation */}
<div className="mt-2 flex items-start gap-2 max-w-sm">
<div className="flex-1">
<p
className={`text-sm text-gray-300 leading-relaxed ${
!promptExpanded ? 'line-clamp-4' : ''
}`}
>
{prompt}
</p>
{prompt.length > 150 && (
<button
onClick={() => setPromptExpanded(!promptExpanded)}
className="mt-1 text-xs text-amber-400 hover:text-amber-300 transition-colors"
>
{promptExpanded ? 'Show less' : 'Show more...'}
</button>
)}
</div>
<PromptReuseButton prompt={prompt} onReuse={onReusePrompt} label={label} />
</div>
</div>
);
}