const { normalizeLink, absoluteInternalUrl, routeLabel, stripTags } = require("./link_normalizer"); const METHODS = /\b(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(\/[^\s<>"')\]]+)/gi; const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; const HTML_LINK = /]*\bhref\s*=\s*(["'])(.*?)\1[^>]*>(.*?)<\/a>/gi; const REPO_PATH = /\b(?:src|plugins|scripts|data)\/[a-z0-9_./-]+\.(?:js|json|ejs|md|css|txt)\b/gi; const SOURCE_DETAIL = /\s*\((?:src|plugins|scripts)\/[^)]+\)/gi; function formatAssistantResponse({ text, links = [], baseUrl = "", verifiedRoutes = [], role = "user", allowModeratorCodeHelp = false, maxLength = null }) { const routeSet = new Set(verifiedRoutes); const collected = []; let output = normalizeCodeFences(String(text || "")); output = output.replace(HTML_LINK, (_match, _quote, href, label) => { const normalized = collectLink(collected, { href, label: stripTags(label), external: true }, baseUrl, routeSet); return normalized ? markdownLink(normalized) : stripTags(label); }); output = output.replace(MARKDOWN_LINK, (_match, label, href) => { const normalized = collectLink(collected, { href, label, external: true }, baseUrl, routeSet); return normalized ? markdownLink(normalized) : label; }); output = output.replace(METHODS, (_match, route) => { const punctuation = route.match(/[.,;:!?]+$/)?.[0] || ""; const cleanRoute = punctuation ? route.slice(0, -punctuation.length) : route; const normalized = collectLink(collected, { href: cleanRoute, label: routeLabel(cleanRoute) }, baseUrl, routeSet); return normalized ? `${markdownLink(normalized)}${punctuation}` : route; }); for (const link of Array.isArray(links) ? links : []) { collectLink(collected, link, baseUrl, routeSet); } output = output .replace(//gi, "\n") .replace(/<\/p\s*>/gi, "\n\n") .replace(/<[^>]*>/g, ""); if (role !== "admin") { output = output.replace(REPO_PATH, "internal Lumi component").replace(SOURCE_DETAIL, ""); if (role === "user" || !allowModeratorCodeHelp) { output = output.replace(/```[\s\S]*?```/g, "Technical implementation details are available to administrators."); } } output = output.replace(/[ \t]+\n/g, "\n").replace(/\n{4,}/g, "\n\n\n").trim(); const originalLength = output.length; const delivered = Number.isFinite(Number(maxLength)) && Number(maxLength) > 0 ? truncateFinal(output, maxLength) : output; return { text: delivered, links: uniqueLinks(collected), original_final_length: originalLength, delivered_length: delivered.length, truncated: delivered.length < originalLength }; } function collectLink(output, link, baseUrl, routeSet) { const normalized = normalizeLink(link, baseUrl, routeSet); if (normalized) output.push(normalized); return normalized; } function truncateFinal(value, maxLength) { const limit = Math.max(100, Number.parseInt(maxLength, 10) || 4000); if (value.length <= limit) return value; const suffix = " [reply truncated]"; return `${value.slice(0, Math.max(1, limit - suffix.length)).trimEnd()}${suffix}`; } function markdownLink(link) { const label = String(link.label || "").replace(/[[\]]/g, ""); const href = String(link.href || "").replace(/[<>\s]/g, (character) => encodeURIComponent(character)); return `[${label}](${href})`; } function uniqueLinks(links) { return [...new Map(links.map((link) => [link.href, link])).values()]; } function normalizeCodeFences(value) { return value.replace(/```(javascript|python)\s+([^\n][\s\S]*?)```/gi, (_match, language, code) => `\`\`\`${language.toLowerCase()}\n${code.trim()}\n\`\`\``); } module.exports = { formatAssistantResponse, normalizeLink, truncateFinal, absoluteInternalUrl, normalizeCodeFences };