diff --git a/AGENTS.md b/AGENTS.md index a3a0ed1..1995304 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,6 +125,54 @@ npm run preview - TypeScript and TSX use 4-space logical indentation. - In TypeScript and TSX only, replace every leading run of 8 spaces with a tab. - Tabs are only for leading indentation, never for spaces after non-space text. +- TypeScript and TSX imports may stay on one line if they remain within the + line limit; do not expand short type-only imports mechanically. +- In TypeScript and TSX, when a function takes one destructured object + argument plus an inline type, prefer this shape when it fits locally: + +```ts +const helper = ( + { value, flag }: { value: string + flag: boolean }, +): Result => { + // ... +} +``` + +- In TypeScript and TSX, put `switch` case block braces on their own lines + when a case needs a lexical block: + +```ts +case 'yes': +case 'no': + { + const expected = valueFor (item) + return expected == null || expected === answer + } +``` + +- In TypeScript and TSX, use `value == null` and `value != null` as the + default nullish checks. Do not use `=== null`, `=== undefined`, + `!== null`, or `!== undefined`. +- If code appears to need a distinction between `null` and `undefined`, treat + that as a design smell and revise the logic to avoid the distinction. + External library APIs that explicitly require distinguishing the two are the + only exception. +- In TypeScript and TSX, keep short arrays on one line when they fit under the + line limit; break arrays only when readability or line length requires it. +- In TypeScript and TSX, when a ternary expression is split across multiple + lines, align `?` and `:` with the condition expression. Do not indent `?` and + `:` one extra level under the condition. + +```ts +const value = + condition + ? consequent + : alternate +``` + +- In TypeScript and TSX, keep short ternary expressions on one line when they + fit cleanly under the line limit. - 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/lib/gekanatorCandidateRecovery.ts b/frontend/src/lib/gekanatorCandidateRecovery.ts index 11f8eed..4d9cf93 100644 --- a/frontend/src/lib/gekanatorCandidateRecovery.ts +++ b/frontend/src/lib/gekanatorCandidateRecovery.ts @@ -1,43 +1,33 @@ import { expectedAnswerForQuestion } from '@/lib/gekanator' -import type { - GekanatorAnswerLog, - GekanatorAnswerValue, - GekanatorQuestion, -} from '@/lib/gekanator' +import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion } from '@/lib/gekanator' import type { Post } from '@/types' - export type RecoveredCandidatePost = { postId: number answerCountAtRecovery: number } -const questionIsFactLikeForHardFiltering = ( - question: GekanatorQuestion, -): boolean => +const questionIsFactLikeForHardFiltering = (question: GekanatorQuestion): boolean => !(question.kind === 'post_similarity' - || ( - question.kind === 'tag' + || (question.kind === 'tag' && question.condition.type === 'tag' && !(question.condition.key.startsWith ('nico:')))) -export const candidatePostsFor = ({ - posts, - questions, - answers, - softenedQuestionIds, - rejectedPostIds, - recoveredCandidatePosts, -}: { - posts: Post[] - questions: GekanatorQuestion[] - answers: GekanatorAnswerLog[] - softenedQuestionIds: Set - rejectedPostIds: Set - recoveredCandidatePosts: Map -}): Post[] => { +export const candidatePostsFor = ( + { posts, + questions, + answers, + softenedQuestionIds, + rejectedPostIds, + recoveredCandidatePosts }: { posts: Post[] + questions: GekanatorQuestion[] + answers: GekanatorAnswerLog[] + softenedQuestionIds: Set + rejectedPostIds: Set + recoveredCandidatePosts: Map }, +): Post[] => { const questionById = new Map (questions.map (question => [question.id, question])) return posts.filter (post => { @@ -47,7 +37,7 @@ export const candidatePostsFor = ({ const answerCountAtRecovery = recoveredCandidatePosts.get (post.id) return answers.every ((answer, index) => { - if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery) + if (answerCountAtRecovery != null && index < answerCountAtRecovery) return true if (softenedQuestionIds.has (answer.questionId)) @@ -62,10 +52,11 @@ export const candidatePostsFor = ({ switch (answer.answer) { case 'yes': - case 'no': { - const expected = expectedAnswerForQuestion (question, post) - return expected === null || expected === 'unknown' || expected === answer.answer - } + case 'no': + { + const expected = expectedAnswerForQuestion (question, post) + return expected === null || expected === 'unknown' || expected === answer.answer + } default: return true } @@ -74,15 +65,11 @@ export const candidatePostsFor = ({ } -export const hardFilteredPostsForAnswer = ({ - posts, - question, - answer, -}: { - posts: Post[] - question: GekanatorQuestion - answer: GekanatorAnswerValue -}): Post[] => { +export const hardFilteredPostsForAnswer = ( + { posts, question, answer }: { posts: Post[] + question: GekanatorQuestion + answer: GekanatorAnswerValue }, +): Post[] => { if (!(questionIsFactLikeForHardFiltering (question))) return posts @@ -91,16 +78,12 @@ export const hardFilteredPostsForAnswer = ({ return posts.filter (post => { const expected = expectedAnswerForQuestion (question, post) - return expected === null || expected === 'unknown' || expected === answer + return expected == null || expected === 'unknown' || expected === answer }) } -const concreteAnswerOptions: GekanatorAnswerValue[] = [ - 'yes', - 'no', - 'partial', - 'probably_no'] +const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', 'probably_no'] export const allConcreteAnswerOptionsExhausted = ( @@ -119,45 +102,39 @@ const nextRecoveryTargetSize = (recoveryStepCount: number): number => 6 * (2 ** recoveryStepCount) -export const recoverCandidatePosts = ({ - posts, - scores, - rejectedPostIds, - recoveredCandidatePosts, - eligiblePostIds, - answerCountAtRecovery, - recoveryStepCount, -}: { - posts: Post[] - scores: Map - rejectedPostIds: Set - recoveredCandidatePosts: Map - eligiblePostIds: Set - answerCountAtRecovery: number - recoveryStepCount: number -}): { - recoveredCandidatePosts: Map - recoveryStepCount: number -} | null => { +export const recoverCandidatePosts = ( + { posts, + scores, + rejectedPostIds, + recoveredCandidatePosts, + eligiblePostIds, + answerCountAtRecovery, + recoveryStepCount }: { posts: Post[] + scores: Map + rejectedPostIds: Set + recoveredCandidatePosts: Map + eligiblePostIds: Set + answerCountAtRecovery: number + recoveryStepCount: number }, +): { recoveredCandidatePosts: Map + recoveryStepCount: number } | null => { const recovered = new Map (recoveredCandidatePosts) const targetSize = nextRecoveryTargetSize (recoveryStepCount) - const countedPostIds = new Set ([ - ...eligiblePostIds, - ...recovered.keys ()]) + const countedPostIds = new Set ([...eligiblePostIds, ...recovered.keys ()]) const addCount = targetSize - countedPostIds.size if (addCount <= 0) - return { - recoveredCandidatePosts: recovered, - recoveryStepCount: recoveryStepCount + 1 } + { + return { recoveredCandidatePosts: recovered, + recoveryStepCount: recoveryStepCount + 1 } + } - const candidates = posts - .filter (post => - !(rejectedPostIds.has (post.id)) - && !(eligiblePostIds.has (post.id)) - && !(recovered.has (post.id))) - .sort ((a, b) => - (scores.get (b.id) ?? Number.NEGATIVE_INFINITY) - - (scores.get (a.id) ?? Number.NEGATIVE_INFINITY)) + const candidates = + posts + .filter (post => (!(rejectedPostIds.has (post.id)) + && !(eligiblePostIds.has (post.id)) + && !(recovered.has (post.id)))) + .sort ((a, b) => ((scores.get (b.id) ?? Number.NEGATIVE_INFINITY) + - (scores.get (a.id) ?? Number.NEGATIVE_INFINITY))) .slice (0, addCount) if (candidates.length === 0) @@ -165,7 +142,6 @@ export const recoverCandidatePosts = ({ candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery)) - return { - recoveredCandidatePosts: recovered, - recoveryStepCount: recoveryStepCount + 1 } + return { recoveredCandidatePosts: recovered, + recoveryStepCount: recoveryStepCount + 1 } } diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 304e997..56535f5 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -307,20 +307,19 @@ const shouldReplaceMergedQuestion = ( const hashString = (value: string): number => { - let hash = 2166136261 + let hash = 2_166_136_261 for (let i = 0; i < value.length; ++i) { hash ^= value.charCodeAt (i) - hash = Math.imul (hash, 16777619) + hash = Math.imul (hash, 16_777_619) } return hash >>> 0 } -const deterministicUnitFloat = (seed: string): number => - hashString (seed) / 4294967295 +const deterministicUnitFloat = (seed: string): number => hashString (seed) / 4_294_967_295 const clearStoredGame = (): void => { @@ -366,16 +365,17 @@ const loadRecentGames = (): RecentGameSummary[] => { if (!(Array.isArray (parsed))) return [] - return parsed - .filter ((item): item is RecentGameSummary => + return ( + parsed + .filter ((item): item is RecentGameSummary => ( typeof item === 'object' - && item !== null + && item != null && Number.isInteger ((item as RecentGameSummary).correctPostId) - && (((item as RecentGameSummary).firstQuestionId === null) + && (((item as RecentGameSummary).firstQuestionId == null) || typeof (item as RecentGameSummary).firstQuestionId === 'string') - && Number.isFinite ((item as RecentGameSummary).savedAt)) + && Number.isFinite ((item as RecentGameSummary).savedAt))) .sort ((a, b) => b.savedAt - a.savedAt) - .slice (0, maxStoredRecentGames) + .slice (0, maxStoredRecentGames)) } catch { @@ -387,13 +387,12 @@ const loadRecentGames = (): RecentGameSummary[] => { const storeRecentGameSummary = ( summary: RecentGameSummary, ): RecentGameSummary[] => { - const next = [ - summary, - ...loadRecentGames ().filter (item => - item.savedAt !== summary.savedAt - && !( - item.correctPostId === summary.correctPostId - && item.firstQuestionId === summary.firstQuestionId))] + const next = + [summary, + ...loadRecentGames ().filter (item => (item.savedAt !== summary.savedAt + && !(item.correctPostId === summary.correctPostId + && (item.firstQuestionId + === summary.firstQuestionId))))] .slice (0, maxStoredRecentGames) try @@ -429,11 +428,10 @@ const loadBackgroundMotionMode = (): BackgroundMotionMode => { const resettableExtraQuestionState = (): { extraQuestions: GekanatorExtraQuestion[] extraQuestionAnswers: Record - extraQuestionState: 'idle' -} => ({ - extraQuestions: [], - extraQuestionAnswers: { }, - extraQuestionState: 'idle' }) + extraQuestionState: 'idle' } => ( + { extraQuestions: [], + extraQuestionAnswers: { }, + extraQuestionState: 'idle' }) const recoveredCandidateMapFromStored = ( @@ -445,9 +443,8 @@ const recoveredCandidateMapFromStored = ( const storedRecoveredCandidatesFromMap = ( recoveredCandidatePosts: Map, ): RecoveredCandidatePost[] => - [...recoveredCandidatePosts.entries ()].map (([postId, answerCountAtRecovery]) => ({ - postId, - answerCountAtRecovery })) + [...recoveredCandidatePosts.entries ()] + .map (([postId, answerCountAtRecovery]) => ({ postId, answerCountAtRecovery })) const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => { @@ -468,10 +465,7 @@ const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => { const distributionEntropy = (weights: number[]): number => - weights.reduce ((sum, weight) => - weight <= 0 - ? sum - : sum - weight * Math.log2 (weight), 0) + weights.reduce ((sum, weight) => weight <= 0 ? sum : sum - weight * Math.log2 (weight), 0) const questionCategoryPenalty = ( @@ -481,9 +475,9 @@ const questionCategoryPenalty = ( ): number => { const earlyFactor = Math.max (0, (3 - answerCount) / 3) const titleLengthPenalty = - titleLengthMinimumForCondition (question.condition) === null - ? 0 - : (answerCount === 0 ? 8 : 3.5) * earlyFactor + titleLengthMinimumForCondition (question.condition) == null + ? 0 + : (answerCount === 0 ? 8 : 3.5) * earlyFactor switch (question.kind) { @@ -673,11 +667,11 @@ const buildMaterialIndex = ( originalYearByPostId.set (post.id, originalYear) originalMonthByPostId.set (post.id, originalMonth) originalMonthDayByPostId.set (post.id, originalMonthDay) - if (originalYear !== null) + if (originalYear != null) addPostIdToIndex (postIdsByOriginalYear, originalYear, post.id) - if (originalMonth !== null) + if (originalMonth != null) addPostIdToIndex (postIdsByOriginalMonth, originalMonth, post.id) - if (originalMonthDay !== null) + if (originalMonthDay != null) addPostIdToIndex (postIdsByOriginalMonthDay, originalMonthDay, post.id) const titleLength = post.title?.length ?? 0 @@ -878,7 +872,7 @@ const matchingPostIdsForCondition = ({ case 'title-length-greater-than': { const threshold = titleLengthMinimumForCondition (condition) - if (threshold === null) + if (threshold == null) return new Set () const cached = materialIndex.titleLengthThresholdCache.get (threshold) @@ -941,7 +935,7 @@ const matchingPostIdsForQuestion = ({ const byCondition = matchingPostIdsForCondition ({ condition: question.condition, materialIndex }) - if (byCondition !== null) + if (byCondition != null) return byCondition const matched = matchIndex.get (question.id) ?? dynamicMatchIndex?.get (question.id) @@ -1146,7 +1140,7 @@ const applyQuestionAnswerDeltaToScores = ({ const propagatedDelta = baseDelta * edge.cos const current = propagatedDeltaByPostId.get (edge.targetPostId) - if (current === undefined || Math.abs (propagatedDelta) > Math.abs (current)) + if (current == null || Math.abs (propagatedDelta) > Math.abs (current)) propagatedDeltaByPostId.set (edge.targetPostId, propagatedDelta) }) }) @@ -1204,13 +1198,13 @@ const buildQuestionsForCandidateIds = ( ): GekanatorQuestion[] => { const total = candidateIds.length const confirmationPost = - confirmationPostId === null + confirmationPostId == null ? null : materialIndex.postById.get (confirmationPostId) ?? null if (mode === 'split' && total === 0) return acceptedQuestions - if (mode === 'confirmation' && confirmationPost === null) + if (mode === 'confirmation' && confirmationPost == null) return acceptedQuestions const tagCounts = new Map () @@ -1228,7 +1222,7 @@ const buildQuestionsForCandidateIds = ( if (host) hostCounts.set (host, (hostCounts.get (host) ?? 0) + 1) const year = materialIndex.originalYearByPostId.get (postId) - if (year !== null && year !== undefined) + if (year != null) yearCounts.set (year, (yearCounts.get (year) ?? 0) + 1) const monthDay = materialIndex.originalMonthDayByPostId.get (postId) if (monthDay) @@ -1272,7 +1266,7 @@ const buildQuestionsForCandidateIds = ( counts: monthDayCounts, total, cap: factCap - }).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) !== null) + }).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null) if (mode === 'split') { @@ -1358,7 +1352,7 @@ const buildQuestionsForCandidateIds = ( materialIndex }) : null) addQuestion ( - year !== null && year !== undefined + year != null && year != undefined ? buildDateQuestion ({ type: 'original-year', year }) @@ -1445,7 +1439,7 @@ const candidatePostsForState = ({ const answerCountAtRecovery = recoveredCandidatePosts.get (post.id) return answers.every ((answer, index) => { - if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery) + if (answerCountAtRecovery != null && index < answerCountAtRecovery) return true if (softenedQuestionIds.has (answer.questionId)) return true @@ -1468,17 +1462,17 @@ const candidatePostsForState = ({ condition, materialIndex }) const useExpectedAnswer = - question !== undefined + question != null && usesLearnedTagExamples (question) if (question && !(questionIsFactLikeForHardFiltering (question))) return true - if (matched !== null) + if (matched != null) { if (useExpectedAnswer) { const expected = expectedAnswerForQuestion (question, post) - return expected === null + return expected == null || expected === 'unknown' || expected === answer.answer } @@ -1492,7 +1486,7 @@ const candidatePostsForState = ({ return true const expected = expectedAnswerForQuestion (question, post) - return expected === null || expected === 'unknown' || expected === answer.answer + return expected == null || expected === 'unknown' || expected === answer.answer }) }) } @@ -1648,7 +1642,7 @@ const previewAnswer = ({ dynamicMatchIndex }) const nextPosts = nextPostIds .map (postId => postById.get (postId)) - .filter ((post): post is Post => post !== undefined) + .filter ((post): post is Post => post != null) if (nextPosts.length === 0) return { answer, @@ -1709,7 +1703,7 @@ const softenNextQuestionIds = ({ .filter ((item): item is { answer: GekanatorAnswerLog question: GekanatorQuestion } => - item.question !== undefined + item.question != null && item.answer.answer !== 'unknown' && !(softenedQuestionIds.has (item.answer.questionId))) .sort ((a, b) => questionDifficulty (b.question) - questionDifficulty (a.question))[0] @@ -1753,9 +1747,9 @@ const sameConditionValue = ( ): boolean => { const leftTitleLength = titleLengthMinimumForCondition (left) const rightTitleLength = titleLengthMinimumForCondition (right) - if (leftTitleLength !== null || rightTitleLength !== null) - return leftTitleLength !== null - && rightTitleLength !== null + if (leftTitleLength != null || rightTitleLength != null) + return leftTitleLength != null + && rightTitleLength != null && leftTitleLength === rightTitleLength if (left.type !== right.type) @@ -1796,7 +1790,7 @@ const isMonthCrossMatch = ( ): boolean => { const candidateMonth = monthForCondition (candidate) const previousMonth = monthForCondition (previous) - if (candidateMonth === null || previousMonth === null) + if (candidateMonth == null || previousMonth == null) return false const sameType = candidate.type === previous.type @@ -1814,12 +1808,12 @@ const isExclusiveContradiction = ( const candidateGroup = exclusiveConditionGroupFor (candidate) const previousGroup = exclusiveConditionGroupFor (previous) - if (candidateGroup !== null && candidateGroup === previousGroup) + if (candidateGroup != null && candidateGroup === previousGroup) return !(sameConditionValue (candidate, previous)) const candidateMonth = monthForCondition (candidate) const previousMonth = monthForCondition (previous) - if (candidateMonth !== null && previousMonth !== null) + if (candidateMonth != null && previousMonth != null) return candidateMonth !== previousMonth return false @@ -2028,7 +2022,7 @@ const chooseQuestion = ( }, 0) / priorWeightTotal)) const priorBonus = - priorSplitScore === null + priorSplitScore == null ? 0 : Math.max (0, .22 - priorSplitScore) * -18 const infoGainBonus = -Math.min (1.2, infoGain) * 4 @@ -2050,7 +2044,7 @@ const chooseQuestion = ( .filter ((item): item is { question: GekanatorQuestion score: number - narrow: boolean } => item !== null && Number.isFinite (item.score)) + narrow: boolean } => item != null && Number.isFinite (item.score)) .sort ((a, b) => a.score - b.score) } @@ -2135,15 +2129,15 @@ const chooseWinningRunQuestion = ({ return false const expected = expectedAnswerForQuestion (question, targetPost) - return expected !== null && expected !== 'unknown' + return expected != null && expected !== 'unknown' }) .map (question => { const expected = expectedAnswerForQuestion (question, targetPost) const priority = - expected === null + expected == null ? null : winningRunPriorityFor (expected) - if (priority === null) + if (priority == null) return null const yesCount = matchingPostCountInIds ({ @@ -2168,7 +2162,7 @@ const chooseWinningRunQuestion = ({ question: GekanatorQuestion priority: number humanOffset: number - matchingCount: number } => item !== null) + matchingCount: number } => item != null) .sort ((a, b) => { if (a.priority !== b.priority) return a.priority - b.priority @@ -2258,7 +2252,7 @@ const chooseFallbackQuestion = ({ question: GekanatorQuestion knownCount: number balance: number - humanOffset: number } => item !== null) + humanOffset: number } => item != null) .sort ((a, b) => { if (a.humanOffset !== b.humanOffset) return a.humanOffset - b.humanOffset @@ -2291,13 +2285,13 @@ const isWinningRunActive = ( winningRunTargetId: number | null, winningRunStartAnswerCount: number | null, ): boolean => - winningRunTargetId !== null && winningRunStartAnswerCount !== null + winningRunTargetId != null && winningRunStartAnswerCount != null const winningRunQuestionCount = ( answers: GekanatorAnswerLog[], winningRunStartAnswerCount: number | null, -): number => winningRunStartAnswerCount === null +): number => winningRunStartAnswerCount == null ? 0 : answers .slice (winningRunStartAnswerCount) @@ -2377,15 +2371,15 @@ const nextQuestionPlanFor = ( ? eligiblePosts[0]?.id ?? null : null const nextWinningRunStartAnswerCount = - nextWinningRunTargetId === null + nextWinningRunTargetId == null ? null : ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount) && winningRunTargetId === nextWinningRunTargetId - && winningRunStartAnswerCount !== null) + && winningRunStartAnswerCount != null) ? winningRunStartAnswerCount : answers.length) const nextWinningRunTargetPost = - nextWinningRunTargetId === null + nextWinningRunTargetId == null ? null : posts.find (post => post.id === nextWinningRunTargetId) ?? null const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] => @@ -2398,8 +2392,8 @@ const nextQuestionPlanFor = ( if (eligiblePosts.length === 1) { const winningRunFinished = - nextWinningRunTargetId !== null - && nextWinningRunStartAnswerCount !== null + nextWinningRunTargetId != null + && nextWinningRunStartAnswerCount != null && eligiblePosts[0]?.id === nextWinningRunTargetId && winningRunQuestionCount ( answers, @@ -2412,7 +2406,7 @@ const nextQuestionPlanFor = ( questionMode: null, winningRunTargetId: nextWinningRunTargetId, winningRunStartAnswerCount: nextWinningRunStartAnswerCount } - if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount === null) + if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount == null) return { question: null, guess: null, @@ -2586,9 +2580,9 @@ const backgroundPostsFor = ({ }): Post[] => { const focusPosts = phase === 'end' || phase === 'review' || phase === 'learned' - ? [reviewCorrectPost, reviewGuessedPost].filter ((post): post is Post => post !== null) + ? [reviewCorrectPost, reviewGuessedPost].filter ((post): post is Post => post != null) : phase === 'guess' - ? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post !== null) + ? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post != null) : eligiblePosts.length > 0 ? eligiblePosts : availablePosts @@ -2646,7 +2640,7 @@ const GekanatorBackdrop: FC<{ const isWinningRunBackdrop = !(guessThumbnail) && phase === 'question' - && winningRunTargetPost !== null + && winningRunTargetPost != null && Boolean (backgroundThumbnailUrl (winningRunTargetPost)) const backdropMode = guessThumbnail @@ -2843,7 +2837,7 @@ const GekanatorBackdrop: FC<{ setActiveDirection (nextDirection) } - if (flipTimerRef.current !== null) + if (flipTimerRef.current != null) { window.clearTimeout (flipTimerRef.current) flipTimerRef.current = null @@ -2924,7 +2918,7 @@ const GekanatorBackdrop: FC<{ }, tileFlipDuration * 1000) return () => { - if (flipTimerRef.current !== null) + if (flipTimerRef.current != null) { window.clearTimeout (flipTimerRef.current) flipTimerRef.current = null @@ -3072,10 +3066,10 @@ const expectedAnswerFor = ( const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const storedGame = useMemo (loadStoredGame, []) - const hasStoredRestore = storedGame !== null && isStoredPhase (storedGame.phase) + const hasStoredRestore = storedGame != null && isStoredPhase (storedGame.phase) const queryClient = useQueryClient () const isAdmin = user?.role === 'admin' - const canPersistGame = user !== null + const canPersistGame = user != null const [recentGames, setRecentGames] = useState ( () => loadRecentGames ()) const [backgroundMotionMode, setBackgroundMotionMode] = useState ( @@ -3188,7 +3182,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { setAskedQuestionBank ( storedAskedQuestionBankIds .map (questionId => questionById.get (questionId)) - .filter ((question): question is GekanatorQuestion => question !== undefined)) + .filter ((question): question is GekanatorQuestion => question != null)) setStoredAskedQuestionBankIds ([]) }, [posts, storedAskedQuestionBankIds, @@ -3340,11 +3334,11 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { ?? null, [acceptedQuestions, questionSuggestionSelectedId]) const canSubmitQuestionSuggestion = useMemo (() => { - if (!(canPersistGame) || reviewCorrectPostId === null) + if (!(canPersistGame) || reviewCorrectPostId == null) return false if (questionSuggestionEntryMode === 'search') - return selectedSuggestedQuestion !== null + return selectedSuggestedQuestion != null return questionSuggestion.trim () !== '' }, [ @@ -3403,7 +3397,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { userPriorWeights, materialIndex, acceptedQuestionMatchIndex, lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount]) const winningRunTargetPost = useMemo ( - () => questionPlan.winningRunTargetId === null + () => questionPlan.winningRunTargetId == null ? null : posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null, [posts, questionPlan.winningRunTargetId]) @@ -3417,7 +3411,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { && winningRunQuestionsAsked < winningRunQuestionLimit && eligiblePosts.length === 1 && eligiblePosts[0]?.id === questionPlan.winningRunTargetId - && winningRunTargetPost !== null + && winningRunTargetPost != null const topScoredPosts = useMemo ( () => eligiblePosts .map (post => ({ post, score: scores.get (post.id) ?? 0 })) @@ -3454,7 +3448,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { posts.find (post => post.id === reviewCorrectPostId) ?? null const effectiveResultWon = resultWon - ?? ((reviewGuessedPostId !== null && reviewCorrectPostId !== null) + ?? ((reviewGuessedPostId != null && reviewCorrectPostId != null) ? reviewGuessedPostId === reviewCorrectPostId : null) const effectiveBackgroundMotionMode = @@ -3895,7 +3889,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { } const startReview = () => { - if (reviewGuessedPostId === null || reviewCorrectPostId === null) + if (reviewGuessedPostId == null || reviewCorrectPostId == null) return saveMutation.reset () @@ -3915,13 +3909,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const saveReviewedResult = (onSuccess: (gameId: number) => void) => { if ( !(canPersistGame) - || reviewGuessedPostId === null - || reviewCorrectPostId === null + || reviewGuessedPostId == null + || reviewCorrectPostId == null || saveMutation.isPending ) return - if (savedGameId !== null) + if (savedGameId != null) { onSuccess (savedGameId) return @@ -3979,7 +3973,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const saveExtraQuestions = () => { if ( !(canPersistGame) - || savedGameId === null + || savedGameId == null || extraQuestionAnswersMutation.isPending || extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)])) ) @@ -4175,7 +4169,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { } const startExtraQuestions = () => { - if (reviewCorrectPostId === null || saveMutation.isPending) + if (reviewCorrectPostId == null || saveMutation.isPending) return saveReviewedResult (gameId => { @@ -4206,7 +4200,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const dialogue = phase === 'learned' ? resultDialogue : introDialogue const saveStatusMessage = saved - && learnedExampleCount !== null + && learnedExampleCount != null ? learnedExampleCount > 0 ? `${ learnedExampleCount }件の回答を学習しました` : null @@ -4436,7 +4430,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {' / '} recoveryStepCount: {recoveryStepCount} {' / '} - currentQuestion===null: {String (currentQuestion === null)} + currentQuestion===null: {String (currentQuestion == null)} {topScoredPosts.length > 0 && (
@@ -4525,7 +4519,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {' / '} recoveryStepCount: {recoveryStepCount} {' / '} - currentQuestion===null: {String (currentQuestion === null)} + currentQuestion===null: {String (currentQuestion == null)}
)}
@@ -4621,7 +4615,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
- {reviewGuessedPostId !== null && reviewCorrectPostId !== null && ( + {reviewGuessedPostId != null && reviewCorrectPostId != null && (

判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}

)} @@ -4648,7 +4642,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { type="button" className="rounded bg-pink-600 px-4 py-2 font-bold text-white hover:bg-pink-500 disabled:opacity-50" - disabled={reviewCorrectPostId === null || saveMutation.isPending} + disabled={reviewCorrectPostId == null || saveMutation.isPending} onClick={saveAndReset}> もう一度 @@ -4659,7 +4653,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { dark:hover:bg-red-900 disabled:opacity-50" disabled={ !(canPersistGame) - || reviewCorrectPostId === null + || reviewCorrectPostId == null || saveMutation.isPending } onClick={startReview}> @@ -4682,7 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { hover:bg-yellow-100 dark:border-red-700 dark:hover:bg-red-900 disabled:opacity-50" disabled={!(canPersistGame) - || reviewCorrectPostId === null + || reviewCorrectPostId == null || saveMutation.isPending || extraQuestionState === 'loading' || extraQuestionAnswersMutation.isPending} @@ -4768,7 +4762,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { - {reviewGuessedPostId !== null && reviewCorrectPostId !== null && ( + {reviewGuessedPostId != null && reviewCorrectPostId != null && (

判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'} @@ -4798,7 +4792,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { hover:bg-pink-500 disabled:opacity-50" disabled={ !(canPersistGame) - || reviewCorrectPostId === null + || reviewCorrectPostId == null || saveMutation.isPending || questionSuggestionMutation.isPending }