feat(community): admin/lyra-post Multi-Locale (motivation/tipp/etc.)
User-Bug: Admin-getriggerter Lyra-Post (Topic 'motivation' o.ä.) zeigte
trotz FR-locale im Frontend immer Deutsch. Root: LYRA_SYSTEM_PROMPT hatte
hartcodiert 'Auf Deutsch' → Groq output war immer DE.
Selbe Multi-Locale-Pattern wie approve.post.ts:
- 4 parallele Groq-Calls (Promise.allSettled) für de/en/fr/ar
- System-Prompt nimmt Lang-Parameter: 'Antwort AUSSCHLIESSLICH in <lang>'
- Content als JSON {de,en,fr,ar} gespeichert
- customContent-Path (Admin tippt selbst Text) bleibt plain — Admin schreibt
in seiner gewählten Sprache, PostCard zeigt allen Usern denselben Text
Frontend (PostCard.tsx) parsed JSON bereits richtig via
resolveLocalizedJsonContent (vorheriger Commit 44a3348).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
44a3348845
commit
c6e2116084
@ -24,28 +24,41 @@ export const TOPIC_HINTS: Record<LyraTopic, string> = {
|
|||||||
"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.",
|
"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<Lang, string> = {
|
||||||
|
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:
|
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
|
- Warmherzig, geerdet, wie jemand der wirklich zuhört
|
||||||
- Niemals klischeehafte Motivationsfloskeln ("Du schaffst das!!!")
|
- Niemals klischeehafte Motivationsfloskeln ("Du schaffst das!!!")
|
||||||
- Kein KI-Sprech, keine Listen, keine Aufzählungen
|
- Kein KI-Sprech, keine Listen, keine Aufzählungen
|
||||||
- Keine Casino-Werbung, keine Links, keine medizinischen Diagnosen
|
- 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.`;
|
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:
|
Du postest Neuigkeiten, Updates und Community-Ankündigungen. Deine Tonalität:
|
||||||
- Offiziell aber nahbar – wie ein Team-Update, nicht wie Werbung
|
- Offiziell aber nahbar – wie ein Team-Update, nicht wie Werbung
|
||||||
- Kurz (max. 3–4 Sätze)
|
- Kurz (max. 3–4 Sätze)
|
||||||
- Sachlich und informativ, gelegentlich motivierend
|
- Sachlich und informativ, gelegentlich motivierend
|
||||||
- Keine medizinischen Ratschläge, keine Links
|
- 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.`;
|
Antworte NUR mit dem Post-Text. Kein "ReBreak:" Prefix, keine Anführungszeichen.`;
|
||||||
|
}
|
||||||
|
|
||||||
/** POST /api/admin/lyra-post — manueller Bot-Post vom Admin-Dashboard */
|
/** POST /api/admin/lyra-post — manueller Bot-Post vom Admin-Dashboard */
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
@ -73,7 +86,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
let content: string;
|
let content: string;
|
||||||
|
|
||||||
if (body?.customContent?.trim()) {
|
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();
|
content = body.customContent.trim();
|
||||||
} else {
|
} else {
|
||||||
if (!config.groqApiKey) {
|
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]}\n\nZusätzlicher Kontext für diesen Post: ${context}`
|
||||||
: TOPIC_HINTS[topic];
|
: TOPIC_HINTS[topic];
|
||||||
|
|
||||||
const systemPrompt =
|
// 4 parallele Groq-Calls für de/en/fr/ar. Promise.allSettled damit ein
|
||||||
author === "rebreak" ? REBREAK_SYSTEM_PROMPT : LYRA_SYSTEM_PROMPT;
|
// 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<{
|
const localized: Partial<Record<Lang, string>> = {};
|
||||||
choices: { message: { content: string } }[];
|
for (const r of results) {
|
||||||
}>("https://api.groq.com/openai/v1/chat/completions", {
|
if (r.status === "fulfilled") {
|
||||||
method: "POST",
|
const [lang, text] = r.value;
|
||||||
headers: {
|
localized[lang] = text;
|
||||||
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" });
|
|
||||||
}
|
}
|
||||||
|
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);
|
const post = await createPost(botUserId, "community", content);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user