このコミットが含まれているのは:
@@ -125,6 +125,54 @@ npm run preview
|
|||||||
- TypeScript and TSX use 4-space logical indentation.
|
- TypeScript and TSX use 4-space logical indentation.
|
||||||
- In TypeScript and TSX only, replace every leading run of 8 spaces with a tab.
|
- 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.
|
- 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 add production dependencies without explicit approval.
|
||||||
- Do not create, modify, or run tests unless the user explicitly asks for
|
- 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
|
test work. When the user asks for tests, keep working and rerun them until
|
||||||
|
|||||||
@@ -1,43 +1,33 @@
|
|||||||
import { expectedAnswerForQuestion } from '@/lib/gekanator'
|
import { expectedAnswerForQuestion } from '@/lib/gekanator'
|
||||||
|
|
||||||
import type {
|
import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion } from '@/lib/gekanator'
|
||||||
GekanatorAnswerLog,
|
|
||||||
GekanatorAnswerValue,
|
|
||||||
GekanatorQuestion,
|
|
||||||
} from '@/lib/gekanator'
|
|
||||||
import type { Post } from '@/types'
|
import type { Post } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
export type RecoveredCandidatePost = {
|
export type RecoveredCandidatePost = {
|
||||||
postId: number
|
postId: number
|
||||||
answerCountAtRecovery: number }
|
answerCountAtRecovery: number }
|
||||||
|
|
||||||
|
|
||||||
const questionIsFactLikeForHardFiltering = (
|
const questionIsFactLikeForHardFiltering = (question: GekanatorQuestion): boolean =>
|
||||||
question: GekanatorQuestion,
|
|
||||||
): boolean =>
|
|
||||||
!(question.kind === 'post_similarity'
|
!(question.kind === 'post_similarity'
|
||||||
|| (
|
|| (question.kind === 'tag'
|
||||||
question.kind === 'tag'
|
|
||||||
&& question.condition.type === 'tag'
|
&& question.condition.type === 'tag'
|
||||||
&& !(question.condition.key.startsWith ('nico:'))))
|
&& !(question.condition.key.startsWith ('nico:'))))
|
||||||
|
|
||||||
|
|
||||||
export const candidatePostsFor = ({
|
export const candidatePostsFor = (
|
||||||
posts,
|
{ posts,
|
||||||
questions,
|
questions,
|
||||||
answers,
|
answers,
|
||||||
softenedQuestionIds,
|
softenedQuestionIds,
|
||||||
rejectedPostIds,
|
rejectedPostIds,
|
||||||
recoveredCandidatePosts,
|
recoveredCandidatePosts }: { posts: Post[]
|
||||||
}: {
|
questions: GekanatorQuestion[]
|
||||||
posts: Post[]
|
answers: GekanatorAnswerLog[]
|
||||||
questions: GekanatorQuestion[]
|
softenedQuestionIds: Set<string>
|
||||||
answers: GekanatorAnswerLog[]
|
rejectedPostIds: Set<number>
|
||||||
softenedQuestionIds: Set<string>
|
recoveredCandidatePosts: Map<number, number> },
|
||||||
rejectedPostIds: Set<number>
|
): Post[] => {
|
||||||
recoveredCandidatePosts: Map<number, number>
|
|
||||||
}): Post[] => {
|
|
||||||
const questionById = new Map (questions.map (question => [question.id, question]))
|
const questionById = new Map (questions.map (question => [question.id, question]))
|
||||||
|
|
||||||
return posts.filter (post => {
|
return posts.filter (post => {
|
||||||
@@ -47,7 +37,7 @@ export const candidatePostsFor = ({
|
|||||||
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
|
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
|
||||||
|
|
||||||
return answers.every ((answer, index) => {
|
return answers.every ((answer, index) => {
|
||||||
if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery)
|
if (answerCountAtRecovery != null && index < answerCountAtRecovery)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if (softenedQuestionIds.has (answer.questionId))
|
if (softenedQuestionIds.has (answer.questionId))
|
||||||
@@ -62,10 +52,11 @@ export const candidatePostsFor = ({
|
|||||||
switch (answer.answer)
|
switch (answer.answer)
|
||||||
{
|
{
|
||||||
case 'yes':
|
case 'yes':
|
||||||
case 'no': {
|
case 'no':
|
||||||
const expected = expectedAnswerForQuestion (question, post)
|
{
|
||||||
return expected === null || expected === 'unknown' || expected === answer.answer
|
const expected = expectedAnswerForQuestion (question, post)
|
||||||
}
|
return expected === null || expected === 'unknown' || expected === answer.answer
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -74,15 +65,11 @@ export const candidatePostsFor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const hardFilteredPostsForAnswer = ({
|
export const hardFilteredPostsForAnswer = (
|
||||||
posts,
|
{ posts, question, answer }: { posts: Post[]
|
||||||
question,
|
question: GekanatorQuestion
|
||||||
answer,
|
answer: GekanatorAnswerValue },
|
||||||
}: {
|
): Post[] => {
|
||||||
posts: Post[]
|
|
||||||
question: GekanatorQuestion
|
|
||||||
answer: GekanatorAnswerValue
|
|
||||||
}): Post[] => {
|
|
||||||
if (!(questionIsFactLikeForHardFiltering (question)))
|
if (!(questionIsFactLikeForHardFiltering (question)))
|
||||||
return posts
|
return posts
|
||||||
|
|
||||||
@@ -91,16 +78,12 @@ export const hardFilteredPostsForAnswer = ({
|
|||||||
|
|
||||||
return posts.filter (post => {
|
return posts.filter (post => {
|
||||||
const expected = expectedAnswerForQuestion (question, post)
|
const expected = expectedAnswerForQuestion (question, post)
|
||||||
return expected === null || expected === 'unknown' || expected === answer
|
return expected == null || expected === 'unknown' || expected === answer
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const concreteAnswerOptions: GekanatorAnswerValue[] = [
|
const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', 'probably_no']
|
||||||
'yes',
|
|
||||||
'no',
|
|
||||||
'partial',
|
|
||||||
'probably_no']
|
|
||||||
|
|
||||||
|
|
||||||
export const allConcreteAnswerOptionsExhausted = (
|
export const allConcreteAnswerOptionsExhausted = (
|
||||||
@@ -119,45 +102,39 @@ const nextRecoveryTargetSize = (recoveryStepCount: number): number =>
|
|||||||
6 * (2 ** recoveryStepCount)
|
6 * (2 ** recoveryStepCount)
|
||||||
|
|
||||||
|
|
||||||
export const recoverCandidatePosts = ({
|
export const recoverCandidatePosts = (
|
||||||
posts,
|
{ posts,
|
||||||
scores,
|
scores,
|
||||||
rejectedPostIds,
|
rejectedPostIds,
|
||||||
recoveredCandidatePosts,
|
recoveredCandidatePosts,
|
||||||
eligiblePostIds,
|
eligiblePostIds,
|
||||||
answerCountAtRecovery,
|
answerCountAtRecovery,
|
||||||
recoveryStepCount,
|
recoveryStepCount }: { posts: Post[]
|
||||||
}: {
|
scores: Map<number, number>
|
||||||
posts: Post[]
|
rejectedPostIds: Set<number>
|
||||||
scores: Map<number, number>
|
recoveredCandidatePosts: Map<number, number>
|
||||||
rejectedPostIds: Set<number>
|
eligiblePostIds: Set<number>
|
||||||
recoveredCandidatePosts: Map<number, number>
|
answerCountAtRecovery: number
|
||||||
eligiblePostIds: Set<number>
|
recoveryStepCount: number },
|
||||||
answerCountAtRecovery: number
|
): { recoveredCandidatePosts: Map<number, number>
|
||||||
recoveryStepCount: number
|
recoveryStepCount: number } | null => {
|
||||||
}): {
|
|
||||||
recoveredCandidatePosts: Map<number, number>
|
|
||||||
recoveryStepCount: number
|
|
||||||
} | null => {
|
|
||||||
const recovered = new Map (recoveredCandidatePosts)
|
const recovered = new Map (recoveredCandidatePosts)
|
||||||
const targetSize = nextRecoveryTargetSize (recoveryStepCount)
|
const targetSize = nextRecoveryTargetSize (recoveryStepCount)
|
||||||
const countedPostIds = new Set ([
|
const countedPostIds = new Set ([...eligiblePostIds, ...recovered.keys ()])
|
||||||
...eligiblePostIds,
|
|
||||||
...recovered.keys ()])
|
|
||||||
const addCount = targetSize - countedPostIds.size
|
const addCount = targetSize - countedPostIds.size
|
||||||
if (addCount <= 0)
|
if (addCount <= 0)
|
||||||
return {
|
{
|
||||||
recoveredCandidatePosts: recovered,
|
return { recoveredCandidatePosts: recovered,
|
||||||
recoveryStepCount: recoveryStepCount + 1 }
|
recoveryStepCount: recoveryStepCount + 1 }
|
||||||
|
}
|
||||||
|
|
||||||
const candidates = posts
|
const candidates =
|
||||||
.filter (post =>
|
posts
|
||||||
!(rejectedPostIds.has (post.id))
|
.filter (post => (!(rejectedPostIds.has (post.id))
|
||||||
&& !(eligiblePostIds.has (post.id))
|
&& !(eligiblePostIds.has (post.id))
|
||||||
&& !(recovered.has (post.id)))
|
&& !(recovered.has (post.id))))
|
||||||
.sort ((a, b) =>
|
.sort ((a, b) => ((scores.get (b.id) ?? Number.NEGATIVE_INFINITY)
|
||||||
(scores.get (b.id) ?? Number.NEGATIVE_INFINITY)
|
- (scores.get (a.id) ?? Number.NEGATIVE_INFINITY)))
|
||||||
- (scores.get (a.id) ?? Number.NEGATIVE_INFINITY))
|
|
||||||
.slice (0, addCount)
|
.slice (0, addCount)
|
||||||
|
|
||||||
if (candidates.length === 0)
|
if (candidates.length === 0)
|
||||||
@@ -165,7 +142,6 @@ export const recoverCandidatePosts = ({
|
|||||||
|
|
||||||
candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery))
|
candidates.forEach (post => recovered.set (post.id, answerCountAtRecovery))
|
||||||
|
|
||||||
return {
|
return { recoveredCandidatePosts: recovered,
|
||||||
recoveredCandidatePosts: recovered,
|
recoveryStepCount: recoveryStepCount + 1 }
|
||||||
recoveryStepCount: recoveryStepCount + 1 }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,20 +307,19 @@ const shouldReplaceMergedQuestion = (
|
|||||||
|
|
||||||
|
|
||||||
const hashString = (value: string): number => {
|
const hashString = (value: string): number => {
|
||||||
let hash = 2166136261
|
let hash = 2_166_136_261
|
||||||
|
|
||||||
for (let i = 0; i < value.length; ++i)
|
for (let i = 0; i < value.length; ++i)
|
||||||
{
|
{
|
||||||
hash ^= value.charCodeAt (i)
|
hash ^= value.charCodeAt (i)
|
||||||
hash = Math.imul (hash, 16777619)
|
hash = Math.imul (hash, 16_777_619)
|
||||||
}
|
}
|
||||||
|
|
||||||
return hash >>> 0
|
return hash >>> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const deterministicUnitFloat = (seed: string): number =>
|
const deterministicUnitFloat = (seed: string): number => hashString (seed) / 4_294_967_295
|
||||||
hashString (seed) / 4294967295
|
|
||||||
|
|
||||||
|
|
||||||
const clearStoredGame = (): void => {
|
const clearStoredGame = (): void => {
|
||||||
@@ -366,16 +365,17 @@ const loadRecentGames = (): RecentGameSummary[] => {
|
|||||||
if (!(Array.isArray (parsed)))
|
if (!(Array.isArray (parsed)))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return parsed
|
return (
|
||||||
.filter ((item): item is RecentGameSummary =>
|
parsed
|
||||||
|
.filter ((item): item is RecentGameSummary => (
|
||||||
typeof item === 'object'
|
typeof item === 'object'
|
||||||
&& item !== null
|
&& item != null
|
||||||
&& Number.isInteger ((item as RecentGameSummary).correctPostId)
|
&& Number.isInteger ((item as RecentGameSummary).correctPostId)
|
||||||
&& (((item as RecentGameSummary).firstQuestionId === null)
|
&& (((item as RecentGameSummary).firstQuestionId == null)
|
||||||
|| typeof (item as RecentGameSummary).firstQuestionId === 'string')
|
|| 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)
|
.sort ((a, b) => b.savedAt - a.savedAt)
|
||||||
.slice (0, maxStoredRecentGames)
|
.slice (0, maxStoredRecentGames))
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -387,13 +387,12 @@ const loadRecentGames = (): RecentGameSummary[] => {
|
|||||||
const storeRecentGameSummary = (
|
const storeRecentGameSummary = (
|
||||||
summary: RecentGameSummary,
|
summary: RecentGameSummary,
|
||||||
): RecentGameSummary[] => {
|
): RecentGameSummary[] => {
|
||||||
const next = [
|
const next =
|
||||||
summary,
|
[summary,
|
||||||
...loadRecentGames ().filter (item =>
|
...loadRecentGames ().filter (item => (item.savedAt !== summary.savedAt
|
||||||
item.savedAt !== summary.savedAt
|
&& !(item.correctPostId === summary.correctPostId
|
||||||
&& !(
|
&& (item.firstQuestionId
|
||||||
item.correctPostId === summary.correctPostId
|
=== summary.firstQuestionId))))]
|
||||||
&& item.firstQuestionId === summary.firstQuestionId))]
|
|
||||||
.slice (0, maxStoredRecentGames)
|
.slice (0, maxStoredRecentGames)
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -429,11 +428,10 @@ const loadBackgroundMotionMode = (): BackgroundMotionMode => {
|
|||||||
const resettableExtraQuestionState = (): {
|
const resettableExtraQuestionState = (): {
|
||||||
extraQuestions: GekanatorExtraQuestion[]
|
extraQuestions: GekanatorExtraQuestion[]
|
||||||
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
|
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
|
||||||
extraQuestionState: 'idle'
|
extraQuestionState: 'idle' } => (
|
||||||
} => ({
|
{ extraQuestions: [],
|
||||||
extraQuestions: [],
|
extraQuestionAnswers: { },
|
||||||
extraQuestionAnswers: { },
|
extraQuestionState: 'idle' })
|
||||||
extraQuestionState: 'idle' })
|
|
||||||
|
|
||||||
|
|
||||||
const recoveredCandidateMapFromStored = (
|
const recoveredCandidateMapFromStored = (
|
||||||
@@ -445,9 +443,8 @@ const recoveredCandidateMapFromStored = (
|
|||||||
const storedRecoveredCandidatesFromMap = (
|
const storedRecoveredCandidatesFromMap = (
|
||||||
recoveredCandidatePosts: Map<number, number>,
|
recoveredCandidatePosts: Map<number, number>,
|
||||||
): RecoveredCandidatePost[] =>
|
): RecoveredCandidatePost[] =>
|
||||||
[...recoveredCandidatePosts.entries ()].map (([postId, answerCountAtRecovery]) => ({
|
[...recoveredCandidatePosts.entries ()]
|
||||||
postId,
|
.map (([postId, answerCountAtRecovery]) => ({ postId, answerCountAtRecovery }))
|
||||||
answerCountAtRecovery }))
|
|
||||||
|
|
||||||
|
|
||||||
const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => {
|
const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => {
|
||||||
@@ -468,10 +465,7 @@ const baseDeltaForAnswer = (answer: GekanatorAnswerValue): number => {
|
|||||||
|
|
||||||
|
|
||||||
const distributionEntropy = (weights: number[]): number =>
|
const distributionEntropy = (weights: number[]): number =>
|
||||||
weights.reduce ((sum, weight) =>
|
weights.reduce ((sum, weight) => weight <= 0 ? sum : sum - weight * Math.log2 (weight), 0)
|
||||||
weight <= 0
|
|
||||||
? sum
|
|
||||||
: sum - weight * Math.log2 (weight), 0)
|
|
||||||
|
|
||||||
|
|
||||||
const questionCategoryPenalty = (
|
const questionCategoryPenalty = (
|
||||||
@@ -481,9 +475,9 @@ const questionCategoryPenalty = (
|
|||||||
): number => {
|
): number => {
|
||||||
const earlyFactor = Math.max (0, (3 - answerCount) / 3)
|
const earlyFactor = Math.max (0, (3 - answerCount) / 3)
|
||||||
const titleLengthPenalty =
|
const titleLengthPenalty =
|
||||||
titleLengthMinimumForCondition (question.condition) === null
|
titleLengthMinimumForCondition (question.condition) == null
|
||||||
? 0
|
? 0
|
||||||
: (answerCount === 0 ? 8 : 3.5) * earlyFactor
|
: (answerCount === 0 ? 8 : 3.5) * earlyFactor
|
||||||
|
|
||||||
switch (question.kind)
|
switch (question.kind)
|
||||||
{
|
{
|
||||||
@@ -673,11 +667,11 @@ const buildMaterialIndex = (
|
|||||||
originalYearByPostId.set (post.id, originalYear)
|
originalYearByPostId.set (post.id, originalYear)
|
||||||
originalMonthByPostId.set (post.id, originalMonth)
|
originalMonthByPostId.set (post.id, originalMonth)
|
||||||
originalMonthDayByPostId.set (post.id, originalMonthDay)
|
originalMonthDayByPostId.set (post.id, originalMonthDay)
|
||||||
if (originalYear !== null)
|
if (originalYear != null)
|
||||||
addPostIdToIndex (postIdsByOriginalYear, originalYear, post.id)
|
addPostIdToIndex (postIdsByOriginalYear, originalYear, post.id)
|
||||||
if (originalMonth !== null)
|
if (originalMonth != null)
|
||||||
addPostIdToIndex (postIdsByOriginalMonth, originalMonth, post.id)
|
addPostIdToIndex (postIdsByOriginalMonth, originalMonth, post.id)
|
||||||
if (originalMonthDay !== null)
|
if (originalMonthDay != null)
|
||||||
addPostIdToIndex (postIdsByOriginalMonthDay, originalMonthDay, post.id)
|
addPostIdToIndex (postIdsByOriginalMonthDay, originalMonthDay, post.id)
|
||||||
|
|
||||||
const titleLength = post.title?.length ?? 0
|
const titleLength = post.title?.length ?? 0
|
||||||
@@ -878,7 +872,7 @@ const matchingPostIdsForCondition = ({
|
|||||||
case 'title-length-greater-than': {
|
case 'title-length-greater-than': {
|
||||||
const threshold =
|
const threshold =
|
||||||
titleLengthMinimumForCondition (condition)
|
titleLengthMinimumForCondition (condition)
|
||||||
if (threshold === null)
|
if (threshold == null)
|
||||||
return new Set<number> ()
|
return new Set<number> ()
|
||||||
|
|
||||||
const cached = materialIndex.titleLengthThresholdCache.get (threshold)
|
const cached = materialIndex.titleLengthThresholdCache.get (threshold)
|
||||||
@@ -941,7 +935,7 @@ const matchingPostIdsForQuestion = ({
|
|||||||
const byCondition = matchingPostIdsForCondition ({
|
const byCondition = matchingPostIdsForCondition ({
|
||||||
condition: question.condition,
|
condition: question.condition,
|
||||||
materialIndex })
|
materialIndex })
|
||||||
if (byCondition !== null)
|
if (byCondition != null)
|
||||||
return byCondition
|
return byCondition
|
||||||
|
|
||||||
const matched = matchIndex.get (question.id) ?? dynamicMatchIndex?.get (question.id)
|
const matched = matchIndex.get (question.id) ?? dynamicMatchIndex?.get (question.id)
|
||||||
@@ -1146,7 +1140,7 @@ const applyQuestionAnswerDeltaToScores = ({
|
|||||||
|
|
||||||
const propagatedDelta = baseDelta * edge.cos
|
const propagatedDelta = baseDelta * edge.cos
|
||||||
const current = propagatedDeltaByPostId.get (edge.targetPostId)
|
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)
|
propagatedDeltaByPostId.set (edge.targetPostId, propagatedDelta)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1204,13 +1198,13 @@ const buildQuestionsForCandidateIds = (
|
|||||||
): GekanatorQuestion[] => {
|
): GekanatorQuestion[] => {
|
||||||
const total = candidateIds.length
|
const total = candidateIds.length
|
||||||
const confirmationPost =
|
const confirmationPost =
|
||||||
confirmationPostId === null
|
confirmationPostId == null
|
||||||
? null
|
? null
|
||||||
: materialIndex.postById.get (confirmationPostId) ?? null
|
: materialIndex.postById.get (confirmationPostId) ?? null
|
||||||
|
|
||||||
if (mode === 'split' && total === 0)
|
if (mode === 'split' && total === 0)
|
||||||
return acceptedQuestions
|
return acceptedQuestions
|
||||||
if (mode === 'confirmation' && confirmationPost === null)
|
if (mode === 'confirmation' && confirmationPost == null)
|
||||||
return acceptedQuestions
|
return acceptedQuestions
|
||||||
|
|
||||||
const tagCounts = new Map<string, number> ()
|
const tagCounts = new Map<string, number> ()
|
||||||
@@ -1228,7 +1222,7 @@ const buildQuestionsForCandidateIds = (
|
|||||||
if (host)
|
if (host)
|
||||||
hostCounts.set (host, (hostCounts.get (host) ?? 0) + 1)
|
hostCounts.set (host, (hostCounts.get (host) ?? 0) + 1)
|
||||||
const year = materialIndex.originalYearByPostId.get (postId)
|
const year = materialIndex.originalYearByPostId.get (postId)
|
||||||
if (year !== null && year !== undefined)
|
if (year != null)
|
||||||
yearCounts.set (year, (yearCounts.get (year) ?? 0) + 1)
|
yearCounts.set (year, (yearCounts.get (year) ?? 0) + 1)
|
||||||
const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
|
const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
|
||||||
if (monthDay)
|
if (monthDay)
|
||||||
@@ -1272,7 +1266,7 @@ const buildQuestionsForCandidateIds = (
|
|||||||
counts: monthDayCounts,
|
counts: monthDayCounts,
|
||||||
total,
|
total,
|
||||||
cap: factCap
|
cap: factCap
|
||||||
}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) !== null)
|
}).filter (([monthDay]) => specialOriginalMonthDayLabelFor (String (monthDay)) != null)
|
||||||
|
|
||||||
if (mode === 'split')
|
if (mode === 'split')
|
||||||
{
|
{
|
||||||
@@ -1358,7 +1352,7 @@ const buildQuestionsForCandidateIds = (
|
|||||||
materialIndex })
|
materialIndex })
|
||||||
: null)
|
: null)
|
||||||
addQuestion (
|
addQuestion (
|
||||||
year !== null && year !== undefined
|
year != null && year != undefined
|
||||||
? buildDateQuestion ({
|
? buildDateQuestion ({
|
||||||
type: 'original-year',
|
type: 'original-year',
|
||||||
year })
|
year })
|
||||||
@@ -1445,7 +1439,7 @@ const candidatePostsForState = ({
|
|||||||
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
|
const answerCountAtRecovery = recoveredCandidatePosts.get (post.id)
|
||||||
|
|
||||||
return answers.every ((answer, index) => {
|
return answers.every ((answer, index) => {
|
||||||
if (answerCountAtRecovery !== undefined && index < answerCountAtRecovery)
|
if (answerCountAtRecovery != null && index < answerCountAtRecovery)
|
||||||
return true
|
return true
|
||||||
if (softenedQuestionIds.has (answer.questionId))
|
if (softenedQuestionIds.has (answer.questionId))
|
||||||
return true
|
return true
|
||||||
@@ -1468,17 +1462,17 @@ const candidatePostsForState = ({
|
|||||||
condition,
|
condition,
|
||||||
materialIndex })
|
materialIndex })
|
||||||
const useExpectedAnswer =
|
const useExpectedAnswer =
|
||||||
question !== undefined
|
question != null
|
||||||
&& usesLearnedTagExamples (question)
|
&& usesLearnedTagExamples (question)
|
||||||
if (question && !(questionIsFactLikeForHardFiltering (question)))
|
if (question && !(questionIsFactLikeForHardFiltering (question)))
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if (matched !== null)
|
if (matched != null)
|
||||||
{
|
{
|
||||||
if (useExpectedAnswer)
|
if (useExpectedAnswer)
|
||||||
{
|
{
|
||||||
const expected = expectedAnswerForQuestion (question, post)
|
const expected = expectedAnswerForQuestion (question, post)
|
||||||
return expected === null
|
return expected == null
|
||||||
|| expected === 'unknown'
|
|| expected === 'unknown'
|
||||||
|| expected === answer.answer
|
|| expected === answer.answer
|
||||||
}
|
}
|
||||||
@@ -1492,7 +1486,7 @@ const candidatePostsForState = ({
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
const expected = expectedAnswerForQuestion (question, post)
|
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 })
|
dynamicMatchIndex })
|
||||||
const nextPosts = nextPostIds
|
const nextPosts = nextPostIds
|
||||||
.map (postId => postById.get (postId))
|
.map (postId => postById.get (postId))
|
||||||
.filter ((post): post is Post => post !== undefined)
|
.filter ((post): post is Post => post != null)
|
||||||
if (nextPosts.length === 0)
|
if (nextPosts.length === 0)
|
||||||
return {
|
return {
|
||||||
answer,
|
answer,
|
||||||
@@ -1709,7 +1703,7 @@ const softenNextQuestionIds = ({
|
|||||||
.filter ((item): item is {
|
.filter ((item): item is {
|
||||||
answer: GekanatorAnswerLog
|
answer: GekanatorAnswerLog
|
||||||
question: GekanatorQuestion } =>
|
question: GekanatorQuestion } =>
|
||||||
item.question !== undefined
|
item.question != null
|
||||||
&& item.answer.answer !== 'unknown'
|
&& item.answer.answer !== 'unknown'
|
||||||
&& !(softenedQuestionIds.has (item.answer.questionId)))
|
&& !(softenedQuestionIds.has (item.answer.questionId)))
|
||||||
.sort ((a, b) => questionDifficulty (b.question) - questionDifficulty (a.question))[0]
|
.sort ((a, b) => questionDifficulty (b.question) - questionDifficulty (a.question))[0]
|
||||||
@@ -1753,9 +1747,9 @@ const sameConditionValue = (
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
const leftTitleLength = titleLengthMinimumForCondition (left)
|
const leftTitleLength = titleLengthMinimumForCondition (left)
|
||||||
const rightTitleLength = titleLengthMinimumForCondition (right)
|
const rightTitleLength = titleLengthMinimumForCondition (right)
|
||||||
if (leftTitleLength !== null || rightTitleLength !== null)
|
if (leftTitleLength != null || rightTitleLength != null)
|
||||||
return leftTitleLength !== null
|
return leftTitleLength != null
|
||||||
&& rightTitleLength !== null
|
&& rightTitleLength != null
|
||||||
&& leftTitleLength === rightTitleLength
|
&& leftTitleLength === rightTitleLength
|
||||||
|
|
||||||
if (left.type !== right.type)
|
if (left.type !== right.type)
|
||||||
@@ -1796,7 +1790,7 @@ const isMonthCrossMatch = (
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
const candidateMonth = monthForCondition (candidate)
|
const candidateMonth = monthForCondition (candidate)
|
||||||
const previousMonth = monthForCondition (previous)
|
const previousMonth = monthForCondition (previous)
|
||||||
if (candidateMonth === null || previousMonth === null)
|
if (candidateMonth == null || previousMonth == null)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
const sameType = candidate.type === previous.type
|
const sameType = candidate.type === previous.type
|
||||||
@@ -1814,12 +1808,12 @@ const isExclusiveContradiction = (
|
|||||||
const candidateGroup = exclusiveConditionGroupFor (candidate)
|
const candidateGroup = exclusiveConditionGroupFor (candidate)
|
||||||
const previousGroup = exclusiveConditionGroupFor (previous)
|
const previousGroup = exclusiveConditionGroupFor (previous)
|
||||||
|
|
||||||
if (candidateGroup !== null && candidateGroup === previousGroup)
|
if (candidateGroup != null && candidateGroup === previousGroup)
|
||||||
return !(sameConditionValue (candidate, previous))
|
return !(sameConditionValue (candidate, previous))
|
||||||
|
|
||||||
const candidateMonth = monthForCondition (candidate)
|
const candidateMonth = monthForCondition (candidate)
|
||||||
const previousMonth = monthForCondition (previous)
|
const previousMonth = monthForCondition (previous)
|
||||||
if (candidateMonth !== null && previousMonth !== null)
|
if (candidateMonth != null && previousMonth != null)
|
||||||
return candidateMonth !== previousMonth
|
return candidateMonth !== previousMonth
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -2028,7 +2022,7 @@ const chooseQuestion = (
|
|||||||
},
|
},
|
||||||
0) / priorWeightTotal))
|
0) / priorWeightTotal))
|
||||||
const priorBonus =
|
const priorBonus =
|
||||||
priorSplitScore === null
|
priorSplitScore == null
|
||||||
? 0
|
? 0
|
||||||
: Math.max (0, .22 - priorSplitScore) * -18
|
: Math.max (0, .22 - priorSplitScore) * -18
|
||||||
const infoGainBonus = -Math.min (1.2, infoGain) * 4
|
const infoGainBonus = -Math.min (1.2, infoGain) * 4
|
||||||
@@ -2050,7 +2044,7 @@ const chooseQuestion = (
|
|||||||
.filter ((item): item is {
|
.filter ((item): item is {
|
||||||
question: GekanatorQuestion
|
question: GekanatorQuestion
|
||||||
score: number
|
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)
|
.sort ((a, b) => a.score - b.score)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2135,15 +2129,15 @@ const chooseWinningRunQuestion = ({
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
const expected = expectedAnswerForQuestion (question, targetPost)
|
const expected = expectedAnswerForQuestion (question, targetPost)
|
||||||
return expected !== null && expected !== 'unknown'
|
return expected != null && expected !== 'unknown'
|
||||||
})
|
})
|
||||||
.map (question => {
|
.map (question => {
|
||||||
const expected = expectedAnswerForQuestion (question, targetPost)
|
const expected = expectedAnswerForQuestion (question, targetPost)
|
||||||
const priority =
|
const priority =
|
||||||
expected === null
|
expected == null
|
||||||
? null
|
? null
|
||||||
: winningRunPriorityFor (expected)
|
: winningRunPriorityFor (expected)
|
||||||
if (priority === null)
|
if (priority == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const yesCount = matchingPostCountInIds ({
|
const yesCount = matchingPostCountInIds ({
|
||||||
@@ -2168,7 +2162,7 @@ const chooseWinningRunQuestion = ({
|
|||||||
question: GekanatorQuestion
|
question: GekanatorQuestion
|
||||||
priority: number
|
priority: number
|
||||||
humanOffset: number
|
humanOffset: number
|
||||||
matchingCount: number } => item !== null)
|
matchingCount: number } => item != null)
|
||||||
.sort ((a, b) => {
|
.sort ((a, b) => {
|
||||||
if (a.priority !== b.priority)
|
if (a.priority !== b.priority)
|
||||||
return a.priority - b.priority
|
return a.priority - b.priority
|
||||||
@@ -2258,7 +2252,7 @@ const chooseFallbackQuestion = ({
|
|||||||
question: GekanatorQuestion
|
question: GekanatorQuestion
|
||||||
knownCount: number
|
knownCount: number
|
||||||
balance: number
|
balance: number
|
||||||
humanOffset: number } => item !== null)
|
humanOffset: number } => item != null)
|
||||||
.sort ((a, b) => {
|
.sort ((a, b) => {
|
||||||
if (a.humanOffset !== b.humanOffset)
|
if (a.humanOffset !== b.humanOffset)
|
||||||
return a.humanOffset - b.humanOffset
|
return a.humanOffset - b.humanOffset
|
||||||
@@ -2291,13 +2285,13 @@ const isWinningRunActive = (
|
|||||||
winningRunTargetId: number | null,
|
winningRunTargetId: number | null,
|
||||||
winningRunStartAnswerCount: number | null,
|
winningRunStartAnswerCount: number | null,
|
||||||
): boolean =>
|
): boolean =>
|
||||||
winningRunTargetId !== null && winningRunStartAnswerCount !== null
|
winningRunTargetId != null && winningRunStartAnswerCount != null
|
||||||
|
|
||||||
|
|
||||||
const winningRunQuestionCount = (
|
const winningRunQuestionCount = (
|
||||||
answers: GekanatorAnswerLog[],
|
answers: GekanatorAnswerLog[],
|
||||||
winningRunStartAnswerCount: number | null,
|
winningRunStartAnswerCount: number | null,
|
||||||
): number => winningRunStartAnswerCount === null
|
): number => winningRunStartAnswerCount == null
|
||||||
? 0
|
? 0
|
||||||
: answers
|
: answers
|
||||||
.slice (winningRunStartAnswerCount)
|
.slice (winningRunStartAnswerCount)
|
||||||
@@ -2377,15 +2371,15 @@ const nextQuestionPlanFor = (
|
|||||||
? eligiblePosts[0]?.id ?? null
|
? eligiblePosts[0]?.id ?? null
|
||||||
: null
|
: null
|
||||||
const nextWinningRunStartAnswerCount =
|
const nextWinningRunStartAnswerCount =
|
||||||
nextWinningRunTargetId === null
|
nextWinningRunTargetId == null
|
||||||
? null
|
? null
|
||||||
: ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
: ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
|
||||||
&& winningRunTargetId === nextWinningRunTargetId
|
&& winningRunTargetId === nextWinningRunTargetId
|
||||||
&& winningRunStartAnswerCount !== null)
|
&& winningRunStartAnswerCount != null)
|
||||||
? winningRunStartAnswerCount
|
? winningRunStartAnswerCount
|
||||||
: answers.length)
|
: answers.length)
|
||||||
const nextWinningRunTargetPost =
|
const nextWinningRunTargetPost =
|
||||||
nextWinningRunTargetId === null
|
nextWinningRunTargetId == null
|
||||||
? null
|
? null
|
||||||
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
|
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
|
||||||
const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] =>
|
const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] =>
|
||||||
@@ -2398,8 +2392,8 @@ const nextQuestionPlanFor = (
|
|||||||
if (eligiblePosts.length === 1)
|
if (eligiblePosts.length === 1)
|
||||||
{
|
{
|
||||||
const winningRunFinished =
|
const winningRunFinished =
|
||||||
nextWinningRunTargetId !== null
|
nextWinningRunTargetId != null
|
||||||
&& nextWinningRunStartAnswerCount !== null
|
&& nextWinningRunStartAnswerCount != null
|
||||||
&& eligiblePosts[0]?.id === nextWinningRunTargetId
|
&& eligiblePosts[0]?.id === nextWinningRunTargetId
|
||||||
&& winningRunQuestionCount (
|
&& winningRunQuestionCount (
|
||||||
answers,
|
answers,
|
||||||
@@ -2412,7 +2406,7 @@ const nextQuestionPlanFor = (
|
|||||||
questionMode: null,
|
questionMode: null,
|
||||||
winningRunTargetId: nextWinningRunTargetId,
|
winningRunTargetId: nextWinningRunTargetId,
|
||||||
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
|
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
|
||||||
if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount === null)
|
if (!(nextWinningRunTargetPost) || nextWinningRunStartAnswerCount == null)
|
||||||
return {
|
return {
|
||||||
question: null,
|
question: null,
|
||||||
guess: null,
|
guess: null,
|
||||||
@@ -2586,9 +2580,9 @@ const backgroundPostsFor = ({
|
|||||||
}): Post[] => {
|
}): Post[] => {
|
||||||
const focusPosts =
|
const focusPosts =
|
||||||
phase === 'end' || phase === 'review' || phase === 'learned'
|
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'
|
: phase === 'guess'
|
||||||
? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post !== null)
|
? [displayedGuess, ...eligiblePosts].filter ((post): post is Post => post != null)
|
||||||
: eligiblePosts.length > 0
|
: eligiblePosts.length > 0
|
||||||
? eligiblePosts
|
? eligiblePosts
|
||||||
: availablePosts
|
: availablePosts
|
||||||
@@ -2646,7 +2640,7 @@ const GekanatorBackdrop: FC<{
|
|||||||
const isWinningRunBackdrop =
|
const isWinningRunBackdrop =
|
||||||
!(guessThumbnail)
|
!(guessThumbnail)
|
||||||
&& phase === 'question'
|
&& phase === 'question'
|
||||||
&& winningRunTargetPost !== null
|
&& winningRunTargetPost != null
|
||||||
&& Boolean (backgroundThumbnailUrl (winningRunTargetPost))
|
&& Boolean (backgroundThumbnailUrl (winningRunTargetPost))
|
||||||
const backdropMode =
|
const backdropMode =
|
||||||
guessThumbnail
|
guessThumbnail
|
||||||
@@ -2843,7 +2837,7 @@ const GekanatorBackdrop: FC<{
|
|||||||
setActiveDirection (nextDirection)
|
setActiveDirection (nextDirection)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flipTimerRef.current !== null)
|
if (flipTimerRef.current != null)
|
||||||
{
|
{
|
||||||
window.clearTimeout (flipTimerRef.current)
|
window.clearTimeout (flipTimerRef.current)
|
||||||
flipTimerRef.current = null
|
flipTimerRef.current = null
|
||||||
@@ -2924,7 +2918,7 @@ const GekanatorBackdrop: FC<{
|
|||||||
}, tileFlipDuration * 1000)
|
}, tileFlipDuration * 1000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (flipTimerRef.current !== null)
|
if (flipTimerRef.current != null)
|
||||||
{
|
{
|
||||||
window.clearTimeout (flipTimerRef.current)
|
window.clearTimeout (flipTimerRef.current)
|
||||||
flipTimerRef.current = null
|
flipTimerRef.current = null
|
||||||
@@ -3072,10 +3066,10 @@ const expectedAnswerFor = (
|
|||||||
|
|
||||||
const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||||
const storedGame = useMemo (loadStoredGame, [])
|
const storedGame = useMemo (loadStoredGame, [])
|
||||||
const hasStoredRestore = storedGame !== null && isStoredPhase (storedGame.phase)
|
const hasStoredRestore = storedGame != null && isStoredPhase (storedGame.phase)
|
||||||
const queryClient = useQueryClient ()
|
const queryClient = useQueryClient ()
|
||||||
const isAdmin = user?.role === 'admin'
|
const isAdmin = user?.role === 'admin'
|
||||||
const canPersistGame = user !== null
|
const canPersistGame = user != null
|
||||||
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
|
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
|
||||||
() => loadRecentGames ())
|
() => loadRecentGames ())
|
||||||
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
|
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
|
||||||
@@ -3188,7 +3182,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
setAskedQuestionBank (
|
setAskedQuestionBank (
|
||||||
storedAskedQuestionBankIds
|
storedAskedQuestionBankIds
|
||||||
.map (questionId => questionById.get (questionId))
|
.map (questionId => questionById.get (questionId))
|
||||||
.filter ((question): question is GekanatorQuestion => question !== undefined))
|
.filter ((question): question is GekanatorQuestion => question != null))
|
||||||
setStoredAskedQuestionBankIds ([])
|
setStoredAskedQuestionBankIds ([])
|
||||||
}, [posts,
|
}, [posts,
|
||||||
storedAskedQuestionBankIds,
|
storedAskedQuestionBankIds,
|
||||||
@@ -3340,11 +3334,11 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
?? null,
|
?? null,
|
||||||
[acceptedQuestions, questionSuggestionSelectedId])
|
[acceptedQuestions, questionSuggestionSelectedId])
|
||||||
const canSubmitQuestionSuggestion = useMemo (() => {
|
const canSubmitQuestionSuggestion = useMemo (() => {
|
||||||
if (!(canPersistGame) || reviewCorrectPostId === null)
|
if (!(canPersistGame) || reviewCorrectPostId == null)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if (questionSuggestionEntryMode === 'search')
|
if (questionSuggestionEntryMode === 'search')
|
||||||
return selectedSuggestedQuestion !== null
|
return selectedSuggestedQuestion != null
|
||||||
|
|
||||||
return questionSuggestion.trim () !== ''
|
return questionSuggestion.trim () !== ''
|
||||||
}, [
|
}, [
|
||||||
@@ -3403,7 +3397,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
userPriorWeights, materialIndex, acceptedQuestionMatchIndex,
|
userPriorWeights, materialIndex, acceptedQuestionMatchIndex,
|
||||||
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
||||||
const winningRunTargetPost = useMemo (
|
const winningRunTargetPost = useMemo (
|
||||||
() => questionPlan.winningRunTargetId === null
|
() => questionPlan.winningRunTargetId == null
|
||||||
? null
|
? null
|
||||||
: posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null,
|
: posts.find (post => post.id === questionPlan.winningRunTargetId) ?? null,
|
||||||
[posts, questionPlan.winningRunTargetId])
|
[posts, questionPlan.winningRunTargetId])
|
||||||
@@ -3417,7 +3411,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
&& winningRunQuestionsAsked < winningRunQuestionLimit
|
&& winningRunQuestionsAsked < winningRunQuestionLimit
|
||||||
&& eligiblePosts.length === 1
|
&& eligiblePosts.length === 1
|
||||||
&& eligiblePosts[0]?.id === questionPlan.winningRunTargetId
|
&& eligiblePosts[0]?.id === questionPlan.winningRunTargetId
|
||||||
&& winningRunTargetPost !== null
|
&& winningRunTargetPost != null
|
||||||
const topScoredPosts = useMemo (
|
const topScoredPosts = useMemo (
|
||||||
() => eligiblePosts
|
() => eligiblePosts
|
||||||
.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
|
.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
|
posts.find (post => post.id === reviewCorrectPostId) ?? null
|
||||||
const effectiveResultWon =
|
const effectiveResultWon =
|
||||||
resultWon
|
resultWon
|
||||||
?? ((reviewGuessedPostId !== null && reviewCorrectPostId !== null)
|
?? ((reviewGuessedPostId != null && reviewCorrectPostId != null)
|
||||||
? reviewGuessedPostId === reviewCorrectPostId
|
? reviewGuessedPostId === reviewCorrectPostId
|
||||||
: null)
|
: null)
|
||||||
const effectiveBackgroundMotionMode =
|
const effectiveBackgroundMotionMode =
|
||||||
@@ -3895,7 +3889,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startReview = () => {
|
const startReview = () => {
|
||||||
if (reviewGuessedPostId === null || reviewCorrectPostId === null)
|
if (reviewGuessedPostId == null || reviewCorrectPostId == null)
|
||||||
return
|
return
|
||||||
|
|
||||||
saveMutation.reset ()
|
saveMutation.reset ()
|
||||||
@@ -3915,13 +3909,13 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
const saveReviewedResult = (onSuccess: (gameId: number) => void) => {
|
const saveReviewedResult = (onSuccess: (gameId: number) => void) => {
|
||||||
if (
|
if (
|
||||||
!(canPersistGame)
|
!(canPersistGame)
|
||||||
|| reviewGuessedPostId === null
|
|| reviewGuessedPostId == null
|
||||||
|| reviewCorrectPostId === null
|
|| reviewCorrectPostId == null
|
||||||
|| saveMutation.isPending
|
|| saveMutation.isPending
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (savedGameId !== null)
|
if (savedGameId != null)
|
||||||
{
|
{
|
||||||
onSuccess (savedGameId)
|
onSuccess (savedGameId)
|
||||||
return
|
return
|
||||||
@@ -3979,7 +3973,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
const saveExtraQuestions = () => {
|
const saveExtraQuestions = () => {
|
||||||
if (
|
if (
|
||||||
!(canPersistGame)
|
!(canPersistGame)
|
||||||
|| savedGameId === null
|
|| savedGameId == null
|
||||||
|| extraQuestionAnswersMutation.isPending
|
|| extraQuestionAnswersMutation.isPending
|
||||||
|| extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)]))
|
|| extraQuestions.some (question => !(extraQuestionAnswers[String (question.id)]))
|
||||||
)
|
)
|
||||||
@@ -4175,7 +4169,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startExtraQuestions = () => {
|
const startExtraQuestions = () => {
|
||||||
if (reviewCorrectPostId === null || saveMutation.isPending)
|
if (reviewCorrectPostId == null || saveMutation.isPending)
|
||||||
return
|
return
|
||||||
|
|
||||||
saveReviewedResult (gameId => {
|
saveReviewedResult (gameId => {
|
||||||
@@ -4206,7 +4200,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
const dialogue = phase === 'learned' ? resultDialogue : introDialogue
|
const dialogue = phase === 'learned' ? resultDialogue : introDialogue
|
||||||
const saveStatusMessage =
|
const saveStatusMessage =
|
||||||
saved
|
saved
|
||||||
&& learnedExampleCount !== null
|
&& learnedExampleCount != null
|
||||||
? learnedExampleCount > 0
|
? learnedExampleCount > 0
|
||||||
? `${ learnedExampleCount }件の回答を学習しました`
|
? `${ learnedExampleCount }件の回答を学習しました`
|
||||||
: null
|
: null
|
||||||
@@ -4436,7 +4430,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
{' / '}
|
{' / '}
|
||||||
recoveryStepCount: {recoveryStepCount}
|
recoveryStepCount: {recoveryStepCount}
|
||||||
{' / '}
|
{' / '}
|
||||||
currentQuestion===null: {String (currentQuestion === null)}
|
currentQuestion===null: {String (currentQuestion == null)}
|
||||||
</div>
|
</div>
|
||||||
{topScoredPosts.length > 0 && (
|
{topScoredPosts.length > 0 && (
|
||||||
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1">
|
<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}
|
recoveryStepCount: {recoveryStepCount}
|
||||||
{' / '}
|
{' / '}
|
||||||
currentQuestion===null: {String (currentQuestion === null)}
|
currentQuestion===null: {String (currentQuestion == null)}
|
||||||
</div>)}
|
</div>)}
|
||||||
<PostMiniCard post={displayedGuess}/>
|
<PostMiniCard post={displayedGuess}/>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -4621,7 +4615,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
{reviewGuessedPostId != null && reviewCorrectPostId != null && (
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
||||||
</p>)}
|
</p>)}
|
||||||
@@ -4648,7 +4642,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
|
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
|
||||||
hover:bg-pink-500 disabled:opacity-50"
|
hover:bg-pink-500 disabled:opacity-50"
|
||||||
disabled={reviewCorrectPostId === null || saveMutation.isPending}
|
disabled={reviewCorrectPostId == null || saveMutation.isPending}
|
||||||
onClick={saveAndReset}>
|
onClick={saveAndReset}>
|
||||||
もう一度
|
もう一度
|
||||||
</button>
|
</button>
|
||||||
@@ -4659,7 +4653,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
dark:hover:bg-red-900 disabled:opacity-50"
|
dark:hover:bg-red-900 disabled:opacity-50"
|
||||||
disabled={
|
disabled={
|
||||||
!(canPersistGame)
|
!(canPersistGame)
|
||||||
|| reviewCorrectPostId === null
|
|| reviewCorrectPostId == null
|
||||||
|| saveMutation.isPending
|
|| saveMutation.isPending
|
||||||
}
|
}
|
||||||
onClick={startReview}>
|
onClick={startReview}>
|
||||||
@@ -4682,7 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
hover:bg-yellow-100 dark:border-red-700
|
hover:bg-yellow-100 dark:border-red-700
|
||||||
dark:hover:bg-red-900 disabled:opacity-50"
|
dark:hover:bg-red-900 disabled:opacity-50"
|
||||||
disabled={!(canPersistGame)
|
disabled={!(canPersistGame)
|
||||||
|| reviewCorrectPostId === null
|
|| reviewCorrectPostId == null
|
||||||
|| saveMutation.isPending
|
|| saveMutation.isPending
|
||||||
|| extraQuestionState === 'loading'
|
|| extraQuestionState === 'loading'
|
||||||
|| extraQuestionAnswersMutation.isPending}
|
|| extraQuestionAnswersMutation.isPending}
|
||||||
@@ -4768,7 +4762,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
{reviewGuessedPostId != null && reviewCorrectPostId != null && (
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
判定:
|
判定:
|
||||||
{reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
{reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
||||||
@@ -4798,7 +4792,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|||||||
hover:bg-pink-500 disabled:opacity-50"
|
hover:bg-pink-500 disabled:opacity-50"
|
||||||
disabled={
|
disabled={
|
||||||
!(canPersistGame)
|
!(canPersistGame)
|
||||||
|| reviewCorrectPostId === null
|
|| reviewCorrectPostId == null
|
||||||
|| saveMutation.isPending
|
|| saveMutation.isPending
|
||||||
|| questionSuggestionMutation.isPending
|
|| questionSuggestionMutation.isPending
|
||||||
}
|
}
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする