refactor: remove checkRateLimit and incrementApiCall completely

This commit is contained in:
2026-03-29 23:38:54 +02:00
parent 2365a95f98
commit 5f47feaf32
12 changed files with 75 additions and 163 deletions
+2
View File
@@ -27,6 +27,8 @@ Technology stack not yet documented. Will populate after codebase mapping or fir
## Conventions ## Conventions
- Immer deutsche Umlaute (ä, ö, ü, Ä, Ö, Ü, ß) in UI-Texten verwenden, niemals ae/oe/ue Transliterierung. - Immer deutsche Umlaute (ä, ö, ü, Ä, Ö, Ü, ß) in UI-Texten verwenden, niemals ae/oe/ue Transliterierung.
- Kein No-Op Code: Keine leeren Funktionen, keine Stubs, keine auskommentierten Blöcke. Wenn etwas entfernt wird, komplett entfernen — inklusive Aufrufe, Imports, Typen und Tests.
- Never predict the future: Kein Code "für später", keine Abstraktionen für hypothetische Anforderungen, keine Platzhalter-Interfaces. Nur implementieren was jetzt gebraucht wird.
<!-- GSD:conventions-end --> <!-- GSD:conventions-end -->
<!-- GSD:architecture-start source:ARCHITECTURE.md --> <!-- GSD:architecture-start source:ARCHITECTURE.md -->
+1 -16
View File
@@ -1,11 +1,6 @@
import "fake-indexeddb/auto";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { GeminiConfig } from "./config"; import type { GeminiConfig } from "./config";
import { import { generateImage, generateText } from "./gemini";
checkRateLimit,
generateImage,
generateText,
} from "./gemini";
const TEST_CONFIG: GeminiConfig = { const TEST_CONFIG: GeminiConfig = {
geminiApiKey: "test-api-key", geminiApiKey: "test-api-key",
@@ -13,8 +8,6 @@ const TEST_CONFIG: GeminiConfig = {
imageModel: "gemini-3.1-flash-preview-image", imageModel: "gemini-3.1-flash-preview-image",
}; };
let testCounter = 0;
function makeGeminiTextResponse(text: string) { function makeGeminiTextResponse(text: string) {
return { return {
candidates: [ candidates: [
@@ -207,11 +200,3 @@ describe("generateImage", () => {
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
describe("Rate limiting (disabled)", () => {
it("checkRateLimit always returns true (no limits)", async () => {
const db = {} as IDBDatabase;
expect(await checkRateLimit(db, "text")).toBe(true);
expect(await checkRateLimit(db, "image")).toBe(true);
});
});
-15
View File
@@ -112,18 +112,3 @@ export async function generateImage(
return null; return null;
} }
} }
export async function checkRateLimit(
_db: IDBDatabase,
_type: "text" | "image",
): Promise<boolean> {
return true;
}
export async function incrementApiCall(
_db: IDBDatabase,
_type: "text" | "image",
): Promise<void> {
// No-op: rate limits removed
}
+18 -8
View File
@@ -8,6 +8,7 @@ import { getForestElements, getProgress, openDB, saveProgress } from "./storage/
import type { ScreenName } from "./types"; import type { ScreenName } from "./types";
import { initParentScreen, showParentCodePrompt } from "./ui/parent"; import { initParentScreen, showParentCodePrompt } from "./ui/parent";
import { initLessonScreen, initWelcomeScreen } from "./ui/screens"; import { initLessonScreen, initWelcomeScreen } from "./ui/screens";
import { levels } from "./game/levels";
let db: IDBDatabase; let db: IDBDatabase;
let lessonCleanup: (() => void) | null = null; let lessonCleanup: (() => void) | null = null;
@@ -26,8 +27,6 @@ export function getDB(): IDBDatabase {
return db; return db;
} }
const MAX_LEVEL = 6;
/** /**
* Ensure progress object has all fields (migration for older saved data). * Ensure progress object has all fields (migration for older saved data).
*/ */
@@ -39,6 +38,16 @@ function migrateProgress(
if (progress.totalCorrect === undefined) progress.totalCorrect = 0; if (progress.totalCorrect === undefined) progress.totalCorrect = 0;
if (progress.totalErrors === undefined) progress.totalErrors = 0; if (progress.totalErrors === undefined) progress.totalErrors = 0;
if (progress.currentForestPage === undefined) progress.currentForestPage = 0; if (progress.currentForestPage === undefined) progress.currentForestPage = 0;
// Migration: advance currentLevel if it was previously capped at a completed level.
// This fixes saved data from before levels 7-16 existed, where MAX_LEVEL was 6.
const maxLevel = levels[levels.length - 1]?.level ?? 16;
if (
progress.completedLevels.includes(progress.currentLevel) &&
progress.currentLevel < maxLevel
) {
progress.currentLevel = progress.currentLevel + 1;
}
} }
async function handleLessonComplete( async function handleLessonComplete(
@@ -74,10 +83,11 @@ async function handleLessonComplete(
} }
// Advance to next level if this was the current level (LEVL-02, LEVL-03) // Advance to next level if this was the current level (LEVL-02, LEVL-03)
// Cap at MAX_LEVEL so we don't go beyond defined levels // Cap at the total number of defined levels so we don't go beyond
const maxLevel = levels[levels.length - 1]?.level ?? 16;
if ( if (
completedLevel === progress.currentLevel && completedLevel === progress.currentLevel &&
completedLevel < MAX_LEVEL completedLevel < maxLevel
) { ) {
progress.currentLevel = completedLevel + 1; progress.currentLevel = completedLevel + 1;
} }
@@ -114,9 +124,7 @@ async function handleLessonComplete(
progress.selectedCharacter, progress.selectedCharacter,
() => { () => {
// "Weiter üben" -- start next lesson // "Weiter üben" -- start next lesson
startLessonFromForest( startLessonFromForest(progress.currentLevel);
Math.min(progress.currentLevel, MAX_LEVEL),
);
}, },
async () => { async () => {
// "Zurück zum Wald" -- navigate to page containing the new element // "Zurück zum Wald" -- navigate to page containing the new element
@@ -214,6 +222,8 @@ export async function initApp(): Promise<void> {
const progress = await getProgress(db); const progress = await getProgress(db);
if (progress && progress.selectedCharacter) { if (progress && progress.selectedCharacter) {
migrateProgress(progress); migrateProgress(progress);
// Persist any migrations (e.g. currentLevel advancement) before rendering
await saveProgress(db, progress);
// Returning user -- init forest and go there // Returning user -- init forest and go there
await initForestScreen(db, startLessonFromForest); await initForestScreen(db, startLessonFromForest);
@@ -235,7 +245,7 @@ export async function initApp(): Promise<void> {
greetingEl.style.display = "flex"; greetingEl.style.display = "flex";
// Fetch greeting async (shows immediately with avatar, text fills in) // Fetch greeting async (shows immediately with avatar, text fills in)
getGreeting(progress.selectedCharacter, db).then((text) => { getGreeting(progress.selectedCharacter).then((text) => {
textEl.innerHTML = renderInlineMarkdown(text); textEl.innerHTML = renderInlineMarkdown(text);
speakText(text); speakText(text);
}); });
+9 -57
View File
@@ -1,6 +1,4 @@
import "fake-indexeddb/auto";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { openDB } from "../storage/db";
import type { GeminiConfig } from "../api/config"; import type { GeminiConfig } from "../api/config";
// Mock modules before importing the module under test // Mock modules before importing the module under test
@@ -11,16 +9,10 @@ vi.mock("../api/config", () => ({
vi.mock("../api/gemini", () => ({ vi.mock("../api/gemini", () => ({
generateText: vi.fn(), generateText: vi.fn(),
checkRateLimit: vi.fn(),
incrementApiCall: vi.fn(),
})); }));
import { loadConfig } from "../api/config"; import { loadConfig } from "../api/config";
import { import { generateText } from "../api/gemini";
checkRateLimit,
generateText,
incrementApiCall,
} from "../api/gemini";
import { getGreeting, getLetterIntro, getForestComment } from "./companion"; import { getGreeting, getLetterIntro, getForestComment } from "./companion";
const TEST_CONFIG: GeminiConfig = { const TEST_CONFIG: GeminiConfig = {
@@ -29,13 +21,8 @@ const TEST_CONFIG: GeminiConfig = {
imageModel: "gemini-3.1-flash-preview-image", imageModel: "gemini-3.1-flash-preview-image",
}; };
let testCounter = 0; beforeEach(() => {
let db: IDBDatabase;
beforeEach(async () => {
vi.clearAllMocks(); vi.clearAllMocks();
testCounter++;
db = await openDB(`companion-test-${testCounter}`);
}); });
afterEach(() => { afterEach(() => {
@@ -43,24 +30,22 @@ afterEach(() => {
}); });
describe("getGreeting", () => { describe("getGreeting", () => {
it("returns AI text when API succeeds and rate limit not exceeded", async () => { it("returns AI text when API succeeds", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue( vi.mocked(generateText).mockResolvedValue(
"Guten Morgen, kleiner Waldfreund!", "Guten Morgen, kleiner Waldfreund!",
); );
const result = await getGreeting("fee", db); const result = await getGreeting("fee");
expect(result).toBe("Guten Morgen, kleiner Waldfreund!"); expect(result).toBe("Guten Morgen, kleiner Waldfreund!");
expect(generateText).toHaveBeenCalledTimes(1); expect(generateText).toHaveBeenCalledTimes(1);
expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
}); });
it("returns fallback text when loadConfig returns null (no API key)", async () => { it("returns fallback text when loadConfig returns null (no API key)", async () => {
vi.mocked(loadConfig).mockResolvedValue(null); vi.mocked(loadConfig).mockResolvedValue(null);
const result = await getGreeting("fee", db); const result = await getGreeting("fee");
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(typeof result).toBe("string"); expect(typeof result).toBe("string");
@@ -69,32 +54,19 @@ describe("getGreeting", () => {
it("returns fallback text when generateText returns null (API error)", async () => { it("returns fallback text when generateText returns null (API error)", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue(null); vi.mocked(generateText).mockResolvedValue(null);
const result = await getGreeting("fee", db); const result = await getGreeting("fee");
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(typeof result).toBe("string"); expect(typeof result).toBe("string");
expect(incrementApiCall).not.toHaveBeenCalled();
});
it("returns fallback text when rate limit exceeded", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(false);
const result = await getGreeting("fee", db);
expect(result).toBeTruthy();
expect(generateText).not.toHaveBeenCalled();
}); });
it("passes correct system prompt with companion personality and time of day", async () => { it("passes correct system prompt with companion personality and time of day", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue("Hallo!"); vi.mocked(generateText).mockResolvedValue("Hallo!");
await getGreeting("fee", db); await getGreeting("fee");
const systemPrompt = vi.mocked(generateText).mock.calls[0]![1]; const systemPrompt = vi.mocked(generateText).mock.calls[0]![1];
expect(systemPrompt).toContain("Lila"); expect(systemPrompt).toContain("Lila");
@@ -105,21 +77,11 @@ describe("getGreeting", () => {
expect(systemPrompt).toContain("Max. 2 kurze Sätze"); expect(systemPrompt).toContain("Max. 2 kurze Sätze");
}); });
it("calls incrementApiCall after successful API call", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue("Hallo Waldkind!");
await getGreeting("einhorn", db);
expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
});
}); });
describe("getLetterIntro", () => { describe("getLetterIntro", () => {
it("returns AI text when API succeeds", async () => { it("returns AI text when API succeeds", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue( vi.mocked(generateText).mockResolvedValue(
"Das F ist wie ein Farn im Wald!", "Das F ist wie ein Farn im Wald!",
); );
@@ -128,11 +90,9 @@ describe("getLetterIntro", () => {
"fee", "fee",
"f", "f",
"linker Zeigefinger", "linker Zeigefinger",
db,
); );
expect(result).toBe("Das F ist wie ein Farn im Wald!"); expect(result).toBe("Das F ist wie ein Farn im Wald!");
expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
}); });
it("returns fallback LetterIntro text when API unavailable", async () => { it("returns fallback LetterIntro text when API unavailable", async () => {
@@ -142,7 +102,6 @@ describe("getLetterIntro", () => {
"fee", "fee",
"f", "f",
"linker Zeigefinger", "linker Zeigefinger",
db,
); );
expect(result).toBeTruthy(); expect(result).toBeTruthy();
@@ -152,10 +111,9 @@ describe("getLetterIntro", () => {
it("system prompt includes letter, finger description, and companion personality", async () => { it("system prompt includes letter, finger description, and companion personality", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue("Das D wohnt hier!"); vi.mocked(generateText).mockResolvedValue("Das D wohnt hier!");
await getLetterIntro("fuchs", "d", "linker Mittelfinger", db); await getLetterIntro("fuchs", "d", "linker Mittelfinger");
const systemPrompt = vi.mocked(generateText).mock.calls[0]![1]; const systemPrompt = vi.mocked(generateText).mock.calls[0]![1];
expect(systemPrompt).toContain("Finn"); expect(systemPrompt).toContain("Finn");
@@ -171,7 +129,6 @@ describe("getLetterIntro", () => {
"fee", "fee",
"@", "@",
"linker kleiner Finger", "linker kleiner Finger",
db,
); );
expect(result).toBe("Drücke die leuchtende Taste!"); expect(result).toBe("Drücke die leuchtende Taste!");
@@ -181,7 +138,6 @@ describe("getLetterIntro", () => {
describe("getForestComment", () => { describe("getForestComment", () => {
it("returns AI text when API succeeds", async () => { it("returns AI text when API succeeds", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue( vi.mocked(generateText).mockResolvedValue(
"Oh, ein Schmetterling ist erschienen!", "Oh, ein Schmetterling ist erschienen!",
); );
@@ -189,11 +145,9 @@ describe("getForestComment", () => {
const result = await getForestComment( const result = await getForestComment(
"eule", "eule",
"ein blauer Schmetterling", "ein blauer Schmetterling",
db,
); );
expect(result).toBe("Oh, ein Schmetterling ist erschienen!"); expect(result).toBe("Oh, ein Schmetterling ist erschienen!");
expect(incrementApiCall).toHaveBeenCalledWith(db, "text");
}); });
it("returns fallback text when API unavailable", async () => { it("returns fallback text when API unavailable", async () => {
@@ -202,7 +156,6 @@ describe("getForestComment", () => {
const result = await getForestComment( const result = await getForestComment(
"eule", "eule",
"ein blauer Schmetterling", "ein blauer Schmetterling",
db,
); );
expect(result).toBeTruthy(); expect(result).toBeTruthy();
@@ -212,10 +165,9 @@ describe("getForestComment", () => {
it("system prompt includes element description and companion personality", async () => { it("system prompt includes element description and companion personality", async () => {
vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG); vi.mocked(loadConfig).mockResolvedValue(TEST_CONFIG);
vi.mocked(checkRateLimit).mockResolvedValue(true);
vi.mocked(generateText).mockResolvedValue("Wie wunderschön!"); vi.mocked(generateText).mockResolvedValue("Wie wunderschön!");
await getForestComment("einhorn", "ein leuchtender Pilz", db); await getForestComment("einhorn", "ein leuchtender Pilz");
const systemPrompt = vi.mocked(generateText).mock.calls[0]![1]; const systemPrompt = vi.mocked(generateText).mock.calls[0]![1];
expect(systemPrompt).toContain("Stella"); expect(systemPrompt).toContain("Stella");
+6 -37
View File
@@ -2,14 +2,14 @@
* Companion text module: AI-generated or fallback text for greetings, * Companion text module: AI-generated or fallback text for greetings,
* letter introductions, and forest comments. * letter introductions, and forest comments.
* *
* Strategy: Try Gemini API first (with rate-limit check), fall back * Strategy: Try Gemini API first, fall back to curated static texts
* to curated static texts when API is unavailable. * when API is unavailable.
*/ */
import { companions } from "./characters"; import { companions } from "./characters";
import type { CompanionType } from "../types"; import type { CompanionType } from "../types";
import { loadConfig } from "../api/config"; import { loadConfig } from "../api/config";
import { generateText, checkRateLimit, incrementApiCall } from "../api/gemini"; import { generateText } from "../api/gemini";
import { import {
type TimeOfDay, type TimeOfDay,
getRandomFallbackGreeting, getRandomFallbackGreeting,
@@ -30,15 +30,11 @@ function getCompanion(type: CompanionType) {
export async function getGreeting( export async function getGreeting(
characterType: CompanionType, characterType: CompanionType,
db: IDBDatabase,
): Promise<string> { ): Promise<string> {
const timeOfDay = getTimeOfDay(); const timeOfDay = getTimeOfDay();
const config = await loadConfig(); const config = await loadConfig();
if (!config) return getRandomFallbackGreeting(timeOfDay); if (!config) return getRandomFallbackGreeting(timeOfDay);
const canCall = await checkRateLimit(db, "text");
if (!canCall) return getRandomFallbackGreeting(timeOfDay);
const companion = getCompanion(characterType); const companion = getCompanion(characterType);
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald. const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
Du sprichst mit einem 7-jährigen Kind, das Tippen lernt. Du sprichst mit einem 7-jährigen Kind, das Tippen lernt.
@@ -56,17 +52,13 @@ Regeln:
systemPrompt, systemPrompt,
config, config,
); );
if (!result) return getRandomFallbackGreeting(timeOfDay); return result ?? getRandomFallbackGreeting(timeOfDay);
await incrementApiCall(db, "text");
return result;
} }
export async function getLetterIntro( export async function getLetterIntro(
characterType: CompanionType, characterType: CompanionType,
letter: string, letter: string,
fingerDescription: string, fingerDescription: string,
db: IDBDatabase,
): Promise<string> { ): Promise<string> {
const config = await loadConfig(); const config = await loadConfig();
if (!config) { if (!config) {
@@ -76,14 +68,6 @@ export async function getLetterIntro(
); );
} }
const canCall = await checkRateLimit(db, "text");
if (!canCall) {
return (
getFallbackLetterIntro(letter)?.text ??
"Drücke die leuchtende Taste!"
);
}
const companion = getCompanion(characterType); const companion = getCompanion(characterType);
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald. const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
Du stellst einem 7-jährigen Kind den Buchstaben ${letter} vor. Du stellst einem 7-jährigen Kind den Buchstaben ${letter} vor.
@@ -100,28 +84,16 @@ Regeln:
systemPrompt, systemPrompt,
config, config,
); );
if (!result) { return result ?? getFallbackLetterIntro(letter)?.text ?? "Drücke die leuchtende Taste!";
return (
getFallbackLetterIntro(letter)?.text ??
"Drücke die leuchtende Taste!"
);
}
await incrementApiCall(db, "text");
return result;
} }
export async function getForestComment( export async function getForestComment(
characterType: CompanionType, characterType: CompanionType,
elementDescription: string, elementDescription: string,
db: IDBDatabase,
): Promise<string> { ): Promise<string> {
const config = await loadConfig(); const config = await loadConfig();
if (!config) return getRandomFallbackForestComment(); if (!config) return getRandomFallbackForestComment();
const canCall = await checkRateLimit(db, "text");
if (!canCall) return getRandomFallbackForestComment();
const companion = getCompanion(characterType); const companion = getCompanion(characterType);
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald. const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
Ein Kind hat gerade eine Tippübung abgeschlossen. Im Wald ist ein neues Wesen erschienen: ${elementDescription}. Ein Kind hat gerade eine Tippübung abgeschlossen. Im Wald ist ein neues Wesen erschienen: ${elementDescription}.
@@ -138,8 +110,5 @@ Regeln:
systemPrompt, systemPrompt,
config, config,
); );
if (!result) return getRandomFallbackForestComment(); return result ?? getRandomFallbackForestComment();
await incrementApiCall(db, "text");
return result;
} }
+2 -17
View File
@@ -7,11 +7,7 @@
*/ */
import { loadConfig } from "../api/config"; import { loadConfig } from "../api/config";
import { import { generateImage } from "../api/gemini";
generateImage,
checkRateLimit,
incrementApiCall,
} from "../api/gemini";
import { import {
getStyleReference, getStyleReference,
saveForestElement, saveForestElement,
@@ -93,14 +89,6 @@ async function generateForestElement(db: IDBDatabase): Promise<{
description: string; description: string;
prompt: string; prompt: string;
}> { }> {
// Check rate limit
const canGenerate = await checkRateLimit(db, "image");
if (!canGenerate) {
const fallback = await getRandomFallbackImage();
return { ...fallback, prompt: "fallback" };
}
// Load config
const config = await loadConfig(); const config = await loadConfig();
if (!config) { if (!config) {
const fallback = await getRandomFallbackImage(); const fallback = await getRandomFallbackImage();
@@ -128,9 +116,6 @@ async function generateForestElement(db: IDBDatabase): Promise<{
return { ...fallback, prompt }; return { ...fallback, prompt };
} }
// Track API usage
await incrementApiCall(db, "image");
return { blob, description: elementType, prompt }; return { blob, description: elementType, prompt };
} }
@@ -170,7 +155,7 @@ export async function initRewardScreen(
// 3. Get companion comment // 3. Get companion comment
let comment: string; let comment: string;
try { try {
comment = await getForestComment(characterType, result.description, db); comment = await getForestComment(characterType, result.description);
} catch { } catch {
comment = getRandomFallbackForestComment(); comment = getRandomFallbackForestComment();
} }
-2
View File
@@ -43,8 +43,6 @@ document.addEventListener("DOMContentLoaded", async () => {
audioEnabled: false, audioEnabled: false,
speechEnabled: false, speechEnabled: false,
apiKey: "", apiKey: "",
apiCallsToday: { text: 0, image: 0 },
lastApiCallDate: "",
}; };
updated.audioEnabled = nowEnabled; updated.audioEnabled = nowEnabled;
await saveSettings(db, updated); await saveSettings(db, updated);
-2
View File
@@ -50,8 +50,6 @@ describe("IndexedDB wrapper", () => {
audioEnabled: true, audioEnabled: true,
speechEnabled: false, speechEnabled: false,
apiKey: "test-key", apiKey: "test-key",
apiCallsToday: { text: 0, image: 0 },
lastApiCallDate: "2026-03-29",
}; };
await saveSettings(db, settings); await saveSettings(db, settings);
const result = await getSettings(db); const result = await getSettings(db);
-2
View File
@@ -56,8 +56,6 @@ export interface Settings {
audioEnabled: boolean; audioEnabled: boolean;
speechEnabled: boolean; speechEnabled: boolean;
apiKey: string; apiKey: string;
apiCallsToday: { text: number; image: number };
lastApiCallDate: string; // ISO date "YYYY-MM-DD"
} }
// Level definition (used in game/levels.ts) // Level definition (used in game/levels.ts)
-4
View File
@@ -198,8 +198,6 @@ async function loadSettings(
audioEnabled: false, audioEnabled: false,
speechEnabled: false, speechEnabled: false,
apiKey: "", apiKey: "",
apiCallsToday: { text: 0, image: 0 },
lastApiCallDate: "",
}; };
currentSettings.audioEnabled = audioCheckbox.checked; currentSettings.audioEnabled = audioCheckbox.checked;
await saveSettings(db, currentSettings); await saveSettings(db, currentSettings);
@@ -212,8 +210,6 @@ async function loadSettings(
audioEnabled: false, audioEnabled: false,
speechEnabled: false, speechEnabled: false,
apiKey: "", apiKey: "",
apiCallsToday: { text: 0, image: 0 },
lastApiCallDate: "",
}; };
currentSettings.apiKey = apiKeyInput.value; currentSettings.apiKey = apiKeyInput.value;
await saveSettings(db, currentSettings); await saveSettings(db, currentSettings);
+36 -2
View File
@@ -92,19 +92,54 @@ export function initWelcomeScreen(
// Finger description for companion letter intros // Finger description for companion letter intros
const fingerDescriptions: Record<string, string> = { const fingerDescriptions: Record<string, string> = {
// Level 1: Home row
f: "linker Zeigefinger", f: "linker Zeigefinger",
j: "rechter Zeigefinger", j: "rechter Zeigefinger",
" ": "Daumen",
// Level 2
d: "linker Mittelfinger", d: "linker Mittelfinger",
k: "rechter Mittelfinger", k: "rechter Mittelfinger",
// Level 3
s: "linker Ringfinger", s: "linker Ringfinger",
l: "rechter Ringfinger", l: "rechter Ringfinger",
// Level 4
a: "linker kleiner Finger", a: "linker kleiner Finger",
ö: "rechter kleiner Finger", ö: "rechter kleiner Finger",
// Level 5
g: "linker Zeigefinger (Streckung)", g: "linker Zeigefinger (Streckung)",
h: "rechter Zeigefinger (Streckung)", h: "rechter Zeigefinger (Streckung)",
// Level 6
e: "linker Mittelfinger (obere Reihe)", e: "linker Mittelfinger (obere Reihe)",
i: "rechter Mittelfinger (obere Reihe)", i: "rechter Mittelfinger (obere Reihe)",
" ": "Daumen", // Level 7
r: "linker Zeigefinger (obere Reihe)",
u: "rechter Zeigefinger (obere Reihe)",
// Level 8
t: "linker Zeigefinger (obere Reihe, Streckung)",
z: "rechter Zeigefinger (obere Reihe, Streckung)",
// Level 9
w: "linker Ringfinger (obere Reihe)",
o: "rechter Ringfinger (obere Reihe)",
// Level 10
q: "linker kleiner Finger (obere Reihe)",
p: "rechter kleiner Finger (obere Reihe)",
// Level 11
n: "rechter Zeigefinger (untere Reihe)",
b: "linker Zeigefinger (untere Reihe)",
// Level 12
v: "linker Zeigefinger (untere Reihe)",
m: "rechter Zeigefinger (untere Reihe)",
// Level 13
c: "linker Mittelfinger (untere Reihe)",
x: "linker Ringfinger (untere Reihe)",
// Level 14
ä: "rechter kleiner Finger",
ü: "rechter kleiner Finger",
// Level 15
y: "linker kleiner Finger (untere Reihe)",
",": "rechter Mittelfinger (untere Reihe)",
// Level 16
".": "rechter Ringfinger (untere Reihe)",
}; };
// Encouragement messages for repetition protection // Encouragement messages for repetition protection
@@ -417,7 +452,6 @@ export function initLessonScreen(
characterType, characterType,
key.toUpperCase(), key.toUpperCase(),
fingerDesc, fingerDesc,
db,
); );
if (cancelled) return; if (cancelled) return;