diff --git a/AGENTS.md b/AGENTS.md index 1995304..58422fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -173,6 +173,16 @@ const value = - In TypeScript and TSX, keep short ternary expressions on one line when they fit cleanly under the line limit. +- In TypeScript and TSX, prefer ternary expressions for simple conditional + value selection. Do not replace a clear ternary with `if` statements, and do + not introduce immediately invoked functions just to avoid or reformat a + ternary expression. +- In TypeScript and TSX, do not write `let` followed by later `if` assignments + when the value can be expressed as a single `const` initializer. Prefer + `const` because it prevents accidental later reassignment. +- When fixing formatting, change formatting only. Do not change expression + structure, control flow, or variable mutability unless the requested style + explicitly requires it. - Do not add production dependencies without explicit approval. - Do not create, modify, or run tests unless the user explicitly asks for test work. When the user asks for tests, keep working and rerun them until diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 56535f5..2cbe7a5 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -25,6 +25,7 @@ import { gekanatorKeys } from '@/lib/queryKeys' import { cn } from '@/lib/utils' import type { FC } from 'react' +import type { Transition } from 'framer-motion' import type { GekanatorAnswerLog, GekanatorAnswerValue, @@ -243,16 +244,23 @@ const normalizeStoredQuestionId = ( const normalizeStoredGame = (game: StoredGekanatorGame): StoredGekanatorGame => ({ ...game, - answers: game.answers.map (answer => ({ - ...answer, - questionId: normalizeStoredQuestionId (answer.questionId, - answer.questionCondition), - questionMode: ((answer.questionMode === 'winning_run' || answer.questionMode === 'normal') - ? answer.questionMode - : undefined), - questionCondition: (answer.questionCondition - ? normalizeTitleLengthCondition (answer.questionCondition) - : undefined) })), + answers: game.answers.map (answer => { + const questionMode = + answer.questionMode === 'winning_run' || answer.questionMode === 'normal' + ? answer.questionMode + : undefined + const questionCondition = + answer.questionCondition + ? normalizeTitleLengthCondition (answer.questionCondition) + : undefined + + return { + ...answer, + questionId: normalizeStoredQuestionId (answer.questionId, + answer.questionCondition), + questionMode, + questionCondition } + }), askedIds: game.askedIds.map (questionId => normalizeStoredQuestionId (questionId)), softenedQuestionIds: (game.softenedQuestionIds .map (questionId => normalizeStoredQuestionId (questionId))), @@ -474,10 +482,12 @@ const questionCategoryPenalty = ( repeatPenalty: number, ): number => { const earlyFactor = Math.max (0, (3 - answerCount) / 3) - const titleLengthPenalty = - titleLengthMinimumForCondition (question.condition) == null - ? 0 - : (answerCount === 0 ? 8 : 3.5) * earlyFactor + const titleLengthPenalty = (() => { + if (titleLengthMinimumForCondition (question.condition) == null) + return 0 + + return (answerCount === 0 ? 8 : 3.5) * earlyFactor + }) () switch (question.kind) { @@ -649,21 +659,21 @@ const buildMaterialIndex = ( const originalValue = post.originalCreatedFrom || post.originalCreatedBefore const date = originalValue - ? new Date (originalValue) - : null + ? new Date (originalValue) + : null const validDate = date && !(Number.isNaN (date.getTime ())) - ? date - : null + ? date + : null const originalYear = validDate?.getFullYear () ?? null const originalMonth = validDate - ? validDate.getMonth () + 1 - : null + ? validDate.getMonth () + 1 + : null const originalMonthDay = validDate - ? `${ validDate.getMonth () + 1 }-${ validDate.getDate () }` - : null + ? `${ validDate.getMonth () + 1 }-${ validDate.getDate () }` + : null originalYearByPostId.set (post.id, originalYear) originalMonthByPostId.set (post.id, originalMonth) originalMonthDayByPostId.set (post.id, originalMonthDay) @@ -758,10 +768,10 @@ const humanPriorityOffsetFor = (question: GekanatorQuestion): number => { case 'source': return -2.5 case 'post_similarity': - return ( - question.source === 'user_suggested' || question.source === 'admin_curated' - ? -3.5 - : -1.5) + if (question.source === 'user_suggested' || question.source === 'admin_curated') + return -3.5 + + return -1.5 case 'original_date': switch (question.condition.type) { @@ -1197,10 +1207,12 @@ const buildQuestionsForCandidateIds = ( confirmationPostId?: number | null }, ): GekanatorQuestion[] => { const total = candidateIds.length - const confirmationPost = - confirmationPostId == null - ? null - : materialIndex.postById.get (confirmationPostId) ?? null + const confirmationPost = (() => { + if (confirmationPostId == null) + return null + + return materialIndex.postById.get (confirmationPostId) ?? null + }) () if (mode === 'split' && total === 0) return acceptedQuestions @@ -1251,17 +1263,23 @@ const buildQuestionsForCandidateIds = ( GekanatorQuestionCondition, { type: 'original-year' | 'original-month' | 'original-month-day' } >, - ): GekanatorQuestion => buildIndexedQuestion ({ + ): GekanatorQuestion => { + const priorityWeight = (() => { + if (condition.type === 'original-year') + return 1.04 + if (condition.type === 'original-month-day') + return 1.01 + + return .92 + }) () + + return buildIndexedQuestion ({ condition, text: originalDateQuestionTextFor (condition), kind: 'original_date', - priorityWeight: - condition.type === 'original-year' - ? 1.04 - : condition.type === 'original-month-day' - ? 1.01 - : .92, + priorityWeight, materialIndex }) + } const specialMonthDays = rankedEntriesForCounts ({ counts: monthDayCounts, total, @@ -1342,27 +1360,21 @@ const buildQuestionsForCandidateIds = ( const titleLength = materialIndex.titleLengthByPostId.get (targetPostId) ?? 0 const tagKeys = materialIndex.tagKeysByPostId.get (targetPostId) ?? [] - addQuestion ( - host - ? buildIndexedQuestion ({ - condition: { type: 'source', host }, - text: `${ host } の投稿を思い浮かべている?`, - kind: 'source', - priorityWeight: 1.02, - materialIndex }) - : null) - addQuestion ( - year != null && year != undefined - ? buildDateQuestion ({ - type: 'original-year', - year }) - : null) - addQuestion ( - monthDay && specialOriginalMonthDayLabelFor (monthDay) - ? buildDateQuestion ({ - type: 'original-month-day', - monthDay }) - : null) + if (host) + addQuestion (buildIndexedQuestion ({ + condition: { type: 'source', host }, + text: `${ host } の投稿を思い浮かべている?`, + kind: 'source', + priorityWeight: 1.02, + materialIndex })) + if (year != null) + addQuestion (buildDateQuestion ({ + type: 'original-year', + year })) + if (monthDay && specialOriginalMonthDayLabelFor (monthDay)) + addQuestion (buildDateQuestion ({ + type: 'original-month-day', + monthDay })) tagKeys .slice (0, 20) @@ -1451,16 +1463,19 @@ const candidatePostsForState = ({ if (!(condition)) return true - const matched = question - ? matchingPostIdsForQuestion ({ + const matched = (() => { + if (question) + return matchingPostIdsForQuestion ({ posts, materialIndex, matchIndex, question, dynamicMatchIndex }) - : matchingPostIdsForCondition ({ - condition, - materialIndex }) + + return matchingPostIdsForCondition ({ + condition, + materialIndex }) + }) () const useExpectedAnswer = question != null && usesLearnedTagExamples (question) @@ -1477,9 +1492,7 @@ const candidatePostsForState = ({ || expected === answer.answer } - return answer.answer === 'yes' - ? matched.has (post.id) - : !(matched.has (post.id)) + return answer.answer === 'yes' ? matched.has (post.id) : !(matched.has (post.id)) } if (!(question)) @@ -1839,17 +1852,21 @@ const contradictionPenaltyFor = ({ case 'partial': return sum + (isExclusiveContradiction (question.condition, previous) ? 25 : 0) case 'no': - return sum + ( + if ( sameConditionValue (question.condition, previous) - || isMonthCrossMatch (question.condition, previous) - ? 40 - : 0) + || isMonthCrossMatch (question.condition, previous) + ) + return sum + 40 + + return sum case 'probably_no': - return sum + ( + if ( sameConditionValue (question.condition, previous) - || isMonthCrossMatch (question.condition, previous) - ? 20 - : 0) + || isMonthCrossMatch (question.condition, previous) + ) + return sum + 20 + + return sum default: return sum } @@ -2003,28 +2020,34 @@ const chooseQuestion = ( const humanOffset = humanPriorityOffsetFor (question) const sourceBonus = sourcePriorityOffset (question) const priorityBonus = priorityWeightOffset (question) - const repeatPenalty = - answers.length === 0 - ? (recentFirstQuestionPenaltyById.get (question.id) ?? 0) * 4.5 - : 0 + const repeatPenalty = (() => { + if (answers.length === 0) + return (recentFirstQuestionPenaltyById.get (question.id) ?? 0) * 4.5 + + return 0 + }) () const categoryPenalty = questionCategoryPenalty ( question, answers.length, repeatPenalty) - const priorSplitScore = - priorWeightTotal <= 0 - ? null - : Math.abs ( - .5 - ( - priorEntries.reduce ( - (sum, [postId, weight]) => { - return sum + (matched.has (postId) ? weight : 0) - }, - 0) / priorWeightTotal)) - const priorBonus = - priorSplitScore == null - ? 0 - : Math.max (0, .22 - priorSplitScore) * -18 + const priorSplitScore = (() => { + if (priorWeightTotal <= 0) + return null + + return Math.abs ( + .5 - ( + priorEntries.reduce ( + (sum, [postId, weight]) => { + return sum + (matched.has (postId) ? weight : 0) + }, + 0) / priorWeightTotal)) + }) () + const priorBonus = (() => { + if (priorSplitScore == null) + return 0 + + return Math.max (0, .22 - priorSplitScore) * -18 + }) () const infoGainBonus = -Math.min (1.2, infoGain) * 4 return { question, @@ -2052,9 +2075,7 @@ const chooseQuestion = ( questions.filter (question => !(askedIds.has (question.id))) const ranked = rank (unansweredQuestions, scoredPosts, normalisedWeightedPosts) const pool = ( - ranked.some (item => !(item.narrow)) - ? ranked.filter (item => !(item.narrow)) - : ranked) + ranked.some (item => !(item.narrow)) ? ranked.filter (item => !(item.narrow)) : ranked) .slice (0, 16) if (pool.length === 0) @@ -2133,10 +2154,7 @@ const chooseWinningRunQuestion = ({ }) .map (question => { const expected = expectedAnswerForQuestion (question, targetPost) - const priority = - expected == null - ? null - : winningRunPriorityFor (expected) + const priority = expected == null ? null : winningRunPriorityFor (expected) if (priority == null) return null @@ -2148,9 +2166,7 @@ const chooseWinningRunQuestion = ({ question, dynamicMatchIndex }) const matchingCount = - expected === 'yes' || expected === 'partial' - ? yesCount - : posts.length - yesCount + expected === 'yes' || expected === 'partial' ? yesCount : posts.length - yesCount return { question, @@ -2291,12 +2307,15 @@ const isWinningRunActive = ( const winningRunQuestionCount = ( answers: GekanatorAnswerLog[], winningRunStartAnswerCount: number | null, -): number => winningRunStartAnswerCount == null - ? 0 - : answers - .slice (winningRunStartAnswerCount) - .filter (answer => answer.questionMode === 'winning_run') - .length +): number => { + if (winningRunStartAnswerCount == null) + return 0 + + return answers + .slice (winningRunStartAnswerCount) + .filter (answer => answer.questionMode === 'winning_run') + .length +} const nextQuestionPlanFor = ( @@ -2335,10 +2354,7 @@ const nextQuestionPlanFor = ( questionMode: QuestionMode winningRunTargetId: number | null winningRunStartAnswerCount: number | null } => { - const guessablePosts = - eligiblePosts.length > 0 - ? eligiblePosts - : availablePosts + const guessablePosts = eligiblePosts.length > 0 ? eligiblePosts : availablePosts const checkpointGuess = answers.length > 0 @@ -2366,22 +2382,25 @@ const nextQuestionPlanFor = ( winningRunStartAnswerCount } } - const nextWinningRunTargetId = - eligiblePosts.length === 1 - ? eligiblePosts[0]?.id ?? null - : null - const nextWinningRunStartAnswerCount = - nextWinningRunTargetId == null - ? null - : ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount) - && winningRunTargetId === nextWinningRunTargetId - && winningRunStartAnswerCount != null) - ? winningRunStartAnswerCount - : answers.length) - const nextWinningRunTargetPost = - nextWinningRunTargetId == null - ? null - : posts.find (post => post.id === nextWinningRunTargetId) ?? null + const nextWinningRunTargetId = eligiblePosts.length === 1 ? eligiblePosts[0]?.id ?? null : null + const nextWinningRunStartAnswerCount = (() => { + if (nextWinningRunTargetId == null) + return null + if ( + isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount) + && winningRunTargetId === nextWinningRunTargetId + && winningRunStartAnswerCount != null + ) + return winningRunStartAnswerCount + + return answers.length + }) () + const nextWinningRunTargetPost = (() => { + if (nextWinningRunTargetId == null) + return null + + return posts.find (post => post.id === nextWinningRunTargetId) ?? null + }) () const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] => buildQuestionsForCandidateIds ({ candidateIds: scopePosts.map (post => post.id), @@ -2580,12 +2599,12 @@ const backgroundPostsFor = ({ }): Post[] => { const focusPosts = phase === 'end' || phase === 'review' || phase === 'learned' - ? [reviewCorrectPost, reviewGuessedPost].filter ((post): post is Post => post != null) - : phase === 'guess' - ? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post != null) - : eligiblePosts.length > 0 - ? eligiblePosts - : availablePosts + ? [reviewCorrectPost, reviewGuessedPost].filter ((post): post is Post => post != null) + : phase === 'guess' + ? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post != null) + : eligiblePosts.length > 0 + ? eligiblePosts + : availablePosts return [...new Map (focusPosts.map (post => [post.id, post])).values ()] } @@ -2634,20 +2653,20 @@ const GekanatorBackdrop: FC<{ { x: -33.333333, y: -33.333333 }], []) const guessThumbnail = - phase === 'guess' && displayedGuess - ? backgroundThumbnailUrl (displayedGuess) - : null + phase === 'guess' && displayedGuess ? backgroundThumbnailUrl (displayedGuess) : null const isWinningRunBackdrop = !(guessThumbnail) && phase === 'question' && winningRunTargetPost != null && Boolean (backgroundThumbnailUrl (winningRunTargetPost)) - const backdropMode = - guessThumbnail - ? 'guess' - : isWinningRunBackdrop - ? 'winning_run' - : 'normal' + const backdropMode = (() => { + if (guessThumbnail) + return 'guess' + if (isWinningRunBackdrop) + return 'winning_run' + + return 'normal' + }) () const normalVisiblePosts = useMemo ( () => posts @@ -2665,9 +2684,10 @@ const GekanatorBackdrop: FC<{ if (mode === 'winning_run' || mode === 'guess') return { columns: 8, rows: 8, opacity: motionMode === 'calm' ? .18 : .24 } - return motionMode === 'calm' - ? { columns: 7, rows: 7, opacity: .14 } - : { columns: 10, rows: 10, opacity: .2 } + if (motionMode === 'calm') + return { columns: 7, rows: 7, opacity: .14 } + + return { columns: 10, rows: 10, opacity: .2 } }, [motionMode]) @@ -2723,10 +2743,12 @@ const GekanatorBackdrop: FC<{ ?? directions[0], [visualSeed, directions]) - const marqueeDuration = - backdropMode === 'winning_run' - ? motionMode === 'calm' ? 28 : 20 - : motionMode === 'calm' ? 34 : 24 + const marqueeDuration = (() => { + if (backdropMode === 'winning_run') + return motionMode === 'calm' ? 28 : 20 + + return motionMode === 'calm' ? 34 : 24 + }) () const tileFlipDuration = motionMode === 'calm' ? .6 : .45 const x = useMotionValue (0) const y = useMotionValue (0) @@ -2944,6 +2966,13 @@ const GekanatorBackdrop: FC<{
) + const backdropTransition: Transition = (() => { + if (displayedBackdropMode === 'winning_run' || displayedBackdropMode === 'guess') + return { duration: motionMode === 'calm' ? .95 : .75, ease: [.16, 1, .3, 1] } + + return { duration: .2 } + }) () + return (
@@ -2958,11 +2987,7 @@ const GekanatorBackdrop: FC<{ animate={{ scale: renderedScale, x: displayedBackdropMode === 'guess' ? guessFocusOffset.x : '0%', y: displayedBackdropMode === 'guess' ? guessFocusOffset.y : '0%' }} - transition={(displayedBackdropMode === 'winning_run' - || displayedBackdropMode === 'guess') - ? { duration: motionMode === 'calm' ? .95 : .75, - ease: [.16, 1, .3, 1] } - : { duration: .2 }}> + transition={backdropTransition}> {Array.from ({ length: 9 }, (_, duplicate) => { const column = duplicate % 3 const row = Math.floor (duplicate / 3) @@ -2988,20 +3013,24 @@ const GekanatorBackdrop: FC<{ index % Math.max (displayedThumbnails.length, 1)] const frontThumbnail = isFlippingTiles - ? fromThumbnails[index % Math.max (fromThumbnails.length, 1)] - : currentThumbnail + ? fromThumbnails[index % Math.max (fromThumbnails.length, 1)] + : currentThumbnail const backThumbnail = isFlippingTiles - ? toThumbnails[index % Math.max (toThumbnails.length, 1)] - : currentThumbnail + ? toThumbnails[index % Math.max (toThumbnails.length, 1)] + : currentThumbnail const thumbnail = displayedBackdropMode === 'winning_run' || displayedBackdropMode === 'guess' - ? nextThumbnails[index % Math.max (nextThumbnails.length, 1)] - : currentThumbnail + ? nextThumbnails[index % Math.max (nextThumbnails.length, 1)] + : currentThumbnail if (!(thumbnail) || !(frontThumbnail) || !(backThumbnail)) return null + const imageSource = ['intro', 'end'].includes (phase) ? mascotAsset : thumbnail + const showStaticTile = + displayedBackdropMode !== 'normal' || !(isFlippingTiles) + return ( - {(displayedBackdropMode !== 'normal' || !(isFlippingTiles)) - ? ( + {showStaticTile && ( ) - : ( + } + {!(showStaticTile) && ( = ({ user }) => { const [askedQuestionBank, setAskedQuestionBank] = useState ( () => (storedGame?.askedQuestionBank ?? []).map (restoreGekanatorQuestion)) const [storedAskedQuestionBankIds, setStoredAskedQuestionBankIds] = useState ( - (storedGame?.askedQuestionBank?.length ?? 0) > 0 - ? [] - : storedGame?.askedQuestionBankIds ?? []) + () => { + if ((storedGame?.askedQuestionBank?.length ?? 0) > 0) + return [] + + return storedGame?.askedQuestionBankIds ?? [] + }) const [search, setSearch] = useState (storedGame?.search ?? '') const [selectingCorrectPost, setSelectingCorrectPost] = useState ( storedGame?.selectingCorrectPost ?? false) @@ -3397,9 +3427,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { userPriorWeights, materialIndex, acceptedQuestionMatchIndex, lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount]) const winningRunTargetPost = useMemo ( - () => questionPlan.winningRunTargetId == null - ? null - : posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null, + () => { + if (questionPlan.winningRunTargetId == null) + return null + + return posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null + }, [posts, questionPlan.winningRunTargetId]) const winningRunQuestionsAsked = winningRunQuestionCount ( answers, @@ -3420,21 +3453,21 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { [eligiblePosts, scores]) const currentQuestion = questionPlan.question const answerPreviews = useMemo ( - () => isAdmin && currentQuestion - ? answerOptions.map (option => previewAnswer ({ - posts: eligiblePosts, - scores, - question: currentQuestion, - answer: option.value, - materialIndex, - matchIndex: acceptedQuestionMatchIndex })) - : [], + () => { + if (!(isAdmin) || !(currentQuestion)) + return [] + + return answerOptions.map (option => previewAnswer ({ + posts: eligiblePosts, + scores, + question: currentQuestion, + answer: option.value, + materialIndex, + matchIndex: acceptedQuestionMatchIndex })) + }, [isAdmin, currentQuestion, eligiblePosts, materialIndex, acceptedQuestionMatchIndex, scores]) - const guessablePosts = - eligiblePosts.length > 0 - ? eligiblePosts - : availablePosts + const guessablePosts = eligiblePosts.length > 0 ? eligiblePosts : availablePosts const guessConfidences = useMemo ( () => confidencesFor (guessablePosts, scores), [guessablePosts, scores]) @@ -3446,17 +3479,22 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { posts.find (post => post.id === reviewGuessedPostId) ?? null const reviewCorrectPost = posts.find (post => post.id === reviewCorrectPostId) ?? null - const effectiveResultWon = - resultWon - ?? ((reviewGuessedPostId != null && reviewCorrectPostId != null) - ? reviewGuessedPostId === reviewCorrectPostId - : null) - const effectiveBackgroundMotionMode = - backgroundMotionMode === 'off' - ? 'off' - : (prefersReducedMotion - ? 'calm' - : backgroundMotionMode) + const effectiveResultWon = (() => { + if (resultWon != null) + return resultWon + if (reviewGuessedPostId == null || reviewCorrectPostId == null) + return null + + return reviewGuessedPostId === reviewCorrectPostId + }) () + const effectiveBackgroundMotionMode = (() => { + if (backgroundMotionMode === 'off') + return 'off' + if (prefersReducedMotion) + return 'calm' + + return backgroundMotionMode + }) () const backgroundPosts = useMemo ( () => backgroundPostsFor ({ phase, @@ -3584,10 +3622,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { let recoveredStepCount = nextRecoveryStepCount const nextAskedQuestionById = new Map (nextAskedQuestionBank.map (question => [question.id, question])) - const answerCountAtRecovery = - allowPreQuestionRecovery - ? nextAnswers.length - : Math.max (nextAnswers.length - 1, 0) + const answerCountAtRecovery = (() => { + if (allowPreQuestionRecovery) + return nextAnswers.length + + return Math.max (nextAnswers.length - 1, 0) + }) () let recoveredScores = recalculateScores ({ posts, questions: nextAskedQuestionBank, @@ -3863,12 +3903,14 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { } const finishGame = (correctPostId: number) => { - const guessedPostId = - phase === 'end' || phase === 'review' - ? reviewGuessedPostId - : phase === 'continue' - ? lastRejectedGuessId ?? displayedGuess?.id - : displayedGuess?.id ?? lastRejectedGuessId + const guessedPostId = (() => { + if (phase === 'end' || phase === 'review') + return reviewGuessedPostId + if (phase === 'continue') + return lastRejectedGuessId ?? displayedGuess?.id + + return displayedGuess?.id ?? lastRejectedGuessId + }) () if (!(guessedPostId)) return @@ -4198,13 +4240,18 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const resultDialogue = effectiveResultWon ? winDialogue : loseDialogue const dialogue = phase === 'learned' ? resultDialogue : introDialogue - const saveStatusMessage = - saved - && learnedExampleCount != null - ? learnedExampleCount > 0 - ? `${ learnedExampleCount }件の回答を学習しました` - : null - : null + const introLoadingMessage = + phase === 'intro' ? '投稿を読み込んでいます……' : '前回のグカネータ状態を復元しています……' + const questionSuggestionTitle = + questionSuggestionEntryMode === 'search' ? 'まず既存質問を探してください。' : '新しい質問を追加します。' + const saveStatusMessage = (() => { + if (!(saved) || learnedExampleCount == null) + return null + if (learnedExampleCount <= 0) + return null + + return `${ learnedExampleCount }件の回答を学習しました` + }) () const introLoading = isLoading || acceptedQuestionsLoading const readyToStart = @@ -4323,18 +4370,23 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {[{ mode: 'off' as const, label: 'オフ' }, { mode: 'on' as const, label: 'オン' }] - .map (({ mode, label }) => ( - ))} + .map (({ mode, label }) => { + const modeClass = + backgroundMotionMode === mode + ? 'bg-pink-600 text-white' + : 'text-neutral-600 hover:bg-yellow-100 dark:text-neutral-300 dark:hover:bg-red-900' + + return ( + ) + })} {prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && ( 端末設定により控えめ表示 @@ -4365,9 +4417,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {introLoading && (

- {phase === 'intro' - ? '投稿を読み込んでいます……' - : '前回のグカネータ状態を復元しています……'} + {introLoadingMessage}

)} {(Boolean (error) || Boolean (acceptedQuestionsError)) &&

グカネータの質問データを読み込めませんでした.

} @@ -4602,9 +4652,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
正解の投稿
- {reviewCorrectPost - ? - :

正解投稿を選んでください。

} + {reviewCorrectPost && } + {!(reviewCorrectPost) && ( +

正解投稿を選んでください。

)} ))} + {searchableSuggestedQuestions.map (question => { + const questionClass = + questionSuggestionSelectedId === (question.recordId ?? null) + ? 'border-pink-600 bg-pink-50 dark:bg-red-900/50' + : 'border-yellow-200 hover:bg-yellow-100 dark:border-red-800 dark:hover:bg-red-900' + + return ( + ) + })}
)} {selectedSuggestedQuestion && ( @@ -4873,7 +4923,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
)} ) - : ( + } + {questionSuggestionEntryMode === 'new' && (
新規質問