feat(02-04): implement companion text module with API-first fallback strategy
- getGreeting: AI greeting with time-of-day context, falls back to static texts - getLetterIntro: AI letter introduction with finger description, falls back to curated intros - getForestComment: AI forest element comment, falls back to static comments - All functions check config availability and rate limits before API calls - System prompts enforce max 25 words, simple German, no performance praise
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Companion text module: AI-generated or fallback text for greetings,
|
||||
* letter introductions, and forest comments.
|
||||
*
|
||||
* Strategy: Try Gemini API first (with rate-limit check), fall back
|
||||
* to curated static texts when API is unavailable.
|
||||
*/
|
||||
|
||||
import { companions } from "./characters";
|
||||
import type { CompanionType } from "../types";
|
||||
import { loadConfig } from "../api/config";
|
||||
import { generateText, checkRateLimit, incrementApiCall } from "../api/gemini";
|
||||
import {
|
||||
type TimeOfDay,
|
||||
getRandomFallbackGreeting,
|
||||
getFallbackLetterIntro,
|
||||
getRandomFallbackForestComment,
|
||||
} from "./fallbacks";
|
||||
|
||||
function getTimeOfDay(): TimeOfDay {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return "morgen";
|
||||
if (hour < 17) return "mittag";
|
||||
return "abend";
|
||||
}
|
||||
|
||||
function getCompanion(type: CompanionType) {
|
||||
return companions.find((c) => c.type === type)!;
|
||||
}
|
||||
|
||||
export async function getGreeting(
|
||||
characterType: CompanionType,
|
||||
db: IDBDatabase,
|
||||
): Promise<string> {
|
||||
const timeOfDay = getTimeOfDay();
|
||||
const config = await loadConfig();
|
||||
if (!config) return getRandomFallbackGreeting(timeOfDay);
|
||||
|
||||
const canCall = await checkRateLimit(db, "text");
|
||||
if (!canCall) return getRandomFallbackGreeting(timeOfDay);
|
||||
|
||||
const companion = getCompanion(characterType);
|
||||
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
|
||||
Du sprichst mit einem 7-jaehrigen Kind, das Tippen lernt.
|
||||
|
||||
Regeln:
|
||||
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
|
||||
- Einfache Sprache, kurze Woerter
|
||||
- Freundlich und warm, nie belehrend
|
||||
- Keine Bewertung von Leistung
|
||||
- Kein Englisch
|
||||
- Beziehe dich auf die Tageszeit: ${timeOfDay}`;
|
||||
|
||||
const result = await generateText(
|
||||
"Schreib eine Begruessung.",
|
||||
systemPrompt,
|
||||
config,
|
||||
);
|
||||
if (!result) return getRandomFallbackGreeting(timeOfDay);
|
||||
|
||||
await incrementApiCall(db, "text");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getLetterIntro(
|
||||
characterType: CompanionType,
|
||||
letter: string,
|
||||
fingerDescription: string,
|
||||
db: IDBDatabase,
|
||||
): Promise<string> {
|
||||
const config = await loadConfig();
|
||||
if (!config) {
|
||||
return (
|
||||
getFallbackLetterIntro(letter)?.text ??
|
||||
"Druecke die leuchtende Taste!"
|
||||
);
|
||||
}
|
||||
|
||||
const canCall = await checkRateLimit(db, "text");
|
||||
if (!canCall) {
|
||||
return (
|
||||
getFallbackLetterIntro(letter)?.text ??
|
||||
"Druecke die leuchtende Taste!"
|
||||
);
|
||||
}
|
||||
|
||||
const companion = getCompanion(characterType);
|
||||
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
|
||||
Du stellst einem 7-jaehrigen Kind den Buchstaben ${letter} vor.
|
||||
|
||||
Regeln:
|
||||
- Max. 2 Saetze (max. 25 Woerter)
|
||||
- Erklaere, welcher Finger den Buchstaben drueckt: ${fingerDescription}
|
||||
- Verwende eine bildhafte Eselsbruecke, die zum Wald/Natur/Tiere-Thema passt
|
||||
- Einfache Sprache
|
||||
- Kein Englisch`;
|
||||
|
||||
const result = await generateText(
|
||||
"Stelle den Buchstaben vor.",
|
||||
systemPrompt,
|
||||
config,
|
||||
);
|
||||
if (!result) {
|
||||
return (
|
||||
getFallbackLetterIntro(letter)?.text ??
|
||||
"Druecke die leuchtende Taste!"
|
||||
);
|
||||
}
|
||||
|
||||
await incrementApiCall(db, "text");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getForestComment(
|
||||
characterType: CompanionType,
|
||||
elementDescription: string,
|
||||
db: IDBDatabase,
|
||||
): Promise<string> {
|
||||
const config = await loadConfig();
|
||||
if (!config) return getRandomFallbackForestComment();
|
||||
|
||||
const canCall = await checkRateLimit(db, "text");
|
||||
if (!canCall) return getRandomFallbackForestComment();
|
||||
|
||||
const companion = getCompanion(characterType);
|
||||
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
|
||||
Ein Kind hat gerade eine Tippuebung abgeschlossen. Im Wald ist ein neues Wesen erschienen: ${elementDescription}.
|
||||
|
||||
Regeln:
|
||||
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
|
||||
- Druecke Staunen und Freude aus
|
||||
- Einfache Sprache fuer 7-Jaehrige
|
||||
- Kein Lob fuer Leistung, sondern Begeisterung ueber das neue Waldelement
|
||||
- Kein Englisch`;
|
||||
|
||||
const result = await generateText(
|
||||
"Kommentiere das neue Element.",
|
||||
systemPrompt,
|
||||
config,
|
||||
);
|
||||
if (!result) return getRandomFallbackForestComment();
|
||||
|
||||
await incrementApiCall(db, "text");
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user