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