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

マージ済み
みてるぞ が 17 個のコミットを feature/041 から main へマージ 2026-06-11 23:21:45 +09:00
3個のファイルの変更173行の追加11行の削除
コミット 49d42d576a の変更だけを表示してゐます - すべてのコミットを表示
+1 -1
ファイルの表示
@@ -19,7 +19,7 @@ class Post < ApplicationRecord
has_many :gekanator_correct_games, has_many :gekanator_correct_games,
class_name: 'GekanatorGame', class_name: 'GekanatorGame',
foreign_key: :correct_post_id, foreign_key: :correct_post_id,
dependent: :nullify, dependent: :delete_all,
inverse_of: :correct_post inverse_of: :correct_post
has_many :parent_post_implications, has_many :parent_post_implications,
+85 -3
ファイルの表示
@@ -65,6 +65,37 @@ const originalYearOf = (post: Post): number | null => {
} }
const originalDateOf = (post: Post): Date | null => {
const value = post.originalCreatedFrom || post.originalCreatedBefore
if (!(value))
return null
const date = new Date (value)
if (Number.isNaN (date.getTime ()))
return null
return date
}
const originalMonthOf = (post: Post): number | null => {
const date = originalDateOf (post)
if (!(date))
return null
return date.getMonth () + 1
}
const originalMonthDayOf = (post: Post): string | null => {
const date = originalDateOf (post)
if (!(date))
return null
return `${ date.getMonth () + 1 }-${ date.getDate () }`
}
const tagQuestionKey = ({ category, name }: { category: string; name: string }): string => const tagQuestionKey = ({ category, name }: { category: string; name: string }): string =>
`${ category }:${ name }` `${ category }:${ name }`
@@ -78,6 +109,27 @@ const tagFromQuestionKey = (key: string): { category: string; name: string } =>
const nicoTagLabel = (name: string): string => name.replace (/^nico:/, '') const nicoTagLabel = (name: string): string => name.replace (/^nico:/, '')
const tagQuestionText = (category: string, label: string): string => {
switch (category)
{
case 'deerjikist':
return `作者・ニジラーとして「${ label }」に関係してゐる?`
case 'meme':
return `元ネタ・ミームとして「${ label }」に関係しさう?`
case 'character':
return `${ label }」といふキャラクターが関係してゐる?`
case 'material':
return `素材として「${ label }」に関係してゐる?`
case 'nico':
return `ニコニコに「${ label }」といふタグが付いてゐる?`
case 'general':
case 'meta':
default:
return `内容として「${ label }」に関係しさう?`
}
}
const questionableTag = (post: Post, key: string): boolean => { const questionableTag = (post: Post, key: string): boolean => {
const { category, name } = tagFromQuestionKey (key) const { category, name } = tagFromQuestionKey (key)
@@ -128,6 +180,14 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
posts posts
.map (originalYearOf) .map (originalYearOf)
.filter ((year): year is number => year !== null)) .filter ((year): year is number => year !== null))
const originalMonths = countBy (
posts
.map (originalMonthOf)
.filter ((month): month is number => month !== null))
const originalMonthDays = countBy (
posts
.map (originalMonthDayOf)
.filter ((monthDay): monthDay is string => monthDay !== null))
const titleLengthMedian = median (posts.map (post => post.title?.length ?? 0)) const titleLengthMedian = median (posts.map (post => post.title?.length ?? 0))
const usefulEntries = <T extends string | number> (counts: Map<T, number>) => const usefulEntries = <T extends string | number> (counts: Map<T, number>) =>
@@ -146,9 +206,7 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
return { return {
id: `tag:${ key }`, id: `tag:${ key }`,
text: category === 'nico' text: tagQuestionText (category, label),
? `ニコニコに「${ label }」といふタグが付いてゐる?`
: `内容として「${ label }」に関係しさう?`,
kind: 'tag' as const, kind: 'tag' as const,
test: (post: Post) => questionableTag (post, String (key)) } test: (post: Post) => questionableTag (post, String (key)) }
}) })
@@ -171,6 +229,28 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
kind: 'original_date' as const, kind: 'original_date' as const,
test: (post: Post) => originalYearOf (post) === year })) test: (post: Post) => originalYearOf (post) === year }))
const originalMonthQuestions = usefulEntries (originalMonths)
.filter (([, count]) => count >= 2 && count <= Math.max (2, posts.length * .7))
.slice (0, 20)
.map (([month]) => ({
id: `original-month:${ month }`,
text: `オリジナルの投稿月は ${ month } 月?`,
kind: 'original_date' as const,
test: (post: Post) => originalMonthOf (post) === month }))
const originalMonthDayQuestions = usefulEntries (originalMonthDays)
.filter (([, count]) => count >= 2 && count <= Math.max (2, posts.length * .7))
.slice (0, 20)
.map (([monthDay]) => {
const [month, day] = String (monthDay).split ('-')
return {
id: `original-month-day:${ monthDay }`,
text: `オリジナルの投稿日は ${ month }${ day } 日?`,
kind: 'original_date' as const,
test: (post: Post) => originalMonthDayOf (post) === monthDay }
})
const titleQuestions = [ const titleQuestions = [
{ {
id: 'title:long', id: 'title:long',
@@ -191,6 +271,8 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
return [ return [
...sourceQuestions, ...sourceQuestions,
...originalYearQuestions, ...originalYearQuestions,
...originalMonthQuestions,
...originalMonthDayQuestions,
...titleQuestions, ...titleQuestions,
...tagQuestions] ...tagQuestions]
} }
+87 -7
ファイルの表示
@@ -18,7 +18,14 @@ import type { GekanatorAnswerLog,
GekanatorQuestion } from '@/lib/gekanator' GekanatorQuestion } from '@/lib/gekanator'
import type { Post } from '@/types' import type { Post } from '@/types'
type Phase = 'intro' | 'question' | 'guess' | 'continue' | 'review' | 'learned' type Phase =
| 'intro'
| 'question'
| 'guess'
| 'continue'
| 'end'
| 'review'
| 'learned'
type AnswerOption = { type AnswerOption = {
label: string label: string
@@ -687,9 +694,11 @@ const GekanatorPage: FC = () => {
} }
} }
const startReview = (correctPostId: number) => { const finishGame = (correctPostId: number) => {
const guessedPostId = const guessedPostId =
phase === 'continue' phase === 'end' || phase === 'review'
? reviewGuessedPostId
: phase === 'continue'
? lastRejectedGuessId ?? displayedGuess?.id ? lastRejectedGuessId ?? displayedGuess?.id
: displayedGuess?.id ?? lastRejectedGuessId : displayedGuess?.id ?? lastRejectedGuessId
if (!(guessedPostId)) if (!(guessedPostId))
@@ -700,6 +709,16 @@ const GekanatorPage: FC = () => {
setReviewCorrectPostId (correctPostId) setReviewCorrectPostId (correctPostId)
setSearch ('') setSearch ('')
setSelectingCorrectPost (false) setSelectingCorrectPost (false)
setPhase ('end')
}
const startReview = () => {
if (reviewGuessedPostId === null || reviewCorrectPostId === null)
return
saveMutation.reset ()
setSelectingCorrectPost (false)
setSearch ('')
setPhase ('review') setPhase ('review')
} }
@@ -805,7 +824,7 @@ const GekanatorPage: FC = () => {
return return
} }
startReview (post.id) finishGame (post.id)
} }
const filteredPosts = posts const filteredPosts = posts
@@ -963,7 +982,7 @@ const GekanatorPage: FC = () => {
hover:bg-pink-500" hover:bg-pink-500"
onClick={() => { onClick={() => {
if (displayedGuess) if (displayedGuess)
startReview (displayedGuess.id) finishGame (displayedGuess.id)
}}> }}>
</button> </button>
@@ -1022,6 +1041,67 @@ const GekanatorPage: FC = () => {
</div> </div>
</div>)} </div>)}
{phase === 'end' && (
<div className="space-y-4">
<div>
<p className="text-sm text-neutral-500"></p>
<p className="text-xl font-bold"></p>
</div>
{reviewGuessedPost && (
<div className="space-y-2">
<div className="font-bold">稿</div>
<PostMiniCard post={reviewGuessedPost}/>
</div>)}
<div className="space-y-2">
<div className="font-bold">稿</div>
{reviewCorrectPost
? <PostMiniCard post={reviewCorrectPost}/>
: <p className="text-sm text-red-600">稿</p>}
<button
type="button"
className="rounded border border-yellow-300 px-3 py-2
hover:bg-yellow-100 dark:border-red-700
dark:hover:bg-red-900"
onClick={() => setSelectingCorrectPost (true)}>
稿
</button>
</div>
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
<p className="text-sm text-neutral-600 dark:text-neutral-300">
: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'}
</p>)}
{saveMutation.isError && (
<p className="text-sm text-red-600">
</p>)}
<div className="flex flex-wrap gap-2">
<button
type="button"
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
hover:bg-pink-500 disabled:opacity-50"
disabled={
reviewCorrectPostId === null || saveMutation.isPending || saved
}
onClick={saveReviewedResult}>
</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"
disabled={reviewCorrectPostId === null || saveMutation.isPending || saved}
onClick={startReview}>
</button>
</div>
</div>)}
{phase === 'review' && ( {phase === 'review' && (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
@@ -1105,7 +1185,7 @@ const GekanatorPage: FC = () => {
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"
onClick={() => setPhase ('guess')}> onClick={() => setPhase ('end')}>
</button> </button>
</div> </div>
@@ -1126,7 +1206,7 @@ const GekanatorPage: FC = () => {
</div> </div>
</section> </section>
{['guess', 'continue', 'question', 'review'].includes (phase) {['guess', 'continue', 'question', 'end', 'review'].includes (phase)
&& selectingCorrectPost && ( && selectingCorrectPost && (
<section className="rounded-lg border border-yellow-300 bg-white p-4 <section className="rounded-lg border border-yellow-300 bg-white p-4
dark:border-red-800 dark:bg-red-950"> dark:border-red-800 dark:bg-red-950">