このコミットが含まれているのは:
2026-06-16 23:48:29 +09:00
コミット f9f0010e03
3個のファイルの変更200行の追加182行の削除
+48
ファイルの表示
@@ -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
+34 -58
ファイルの表示
@@ -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,
export const candidatePostsFor = (
{ posts,
questions,
answers,
softenedQuestionIds,
rejectedPostIds,
recoveredCandidatePosts,
}: {
posts: Post[]
recoveredCandidatePosts }: { posts: Post[]
questions: GekanatorQuestion[]
answers: GekanatorAnswerLog[]
softenedQuestionIds: Set<string>
rejectedPostIds: Set<number>
recoveredCandidatePosts: Map<number, number>
}): Post[] => {
recoveredCandidatePosts: Map<number, number> },
): 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,7 +52,8 @@ export const candidatePostsFor = ({
switch (answer.answer)
{
case 'yes':
case 'no': {
case 'no':
{
const expected = expectedAnswerForQuestion (question, post)
return expected === null || expected === 'unknown' || expected === answer.answer
}
@@ -74,15 +65,11 @@ export const candidatePostsFor = ({
}
export const hardFilteredPostsForAnswer = ({
posts,
question,
answer,
}: {
posts: Post[]
export const hardFilteredPostsForAnswer = (
{ posts, question, answer }: { posts: Post[]
question: GekanatorQuestion
answer: GekanatorAnswerValue
}): Post[] => {
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,
export const recoverCandidatePosts = (
{ posts,
scores,
rejectedPostIds,
recoveredCandidatePosts,
eligiblePostIds,
answerCountAtRecovery,
recoveryStepCount,
}: {
posts: Post[]
recoveryStepCount }: { posts: Post[]
scores: Map<number, number>
rejectedPostIds: Set<number>
recoveredCandidatePosts: Map<number, number>
eligiblePostIds: Set<number>
answerCountAtRecovery: number
recoveryStepCount: number
}): {
recoveredCandidatePosts: Map<number, number>
recoveryStepCount: number
} | null => {
recoveryStepCount: number },
): { recoveredCandidatePosts: Map<number, number>
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,
{
return { recoveredCandidatePosts: recovered,
recoveryStepCount: recoveryStepCount + 1 }
}
const candidates = posts
.filter (post =>
!(rejectedPostIds.has (post.id))
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))
&& !(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,
return { recoveredCandidatePosts: recovered,
recoveryStepCount: recoveryStepCount + 1 }
}
+89 -95
ファイルの表示
@@ -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,9 +428,8 @@ const loadBackgroundMotionMode = (): BackgroundMotionMode => {
const resettableExtraQuestionState = (): {
extraQuestions: GekanatorExtraQuestion[]
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
extraQuestionState: 'idle'
} => ({
extraQuestions: [],
extraQuestionState: 'idle' } => (
{ extraQuestions: [],
extraQuestionAnswers: { },
extraQuestionState: 'idle' })
@@ -445,9 +443,8 @@ const recoveredCandidateMapFromStored = (
const storedRecoveredCandidatesFromMap = (
recoveredCandidatePosts: Map<number, number>,
): 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,7 +475,7 @@ const questionCategoryPenalty = (
): number => {
const earlyFactor = Math.max (0, (3 - answerCount) / 3)
const titleLengthPenalty =
titleLengthMinimumForCondition (question.condition) === null
titleLengthMinimumForCondition (question.condition) == null
? 0
: (answerCount === 0 ? 8 : 3.5) * earlyFactor
@@ -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<number> ()
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<string, number> ()
@@ -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<RecentGameSummary[]> (
() => loadRecentGames ())
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
@@ -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)}
</div>
{topScoredPosts.length > 0 && (
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1">
@@ -4525,7 +4519,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{' / '}
recoveryStepCount: {recoveryStepCount}
{' / '}
currentQuestion===null: {String (currentQuestion === null)}
currentQuestion===null: {String (currentQuestion == null)}
</div>)}
<PostMiniCard post={displayedGuess}/>
<div className="flex flex-wrap gap-2">
@@ -4621,7 +4615,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</button>
</div>
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
{reviewGuessedPostId != null && reviewCorrectPostId != null && (
<p className="text-sm text-neutral-600 dark:text-neutral-300">
: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
</p>)}
@@ -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}>
</button>
@@ -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 }) => {
</div>
</div>
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
{reviewGuessedPostId != null && reviewCorrectPostId != null && (
<p className="text-sm text-neutral-600 dark:text-neutral-300">
:
{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
}