Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Super Hyper Learning #211

Merged
merged 5 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 43 additions & 22 deletions src/islands/ai-quiz/components/Finished.qwik.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
/** @jsxImportSource @builder.io/qwik */

import { component$, useComputed$, useContext } from '@builder.io/qwik'
import { QUIZ_STATE_CTX, type QuizState } from '../store'
import {
component$,
useComputed$,
useContext,
useVisibleTask$,
} from '@builder.io/qwik'
import { QUIZ_STATE_CTX, SCREEN_STATE_CTX } from '../store'
import { QuizDB } from '../storage'

export const FinishedScreen = component$(() => {
const screenState = useContext(SCREEN_STATE_CTX)
const quizState = useContext(QUIZ_STATE_CTX)

const result = useComputed$(() => {
return {
all: quizState.goalQuestions,
all: quizState.correctQuizzes.length + quizState.incorrectQuizzes.length,
correct: quizState.correctQuizzes.length,
incorrect: quizState.incorrectQuizzes.length,

isAllCorrect: quizState.incorrectQuizzes.length === 0,
}
})

useVisibleTask$(async ({ track }) => {
track(() => quizState.incorrectQuizzes)

const quizDB = new QuizDB()
screenState.lastMissedQuizIds = quizState.incorrectQuizzes.map((q) => q.id)

for (const q of quizState.incorrectQuizzes) {
const current = await quizDB.quizzesByNote.get(q.id)
if (!current) continue
current.rateSource.total++
await quizDB.quizzesByNote.update(q.id, {
rateSource: current.rateSource,
rate: current.rateSource.correct / current.rateSource.total,
})
}
for (const q of quizState.correctQuizzes) {
const current = await quizDB.quizzesByNote.get(q.id)
if (!current) continue
current.rateSource.correct++
current.rateSource.total++
const rate = current.rateSource.correct / current.rateSource.total
if (rate > 0.8) {
// もうたぶん覚えた
await quizDB.quizzesByNote.delete(q.id)
continue
}
await quizDB.quizzesByNote.update(q.id, {
rateSource: current.rateSource,
rate,
})
}
})

return (
<div>
<div class="text-3xl text-center font-bold">Finished!</div>
Expand All @@ -36,28 +76,9 @@ export const FinishedScreen = component$(() => {
</div>
</div>
<div class="flex flex-col gap-2">
<button
onClick$={() => {
quizState.quizzes = quizState.incorrectQuizzes
quizState.goalQuestions = quizState.incorrectQuizzes.length
quizState.generatedQuizzes = quizState.incorrectQuizzes.length

quizState.correctQuizzes = []
quizState.incorrectQuizzes = []

quizState.current = null
quizState.isFinished = false
}}
class="filled-button disabled:opacity-30"
disabled={result.value.isAllCorrect}
type="button"
>
間違えた問題をやり直す
</button>
<button
onClick$={() => {
quizState.quizzes = []
quizState.goalQuestions = 5
quizState.correctQuizzes = []
quizState.incorrectQuizzes = []
quizState.current = null
Expand Down
174 changes: 151 additions & 23 deletions src/islands/ai-quiz/components/Quiz.qwik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
$,
component$,
useComputed$,
useContext,
useContextProvider,
useSignal,
Expand All @@ -12,7 +13,9 @@ import {
import {
QUIZ_STATE_CTX,
SCREEN_STATE_CTX,
SETTINGS_CTX,
type Quiz,
type QuizFrom,
type QuizState,
} from '../store'
import { shuffle } from '../../../utils/arr'
Expand All @@ -26,6 +29,7 @@ import {
} from '../constants'
import { safeParse } from 'valibot'
import { Loading } from './Utils.qwik'
import { QuizDB } from '../storage'

export const QuizScreen = component$(() => {
const quizState = useStore<QuizState>(
Expand All @@ -35,20 +39,22 @@ export const QuizScreen = component$(() => {

quizzes: [],
current: null,
goalQuestions: 5,

isFinished: false,

generatedQuizzes: 0,

finishedQuizIndexes: new Set(),

lastMissedQuizzes: 0,
},
{
deep: false,
},
)
useContextProvider(QUIZ_STATE_CTX, quizState)
const screenState = useContext(SCREEN_STATE_CTX)
const settings = useContext(SETTINGS_CTX)

const isShownCorrectDialog = useSignal(false)
const isShownIncorrectScreen = useSignal<
Expand All @@ -58,6 +64,10 @@ export const QuizScreen = component$(() => {
}
>(false)

const allQuizzes = useComputed$(() => {
return quizState.lastMissedQuizzes + quizState.generatedQuizzes
})

const setQuiz = $(() => {
const arailableQuizIndexes = quizState.quizzes
.map((_quiz, index) => {
Expand All @@ -76,20 +86,92 @@ export const QuizScreen = component$(() => {
return
}
quizState.current = {
quiz: nextQuiz,
quiz: nextQuiz.quiz,
choices: shuffle([
...nextQuiz.content.damyAnswers,
nextQuiz.content.correctAnswer,
...nextQuiz.quiz.content.damyAnswers,
nextQuiz.quiz.content.correctAnswer,
]),
index: (quizState.current?.index ?? -1) + 1,
from: nextQuiz.from,
}
const nextQuizzes = [...quizState.quizzes]
nextQuizzes.splice(nextQuizIndex, 1)
quizState.quizzes = nextQuizzes
})
useVisibleTask$(async ({ track }) => {
track(() => quizState.isFinished)

const quizDB = new QuizDB()

const notes =
typeof screenState.note === 'string' ? [] : screenState.note?.notes!

const missedQuizzes: typeof quizState.quizzes = await Promise.all(
screenState.lastMissedQuizIds.map((id) =>
quizDB.quizzesByNote.get(id).then(
(q) =>
({
quiz: {
id,
content: q!.quiz,
source: notes.find((note) => note.id === q!.noteId)!,
},
from: 'missed',
}) as const,
),
),
)

quizState.lastMissedQuizzes = screenState.lastMissedQuizIds.length

// Low Correct Rate
const targetNotebook =
screenState.noteLoadType.from === 'local'
? `local-${screenState.noteLoadType.id}`
: ''
const howManyUseLowCorrectRate = Math.max(
settings.quizzesByRound - missedQuizzes.length,
5,
)
const lowRateQuizzes = await quizDB.quizzesByNote
.where('targetNotebook')
.equals(targetNotebook)
.sortBy('rate')
const deletePromises: Promise<void>[] = []

quizState.quizzes = [
...missedQuizzes,
...lowRateQuizzes
// Check timestamp as same
.filter((q) => {
if (!screenState.rangeNotes.has(q.noteId)) {
return false
}
if (
notes.find((note) => note.id === q.noteId)!.timestamp ===
q.noteTimestamp
) {
return true
}
deletePromises.push(quizDB.quizzesByNote.delete(q.id!))
return false
})
.slice(0, howManyUseLowCorrectRate)
.map(
(q) =>
({
quiz: {
id: q.id!,
content: q!.quiz,
source: notes.find((note) => note.id === q!.noteId)!,
},
from: 'lowRate' satisfies QuizFrom,
}) as const,
),
]

isShownIncorrectScreen.value = false

// Generate Quizzes
if (
screenState.note === 'pending' ||
Expand All @@ -102,7 +184,7 @@ export const QuizScreen = component$(() => {
if (!gemini) {
return alert('AIエラー')
}
const model = await gemini.getGenerativeModel({
const model = gemini.getGenerativeModel({
model: 'gemini-1.5-flash',
generationConfig: {
responseMimeType: 'application/json',
Expand All @@ -117,8 +199,12 @@ export const QuizScreen = component$(() => {
(note) => note.type === 'text',
)

let generatedQuizzes = 0
while (true) {
if (quizState.generatedQuizzes >= quizState.goalQuestions) {
if (
generatedQuizzes + quizState.lastMissedQuizzes >
settings.quizzesByRound
) {
break
}
const randomNote =
Expand All @@ -128,23 +214,61 @@ export const QuizScreen = component$(() => {
.startChat()
.sendMessage(randomNote?.canToJsonData.html || '')

const contents: unknown[] = JSON.parse(res.response.text())
let contents: unknown
try {
contents = JSON.parse(res.response.text())
} catch {
// Unable to parse
continue
}

const quizzes = contents
.filter(
(content): content is QuizContent =>
safeParse(CONTENT_SCHEMA, content).success,
)
.map(
(content) =>
({
if (!Array.isArray(contents)) {
continue
}
const quizzes = await Promise.all(
contents
.filter(
(content): content is QuizContent =>
safeParse(CONTENT_SCHEMA, content).success,
)
.map(async (content): Promise<Quiz> => {
const res = await quizDB.quizzesByNote.add({
noteId: randomNote.id,
quiz: {
type: 'select',
...content,
},
rate: 0,
rateSource: {
correct: 0,
total: 0,
},
targetNotebook:
screenState.noteLoadType.from === 'local'
? `local-${screenState.noteLoadType.id}`
: 'unknown',
noteTimestamp: randomNote.timestamp,
})
return {
content: content,
source: randomNote,
}) satisfies Quiz,
)

quizState.generatedQuizzes += quizzes.length
quizState.quizzes = [...quizState.quizzes, ...quizzes]
id: res,
}
}),
)
const addingQuizzes: typeof quizState.quizzes = []
for (const quiz of quizzes) {
generatedQuizzes += 1
if (
generatedQuizzes + quizState.lastMissedQuizzes >
settings.quizzesByRound
) {
break
}
addingQuizzes.push({ quiz, from: 'generated' })
}
quizState.quizzes = [...quizState.quizzes, ...addingQuizzes]
quizState.generatedQuizzes = generatedQuizzes
}
})

Expand Down Expand Up @@ -177,7 +301,7 @@ export const QuizScreen = component$(() => {
* 次の問題
*/
const next = $(() => {
if (quizState.goalQuestions === (quizState.current?.index ?? 0) + 1) {
if (settings.quizzesByRound === (quizState.current?.index ?? 0) + 1) {
quizState.isFinished = true
return
}
Expand Down Expand Up @@ -233,14 +357,18 @@ export const QuizScreen = component$(() => {
<div class="h-full flex flex-col">
<div>
問<span>{quizState.current.index + 1}</span>/
<span>{quizState.goalQuestions}</span>
<span>{settings.quizzesByRound}</span>
</div>
<div class="text-2xl text-center">
{quizState.current.quiz.content.question}
</div>
<hr class="my-2" />
<div class="text-base text-on-surface-variant text-right">
✨AI Generated
{quizState.current.from === 'generated'
? '⚡新しい問題'
: quizState.current.from === 'missed'
? '📝再チャレンジ'
: '😒低正答率'}
</div>
<div class="grow grid items-center">
<div class="flex flex-col gap-2 justify-around grow">
Expand Down
5 changes: 4 additions & 1 deletion src/islands/ai-quiz/index.qwik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ export default component$<{
noteLoadType: props.noteLoadType,

rangeNotes: new Set(),

lastMissedQuizIds: [],
})
useContextProvider(SCREEN_STATE_CTX, screenState)

const settings = useStore<Settings>({
quizzes: 5,
quizzesByRound: 5,
lowRateQuizzesInRound: 1,
})
useContextProvider(SETTINGS_CTX, settings)

Expand Down
Loading
Loading