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 (- {phase === 'intro' - ? '投稿を読み込んでいます……' - : '前回のグカネータ状態を復元しています……'} + {introLoadingMessage}
)} {(Boolean (error) || Boolean (acceptedQuestionsError)) &&グカネータの質問データを読み込めませんでした.
} @@ -4602,9 +4652,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {正解投稿を選んでください。
} + {reviewCorrectPost &&正解投稿を選んでください。
)}