pokedex-web / frontend /src /components /FeedbackWidget.js
gpimentel's picture
feat: enhance image upload validation and feedback storage for model improvement
960a6a9
Raw
History Blame Contribute Delete
4.96 kB
'use client';
import { useState } from 'react';
import styles from './FeedbackWidget.module.css';
export default function FeedbackWidget({ imageHash, predictedId, top5, isOodScreen, onFeedbackSuccess }) {
const [feedbackState, setFeedbackState] = useState('idle');
const [errorMessage, setErrorMessage] = useState('');
const sendFeedback = async (isCorrect, userCorrectionId = null) => {
setFeedbackState('submitting');
try {
const response = await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_hash: imageHash,
predicted_id: predictedId,
is_correct: isCorrect,
user_correction_id: userCorrectionId,
is_ood_screen: isOodScreen || false
})
});
if (!response.ok) {
if (response.status === 429) {
throw new Error("Muitas requisições. Tente novamente mais tarde.");
}
throw new Error("Erro ao salvar feedback");
}
setFeedbackState('success');
if (onFeedbackSuccess) {
onFeedbackSuccess(isCorrect);
}
setTimeout(() => {
setFeedbackState('hidden');
}, 3000);
} catch (error) {
setFeedbackState('error');
setErrorMessage(error.message);
setTimeout(() => setFeedbackState('idle'), 4000);
}
};
if (feedbackState === 'hidden') {
return null;
}
if (feedbackState === 'success') {
return (
<div className={styles.successMessage}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{marginRight: '8px', color: '#4caf50'}}><polyline points="20 6 9 17 4 12"></polyline></svg>
Feedback sent, thank you!
</div>
);
}
return (
<div className={styles.feedbackContainer}>
{feedbackState === 'idle' || feedbackState === 'error' ? (
<div className={styles.voteButtons}>
<span className={styles.prompt}>
{isOodScreen ? "Did PokedexNet get this right?" : "Was this prediction accurate?"}
</span>
<button
className={`${styles.voteBtn} ${styles.likeBtn}`}
onClick={() => sendFeedback(true, isOodScreen ? 'none' : null)}
title={isOodScreen ? "Yes, it is NOT a Pokémon" : "Yes, it's correct"}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>
</button>
<button
className={`${styles.voteBtn} ${styles.dislikeBtn}`}
onClick={() => isOodScreen ? sendFeedback(false, 'unknown_pokemon') : setFeedbackState('dislike_menu')}
title={isOodScreen ? "No, it actually IS a Pokémon" : "No, it's wrong"}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>
</button>
{feedbackState === 'error' && <span className={styles.errorText}>{errorMessage}</span>}
</div>
) : feedbackState === 'submitting' ? (
<div className={styles.loadingMessage}>Sending feedback...</div>
) : feedbackState === 'dislike_menu' ? (
<div className={styles.dislikeMenu}>
<span className={styles.prompt}>What Pokémon is it really?</span>
<div className={styles.optionsGrid}>
{top5 && top5.slice(1).map(pred => (
<button
key={pred.pokemon_id}
className={styles.optionBtn}
onClick={() => sendFeedback(false, pred.pokemon_id)}
>
{pred.name}
</button>
))}
<button
className={`${styles.optionBtn} ${styles.noneBtn}`}
onClick={() => sendFeedback(false, 'none')}
>
None of these
</button>
</div>
<button
className={styles.cancelBtn}
onClick={() => setFeedbackState('idle')}
>
Cancel
</button>
</div>
) : null}
{(feedbackState === 'idle' || feedbackState === 'error' || feedbackState === 'dislike_menu') && (
<span className={styles.privacyNotice}>
By submitting feedback, your image may be anonymously stored for model improvement.
</span>
)}
</div>
);
}