feat: Implement AI-powered comment rewriting and a collapsible comment form for user feedback.

This commit is contained in:
Hördle Bot
2025-12-04 08:54:25 +01:00
parent b204a35628
commit 7db4e26b2c
6 changed files with 933 additions and 768 deletions

View File

@@ -65,6 +65,8 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
const [commentSending, setCommentSending] = useState(false);
const [commentSent, setCommentSent] = useState(false);
const [commentError, setCommentError] = useState<string | null>(null);
const [commentCollapsed, setCommentCollapsed] = useState(true);
const [rewrittenMessage, setRewrittenMessage] = useState<string | null>(null);
useEffect(() => {
const updateCountdown = () => {
@@ -139,7 +141,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
} else {
setHasRated(false);
}
// Check if comment already sent for this puzzle
const playerIdentifier = getOrCreatePlayerId();
if (playerIdentifier) {
@@ -220,7 +222,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
const handleSkip = () => {
// Prevent skipping if already solved or failed
if (gameState?.isSolved || gameState?.isFailed) return;
// If user hasn't played audio yet on first attempt, start it instead of skipping
if (gameState.guesses.length === 0 && !hasPlayedAudio) {
handleStartAudio();
@@ -251,7 +253,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
const handleGiveUp = () => {
// Prevent giving up if already solved or failed
if (gameState?.isSolved || gameState?.isFailed) return;
setLastAction('SKIP');
addGuess("SKIPPED", false);
giveUp(); // Ensure game is marked as failed and score reset to 0
@@ -321,6 +323,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
setCommentSending(true);
setCommentError(null);
setRewrittenMessage(null);
try {
const playerIdentifier = getOrCreatePlayerId();
@@ -328,6 +331,26 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
throw new Error('Could not get player identifier');
}
// 1. Rewrite message using AI
const rewriteResponse = await fetch('/api/rewrite-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: commentText.trim() })
});
let finalMessage = commentText.trim();
if (rewriteResponse.ok) {
const rewriteData = await rewriteResponse.json();
if (rewriteData.rewrittenMessage) {
finalMessage = rewriteData.rewrittenMessage;
// If message was changed significantly (simple check), show it
if (finalMessage !== commentText.trim()) {
setRewrittenMessage(finalMessage);
}
}
}
// 2. Send comment
// For specials, genreId should be null. For global, also null. For genres, we pass null and let API determine from puzzle
const genreId = isSpecial ? null : null; // API will determine from puzzle
@@ -339,7 +362,8 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
body: JSON.stringify({
puzzleId: dailyPuzzle.id,
genreId: genreId,
message: commentText.trim(),
message: finalMessage,
originalMessage: commentText.trim() !== finalMessage ? commentText.trim() : undefined,
playerIdentifier: playerIdentifier
})
});
@@ -352,7 +376,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
setCommentSent(true);
setCommentText('');
// Store in localStorage that comment was sent
const commentedPuzzles = JSON.parse(localStorage.getItem(`${config.appName.toLowerCase()}_commented_puzzles`) || '[]');
if (!commentedPuzzles.includes(dailyPuzzle.id)) {
@@ -602,60 +626,84 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
{/* Comment Form */}
{!commentSent && (
<div style={{ marginTop: '1.5rem', padding: '1rem', background: 'rgba(255,255,255,0.5)', borderRadius: '0.5rem' }}>
<h3 style={{ fontSize: '1rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>
{t('sendComment')}
</h3>
<p style={{ fontSize: '0.85rem', color: 'var(--muted-foreground)', marginBottom: '0.75rem' }}>
{t('commentHelp')}
</p>
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder={t('commentPlaceholder')}
maxLength={2000}
<div
onClick={() => setCommentCollapsed(!commentCollapsed)}
style={{
width: '100%',
minHeight: '100px',
padding: '0.75rem',
borderRadius: '0.5rem',
border: '1px solid var(--border)',
fontSize: '0.9rem',
fontFamily: 'inherit',
resize: 'vertical',
marginBottom: '0.5rem'
}}
disabled={commentSending}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--muted-foreground)' }}>
{commentText.length}/2000
</span>
{commentError && (
<span style={{ fontSize: '0.75rem', color: 'var(--danger)' }}>
{commentError}
</span>
)}
</div>
<button
onClick={handleCommentSubmit}
disabled={!commentText.trim() || commentSending || commentSent}
className="btn-primary"
style={{
width: '100%',
opacity: (!commentText.trim() || commentSending || commentSent) ? 0.5 : 1,
cursor: (!commentText.trim() || commentSending || commentSent) ? 'not-allowed' : 'pointer'
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
marginBottom: commentCollapsed ? 0 : '1rem'
}}
>
{commentSending ? t('sending') : t('sendComment')}
</button>
<h3 style={{ fontSize: '1rem', fontWeight: 'bold', margin: 0 }}>
{t('sendComment')}
</h3>
<span>{commentCollapsed ? '▼' : '▲'}</span>
</div>
{!commentCollapsed && (
<>
<p style={{ fontSize: '0.85rem', color: 'var(--muted-foreground)', marginBottom: '0.75rem' }}>
{t('commentHelp')}
</p>
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder={t('commentPlaceholder')}
maxLength={300}
style={{
width: '100%',
minHeight: '100px',
padding: '0.75rem',
borderRadius: '0.5rem',
border: '1px solid var(--border)',
fontSize: '0.9rem',
fontFamily: 'inherit',
resize: 'vertical',
marginBottom: '0.5rem',
display: 'block' // Ensure block display for proper alignment
}}
disabled={commentSending}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--muted-foreground)' }}>
{commentText.length}/300
</span>
{commentError && (
<span style={{ fontSize: '0.75rem', color: 'var(--danger)' }}>
{commentError}
</span>
)}
</div>
<button
onClick={handleCommentSubmit}
disabled={!commentText.trim() || commentSending || commentSent}
className="btn-primary"
style={{
width: '100%',
opacity: (!commentText.trim() || commentSending || commentSent) ? 0.5 : 1,
cursor: (!commentText.trim() || commentSending || commentSent) ? 'not-allowed' : 'pointer'
}}
>
{commentSending ? t('sending') : t('sendComment')}
</button>
</>
)}
</div>
)}
{commentSent && (
<div style={{ marginTop: '1.5rem', padding: '1rem', background: 'rgba(16, 185, 129, 0.1)', borderRadius: '0.5rem', border: '1px solid rgba(16, 185, 129, 0.3)' }}>
<p style={{ fontSize: '0.9rem', color: 'var(--success)', textAlign: 'center' }}>
<p style={{ fontSize: '0.9rem', color: 'var(--success)', textAlign: 'center', marginBottom: rewrittenMessage ? '0.5rem' : 0 }}>
{t('commentSent')}
</p>
{rewrittenMessage && (
<div style={{ fontSize: '0.85rem', color: 'var(--muted-foreground)', textAlign: 'center' }}>
<p style={{ marginBottom: '0.25rem' }}>{t('commentRewritten')}</p>
<p style={{ fontStyle: 'italic' }}>"{rewrittenMessage}"</p>
</div>
)}
</div>
)}