このコミットが含まれているのは:
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
@@ -7,8 +7,10 @@ import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { buildGekanatorQuestions,
|
||||
fetchGekanatorPosts,
|
||||
restoreGekanatorQuestion,
|
||||
saveGekanatorGame,
|
||||
saveGekanatorQuestionSuggestion } from '@/lib/gekanator'
|
||||
saveGekanatorQuestionSuggestion,
|
||||
storeGekanatorQuestion } from '@/lib/gekanator'
|
||||
import { gekanatorKeys } from '@/lib/queryKeys'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -16,7 +18,8 @@ import type { FC } from 'react'
|
||||
|
||||
import type { GekanatorAnswerLog,
|
||||
GekanatorAnswerValue,
|
||||
GekanatorQuestion } from '@/lib/gekanator'
|
||||
GekanatorQuestion,
|
||||
StoredGekanatorQuestion } from '@/lib/gekanator'
|
||||
import type { Post } from '@/types'
|
||||
|
||||
type Phase =
|
||||
@@ -61,6 +64,28 @@ type GameSnapshot = {
|
||||
reviewGuessedPostId: number | null
|
||||
reviewCorrectPostId: number | null }
|
||||
|
||||
type StoredGekanatorGame = {
|
||||
phase: Phase
|
||||
scores: [number, number][]
|
||||
answers: GekanatorAnswerLog[]
|
||||
askedIds: string[]
|
||||
softenedQuestionIds: string[]
|
||||
askedQuestionBank?: StoredGekanatorQuestion[]
|
||||
askedQuestionBankIds?: string[]
|
||||
search: string
|
||||
selectingCorrectPost: boolean
|
||||
saved: boolean
|
||||
resultWon: boolean | null
|
||||
rejectedPostIds: number[]
|
||||
lastGuessQuestionCount: number
|
||||
lastRejectedGuessId: number | null
|
||||
activeGuessId: number | null
|
||||
reviewGuessedPostId: number | null
|
||||
reviewCorrectPostId: number | null
|
||||
savedGameId: number | null
|
||||
questionSuggestion: string
|
||||
questionSuggestionAnswer: GekanatorAnswerValue }
|
||||
|
||||
const answerOptions: AnswerOption[] = [
|
||||
{ label: 'はい', value: 'yes' },
|
||||
{ label: 'いいえ', value: 'no' },
|
||||
@@ -78,6 +103,39 @@ const runnerUpMaxPercent = .5
|
||||
const hardMaxQuestions = 80
|
||||
const softenedAnswerWeight = .35
|
||||
const confidenceTemperature = 6
|
||||
const gameStorageKey = 'gekanator:game:v1'
|
||||
|
||||
|
||||
const clearStoredGame = (): void => {
|
||||
try
|
||||
{
|
||||
sessionStorage.removeItem (gameStorageKey)
|
||||
}
|
||||
catch
|
||||
{
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const loadStoredGame = (): StoredGekanatorGame | null => {
|
||||
try
|
||||
{
|
||||
const raw = sessionStorage.getItem (gameStorageKey)
|
||||
if (!(raw))
|
||||
return null
|
||||
|
||||
return JSON.parse (raw) as StoredGekanatorGame
|
||||
}
|
||||
catch
|
||||
{
|
||||
clearStoredGame ()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const isStoredPhase = (phase: Phase): boolean => phase !== 'intro'
|
||||
|
||||
|
||||
const deltaFor = (matched: boolean, answer: GekanatorAnswerValue): number => {
|
||||
@@ -442,30 +500,124 @@ const expectedAnswerFor = (
|
||||
|
||||
|
||||
const GekanatorPage: FC = () => {
|
||||
const [phase, setPhase] = useState<Phase> ('intro')
|
||||
const [scores, setScores] = useState<Map<number, number>> (new Map ())
|
||||
const [answers, setAnswers] = useState<GekanatorAnswerLog[]> ([])
|
||||
const [askedIds, setAskedIds] = useState<Set<string>> (new Set ())
|
||||
const [softenedQuestionIds, setSoftenedQuestionIds] = useState<Set<string>> (new Set ())
|
||||
const [askedQuestionBank, setAskedQuestionBank] = useState<GekanatorQuestion[]> ([])
|
||||
const [search, setSearch] = useState ('')
|
||||
const [selectingCorrectPost, setSelectingCorrectPost] = useState (false)
|
||||
const [saved, setSaved] = useState (false)
|
||||
const [resultWon, setResultWon] = useState<boolean | null> (null)
|
||||
const [rejectedPostIds, setRejectedPostIds] = useState<Set<number>> (new Set ())
|
||||
const [lastGuessQuestionCount, setLastGuessQuestionCount] = useState (0)
|
||||
const [lastRejectedGuessId, setLastRejectedGuessId] = useState<number | null> (null)
|
||||
const [activeGuessId, setActiveGuessId] = useState<number | null> (null)
|
||||
const [reviewGuessedPostId, setReviewGuessedPostId] = useState<number | null> (null)
|
||||
const [reviewCorrectPostId, setReviewCorrectPostId] = useState<number | null> (null)
|
||||
const [savedGameId, setSavedGameId] = useState<number | null> (null)
|
||||
const [questionSuggestion, setQuestionSuggestion] = useState ('')
|
||||
const storedGame = useMemo (loadStoredGame, [])
|
||||
const [phase, setPhase] = useState<Phase> (storedGame?.phase ?? 'intro')
|
||||
const [scores, setScores] = useState<Map<number, number>> (
|
||||
() => new Map (storedGame?.scores ?? []))
|
||||
const [answers, setAnswers] = useState<GekanatorAnswerLog[]> (
|
||||
storedGame?.answers ?? [])
|
||||
const [askedIds, setAskedIds] = useState<Set<string>> (
|
||||
() => new Set (storedGame?.askedIds ?? []))
|
||||
const [softenedQuestionIds, setSoftenedQuestionIds] = useState<Set<string>> (
|
||||
() => new Set (storedGame?.softenedQuestionIds ?? []))
|
||||
const [askedQuestionBank, setAskedQuestionBank] = useState<GekanatorQuestion[]> (
|
||||
() => (storedGame?.askedQuestionBank ?? []).map (restoreGekanatorQuestion))
|
||||
const [storedAskedQuestionBankIds, setStoredAskedQuestionBankIds] = useState<string[]> (
|
||||
(storedGame?.askedQuestionBank?.length ?? 0) > 0
|
||||
? []
|
||||
: storedGame?.askedQuestionBankIds ?? [])
|
||||
const [search, setSearch] = useState (storedGame?.search ?? '')
|
||||
const [selectingCorrectPost, setSelectingCorrectPost] = useState (
|
||||
storedGame?.selectingCorrectPost ?? false)
|
||||
const [saved, setSaved] = useState (storedGame?.saved ?? false)
|
||||
const [resultWon, setResultWon] = useState<boolean | null> (
|
||||
storedGame?.resultWon ?? null)
|
||||
const [rejectedPostIds, setRejectedPostIds] = useState<Set<number>> (
|
||||
() => new Set (storedGame?.rejectedPostIds ?? []))
|
||||
const [lastGuessQuestionCount, setLastGuessQuestionCount] = useState (
|
||||
storedGame?.lastGuessQuestionCount ?? 0)
|
||||
const [lastRejectedGuessId, setLastRejectedGuessId] = useState<number | null> (
|
||||
storedGame?.lastRejectedGuessId ?? null)
|
||||
const [activeGuessId, setActiveGuessId] = useState<number | null> (
|
||||
storedGame?.activeGuessId ?? null)
|
||||
const [reviewGuessedPostId, setReviewGuessedPostId] = useState<number | null> (
|
||||
storedGame?.reviewGuessedPostId ?? null)
|
||||
const [reviewCorrectPostId, setReviewCorrectPostId] = useState<number | null> (
|
||||
storedGame?.reviewCorrectPostId ?? null)
|
||||
const [savedGameId, setSavedGameId] = useState<number | null> (
|
||||
storedGame?.savedGameId ?? null)
|
||||
const [questionSuggestion, setQuestionSuggestion] = useState (
|
||||
storedGame?.questionSuggestion ?? '')
|
||||
const [questionSuggestionAnswer, setQuestionSuggestionAnswer] =
|
||||
useState<GekanatorAnswerValue> (storedGame?.questionSuggestionAnswer ?? 'yes')
|
||||
const [history, setHistory] = useState<GameSnapshot[]> ([])
|
||||
|
||||
const { data: posts = [], isLoading, error } = useQuery ({
|
||||
queryKey: gekanatorKeys.posts (),
|
||||
queryFn: fetchGekanatorPosts })
|
||||
|
||||
useEffect (() => {
|
||||
if (posts.length === 0 || storedAskedQuestionBankIds.length === 0)
|
||||
return
|
||||
|
||||
const questionById = new Map (
|
||||
buildGekanatorQuestions (posts).map (question => [question.id, question]))
|
||||
setAskedQuestionBank (
|
||||
storedAskedQuestionBankIds
|
||||
.map (questionId => questionById.get (questionId))
|
||||
.filter ((question): question is GekanatorQuestion => question !== undefined))
|
||||
setStoredAskedQuestionBankIds ([])
|
||||
}, [posts, storedAskedQuestionBankIds])
|
||||
|
||||
useEffect (() => {
|
||||
if (!(isStoredPhase (phase)) && answers.length === 0)
|
||||
{
|
||||
clearStoredGame ()
|
||||
return
|
||||
}
|
||||
|
||||
const stored: StoredGekanatorGame = {
|
||||
phase,
|
||||
scores: [...scores.entries ()],
|
||||
answers,
|
||||
askedIds: [...askedIds],
|
||||
softenedQuestionIds: [...softenedQuestionIds],
|
||||
askedQuestionBank: askedQuestionBank.map (storeGekanatorQuestion),
|
||||
askedQuestionBankIds: storedAskedQuestionBankIds,
|
||||
search,
|
||||
selectingCorrectPost,
|
||||
saved,
|
||||
resultWon,
|
||||
rejectedPostIds: [...rejectedPostIds],
|
||||
lastGuessQuestionCount,
|
||||
lastRejectedGuessId,
|
||||
activeGuessId,
|
||||
reviewGuessedPostId,
|
||||
reviewCorrectPostId,
|
||||
savedGameId,
|
||||
questionSuggestion,
|
||||
questionSuggestionAnswer }
|
||||
|
||||
try
|
||||
{
|
||||
sessionStorage.setItem (gameStorageKey, JSON.stringify (stored))
|
||||
}
|
||||
catch
|
||||
{
|
||||
return
|
||||
}
|
||||
}, [
|
||||
phase,
|
||||
scores,
|
||||
answers,
|
||||
askedIds,
|
||||
softenedQuestionIds,
|
||||
askedQuestionBank,
|
||||
storedAskedQuestionBankIds,
|
||||
search,
|
||||
selectingCorrectPost,
|
||||
saved,
|
||||
resultWon,
|
||||
rejectedPostIds,
|
||||
lastGuessQuestionCount,
|
||||
lastRejectedGuessId,
|
||||
activeGuessId,
|
||||
reviewGuessedPostId,
|
||||
reviewCorrectPostId,
|
||||
savedGameId,
|
||||
questionSuggestion,
|
||||
questionSuggestionAnswer])
|
||||
|
||||
const eligiblePosts = useMemo (
|
||||
() => candidatePostsFor ({
|
||||
posts,
|
||||
@@ -531,10 +683,12 @@ const GekanatorPage: FC = () => {
|
||||
mutationFn: saveGekanatorQuestionSuggestion,
|
||||
onSuccess: () => {
|
||||
setQuestionSuggestion ('')
|
||||
setQuestionSuggestionAnswer ('yes')
|
||||
reset ()
|
||||
}})
|
||||
|
||||
const reset = () => {
|
||||
clearStoredGame ()
|
||||
saveMutation.reset ()
|
||||
setPhase ('intro')
|
||||
setScores (new Map ())
|
||||
@@ -554,6 +708,7 @@ const GekanatorPage: FC = () => {
|
||||
setReviewCorrectPostId (null)
|
||||
setSavedGameId (null)
|
||||
setQuestionSuggestion ('')
|
||||
setQuestionSuggestionAnswer ('yes')
|
||||
setHistory ([])
|
||||
}
|
||||
|
||||
@@ -659,6 +814,7 @@ const GekanatorPage: FC = () => {
|
||||
const nextAnswers = [...answers, {
|
||||
questionId: currentQuestion.id,
|
||||
questionText: currentQuestion.text,
|
||||
questionCondition: currentQuestion.condition,
|
||||
answer: value,
|
||||
originalAnswer: value }]
|
||||
const nextAskedIds = new Set ([...askedIds, currentQuestion.id])
|
||||
@@ -784,7 +940,10 @@ const GekanatorPage: FC = () => {
|
||||
return
|
||||
|
||||
saveReviewedResult (gekanatorGameId => {
|
||||
questionSuggestionMutation.mutate ({ gekanatorGameId, questionText })
|
||||
questionSuggestionMutation.mutate ({
|
||||
gekanatorGameId,
|
||||
questionText,
|
||||
answer: questionSuggestionAnswer })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -936,7 +1095,12 @@ const GekanatorPage: FC = () => {
|
||||
{dialogue}
|
||||
</p>
|
||||
|
||||
{isLoading && <p>投稿を読み込んでゐます...</p>}
|
||||
{isLoading && (
|
||||
<p>
|
||||
{phase === 'intro'
|
||||
? '投稿を読み込んでゐます...'
|
||||
: '前回のグカネータ状態を復元してゐます...'}
|
||||
</p>)}
|
||||
{Boolean (error) && <p>投稿を読み込めませんでした.</p>}
|
||||
|
||||
{phase === 'intro' && !(isLoading) && posts.length > 0 && (
|
||||
@@ -1008,7 +1172,7 @@ const GekanatorPage: FC = () => {
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{phase === 'question' && !(currentQuestion) && (
|
||||
{!(isLoading) && phase === 'question' && !(currentQuestion) && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xl font-bold">
|
||||
もう十分わかった。
|
||||
@@ -1157,7 +1321,8 @@ const GekanatorPage: FC = () => {
|
||||
className="rounded border border-yellow-300 px-4 py-2
|
||||
hover:bg-yellow-100 dark:border-red-700
|
||||
dark:hover:bg-red-900"
|
||||
disabled={saveMutation.isPending || questionSuggestionMutation.isPending}
|
||||
disabled={saveMutation.isPending
|
||||
|| questionSuggestionMutation.isPending}
|
||||
onClick={() => setPhase ('question_suggestion')}>
|
||||
質問を追加
|
||||
</button>
|
||||
@@ -1290,13 +1455,29 @@ const GekanatorPage: FC = () => {
|
||||
bg-white px-3 py-2 dark:border-red-700
|
||||
dark:bg-red-950"/>
|
||||
</label>
|
||||
<label className="block space-y-2">
|
||||
<span className="font-bold">この正解投稿に対する答え</span>
|
||||
<select
|
||||
value={questionSuggestionAnswer}
|
||||
className="rounded border border-yellow-300 bg-white px-2 py-1
|
||||
dark:border-red-700 dark:bg-red-950"
|
||||
onChange={ev =>
|
||||
setQuestionSuggestionAnswer (
|
||||
ev.target.value as GekanatorAnswerValue)}>
|
||||
{answerOptions.map (option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-neutral-300 px-4 py-2
|
||||
hover:bg-neutral-100 dark:border-neutral-700
|
||||
dark:hover:bg-red-900"
|
||||
disabled={saveMutation.isPending || questionSuggestionMutation.isPending}
|
||||
disabled={saveMutation.isPending
|
||||
|| questionSuggestionMutation.isPending}
|
||||
onClick={() => setPhase ('end')}>
|
||||
戻る
|
||||
</button>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする