diff --git a/src/renderer/components/chat/markdownComponents.tsx b/src/renderer/components/chat/markdownComponents.tsx
index 8117d722..03ae8b58 100644
--- a/src/renderer/components/chat/markdownComponents.tsx
+++ b/src/renderer/components/chat/markdownComponents.tsx
@@ -4,7 +4,9 @@ import { api } from '@renderer/api';
import { CopyButton } from '@renderer/components/common/CopyButton';
import { PROSE_BODY } from '@renderer/constants/cssVariables';
-import { MermaidViewer } from './viewers/MermaidViewer';
+const MermaidViewer = React.lazy(() =>
+ import('./viewers/MermaidViewer').then((m) => ({ default: m.MermaidViewer }))
+);
import { highlightSearchInChildren, type SearchContext } from './searchHighlightUtils';
import type { Components } from 'react-markdown';
@@ -129,7 +131,11 @@ export function createMarkdownComponents(searchCtx: SearchContext | null): Compo
const text = content.replace(/\n$/, '');
if (lang === 'mermaid') {
- return
wrapper for mermaid diagrams, with copy button
- pre: ({ children }) => {
- const child = React.Children.only(children) as React.ReactElement;
- if (child?.type === MermaidViewer) {
+ pre: ({ children, node }) => {
+ // Detect mermaid: check if the child has class "language-mermaid"
+ const codeEl = node?.children?.find((c) => 'tagName' in c && c.tagName === 'code') as
+ | { properties?: { className?: string[] } }
+ | undefined;
+ const isMermaid = codeEl?.properties?.className?.includes('language-mermaid');
+ if (isMermaid) {
return children as React.ReactElement;
}
// Extract text from nested children for the copy button
- const extractText = (node: React.ReactNode): string => {
- if (typeof node === 'string') return node;
- if (Array.isArray(node)) return node.map(extractText).join('');
- if (React.isValidElement(node) && node.props) {
- const props = node.props as { children?: React.ReactNode };
+ const extractText = (child: React.ReactNode): string => {
+ if (typeof child === 'string') return child;
+ if (Array.isArray(child)) return child.map(extractText).join('');
+ if (React.isValidElement(child) && child.props) {
+ const props = child.props as { children?: React.ReactNode };
return extractText(props.children);
}
return '';
diff --git a/src/renderer/components/chat/viewers/MarkdownViewer.tsx b/src/renderer/components/chat/viewers/MarkdownViewer.tsx
index 21e9b339..9e82b0a7 100644
--- a/src/renderer/components/chat/viewers/MarkdownViewer.tsx
+++ b/src/renderer/components/chat/viewers/MarkdownViewer.tsx
@@ -33,7 +33,9 @@ import {
highlightSearchInChildren,
type SearchContext,
} from '../searchHighlightUtils';
-import { MermaidViewer } from '../viewers/MermaidViewer';
+const MermaidViewer = React.lazy(() =>
+ import('../viewers/MermaidViewer').then((m) => ({ default: m.MermaidViewer }))
+);
import { highlightLine } from '../viewers/syntaxHighlighter';
// =============================================================================
@@ -162,7 +164,11 @@ function createViewerMarkdownComponents(searchCtx: SearchContext | null): Compon
const text = raw.replace(/\n$/, '');
if (lang === 'mermaid') {
- return ;
+ return (
+ {text} }>
+
+
+ );
}
const lines = text.split('\n');
@@ -192,9 +198,12 @@ function createViewerMarkdownComponents(searchCtx: SearchContext | null): Compon
},
// Code blocks — skip wrapper for mermaid diagrams
- pre: ({ children }) => {
- const child = React.Children.only(children) as React.ReactElement;
- if (child?.type === MermaidViewer) {
+ pre: ({ children, node }) => {
+ const codeEl = node?.children?.find((c) => 'tagName' in c && c.tagName === 'code') as
+ | { properties?: { className?: string[] } }
+ | undefined;
+ const isMermaid = codeEl?.properties?.className?.includes('language-mermaid');
+ if (isMermaid) {
return children as React.ReactElement;
}
return (
diff --git a/src/renderer/components/chat/viewers/MermaidViewer.tsx b/src/renderer/components/chat/viewers/MermaidViewer.tsx
index 02459918..92a73bbe 100644
--- a/src/renderer/components/chat/viewers/MermaidViewer.tsx
+++ b/src/renderer/components/chat/viewers/MermaidViewer.tsx
@@ -11,26 +11,37 @@ import {
} from '@renderer/constants/cssVariables';
import { useTheme } from '@renderer/hooks/useTheme';
import { Code, GitBranch } from 'lucide-react';
-import mermaid from 'mermaid';
+
+import type mermaidApi from 'mermaid';
// =============================================================================
-// Mermaid initialization
+// Mermaid initialization (lazy-loaded to keep it out of the main bundle)
// =============================================================================
+let mermaidInstance: typeof mermaidApi | null = null;
let lastMermaidTheme: 'dark' | 'default' | null = null;
-function ensureMermaidInit(isDark: boolean): void {
+async function getMermaid(): Promise {
+ if (!mermaidInstance) {
+ const mod = await import('mermaid');
+ mermaidInstance = mod.default;
+ }
+ return mermaidInstance;
+}
+
+async function ensureMermaidInit(isDark: boolean): Promise {
+ const m = await getMermaid();
const theme: 'dark' | 'default' = isDark ? 'dark' : 'default';
- if (lastMermaidTheme === theme) {
- return;
+ if (lastMermaidTheme !== theme) {
+ m.initialize({
+ startOnLoad: false,
+ theme,
+ securityLevel: 'strict',
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
+ });
+ lastMermaidTheme = theme;
}
- mermaid.initialize({
- startOnLoad: false,
- theme,
- securityLevel: 'strict',
- fontFamily: 'ui-sans-serif, system-ui, sans-serif',
- });
- lastMermaidTheme = theme;
+ return m;
}
// =============================================================================
@@ -53,9 +64,9 @@ export const MermaidViewer: React.FC = ({ code }) => {
let cancelled = false;
const render = async (): Promise => {
try {
- ensureMermaidInit(isDark);
+ const m = await ensureMermaidInit(isDark);
const id = `mermaid-${uniqueId}`;
- const { svg: rendered } = await mermaid.render(id, code);
+ const { svg: rendered } = await m.render(id, code);
if (!cancelled) {
setSvg(rendered);
setError(null);