Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
nakasyou committed Nov 11, 2024
1 parent d4a6b04 commit 0b7ffb8
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 33 deletions.
141 changes: 123 additions & 18 deletions src/islands/ai-quiz/QuizScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const QuizSelection = (props: {
createEffect(() => {
props.onChange(getSelected())
})

return (
<div class="w-full">
<button
Expand All @@ -46,6 +47,7 @@ const SelectAnswerScreen = (props: {
}) => {
const [getSelected, setSelected] = createSignal<string[]>([])
const getSelections = createMemo(() => {
setSelected([])
const allSelections = [
...props.quiz.content.corrects,
...props.quiz.content.damys,
Expand All @@ -57,6 +59,20 @@ const SelectAnswerScreen = (props: {
<div class="h-full grid place-items-center">
<div class="text-lg p-2">
<div>{props.quiz.content.question}</div>
<Show
when={props.quiz.reason === 'new'}
fallback={
<div>
😒低正答率 (
{Math.round(
(props.quiz.rate.correct / props.quiz.rate.proposed) * 10000,
) / 100}%
)
</div>
}
>
⚡新しい問題
</Show>
</div>
<div class="grid grid-cols-2 gap-2">
<For each={getSelections()}>
Expand Down Expand Up @@ -127,8 +143,10 @@ const ExplainScreen = (props: {
}) => {
return (
<div class="h-full flex flex-col justify-between p-2">
<div class="text-3xl text-center my-2">😒不正解..</div>
<div class="text-center p-2">{props.quiz.content.question}</div>
<div>
<div class="text-3xl text-center my-2">😒不正解..</div>
<div class="text-center p-2">{props.quiz.content.question}</div>
</div>
<div class="grid place-items-center">
<div class="grid grid-cols-3 gap-2">
<div>選択肢</div>
Expand Down Expand Up @@ -169,7 +187,11 @@ const ExplainScreen = (props: {
</div>
</div>
<div class="grid place-items-center">
<button class="flex items-center filled-button" type="button" onClick={() => props.onEnd()}>
<button
class="flex items-center filled-button"
type="button"
onClick={() => props.onEnd()}
>
次の問題
<div innerHTML={icon('chevronRight')} class="w-8 h-8" />
</button>
Expand All @@ -178,6 +200,52 @@ const ExplainScreen = (props: {
)
}

const ResultScreen = (props: {
correct: number
all: number

onFinish(): void
onNextRound(): void
}) => {
const rate = createMemo(() => (props.correct / props.all) * 100)

return (
<div class="h-full flex flex-col justify-around items-center">
<div>
<div
class="w-48 h-48 rounded-full"
style={{
background: `conic-gradient(rgb(16 185 129) 0%, rgb(16 185 129) ${rate()}%, rgb(239 68 68) ${rate()}%, rgb(239 68 68) 100%)`,
}}
/>
<div class="flex flex-wrap gap-2 justify-center">
<div>{Math.round(rate() * 100) / 100}%</div>
<div>
{props.correct} / {props.all} 正解
</div>
</div>
</div>
<div class="flex">
<button
onClick={() => props.onNextRound()}
type="button"
class="filled-button flex gap-2 justify-center items-center"
>
次のラウンド
<div class="w-10 h-10" innerHTML={icon('chevronRight')} />
</button>
<button
onClick={() => props.onFinish()}
type="button"
class="text-button"
>
終了する
</button>
</div>
</div>
)
}

export const QuizScreen = (props: {
notes: MargedNoteData[]
noteId: number
Expand All @@ -187,51 +255,82 @@ export const QuizScreen = (props: {
const [getIsShownCorrect, setIsShownCorrect] = createSignal(false)
const [getIsShownExplain, setIsShownExplain] = createSignal(false)
const [getSelected, setSelected] = createSignal<string[]>([])
const [getCorrectCount, setCorrectCount] = createSignal(0)
const currentQuiz = createMemo(() => getQuizzes()[getQuizIndex()])

onMount(async () => {
// Init
const db = new QuizDB()
const quizManager = new QuizManager(db)
const isFinished = createMemo(() => getQuizIndex() === 10)

let quizManager!: QuizManager

const nextRound = async () => {
// Generate
const generated = await quizManager.generateQuizzes(
5,
10,
props.notes,
props.noteId,
)
setQuizzes(generated)
setQuizIndex(0)
setIsShownCorrect(false)
setIsShownExplain(false)
setSelected([])
setCorrectCount(0)
}
onMount(async () => {
// Init
const db = new QuizDB()
quizManager = new QuizManager(db)
await nextRound()
})

const answered = (selected: string[]) => {
const selectedSet = new Set(selected)
const correctIndexiesSet = new Set(currentQuiz()!.content.corrects)
const correctSet = new Set(currentQuiz()!.content.corrects)

const isCorrect =
selectedSet.isSubsetOf(correctIndexiesSet) &&
selectedSet.isSupersetOf(correctIndexiesSet)
selectedSet.isSubsetOf(correctSet) && selectedSet.isSupersetOf(correctSet)

setSelected(selected)

if (isCorrect) {
setIsShownCorrect(true)
setCorrectCount((p) => p + 1)
} else {
setIsShownExplain(true)
}
quizManager.updateQuizStat(currentQuiz()!.id, isCorrect)
}

const nextQuiz = () => {
setQuizIndex(p => p + 1)
setQuizIndex((p) => p + 1)
setSelected([])
setIsShownExplain(false)
setIsShownCorrect(false)
}

const finish = () => {
location.href = location.href.replace(/\/quiz\/?$/, '')
}

return (
<div class="h-full">
<Show
when={currentQuiz()}
fallback={<div class="h-full grid place-items-center">生成中...</div>}
fallback={
<Show
when={isFinished()}
fallback={
<div class="h-full grid place-items-center">生成中...</div>
}
>
<ResultScreen
all={10}
correct={getCorrectCount()}
onFinish={() => finish()}
onNextRound={() => nextRound()}
/>
</Show>
}
>
{(quiz) => (
<Show
Expand All @@ -240,15 +339,21 @@ export const QuizScreen = (props: {
<SelectAnswerScreen quiz={quiz()} onAnswer={(s) => answered(s)} />
}
>
<ExplainScreen quiz={currentQuiz()!} selected={getSelected()} onEnd={() => nextQuiz()} />
<ExplainScreen
quiz={currentQuiz()!}
selected={getSelected()}
onEnd={() => nextQuiz()}
/>
</Show>
)}
</Show>
<Show when={getIsShownCorrect()}>
<CorrectShow onEndShow={() => {
setIsShownCorrect(false)
nextQuiz()
}} />
<CorrectShow
onEndShow={() => {
setIsShownCorrect(false)
nextQuiz()
}}
/>
</Show>
</div>
)
Expand Down
78 changes: 64 additions & 14 deletions src/islands/ai-quiz/quiz-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,28 @@ const generateQuizzesFromAI = async (text: string): Promise<QuizContent[]> => {
if (!Array.isArray(json)) {
return []
}
return json.filter(r => safeParse(CONTENT_SCHEMA, r).success)
return json.flatMap(r => {
const parsed = safeParse(CONTENT_SCHEMA, r)
if (!parsed.success) {
return []
}
const data = parsed.output
if (!new Set(data.damys).isDisjointFrom(new Set(data.corrects))) {
return []
}
return [data]
})
}

export interface GeneratedQuiz {
content: QuizContent
noteDataId: string
reason: 'new'
reason: 'new' | 'lowRate'
id: number
rate: {
proposed: number
correct: number
}
}
export class QuizManager {
#db: QuizDB
Expand All @@ -51,6 +66,13 @@ export class QuizManager {
}).toArray()
return quizzes
}
async getLowCorrectRateQuizzes(noteId: number) {
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))
return quizzes
}
async #addProposedQuizz(notes: MargedNoteData[], noteId: number) {
const textNotes = notes.filter(note => note.type === 'text') as TextNoteData[]
const randomTextNote = textNotes[Math.floor(textNotes.length * Math.random())]
Expand All @@ -67,25 +89,53 @@ export class QuizManager {
await this.#db.quizzes.bulkAdd(quizzes)
}
async generateQuizzes(n: number, notes: MargedNoteData[], noteId: number): Promise<GeneratedQuiz[]> {
const quizzes: Map<number, GeneratedQuiz> = new Map()

// First, propose 5 low rate quizzes
const lowRates = await this.getLowCorrectRateQuizzes(noteId)
for (let i = 0; i < 5; i++) {
const lowRateQuiz = lowRates[i]
if (!lowRateQuiz) {
break
}
quizzes.set(lowRateQuiz.id ?? 0, {
content: lowRateQuiz.content,
id: lowRateQuiz.id ?? 0,
reason: 'lowRate',
noteDataId: lowRateQuiz.noteDataId,
rate: {
proposed: lowRateQuiz.proposeCount, correct: lowRateQuiz.correctCount
}
})
}

// Second, generate quizzes
while (true) {
const gotQuizzes = await this.#getNeverProposedQuizzes(noteId)
if (gotQuizzes.length >= n) {
return shuffle(gotQuizzes).slice(0, n).map(data => ({
content: data.content,
noteDataId: data.noteDataId,
const gotQuizzes = shuffle(await this.#getNeverProposedQuizzes(noteId))
for (const quiz of gotQuizzes) {
quizzes.set(quiz.id ?? 0, {
id: quiz.id ?? 0,
content: quiz.content,
reason: 'new',
id: data.id ?? 0
}))
noteDataId: quiz.noteDataId,
rate: { proposed: quiz.proposeCount, correct: quiz.correctCount }
})
}

if ([...quizzes.keys()].length >= n) {
return shuffle([...quizzes.values()])
}
await this.#addProposedQuizz(notes, noteId)
}
}
async updateQuizStat (id: number, corrected: boolean) {
async updateQuizStat(id: number, corrected: boolean) {
const prev = await this.#db.quizzes.get(id)
if (corrected) {
prev.correctCount ++
if (!prev) {
return
}
prev.proposeCount ++
await this.#db.quizzes
await this.#db.quizzes.update(id, {
proposeCount: prev.proposeCount + 1,
correctCount: corrected ? prev.correctCount + 1 : prev.correctCount
})
}
}
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"jsxImportSource": "solid-js",
"allowImportingTsExtensions": true,
"types": ["bun-types"],
"noUncheckedIndexedAccess": true
"noUncheckedIndexedAccess": true,
"lib": ["dom", "ESNext"],
"target": "ESNext"
},
"exclude": ["dist", "./node_modules/@qwikdev/astro/**/*"]
}

0 comments on commit 0b7ffb8

Please sign in to comment.