Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/i18n/messages/en-US/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 6 additions & 0 deletions frontend/i18n/messages/zh-CN/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@
"thinking": {
"active": "正在思考",
"done": "思考完成"
},
"diagram": {
"downloadDiagram": "下载图表",
"downloadDiagramAsSvg": "导出为 SVG",
"downloadDiagramAsPng": "导出为 PNG",
"downloadDiagramAsMmd": "导出为 MMD 源码"
}
},
"artifacts": {
Expand Down
26 changes: 23 additions & 3 deletions frontend/shared/components/markdown/streamdown-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]",
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -525,6 +540,7 @@ function ThinkingSegmentBlock({
shikiTheme={["github-light", "github-dark"]}
animated={false}
isAnimating={false}
translations={translations}
>
{content}
</Streamdown>
Expand All @@ -546,6 +562,7 @@ function HTMLMarkdownRenderProvider({
components: Components;
plugins: PluginConfig;
}) {
const translations = useStreamdownTranslations();
const renderHTMLMarkdown = React.useCallback(
(source: string) => (
<MarkdownHTMLMarkdownRendererContext.Provider value={null}>
Expand All @@ -563,12 +580,13 @@ function HTMLMarkdownRenderProvider({
shikiTheme={["github-light", "github-dark"]}
animated={false}
isAnimating={false}
translations={translations}
>
{source}
</Streamdown>
</MarkdownHTMLMarkdownRendererContext.Provider>
),
[className, components, plugins],
[className, components, plugins, translations],
);

return (
Expand Down Expand Up @@ -613,6 +631,7 @@ export const StreamdownRender = React.memo(function StreamdownRender({
() => segments.filter((segment): segment is Extract<RenderSegment, { type: "thinking" }> => segment.type === "thinking"),
[segments],
);
const translations = useStreamdownTranslations();
const markdownSegments = React.useMemo(
() => segments.filter((segment): segment is Extract<RenderSegment, { type: "markdown" }> => segment.type === "markdown"),
[segments],
Expand Down Expand Up @@ -683,6 +702,7 @@ export const StreamdownRender = React.memo(function StreamdownRender({
shikiTheme={["github-light", "github-dark"]}
animated={false}
isAnimating={streaming}
translations={translations}
>
{segment.content}
</Streamdown>
Expand Down
Loading