diff --git a/webui/src/components/MarkdownTextRenderer.tsx b/webui/src/components/MarkdownTextRenderer.tsx index 2a4c410c7..d3a664b92 100644 --- a/webui/src/components/MarkdownTextRenderer.tsx +++ b/webui/src/components/MarkdownTextRenderer.tsx @@ -30,9 +30,10 @@ type MarkdownAstNode = { }; }; -type CitationLink = { +type InlineLinkPreview = { href: string; origin: string; + prefix?: string; title: string; initials: string; }; @@ -186,7 +187,7 @@ function nodeText(value: ReactNode): string { .join(""); } -function citationParts(value: ReactNode): { text: string; href?: string } { +function linkPreviewParts(value: ReactNode): { text: string; href?: string } { let text = ""; let href: string | undefined; for (const child of Children.toArray(value)) { @@ -201,21 +202,21 @@ function citationParts(value: ReactNode): { text: string; href?: string } { if (!href && typeof props.href === "string" && /^https?:\/\//i.test(props.href)) { href = props.href; } - const nested = citationParts(props.children); + const nested = linkPreviewParts(props.children); text += nested.text; href ||= nested.href; } return { text, href }; } -function cleanCitationText(value: string): string { +function cleanLinkPreviewText(value: string): string { return value .replace(/\s+/g, " ") .replace(/^[\s"'“”‘’]+|[\s"'“”‘’]+$/g, "") .trim(); } -function citationInitials(value: string): string { +function linkPreviewInitials(value: string): string { const clean = value .replace(/^https?:\/\//i, "") .replace(/^www\./i, "") @@ -225,8 +226,8 @@ function citationInitials(value: string): string { .toUpperCase(); } -function sourceLinkFromChildren(children: ReactNode): CitationLink | null { - const { text: rawText, href } = citationParts(children); +function inlineLinkPreviewFromChildren(children: ReactNode): InlineLinkPreview | null { + const { text: rawText, href } = linkPreviewParts(children); if (!href) return null; let url: URL; @@ -246,50 +247,54 @@ function sourceLinkFromChildren(children: ReactNode): CitationLink | null { if (!strippedUrl || strippedUrl.length < 4) return null; const sourceMatch = /^(.*?)\s*(?:[—–]| - |:)\s*(.+)$/.exec(strippedUrl); - const sourceLabel = sourceMatch?.[1] ? cleanCitationText(sourceMatch[1]) : undefined; - const title = cleanCitationText(sourceMatch?.[2] ?? strippedUrl); + const prefix = sourceMatch?.[1] ? cleanLinkPreviewText(sourceMatch[1]) : undefined; + const title = cleanLinkPreviewText(sourceMatch?.[2] ?? strippedUrl); if (!title || /^https?:\/\//i.test(title)) return null; return { href, origin: url.origin, + prefix, title, - initials: citationInitials(sourceLabel || url.hostname), + initials: linkPreviewInitials(prefix || url.hostname), }; } -function CitationRow({ citation }: { citation: CitationLink }) { +function InlineLinkPreviewRow({ link }: { link: InlineLinkPreview }) { + const label = link.prefix + ? `${link.prefix} — ${link.title}` + : link.title; return ( - {citation.initials} + {link.initials} { event.currentTarget.style.display = "none"; }} /> - - {citation.title} + + {label} ); @@ -412,13 +417,12 @@ export default function MarkdownTextRenderer({ ); }, - li({ children: markdownChildren, className: itemClassName, node: _node }) { - void _node; - const citation = sourceLinkFromChildren(markdownChildren); - if (citation) { + li({ children: markdownChildren, className: itemClassName }) { + const link = inlineLinkPreviewFromChildren(markdownChildren); + if (link) { return (
  • - +
  • ); } diff --git a/webui/src/tests/markdown-text-renderer.test.tsx b/webui/src/tests/markdown-text-renderer.test.tsx index 3c76f5976..4fc6ed1dc 100644 --- a/webui/src/tests/markdown-text-renderer.test.tsx +++ b/webui/src/tests/markdown-text-renderer.test.tsx @@ -68,7 +68,7 @@ describe("MarkdownTextRenderer", () => { expect(screen.queryByRole("img", { name: "index.html" })).not.toBeInTheDocument(); }); - it("renders source-style link lists as citation rows", () => { + it("renders title plus url list items as compact link rows", () => { render( { @@ -78,17 +78,37 @@ describe("MarkdownTextRenderer", () => { ); expect( - screen.getByRole("link", { name: "Open source: When will GPT-5.6 be released?" }), + screen.getByRole("link", { + name: "Open link: Polymarket — When will GPT-5.6 be released?", + }), ).toHaveAttribute( "href", "https://polymarket.com/event/when-will-gpt-5pt6-be-released", ); expect( - screen.getByRole("link", { name: "Open source: GPT-5.6 released by...?" }), + screen.getByRole("link", { + name: "Open link: Polymarket — GPT-5.6 released by...?", + }), ).toHaveAttribute("href", "https://polymarket.com/event/gpt-5pt6-released-by"); expect(screen.queryByText("Polymarket · polymarket.com")).not.toBeInTheDocument(); }); + it("does not require a source heading for compact link rows", () => { + render( + + { + "Useful links:\n\n- Polymarket — “When will GPT-5.6 be released?”\n https://polymarket.com/event/when-will-gpt-5pt6-be-released" + } + , + ); + + expect( + screen.getByRole("link", { + name: "Open link: Polymarket — When will GPT-5.6 be released?", + }), + ).toHaveAttribute("href", "https://polymarket.com/event/when-will-gpt-5pt6-be-released"); + }); + it("renders media attachments without an extra preview/code wrapper", () => { render(![Diagram](/api/media/sig/payload));