deer-flow/frontend/tests/unit/components/workspace/citations/citation-sources-panel.test.ts
Ryker_Feng 8a26b5c9a4
feat(frontend): add citation sources evidence panel (#3907)
* feat(frontend): add citation sources evidence panel

Inline [citation:Title](URL) links render as badges, but with many
citations the reader has no consolidated, deduplicated list of what a
message or report actually drew on. Add a collapsible "sources" panel
that extracts citation links from AI messages and markdown artifacts,
dedupes by URL, counts occurrences, and offers per-source copy of a
reusable markdown reference.

- extractCitationSources: parse [citation:...](url) links, skip images
  and fenced code, dedupe by normalized URL, fall back to domain for
  generic labels
- CitationSourcesPanel: collapsible list with per-source cite counts,
  internal scroll for long lists, and copy-to-clipboard reference button
- Wire panel into AI message content and markdown artifact preview
- Add en-US/zh-CN citation strings and types
- Unit tests for extraction and panel rendering

* fix(frontend): preserve default link styling in message content override

MessageContent_ passes a custom `a` renderer to MarkdownContent, whose
default `a` (primary underline + external target/rel) is overridden
because MarkdownContent spreads props components last. Restore that
styling/external behavior in the fallback branch so normal links in
messages aren't regressed, while keeping citation: and /mnt/ handling.

* fix(frontend): harden citation source extraction and dedupe link renderer

Address review findings on the citation sources panel:
- Use a non-consuming lookbehind so back-to-back citations no longer drop
  every other source.
- Match balanced parenthetical groups in URLs so disambiguation links like
  .../Foo_(a)_(b) are no longer truncated.
- Mask inline code (and unclosed streaming fences) so example citations in
  code aren't scraped as real sources; masking preserves indices.
- Extract a shared createMarkdownLinkComponent factory used by both message
  content and markdown content, removing the duplicated `a` renderer.
2026-07-03 18:03:43 +08:00

89 lines
2.6 KiB
TypeScript

import { describe, expect, it } from "@rstest/core";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { CitationSourcesPanel } from "@/components/workspace/citations/citation-sources-panel";
import type { CitationSource } from "@/core/citations/sources";
import { I18nContext } from "@/core/i18n/context";
const sources: CitationSource[] = [
{
id: "https://example.com/a",
title: "Paper A",
url: "https://example.com/a",
domain: "example.com",
count: 2,
occurrences: [
{ index: 10, title: "Paper A" },
{ index: 90, title: "Paper A" },
],
},
{
id: "https://news.example.org/report",
title: "Report B",
url: "https://news.example.org/report",
domain: "news.example.org",
count: 1,
occurrences: [{ index: 120, title: "Report B" }],
},
];
describe("CitationSourcesPanel", () => {
it("renders nothing when there are no sources", () => {
expect(renderPanel([], "en-US")).toBe("");
});
it("renders a compact source list with occurrence counts", () => {
const html = renderPanel(sources, "en-US");
expect(html).toContain("Used 2 sources");
expect(html).toContain("Paper A");
expect(html).toContain("example.com");
expect(html).toContain("2 cites");
expect(html).toContain("Report B");
expect(html).toContain("news.example.org");
expect(html).toContain('href="https://news.example.org/report"');
});
it("constrains long source lists inside an internal scroll area", () => {
const html = renderPanel(sources, "en-US");
expect(html).toContain("max-h-80");
expect(html).toContain("overflow-y-auto");
expect(html).toContain("overscroll-contain");
});
it("uses localized summary and cite labels", () => {
const html = renderPanel(sources, "zh-CN");
expect(html).toContain("使用了 2 个来源");
expect(html).toContain("2 次引用");
expect(html).toContain("复制 Paper A 引用");
});
it("renders accessible copied-state labels for copy feedback", () => {
const html = renderPanel(sources, "en-US");
expect(html).toContain("Copy Paper A reference");
expect(html).toContain('data-copied-label="Copied Paper A reference"');
});
});
function renderPanel(
panelSources: CitationSource[],
initialLocale: "en-US" | "zh-CN",
) {
return renderToStaticMarkup(
createElement(
I18nContext.Provider,
{
value: {
locale: initialLocale,
setLocale: () => undefined,
},
},
createElement(CitationSourcesPanel, { sources: panelSources }),
),
);
}