mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-09 16:08:31 +00:00
fix: add heading anchors to markdown artifact previews (#3805)
* fix: add markdown heading anchors for artifacts * fix: scope markdown anchors to artifact previews --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
7a6c4a994a
commit
b5cac5e721
6 changed files with 145 additions and 2 deletions
|
|
@ -79,6 +79,7 @@
|
|||
"react-resizable-panels": "^4.4.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shiki": "3.15.0",
|
||||
|
|
|
|||
21
frontend/pnpm-lock.yaml
generated
21
frontend/pnpm-lock.yaml
generated
|
|
@ -188,6 +188,9 @@ importers:
|
|||
rehype-raw:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
rehype-slug:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
|
|
@ -3698,6 +3701,9 @@ packages:
|
|||
hast-util-from-parse5@8.0.3:
|
||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
||||
|
||||
hast-util-heading-rank@3.0.0:
|
||||
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
|
||||
|
||||
hast-util-is-element@3.0.0:
|
||||
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
|
||||
|
||||
|
|
@ -4979,6 +4985,9 @@ packages:
|
|||
rehype-recma@1.0.0:
|
||||
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
|
||||
|
||||
rehype-slug@6.0.0:
|
||||
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
|
||||
|
||||
remark-frontmatter@5.0.0:
|
||||
resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==}
|
||||
|
||||
|
|
@ -9540,6 +9549,10 @@ snapshots:
|
|||
vfile-location: 5.0.3
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-heading-rank@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-is-element@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
|
@ -11238,6 +11251,14 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
rehype-slug@6.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
github-slugger: 2.0.0
|
||||
hast-util-heading-rank: 3.0.0
|
||||
hast-util-to-string: 3.0.1
|
||||
unist-util-visit: 5.1.0
|
||||
|
||||
remark-frontmatter@5.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import { writeTextToClipboard } from "@/core/clipboard";
|
|||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { findToolCallResult } from "@/core/messages/utils";
|
||||
import { installSkill } from "@/core/skills/api";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { SafeStreamdown } from "@/core/streamdown/components";
|
||||
import {
|
||||
canBrowserPreviewFile,
|
||||
|
|
@ -59,6 +58,7 @@ import { useThread } from "../messages/context";
|
|||
import { Tooltip } from "../tooltip";
|
||||
|
||||
import { useArtifacts } from "./context";
|
||||
import { artifactMarkdownPlugins } from "./markdown-preview-plugins";
|
||||
|
||||
const WRITE_FILE_PREVIEW_REFRESH_INTERVAL_MS = 3000;
|
||||
|
||||
|
|
@ -464,7 +464,7 @@ export function ArtifactFilePreview({
|
|||
<div className="size-full px-4">
|
||||
<SafeStreamdown
|
||||
className="size-full"
|
||||
{...streamdownPlugins}
|
||||
{...artifactMarkdownPlugins}
|
||||
components={{ a: ArtifactLink }}
|
||||
>
|
||||
{content ?? ""}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import rehypeSlug from "rehype-slug";
|
||||
|
||||
import { type ClipboardSafeStreamdownProps } from "@/components/ai-elements/streamdown";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
|
||||
const baseRehypePlugins = streamdownPlugins.rehypePlugins ?? [];
|
||||
|
||||
export const artifactMarkdownPlugins = {
|
||||
...streamdownPlugins,
|
||||
rehypePlugins: [
|
||||
...baseRehypePlugins.slice(0, 1),
|
||||
rehypeSlug,
|
||||
...baseRehypePlugins.slice(1),
|
||||
] as ClipboardSafeStreamdownProps["rehypePlugins"],
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ const JSON_ARTIFACT_PATH = "/artifact-fixtures/report.json";
|
|||
const IN_PROGRESS_THREAD_ID = "00000000-0000-0000-0000-000000003119";
|
||||
const COMPLETE_THREAD_ID = "00000000-0000-0000-0000-000000003120";
|
||||
const MARKDOWN_THREAD_ID = "00000000-0000-0000-0000-000000003121";
|
||||
const MARKDOWN_ANCHOR_THREAD_ID = "00000000-0000-0000-0000-000000003123";
|
||||
const JSON_THREAD_ID = "00000000-0000-0000-0000-000000003122";
|
||||
|
||||
function writeFileMessages({
|
||||
|
|
@ -140,6 +141,69 @@ test.describe("Artifact preview stability", () => {
|
|||
await expect(artifactsPanel.getByText("测试内容 1")).toBeVisible();
|
||||
});
|
||||
|
||||
test("scrolls markdown artifact preview to heading anchors", async ({
|
||||
page,
|
||||
}) => {
|
||||
const filler = Array.from(
|
||||
{ length: 40 },
|
||||
(_, index) => `填充段落 ${index + 1}`,
|
||||
).join("\n\n");
|
||||
|
||||
mockLangGraphAPI(page, {
|
||||
threads: [
|
||||
{
|
||||
thread_id: MARKDOWN_ANCHOR_THREAD_ID,
|
||||
title: "Markdown artifact anchor navigation",
|
||||
messages: writeFileMessages({
|
||||
path: MARKDOWN_ARTIFACT_PATH,
|
||||
content: [
|
||||
"# Report",
|
||||
"",
|
||||
"- [概述](#概述)",
|
||||
"",
|
||||
filler,
|
||||
"",
|
||||
"## 概述",
|
||||
"",
|
||||
"目标章节内容",
|
||||
].join("\n"),
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto(`/workspace/chats/${MARKDOWN_ANCHOR_THREAD_ID}`);
|
||||
|
||||
await expect(page.getByText(MARKDOWN_ARTIFACT_PATH)).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.getByText(MARKDOWN_ARTIFACT_PATH).click();
|
||||
|
||||
const artifactsPanel = page.locator("#artifacts");
|
||||
await expect(artifactsPanel.getByText("report.md")).toBeVisible();
|
||||
|
||||
const targetHeading = artifactsPanel.locator("h2#概述");
|
||||
await expect(targetHeading).toHaveCount(1);
|
||||
await artifactsPanel.getByRole("link", { name: "概述" }).click();
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
targetHeading.evaluate((element) => {
|
||||
const panel = document.querySelector("#artifacts");
|
||||
if (!panel) {
|
||||
return false;
|
||||
}
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const headingRect = element.getBoundingClientRect();
|
||||
return (
|
||||
headingRect.top >= panelRect.top &&
|
||||
headingRect.top <= panelRect.bottom
|
||||
);
|
||||
}),
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("renders code view for an in-progress non-preview write artifact", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
|
|||
42
frontend/tests/unit/core/streamdown-plugins.test.ts
Normal file
42
frontend/tests/unit/core/streamdown-plugins.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { expect, test } from "@rstest/core";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { artifactMarkdownPlugins } from "@/components/workspace/artifacts/markdown-preview-plugins";
|
||||
import { ArtifactLink } from "@/components/workspace/citations/artifact-link";
|
||||
import { SafeStreamdown, streamdownPlugins } from "@/core/streamdown";
|
||||
|
||||
function renderArtifactMarkdown(content: string) {
|
||||
return renderToStaticMarkup(
|
||||
createElement(
|
||||
SafeStreamdown,
|
||||
{ ...artifactMarkdownPlugins, components: { a: ArtifactLink } },
|
||||
content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function renderSharedMarkdown(content: string) {
|
||||
return renderToStaticMarkup(
|
||||
createElement(SafeStreamdown, streamdownPlugins, content),
|
||||
);
|
||||
}
|
||||
|
||||
test("adds GitHub-style heading anchors to artifact markdown previews", () => {
|
||||
const html = renderArtifactMarkdown(
|
||||
["[概述](#概述)", "", "## 概述"].join("\n"),
|
||||
);
|
||||
|
||||
expect(html).toContain('href="#%E6%A6%82%E8%BF%B0"');
|
||||
expect(html).toContain('id="概述"');
|
||||
expect(html).not.toContain("target=");
|
||||
});
|
||||
|
||||
test("does not add heading anchors to the shared streamdown plugin config", () => {
|
||||
const html = [
|
||||
renderSharedMarkdown("## Summary"),
|
||||
renderSharedMarkdown("## Summary"),
|
||||
].join("");
|
||||
|
||||
expect(html).not.toContain('id="summary"');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue