グカネータ作成 / テスト型バグ修正 (#41) #363
@@ -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
|
||||
@@ -8,7 +8,8 @@ class GekanatorQuestionSuggestionsController < ApplicationController
|
||||
suggestion = GekanatorQuestionSuggestion.new(
|
||||
gekanator_game: game,
|
||||
user: current_user,
|
||||
question_text: params.require(:question_text))
|
||||
question_text: params.require(:question_text),
|
||||
answer: params.require(:answer))
|
||||
|
||||
if suggestion.save
|
||||
render json: { id: suggestion.id }, status: :created
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
class GekanatorQuestionSuggestion < ApplicationRecord
|
||||
MAX_QUESTIONS_PER_GAME = 1
|
||||
ANSWERS = ['yes', 'no', 'partial', 'probably_no', 'unknown'].freeze
|
||||
|
||||
belongs_to :gekanator_game
|
||||
belongs_to :user
|
||||
|
||||
validates :question_text, presence: true, length: { maximum: 1000 }
|
||||
validates :answer, presence: true, inclusion: { in: ANSWERS }
|
||||
validates :processed, inclusion: { in: [true, false] }
|
||||
validate :question_suggestion_limit_per_game, on: :create
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :gekanator do
|
||||
resources :games, only: [:create], controller: '/gekanator_games'
|
||||
resources :posts, only: [:index], controller: '/gekanator_posts'
|
||||
resources :question_suggestions,
|
||||
only: [:create],
|
||||
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.
|
||||
|
||||
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|
|
||||
t.string "name", 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.datetime "created_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 ["user_id"], name: "index_gekanator_question_suggestions_on_user_id"
|
||||
end
|
||||
|
||||
+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>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする