mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-10 09:25:57 +00:00
fix(frontend): fix Mermaid preview failure in historical messages (#3196)
* fix(frontend): render historical mermaid diagrams * fix(frontend): address mermaid review feedback * Stabilize cancel lifecycle test * fix(frontend): handle mermaid fence variants * fix(frontend): normalize mermaid arrow spacing * fix(frontend): handle mermaid CRLF fences * chore: keep mermaid fix frontend-scoped --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import {
|
||||
mockLangGraphAPI,
|
||||
MOCK_RUN_ID,
|
||||
MOCK_THREAD_ID,
|
||||
} from "./utils/mock-api";
|
||||
|
||||
const mermaidContent = `Here is a relationship diagram.
|
||||
|
||||
\`\`\`mermaid
|
||||
flowchart TD
|
||||
A[Lin<br/>protagonist]
|
||||
F[Gu<br/>daughter]
|
||||
A -- "sealed memory" -.-> F
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
test("historical run messages preview labelled dotted Mermaid arrows", async ({
|
||||
page,
|
||||
}) => {
|
||||
mockLangGraphAPI(page, {
|
||||
threads: [
|
||||
{
|
||||
thread_id: MOCK_THREAD_ID,
|
||||
title: "Mermaid history",
|
||||
updated_at: "2026-05-24T04:47:01.123949+00:00",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.route(/\/api\/langgraph\/threads\/[^/]+\/runs(\?|$)/, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
run_id: MOCK_RUN_ID,
|
||||
thread_id: MOCK_THREAD_ID,
|
||||
status: "success",
|
||||
created_at: "2026-05-24T04:46:42.565307+00:00",
|
||||
updated_at: "2026-05-24T04:47:01.123949+00:00",
|
||||
},
|
||||
]),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.route(
|
||||
`**/api/threads/${MOCK_THREAD_ID}/runs/${MOCK_RUN_ID}/messages`,
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
thread_id: MOCK_THREAD_ID,
|
||||
run_id: MOCK_RUN_ID,
|
||||
event_type: "llm.ai.response",
|
||||
category: "message",
|
||||
content: {
|
||||
content: mermaidContent,
|
||||
additional_kwargs: {},
|
||||
response_metadata: {},
|
||||
type: "ai",
|
||||
name: null,
|
||||
id: "lc_run--issue-3193",
|
||||
tool_calls: [],
|
||||
invalid_tool_calls: [],
|
||||
},
|
||||
seq: 720,
|
||||
created_at: "2026-05-24T04:47:01.123949+00:00",
|
||||
metadata: {
|
||||
caller: "lead_agent",
|
||||
content_is_json: true,
|
||||
content_is_dict: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
|
||||
|
||||
await expect(page.getByLabel("Mermaid chart")).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.getByText("Mermaid Error:")).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import { normalizeMermaidMarkdown } from "@/core/streamdown/mermaid";
|
||||
import { preprocessStreamdownMarkdown } from "@/core/streamdown/preprocess";
|
||||
|
||||
test("normalizes labelled dotted arrows inside mermaid fences", () => {
|
||||
const markdown = [
|
||||
"```mermaid",
|
||||
"flowchart TD",
|
||||
' A -- "sealed memory" -.-> F',
|
||||
' B -- "resonance" -.-> A',
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(
|
||||
[
|
||||
"```mermaid",
|
||||
"flowchart TD",
|
||||
' A -. "sealed memory" .-> F',
|
||||
' B -. "resonance" .-> A',
|
||||
"```",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("does not rewrite non-mermaid code fences", () => {
|
||||
const markdown = ["```text", 'A -- "sealed memory" -.-> F', "```"].join("\n");
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(markdown);
|
||||
});
|
||||
|
||||
test("preserves mermaid fence metadata", () => {
|
||||
const markdown = [
|
||||
'```mermaid title="relationships"',
|
||||
'A -- "sealed memory" -.-> F',
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(
|
||||
[
|
||||
'```mermaid title="relationships"',
|
||||
'A -. "sealed memory" .-> F',
|
||||
"```",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes labelled dotted arrows with inconsistent spacing", () => {
|
||||
const markdown = [
|
||||
"```mermaid",
|
||||
'A--"sealed memory"-.->F',
|
||||
'B --"resonance"-.-> A',
|
||||
'C-- "handoff" -.->D',
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(
|
||||
[
|
||||
"```mermaid",
|
||||
'A -. "sealed memory" .-> F',
|
||||
'B -. "resonance" .-> A',
|
||||
'C -. "handoff" .-> D',
|
||||
"```",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes mermaid fences with CRLF line endings", () => {
|
||||
const markdown = ["```mermaid", 'A--"sealed memory"-.->F', "```"].join(
|
||||
"\r\n",
|
||||
);
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(
|
||||
["```mermaid", 'A -. "sealed memory" .-> F', "```"].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves empty mermaid fences", () => {
|
||||
const markdown = ["```mermaid", "```"].join("\n");
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(markdown);
|
||||
});
|
||||
|
||||
test("normalizes labelled dotted arrows inside tilde mermaid fences", () => {
|
||||
const markdown = ["~~~mermaid", 'A -- "sealed memory" -.-> F', "~~~"].join(
|
||||
"\n",
|
||||
);
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(
|
||||
["~~~mermaid", 'A -. "sealed memory" .-> F', "~~~"].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes mermaid fences with longer backtick closing fences", () => {
|
||||
const markdown = ["```mermaid", 'A -- "sealed memory" -.-> F', "````"].join(
|
||||
"\n",
|
||||
);
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(
|
||||
["```mermaid", 'A -. "sealed memory" .-> F', "````"].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes mermaid fences with longer tilde closing fences", () => {
|
||||
const markdown = ["~~~mermaid", 'A -- "sealed memory" -.-> F', "~~~~"].join(
|
||||
"\n",
|
||||
);
|
||||
|
||||
expect(normalizeMermaidMarkdown(markdown)).toBe(
|
||||
["~~~mermaid", 'A -. "sealed memory" .-> F', "~~~~"].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("preprocesses markdown only when mermaid normalization can apply", () => {
|
||||
const textOnlyMarkdown = 'A -- "sealed memory" -.-> F';
|
||||
const plainMermaidMarkdown = ["```mermaid", "A --> F", "```"].join("\n");
|
||||
const labelledMermaidMarkdown = [
|
||||
"```mermaid",
|
||||
'A -- "sealed memory" -.-> F',
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
expect(preprocessStreamdownMarkdown(textOnlyMarkdown)).toBe(textOnlyMarkdown);
|
||||
expect(preprocessStreamdownMarkdown(plainMermaidMarkdown)).toBe(
|
||||
plainMermaidMarkdown,
|
||||
);
|
||||
expect(preprocessStreamdownMarkdown(labelledMermaidMarkdown)).toBe(
|
||||
["```mermaid", 'A -. "sealed memory" .-> F', "```"].join("\n"),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user