diff --git a/backend/server/api/coach/message.post.ts b/backend/server/api/coach/message.post.ts index 79c3daa..ca431a6 100644 --- a/backend/server/api/coach/message.post.ts +++ b/backend/server/api/coach/message.post.ts @@ -243,6 +243,7 @@ import { PLAN_LIMITS } from "../../utils/plan-features"; import { usePrisma } from "../../utils/prisma"; import { getMemoriesForUser, markReferenced } from "../../db/lyraMemory"; import { extractAndStoreMemories } from "../../utils/lyraMemoryExtract"; +import { stripMarkdown } from "../../utils/strip-markdown"; /** * Lyra-Plan-Beschreibung dynamisch aus PLAN_LIMITS generieren. @@ -599,6 +600,11 @@ export default defineEventHandler(async (event) => { }); } + // Markdown-Strip safety-net: trotz expliziter Prompt-Regel emittieren manche + // Modelle (insbesondere Haiku) weiterhin **bold** + Bullet-Lists. RN-Mobile + // rendert kein Markdown — User sieht rohe Sterne. Hier final cleanen. + text = stripMarkdown(text); + const feedbackSaved = await feedbackPromise; // Memory: markReferenced + Extraction fire-and-forget diff --git a/backend/server/utils/strip-markdown.ts b/backend/server/utils/strip-markdown.ts new file mode 100644 index 0000000..0cd4f89 --- /dev/null +++ b/backend/server/utils/strip-markdown.ts @@ -0,0 +1,58 @@ +/** + * Entfernt Markdown-Formatierung aus LLM-Antworten. + * + * Hintergrund: Trotz expliziter "kein Markdown"-Anweisung im System-Prompt + * emittieren manche Modelle (insbesondere Claude Haiku) weiterhin **bold**, + * Bullet-Lists und ähnliches. Das wirkt in der Mobile-App unsauber, weil dort + * kein Markdown-Renderer aktiv ist — User sehen rohe Sterne. + * + * Diese Funktion ist ein Safety-Net: nach LLM-Call angewendet, garantiert + * sie sauberen Klartext, ohne den eigentlichen Inhalt zu beschädigen. + */ +export function stripMarkdown(input: string): string { + if (!input) return input; + + let out = input; + + // Bold/italic: **text** / __text__ / *text* / _text_ + out = out.replace(/\*\*\*(.+?)\*\*\*/g, "$1"); + out = out.replace(/___(.+?)___/g, "$1"); + out = out.replace(/\*\*(.+?)\*\*/g, "$1"); + out = out.replace(/__(.+?)__/g, "$1"); + // Single * or _ als italic — nur wenn auf beiden Seiten Wort-Boundary, + // sonst zerstören wir Listen-Bullets oder Multiplikationen + out = out.replace(/(^|\s)\*([^\s*][^*]*?)\*(?=\s|$|[.,;:!?])/g, "$1$2"); + out = out.replace(/(^|\s)_([^\s_][^_]*?)_(?=\s|$|[.,;:!?])/g, "$1$2"); + + // Headings: # / ## / ### am Zeilenanfang + out = out.replace(/^#{1,6}\s+/gm, ""); + + // Bullet-Lists: "- foo", "* foo", "+ foo" am Zeilenanfang + // Behalte einfachen Aufzählungs-Bindestrich aber entferne markdown-marker-Charakter + // → "- Schutz" wird "• Schutz" (Bullet ohne Markdown-Semantik) + out = out.replace(/^[ \t]*[-*+][ \t]+/gm, "• "); + + // Numbered Lists: "1. foo", "2. foo" am Zeilenanfang + // Zahlen behalten (informativer als bullet) — nur falls strikt entfernt werden soll + // out = out.replace(/^[ \t]*\d+\.[ \t]+/gm, ""); + + // Inline-Code: `text` + out = out.replace(/`([^`]+)`/g, "$1"); + + // Code-Blocks: ```...``` + out = out.replace(/```[a-zA-Z]*\n?([\s\S]*?)```/g, "$1"); + + // Links: [text](url) → text + out = out.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); + + // Blockquotes: "> foo" → "foo" + out = out.replace(/^>[ \t]?/gm, ""); + + // Horizontal rules: --- / *** / ___ alleinstehend + out = out.replace(/^[\t ]*[-*_]{3,}[\t ]*$/gm, ""); + + // Multiple Leerzeilen → max 1 Leerzeile + out = out.replace(/\n{3,}/g, "\n\n"); + + return out.trim(); +}