Skip to content

Commit

Permalink
=== v4.0.0 Initial Draft
Browse files Browse the repository at this point in the history
* v4 First Draft
  • Loading branch information
RickCarlino authored Oct 27, 2024
1 parent 00c6b0d commit 21eb839
Show file tree
Hide file tree
Showing 50 changed files with 1,077 additions and 1,741 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ yarn-error.log*
notes/
data/
db/
.aider*
17 changes: 10 additions & 7 deletions koala/fetch-lesson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { map, shuffle } from "radash";
import { getUserSettings } from "./auth-helpers";
import { errorReport } from "./error-report";
import { maybeGetCardImageUrl } from "./image";
import { calculateSchedulingData } from "./routes/import-cards";
import { calculateSchedulingData } from "./trpc-routes/import-cards";
import { LessonType } from "./shared-types";
import { generateLessonAudio } from "./speech";

Expand Down Expand Up @@ -193,11 +193,6 @@ export default async function getLessons(p: GetLessonInputParams) {
quizType: q.repetitions ? q.quizType : "dictation",
};

const audio = await generateLessonAudio({
card: quiz.Card,
lessonType: quiz.quizType as LessonType,
speed: 100,
});
return {
quizId: quiz.id,
cardId: quiz.cardId,
Expand All @@ -206,7 +201,15 @@ export default async function getLessons(p: GetLessonInputParams) {
repetitions: quiz.repetitions,
lapses: quiz.lapses,
lessonType: quiz.quizType as LessonType,
audio,
definitionAudio: await generateLessonAudio({
card: quiz.Card,
lessonType: "listening",
speed: 100,
}),
termAudio: await generateLessonAudio({
card: quiz.Card,
lessonType: "speaking",
}),
langCode: quiz.Card.langCode,
lastReview: quiz.lastReview || 0,
imageURL: await maybeGetCardImageUrl(quiz.Card.imageBlobId),
Expand Down
3 changes: 1 addition & 2 deletions koala/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ export async function maybeAddImages(userId: string, take: number) {
});

if (!cards.length) {
// console.log(`=== No cards left to illustrate ===`);
return;
}
// console.log(`=== Adding images to ${cards.length} cards ===`);

const x = await Promise.all(cards.map(maybeAddImageToCard));
console.log(cards.map((x) => x.term).join("\n"));
console.log(x.join("\n"));
Expand Down
163 changes: 0 additions & 163 deletions koala/quiz-failure.tsx

This file was deleted.

37 changes: 37 additions & 0 deletions koala/review/grade-buttons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button, Group } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import { Grade } from "femto-fsrs";
import React from "react";

// Define the props for the component
interface DifficultyButtonsProps {
current: Grade | undefined;
onSelectDifficulty: (difficulty: Grade) => void;
}

export const DifficultyButtons: React.FC<DifficultyButtonsProps> = ({
current,
onSelectDifficulty,
}) => {
const grades: (keyof typeof Grade)[] = ["AGAIN", "HARD", "GOOD", "EASY"];
useHotkeys([
["a", () => onSelectDifficulty(Grade.AGAIN)],
["s", () => onSelectDifficulty(Grade.HARD)],
["d", () => onSelectDifficulty(Grade.GOOD)],
["f", () => onSelectDifficulty(Grade.EASY)],
]);

return (
<Group>
{grades.map((grade) => (
<Button
key={grade}
disabled={current === Grade[grade]}
onClick={() => onSelectDifficulty(Grade[grade])}
>
{grade}
</Button>
))}
</Group>
);
};
125 changes: 125 additions & 0 deletions koala/review/listening-quiz.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { playAudio } from "@/koala/play-audio";
import { blobToBase64, convertBlobToWav } from "@/koala/record-button";
import { trpc } from "@/koala/trpc-config";
import { useVoiceRecorder } from "@/koala/use-recorder";
import { Button, Stack, Text } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import { Grade } from "femto-fsrs";
import { useEffect, useState, useCallback } from "react";
import { DifficultyButtons } from "./grade-buttons";
import { QuizComp } from "./types";

const REPETITIONS = 1;

export const ListeningQuiz: QuizComp = ({
quiz: card,
onGraded,
onComplete,
}) => {
// State variables
const [successfulAttempts, setSuccessfulAttempts] = useState(0);
const [isRecording, setIsRecording] = useState(false);
const [phase, setPhase] = useState<"play" | "record" | "done">("play");
const transcribeAudio = trpc.transcribeAudio.useMutation();
const voiceRecorder = useVoiceRecorder(handleRecordingResult);

const transitionToNextPhase = useCallback(
() => setPhase(phase === "play" ? "record" : "done"),
[phase],
);

const handlePlayClick = async () => {
await playAudio(card.definitionAudio);
setPhase("record");
};

const handleRecordClick = () => {
setIsRecording((prev) => !prev);
isRecording ? voiceRecorder.stop() : voiceRecorder.start();
};

async function handleRecordingResult(audioBlob: Blob) {
setIsRecording(false);
try {
const base64Audio = await blobToBase64(await convertBlobToWav(audioBlob));
const { result: transcription } = await transcribeAudio.mutateAsync({
audio: base64Audio,
lang: "ko",
targetText: card.term,
});

if (transcription.trim() === card.term.trim())
setSuccessfulAttempts((prev) => prev + 1);
transitionToNextPhase();
} catch (error) {
setPhase("play"); // Retry
}
}

const handleFailClick = () => {
onGraded(Grade.AGAIN);
onComplete("fail", "You hit the FAIL button");
};

const handleDifficultySelect = (grade: Grade) => {
onGraded(grade);
onComplete("pass", "");
};

useHotkeys([
[
"space",
() => (phase === "play" ? handlePlayClick() : handleRecordClick()),
],
]);

useEffect(() => {
setSuccessfulAttempts(0);
setIsRecording(false);
setPhase("play");
}, [card.term]);

switch (phase) {
case "play":
return (
<Stack>
<Text size="xl">{card.term}</Text>
<Button onClick={handlePlayClick}>Play</Button>
<Text>
Repetitions: {successfulAttempts}/{REPETITIONS}
</Text>
<Button variant="outline" color="red" onClick={handleFailClick}>
Fail
</Button>
</Stack>
);
case "record":
return (
<Stack>
<Text size="xl">{card.term}</Text>
<Button onClick={handleRecordClick}>
{isRecording ? "Stop Recording" : "Record Response"}
</Button>
{isRecording && <Text>Recording...</Text>}
<Text>
Repetitions: {successfulAttempts}/{REPETITIONS}
</Text>
<Button variant="outline" color="red" onClick={handleFailClick}>
Fail
</Button>
</Stack>
);
case "done":
return (
<Stack>
<Text size="xl">Select difficulty:</Text>
<DifficultyButtons
current={undefined}
onSelectDifficulty={handleDifficultySelect}
/>
</Stack>
);
default:
return <div>{`Unknown phase: ${phase}`}</div>;
}
};
Loading

0 comments on commit 21eb839

Please sign in to comment.