このコミットが含まれているのは:
2026-06-10 20:02:08 +09:00
コミット 7fe7dbd909
14個のファイルの変更606行の追加41行の削除
+99 -11
ファイルの表示
@@ -21,6 +21,7 @@ export type GekanatorQuestionKind =
| 'source'
| 'title'
| 'original_date'
| 'post_similarity'
export type GekanatorQuestionSource =
| 'default'
@@ -36,6 +37,18 @@ export type GekanatorQuestionCondition =
| { type: 'original-month-day'; monthDay: string }
| { type: 'title-length-greater-than'; length: number }
| { type: 'title-has-ascii' }
| {
type: 'post-similarity'
postId: number
answer: GekanatorAnswerValue
threshold: number
}
export type GekanatorExtraQuestion = {
id: number
text: string
source: GekanatorQuestionSource
priorityWeight: number }
export type StoredGekanatorQuestion = {
id: string
@@ -43,7 +56,8 @@ export type StoredGekanatorQuestion = {
kind: GekanatorQuestionKind
condition: GekanatorQuestionCondition
source?: GekanatorQuestionSource
priorityWeight?: number }
priorityWeight?: number
exampleAnswers?: Record<`${ number }`, GekanatorAnswerValue> }
export type GekanatorQuestion = {
id: string
@@ -52,8 +66,24 @@ export type GekanatorQuestion = {
condition: GekanatorQuestionCondition
source: GekanatorQuestionSource
priorityWeight: number
exampleAnswers?: Record<`${ number }`, GekanatorAnswerValue>
test: (post: Post) => boolean }
const directExampleAnswerFor = (
question: StoredGekanatorQuestion,
post: Post,
): GekanatorAnswerValue | null => {
const direct = question.exampleAnswers?.[String (post.id) as `${ number }`]
if (direct)
return direct
if (question.condition.type === 'post-similarity' && question.condition.postId === post.id)
return question.condition.answer
return null
}
const countBy = <T extends string | number> (values: T[]): Map<T, number> => {
const counts = new Map<T, number> ()
values.forEach (value => counts.set (value, (counts.get (value) ?? 0) + 1))
@@ -172,24 +202,59 @@ const questionableTag = (post: Post, key: string): boolean => {
const questionMatches = (
post: Post,
condition: GekanatorQuestionCondition,
question: StoredGekanatorQuestion,
): boolean => {
switch (condition.type)
const directAnswer = directExampleAnswerFor (question, post)
if (directAnswer)
return question.condition.type === 'post-similarity'
? directAnswer === question.condition.answer
: directAnswer === 'yes'
switch (question.condition.type)
{
case 'tag':
return questionableTag (post, condition.key)
return questionableTag (post, question.condition.key)
case 'source':
return hostOf (post) === condition.host
return hostOf (post) === question.condition.host
case 'original-year':
return originalYearOf (post) === condition.year
return originalYearOf (post) === question.condition.year
case 'original-month':
return originalMonthOf (post) === condition.month
return originalMonthOf (post) === question.condition.month
case 'original-month-day':
return originalMonthDayOf (post) === condition.monthDay
return originalMonthDayOf (post) === question.condition.monthDay
case 'title-length-greater-than':
return (post.title?.length ?? 0) > condition.length
return (post.title?.length ?? 0) > question.condition.length
case 'title-has-ascii':
return /[A-Za-z0-9]/.test (post.title ?? '')
case 'post-similarity':
return false
}
}
export const expectedAnswerForQuestion = (
question: StoredGekanatorQuestion | GekanatorQuestion | undefined,
post: Post | null,
): GekanatorAnswerValue | null => {
if (!(question) || !(post))
return null
const directAnswer = directExampleAnswerFor (question, post)
if (directAnswer)
return directAnswer
switch (question.condition.type)
{
case 'tag':
case 'source':
case 'original-year':
case 'original-month':
case 'original-month-day':
case 'title-length-greater-than':
case 'title-has-ascii':
return questionMatches (post, question) ? 'yes' : 'no'
case 'post-similarity':
return null
}
}
@@ -200,7 +265,7 @@ export const restoreGekanatorQuestion = (
...question,
source: question.source ?? 'default',
priorityWeight: question.priorityWeight ?? 1,
test: (post: Post) => questionMatches (post, question.condition) })
test: (post: Post) => questionMatches (post, question) })
export const storeGekanatorQuestion = (
@@ -211,7 +276,8 @@ export const storeGekanatorQuestion = (
kind: question.kind,
condition: question.condition,
source: question.source,
priorityWeight: question.priorityWeight })
priorityWeight: question.priorityWeight,
exampleAnswers: question.exampleAnswers })
export const fetchGekanatorPosts = async (): Promise<Post[]> => {
@@ -226,6 +292,15 @@ export const fetchGekanatorQuestions = async (): Promise<StoredGekanatorQuestion
}
export const fetchGekanatorExtraQuestions = async (
gameId: number,
): Promise<GekanatorExtraQuestion[]> => {
const data = await apiGet<{ questions: GekanatorExtraQuestion[] }> (
`/gekanator/games/${ gameId }/extra_questions`)
return data.questions
}
export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
const tagCounts = countBy (posts.flatMap (post =>
post.tags
@@ -393,3 +468,16 @@ export const saveGekanatorQuestionSuggestion = async ({
gekanator_game_id: gekanatorGameId,
question_text: questionText,
answer })
export const saveGekanatorExtraQuestionAnswers = async ({
gameId,
answers,
}: {
gameId: number
answers: { questionId: number; answer: GekanatorAnswerValue }[]
}) =>
await apiPost (`/gekanator/games/${ gameId }/extra_question_answers`, {
answers: answers.map (item => ({
question_id: item.questionId,
answer: item.answer })) })