pokedex-web / frontend /src /components /ResultCard.js
gpimentel's picture
feat: integrate BiRefNet model with LFS tracking for v1.2.0
81faab5
Raw
History Blame Contribute Delete
7.4 kB
'use client';
import { useState } from 'react';
import confetti from 'canvas-confetti';
import styles from './ResultCard.module.css';
import TypeBadge from './TypeBadge';
import ConfidenceBar from './ConfidenceBar';
import StatsRadar from './StatsRadar';
import TopPredictions from './TopPredictions';
import FeedbackWidget from './FeedbackWidget';
export default function ResultCard({ predictionData, pokemonDetails, originalImage, onReset, onShinyActivated, onDislikeActivated }) {
const [showShiny, setShowShiny] = useState(false);
if (!predictionData) return null;
const { pokemon_id, name, confidence, top_5 } = predictionData;
const types = pokemonDetails?.types || [];
const stats = pokemonDetails?.stats || {};
const spriteUrl = showShiny
? `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/${pokemon_id}.png`
: pokemonDetails?.sprite_url;
return (
<div className={`glass-panel ${styles.card}`}>
<div className={styles.header}>
<div className={styles.titleArea}>
<h2 className={styles.name}>{name}</h2>
<span className={`${styles.idBadge} mono`}>#{String(pokemon_id).padStart(4, '0')}</span>
</div>
<button className={styles.closeBtn} onClick={onReset} aria-label="Try another"></button>
</div>
{predictionData.status === 'UNCERTAIN_POKEMON' && (
<div style={{ backgroundColor: 'rgba(255, 165, 0, 0.1)', border: '1px solid rgba(255, 165, 0, 0.3)', borderLeft: '4px solid orange', padding: '1rem', margin: '0 2rem 1.5rem 2rem', borderRadius: '8px', display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
<span style={{ fontSize: '1.5rem', lineHeight: '1' }}>⚠️</span>
<span style={{ color: 'rgba(255, 255, 255, 0.9)', fontSize: '0.95rem', lineHeight: '1.4' }}>
<strong style={{ color: 'orange', display: 'block', marginBottom: '0.25rem' }}>Uncertainty Warning</strong>
Unfortunately, PokedexNet cannot guarantee this prediction. While this is our best guess, the silhouette is heavily occluded, noisy, or atypical, triggering an epistemic disagreement in our ensemble.
</span>
</div>
)}
<div className={styles.content}>
<div className={styles.visualColumn}>
<div className={`${styles.spriteContainer} ${styles.stagger1}`}>
{spriteUrl ? (
<img
src={spriteUrl}
alt={name}
className={styles.sprite}
/>
) : (
<div className={styles.noSprite}>?</div>
)}
</div>
</div>
<div className={styles.dataColumn}>
<div className={`${styles.typesRow} ${styles.stagger1}`}>
{types.map(type => (
<TypeBadge key={type} type={type} />
))}
{showShiny && (
<span style={{
display: 'inline-block',
padding: '4px 16px',
borderRadius: '20px',
fontSize: '0.85rem',
fontWeight: '700',
letterSpacing: '1px',
marginRight: '8px',
marginBottom: '8px',
backdropFilter: 'blur(4px)',
backgroundColor: 'color-mix(in srgb, #FFD700 20%, transparent)',
color: '#FFD700',
border: '1px solid color-mix(in srgb, #FFD700 50%, transparent)'
}}>
SHINY
</span>
)}
</div>
<div className={styles.stagger2}>
<ConfidenceBar confidence={confidence} />
</div>
<div className={styles.stagger3}>
<h3 className={styles.sectionTitle}>Base Stats</h3>
<StatsRadar stats={stats} typeColor={`var(--type-${(types[0] || 'normal').toLowerCase()})`} />
</div>
<div className={styles.stagger4}>
<TopPredictions predictions={top_5} />
</div>
<div className={styles.stagger4}>
<FeedbackWidget
imageHash={predictionData.image_hash}
predictedId={pokemon_id}
top5={top_5}
onFeedbackSuccess={(isCorrect) => {
if (isCorrect) {
setShowShiny(true);
if (onShinyActivated) {
onShinyActivated();
}
const defaults = {
spread: 360,
ticks: 50,
gravity: 0,
decay: 0.94,
startVelocity: 24,
shapes: ['star'],
colors: ['FFE400', 'FFBD00', 'E89400', 'FFCA6C', 'FDFFB8']
};
function shoot() {
confetti({
...defaults,
particleCount: 30,
scalar: 1.1,
shapes: ['star']
});
confetti({
...defaults,
particleCount: 8,
scalar: 0.6,
shapes: ['circle']
});
}
setTimeout(shoot, 0);
setTimeout(shoot, 100);
setTimeout(shoot, 200);
} else {
if (onDislikeActivated) {
onDislikeActivated();
}
}
}}
/>
</div>
</div>
</div>
{/* Debug Mode Comparison Section */}
{(predictionData.debug_mode || predictionData.debug_silhouette) && (
<div className={styles.debugComparison}>
<h3 className={styles.debugTitle}>
{predictionData.detected_source === 'birefnet' ? 'BiRefNet' : 'SAM 2'} Silhouette Preprocessing (Debug Mode)
</h3>
<div className={styles.comparisonGrid}>
<div className={styles.imageBox}>
<span className={styles.imageLabel}>Original Input</span>
{originalImage ? (
<img src={originalImage} alt="Original Input" className={styles.debugImg} />
) : (
<div className={styles.noImage}>No original image available</div>
)}
</div>
<div className={styles.arrowIcon}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div className={styles.imageBox}>
<span className={styles.imageLabel}>
{predictionData.detected_source === 'birefnet' ? 'BiRefNet' : 'SAM 2'} Silhouette
</span>
{predictionData.debug_silhouette ? (
<img src={`data:image/png;base64,${predictionData.debug_silhouette}`} alt="Active Silhouette" className={styles.debugImg} />
) : (
<div className={styles.noImage}>No silhouette available</div>
)}
</div>
</div>
</div>
)}
</div>
);
}