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