Reviewed-on: #372 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #372 でマージされました.
このコミットが含まれているのは:
@@ -30,7 +30,7 @@ export type GekanatorQuestionSource =
|
||||
| 'ai_generated'
|
||||
| 'admin_curated'
|
||||
|
||||
export type GekanatorPerformanceMode = 'lite' | 'normal'
|
||||
export type GekanatorPerformanceMode = 'normal'
|
||||
|
||||
export type GekanatorQuestionCondition =
|
||||
| { type: 'tag'; key: string }
|
||||
|
||||
@@ -29,7 +29,6 @@ import type { FC } from 'react'
|
||||
import type { GekanatorAnswerLog,
|
||||
GekanatorAnswerValue,
|
||||
GekanatorExtraQuestion,
|
||||
GekanatorPerformanceMode,
|
||||
GekanatorQuestionCondition,
|
||||
GekanatorQuestionKind,
|
||||
GekanatorQuestion,
|
||||
@@ -162,7 +161,6 @@ const confidenceTemperature = 6
|
||||
const gameStorageKey = 'gekanator:game:v1'
|
||||
const recentGamesStorageKey = 'gekanator:recent-games:v1'
|
||||
const backgroundMotionStorageKey = 'gekanator:background-motion:v1'
|
||||
const performanceModeStorageKey = 'gekanator:performance-mode:v1'
|
||||
const maxQuestionSuggestionsPerGame = 3
|
||||
const maxStoredRecentGames = 12
|
||||
const mascotAssetByState: Record<MascotState, string> = {
|
||||
@@ -391,14 +389,8 @@ const storeRecentGameSummary = (
|
||||
}
|
||||
|
||||
|
||||
const loadBackgroundMotionMode = (
|
||||
performanceMode?: GekanatorPerformanceMode,
|
||||
): BackgroundMotionMode => {
|
||||
const fallbackMode =
|
||||
performanceMode === 'lite' ? 'off'
|
||||
: performanceMode === 'normal' ? 'on'
|
||||
: detectDefaultPerformanceMode () === 'lite' ? 'off'
|
||||
: 'on'
|
||||
const loadBackgroundMotionMode = (): BackgroundMotionMode => {
|
||||
const fallbackMode = 'on'
|
||||
try
|
||||
{
|
||||
const raw = localStorage.getItem (backgroundMotionStorageKey)
|
||||
@@ -414,38 +406,6 @@ const loadBackgroundMotionMode = (
|
||||
}
|
||||
|
||||
|
||||
const detectDefaultPerformanceMode = (): GekanatorPerformanceMode => {
|
||||
if (typeof window === 'undefined')
|
||||
return 'normal'
|
||||
|
||||
const isMobileWidth =
|
||||
typeof window.matchMedia === 'function'
|
||||
? window.matchMedia ('(max-width: 767px)').matches
|
||||
: window.innerWidth <= 767
|
||||
const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory
|
||||
if ((typeof memory === 'number' && memory <= 4) || isMobileWidth)
|
||||
return 'lite'
|
||||
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
|
||||
const loadPerformanceMode = (): GekanatorPerformanceMode => {
|
||||
try
|
||||
{
|
||||
const raw = localStorage.getItem (performanceModeStorageKey)
|
||||
if (raw === 'lite' || raw === 'normal')
|
||||
return raw
|
||||
}
|
||||
catch
|
||||
{
|
||||
return detectDefaultPerformanceMode ()
|
||||
}
|
||||
|
||||
return detectDefaultPerformanceMode ()
|
||||
}
|
||||
|
||||
|
||||
const resettableExtraQuestionState = (): {
|
||||
extraQuestions: GekanatorExtraQuestion[]
|
||||
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
|
||||
@@ -1055,11 +1015,9 @@ const rankedEntriesForCounts = <T extends string | number> (
|
||||
const buildQuestionsForCandidateIds = (
|
||||
{ candidateIds,
|
||||
materialIndex,
|
||||
performanceMode,
|
||||
acceptedQuestions }: { candidateIds: number[]
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
performanceMode: GekanatorPerformanceMode
|
||||
acceptedQuestions: GekanatorQuestion[] },
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
acceptedQuestions: GekanatorQuestion[] },
|
||||
): GekanatorQuestion[] => {
|
||||
const total = candidateIds.length
|
||||
if (total === 0)
|
||||
@@ -1089,23 +1047,16 @@ const buildQuestionsForCandidateIds = (
|
||||
const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
|
||||
if (monthDay)
|
||||
monthDayCounts.set (monthDay, (monthDayCounts.get (monthDay) ?? 0) + 1)
|
||||
if (performanceMode === 'normal')
|
||||
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
|
||||
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
|
||||
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
|
||||
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
|
||||
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
|
||||
titleLengths.push (titleLength)
|
||||
if (materialIndex.titleAsciiPostIds.has (postId))
|
||||
++asciiCount
|
||||
})
|
||||
|
||||
const tagCap =
|
||||
performanceMode === 'lite'
|
||||
? total >= 80 ? 96 : 64
|
||||
: total >= 120 ? 128 : 96
|
||||
const titleTermCap =
|
||||
performanceMode === 'lite'
|
||||
? 0
|
||||
: total >= 80 ? 10 : total >= 24 ? 14 : 20
|
||||
const tagCap = total >= 120 ? 128 : 96
|
||||
const titleTermCap = total >= 80 ? 10 : total >= 24 ? 14 : 20
|
||||
const factCap = total >= 80 ? 8 : 12
|
||||
const sortedLengths = [...titleLengths].sort ((a, b) => a - b)
|
||||
const titleLengthMedian = sortedLengths[Math.floor (sortedLengths.length / 2)] ?? 0
|
||||
@@ -1181,17 +1132,16 @@ const buildQuestionsForCandidateIds = (
|
||||
materialIndex }))
|
||||
})
|
||||
|
||||
if (performanceMode === 'normal')
|
||||
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
|
||||
.filter (([term]) => String (term).length <= 24)
|
||||
.forEach (([term]) => {
|
||||
questions.push (buildIndexedQuestion ({
|
||||
condition: { type: 'title-contains', text: String (term) },
|
||||
text: `題名に「${ term }」が含まれる?`,
|
||||
kind: 'title',
|
||||
priorityWeight: .96,
|
||||
materialIndex }))
|
||||
})
|
||||
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
|
||||
.filter (([term]) => String (term).length <= 24)
|
||||
.forEach (([term]) => {
|
||||
questions.push (buildIndexedQuestion ({
|
||||
condition: { type: 'title-contains', text: String (term) },
|
||||
text: `題名に「${ term }」が含まれる?`,
|
||||
kind: 'title',
|
||||
priorityWeight: .96,
|
||||
materialIndex }))
|
||||
})
|
||||
|
||||
return mergeQuestions ([...questions, ...acceptedQuestions])
|
||||
}
|
||||
@@ -1615,41 +1565,33 @@ const contradictionPenaltyFor = ({
|
||||
}
|
||||
|
||||
|
||||
const chooseQuestion = ({
|
||||
posts,
|
||||
questions,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
}: {
|
||||
posts: Post[]
|
||||
questions: GekanatorQuestion[]
|
||||
scores: Map<number, number>
|
||||
answers: GekanatorAnswerLog[]
|
||||
askedIds: Set<string>
|
||||
gameSeed: string
|
||||
recentFirstQuestionPenaltyById: Map<string, number>
|
||||
userPriorWeights: Map<number, number>
|
||||
performanceMode: GekanatorPerformanceMode
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
}): GekanatorQuestion | null => {
|
||||
const candidateIds = posts.map (post => post.id)
|
||||
const candidateIdSet = new Set (candidateIds)
|
||||
const chooseQuestion = (
|
||||
{ posts,
|
||||
questions,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
materialIndex,
|
||||
matchIndex }: { posts: Post[]
|
||||
questions: GekanatorQuestion[]
|
||||
scores: Map<number, number>
|
||||
answers: GekanatorAnswerLog[]
|
||||
askedIds: Set<string>
|
||||
gameSeed: string
|
||||
recentFirstQuestionPenaltyById: Map<string, number>
|
||||
userPriorWeights: Map<number, number>
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex },
|
||||
): GekanatorQuestion | null => {
|
||||
const dynamicMatchIndex = new Map<string, Set<number>> ()
|
||||
|
||||
const invertedSignature = (signature: string): string =>
|
||||
signature.replace (/[01]/g, value => value === '1' ? '0' : '1')
|
||||
|
||||
const redundantSignatures = (
|
||||
candidates: Post[],
|
||||
): Set<string> => {
|
||||
const redundantSignatures = (candidates: Post[]): Set<string> => {
|
||||
const signatures = new Set<string> ()
|
||||
questions
|
||||
.filter (question => askedIds.has (question.id))
|
||||
@@ -1668,89 +1610,6 @@ const chooseQuestion = ({
|
||||
return signatures
|
||||
}
|
||||
|
||||
if (performanceMode === 'lite')
|
||||
{
|
||||
const nonTagCount =
|
||||
questions.filter (question => askedIds.has (question.id) && question.kind !== 'tag').length
|
||||
const ranked = questions
|
||||
.filter (question => !(askedIds.has (question.id)))
|
||||
.map (question => {
|
||||
if (isQuestionHardFilteredAfterAnswers (question, answers))
|
||||
return null
|
||||
|
||||
const yes = matchingPostCountInIds ({
|
||||
candidateIds,
|
||||
candidateIdSet,
|
||||
posts,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
question,
|
||||
dynamicMatchIndex })
|
||||
const no = posts.length - yes
|
||||
if (yes === 0 || no === 0)
|
||||
return null
|
||||
|
||||
const splitScore = Math.abs (posts.length / 2 - yes) / posts.length
|
||||
const minSide = posts.length < 10 ? 1 : Math.max (2, Math.floor (posts.length * .08))
|
||||
const narrowPenalty = yes < minSide || no < minSide ? .18 : 0
|
||||
const tagPenalty = question.kind === 'tag' && nonTagCount < 3 ? .1 : 0
|
||||
const contradictionPenalty = contradictionPenaltyFor ({ question, answers })
|
||||
const sourceBonus = sourcePriorityOffset (question)
|
||||
const priorityBonus = priorityWeightOffset (question)
|
||||
const categoryPenalty = questionCategoryPenalty (question, answers.length, 0)
|
||||
|
||||
return {
|
||||
question,
|
||||
score: splitScore * 100
|
||||
+ narrowPenalty
|
||||
+ tagPenalty
|
||||
+ contradictionPenalty
|
||||
+ sourceBonus
|
||||
+ priorityBonus
|
||||
+ categoryPenalty,
|
||||
narrow: narrowPenalty > 0 }
|
||||
})
|
||||
.filter ((item): item is {
|
||||
question: GekanatorQuestion
|
||||
score: number
|
||||
narrow: boolean } => item !== null && Number.isFinite (item.score))
|
||||
.sort ((a, b) => {
|
||||
if (a.score !== b.score)
|
||||
return a.score - b.score
|
||||
|
||||
return a.question.id.localeCompare (b.question.id)
|
||||
})
|
||||
const pool = (
|
||||
ranked.some (item => !(item.narrow))
|
||||
? ranked.filter (item => !(item.narrow))
|
||||
: ranked)
|
||||
.slice (0, 8)
|
||||
|
||||
if (pool.length === 0)
|
||||
return null
|
||||
|
||||
const bestScore = pool[0]?.score ?? 0
|
||||
const weightedPool = pool.map (item => ({
|
||||
...item,
|
||||
weight: Math.exp ((bestScore - item.score) / 1.6) }))
|
||||
const totalPoolWeight =
|
||||
weightedPool.reduce ((sum, item) => sum + item.weight, 0) || 1
|
||||
const seed = `${ gameSeed }:lite:${ [...askedIds].sort ().join ('|') }:${
|
||||
weightedPool.map (item => `${ item.question.id }:${ item.score.toFixed (4) }`).join ('|')
|
||||
}`
|
||||
const target = deterministicUnitFloat (seed) * totalPoolWeight
|
||||
let cumulative = 0
|
||||
|
||||
for (const item of weightedPool)
|
||||
{
|
||||
cumulative += item.weight
|
||||
if (target <= cumulative)
|
||||
return item.question
|
||||
}
|
||||
|
||||
return weightedPool[weightedPool.length - 1]?.question ?? null
|
||||
}
|
||||
|
||||
const scoredPosts = posts
|
||||
.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
|
||||
.sort ((a, b) => b.score - a.score)
|
||||
@@ -2361,7 +2220,6 @@ const nextQuestionPlanFor = (
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex,
|
||||
lastGuessQuestionCount,
|
||||
@@ -2376,7 +2234,6 @@ const nextQuestionPlanFor = (
|
||||
gameSeed: string
|
||||
recentFirstQuestionPenaltyById: Map<string, number>
|
||||
userPriorWeights: Map<number, number>
|
||||
performanceMode: GekanatorPerformanceMode
|
||||
materialIndex: GekanatorQuestionMaterialIndex
|
||||
matchIndex: GekanatorMatchIndex
|
||||
lastGuessQuestionCount: number
|
||||
@@ -2439,7 +2296,6 @@ const nextQuestionPlanFor = (
|
||||
buildQuestionsForCandidateIds ({
|
||||
candidateIds: scopePosts.map (post => post.id),
|
||||
materialIndex,
|
||||
performanceMode,
|
||||
acceptedQuestions })
|
||||
|
||||
if (eligiblePosts.length === 1)
|
||||
@@ -2505,7 +2361,6 @@ const nextQuestionPlanFor = (
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex })
|
||||
|
||||
@@ -3108,10 +2963,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
const canPersistGame = user !== null
|
||||
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
|
||||
() => loadRecentGames ())
|
||||
const [performanceMode] =
|
||||
useState<GekanatorPerformanceMode> (() => loadPerformanceMode ())
|
||||
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
|
||||
() => loadBackgroundMotionMode (loadPerformanceMode ()))
|
||||
() => loadBackgroundMotionMode ())
|
||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState (false)
|
||||
const [gameSeed, setGameSeed] = useState (
|
||||
storedGame?.gameSeed ?? createGameSeed ())
|
||||
@@ -3328,17 +3181,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
}
|
||||
}, [backgroundMotionMode])
|
||||
|
||||
useEffect (() => {
|
||||
try
|
||||
{
|
||||
localStorage.setItem (performanceModeStorageKey, performanceMode)
|
||||
}
|
||||
catch
|
||||
{
|
||||
return
|
||||
}
|
||||
}, [performanceMode])
|
||||
|
||||
const askedQuestionById = useMemo (
|
||||
() => new Map (askedQuestionBank.map (question => [question.id, question])),
|
||||
[askedQuestionBank])
|
||||
@@ -3361,9 +3203,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
() => new Map (scoringQuestions.map (question => [question.id, question])),
|
||||
[scoringQuestions])
|
||||
const recentFirstQuestionPenaltyById = useMemo (() => {
|
||||
if (performanceMode === 'lite')
|
||||
return new Map<string, number> ()
|
||||
|
||||
const penalties = new Map<string, number> ()
|
||||
|
||||
recentGames.forEach ((game, index) => {
|
||||
@@ -3376,12 +3215,10 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
})
|
||||
|
||||
return penalties
|
||||
}, [performanceMode, recentGames])
|
||||
}, [recentGames])
|
||||
const userPriorWeights = useMemo (
|
||||
() => performanceMode === 'lite'
|
||||
? new Map<number, number> ()
|
||||
: userPriorWeightsFor (posts, recentGames),
|
||||
[performanceMode, posts, recentGames])
|
||||
() => userPriorWeightsFor (posts, recentGames),
|
||||
[posts, recentGames])
|
||||
const availablePosts = useMemo (
|
||||
() => posts.filter (post => !(rejectedPostIds.has (post.id))),
|
||||
[posts, rejectedPostIds])
|
||||
@@ -3397,7 +3234,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex: acceptedQuestionMatchIndex,
|
||||
lastGuessQuestionCount,
|
||||
@@ -3405,7 +3241,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
winningRunStartAnswerCount }),
|
||||
[posts, eligiblePosts, availablePosts, acceptedQuestions, scores,
|
||||
answers, askedIds, gameSeed, recentFirstQuestionPenaltyById,
|
||||
userPriorWeights, performanceMode, materialIndex, acceptedQuestionMatchIndex,
|
||||
userPriorWeights, materialIndex, acceptedQuestionMatchIndex,
|
||||
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
||||
const winningRunTargetPost = useMemo (
|
||||
() => questionPlan.winningRunTargetId === null
|
||||
@@ -3458,35 +3294,28 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
const reviewCorrectPost =
|
||||
posts.find (post => post.id === reviewCorrectPostId) ?? null
|
||||
const effectiveResultWon =
|
||||
resultWon ?? (
|
||||
reviewGuessedPostId !== null
|
||||
&& reviewCorrectPostId !== null
|
||||
? reviewGuessedPostId === reviewCorrectPostId
|
||||
: null)
|
||||
resultWon
|
||||
?? ((reviewGuessedPostId !== null && reviewCorrectPostId !== null)
|
||||
? reviewGuessedPostId === reviewCorrectPostId
|
||||
: null)
|
||||
const effectiveBackgroundMotionMode =
|
||||
performanceMode === 'lite'
|
||||
? 'off'
|
||||
: backgroundMotionMode === 'off'
|
||||
? 'off'
|
||||
: prefersReducedMotion
|
||||
? 'calm'
|
||||
: backgroundMotionMode
|
||||
backgroundMotionMode === 'off'
|
||||
? 'off'
|
||||
: (prefersReducedMotion
|
||||
? 'calm'
|
||||
: backgroundMotionMode)
|
||||
const backgroundPosts = useMemo (
|
||||
() => performanceMode === 'lite'
|
||||
? []
|
||||
: backgroundPostsFor ({
|
||||
() => backgroundPostsFor ({
|
||||
phase,
|
||||
eligiblePosts,
|
||||
availablePosts,
|
||||
displayedGuess,
|
||||
reviewCorrectPost,
|
||||
reviewGuessedPost }),
|
||||
[performanceMode, phase, eligiblePosts, availablePosts, displayedGuess,
|
||||
reviewCorrectPost, reviewGuessedPost])
|
||||
[phase, eligiblePosts, availablePosts, displayedGuess, reviewCorrectPost,
|
||||
reviewGuessedPost])
|
||||
const backgroundVisualSeed =
|
||||
performanceMode === 'lite'
|
||||
? ''
|
||||
: `${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
|
||||
`${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
|
||||
questionPlan.question?.id ?? ''
|
||||
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
|
||||
rejectedPostIds.size
|
||||
@@ -3617,7 +3446,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
let recoveredQuestions = buildQuestionsForCandidateIds ({
|
||||
candidateIds: recoveredEligiblePosts.map (post => post.id),
|
||||
materialIndex,
|
||||
performanceMode,
|
||||
acceptedQuestions })
|
||||
let recoveredScoringQuestions = mergeQuestions ([
|
||||
...recoveredQuestions,
|
||||
@@ -3643,7 +3471,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
recoveredQuestions = buildQuestionsForCandidateIds ({
|
||||
candidateIds: recoveredEligiblePosts.map (post => post.id),
|
||||
materialIndex,
|
||||
performanceMode,
|
||||
acceptedQuestions })
|
||||
recoveredScoringQuestions = mergeQuestions ([
|
||||
...recoveredQuestions,
|
||||
@@ -3667,7 +3494,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex: acceptedQuestionMatchIndex })
|
||||
const fallbackQuestion = nextQuestion ?? chooseFallbackQuestion ({
|
||||
@@ -3735,7 +3561,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
}, [
|
||||
posts,
|
||||
gameSeed,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
acceptedQuestions,
|
||||
acceptedQuestionMatchIndex,
|
||||
@@ -3806,7 +3631,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex: acceptedQuestionMatchIndex,
|
||||
lastGuessQuestionCount,
|
||||
@@ -3840,7 +3664,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex: acceptedQuestionMatchIndex,
|
||||
lastGuessQuestionCount,
|
||||
@@ -4071,7 +3894,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
gameSeed,
|
||||
recentFirstQuestionPenaltyById,
|
||||
userPriorWeights,
|
||||
performanceMode,
|
||||
materialIndex,
|
||||
matchIndex: acceptedQuestionMatchIndex,
|
||||
lastGuessQuestionCount,
|
||||
@@ -4286,16 +4108,15 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
{performanceMode !== 'lite' && (
|
||||
<GekanatorBackdrop
|
||||
posts={backgroundPosts}
|
||||
mascotAsset={mascotAsset}
|
||||
phase={phase}
|
||||
displayedGuess={displayedGuess}
|
||||
visualSeed={backgroundVisualSeed}
|
||||
motionMode={effectiveBackgroundMotionMode}
|
||||
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
|
||||
winningRunQuestionCount={winningRunQuestionsAsked}/>)}
|
||||
<GekanatorBackdrop
|
||||
posts={backgroundPosts}
|
||||
mascotAsset={mascotAsset}
|
||||
phase={phase}
|
||||
displayedGuess={displayedGuess}
|
||||
visualSeed={backgroundVisualSeed}
|
||||
motionMode={effectiveBackgroundMotionMode}
|
||||
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
|
||||
winningRunQuestionCount={winningRunQuestionsAsked}/>
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-4xl space-y-6">
|
||||
<header className="flex flex-wrap items-end justify-between gap-3">
|
||||
@@ -4305,32 +4126,31 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{performanceMode === 'normal' && (
|
||||
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
|
||||
text-xs shadow-sm backdrop-blur dark:border-red-800
|
||||
dark:bg-red-950/75">
|
||||
<span className="mr-2 font-bold text-neutral-600 dark:text-neutral-300">
|
||||
背景
|
||||
</span>
|
||||
{[{ mode: 'off' as const, label: 'オフ' },
|
||||
{ mode: 'on' as const, label: 'オン' }]
|
||||
.map (({ mode, label }) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
className={cn (
|
||||
'rounded-full px-2.5 py-1 transition-colors',
|
||||
backgroundMotionMode === mode
|
||||
? 'bg-pink-600 text-white'
|
||||
: 'text-neutral-600 hover:bg-yellow-100 dark:text-neutral-300 dark:hover:bg-red-900')}
|
||||
onClick={() => setBackgroundMotionMode (mode)}>
|
||||
{label}
|
||||
</button>))}
|
||||
{prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && (
|
||||
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||
端末設定により控えめ表示
|
||||
</span>)}
|
||||
</div>)}
|
||||
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
|
||||
text-xs shadow-sm backdrop-blur dark:border-red-800
|
||||
dark:bg-red-950/75">
|
||||
<span className="mr-2 font-bold text-neutral-600 dark:text-neutral-300">
|
||||
背景
|
||||
</span>
|
||||
{[{ mode: 'off' as const, label: 'オフ' },
|
||||
{ mode: 'on' as const, label: 'オン' }]
|
||||
.map (({ mode, label }) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
className={cn (
|
||||
'rounded-full px-2.5 py-1 transition-colors',
|
||||
backgroundMotionMode === mode
|
||||
? 'bg-pink-600 text-white'
|
||||
: 'text-neutral-600 hover:bg-yellow-100 dark:text-neutral-300 dark:hover:bg-red-900')}
|
||||
onClick={() => setBackgroundMotionMode (mode)}>
|
||||
{label}
|
||||
</button>))}
|
||||
{prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && (
|
||||
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||
端末設定により控えめ表示
|
||||
</span>)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -4608,7 +4428,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
|
||||
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'}
|
||||
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
||||
</p>)}
|
||||
|
||||
{saveMutation.isError && (
|
||||
|
||||
新しい課題から参照
ユーザをブロックする