feat(identity-generator): Add GenerationPreview, IdentitySelector, and SceneBuilder components with new styling and entry point exports

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-28 14:57:09 -07:00
parent 241c1c3e80
commit 60ac1bb2a8
5 changed files with 0 additions and 1236 deletions

View file

@ -1,195 +0,0 @@
/**
* Generation Preview Component
*
* Displays generated image results with verification score.
*/
import type { IdentityGenerationResult } from '../hooks/useIdentityGeneration';
import {
PreviewContainer,
PreviewImageWrapper,
PreviewImage,
PreviewPlaceholder,
GeneratingOverlay,
GeneratingText,
GeneratingProgress,
VerificationBadge,
ResultStats,
ResultStat,
ResultStatLabel,
ResultStatValue,
ResultActions,
ActionButton,
SectionHeader,
ErrorMessage,
} from './styles';
export interface GenerationPreviewProps {
/** Last generation result */
result: IdentityGenerationResult | null;
/** Whether generation is in progress */
isGenerating: boolean;
/** Generation error */
error?: Error | null;
/** Called when regenerate is clicked */
onRegenerate?: () => void;
/** Called when save is clicked */
onSave?: (result: IdentityGenerationResult) => void;
/** Called when download is clicked */
onDownload?: (result: IdentityGenerationResult) => void;
}
/**
* Convert confidence level to human-readable text
*/
function formatConfidence(confidence: 'high' | 'medium' | 'low'): string {
switch (confidence) {
case 'high':
return 'High confidence match';
case 'medium':
return 'Medium confidence';
case 'low':
return 'Low confidence';
}
}
/**
* Format duration in milliseconds to human-readable string
*/
function formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
}
return `${(ms / 1000).toFixed(1)}s`;
}
/**
* Download image from base64 data
*/
function downloadImage(base64: string, filename: string = 'generated-image.png') {
const link = document.createElement('a');
link.href = base64.startsWith('data:') ? base64 : `data:image/png;base64,${base64}`;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
export function GenerationPreview({
result,
isGenerating,
error,
onRegenerate,
onSave,
onDownload,
}: GenerationPreviewProps) {
const imageUrl = result?.imageBase64
? result.imageBase64.startsWith('data:')
? result.imageBase64
: `data:image/png;base64,${result.imageBase64}`
: null;
const handleDownload = () => {
if (result) {
if (onDownload) {
onDownload(result);
} else {
downloadImage(result.imageBase64, `identity-gen-${result.seed}.png`);
}
}
};
return (
<div>
<SectionHeader>Generated Result</SectionHeader>
<PreviewContainer>
<PreviewImageWrapper>
{imageUrl && !isGenerating && (
<PreviewImage src={imageUrl} alt="Generated image" />
)}
{!imageUrl && !isGenerating && (
<PreviewPlaceholder>
Configure settings and click Generate to create an image
</PreviewPlaceholder>
)}
{isGenerating && (
<GeneratingOverlay>
<GeneratingText>Generating your image...</GeneratingText>
<GeneratingProgress />
</GeneratingOverlay>
)}
</PreviewImageWrapper>
{error && (
<ErrorMessage>
Generation failed: {error.message}
</ErrorMessage>
)}
{result && !isGenerating && (
<>
<VerificationBadge $score={result.identityMatchScore}>
Identity Match: {(result.identityMatchScore * 100).toFixed(0)}%
{result.identityMatchScore >= 0.8 && ' ✓'}
</VerificationBadge>
<ResultStats>
<ResultStat>
<ResultStatLabel>Confidence</ResultStatLabel>
<ResultStatValue>
{formatConfidence(result.identityConfidence)}
</ResultStatValue>
</ResultStat>
<ResultStat>
<ResultStatLabel>Aesthetic Score</ResultStatLabel>
<ResultStatValue>
{(result.aestheticScore * 100).toFixed(0)}%
</ResultStatValue>
</ResultStat>
<ResultStat>
<ResultStatLabel>Seed</ResultStatLabel>
<ResultStatValue>{result.seed}</ResultStatValue>
</ResultStat>
<ResultStat>
<ResultStatLabel>Duration</ResultStatLabel>
<ResultStatValue>
{formatDuration(result.durationMs)}
</ResultStatValue>
</ResultStat>
<ResultStat>
<ResultStatLabel>Model</ResultStatLabel>
<ResultStatValue>{result.model}</ResultStatValue>
</ResultStat>
<ResultStat>
<ResultStatLabel>Size</ResultStatLabel>
<ResultStatValue>
{result.width}x{result.height}
</ResultStatValue>
</ResultStat>
</ResultStats>
<ResultActions>
{onRegenerate && (
<ActionButton type="button" onClick={onRegenerate}>
Regenerate
</ActionButton>
)}
{onSave && (
<ActionButton
type="button"
onClick={() => onSave(result)}
>
Save to Gallery
</ActionButton>
)}
<ActionButton type="button" onClick={handleDownload}>
Download
</ActionButton>
</ResultActions>
</>
)}
</PreviewContainer>
</div>
);
}

View file

@ -1,140 +0,0 @@
/**
* Identity Selector Component
*
* Displays and allows selection of stored identities.
*/
import { type Identity } from '../hooks/useIdentityGeneration';
import {
IdentityGrid,
IdentityCard,
IdentityAvatar,
IdentityInfo,
IdentityName,
IdentityMeta,
SelectedBadge,
AddIdentityButton,
SectionHeader,
ErrorMessage,
} from './styles';
export interface IdentitySelectorProps {
/** Available identities */
identities: Identity[];
/** Currently selected identity ID */
selectedId: string | null;
/** Called when an identity is selected */
onSelect: (identity: Identity) => void;
/** Called when "Add New" is clicked */
onAddNew?: () => void;
/** Loading state */
isLoading?: boolean;
/** Error state */
error?: Error | null;
}
/**
* Get initials from a name for the avatar
*/
function getInitials(name: string): string {
return name
.split(' ')
.map((word) => word[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
/**
* Format date for display
*/
function formatDate(dateString: string): string {
try {
const date = new Date(dateString);
return date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
});
} catch {
return 'Unknown';
}
}
export function IdentitySelector({
identities,
selectedId,
onSelect,
onAddNew,
isLoading,
error,
}: IdentitySelectorProps) {
if (error) {
return (
<div>
<SectionHeader>Select Identity</SectionHeader>
<ErrorMessage>
Failed to load identities: {error.message}
</ErrorMessage>
</div>
);
}
return (
<div>
<SectionHeader>Select Identity</SectionHeader>
<IdentityGrid>
{isLoading ? (
<IdentityCard>
<IdentityInfo>
<IdentityMeta>Loading identities...</IdentityMeta>
</IdentityInfo>
</IdentityCard>
) : identities.length === 0 ? (
<IdentityCard>
<IdentityInfo>
<IdentityMeta>No identities found</IdentityMeta>
</IdentityInfo>
</IdentityCard>
) : (
identities.map((identity) => (
<IdentityCard
key={identity.id}
$selected={identity.id === selectedId}
onClick={() => onSelect(identity)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelect(identity);
}
}}
>
<IdentityAvatar>{getInitials(identity.name)}</IdentityAvatar>
<IdentityInfo>
<IdentityName>
{identity.name}
{identity.id === selectedId && (
<SelectedBadge>Selected</SelectedBadge>
)}
</IdentityName>
<IdentityMeta>
{identity.imageCount} images | {formatDate(identity.createdAt)}
</IdentityMeta>
</IdentityInfo>
</IdentityCard>
))
)}
{onAddNew && (
<AddIdentityButton onClick={onAddNew} type="button">
<span>+</span>
<span>Add New Identity</span>
</AddIdentityButton>
)}
</IdentityGrid>
</div>
);
}

View file

@ -1,222 +0,0 @@
/**
* Scene Builder Component
*
* Configure generation parameters for identity-preserving image generation.
*/
import { useState } from 'react';
import type { IdentityGenerationParams } from '../hooks/useIdentityGeneration';
import {
FormSection,
FormLabel,
FormTextArea,
FormInput,
FormSelect,
SliderContainer,
SliderRow,
Slider,
SliderValue,
AdvancedToggle,
AdvancedSection,
GenerateButton,
SectionHeader,
} from './styles';
export interface SceneBuilderProps {
/** Called when generate is clicked */
onGenerate: (params: Omit<IdentityGenerationParams, 'identityId'>) => void;
/** Whether generation is in progress */
isGenerating: boolean;
/** Whether a valid identity is selected */
hasIdentity: boolean;
}
type PoseType = 'standing' | 'sitting' | 'walking' | 'running';
const POSE_OPTIONS: { value: PoseType | ''; label: string }[] = [
{ value: '', label: 'Auto (no pose control)' },
{ value: 'standing', label: 'Standing' },
{ value: 'sitting', label: 'Sitting' },
{ value: 'walking', label: 'Walking' },
{ value: 'running', label: 'Running' },
];
const MODEL_OPTIONS = [
{ value: 'juggernaut-xi-v11', label: 'Juggernaut XI v11 (Recommended)' },
{ value: 'realvisxl-v4', label: 'RealVisXL v4' },
{ value: 'dreamshaper-xl', label: 'DreamShaper XL' },
];
export function SceneBuilder({
onGenerate,
isGenerating,
hasIdentity,
}: SceneBuilderProps) {
const [prompt, setPrompt] = useState('');
const [negativePrompt, setNegativePrompt] = useState('');
const [poseType, setPoseType] = useState<PoseType | ''>('');
const [outfitDescription, setOutfitDescription] = useState('');
const [identityStrength, setIdentityStrength] = useState(0.85);
const [showAdvanced, setShowAdvanced] = useState(false);
// Advanced options
const [enableInstantid, setEnableInstantid] = useState(true);
const [ipAdapterScale, setIpAdapterScale] = useState(0.6);
const [model, setModel] = useState('juggernaut-xi-v11');
const handleGenerate = () => {
const params: Omit<IdentityGenerationParams, 'identityId'> = {
prompt,
negativePrompt: negativePrompt || undefined,
identityStrength,
enableInstantid,
ipAdapterScale,
poseType: poseType || undefined,
outfitDescription: outfitDescription || undefined,
model,
};
onGenerate(params);
};
const canGenerate = hasIdentity && prompt.trim().length > 0 && !isGenerating;
return (
<div>
<SectionHeader>Scene Description</SectionHeader>
<FormSection>
<FormLabel htmlFor="prompt">
Describe the scene, setting, and style
</FormLabel>
<FormTextArea
id="prompt"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="professional photo on a beach at sunset, smiling, warm lighting, golden hour..."
disabled={isGenerating}
/>
</FormSection>
<FormSection style={{ marginTop: '1rem' }}>
<FormLabel htmlFor="outfit">Outfit Description (optional)</FormLabel>
<FormInput
id="outfit"
type="text"
value={outfitDescription}
onChange={(e) => setOutfitDescription(e.target.value)}
placeholder="white summer dress, sandals"
disabled={isGenerating}
/>
</FormSection>
<FormSection style={{ marginTop: '1rem' }}>
<FormLabel htmlFor="pose">Pose</FormLabel>
<FormSelect
id="pose"
value={poseType}
onChange={(e) => setPoseType(e.target.value as PoseType | '')}
disabled={isGenerating}
>
{POSE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</FormSelect>
</FormSection>
<SliderContainer style={{ marginTop: '1.5rem' }}>
<FormLabel>Identity Strength</FormLabel>
<SliderRow>
<Slider
min={0.5}
max={1.2}
step={0.05}
value={identityStrength}
onChange={(e) => setIdentityStrength(parseFloat(e.target.value))}
disabled={isGenerating}
/>
<SliderValue>{identityStrength.toFixed(2)}</SliderValue>
</SliderRow>
</SliderContainer>
<AdvancedToggle
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
style={{ marginTop: '1rem' }}
>
<span>{showAdvanced ? '▼' : '▶'}</span>
<span>Advanced Options</span>
</AdvancedToggle>
<AdvancedSection $open={showAdvanced}>
<FormSection>
<FormLabel htmlFor="negative-prompt">Negative Prompt</FormLabel>
<FormTextArea
id="negative-prompt"
value={negativePrompt}
onChange={(e) => setNegativePrompt(e.target.value)}
placeholder="blurry, low quality, distorted face, extra limbs..."
style={{ minHeight: '60px' }}
disabled={isGenerating}
/>
</FormSection>
<FormSection>
<FormLabel htmlFor="model">Model</FormLabel>
<FormSelect
id="model"
value={model}
onChange={(e) => setModel(e.target.value)}
disabled={isGenerating}
>
{MODEL_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</FormSelect>
</FormSection>
<FormSection>
<FormLabel>
<label>
<input
type="checkbox"
checked={enableInstantid}
onChange={(e) => setEnableInstantid(e.target.checked)}
disabled={isGenerating}
style={{ marginRight: '0.5rem' }}
/>
Enable InstantID (enhanced identity fidelity)
</label>
</FormLabel>
</FormSection>
<SliderContainer>
<FormLabel>IP-Adapter Scale</FormLabel>
<SliderRow>
<Slider
min={0}
max={1}
step={0.05}
value={ipAdapterScale}
onChange={(e) => setIpAdapterScale(parseFloat(e.target.value))}
disabled={isGenerating}
/>
<SliderValue>{ipAdapterScale.toFixed(2)}</SliderValue>
</SliderRow>
</SliderContainer>
</AdvancedSection>
<GenerateButton
type="button"
onClick={handleGenerate}
disabled={!canGenerate}
style={{ marginTop: '1.5rem', width: '100%' }}
>
{isGenerating ? 'Generating...' : 'Generate Image'}
</GenerateButton>
</div>
);
}

View file

@ -1,171 +0,0 @@
/**
* Identity Generator Component
*
* Main component for identity-preserving image generation.
* Integrates identity selection, scene configuration, and result preview.
*/
import { useState, useCallback } from 'react';
import {
useIdentityGeneration,
type Identity,
type IdentityGenerationParams,
type IdentityGenerationResult,
type UseIdentityGenerationConfig,
} from '../hooks/useIdentityGeneration';
import { IdentitySelector } from './IdentitySelector';
import { SceneBuilder } from './SceneBuilder';
import { GenerationPreview } from './GenerationPreview';
import {
GeneratorContainer,
LeftPanel,
RightPanel,
ErrorMessage,
} from './styles';
export interface IdentityGeneratorProps extends UseIdentityGenerationConfig {
/** Called when the modal is closed */
onClose?: () => void;
/** Called when an image is generated */
onImageGenerated?: (result: IdentityGenerationResult) => void;
/** Called when an image is saved to gallery */
onSaveToGallery?: (result: IdentityGenerationResult) => void;
/** Pre-select an identity by ID */
defaultIdentityId?: string;
}
export function IdentityGenerator({
onClose,
onImageGenerated,
onSaveToGallery,
defaultIdentityId,
identityServiceUrl,
pipelineServiceUrl,
identitiesRefetchInterval,
}: IdentityGeneratorProps) {
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null);
const [lastParams, setLastParams] = useState<Omit<
IdentityGenerationParams,
'identityId'
> | null>(null);
const {
identities,
identitiesLoading,
identitiesError,
generate,
isGenerating,
generationError,
lastResult,
isHealthy,
identityServiceHealthy,
pipelineServiceHealthy,
} = useIdentityGeneration({
identityServiceUrl,
pipelineServiceUrl,
identitiesRefetchInterval,
});
// Auto-select identity if defaultIdentityId is provided
useState(() => {
if (defaultIdentityId && identities.length > 0 && !selectedIdentity) {
const identity = identities.find((i) => i.id === defaultIdentityId);
if (identity) {
setSelectedIdentity(identity);
}
}
});
const handleSelectIdentity = useCallback((identity: Identity) => {
setSelectedIdentity(identity);
}, []);
const handleGenerate = useCallback(
async (params: Omit<IdentityGenerationParams, 'identityId'>) => {
if (!selectedIdentity) return;
setLastParams(params);
try {
const result = await generate({
...params,
identityId: selectedIdentity.id,
});
onImageGenerated?.(result);
} catch (error) {
console.error('Generation failed:', error);
}
},
[selectedIdentity, generate, onImageGenerated],
);
const handleRegenerate = useCallback(() => {
if (lastParams) {
handleGenerate({
...lastParams,
seed: undefined, // Use new random seed
});
}
}, [lastParams, handleGenerate]);
const handleSaveToGallery = useCallback(
(result: IdentityGenerationResult) => {
onSaveToGallery?.(result);
},
[onSaveToGallery],
);
// Show health status warnings
const healthWarning =
!isHealthy && !identitiesLoading ? (
<ErrorMessage style={{ marginBottom: '1rem' }}>
{!identityServiceHealthy && !pipelineServiceHealthy
? 'Identity and Pipeline services are unavailable'
: !identityServiceHealthy
? 'Identity service is unavailable'
: 'Pipeline service is unavailable'}
</ErrorMessage>
) : null;
return (
<div>
{healthWarning}
<GeneratorContainer>
<LeftPanel>
<IdentitySelector
identities={identities}
selectedId={selectedIdentity?.id ?? null}
onSelect={handleSelectIdentity}
isLoading={identitiesLoading}
error={identitiesError as Error | null}
/>
</LeftPanel>
<RightPanel>
<SceneBuilder
onGenerate={handleGenerate}
isGenerating={isGenerating}
hasIdentity={selectedIdentity !== null}
/>
<GenerationPreview
result={lastResult ?? null}
isGenerating={isGenerating}
error={generationError as Error | null}
onRegenerate={lastParams ? handleRegenerate : undefined}
onSave={onSaveToGallery ? handleSaveToGallery : undefined}
/>
</RightPanel>
</GeneratorContainer>
</div>
);
}
// Re-export sub-components for customization
export { IdentitySelector } from './IdentitySelector';
export type { IdentitySelectorProps } from './IdentitySelector';
export { SceneBuilder } from './SceneBuilder';
export type { SceneBuilderProps } from './SceneBuilder';
export { GenerationPreview } from './GenerationPreview';
export type { GenerationPreviewProps } from './GenerationPreview';

View file

@ -1,508 +0,0 @@
/**
* Styled components for IdentityGenerator
*
* Follows the dark cyberpunk aesthetic with pink accents.
*/
import styled, { css, keyframes } from 'styled-components';
// ============================================================================
// Animations
// ============================================================================
const pulse = keyframes`
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
`;
const shimmer = keyframes`
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
`;
// ============================================================================
// Layout Components
// ============================================================================
export const GeneratorContainer = styled.div`
display: grid;
grid-template-columns: 280px 1fr;
gap: 1.5rem;
min-height: 500px;
@media (max-width: 900px) {
grid-template-columns: 1fr;
}
`;
export const LeftPanel = styled.div`
display: flex;
flex-direction: column;
gap: 1rem;
`;
export const RightPanel = styled.div`
display: flex;
flex-direction: column;
gap: 1.5rem;
`;
// ============================================================================
// Identity Selector Components
// ============================================================================
export const IdentityGrid = styled.div`
display: flex;
flex-direction: column;
gap: 0.75rem;
max-height: 400px;
overflow-y: auto;
padding-right: 0.5rem;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 105, 180, 0.4);
border-radius: 2px;
}
`;
export const IdentityCard = styled.div<{ $selected?: boolean }>`
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.4);
border: 2px solid ${(p) => (p.$selected ? '#ff69b4' : 'transparent')};
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: ${(p) =>
p.$selected ? '#ff69b4' : 'rgba(255, 105, 180, 0.5)'};
transform: translateY(-2px);
}
${(p) =>
p.$selected &&
css`
box-shadow: 0 0 20px rgba(255, 105, 180, 0.2);
`}
`;
export const IdentityAvatar = styled.div`
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #ff69b4, #9b59b6);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: 600;
color: #fff;
flex-shrink: 0;
`;
export const IdentityInfo = styled.div`
flex: 1;
min-width: 0;
`;
export const IdentityName = styled.div`
font-weight: 600;
color: #fff;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
export const IdentityMeta = styled.div`
color: #888;
font-size: 0.75rem;
margin-top: 0.125rem;
`;
export const SelectedBadge = styled.span`
color: #22c55e;
font-size: 0.75rem;
margin-left: 0.5rem;
`;
export const AddIdentityButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
background: transparent;
border: 2px dashed rgba(255, 255, 255, 0.2);
border-radius: 12px;
color: #888;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: rgba(255, 105, 180, 0.5);
color: #ff69b4;
}
`;
// ============================================================================
// Scene Builder Components
// ============================================================================
export const FormSection = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
`;
export const FormLabel = styled.label`
font-size: 0.875rem;
font-weight: 500;
color: #ccc;
`;
export const FormInput = styled.input`
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(0, 0, 0, 0.3);
color: #fff;
font-size: 0.875rem;
transition: border-color 0.2s;
&::placeholder {
color: #666;
}
&:focus {
outline: none;
border-color: #ff69b4;
}
`;
export const FormTextArea = styled.textarea`
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(0, 0, 0, 0.3);
color: #fff;
font-size: 0.875rem;
resize: vertical;
min-height: 100px;
font-family: inherit;
transition: border-color 0.2s;
&::placeholder {
color: #666;
}
&:focus {
outline: none;
border-color: #ff69b4;
}
`;
export const FormSelect = styled.select`
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(0, 0, 0, 0.3);
color: #fff;
font-size: 0.875rem;
cursor: pointer;
transition: border-color 0.2s;
&:focus {
outline: none;
border-color: #ff69b4;
}
option {
background: #1a1a2e;
color: #fff;
}
`;
export const SliderContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.25rem;
`;
export const SliderRow = styled.div`
display: flex;
align-items: center;
gap: 1rem;
`;
export const Slider = styled.input.attrs({ type: 'range' })`
flex: 1;
height: 4px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.2);
appearance: none;
cursor: pointer;
&::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg, #ff69b4, #9b59b6);
cursor: pointer;
transition: transform 0.1s;
}
&::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
`;
export const SliderValue = styled.span`
font-size: 0.875rem;
font-weight: 600;
color: #ff69b4;
min-width: 40px;
text-align: right;
`;
export const AdvancedToggle = styled.button`
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
background: transparent;
border: none;
color: #888;
font-size: 0.75rem;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #ff69b4;
}
`;
export const AdvancedSection = styled.div<{ $open?: boolean }>`
display: ${(p) => (p.$open ? 'flex' : 'none')};
flex-direction: column;
gap: 1rem;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
margin-top: 0.5rem;
`;
// ============================================================================
// Generation Preview Components
// ============================================================================
export const PreviewContainer = styled.div`
display: flex;
flex-direction: column;
gap: 1rem;
`;
export const PreviewImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
max-width: 512px;
background: rgba(0, 0, 0, 0.4);
border-radius: 12px;
overflow: hidden;
`;
export const PreviewImage = styled.img`
width: 100%;
height: 100%;
object-fit: cover;
`;
export const PreviewPlaceholder = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
font-size: 0.875rem;
`;
export const GeneratingOverlay = styled.div`
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
gap: 1rem;
`;
export const GeneratingText = styled.div`
color: #fff;
font-size: 0.875rem;
animation: ${pulse} 1.5s ease-in-out infinite;
`;
export const GeneratingProgress = styled.div`
width: 200px;
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
overflow: hidden;
&::after {
content: '';
display: block;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
#ff69b4,
transparent
);
animation: ${shimmer} 1.5s infinite;
background-size: 200% 100%;
}
`;
// ============================================================================
// Verification Badge
// ============================================================================
export const VerificationBadge = styled.div<{ $score: number }>`
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
background: ${(p) =>
p.$score >= 0.8
? 'rgba(34, 197, 94, 0.2)'
: p.$score >= 0.6
? 'rgba(234, 179, 8, 0.2)'
: 'rgba(239, 68, 68, 0.2)'};
border: 1px solid
${(p) =>
p.$score >= 0.8
? '#22c55e'
: p.$score >= 0.6
? '#eab308'
: '#ef4444'};
color: ${(p) =>
p.$score >= 0.8 ? '#22c55e' : p.$score >= 0.6 ? '#eab308' : '#ef4444'};
`;
export const ResultStats = styled.div`
display: flex;
flex-wrap: wrap;
gap: 1rem;
`;
export const ResultStat = styled.div`
display: flex;
flex-direction: column;
gap: 0.125rem;
`;
export const ResultStatLabel = styled.span`
font-size: 0.75rem;
color: #888;
`;
export const ResultStatValue = styled.span`
font-size: 0.875rem;
font-weight: 500;
color: #fff;
`;
export const ResultActions = styled.div`
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
`;
// ============================================================================
// Buttons
// ============================================================================
export const GenerateButton = styled.button<{ $disabled?: boolean }>`
padding: 1rem 2rem;
border-radius: 12px;
border: none;
background: linear-gradient(135deg, #ff69b4, #9b59b6);
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
&:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(255, 105, 180, 0.4);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
export const ActionButton = styled.button`
padding: 0.5rem 1rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 105, 180, 0.5);
}
`;
// ============================================================================
// Error Display
// ============================================================================
export const ErrorMessage = styled.div`
padding: 0.75rem 1rem;
border-radius: 8px;
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #ef4444;
font-size: 0.875rem;
`;
// ============================================================================
// Section Headers
// ============================================================================
export const SectionHeader = styled.h3`
font-size: 0.875rem;
font-weight: 600;
color: #ccc;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
`;