このコミットが含まれているのは:
+74
-23
@@ -1,5 +1,4 @@
|
||||
import { apiPost } from '@/lib/api'
|
||||
import { fetchPosts } from '@/lib/posts'
|
||||
import { apiGet, apiPost } from '@/lib/api'
|
||||
|
||||
import type { Post } from '@/types'
|
||||
|
||||
@@ -13,6 +12,7 @@ export type GekanatorAnswerValue =
|
||||
export type GekanatorAnswerLog = {
|
||||
questionId: string
|
||||
questionText: string
|
||||
questionCondition?: GekanatorQuestionCondition
|
||||
answer: GekanatorAnswerValue
|
||||
originalAnswer: GekanatorAnswerValue }
|
||||
|
||||
@@ -22,10 +22,26 @@ export type GekanatorQuestionKind =
|
||||
| 'title'
|
||||
| 'original_date'
|
||||
|
||||
export type GekanatorQuestionCondition =
|
||||
| { type: 'tag'; key: string }
|
||||
| { type: 'source'; host: string }
|
||||
| { type: 'original-year'; year: number }
|
||||
| { type: 'original-month'; month: number }
|
||||
| { type: 'original-month-day'; monthDay: string }
|
||||
| { type: 'title-length-greater-than'; length: number }
|
||||
| { type: 'title-has-ascii' }
|
||||
|
||||
export type StoredGekanatorQuestion = {
|
||||
id: string
|
||||
text: string
|
||||
kind: GekanatorQuestionKind
|
||||
condition: GekanatorQuestionCondition }
|
||||
|
||||
export type GekanatorQuestion = {
|
||||
id: string
|
||||
text: string
|
||||
kind: GekanatorQuestionKind
|
||||
condition: GekanatorQuestionCondition
|
||||
test: (post: Post) => boolean }
|
||||
|
||||
const countBy = <T extends string | number> (values: T[]): Map<T, number> => {
|
||||
@@ -144,27 +160,49 @@ const questionableTag = (post: Post, key: string): boolean => {
|
||||
}
|
||||
|
||||
|
||||
const questionMatches = (
|
||||
post: Post,
|
||||
condition: GekanatorQuestionCondition,
|
||||
): boolean => {
|
||||
switch (condition.type)
|
||||
{
|
||||
case 'tag':
|
||||
return questionableTag (post, condition.key)
|
||||
case 'source':
|
||||
return hostOf (post) === condition.host
|
||||
case 'original-year':
|
||||
return originalYearOf (post) === condition.year
|
||||
case 'original-month':
|
||||
return originalMonthOf (post) === condition.month
|
||||
case 'original-month-day':
|
||||
return originalMonthDayOf (post) === condition.monthDay
|
||||
case 'title-length-greater-than':
|
||||
return (post.title?.length ?? 0) > condition.length
|
||||
case 'title-has-ascii':
|
||||
return /[A-Za-z0-9]/.test (post.title ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const restoreGekanatorQuestion = (
|
||||
question: StoredGekanatorQuestion,
|
||||
): GekanatorQuestion => ({
|
||||
...question,
|
||||
test: (post: Post) => questionMatches (post, question.condition) })
|
||||
|
||||
|
||||
export const storeGekanatorQuestion = (
|
||||
question: GekanatorQuestion,
|
||||
): StoredGekanatorQuestion => ({
|
||||
id: question.id,
|
||||
text: question.text,
|
||||
kind: question.kind,
|
||||
condition: question.condition })
|
||||
|
||||
|
||||
export const fetchGekanatorPosts = async (): Promise<Post[]> => {
|
||||
const limit = 200
|
||||
const first = await fetchPosts ({
|
||||
url: '', title: '', tags: '', match: 'all',
|
||||
originalCreatedFrom: '', originalCreatedTo: '',
|
||||
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
|
||||
page: 1, limit, order: 'original_created_at:desc' })
|
||||
const posts = [...first.posts]
|
||||
const totalPages = Math.ceil (first.count / limit)
|
||||
|
||||
for (let page = 2; page <= totalPages; page++)
|
||||
{
|
||||
const data = await fetchPosts ({
|
||||
url: '', title: '', tags: '', match: 'all',
|
||||
originalCreatedFrom: '', originalCreatedTo: '',
|
||||
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
|
||||
page, limit, order: 'original_created_at:desc' })
|
||||
posts.push (...data.posts)
|
||||
}
|
||||
|
||||
return posts
|
||||
const data = await apiGet<{ posts: Post[] }> ('/gekanator/posts')
|
||||
return data.posts
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +247,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
|
||||
id: `tag:${ key }`,
|
||||
text: tagQuestionText (category, label),
|
||||
kind: 'tag' as const,
|
||||
condition: { type: 'tag' as const, key: String (key) },
|
||||
test: (post: Post) => questionableTag (post, String (key)) }
|
||||
})
|
||||
|
||||
@@ -219,6 +258,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
|
||||
id: `source:${ host }`,
|
||||
text: `${ host } の投稿を思ひ浮かべてゐる?`,
|
||||
kind: 'source' as const,
|
||||
condition: { type: 'source' as const, host },
|
||||
test: (post: Post) => hostOf (post) === host }))
|
||||
|
||||
const originalYearQuestions = usefulEntries (originalYears)
|
||||
@@ -228,6 +268,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
|
||||
id: `original-year:${ year }`,
|
||||
text: `オリジナルの投稿年は ${ year } 年?`,
|
||||
kind: 'original_date' as const,
|
||||
condition: { type: 'original-year' as const, year },
|
||||
test: (post: Post) => originalYearOf (post) === year }))
|
||||
|
||||
const originalMonthQuestions = usefulEntries (originalMonths)
|
||||
@@ -237,6 +278,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
|
||||
id: `original-month:${ month }`,
|
||||
text: `オリジナルの投稿月は ${ month } 月?`,
|
||||
kind: 'original_date' as const,
|
||||
condition: { type: 'original-month' as const, month },
|
||||
test: (post: Post) => originalMonthOf (post) === month }))
|
||||
|
||||
const originalMonthDayQuestions = usefulEntries (originalMonthDays)
|
||||
@@ -249,6 +291,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
|
||||
id: `original-month-day:${ monthDay }`,
|
||||
text: `オリジナルの投稿日は ${ month } 月 ${ day } 日?`,
|
||||
kind: 'original_date' as const,
|
||||
condition: { type: 'original-month-day' as const, monthDay: String (monthDay) },
|
||||
test: (post: Post) => originalMonthDayOf (post) === monthDay }
|
||||
})
|
||||
|
||||
@@ -257,11 +300,15 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
|
||||
id: 'title:long',
|
||||
text: '題名が長めの投稿?',
|
||||
kind: 'title' as const,
|
||||
condition: {
|
||||
type: 'title-length-greater-than' as const,
|
||||
length: titleLengthMedian },
|
||||
test: (post: Post) => (post.title?.length ?? 0) > titleLengthMedian },
|
||||
{
|
||||
id: 'title:ascii',
|
||||
text: '題名に英数字が混じってゐる?',
|
||||
kind: 'title' as const,
|
||||
condition: { type: 'title-has-ascii' as const },
|
||||
test: (post: Post) => /[A-Za-z0-9]/.test (post.title ?? '') }]
|
||||
.filter (question => {
|
||||
const yes = posts.filter (post => question.test (post)).length
|
||||
@@ -294,6 +341,7 @@ export const saveGekanatorGame = async ({
|
||||
answers: answers.map (answer => ({
|
||||
question_id: answer.questionId,
|
||||
question_text: answer.questionText,
|
||||
question_condition: answer.questionCondition ?? null,
|
||||
answer: answer.answer,
|
||||
original_answer: answer.originalAnswer })) })
|
||||
|
||||
@@ -301,10 +349,13 @@ export const saveGekanatorGame = async ({
|
||||
export const saveGekanatorQuestionSuggestion = async ({
|
||||
gekanatorGameId,
|
||||
questionText,
|
||||
answer,
|
||||
}: {
|
||||
gekanatorGameId: number
|
||||
questionText: string
|
||||
answer: GekanatorAnswerValue
|
||||
}): Promise<{ id: number }> =>
|
||||
await apiPost ('/gekanator/question_suggestions', {
|
||||
gekanator_game_id: gekanatorGameId,
|
||||
question_text: questionText })
|
||||
question_text: questionText,
|
||||
answer })
|
||||
|
||||
@@ -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>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする