Spaces:
Sleeping
Sleeping
| '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> | |
| ); | |
| } | |