diff --git a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx index e0a381ff52..11fc3cafa5 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo, useRef, useEffect } from 'react'; +import React, { memo, useMemo, useRef, useEffect, lazy, Suspense } from 'react'; import { useRecoilValue } from 'recoil'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; import { useToastContext, useCodeBlockContext } from '~/Providers'; @@ -9,6 +9,16 @@ import useLocalize from '~/hooks/useLocalize'; import { handleDoubleClick } from '~/utils'; import store from '~/store'; +// Loading fallback component for lazy-loaded Mermaid diagrams +const MermaidLoadingFallback = memo(() => { + const localize = useLocalize(); + return ( +
@@ -55,9 +73,17 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
+ const isMermaid = lang === 'mermaid';
if (lang === 'math') {
return children;
+ } else if (isMermaid && typeof children === 'string') {
+ const SandpackMermaidDiagram = lazy(() => import('./SandpackMermaidDiagram'));
+ return (
+ }>
+
+
+ );
} else if (typeof children === 'string' && children.split('\n').length === 1) {
return (
diff --git a/client/src/components/Chat/Messages/Content/SandpackMermaidDiagram.tsx b/client/src/components/Chat/Messages/Content/SandpackMermaidDiagram.tsx
new file mode 100644
index 0000000000..66d879e260
--- /dev/null
+++ b/client/src/components/Chat/Messages/Content/SandpackMermaidDiagram.tsx
@@ -0,0 +1,289 @@
+import React, { memo, useMemo, useEffect } from 'react';
+import { SandpackPreview, SandpackProvider } from '@codesandbox/sandpack-react/unstyled';
+import dedent from 'dedent';
+import { cn } from '~/utils';
+import { sharedOptions } from '~/utils/artifacts';
+
+interface SandpackMermaidDiagramProps {
+ content: string;
+ className?: string;
+}
+
+// Minimal dependencies for Mermaid only
+const mermaidDependencies = {
+ mermaid: '^11.8.1',
+ 'react-zoom-pan-pinch': '^3.7.0',
+};
+
+// Lean mermaid template with inline SVG icons
+const leanMermaidTemplate = dedent`
+import React, { useEffect, useRef, useState } from "react";
+import {
+ TransformWrapper,
+ TransformComponent,
+ ReactZoomPanPinchRef,
+} from "react-zoom-pan-pinch";
+import mermaid from "mermaid";
+
+// Inline SVG icons
+const ZoomInIcon = () => (
+
+);
+
+const ZoomOutIcon = () => (
+
+);
+
+const ResetIcon = () => (
+
+);
+
+interface MermaidDiagramProps {
+ content: string;
+}
+
+const MermaidDiagram: React.FC = ({ content }) => {
+ const mermaidRef = useRef(null);
+ const transformRef = useRef(null);
+ const [isRendered, setIsRendered] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: "default",
+ securityLevel: "loose",
+ flowchart: {
+ useMaxWidth: true,
+ htmlLabels: true,
+ curve: "basis",
+ },
+ });
+
+ const renderDiagram = async () => {
+ if (mermaidRef.current) {
+ try {
+ const id = "mermaid-" + Date.now();
+ const { svg } = await mermaid.render(id, content);
+ mermaidRef.current.innerHTML = svg;
+
+ const svgElement = mermaidRef.current.querySelector("svg");
+ if (svgElement) {
+ svgElement.style.width = "100%";
+ svgElement.style.height = "100%";
+ }
+ setIsRendered(true);
+ setError(null);
+ } catch (err) {
+ console.error("Mermaid rendering error:", err);
+ setError(err.message || "Failed to render diagram");
+ }
+ }
+ };
+
+ renderDiagram();
+ }, [content]);
+
+ const handleZoomIn = () => {
+ if (transformRef.current) {
+ transformRef.current.zoomIn(0.2);
+ }
+ };
+
+ const handleZoomOut = () => {
+ if (transformRef.current) {
+ transformRef.current.zoomOut(0.2);
+ }
+ };
+
+ const handleReset = () => {
+ if (transformRef.current) {
+ transformRef.current.resetTransform();
+ transformRef.current.centerView(1, 0);
+ }
+ };
+
+ if (error) {
+ return (
+
+ Error: {error}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {isRendered && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default MermaidDiagram;
+`;
+
+const wrapLeanMermaidDiagram = (content: string) => {
+ return dedent`
+import React from 'react';
+import MermaidDiagram from './MermaidDiagram';
+
+export default function App() {
+ const content = \`${content.replace(/`/g, '\\`')}\`;
+ return ;
+}
+`;
+};
+
+const getLeanMermaidFiles = (content: string) => {
+ return {
+ '/App.tsx': wrapLeanMermaidDiagram(content),
+ '/MermaidDiagram.tsx': leanMermaidTemplate,
+ };
+};
+
+const SandpackMermaidDiagram = memo(({ content, className }: SandpackMermaidDiagramProps) => {
+ const files = useMemo(() => getLeanMermaidFiles(content), [content]);
+ const sandpackProps = useMemo(
+ () => ({
+ customSetup: {
+ dependencies: mermaidDependencies,
+ },
+ }),
+ [],
+ );
+
+ // Force iframe to respect container height
+ useEffect(() => {
+ const fixIframeHeight = () => {
+ const container = document.querySelector('.sandpack-mermaid-diagram');
+ if (container) {
+ const iframe = container.querySelector('iframe');
+ if (iframe && iframe.style.height && iframe.style.height !== '100%') {
+ iframe.style.height = '100%';
+ iframe.style.minHeight = '100%';
+ }
+ }
+ };
+
+ // Initial fix
+ fixIframeHeight();
+
+ // Fix on any DOM changes
+ const observer = new MutationObserver(fixIframeHeight);
+ const container = document.querySelector('.sandpack-mermaid-diagram');
+ if (container) {
+ observer.observe(container, {
+ attributes: true,
+ childList: true,
+ subtree: true,
+ attributeFilter: ['style'],
+ });
+ }
+
+ return () => observer.disconnect();
+ }, [content]);
+
+ return (
+
+
+
+ );
+});
+
+SandpackMermaidDiagram.displayName = 'SandpackMermaidDiagram';
+
+export default SandpackMermaidDiagram;
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
index 655a5a825a..5d12b39177 100644
--- a/client/src/locales/en/translation.json
+++ b/client/src/locales/en/translation.json
@@ -857,6 +857,7 @@
"com_ui_librechat_code_api_subtitle": "Secure. Multi-language. Input/Output Files.",
"com_ui_librechat_code_api_title": "Run AI Code",
"com_ui_loading": "Loading...",
+ "com_ui_loading_diagram": "Loading diagram...",
"com_ui_locked": "Locked",
"com_ui_logo": "{{0}} Logo",
"com_ui_low": "Low",
diff --git a/client/src/mobile.css b/client/src/mobile.css
index 890053e221..a09466fcbb 100644
--- a/client/src/mobile.css
+++ b/client/src/mobile.css
@@ -371,4 +371,130 @@ p.whitespace-pre-wrap a, li a {
.dark p.whitespace-pre-wrap a, .dark li a {
color: #52a0ff;
-}
\ No newline at end of file
+}
+
+/* .sandpack-mermaid-diagram {
+ display: flex !important;
+ flex-direction: column !important;
+}
+
+.sandpack-mermaid-diagram > div {
+ height: 100% !important;
+ min-height: 100% !important;
+ flex: 1 !important;
+}
+
+.sandpack-mermaid-diagram .sp-wrapper {
+ height: 100% !important;
+ min-height: inherit !important;
+ display: flex !important;
+ flex-direction: column !important;
+}
+
+.sandpack-mermaid-diagram .sp-stack {
+ height: 100% !important;
+ min-height: inherit !important;
+ flex: 1 !important;
+ display: flex !important;
+ flex-direction: column !important;
+}
+
+.sandpack-mermaid-diagram .sp-preview {
+ height: 100% !important;
+ min-height: inherit !important;
+ flex: 1 !important;
+ display: flex !important;
+ flex-direction: column !important;
+}
+
+.sandpack-mermaid-diagram .sp-preview-container {
+ height: 100% !important;
+ min-height: inherit !important;
+ flex: 1 !important;
+ background: transparent !important;
+ display: flex !important;
+ flex-direction: column !important;
+ position: relative !important;
+}
+
+.sandpack-mermaid-diagram .sp-preview-iframe {
+ position: absolute !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ min-height: 100% !important;
+ border: none !important;
+}
+
+.sandpack-mermaid-diagram .sp-preview-actions {
+ display: none !important;
+}
+
+.sandpack-mermaid-diagram .sp-preview-container::after {
+ display: none !important;
+}
+
+.sandpack-mermaid-diagram [style*="height: 346px"] {
+ height: 100% !important;
+}
+
+.sandpack-mermaid-diagram iframe[style*="height"] {
+ height: 100% !important;
+}
+
+.sandpack-mermaid-diagram [style*="height:"] {
+ height: 100% !important;
+ min-height: 100% !important;
+}
+
+.sandpack-mermaid-diagram iframe {
+ height: 100% !important;
+ min-height: 100% !important;
+ position: absolute !important;
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+ width: 100% !important;
+}
+
+.sandpack-mermaid-diagram .sp-stack {
+ max-height: none !important;
+}
+
+.sandpack-mermaid-diagram .sp-wrapper,
+.sandpack-mermaid-diagram .sp-stack,
+.sandpack-mermaid-diagram .sp-preview,
+.sandpack-mermaid-diagram .sp-preview-container {
+ max-height: none !important;
+ height: 100% !important;
+ min-height: 100% !important;
+}
+
+.sandpack-mermaid-diagram .p-4 > div {
+ height: 100% !important;
+ display: flex !important;
+ flex-direction: column !important;
+}
+
+.sandpack-mermaid-diagram .sp-wrapper {
+ height: 100% !important;
+ display: flex !important;
+ flex-direction: column !important;
+}
+
+.sandpack-mermaid-diagram iframe[style*="height"] {
+ height: 100% !important;
+}
+
+.sandpack-mermaid-diagram [style*="height:"] {
+ height: 100% !important;
+}
+
+.sandpack-mermaid-diagram .sp-wrapper,
+.sandpack-mermaid-diagram .sp-stack,
+.sandpack-mermaid-diagram .sp-preview,
+.sandpack-mermaid-diagram .sp-preview-container {
+ max-height: none !important;
+} */
\ No newline at end of file
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 4c4c5a7d65..9f5dfdc83e 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -113,8 +113,8 @@ export default defineConfig(({ command }) => ({
if (id.includes('i18next') || id.includes('react-i18next')) {
return 'i18n';
}
- if (id.includes('lodash')) {
- return 'utilities';
+ if (id.includes('node_modules/lodash-es')) {
+ return 'lodash-es';
}
if (id.includes('date-fns')) {
return 'date-utils';