diff --git a/backend/server/api/admin/lyra-post.post.ts b/backend/server/api/admin/lyra-post.post.ts index ea6eac4..1234209 100644 --- a/backend/server/api/admin/lyra-post.post.ts +++ b/backend/server/api/admin/lyra-post.post.ts @@ -24,28 +24,41 @@ export const TOPIC_HINTS: Record = { "Weise freundlich auf ein ReBreak-Feature hin (Blocker, Streak, Mail-Agent, Lyra-Chat, SOS-Atemübung) – wähle eines zufällig oder nutze den gegebenen Kontext.", }; -const LYRA_SYSTEM_PROMPT = `Du bist Lyra – Recovery-Coach und Begleiterin der ReBreak-Community. Du hast tiefes Wissen in CBT, Verhaltenspsychologie und dem Alltag von Menschen mit Spielsucht. +type Lang = "de" | "en" | "fr" | "ar"; +const LANGS: Lang[] = ["de", "en", "fr", "ar"]; +const LANG_NAME: Record = { + de: "Deutsch", + en: "English", + fr: "français", + ar: "العربية (Arabic)", +}; + +function lyraSystemPrompt(lang: Lang): string { + return `Du bist Lyra – Recovery-Coach und Begleiterin der ReBreak-Community. Du hast tiefes Wissen in CBT, Verhaltenspsychologie und dem Alltag von Menschen mit Spielsucht. Du schreibst kurze Community-Beiträge. Deine Stimme: -- Direkt und persönlich – du sprichst die Person an ("du") +- Direkt und persönlich – du sprichst die Person an ("du" / "tu" / "you" / "أنت" je nach Sprache) - Warmherzig, geerdet, wie jemand der wirklich zuhört - Niemals klischeehafte Motivationsfloskeln ("Du schaffst das!!!") - Kein KI-Sprech, keine Listen, keine Aufzählungen - Keine Casino-Werbung, keine Links, keine medizinischen Diagnosen -- Auf Deutsch, max. 3–4 Sätze +- WICHTIG: Antwort AUSSCHLIESSLICH in ${LANG_NAME[lang]}, max. 3–4 Sätze Antworte NUR mit dem Post-Text. Kein "Lyra:" Prefix, keine Anführungszeichen.`; +} -const REBREAK_SYSTEM_PROMPT = `Du bist der offizielle ReBreak Account. ReBreak ist eine App zur Überwindung von Glücksspielsucht. +function rebreakSystemPrompt(lang: Lang): string { + return `Du bist der offizielle ReBreak Account. ReBreak ist eine App zur Überwindung von Glücksspielsucht. Du postest Neuigkeiten, Updates und Community-Ankündigungen. Deine Tonalität: - Offiziell aber nahbar – wie ein Team-Update, nicht wie Werbung - Kurz (max. 3–4 Sätze) - Sachlich und informativ, gelegentlich motivierend - Keine medizinischen Ratschläge, keine Links -- Auf Deutsch +- WICHTIG: Antwort AUSSCHLIESSLICH in ${LANG_NAME[lang]} Antworte NUR mit dem Post-Text. Kein "ReBreak:" Prefix, keine Anführungszeichen.`; +} /** POST /api/admin/lyra-post — manueller Bot-Post vom Admin-Dashboard */ export default defineEventHandler(async (event) => { @@ -73,7 +86,9 @@ export default defineEventHandler(async (event) => { let content: string; if (body?.customContent?.trim()) { - // Admin hat Text direkt eingegeben – kein LLM-Call nötig + // Admin hat Text direkt eingegeben – kein LLM-Call nötig. + // Admin schreibt in genau einer Sprache (was er will) → wir speichern's + // plain. PostCard.tsx zeigt das dann allen Usern in der Schreibsprache. content = body.customContent.trim(); } else { if (!config.groqApiKey) { @@ -92,31 +107,51 @@ export default defineEventHandler(async (event) => { ? `${TOPIC_HINTS[topic]}\n\nZusätzlicher Kontext für diesen Post: ${context}` : TOPIC_HINTS[topic]; - const systemPrompt = - author === "rebreak" ? REBREAK_SYSTEM_PROMPT : LYRA_SYSTEM_PROMPT; + // 4 parallele Groq-Calls für de/en/fr/ar. Promise.allSettled damit ein + // fehlgeschlagener Locale-Call die anderen 3 nicht killt. + const results = await Promise.allSettled( + LANGS.map(async (lang) => { + const systemPrompt = + author === "rebreak" ? rebreakSystemPrompt(lang) : lyraSystemPrompt(lang); + const response = await $fetch<{ + choices: { message: { content: string } }[]; + }>("https://api.groq.com/openai/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${config.groqApiKey}`, + "Content-Type": "application/json", + }, + body: { + model: "llama-3.3-70b-versatile", + max_tokens: 200, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + }, + }); + const text = response.choices?.[0]?.message?.content?.trim() ?? ""; + if (!text) throw new Error("empty content"); + return [lang, text] as const; + }), + ); - const response = await $fetch<{ - choices: { message: { content: string } }[]; - }>("https://api.groq.com/openai/v1/chat/completions", { - method: "POST", - headers: { - Authorization: `Bearer ${config.groqApiKey}`, - "Content-Type": "application/json", - }, - body: { - model: "llama-3.3-70b-versatile", - max_tokens: 200, - messages: [ - { role: "system", content: systemPrompt }, - { role: "user", content: userPrompt }, - ], - }, - }); - - content = response.choices?.[0]?.message?.content?.trim() ?? ""; - if (!content) { - throw createError({ statusCode: 500, message: "Keine Antwort von LLM" }); + const localized: Partial> = {}; + for (const r of results) { + if (r.status === "fulfilled") { + const [lang, text] = r.value; + localized[lang] = text; + } } + if (!localized.de) { + throw createError({ + statusCode: 500, + message: "LLM-Generation für DE fehlgeschlagen (alle Locales).", + }); + } + // Content als JSON-encoded {de,en,fr,ar} — PostCard.tsx parsed + picked + // current-locale; fällt auf DE zurück wenn locale fehlt. + content = JSON.stringify(localized); } const post = await createPost(botUserId, "community", content);