Skip to content

Commit

Permalink
Add pinyin quiz
Browse files Browse the repository at this point in the history
  • Loading branch information
bradleyayers committed Nov 11, 2024
1 parent 4cb0473 commit ac5f46a
Show file tree
Hide file tree
Showing 16 changed files with 783 additions and 726 deletions.
2 changes: 1 addition & 1 deletion projects/app/bin/generateRadicalMnemonics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export const mnemonics = new Map<string, { mnemonic: string; rationale: string }

// Write ts to disk using async node fs APIs
await writeFile(
join(import.meta.dirname, `../src/dictionary/radicalMnemonics.gen.ts`),
join(import.meta.dirname, `../src/dictionary/radicalNameMnemonics.gen.ts`),
ts,
`utf8`,
);
Expand Down
10 changes: 5 additions & 5 deletions projects/app/src/app/(sidenav)/radical/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export default function RadicalPage() {
}
body={
<>
{radical?.mnemonic !== undefined ? (
{radical?.nameMnemonic !== undefined ? (
<ReferencePageBodySection title="Mnemonic">
{radical.mnemonic}
{radical.nameMnemonic}
</ReferencePageBodySection>
) : null}

Expand All @@ -32,9 +32,9 @@ export default function RadicalPage() {
</ReferencePageBodySection>
) : null}

{radical?.pronunciations !== undefined ? (
<ReferencePageBodySection title="Pronunciation">
{radical.pronunciations.join(`, `)}
{radical?.pinyin !== undefined ? (
<ReferencePageBodySection title="Pinyin">
{radical.pinyin.join(`, `)}
</ReferencePageBodySection>
) : null}
</>
Expand Down
60 changes: 27 additions & 33 deletions projects/app/src/app/learn/hsk1.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { QuizDeck } from "@/components/QuizDeck";
import { useReplicache } from "@/components/ReplicacheContext";
import { generateQuestionForSkill } from "@/data/generator";
import { IndexName, indexScan, marshalSkillStateKey } from "@/data/marshal";
import { HanziSkill, Skill, SkillType } from "@/data/model";
import { sentryCaptureException } from "@/components/util";
import { generateQuestionForSkillOrThrow } from "@/data/generator";
import { marshalSkillStateKey } from "@/data/marshal";
import { HanziSkill, Question, QuestionType, SkillType } from "@/data/model";
import { questionsForReview } from "@/data/query";
import { hsk1Words } from "@/dictionary/words";
import { useQuery } from "@tanstack/react-query";
import isEqual from "lodash/isEqual";
import shuffle from "lodash/shuffle";
import take from "lodash/take";
import { useId } from "react";
import { Text, View } from "react-native";

Expand All @@ -18,36 +18,20 @@ export default function LearnHsk1Page() {
queryKey: [LearnHsk1Page.name, `quiz`, useId()],
queryFn: async () => {
const quizSize = 10;
const skills: Skill[] = [];

const radicalSkillTypes = new Set([
// SkillType.EnglishToRadical,
SkillType.HanziWordToEnglish,
]);

// Start with practicing skills that are due
skills.push(
...(await r.query(async (tx) => {
const now = new Date();
return take(
shuffle(
// TODO: paginate or have a radical index
(await indexScan(tx, IndexName.SkillStateByDue, 50))
.filter(
([{ type, hanzi }]) =>
radicalSkillTypes.has(type) && hsk1Words.includes(hanzi),
)
.filter(([, { due }]) => due <= now)
.map(([skill]) => skill),
),
quizSize,
);
})),
const questions: Question[] = await r.query((tx) =>
questionsForReview(tx, {
limit: quizSize,
sampleSize: 50,
filter: (skill) => hsk1Words.includes(skill.hanzi),
skillTypes: [SkillType.HanziWordToEnglish],
}),
);

// Fill the rest with new skills
// Create skills to pad out the rest of the quiz
if (skills.length < quizSize) {
if (questions.length < quizSize) {
const hsk1Skills: HanziSkill[] = [];
for (const hanzi of hsk1Words) {
hsk1Skills.push({
Expand All @@ -60,20 +44,30 @@ export default function LearnHsk1Page() {
for (const skill of hsk1Skills) {
if (
// Don't add skills that are already in the quiz
!skills.some((s) => isEqual(s.hanzi, skill.hanzi)) &&
!questions.some(
(q) =>
q.type === QuestionType.OneCorrectPair &&
isEqual(q.skill.hanzi, skill.hanzi),
) &&
// Don't include skills that are already practiced
!(await tx.has(marshalSkillStateKey(skill)))
) {
skills.push(skill);
if (skills.length === quizSize) {
try {
questions.push(generateQuestionForSkillOrThrow(skill));
} catch (e) {
sentryCaptureException(e);
continue;
}

if (questions.length === quizSize) {
return;
}
}
}
});
}

return skills.map((skill) => generateQuestionForSkill(skill));
return questions;
},
retry: false,
// Preserves referential integrity of returned data, this is important so
Expand Down
66 changes: 35 additions & 31 deletions projects/app/src/app/learn/radicals.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { QuizDeck } from "@/components/QuizDeck";
import { RectButton } from "@/components/RectButton";
import { useReplicache } from "@/components/ReplicacheContext";
import { generateQuestionForSkill } from "@/data/generator";
import { IndexName, indexScan, marshalSkillStateKey } from "@/data/marshal";
import { RadicalSkill, Skill, SkillType } from "@/data/model";
import { sentryCaptureException } from "@/components/util";
import { generateQuestionForSkillOrThrow } from "@/data/generator";
import { marshalSkillStateKey } from "@/data/marshal";
import { Question, RadicalSkill, SkillType } from "@/data/model";
import { questionsForReview } from "@/data/query";
import { radicals } from "@/dictionary/radicals";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router";
import shuffle from "lodash/shuffle";
import take from "lodash/take";
import { Text, View } from "react-native";

export default function RadicalsPage() {
Expand All @@ -19,33 +19,23 @@ export default function RadicalsPage() {
// later
queryKey: [RadicalsPage.name, `skills`],
queryFn: async () => {
const skills: Skill[] = [];

const radicalSkillTypes = new Set([
// SkillType.EnglishToRadical,
SkillType.RadicalToEnglish,
]);

// Start with practicing skills that are due
skills.push(
...(await r.query(async (tx) => {
const now = new Date();
return take(
shuffle(
// TODO: paginate or have a radical index
(await indexScan(tx, IndexName.SkillStateByDue, 50))
.filter(([{ type }]) => radicalSkillTypes.has(type))
.filter(([, { due }]) => due <= now)
.map(([skill]) => skill),
),
10,
);
})),
const limit = 10;
const questions: Question[] = await r.query((tx) =>
questionsForReview(tx, {
limit,
sampleSize: 50,
skillTypes: [
// SkillType.EnglishToRadical,
SkillType.RadicalToEnglish,
SkillType.RadicalToPinyin,
],
}),
);

// Fill the rest with new skills
// Create skills to pad out the rest of the quiz
{
if (questions.length < limit) {
// TODO: could be generated once and cached somewhere.
const allRadicalSkills: RadicalSkill[] = [];
for (const radical of radicals) {
for (const hanzi of radical.hanzi) {
Expand All @@ -56,24 +46,38 @@ export default function RadicalsPage() {
name,
});
}
for (const pinyin of radical.pinyin) {
allRadicalSkills.push({
type: SkillType.RadicalToPinyin,
hanzi,
pinyin,
});
}
}
}

await r.query(async (tx) => {
for (const skill of allRadicalSkills) {
if (!(await tx.has(marshalSkillStateKey(skill)))) {
skills.push(skill);
if (skills.length >= 10) {
try {
questions.push(generateQuestionForSkillOrThrow(skill));
} catch (e) {
sentryCaptureException(e);
continue;
}

if (questions.length === limit) {
return;
}
}
}
});
}

return skills.map((skill) => generateQuestionForSkill(skill));
return questions;
},
retry: false,
throwOnError: true,
// Preserves referential integrity of returned data, this is important so
// that `answer` objects are comparable to groups.
structuralSharing: false,
Expand Down
25 changes: 9 additions & 16 deletions projects/app/src/app/learn/reviews.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
import { QuizDeck } from "@/components/QuizDeck";
import { RectButton } from "@/components/RectButton";
import { useQueryOnce } from "@/components/ReplicacheContext";
import { generateQuestionForSkill } from "@/data/generator";
import { IndexName, indexScan } from "@/data/marshal";
import { questionsForReview } from "@/data/query";
import { formatDuration } from "date-fns/formatDuration";
import { interval } from "date-fns/interval";
import { intervalToDuration } from "date-fns/intervalToDuration";
import { router } from "expo-router";
import shuffle from "lodash/shuffle";
import take from "lodash/take";
import { Text, View } from "react-native";

export default function ReviewsPage() {
const questions = useQueryOnce(async (tx) => {
const now = new Date();

// Look ahead at the next 50 skills, shuffle them and take 10. This way
// you don't end up with the same set over and over again (which happens a
// lot in development).
return take(
shuffle(
(await indexScan(tx, IndexName.SkillStateByDue, 50)).filter(
([, { due }]) => due <= now,
),
),
10,
).map(([skill]) => generateQuestionForSkill(skill));
return await questionsForReview(tx, {
limit: 10,
dueBeforeNow: true,
// Look ahead at the next 50 skills, shuffle them and take 10. This way
// you don't end up with the same set over and over again (which happens a
// lot in development).
sampleSize: 50,
});
});

const nextSkillState = useQueryOnce(
Expand Down
Loading

0 comments on commit ac5f46a

Please sign in to comment.