/,
+ '$1 style="border-left:3px solid #6b7280">',
+ ),
+ );
+ i++;
+ continue;
+ }
+
+ // Horizontal rule
+ if (/^-{3,}$/.test(line.trim())) {
+ out.push('
');
+ i++;
+ continue;
+ }
+
+ // Headings — apply role-specific colors for h2
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
+ if (headingMatch) {
+ const level = headingMatch[1].length;
+ const text = headingMatch[2];
+ let style = '';
+ if (level === 2) {
+ if (text.includes('\u{1F464}')) style = ' style="color:#1d4ed8"'; // blue for User
+ else if (text.includes('\u{1F916}')) style = ' style="color:#15803d"'; // green for Assistant
+ }
+ out.push(`${inlineFormat(text)}`);
+ i++;
+ continue;
+ }
+
+ // Fenced code block
+ if (line.trim().startsWith('```')) {
+ i++;
+ const codeLines: string[] = [];
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
+ codeLines.push(lines[i]);
+ i++;
+ }
+ i++; // skip closing ```
+ out.push(`${escapeHtml(codeLines.join('\n'))}
`);
+ continue;
+ }
+
+ // Table (starts with |)
+ if (line.trim().startsWith('|')) {
+ const tableLines: string[] = [];
+ while (i < lines.length && lines[i].trim().startsWith('|')) {
+ tableLines.push(lines[i]);
+ i++;
+ }
+ out.push(parseTable(tableLines));
+ continue;
+ }
+
+ // Paragraph
+ out.push(`${inlineFormat(line)}
`);
+ i++;
+ }
+
+ return out.join('\n');
+}
+
+function inlineFormat(s: string): string {
+ // Escape HTML first to prevent XSS from message content
+ let result = escapeHtml(s);
+ // Markdown links: [text](url) — render as clickable anchors in PDF (http/https only)
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match: string, text: string, url: string) => {
+ try {
+ const parsed = new URL(url);
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
+ return `${text}`;
+ }
+ } catch {
+ // invalid URL — fall through
+ }
+ return `${text} (${url})`;
+ });
+ result = result.replace(/\*\*(.+?)\*\*/g, '$1');
+ result = result.replace(/(?$1');
+ result = result.replace(/`([^`]+?)`/g, '$1');
+ return result;
+}
+
+function parseTable(rows: string[]): string {
+ const parsed = rows
+ .filter((r) => !/^\|\s*[-:]+/.test(r))
+ .map((r) =>
+ r
+ .split('|')
+ .slice(1, -1)
+ .map((c) => c.trim()),
+ );
+
+ if (parsed.length === 0) return '';
+
+ const [header, ...body] = parsed;
+ let html = '';
+ for (const cell of header) html += `| ${inlineFormat(cell)} | `;
+ html += '
';
+ for (const row of body) {
+ html += '';
+ for (const cell of row) html += `| ${inlineFormat(cell)} | `;
+ html += '
';
+ }
+ html += '
';
+ return html;
+}
diff --git a/components/frontend/tailwind.config.js b/components/frontend/tailwind.config.js
index 7e3e0e0cc..bc21d100d 100644
--- a/components/frontend/tailwind.config.js
+++ b/components/frontend/tailwind.config.js
@@ -93,8 +93,5 @@ module.exports = {
},
},
},
- plugins: [
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- require("tw-animate-css")
- ],
+ plugins: [],
}