mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 10:56:02 +00:00
* 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.
89 lines
2.6 KiB
TypeScript
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 }),
|
|
),
|
|
);
|
|
}
|