TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1'use client';23import Link from 'next/link';4import { Fragment, type ReactNode } from 'react';56/**7 * Tiny dependency-free markdown renderer for assistant answers: paragraphs, headings, bullet and8 * numbered lists, pipe tables, bold/italic/code and links (internal links use next/link).9 * Not a full CommonMark implementation — enough for terminal-style research answers.10 */11export function Markdown({ text }: { text: string }) {12 const blocks = splitBlocks(text);13 return <div className="space-y-2 text-[13px] leading-relaxed">{blocks.map((b, i) => <Fragment key={i}>{renderBlock(b)}</Fragment>)}</div>;14}1516function splitBlocks(text: string): string[] {17 return text.replace(/\r\n/g, '\n').split(/\n{2,}/).map((b) => b.trim()).filter(Boolean);18}1920function renderBlock(block: string): ReactNode {21 const lines = block.split('\n');22 if (lines.length >= 2 && lines.every((l) => l.trim().startsWith('|'))) return renderTable(lines);23 if (lines.every((l) => /^\s*[-*•]\s+/.test(l))) return <ul className="list-disc space-y-0.5 pl-5">{lines.map((l, i) => <li key={i}>{inline(l.replace(/^\s*[-*•]\s+/, ''))}</li>)}</ul>;24 if (lines.every((l) => /^\s*\d+[.)]\s+/.test(l))) return <ol className="list-decimal space-y-0.5 pl-5">{lines.map((l, i) => <li key={i}>{inline(l.replace(/^\s*\d+[.)]\s+/, ''))}</li>)}</ol>;25 const h = block.match(/^(#{1,4})\s+(.*)$/);26 if (h && lines.length === 1) {27 const level = h[1]!.length;28 const cls = level <= 2 ? 'text-sm font-semibold' : 'text-[13px] font-semibold';29 return <p className={cls}>{inline(h[2]!)}</p>;30 }31 if (block.startsWith('```')) return <pre className="overflow-x-auto rounded-md bg-inset p-2 font-mono text-[11px]">{block.replace(/^```\w*\n?/, '').replace(/```$/, '')}</pre>;32 return <p>{lines.map((l, i) => <Fragment key={i}>{i > 0 ? <br /> : null}{inline(l)}</Fragment>)}</p>;33}3435function renderTable(lines: string[]): ReactNode {36 const rows = lines.filter((l) => !/^\s*\|?\s*:?-{2,}/.test(l)).map((l) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim()));37 const [head, ...body] = rows;38 if (!head) return null;39 return (40 <div className="overflow-x-auto">41 <table className="w-full border-collapse text-[12px]">42 <thead>43 <tr>{head.map((c, i) => <th key={i} className="border-b border-border px-2 py-1 text-left text-[10px] font-semibold uppercase tracking-wider text-subtle">{inline(c)}</th>)}</tr>44 </thead>45 <tbody>46 {body.map((r, i) => (47 <tr key={i}>{r.map((c, j) => <td key={j} className={`border-b border-border px-2 py-1 ${/^[\s$€£¥+\-−]?[\d.,]+%?$/.test(c) ? 'num text-right' : ''}`}>{inline(c)}</td>)}</tr>48 ))}49 </tbody>50 </table>51 </div>52 );53}5455const INLINE = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g;5657function inline(s: string): ReactNode {58 const parts = s.split(INLINE).filter((p) => p !== '');59 return parts.map((p, i) => {60 if (p.startsWith('**') && p.endsWith('**')) return <strong key={i}>{p.slice(2, -2)}</strong>;61 if (p.startsWith('*') && p.endsWith('*') && p.length > 2) return <em key={i}>{p.slice(1, -1)}</em>;62 if (p.startsWith('`') && p.endsWith('`')) return <code key={i} className="rounded-sm bg-inset px-1 font-mono text-[11px]">{p.slice(1, -1)}</code>;63 const m = p.match(/^\[([^\]]+)\]\(([^)]+)\)$/);64 if (m) {65 const href = m[2]!;66 const cls = 'font-medium underline decoration-border-strong underline-offset-2 hover:decoration-fg';67 return href.startsWith('/') ? <Link key={i} href={href} className={cls}>{m[1]}</Link> : <a key={i} href={href} target="_blank" rel="noreferrer noopener" className={cls}>{m[1]}</a>;68 }69 return <Fragment key={i}>{p}</Fragment>;70 });71}72