このコミットが含まれているのは:
2026-06-14 02:35:54 +09:00
コミット 01b063f473
2個のファイルの変更258行の追加229行の削除
+10
ファイルの表示
@@ -158,6 +158,13 @@ npm run preview
- Keep page-level code under `frontend/src/pages` and shared UI/feature code
under `frontend/src/components` unless existing patterns point elsewhere.
- Match existing Tailwind, component, and import alias conventions.
- In TypeScript and TSX, prefer direct comparison operators such as `===` and
`!==` over negating a comparison like `!(a === b)`.
- In TypeScript and TSX, prefer `++i` or `--i` over `i += 1` or `i -= 1` for
simple unit-step counter updates.
- For user-facing Japanese text, prefer modern kana usage and natural current
phrasing over historical spellings or awkward literal wording.
- For user-facing Japanese ellipses, prefer `……` over ASCII `...`.
### Frontend TSX style
@@ -179,6 +186,9 @@ npm run preview
single physical line.
- Always add braces around `if`, `else`, or `for` bodies when the body spans
two or more physical lines, even if it is one statement.
- Do not use a leading semicolon for expression statements such as
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
explicitly, for example with `void`.
Preferred:
+248 -229
ファイルの表示
@@ -123,14 +123,18 @@ type RecentGameSummary = {
savedAt: number }
type BackgroundMotionMode = 'on' | 'calm' | 'off'
type GuessReason =
| 'hard_max_questions'
| 'winning_run_finished'
| 'question_count_checkpoint'
| 'question_generation_stalled'
type QuestionMode =
| 'winning_run'
| 'normal'
| null
type MascotState =
| 'idle'
| 'thinking_far'
@@ -214,9 +218,9 @@ const normalizeStoredQuestionId = (
if (questionId.startsWith ('title:length-greater-than:'))
{
const length = Number (questionId.split (':').pop ())
if (Number.isInteger (length))
return `title:length-at-least:${ length + 1 }`
const length = Number (questionId.split (':').pop ())
if (Number.isInteger (length))
return `title:length-at-least:${ length + 1 }`
}
return questionId
@@ -227,30 +231,27 @@ const normalizeStoredGame = (game: StoredGekanatorGame): StoredGekanatorGame =>
...game,
answers: game.answers.map (answer => ({
...answer,
questionId: normalizeStoredQuestionId (
answer.questionId,
answer.questionCondition),
questionMode:
answer.questionMode === 'winning_run' || answer.questionMode === 'normal'
? answer.questionMode
: undefined,
questionCondition: answer.questionCondition
? normalizeTitleLengthCondition (answer.questionCondition)
: undefined })),
questionId: normalizeStoredQuestionId (answer.questionId,
answer.questionCondition),
questionMode: ((answer.questionMode === 'winning_run' || answer.questionMode === 'normal')
? answer.questionMode
: undefined),
questionCondition: (answer.questionCondition
? normalizeTitleLengthCondition (answer.questionCondition)
: undefined) })),
askedIds: game.askedIds.map (questionId => normalizeStoredQuestionId (questionId)),
softenedQuestionIds: game.softenedQuestionIds.map (questionId =>
normalizeStoredQuestionId (questionId)),
softenedQuestionIds: (game.softenedQuestionIds
.map (questionId => normalizeStoredQuestionId (questionId))),
recoveredCandidatePosts: game.recoveredCandidatePosts ?? [],
recoveryStepCount: game.recoveryStepCount ?? 0,
winningRunTargetId: game.winningRunTargetId ?? null,
winningRunStartAnswerCount: game.winningRunStartAnswerCount ?? null,
askedQuestionBank: game.askedQuestionBank?.map (question =>
({
...question,
id: normalizeStoredQuestionId (question.id, question.condition),
condition: normalizeTitleLengthCondition (question.condition) })),
askedQuestionBankIds: game.askedQuestionBankIds?.map (questionId =>
normalizeStoredQuestionId (questionId)) })
askedQuestionBank: game.askedQuestionBank?.map (question => ({
...question,
id: normalizeStoredQuestionId (question.id, question.condition),
condition: normalizeTitleLengthCondition (question.condition) })),
askedQuestionBankIds: (
game.askedQuestionBankIds?.map (questionId => normalizeStoredQuestionId (questionId))) })
const sourcePriorityForMerge = (question: GekanatorQuestion): number => {
@@ -290,10 +291,10 @@ const shouldReplaceMergedQuestion = (
const hashString = (value: string): number => {
let hash = 2166136261
for (let i = 0; i < value.length; i += 1)
for (let i = 0; i < value.length; ++i)
{
hash ^= value.charCodeAt (i)
hash = Math.imul (hash, 16777619)
hash ^= value.charCodeAt (i)
hash = Math.imul (hash, 16777619)
}
return hash >>> 0
@@ -694,7 +695,7 @@ const buildMaterialIndex = (
const tagKeys = post.tags
.filter (tag =>
!(tag.category === 'meta')
tag.category !== 'meta'
&& !(tag.name.includes ('タグ希望'))
&& !(tag.name.includes ('bot操作')))
.map (tag => `${ tag.category }:${ tag.name }`)
@@ -781,17 +782,17 @@ const indexedQuestionTextForTag = (key: string): string => {
switch (category)
{
case 'deerjikist':
return `作者・ニジラーとして「${ label }」に関係してる?`
return `ニジラーとして「${ label }」に関係してる?`
case 'meme':
return `元ネタ・ミームとして「${ label }に関係しう?`
return `${ label }に関係しう?`
case 'character':
return `${ label }」といキャラクターが関係してる?`
return `${ label }」といキャラクターが関係してる?`
case 'material':
return `素材として${ label }」に関係してる?`
return `素材「${ label }」に関係してる?`
case 'nico':
return `ニコニコに「${ label }」といふタグがいてる?`
return `ニコニコに「${ label }」といふタグがいてる?`
default:
return `内容として${ label }に関係しさう?`
return `${ label }が含まれる?`
}
}
@@ -915,32 +916,30 @@ const matchingPostCountInIds = ({
if (matched.size < ids.size)
matched.forEach (postId => {
if (ids.has (postId))
count += 1
++count
})
else
ids.forEach (postId => {
if (matched.has (postId))
count += 1
++count
})
return count
}
const matchingWeightInCandidates = ({
candidates,
posts,
materialIndex,
matchIndex,
question,
dynamicMatchIndex,
}: {
candidates: { post: Post; weight: number }[]
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
question: GekanatorQuestion
dynamicMatchIndex?: GekanatorMatchIndex
}): number => {
const matchingWeightInCandidates = (
{ candidates,
posts,
materialIndex,
matchIndex,
question,
dynamicMatchIndex }: { candidates: { post: Post; weight: number }[]
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
question: GekanatorQuestion
dynamicMatchIndex?: GekanatorMatchIndex },
): number => {
const matched = matchingPostIdsForQuestion ({
posts,
materialIndex,
@@ -952,21 +951,19 @@ const matchingWeightInCandidates = ({
sum + (matched.has (item.post.id) ? item.weight : 0), 0)
}
const signatureForCandidateIds = ({
candidateIds,
posts,
materialIndex,
matchIndex,
question,
dynamicMatchIndex,
}: {
candidateIds: number[]
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
question: GekanatorQuestion
dynamicMatchIndex?: GekanatorMatchIndex
}): string => {
const signatureForCandidateIds = (
{ candidateIds,
posts,
materialIndex,
matchIndex,
question,
dynamicMatchIndex, }: { candidateIds: number[]
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
question: GekanatorQuestion
dynamicMatchIndex?: GekanatorMatchIndex },
): string => {
const matched = matchingPostIdsForQuestion ({
posts,
materialIndex,
@@ -977,28 +974,24 @@ const signatureForCandidateIds = ({
return candidateIds.map (postId => matched.has (postId) ? '1' : '0').join ('')
}
const postIdsForHardAnswer = ({
candidateIds,
question,
answer,
posts,
materialIndex,
matchIndex,
dynamicMatchIndex,
}: {
candidateIds: number[]
question: GekanatorQuestion
answer: GekanatorAnswerValue
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
dynamicMatchIndex?: GekanatorMatchIndex
}): number[] => {
if (
answer === 'unknown'
const postIdsForHardAnswer = (
{ candidateIds,
question,
answer,
posts,
materialIndex,
matchIndex,
dynamicMatchIndex }: { candidateIds: number[]
question: GekanatorQuestion
answer: GekanatorAnswerValue
posts: Post[]
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
dynamicMatchIndex?: GekanatorMatchIndex },
): number[] => {
if (answer === 'unknown'
|| answer === 'partial'
|| answer === 'probably_no'
)
|| answer === 'probably_no')
return candidateIds
if (answer === 'yes')
@@ -1026,55 +1019,48 @@ const postIdsForHardAnswer = ({
return candidateIds
}
const buildIndexedQuestion = ({
condition,
const buildIndexedQuestion = (
{ condition,
text,
kind,
priorityWeight,
materialIndex }: {
condition: Exclude<GekanatorQuestionCondition, { type: 'post-similarity' }>
text: string
kind: GekanatorQuestionKind
priorityWeight: number
materialIndex: GekanatorQuestionMaterialIndex },
): GekanatorQuestion => ({
id: questionIdForCondition (condition),
text,
kind,
condition,
source: 'default',
priorityWeight,
materialIndex,
}: {
condition: Exclude<GekanatorQuestionCondition, { type: 'post-similarity' }>
text: string
kind: GekanatorQuestionKind
priorityWeight: number
materialIndex: GekanatorQuestionMaterialIndex
}): GekanatorQuestion => ({
id: questionIdForCondition (condition),
text,
kind,
condition,
source: 'default',
priorityWeight,
test: post =>
(matchingPostIdsForCondition ({
condition,
materialIndex }) ?? new Set<number> ()).has (post.id) })
test: post =>
(matchingPostIdsForCondition ({
condition,
materialIndex }) ?? new Set<number> ()).has (post.id) })
const rankedEntriesForCounts = <T extends string | number> ({
counts,
total,
cap,
}: {
counts: Map<T, number>
total: number
cap: number
}): [T, number][] =>
[...counts.entries ()]
.filter (([, count]) => count > 0 && count < total)
.sort ((a, b) => Math.abs (total / 2 - a[1]) - Math.abs (total / 2 - b[1]))
.slice (0, cap)
const rankedEntriesForCounts = <T extends string | number> (
{ counts, total, cap }: { counts: Map<T, number>
total: number
cap: number },
): [T, number][] =>
([...counts.entries ()]
.filter (([, count]) => count > 0 && count < total)
.sort ((a, b) => Math.abs (total / 2 - a[1]) - Math.abs (total / 2 - b[1]))
.slice (0, cap))
const buildQuestionsForCandidateIds = ({
candidateIds,
materialIndex,
performanceMode,
acceptedQuestions,
}: {
candidateIds: number[]
materialIndex: GekanatorQuestionMaterialIndex
performanceMode: GekanatorPerformanceMode
acceptedQuestions: GekanatorQuestion[]
}): GekanatorQuestion[] => {
const buildQuestionsForCandidateIds = (
{ candidateIds,
materialIndex,
performanceMode,
acceptedQuestions }: { candidateIds: number[]
materialIndex: GekanatorQuestionMaterialIndex
performanceMode: GekanatorPerformanceMode
acceptedQuestions: GekanatorQuestion[] },
): GekanatorQuestion[] => {
const total = candidateIds.length
if (total === 0)
return acceptedQuestions
@@ -1109,7 +1095,7 @@ const buildQuestionsForCandidateIds = ({
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
titleLengths.push (titleLength)
if (materialIndex.titleAsciiPostIds.has (postId))
asciiCount += 1
++asciiCount
})
const tagCap =
@@ -1130,7 +1116,7 @@ const buildQuestionsForCandidateIds = ({
.forEach (([host]) => {
questions.push (buildIndexedQuestion ({
condition: { type: 'source', host },
text: `${ host } の投稿を思浮かべてる?`,
text: `${ host } の投稿を思浮かべてる?`,
kind: 'source',
priorityWeight: 1,
materialIndex }))
@@ -1180,7 +1166,7 @@ const buildQuestionsForCandidateIds = ({
if (asciiCount > 0 && asciiCount < total)
questions.push (buildIndexedQuestion ({
condition: { type: 'title-has-ascii' },
text: '題名に英数字が混じってる?',
text: '題名に英数字が混じってる?',
kind: 'title',
priorityWeight: 1,
materialIndex }))
@@ -1959,9 +1945,9 @@ const winningRunTagText = (
switch (category)
{
case 'nico':
return `ニコニコに「${ name.replace (/^nico:/, '') }」タグがいてる?`
return `ニコニコに「${ name.replace (/^nico:/, '') }」タグがいてる?`
default:
return `${ name }」タグがいてる?`
return `${ name }」タグがいてる?`
}
}
@@ -2019,7 +2005,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
? null
: {
id: questionIdForCondition ({ type: 'source', host }),
text: `${ host } の投稿を思浮かべてる?`,
text: `${ host } の投稿を思浮かべてる?`,
kind: 'source',
condition: { type: 'source', host },
source: 'default',
@@ -2088,7 +2074,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
})
addQuestion ({
id: questionIdForCondition ({ type: 'title-has-ascii' }),
text: '題名に英数字が混じってる?',
text: '題名に英数字が混じってる?',
kind: 'title',
condition: { type: 'title-has-ascii' },
source: 'default',
@@ -2107,7 +2093,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
targetPost.tags
.filter (tag =>
!(tag.category === 'meta')
tag.category !== 'meta'
&& !(tag.name.includes ('タグ希望'))
&& !(tag.name.includes ('bot操作')))
.slice (0, 20)
@@ -2124,12 +2110,12 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
test: post => post.tags.some (candidate =>
candidate.category === tag.category
&& candidate.name === tag.name
&& !(candidate.category === 'meta')
&& candidate.category !== 'meta'
&& !(candidate.name.includes ('タグ希望'))
&& !(candidate.name.includes ('bot操作'))) })
})
;([
void ([
{
answer: 'yes' as const,
threshold: .9,
@@ -2141,7 +2127,7 @@ const winningRunCandidateQuestionsFor = (targetPost: Post): GekanatorQuestion[]
{
answer: 'no' as const,
threshold: .25,
text: '少し違印象もある?' }]).forEach ((item, index) => {
text: '少し違印象もある?' }]).forEach ((item, index) => {
addQuestion ({
id: `winning-run:post-similarity:${ targetPost.id }:${ item.answer }:${ item.threshold }`,
text: item.text,
@@ -2342,8 +2328,10 @@ const chooseFallbackQuestion = ({
const shouldEnterGuessPhase = (
reason: GuessReason | null,
): reason is 'hard_max_questions' | 'winning_run_finished' =>
reason === 'hard_max_questions' || reason === 'winning_run_finished'
): reason is 'hard_max_questions' | 'winning_run_finished' | 'question_count_checkpoint' =>
(reason === 'hard_max_questions'
|| reason === 'winning_run_finished'
|| reason === 'question_count_checkpoint')
const isWinningRunActive = (
@@ -2364,65 +2352,92 @@ const winningRunQuestionCount = (
.length
const nextQuestionPlanFor = ({
posts,
eligiblePosts,
availablePosts,
acceptedQuestions,
scores,
answers,
askedIds,
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex,
lastGuessQuestionCount,
winningRunTargetId,
winningRunStartAnswerCount,
}: {
posts: Post[]
eligiblePosts: Post[]
availablePosts: Post[]
acceptedQuestions: 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
lastGuessQuestionCount: number
winningRunTargetId: number | null
winningRunStartAnswerCount: number | null
}): {
question: GekanatorQuestion | null
guess: Post | null
guessReason: GuessReason | null
questionMode: QuestionMode
winningRunTargetId: number | null
winningRunStartAnswerCount: number | null
} => {
const nextQuestionPlanFor = (
{ posts,
eligiblePosts,
availablePosts,
acceptedQuestions,
scores,
answers,
askedIds,
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex,
lastGuessQuestionCount,
winningRunTargetId,
winningRunStartAnswerCount }: { posts: Post[]
eligiblePosts: Post[]
availablePosts: Post[]
acceptedQuestions: 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
lastGuessQuestionCount: number
winningRunTargetId: number | null
winningRunStartAnswerCount: number | null },
): { question: GekanatorQuestion | null
guess: Post | null
guessReason: GuessReason | null
questionMode: QuestionMode
winningRunTargetId: number | null
winningRunStartAnswerCount: number | null } => {
const guessablePosts =
eligiblePosts.length > 0
? eligiblePosts
: availablePosts
const checkpointGuess =
answers.length > 0
&& answers.length - lastGuessQuestionCount >= minQuestionsBeforeCertainGuess
if (answers.length >= hardMaxQuestions)
{
return {
question: null,
guess: bestPost (guessablePosts, scores),
guessReason: 'hard_max_questions',
questionMode: null,
winningRunTargetId,
winningRunStartAnswerCount }
}
if (checkpointGuess)
{
return {
question: null,
guess: bestPost (guessablePosts, scores),
guessReason: 'question_count_checkpoint',
questionMode: null,
winningRunTargetId,
winningRunStartAnswerCount }
}
const nextQuestionsSinceLastGuess = answers.length - lastGuessQuestionCount
const nextWinningRunTargetId =
eligiblePosts.length === 1
? eligiblePosts[0]?.id ?? null
: null
? eligiblePosts[0]?.id ?? null
: null
const nextWinningRunStartAnswerCount =
nextWinningRunTargetId === null
? null
: isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
? null
: ((isWinningRunActive (winningRunTargetId, winningRunStartAnswerCount)
&& winningRunTargetId === nextWinningRunTargetId
&& winningRunStartAnswerCount !== null
? winningRunStartAnswerCount
: answers.length
&& winningRunStartAnswerCount !== null)
? winningRunStartAnswerCount
: answers.length)
const nextWinningRunTargetPost =
nextWinningRunTargetId === null
? null
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
? null
: posts.find (post => post.id === nextWinningRunTargetId) ?? null
const buildQuestionsForPosts = (scopePosts: Post[]): GekanatorQuestion[] =>
buildQuestionsForCandidateIds ({
candidateIds: scopePosts.map (post => post.id),
@@ -2490,8 +2505,9 @@ const nextQuestionPlanFor = ({
const evaluationPosts =
nextQuestionsSinceLastGuess >= minQuestionsBeforeCertainGuess
? eligiblePosts
: availablePosts
? eligiblePosts
: availablePosts
const evaluationQuestions = buildQuestionsForPosts (evaluationPosts)
const normalQuestion = chooseQuestion ({
posts: evaluationPosts,
@@ -2505,6 +2521,7 @@ const nextQuestionPlanFor = ({
performanceMode,
materialIndex,
matchIndex })
const fallbackQuestion = normalQuestion ?? chooseFallbackQuestion ({
posts: evaluationPosts.length > 0 ? evaluationPosts : availablePosts,
allPosts: posts,
@@ -2514,19 +2531,17 @@ const nextQuestionPlanFor = ({
scores,
materialIndex,
matchIndex })
if (fallbackQuestion)
return {
question: fallbackQuestion,
guess: null,
guessReason: null,
questionMode: 'normal',
winningRunTargetId: nextWinningRunTargetId,
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
const guessablePosts =
eligiblePosts.length > 0
? eligiblePosts
: availablePosts
if (fallbackQuestion)
{
return {
question: fallbackQuestion,
guess: null,
guessReason: null,
questionMode: 'normal',
winningRunTargetId: nextWinningRunTargetId,
winningRunStartAnswerCount: nextWinningRunStartAnswerCount }
}
return {
question: null,
@@ -4184,8 +4199,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const dialogue =
phase === 'learned' && resultWon
? <>wwwww <ruby>鹿<rt></rt></ruby>!</>
: <><ruby>鹿<rt></rt></ruby>稿</>
? <>wwwww&emsp;<ruby>鹿<rt></rt></ruby>!</>
: <><ruby>鹿<rt></rt></ruby>稿</>
const introLoading = isLoading || acceptedQuestionsLoading
const readyToStart =
!(introLoading)
@@ -4274,7 +4289,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
return (
<MainArea className="relative overflow-hidden bg-yellow-50 dark:bg-red-975">
<Helmet>
<meta name="robots" content="noindex"/>
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
</Helmet>
@@ -4340,15 +4354,16 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</div>
</div>
<div className="min-w-0 flex-1 space-y-3">
<p className="text-lg font-bold">
{dialogue}
</p>
{phase === 'intro' && (
<p className="text-lg font-bold">
{dialogue}
</p>)}
{introLoading && (
<p>
{phase === 'intro'
? '投稿を読み込んでます...'
: '前回のグカネータ状態を復元してます...'}
? '投稿を読み込んでます……'
: '前回のグカネータ状態を復元してます……'}
</p>)}
{(Boolean (error) || Boolean (acceptedQuestionsError))
&& <p></p>}
@@ -4363,6 +4378,18 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{phase === 'intro' && readyToStart && restorePromptVisible && (
<div className="flex flex-wrap gap-2">
<button
type="button"
className="rounded border border-yellow-300 px-4 py-2
hover:bg-yellow-100 dark:border-red-700
dark:hover:bg-red-900"
onClick={() => {
reset ()
setRestorePromptVisible (false)
setPhase ('question')
}}>
</button>
<button
type="button"
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
@@ -4370,14 +4397,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
onClick={continueStoredGame}>
</button>
<button
type="button"
className="rounded border border-yellow-300 px-4 py-2
hover:bg-yellow-100 dark:border-red-700
dark:hover:bg-red-900"
onClick={reset}>
</button>
</div>)}
{phase === 'intro' && readyToStart && !(restorePromptVisible) && (
@@ -4389,7 +4408,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setRestorePromptVisible (false)
setPhase ('question')
}}>
</button>)}
{phase === 'question' && currentQuestion && (
@@ -4490,7 +4509,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</div>)}
{phase === 'guess' && displayedGuess && (
<div className="space-y-4">
<p className="text-xl font-bold">?</p>
<p className="text-xl font-bold">?</p>
{isAdmin && (
<div className="rounded border border-yellow-100 px-3 py-2
text-sm dark:border-red-900">
@@ -4524,7 +4543,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
hover:bg-yellow-100 dark:border-red-700
dark:hover:bg-red-900"
onClick={rejectGuess}>
</button>
{history.length > 0 && (
<button
@@ -4577,7 +4596,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
<div className="space-y-4">
<div>
<p className="text-sm text-neutral-500"></p>
<p className="text-xl font-bold">wwwww</p>
<p className="text-xl font-bold">wwwww&emsp;<ruby>鹿<rt></rt></ruby>!</p>
</div>
{reviewGuessedPost && (
@@ -4861,11 +4880,11 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
<div className="space-y-4">
<div>
<p className="text-sm text-neutral-500"></p>
<p className="text-xl font-bold"> 2 </p>
<p className="text-xl font-bold"> 2 </p>
</div>
{extraQuestionState === 'loading' && (
<p>...</p>)}
<p></p>)}
{extraQuestionState === 'empty' && (
<p></p>)}