Skip to content

Commit

Permalink
Merge pull request #101 from RickCarlino/prod
Browse files Browse the repository at this point in the history
Bug Fixes and Quality of Life Improvements.
  • Loading branch information
RickCarlino authored Nov 2, 2024
2 parents 3027cf5 + 5e356c0 commit a6d0ed3
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 177 deletions.
160 changes: 58 additions & 102 deletions koala/fetch-lesson.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prismaClient } from "@/koala/prisma-client";
import { map, shuffle } from "radash";
import { Quiz, Card } from "@prisma/client";
import { getUserSettings } from "./auth-helpers";
import { errorReport } from "./error-report";
import { maybeGetCardImageUrl } from "./image";
Expand All @@ -14,120 +15,75 @@ type GetLessonInputParams = {
take: number;
};

type CardLike = { repetitions?: number };

type MaybeFilterNewCards = <T extends CardLike>(
cards: T[],
async function getCards(
userId: string,
) => Promise<T[]>;
now: number,
take: number,
isReview: boolean,
) {
if (take < 1) return [];

const maybeFilterNewCards: MaybeFilterNewCards = async (cards, userId) => {
const limit = (await getUserSettings(userId)).cardsPerDayMax;
const ONE_DAY = 1000 * 60 * 60 * 24;
let newCards = await prismaClient.quiz.count({
where: {
Card: {
userId: userId,
},
firstReview: {
gte: Date.now() - ONE_DAY,
},
},
});
return cards.filter((c) => {
const isNew = (c.repetitions || 0) === 0;
if (isNew) {
newCards++;
return newCards <= limit;
} else {
return true;
}
});
};
const base = {
Card: { userId, flagged: { not: true } },
};

const whereClause = isReview
? { ...base, nextReview: { lt: now }, repetitions: { gt: 0 } }
: { ...base, repetitions: 0 };

async function getReviewCards(userId: string, now: number) {
return await prismaClient.quiz.findMany({
where: {
Card: {
userId: userId,
flagged: { not: true },
},
nextReview: {
lt: now,
},
repetitions: {
gt: 0,
},
},
where: whereClause,
distinct: ["cardId"],
orderBy: [{ quizType: "asc" }],
take: 15,
include: {
Card: true, // Include related Card data in the result
},
orderBy: [{ quizType: "asc" }, { nextReview: "asc" }],
include: { Card: true },
take,
});
}

async function getNewCards(userId: string) {
const cards = await prismaClient.quiz.findMany({
where: {
Card: {
userId: userId,
flagged: { not: true },
},
repetitions: 0,
},
distinct: ["cardId"],
orderBy: [{ quizType: "asc" }],
take: 5,
include: {
Card: true, // Include related Card data in the result
},
});
return maybeFilterNewCards(cards, userId);
// A prisma quiz with Card included
type LocalQuiz = Quiz & { Card: Card };

async function prepareQuizData(quiz: LocalQuiz, playbackPercentage: number) {
return {
quizId: quiz.id,
cardId: quiz.cardId,
definition: quiz.Card.definition,
term: quiz.Card.term,
repetitions: quiz.repetitions,
lapses: quiz.lapses,
lessonType: quiz.quizType as LessonType,
definitionAudio: await generateLessonAudio({
card: quiz.Card,
lessonType: "speaking",
speed: 115,
}),
termAudio: await generateLessonAudio({
card: quiz.Card,
lessonType: "listening",
speed: playbackPercentage,
}),
langCode: quiz.Card.langCode,
lastReview: quiz.lastReview || 0,
imageURL: await maybeGetCardImageUrl(quiz.Card.imageBlobId),
};
}

const playbackSpeed = async (userID: string) => {
return await getUserSettings(userID).then((s) => s.playbackSpeed || 1.05);
};
export default async function getLessons(p: GetLessonInputParams) {
if (p.take > 15) {
return errorReport("Too many cards requested.");
}
if (p.take > 15) return errorReport("Too many cards requested.");

const { userId, now, take } = p;

const playbackPercentage = Math.round((await playbackSpeed(userId)) * 100);

// pruneOldAndHardQuizzes(p.userId);
const playbackSpeed = await getUserSettings(p.userId).then(
(s) => s.playbackSpeed || 1.05,
);
const playbackPercentage = Math.round(playbackSpeed * 100);
const reviewCards = await getReviewCards(p.userId, p.now);
const newCards = await getNewCards(p.userId);
// 25% of cards are new cards.
const combined = shuffle([...reviewCards, ...newCards]).slice(0, p.take);
return await map(combined, async (q) => {
const quiz = {
...q,
quizType: q.repetitions ? q.quizType : "dictation",
};
const oldCards = await getCards(p.userId, p.now, take, true);
const newCards = await getCards(userId, now, take - oldCards.length, false);
const combined = [...shuffle(oldCards), ...shuffle(newCards)].slice(0, take);

return {
quizId: quiz.id,
cardId: quiz.cardId,
definition: quiz.Card.definition,
term: quiz.Card.term,
repetitions: quiz.repetitions,
lapses: quiz.lapses,
lessonType: quiz.quizType as LessonType,
definitionAudio: await generateLessonAudio({
card: quiz.Card,
lessonType: "speaking",
speed: 110,
}),
termAudio: await generateLessonAudio({
card: quiz.Card,
lessonType: "listening",
speed: playbackPercentage,
}),
langCode: quiz.Card.langCode,
lastReview: quiz.lastReview || 0,
imageURL: await maybeGetCardImageUrl(quiz.Card.imageBlobId),
};
return await map(combined, (q) => {
const quiz = { ...q, quizType: q.repetitions ? q.quizType : "dictation" };
return prepareQuizData(quiz, playbackPercentage);
});
}
1 change: 1 addition & 0 deletions koala/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const SINGLE_WORD = [
`Create a DALL-e prompt to generate an image of the foreign language word above.`,
`Make it as realistic and accurate to the words meaning as possible.`,
`The illustration must convey the word's meaning to the student.`,
`humans must be anthropomorphized.`,
`Do not add text. It will give away the answer!`,
].join("\n");

Expand Down
61 changes: 34 additions & 27 deletions koala/review/listening-quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ import { FailButton } from "./fail-button";
type Phase = "play" | "record" | "done";

const DEFAULT_STATE = {
successfulAttempts: 0,
isRecording: false,
phase: "play" as Phase,
isProcessing: false,
transcriptionFailed: false,
};

function strip(input: string): string {
return input.replace(/[^\p{L}]+/gu, "");
}

export const ListeningQuiz: QuizComp = ({
quiz: card,
onGraded,
Expand All @@ -32,7 +35,6 @@ export const ListeningQuiz: QuizComp = ({

useEffect(() => {
setState({
successfulAttempts: 0,
isRecording: false,
phase: "play",
isProcessing: false,
Expand Down Expand Up @@ -72,28 +74,31 @@ export const ListeningQuiz: QuizComp = ({
targetText: card.term,
});

console.log([transcription, card.term].join(" VS "));

if (transcription.trim() === card.term.trim()) {
setState((prevState) => ({
...prevState,
successfulAttempts: prevState.successfulAttempts + 1,
phase: "done",
}));
const OK = strip(transcription) === strip(card.term);
console.log([strip(transcription), strip(card.term)].join(" VS "));
console.log(OK ? "OK" : "FAIL");
if (OK) {
await playAudio(card.termAudio);
await playAudio(card.definitionAudio);
setState((prevState) => {
return {
...prevState,
phase: "done",
};
});
} else {
// Transcription did not match
await playAudio(card.termAudio);
setState((prevState) => ({
...prevState,
transcriptionFailed: true,
phase: "record",
}));
}
} catch (error) {
console.error(error);
setState((prevState) => ({
...prevState,
transcriptionFailed: true,
phase: "record",
}));
} finally {
setState((prevState) => ({ ...prevState, isProcessing: false }));
Expand Down Expand Up @@ -123,7 +128,7 @@ export const ListeningQuiz: QuizComp = ({
return (
<PlayPhase
isDictation={isDictation}
showTerm={state.successfulAttempts === 0 || isDictation}
showTerm={isDictation}
term={card.term}
definition={card.definition}
onPlayClick={handlePlayClick}
Expand Down Expand Up @@ -178,13 +183,11 @@ const PlayPhase = ({

return (
<Stack>
{isDictation && (
<Center>
<Text size="xl">NEW CARD</Text>
</Center>
)}
{showTerm && <Text>Term: {term}</Text>}
{isDictation && <Text>Meaning: {definition}</Text>}
<Center>
<Text size="xl">Listen and Repeat</Text>
</Center>
{showTerm && <Text>{term}</Text>}
{isDictation && <Text>Definition: {definition}</Text>}
<Button onClick={onPlayClick}>
Listen to Audio and Proceed to Exercise
</Button>
Expand Down Expand Up @@ -216,18 +219,22 @@ const RecordPhase = ({
onFailClick,
}: RecordPhaseProps) => {
useHotkeys([["space", () => !isProcessing && onRecordClick()]]);

const recordLabel = isRecording ? "Stop Recording" : "Begin Recording";
const header = isDictation ? `NEW: ${term}` : "Record What You Hear";
const recordingText = isRecording ? "Stop Recording" : "Begin Recording";
const buttonLabel = isProcessing ? "Processing..." : recordingText;
const header = isDictation
? `Repeat the Phrase: ${term}`
: "Repeat the Phrase Without Reading";

return (
<Stack>
<Text size="xl">{header}</Text>
<Center>
<Text size="xl">{header}</Text>
</Center>
{transcriptionFailed && (
<Text>The transcription did not match. Please try again.</Text>
<Text>Pronunciation failure. Please try again.</Text>
)}
<Button onClick={onRecordClick} disabled={isProcessing}>
{recordLabel}
{buttonLabel}
</Button>
<Button onClick={onPlayAudioAgain}>Play Audio Again</Button>
<FailButton onClick={onFailClick} />
Expand All @@ -249,7 +256,7 @@ const DonePhase = ({
}: DonePhaseProps) => (
<Stack>
<Center>
<Text size="xl">Select Difficulty</Text>
<Text size="xl">How Well Did You Understand the Phrase?</Text>
</Center>
<Text>Term: {term}</Text>
<Text>Definition: {definition}</Text>
Expand Down
14 changes: 8 additions & 6 deletions koala/review/review-over.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ type ReviewOverProps = {
onSave: () => Promise<void>;
onUpdateDifficulty: (quizId: number, grade: Grade) => void;
};

const PerfectScore = ({ onSave }: { onSave: () => void }) => {
type PerfectScoreProps = { onSave: () => void; isSaving: boolean };
const PerfectScore = ({ onSave, isSaving }: PerfectScoreProps) => {
useHotkeys([["space", onSave]]);
return (
<Center style={{ width: "100%" }}>
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Title order={2}>Perfect Score!</Title>
<Title order={2}>Lesson Complete</Title>
<Stack>
<Button onClick={onSave}>Save Progress</Button>
<Button loading={isSaving} onClick={onSave}>
Save Progress
</Button>
</Stack>
</Card>
</Center>
Expand Down Expand Up @@ -62,7 +64,7 @@ export const ReviewOver = ({
return (
<Center style={{ width: "100%", height: "100vh" }}>
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Title order={2}>No quizzes to review</Title>
<Title order={2}>{isSaving ? "" : "No quizzes to review"}</Title>
</Card>
</Center>
);
Expand All @@ -72,7 +74,7 @@ export const ReviewOver = ({
const numTotal = state.length;

if (numWrong === 0 && numTotal > 0) {
return <PerfectScore onSave={handleSave} />;
return <PerfectScore onSave={handleSave} isSaving={isSaving} />;
}

return (
Expand Down
Loading

0 comments on commit a6d0ed3

Please sign in to comment.