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

340 lines
9.5 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';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
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;
};
}
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';
type CodeTab = 'curl' | 'fetch' | 'rest';
export function ResultCard({
result,
apiKey,
onZoom,
onCopy,
onDownload,
onReusePrompt,
}: ResultCardProps) {
const [viewMode, setViewMode] = useState<ViewMode>('preview');
const [activeTab, setActiveTab] = useState<CodeTab>('curl');
const curlCode = `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 = `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 = `### 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 getCodeForTab = () => {
switch (activeTab) {
case 'curl':
return curlCode;
case 'fetch':
return fetchCode;
case 'rest':
return 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"
/>
</div>
</div>
{/* API Code Examples */}
<CodeExamples
activeTab={activeTab}
setActiveTab={setActiveTab}
code={getCodeForTab()}
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,
}: {
image: GenerationResult['leftImage'];
label: string;
prompt: string;
onZoom: (url: string) => void;
onDownload: (url: string, filename: string) => void;
onReusePrompt: (prompt: string) => void;
filename: string;
}) {
return (
<div className="flex-shrink-0">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-400">{label}</span>
</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>
)
)}
<div className="mt-2 flex items-start gap-2 max-w-sm">
<p className="flex-1 text-sm text-gray-300 leading-relaxed">{prompt}</p>
<PromptReuseButton prompt={prompt} onReuse={onReusePrompt} label={label} />
</div>
</div>
);
}
// Code Examples Component
function CodeExamples({
activeTab,
setActiveTab,
code,
onCopy,
}: {
activeTab: CodeTab;
setActiveTab: (tab: CodeTab) => void;
code: string;
onCopy: (text: string) => void;
}) {
return (
<div className="bg-slate-950/50 rounded-xl border border-slate-700 overflow-hidden">
<div className="flex items-center gap-2 bg-slate-900/50 px-4 py-2 border-b border-slate-700">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500/50"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500/50"></div>
<div className="w-3 h-3 rounded-full bg-green-500/50"></div>
</div>
<div className="flex-1 flex gap-2 ml-4">
<TabButton
active={activeTab === 'curl'}
onClick={() => setActiveTab('curl')}
label="cURL"
/>
<TabButton
active={activeTab === 'fetch'}
onClick={() => setActiveTab('fetch')}
label="JS Fetch"
/>
<TabButton
active={activeTab === 'rest'}
onClick={() => setActiveTab('rest')}
label="REST"
/>
</div>
<button
onClick={() => onCopy(code)}
className="px-3 py-1 text-xs bg-amber-600/20 hover:bg-amber-600/30 text-amber-400 rounded transition-colors"
aria-label="Copy code"
>
Copy
</button>
</div>
<pre className="p-4 text-xs md:text-sm text-gray-300 overflow-x-auto">
<code>{code}</code>
</pre>
</div>
);
}
function TabButton({
active,
onClick,
label,
}: {
active: boolean;
onClick: () => void;
label: string;
}) {
return (
<button
onClick={onClick}
className={`px-3 py-1 text-xs rounded transition-colors ${
active ? 'bg-slate-700 text-white' : 'text-gray-400 hover:text-white'
}`}
aria-pressed={active}
>
{label}
</button>
);
}