グカネータ作成 / 質問パターン修正 (#41) #364

マージ済み
みてるぞ が 17 個のコミットを feature/041 から main へマージ 2026-06-11 23:21:45 +09:00
8個のファイルの変更339行の追加51行の削除
コミット be5359eb84 の変更だけを表示してゐます - すべてのコミットを表示
+46
ファイルの表示
@@ -0,0 +1,46 @@
class GekanatorPostsController < ApplicationController
def index
return head :not_found unless current_user&.admin?
posts =
Post
.preload(tags: :tag_name)
.with_attached_thumbnail
.order(Arel.sql(
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE, ' \
'posts.original_created_from, posts.created_at) DESC, posts.id DESC'))
render json: { posts: posts.map { |post| post_json(post) } }
end
private
def post_json post
{
id: post.id,
url: post.url,
title: post.title,
thumbnail: thumbnail_url(post),
thumbnail_base: post.thumbnail_base,
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
tags: post.tags.map { |tag| tag_json(tag) }
}
end
def tag_json tag
{
id: tag.id,
name: tag.name,
category: tag.category
}
end
def thumbnail_url post
return nil unless post.thumbnail.attached?
rails_storage_proxy_url(post.thumbnail, only_path: false)
rescue
nil
end
end
+2 -1
ファイルの表示
@@ -8,7 +8,8 @@ class GekanatorQuestionSuggestionsController < ApplicationController
suggestion = GekanatorQuestionSuggestion.new( suggestion = GekanatorQuestionSuggestion.new(
gekanator_game: game, gekanator_game: game,
user: current_user, user: current_user,
question_text: params.require(:question_text)) question_text: params.require(:question_text),
answer: params.require(:answer))
if suggestion.save if suggestion.save
render json: { id: suggestion.id }, status: :created render json: { id: suggestion.id }, status: :created
+2
ファイルの表示
@@ -1,10 +1,12 @@
class GekanatorQuestionSuggestion < ApplicationRecord class GekanatorQuestionSuggestion < ApplicationRecord
MAX_QUESTIONS_PER_GAME = 1 MAX_QUESTIONS_PER_GAME = 1
ANSWERS = ['yes', 'no', 'partial', 'probably_no', 'unknown'].freeze
belongs_to :gekanator_game belongs_to :gekanator_game
belongs_to :user belongs_to :user
validates :question_text, presence: true, length: { maximum: 1000 } validates :question_text, presence: true, length: { maximum: 1000 }
validates :answer, presence: true, inclusion: { in: ANSWERS }
validates :processed, inclusion: { in: [true, false] } validates :processed, inclusion: { in: [true, false] }
validate :question_suggestion_limit_per_game, on: :create validate :question_suggestion_limit_per_game, on: :create
+1
ファイルの表示
@@ -65,6 +65,7 @@ Rails.application.routes.draw do
namespace :gekanator do namespace :gekanator do
resources :games, only: [:create], controller: '/gekanator_games' resources :games, only: [:create], controller: '/gekanator_games'
resources :posts, only: [:index], controller: '/gekanator_posts'
resources :question_suggestions, resources :question_suggestions,
only: [:create], only: [:create],
controller: '/gekanator_question_suggestions' controller: '/gekanator_question_suggestions'
@@ -0,0 +1,5 @@
class AddAnswerToGekanatorQuestionSuggestions < ActiveRecord::Migration[8.0]
def change
add_column :gekanator_question_suggestions, :answer, :string, null: false
end
end
生成ファイル
+2 -1
ファイルの表示
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_06_08_000000) do ActiveRecord::Schema[8.0].define(version: 2026_06_08_002000) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@@ -70,6 +70,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_08_000000) do
t.boolean "processed", default: false, null: false t.boolean "processed", default: false, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "answer", null: false
t.index ["gekanator_game_id"], name: "index_gekanator_question_suggestions_on_gekanator_game_id" t.index ["gekanator_game_id"], name: "index_gekanator_question_suggestions_on_gekanator_game_id"
t.index ["user_id"], name: "index_gekanator_question_suggestions_on_user_id" t.index ["user_id"], name: "index_gekanator_question_suggestions_on_user_id"
end end
+72 -21
ファイルの表示
@@ -1,5 +1,4 @@
import { apiPost } from '@/lib/api' import { apiGet, apiPost } from '@/lib/api'
import { fetchPosts } from '@/lib/posts'
import type { Post } from '@/types' import type { Post } from '@/types'
@@ -13,6 +12,7 @@ export type GekanatorAnswerValue =
export type GekanatorAnswerLog = { export type GekanatorAnswerLog = {
questionId: string questionId: string
questionText: string questionText: string
questionCondition?: GekanatorQuestionCondition
answer: GekanatorAnswerValue answer: GekanatorAnswerValue
originalAnswer: GekanatorAnswerValue } originalAnswer: GekanatorAnswerValue }
@@ -22,10 +22,26 @@ export type GekanatorQuestionKind =
| 'title' | 'title'
| 'original_date' | '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 = { export type GekanatorQuestion = {
id: string id: string
text: string text: string
kind: GekanatorQuestionKind kind: GekanatorQuestionKind
condition: GekanatorQuestionCondition
test: (post: Post) => boolean } test: (post: Post) => boolean }
const countBy = <T extends string | number> (values: T[]): Map<T, number> => { const countBy = <T extends string | number> (values: T[]): Map<T, number> => {
@@ -144,27 +160,49 @@ const questionableTag = (post: Post, key: string): boolean => {
} }
export const fetchGekanatorPosts = async (): Promise<Post[]> => { const questionMatches = (
const limit = 200 post: Post,
const first = await fetchPosts ({ condition: GekanatorQuestionCondition,
url: '', title: '', tags: '', match: 'all', ): boolean => {
originalCreatedFrom: '', originalCreatedTo: '', switch (condition.type)
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 ({ case 'tag':
url: '', title: '', tags: '', match: 'all', return questionableTag (post, condition.key)
originalCreatedFrom: '', originalCreatedTo: '', case 'source':
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '', return hostOf (post) === condition.host
page, limit, order: 'original_created_at:desc' }) case 'original-year':
posts.push (...data.posts) 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 ?? '')
}
} }
return posts
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 data = await apiGet<{ posts: Post[] }> ('/gekanator/posts')
return data.posts
} }
@@ -209,6 +247,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
id: `tag:${ key }`, id: `tag:${ key }`,
text: tagQuestionText (category, label), text: tagQuestionText (category, label),
kind: 'tag' as const, kind: 'tag' as const,
condition: { type: 'tag' as const, key: String (key) },
test: (post: Post) => questionableTag (post, String (key)) } test: (post: Post) => questionableTag (post, String (key)) }
}) })
@@ -219,6 +258,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
id: `source:${ host }`, id: `source:${ host }`,
text: `${ host } の投稿を思ひ浮かべてゐる?`, text: `${ host } の投稿を思ひ浮かべてゐる?`,
kind: 'source' as const, kind: 'source' as const,
condition: { type: 'source' as const, host },
test: (post: Post) => hostOf (post) === host })) test: (post: Post) => hostOf (post) === host }))
const originalYearQuestions = usefulEntries (originalYears) const originalYearQuestions = usefulEntries (originalYears)
@@ -228,6 +268,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
id: `original-year:${ year }`, id: `original-year:${ year }`,
text: `オリジナルの投稿年は ${ year } 年?`, text: `オリジナルの投稿年は ${ year } 年?`,
kind: 'original_date' as const, kind: 'original_date' as const,
condition: { type: 'original-year' as const, year },
test: (post: Post) => originalYearOf (post) === year })) test: (post: Post) => originalYearOf (post) === year }))
const originalMonthQuestions = usefulEntries (originalMonths) const originalMonthQuestions = usefulEntries (originalMonths)
@@ -237,6 +278,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
id: `original-month:${ month }`, id: `original-month:${ month }`,
text: `オリジナルの投稿月は ${ month } 月?`, text: `オリジナルの投稿月は ${ month } 月?`,
kind: 'original_date' as const, kind: 'original_date' as const,
condition: { type: 'original-month' as const, month },
test: (post: Post) => originalMonthOf (post) === month })) test: (post: Post) => originalMonthOf (post) === month }))
const originalMonthDayQuestions = usefulEntries (originalMonthDays) const originalMonthDayQuestions = usefulEntries (originalMonthDays)
@@ -249,6 +291,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
id: `original-month-day:${ monthDay }`, id: `original-month-day:${ monthDay }`,
text: `オリジナルの投稿日は ${ month }${ day } 日?`, text: `オリジナルの投稿日は ${ month }${ day } 日?`,
kind: 'original_date' as const, kind: 'original_date' as const,
condition: { type: 'original-month-day' as const, monthDay: String (monthDay) },
test: (post: Post) => originalMonthDayOf (post) === monthDay } test: (post: Post) => originalMonthDayOf (post) === monthDay }
}) })
@@ -257,11 +300,15 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
id: 'title:long', id: 'title:long',
text: '題名が長めの投稿?', text: '題名が長めの投稿?',
kind: 'title' as const, kind: 'title' as const,
condition: {
type: 'title-length-greater-than' as const,
length: titleLengthMedian },
test: (post: Post) => (post.title?.length ?? 0) > titleLengthMedian }, test: (post: Post) => (post.title?.length ?? 0) > titleLengthMedian },
{ {
id: 'title:ascii', id: 'title:ascii',
text: '題名に英数字が混じってゐる?', text: '題名に英数字が混じってゐる?',
kind: 'title' as const, kind: 'title' as const,
condition: { type: 'title-has-ascii' as const },
test: (post: Post) => /[A-Za-z0-9]/.test (post.title ?? '') }] test: (post: Post) => /[A-Za-z0-9]/.test (post.title ?? '') }]
.filter (question => { .filter (question => {
const yes = posts.filter (post => question.test (post)).length const yes = posts.filter (post => question.test (post)).length
@@ -294,6 +341,7 @@ export const saveGekanatorGame = async ({
answers: answers.map (answer => ({ answers: answers.map (answer => ({
question_id: answer.questionId, question_id: answer.questionId,
question_text: answer.questionText, question_text: answer.questionText,
question_condition: answer.questionCondition ?? null,
answer: answer.answer, answer: answer.answer,
original_answer: answer.originalAnswer })) }) original_answer: answer.originalAnswer })) })
@@ -301,10 +349,13 @@ export const saveGekanatorGame = async ({
export const saveGekanatorQuestionSuggestion = async ({ export const saveGekanatorQuestionSuggestion = async ({
gekanatorGameId, gekanatorGameId,
questionText, questionText,
answer,
}: { }: {
gekanatorGameId: number gekanatorGameId: number
questionText: string questionText: string
answer: GekanatorAnswerValue
}): Promise<{ id: number }> => }): Promise<{ id: number }> =>
await apiPost ('/gekanator/question_suggestions', { await apiPost ('/gekanator/question_suggestions', {
gekanator_game_id: gekanatorGameId, gekanator_game_id: gekanatorGameId,
question_text: questionText }) question_text: questionText,
answer })
+207 -26
ファイルの表示
@@ -1,5 +1,5 @@
import { useMutation, useQuery } from '@tanstack/react-query' 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 { Helmet } from 'react-helmet-async'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
@@ -7,8 +7,10 @@ import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { buildGekanatorQuestions, import { buildGekanatorQuestions,
fetchGekanatorPosts, fetchGekanatorPosts,
restoreGekanatorQuestion,
saveGekanatorGame, saveGekanatorGame,
saveGekanatorQuestionSuggestion } from '@/lib/gekanator' saveGekanatorQuestionSuggestion,
storeGekanatorQuestion } from '@/lib/gekanator'
import { gekanatorKeys } from '@/lib/queryKeys' import { gekanatorKeys } from '@/lib/queryKeys'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -16,7 +18,8 @@ import type { FC } from 'react'
import type { GekanatorAnswerLog, import type { GekanatorAnswerLog,
GekanatorAnswerValue, GekanatorAnswerValue,
GekanatorQuestion } from '@/lib/gekanator' GekanatorQuestion,
StoredGekanatorQuestion } from '@/lib/gekanator'
import type { Post } from '@/types' import type { Post } from '@/types'
type Phase = type Phase =
@@ -61,6 +64,28 @@ type GameSnapshot = {
reviewGuessedPostId: number | null reviewGuessedPostId: number | null
reviewCorrectPostId: 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[] = [ const answerOptions: AnswerOption[] = [
{ label: 'はい', value: 'yes' }, { label: 'はい', value: 'yes' },
{ label: 'いいえ', value: 'no' }, { label: 'いいえ', value: 'no' },
@@ -78,6 +103,39 @@ const runnerUpMaxPercent = .5
const hardMaxQuestions = 80 const hardMaxQuestions = 80
const softenedAnswerWeight = .35 const softenedAnswerWeight = .35
const confidenceTemperature = 6 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 => { const deltaFor = (matched: boolean, answer: GekanatorAnswerValue): number => {
@@ -442,30 +500,124 @@ const expectedAnswerFor = (
const GekanatorPage: FC = () => { const GekanatorPage: FC = () => {
const [phase, setPhase] = useState<Phase> ('intro') const storedGame = useMemo (loadStoredGame, [])
const [scores, setScores] = useState<Map<number, number>> (new Map ()) const [phase, setPhase] = useState<Phase> (storedGame?.phase ?? 'intro')
const [answers, setAnswers] = useState<GekanatorAnswerLog[]> ([]) const [scores, setScores] = useState<Map<number, number>> (
const [askedIds, setAskedIds] = useState<Set<string>> (new Set ()) () => new Map (storedGame?.scores ?? []))
const [softenedQuestionIds, setSoftenedQuestionIds] = useState<Set<string>> (new Set ()) const [answers, setAnswers] = useState<GekanatorAnswerLog[]> (
const [askedQuestionBank, setAskedQuestionBank] = useState<GekanatorQuestion[]> ([]) storedGame?.answers ?? [])
const [search, setSearch] = useState ('') const [askedIds, setAskedIds] = useState<Set<string>> (
const [selectingCorrectPost, setSelectingCorrectPost] = useState (false) () => new Set (storedGame?.askedIds ?? []))
const [saved, setSaved] = useState (false) const [softenedQuestionIds, setSoftenedQuestionIds] = useState<Set<string>> (
const [resultWon, setResultWon] = useState<boolean | null> (null) () => new Set (storedGame?.softenedQuestionIds ?? []))
const [rejectedPostIds, setRejectedPostIds] = useState<Set<number>> (new Set ()) const [askedQuestionBank, setAskedQuestionBank] = useState<GekanatorQuestion[]> (
const [lastGuessQuestionCount, setLastGuessQuestionCount] = useState (0) () => (storedGame?.askedQuestionBank ?? []).map (restoreGekanatorQuestion))
const [lastRejectedGuessId, setLastRejectedGuessId] = useState<number | null> (null) const [storedAskedQuestionBankIds, setStoredAskedQuestionBankIds] = useState<string[]> (
const [activeGuessId, setActiveGuessId] = useState<number | null> (null) (storedGame?.askedQuestionBank?.length ?? 0) > 0
const [reviewGuessedPostId, setReviewGuessedPostId] = useState<number | null> (null) ? []
const [reviewCorrectPostId, setReviewCorrectPostId] = useState<number | null> (null) : storedGame?.askedQuestionBankIds ?? [])
const [savedGameId, setSavedGameId] = useState<number | null> (null) const [search, setSearch] = useState (storedGame?.search ?? '')
const [questionSuggestion, setQuestionSuggestion] = useState ('') 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 [history, setHistory] = useState<GameSnapshot[]> ([])
const { data: posts = [], isLoading, error } = useQuery ({ const { data: posts = [], isLoading, error } = useQuery ({
queryKey: gekanatorKeys.posts (), queryKey: gekanatorKeys.posts (),
queryFn: fetchGekanatorPosts }) 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 ( const eligiblePosts = useMemo (
() => candidatePostsFor ({ () => candidatePostsFor ({
posts, posts,
@@ -531,10 +683,12 @@ const GekanatorPage: FC = () => {
mutationFn: saveGekanatorQuestionSuggestion, mutationFn: saveGekanatorQuestionSuggestion,
onSuccess: () => { onSuccess: () => {
setQuestionSuggestion ('') setQuestionSuggestion ('')
setQuestionSuggestionAnswer ('yes')
reset () reset ()
}}) }})
const reset = () => { const reset = () => {
clearStoredGame ()
saveMutation.reset () saveMutation.reset ()
setPhase ('intro') setPhase ('intro')
setScores (new Map ()) setScores (new Map ())
@@ -554,6 +708,7 @@ const GekanatorPage: FC = () => {
setReviewCorrectPostId (null) setReviewCorrectPostId (null)
setSavedGameId (null) setSavedGameId (null)
setQuestionSuggestion ('') setQuestionSuggestion ('')
setQuestionSuggestionAnswer ('yes')
setHistory ([]) setHistory ([])
} }
@@ -659,6 +814,7 @@ const GekanatorPage: FC = () => {
const nextAnswers = [...answers, { const nextAnswers = [...answers, {
questionId: currentQuestion.id, questionId: currentQuestion.id,
questionText: currentQuestion.text, questionText: currentQuestion.text,
questionCondition: currentQuestion.condition,
answer: value, answer: value,
originalAnswer: value }] originalAnswer: value }]
const nextAskedIds = new Set ([...askedIds, currentQuestion.id]) const nextAskedIds = new Set ([...askedIds, currentQuestion.id])
@@ -784,7 +940,10 @@ const GekanatorPage: FC = () => {
return return
saveReviewedResult (gekanatorGameId => { saveReviewedResult (gekanatorGameId => {
questionSuggestionMutation.mutate ({ gekanatorGameId, questionText }) questionSuggestionMutation.mutate ({
gekanatorGameId,
questionText,
answer: questionSuggestionAnswer })
}) })
} }
@@ -936,7 +1095,12 @@ const GekanatorPage: FC = () => {
{dialogue} {dialogue}
</p> </p>
{isLoading && <p>稿...</p>} {isLoading && (
<p>
{phase === 'intro'
? '投稿を読み込んでゐます...'
: '前回のグカネータ状態を復元してゐます...'}
</p>)}
{Boolean (error) && <p>稿</p>} {Boolean (error) && <p>稿</p>}
{phase === 'intro' && !(isLoading) && posts.length > 0 && ( {phase === 'intro' && !(isLoading) && posts.length > 0 && (
@@ -1008,7 +1172,7 @@ const GekanatorPage: FC = () => {
</div> </div>
</div>)} </div>)}
{phase === 'question' && !(currentQuestion) && ( {!(isLoading) && phase === 'question' && !(currentQuestion) && (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-xl font-bold"> <p className="text-xl font-bold">
@@ -1157,7 +1321,8 @@ const GekanatorPage: FC = () => {
className="rounded border border-yellow-300 px-4 py-2 className="rounded border border-yellow-300 px-4 py-2
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"
disabled={saveMutation.isPending || questionSuggestionMutation.isPending} disabled={saveMutation.isPending
|| questionSuggestionMutation.isPending}
onClick={() => setPhase ('question_suggestion')}> onClick={() => setPhase ('question_suggestion')}>
</button> </button>
@@ -1290,13 +1455,29 @@ const GekanatorPage: FC = () => {
bg-white px-3 py-2 dark:border-red-700 bg-white px-3 py-2 dark:border-red-700
dark:bg-red-950"/> dark:bg-red-950"/>
</label> </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"> <div className="flex flex-wrap gap-2">
<button <button
type="button" type="button"
className="rounded border border-neutral-300 px-4 py-2 className="rounded border border-neutral-300 px-4 py-2
hover:bg-neutral-100 dark:border-neutral-700 hover:bg-neutral-100 dark:border-neutral-700
dark:hover:bg-red-900" dark:hover:bg-red-900"
disabled={saveMutation.isPending || questionSuggestionMutation.isPending} disabled={saveMutation.isPending
|| questionSuggestionMutation.isPending}
onClick={() => setPhase ('end')}> onClick={() => setPhase ('end')}>
</button> </button>