グカネータ作成 (#041) #362
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする