feat(03-01): extend Progress type, add LessonPhase, update rate limits

- Progress: added totalCorrect, totalErrors, errorKeyCounts, lastPlayedLevels
- New LessonPhase type: discover | practice | words
- Rate limits: 10 text / 5 image per day (was 1 each)
- Updated screens.ts with new Progress field defaults
- Updated existing tests for new rate limits and Progress shape

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 13:42:32 +02:00
co-authored by Claude Opus 4.6
parent e1fc5eb579
commit d4207bf33d
5 changed files with 25 additions and 7 deletions
+6 -6
View File
@@ -230,9 +230,9 @@ describe("Rate limiting", () => {
expect(canCallImage).toBe(true); expect(canCallImage).toBe(true);
}); });
it("checkRateLimit returns false when at limit", async () => { it("checkRateLimit returns false when at limit (10 text, 5 image)", async () => {
await incrementApiCall(db, "text"); for (let i = 0; i < 10; i++) await incrementApiCall(db, "text");
await incrementApiCall(db, "image"); for (let i = 0; i < 5; i++) await incrementApiCall(db, "image");
const canCallText = await checkRateLimit(db, "text"); const canCallText = await checkRateLimit(db, "text");
const canCallImage = await checkRateLimit(db, "image"); const canCallImage = await checkRateLimit(db, "image");
@@ -241,7 +241,7 @@ describe("Rate limiting", () => {
}); });
it("incrementApiCall increments correct counter", async () => { it("incrementApiCall increments correct counter", async () => {
await incrementApiCall(db, "text"); for (let i = 0; i < 10; i++) await incrementApiCall(db, "text");
const canCallText = await checkRateLimit(db, "text"); const canCallText = await checkRateLimit(db, "text");
const canCallImage = await checkRateLimit(db, "image"); const canCallImage = await checkRateLimit(db, "image");
@@ -251,8 +251,8 @@ describe("Rate limiting", () => {
it("rate limit resets when lastApiCallDate differs from today", async () => { it("rate limit resets when lastApiCallDate differs from today", async () => {
// Increment to hit limit // Increment to hit limit
await incrementApiCall(db, "text"); for (let i = 0; i < 10; i++) await incrementApiCall(db, "text");
await incrementApiCall(db, "image"); for (let i = 0; i < 5; i++) await incrementApiCall(db, "image");
// Manually set lastApiCallDate to yesterday // Manually set lastApiCallDate to yesterday
const { getSettings, saveSettings } = await import("../storage/db"); const { getSettings, saveSettings } = await import("../storage/db");
+5 -1
View File
@@ -5,6 +5,8 @@ import type { GeminiConfig } from "./config";
const GEMINI_BASE_URL = const GEMINI_BASE_URL =
"https://generativelanguage.googleapis.com/v1beta/models"; "https://generativelanguage.googleapis.com/v1beta/models";
const RETRY_DELAYS = [1000, 2000, 4000]; const RETRY_DELAYS = [1000, 2000, 4000];
const TEXT_RATE_LIMIT = 10;
const IMAGE_RATE_LIMIT = 5;
function sleep(ms: number): Promise<void> { function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
@@ -159,7 +161,9 @@ export async function checkRateLimit(
await saveSettings(db, settings); await saveSettings(db, settings);
} }
return settings.apiCallsToday[type] < 1; return type === "text"
? settings.apiCallsToday.text < TEXT_RATE_LIMIT
: settings.apiCallsToday.image < IMAGE_RATE_LIMIT;
} }
export async function incrementApiCall( export async function incrementApiCall(
+4
View File
@@ -33,6 +33,10 @@ describe("IndexedDB wrapper", () => {
sessionDates: [], sessionDates: [],
selectedCharacter: "fee", selectedCharacter: "fee",
selectedLayout: "de", selectedLayout: "de",
totalCorrect: 0,
totalErrors: 0,
errorKeyCounts: {},
lastPlayedLevels: [],
}; };
await saveProgress(db, progress); await saveProgress(db, progress);
const result = await getProgress(db); const result = await getProgress(db);
+6
View File
@@ -14,8 +14,14 @@ export interface Progress {
sessionDates: string[]; // ISO date strings (days only) sessionDates: string[]; // ISO date strings (days only)
selectedCharacter: CompanionType; selectedCharacter: CompanionType;
selectedLayout: KeyboardLayout; selectedLayout: KeyboardLayout;
totalCorrect: number;
totalErrors: number;
errorKeyCounts: Record<string, number>;
lastPlayedLevels: number[]; // last 3 played levels for review mechanic
} }
export type LessonPhase = "discover" | "practice" | "words";
// Companion assets store // Companion assets store
export interface CompanionAssets { export interface CompanionAssets {
characterType: CompanionType; characterType: CompanionType;
+4
View File
@@ -70,6 +70,10 @@ export function initWelcomeScreen(
sessionDates: [], sessionDates: [],
selectedCharacter: selectedCompanion, selectedCharacter: selectedCompanion,
selectedLayout: selectedLayout, selectedLayout: selectedLayout,
totalCorrect: 0,
totalErrors: 0,
errorKeyCounts: {},
lastPlayedLevels: [],
}; };
await saveProgress(db, progress); await saveProgress(db, progress);
navigateTo("forest"); navigateTo("forest");