diff --git a/frontend/i18n/messages/en-US/chat.json b/frontend/i18n/messages/en-US/chat.json
index b332728f..a10d19a2 100644
--- a/frontend/i18n/messages/en-US/chat.json
+++ b/frontend/i18n/messages/en-US/chat.json
@@ -155,6 +155,12 @@
"thinking": {
"active": "Thinking",
"done": "Thinking complete"
+ },
+ "diagram": {
+ "downloadDiagram": "Download diagram",
+ "downloadDiagramAsSvg": "Download diagram as SVG",
+ "downloadDiagramAsPng": "Download diagram as PNG",
+ "downloadDiagramAsMmd": "Download diagram as MMD"
}
},
"artifacts": {
diff --git a/frontend/i18n/messages/zh-CN/chat.json b/frontend/i18n/messages/zh-CN/chat.json
index c03014bf..5bc349b0 100644
--- a/frontend/i18n/messages/zh-CN/chat.json
+++ b/frontend/i18n/messages/zh-CN/chat.json
@@ -155,6 +155,12 @@
"thinking": {
"active": "正在思考",
"done": "思考完成"
+ },
+ "diagram": {
+ "downloadDiagram": "下载图表",
+ "downloadDiagramAsSvg": "导出为 SVG",
+ "downloadDiagramAsPng": "导出为 PNG",
+ "downloadDiagramAsMmd": "导出为 MMD 源码"
}
},
"artifacts": {
diff --git a/frontend/shared/components/markdown/streamdown-render.tsx b/frontend/shared/components/markdown/streamdown-render.tsx
index eba79b3b..2df55d73 100644
--- a/frontend/shared/components/markdown/streamdown-render.tsx
+++ b/frontend/shared/components/markdown/streamdown-render.tsx
@@ -104,13 +104,26 @@ const STREAMDOWN_CONTROLS = {
},
mermaid: {
copy: true,
- download: false,
+ download: true,
fullscreen: true,
panZoom: true,
},
table: false,
} as const;
+function useStreamdownTranslations() {
+ const t = useTranslations("chat.markdown.diagram");
+ return React.useMemo(
+ () => ({
+ downloadDiagram: t("downloadDiagram"),
+ downloadDiagramAsSvg: t("downloadDiagramAsSvg"),
+ downloadDiagramAsPng: t("downloadDiagramAsPng"),
+ downloadDiagramAsMmd: t("downloadDiagramAsMmd"),
+ }),
+ [t],
+ );
+}
+
const STREAMDOWN_REMEND = {
linkMode: "text-only",
} as const;
@@ -216,7 +229,8 @@ const BASE_MARKDOWN_CLASSNAME = cn(
"[&_[data-streamdown='mermaid']_svg]:mx-auto [&_[data-streamdown='mermaid']_svg]:block [&_[data-streamdown='mermaid']_svg]:h-auto [&_[data-streamdown='mermaid']_svg]:max-h-[280px] [&_[data-streamdown='mermaid']_svg]:max-w-full [&_[data-streamdown='mermaid']_svg]:bg-transparent",
"[&_[data-streamdown='mermaid']>div>div:first-child]:!left-0 [&_[data-streamdown='mermaid']>div>div:first-child]:rounded-none [&_[data-streamdown='mermaid']>div>div:first-child]:border-0 [&_[data-streamdown='mermaid']>div>div:first-child]:bg-transparent [&_[data-streamdown='mermaid']>div>div:first-child]:p-0 [&_[data-streamdown='mermaid']>div>div:first-child]:shadow-none [&_[data-streamdown='mermaid']>div>div:first-child]:backdrop-blur-none",
"[&_[data-streamdown='mermaid-block-actions']]:gap-2 [&_[data-streamdown='mermaid-block-actions']]:border-0 [&_[data-streamdown='mermaid-block-actions']]:rounded-none [&_[data-streamdown='mermaid-block-actions']]:bg-transparent [&_[data-streamdown='mermaid-block-actions']]:p-0 [&_[data-streamdown='mermaid-block-actions']]:shadow-none [&_[data-streamdown='mermaid-block-actions']]:backdrop-blur-none",
- "[&_[data-streamdown='mermaid-block-actions']_button]:border-0 [&_[data-streamdown='mermaid-block-actions']_button]:bg-transparent [&_[data-streamdown='mermaid-block-actions']_button]:shadow-none [&_[data-streamdown='mermaid-block-actions']_button:hover]:bg-foreground/[0.04] [&_[data-streamdown='mermaid-block-actions']_button:hover]:text-foreground",
+ "[&_[data-streamdown='mermaid-block-actions']>button]:border-0 [&_[data-streamdown='mermaid-block-actions']>button]:bg-transparent [&_[data-streamdown='mermaid-block-actions']>button]:shadow-none [&_[data-streamdown='mermaid-block-actions']>button:hover]:bg-foreground/[0.04] [&_[data-streamdown='mermaid-block-actions']>button:hover]:text-foreground",
+ "[&_[data-streamdown='mermaid-block-actions']>div>button]:border-0 [&_[data-streamdown='mermaid-block-actions']>div>button]:bg-transparent [&_[data-streamdown='mermaid-block-actions']>div>button]:shadow-none [&_[data-streamdown='mermaid-block-actions']>div>button:hover]:bg-foreground/[0.04] [&_[data-streamdown='mermaid-block-actions']>div>button:hover]:text-foreground",
"[&_[data-streamdown='mermaid-block-actions']_svg]:size-3",
"[&_[data-streamdown='mermaid-block']_button>svg]:size-3",
"[&_code:not(pre_code)]:rounded-md [&_code:not(pre_code)]:bg-foreground/[0.05] [&_code:not(pre_code)]:px-1.5 [&_code:not(pre_code)]:py-0.5 [&_code:not(pre_code)]:font-mono [&_code:not(pre_code)]:text-[0.85em] [&_code:not(pre_code)]:text-primary [&_code:not(pre_code)]:whitespace-pre-wrap [&_code:not(pre_code)]:break-words [&_code:not(pre_code)]:[overflow-wrap:anywhere]",
@@ -451,6 +465,7 @@ function ThinkingSegmentBlock({
streaming: boolean;
}) {
const t = useTranslations("chat.markdown.thinking");
+ const translations = useStreamdownTranslations();
const active = streaming || incomplete;
const [accordionValue, setAccordionValue] = React.useState(() => (active ? "thinking" : ""));
const wasActiveRef = React.useRef(active);
@@ -525,6 +540,7 @@ function ThinkingSegmentBlock({
shikiTheme={["github-light", "github-dark"]}
animated={false}
isAnimating={false}
+ translations={translations}
>
{content}
@@ -546,6 +562,7 @@ function HTMLMarkdownRenderProvider({
components: Components;
plugins: PluginConfig;
}) {
+ const translations = useStreamdownTranslations();
const renderHTMLMarkdown = React.useCallback(
(source: string) => (
@@ -563,12 +580,13 @@ function HTMLMarkdownRenderProvider({
shikiTheme={["github-light", "github-dark"]}
animated={false}
isAnimating={false}
+ translations={translations}
>
{source}
),
- [className, components, plugins],
+ [className, components, plugins, translations],
);
return (
@@ -613,6 +631,7 @@ export const StreamdownRender = React.memo(function StreamdownRender({
() => segments.filter((segment): segment is Extract => segment.type === "thinking"),
[segments],
);
+ const translations = useStreamdownTranslations();
const markdownSegments = React.useMemo(
() => segments.filter((segment): segment is Extract => segment.type === "markdown"),
[segments],
@@ -683,6 +702,7 @@ export const StreamdownRender = React.memo(function StreamdownRender({
shikiTheme={["github-light", "github-dark"]}
animated={false}
isAnimating={streaming}
+ translations={translations}
>
{segment.content}