From 491d8df20a41b9291de18cf596e77f56d209d93f Mon Sep 17 00:00:00 2001 From: nakasyou Date: Tue, 12 Nov 2024 19:06:05 +0900 Subject: [PATCH] wip --- src/islands/ai-quiz/QuizScreen.css | 3 + src/islands/ai-quiz/QuizScreen.tsx | 154 +++++++++++++++++++--------- src/islands/ai-quiz/quiz-manager.ts | 58 +++++++++-- src/islands/ai-quiz/storage.ts | 4 +- src/islands/shared/hash.ts | 12 +++ 5 files changed, 173 insertions(+), 58 deletions(-) create mode 100644 src/islands/ai-quiz/QuizScreen.css create mode 100644 src/islands/shared/hash.ts diff --git a/src/islands/ai-quiz/QuizScreen.css b/src/islands/ai-quiz/QuizScreen.css new file mode 100644 index 0000000..b08076a --- /dev/null +++ b/src/islands/ai-quiz/QuizScreen.css @@ -0,0 +1,3 @@ +.nanoha-sheet { + @apply bg-secondary-container text-on-secondary-container; +} \ No newline at end of file diff --git a/src/islands/ai-quiz/QuizScreen.tsx b/src/islands/ai-quiz/QuizScreen.tsx index dc99452..d4fb407 100644 --- a/src/islands/ai-quiz/QuizScreen.tsx +++ b/src/islands/ai-quiz/QuizScreen.tsx @@ -12,6 +12,8 @@ import { QuizDB, type Quizzes } from './storage' import type { QuizContent } from './constants' import { shuffle } from '../../utils/arr' import { icon } from '../../utils/icons' +import type { TextNoteData } from '../note/components/notes/TextNote/types' +import './QuizScreen.css' const QuizSelection = (props: { text: string @@ -66,8 +68,8 @@ const SelectAnswerScreen = (props: { 😒低正答率 ( {Math.round( (props.quiz.rate.correct / props.quiz.rate.proposed) * 10000, - ) / 100}% - ) + ) / 100} + % ) } > @@ -134,6 +136,22 @@ const CorrectShow = (props: { ) } +const ExplainContent = (props: { + quiz: GeneratedQuiz +}) => { + return
+
+
✨AI による解説
+
{props.quiz.content.explanation}
+
+
+
📒使用されたノート
+
+
+
+
+
+} /** 不正解時 */ const ExplainScreen = (props: { quiz: GeneratedQuiz @@ -141,13 +159,30 @@ const ExplainScreen = (props: { onEnd(): void }) => { + const [getIsShownExplain, setIsShownExplain] = createSignal(false) + const [getIsTopExplain, setIsTopExplain] = createSignal(false) + return (
+ +
+
+
解説
+
+ +
+
😒不正解..
{props.quiz.content.question}
-
+
選択肢
あなたの回答
@@ -185,8 +220,22 @@ const ExplainScreen = (props: { )}
+
+
) } diff --git a/src/islands/ai-quiz/quiz-manager.ts b/src/islands/ai-quiz/quiz-manager.ts index 3fd19f0..9caeaac 100644 --- a/src/islands/ai-quiz/quiz-manager.ts +++ b/src/islands/ai-quiz/quiz-manager.ts @@ -5,6 +5,7 @@ import type { MargedNoteData } from '../note/components/notes-utils' import type { TextNoteData } from '../note/components/notes/TextNote/types' import type { QuizDB, Quizzes } from './storage' import { shuffle } from '../../utils/arr' +import { sha256 } from '../shared/hash' const generateQuizzesFromAI = async (text: string): Promise => { const gemini = getGoogleGenerativeAI() @@ -53,24 +54,58 @@ export interface GeneratedQuiz { proposed: number correct: number } + usedNote: TextNoteData } export class QuizManager { #db: QuizDB constructor(db: QuizDB) { this.#db = db } - async #getNeverProposedQuizzes(noteId: number) { + async #getNeverProposedQuizzes(noteId: number, notes: MargedNoteData[]) { + const noteHashes = new Set(await Promise.all(notes.map(note => sha256(JSON.stringify(note.canToJsonData))))) + const quizzes = await this.#db.quizzes.where({ noteId, proposeCount: 0 }).toArray() + + const toDeleteIndexes: number[] = [] + for (let i = 0; i < quizzes.length; i++) { + const quiz = quizzes[i]! + if (!noteHashes.has(quiz.noteHash)) { + await this.#db.quizzes.delete(quiz.id!) + + toDeleteIndexes.push(i) + } + } + + for (const toDeleteIndex of toDeleteIndexes.reverse()) { + quizzes.splice(toDeleteIndex, 1) + } + return quizzes } - async getLowCorrectRateQuizzes(noteId: number) { + async getLowCorrectRateQuizzes(noteId: number, notes: MargedNoteData[]) { + const noteHashes = new Set(await Promise.all(notes.map(note => sha256(JSON.stringify(note.canToJsonData))))) + const quizzes = (await this.#db.quizzes.where({ noteId }).toArray()) .filter(q => q.proposeCount > 0).sort((a, b) => (a.correctCount / a.proposeCount) - (b.correctCount / b.proposeCount)) + + const toDeleteIndexes: number[] = [] + for (let i = 0; i < quizzes.length; i++) { + const quiz = quizzes[i]! + if (!noteHashes.has(quiz.noteHash)) { + await this.#db.quizzes.delete(quiz.id!) + + toDeleteIndexes.push(i) + } + } + for (const toDeleteIndex of toDeleteIndexes.reverse()) { + quizzes.splice(toDeleteIndex, 1) + } + return quizzes } async #addProposedQuizz(notes: MargedNoteData[], noteId: number) { @@ -78,13 +113,14 @@ export class QuizManager { const randomTextNote = textNotes[Math.floor(textNotes.length * Math.random())] const generated = await generateQuizzesFromAI(randomTextNote?.canToJsonData.html ?? '') - const quizzes = generated.map(content => ({ + const quizzes = await Promise.all(generated.map(async content => ({ content, correctCount: 0, proposeCount: 0, noteDataId: randomTextNote?.id ?? '', - noteId - } satisfies Quizzes)) + noteId, + noteHash: await sha256(JSON.stringify(randomTextNote?.canToJsonData)) + } satisfies Quizzes))) await this.#db.quizzes.bulkAdd(quizzes) } @@ -92,7 +128,7 @@ export class QuizManager { const quizzes: Map = new Map() // First, propose 5 low rate quizzes - const lowRates = await this.getLowCorrectRateQuizzes(noteId) + const lowRates = await this.getLowCorrectRateQuizzes(noteId, notes) for (let i = 0; i < 5; i++) { const lowRateQuiz = lowRates[i] if (!lowRateQuiz) { @@ -105,25 +141,27 @@ export class QuizManager { noteDataId: lowRateQuiz.noteDataId, rate: { proposed: lowRateQuiz.proposeCount, correct: lowRateQuiz.correctCount - } + }, + usedNote: notes.find(note => note.id === lowRateQuiz.noteDataId)! as TextNoteData }) } // Second, generate quizzes while (true) { - const gotQuizzes = shuffle(await this.#getNeverProposedQuizzes(noteId)) + const gotQuizzes = shuffle(await this.#getNeverProposedQuizzes(noteId, notes)) for (const quiz of gotQuizzes) { quizzes.set(quiz.id ?? 0, { id: quiz.id ?? 0, content: quiz.content, reason: 'new', noteDataId: quiz.noteDataId, - rate: { proposed: quiz.proposeCount, correct: quiz.correctCount } + rate: { proposed: quiz.proposeCount, correct: quiz.correctCount }, + usedNote: notes.find(note => note.id === quiz.noteDataId)! as TextNoteData }) } if ([...quizzes.keys()].length >= n) { - return shuffle([...quizzes.values()]) + return shuffle([...quizzes.values()]).slice(0, n) } await this.#addProposedQuizz(notes, noteId) } diff --git a/src/islands/ai-quiz/storage.ts b/src/islands/ai-quiz/storage.ts index ad0b6b6..7a20a7c 100644 --- a/src/islands/ai-quiz/storage.ts +++ b/src/islands/ai-quiz/storage.ts @@ -8,6 +8,8 @@ export interface Quizzes { noteId: number /** ノートの中のノートの ID */ noteDataId: string + /** ノートの SHA256 */ + noteHash: string /** 問題 */ content: QuizContent @@ -25,7 +27,7 @@ export class QuizDB extends Dexie { this.version(1).stores({ quizzes: - 'id++, noteId, noteDataId, content, proposeCount, correctCount', + 'id++, noteId, noteDataId, noteHash, content, proposeCount, correctCount', }) this.quizzes = this.table('quizzes') diff --git a/src/islands/shared/hash.ts b/src/islands/shared/hash.ts new file mode 100644 index 0000000..cedc581 --- /dev/null +++ b/src/islands/shared/hash.ts @@ -0,0 +1,12 @@ +export const sha256 = async (input: string): Promise => { + const encoder = new TextEncoder() + + const data = encoder.encode(input) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + + // Convert bytes to hex + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') + + return hashHex +} \ No newline at end of file