Compare commits

...

3 Commits

9 changed files with 932 additions and 7 deletions

3
.gitignore vendored
View File

@@ -56,6 +56,7 @@ bower_components/
.clineignore
.cursor
.aider*
CLAUDE.md
# Floobits
.floo
@@ -124,4 +125,4 @@ helm/**/.values.yaml
!/client/src/@types/i18next.d.ts
# SAML Idp cert
*.cert
*.cert

View File

@@ -72,7 +72,7 @@
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"librechat-data-provider": "*",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lucide-react": "^0.394.0",
"match-sorter": "^6.3.4",
"micromark-extension-llm-math": "^3.1.0",

View File

@@ -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 (
<div className="my-4 rounded-lg border border-border-light bg-surface-primary p-4 text-center text-text-secondary dark:border-border-heavy dark:bg-surface-primary-alt">
{localize('com_ui_loading_diagram')}
</div>
);
});
type TCodeProps = {
inline?: boolean;
className?: string;
@@ -23,6 +33,7 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
const isMath = lang === 'math';
const isMermaid = lang === 'mermaid';
const isSingleLine = typeof children === 'string' && children.split('\n').length === 1;
const { getNextIndex, resetCounter } = useCodeBlockContext();
@@ -34,6 +45,13 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
if (isMath) {
return <>{children}</>;
} else if (isMermaid && typeof children === 'string') {
const SandpackMermaidDiagram = lazy(() => import('./SandpackMermaidDiagram'));
return (
<Suspense fallback={<MermaidLoadingFallback />}>
<SandpackMermaidDiagram content={children} />
</Suspense>
);
} else if (isSingleLine) {
return (
<code onDoubleClick={handleDoubleClick} className={className}>
@@ -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 (
<Suspense fallback={<MermaidLoadingFallback />}>
<SandpackMermaidDiagram content={children} />
</Suspense>
);
} else if (typeof children === 'string' && children.split('\n').length === 1) {
return (
<code onDoubleClick={handleDoubleClick} className={className}>

View File

@@ -0,0 +1,455 @@
import React, {
useLayoutEffect,
useState,
memo,
useContext,
useMemo,
useCallback,
useRef,
} from 'react';
import { useTranslation } from 'react-i18next';
import DOMPurify from 'dompurify';
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
import { cn } from '~/utils';
import { ThemeContext, isDark } from '~/hooks/ThemeContext';
import { ClipboardIcon, CheckIcon, ZoomIn, ZoomOut, RotateCcw } from 'lucide-react';
interface InlineMermaidProps {
content: string;
className?: string;
}
const InlineMermaidDiagram = memo(({ content, className }: InlineMermaidProps) => {
const { t } = useTranslation();
const [svgContent, setSvgContent] = useState<string>('');
const [isRendered, setIsRendered] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isCopied, setIsCopied] = useState(false);
const [wasAutoCorrected, setWasAutoCorrected] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { theme } = useContext(ThemeContext);
const isDarkMode = isDark(theme);
const transformRef = useRef<ReactZoomPanPinchRef>(null);
const diagramKey = useMemo(
() => `${content.trim()}-${isDarkMode ? 'dark' : 'light'}`,
[content, isDarkMode],
);
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(content);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
console.error('Failed to copy diagram content:', err);
}
}, [content]);
const handleZoomIn = useCallback(() => {
if (transformRef.current) {
transformRef.current.zoomIn(0.2);
}
}, []);
const handleZoomOut = useCallback(() => {
if (transformRef.current) {
transformRef.current.zoomOut(0.2);
}
}, []);
const handleResetZoom = useCallback(() => {
if (transformRef.current) {
transformRef.current.resetTransform();
transformRef.current.centerView(1, 0);
}
}, []);
// Memoized to prevent re-renders when content/theme changes
const fixCommonSyntaxIssues = useMemo(() => {
return (text: string) => {
let fixed = text;
fixed = fixed.replace(/--\s+>/g, '-->');
fixed = fixed.replace(/--\s+\|/g, '--|');
fixed = fixed.replace(/\|\s+-->/g, '|-->');
fixed = fixed.replace(/\[([^[\]]*)"([^[\]]*)"([^[\]]*)\]/g, '[$1$2$3]');
fixed = fixed.replace(/subgraph([A-Za-z])/g, 'subgraph $1');
return fixed;
};
}, []);
const handleTryFix = useCallback(() => {
const fixedContent = fixCommonSyntaxIssues(content);
if (fixedContent !== content) {
// Currently just copies the fixed version to clipboard
navigator.clipboard.writeText(fixedContent).then(() => {
setError(t('com_mermaid_fix_copied'));
});
}
}, [content, fixCommonSyntaxIssues, t]);
// Use ref to track timeout to prevent stale closures
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
useLayoutEffect(() => {
let isCancelled = false;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
// Clear previous SVG content
setSvgContent('');
const cleanContent = content.trim();
setError(null);
setWasAutoCorrected(false);
setIsRendered(false);
setIsLoading(false);
if (!cleanContent) {
setError(t('com_mermaid_error_no_content'));
return;
}
// Debounce rendering to avoid flickering during rapid content changes
timeoutRef.current = setTimeout(() => {
if (!isCancelled) {
renderDiagram();
}
}, 300);
async function renderDiagram() {
if (isCancelled) return;
try {
if (
!cleanContent.match(
/^(graph|flowchart|sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|gitgraph|mindmap|timeline|quadrant|block-beta|sankey|xychart|gitgraph)/i,
)
) {
if (!isCancelled) {
setError(t('com_mermaid_error_invalid_type'));
setWasAutoCorrected(false);
}
return;
}
// Dynamic import to reduce bundle size
setIsLoading(true);
const mermaid = await import('mermaid').then((m) => m.default);
if (isCancelled) {
return;
}
// Initialize with error suppression to avoid console spam
mermaid.initialize({
startOnLoad: false,
theme: isDarkMode ? 'dark' : 'default',
securityLevel: 'loose',
logLevel: 'fatal',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
},
suppressErrorRendering: true,
});
let result;
let contentToRender = cleanContent;
try {
const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
result = await mermaid.render(id, contentToRender);
} catch (_renderError) {
const fixedContent = fixCommonSyntaxIssues(cleanContent);
if (fixedContent !== cleanContent) {
try {
const fixedId = `mermaid-fixed-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
result = await mermaid.render(fixedId, fixedContent);
contentToRender = fixedContent;
setWasAutoCorrected(true);
} catch (_fixedRenderError) {
if (!isCancelled) {
setError(t('com_mermaid_error_invalid_syntax_auto_correct'));
setWasAutoCorrected(false);
setIsLoading(false);
}
return;
}
} else {
if (!isCancelled) {
setError(t('com_mermaid_error_invalid_syntax'));
setWasAutoCorrected(false);
setIsLoading(false);
}
return;
}
}
// Check if component was unmounted during async render
if (isCancelled) {
return;
}
if (result && result.svg) {
let processedSvg = result.svg;
// Enhance SVG for better zoom/pan interaction
processedSvg = processedSvg.replace(
'<svg',
'<svg style="width: 100%; height: auto;" preserveAspectRatio="xMidYMid meet"',
);
// Sanitize SVG content to prevent XSS attacks
const sanitizedSvg = DOMPurify.sanitize(processedSvg, {
USE_PROFILES: { svg: true, svgFilters: true },
ADD_TAGS: ['foreignObject'],
ADD_ATTR: ['preserveAspectRatio'],
FORBID_TAGS: ['script', 'object', 'embed', 'iframe'],
FORBID_ATTR: ['onerror', 'onload', 'onclick'],
});
if (!isCancelled) {
setSvgContent(sanitizedSvg);
setIsRendered(true);
setIsLoading(false);
}
} else {
if (!isCancelled) {
setError(t('com_mermaid_error_no_svg'));
setWasAutoCorrected(false);
setIsLoading(false);
}
}
} catch (err) {
console.error('Mermaid rendering error:', err);
if (!isCancelled) {
const errorMessage =
err instanceof Error
? err.message
: t('com_mermaid_error_rendering_failed', 'Failed to render diagram');
setError(t('com_mermaid_error_rendering_failed', { '0': errorMessage }));
setWasAutoCorrected(false);
setIsLoading(false);
}
}
}
return () => {
isCancelled = true;
};
}, [diagramKey, content, isDarkMode, fixCommonSyntaxIssues, t]);
useLayoutEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, []);
if (error) {
const fixedContent = fixCommonSyntaxIssues(content);
const canTryFix = fixedContent !== content;
return (
<div
className={cn(
'my-4 overflow-auto rounded-lg border border-red-300 bg-red-50',
'dark:border-red-700 dark:bg-red-900/20',
className,
)}
>
<div className="p-4 text-red-600 dark:text-red-400">
<div className="flex items-start justify-between">
<div className="flex-1">
<strong>{t('com_mermaid_error')}</strong> {error}
{canTryFix && (
<div className={cn('mt-2 text-sm text-red-500 dark:text-red-300')}>
💡 {t('com_mermaid_error_fixes_detected')}
</div>
)}
</div>
<div className="ml-4 flex gap-2">
{canTryFix && (
<button
onClick={handleTryFix}
className={cn(
'rounded border px-3 py-1 text-xs transition-colors',
'border-blue-300 bg-blue-100 text-blue-700 hover:bg-blue-200',
'dark:border-blue-700 dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800',
)}
title={t('com_mermaid_copy_potential_fix')}
>
{t('com_mermaid_try_fix')}
</button>
)}
<button
onClick={handleCopy}
className={cn(
'rounded border px-3 py-1 text-xs transition-colors',
'border-gray-300 bg-gray-100 text-gray-700 hover:bg-gray-200',
'dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700',
)}
title={t('com_mermaid_copy_code')}
>
{isCopied ? `${t('com_mermaid_copied')}` : t('com_mermaid_copy')}
</button>
</div>
</div>
</div>
<div className="p-4 pt-0">
<pre className="overflow-x-auto rounded bg-gray-100 p-2 text-sm dark:bg-gray-800">
<code className="language-mermaid">{content}</code>
</pre>
{canTryFix && (
<div className="mt-3 rounded border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950">
<div className={cn('mb-2 text-sm font-medium text-blue-800 dark:text-blue-200')}>
{t('com_mermaid_suggested_fix')}
</div>
<pre className="overflow-x-auto rounded border bg-white p-2 text-sm dark:bg-gray-800">
<code className="language-mermaid">{fixedContent}</code>
</pre>
</div>
)}
</div>
</div>
);
}
return (
<div
key={diagramKey}
className={cn(
'relative my-4 overflow-auto rounded-lg border border-border-light bg-surface-primary',
'dark:border-border-heavy dark:bg-surface-primary-alt',
className,
)}
>
{isRendered && wasAutoCorrected && (
<div
className={cn(
'absolute left-2 top-2 z-10 rounded-md px-2 py-1 text-xs',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
'border border-yellow-300 dark:border-yellow-700',
'shadow-sm',
)}
>
{t('com_mermaid_auto_fixed')}
</div>
)}
{isRendered && svgContent && (
<div className="absolute right-2 top-2 z-10 flex gap-1">
<button
onClick={handleZoomIn}
className={cn(
'rounded-md p-2 transition-all duration-200',
'hover:bg-surface-hover active:bg-surface-active',
'text-text-secondary hover:text-text-primary',
'border border-border-light dark:border-border-heavy',
'bg-surface-primary dark:bg-surface-primary-alt',
'shadow-sm hover:shadow-md',
)}
title={t('com_mermaid_zoom_in')}
>
<ZoomIn className="h-4 w-4" />
</button>
<button
onClick={handleZoomOut}
className={cn(
'rounded-md p-2 transition-all duration-200',
'hover:bg-surface-hover active:bg-surface-active',
'text-text-secondary hover:text-text-primary',
'border border-border-light dark:border-border-heavy',
'bg-surface-primary dark:bg-surface-primary-alt',
'shadow-sm hover:shadow-md',
)}
title={t('com_mermaid_zoom_out')}
>
<ZoomOut className="h-4 w-4" />
</button>
<button
onClick={handleResetZoom}
className={cn(
'rounded-md p-2 transition-all duration-200',
'hover:bg-surface-hover active:bg-surface-active',
'text-text-secondary hover:text-text-primary',
'border border-border-light dark:border-border-heavy',
'bg-surface-primary dark:bg-surface-primary-alt',
'shadow-sm hover:shadow-md',
)}
title={t('com_mermaid_reset_zoom')}
>
<RotateCcw className="h-4 w-4" />
</button>
<button
onClick={handleCopy}
className={cn(
'rounded-md p-2 transition-all duration-200',
'hover:bg-surface-hover active:bg-surface-active',
'text-text-secondary hover:text-text-primary',
'border border-border-light dark:border-border-heavy',
'bg-surface-primary dark:bg-surface-primary-alt',
'shadow-sm hover:shadow-md',
)}
title={t('com_mermaid_copy_code')}
>
{isCopied ? (
<CheckIcon className="h-4 w-4 text-green-500" />
) : (
<ClipboardIcon className="h-4 w-4" />
)}
</button>
</div>
)}
<div className="p-4">
{(isLoading || !isRendered) && (
<div className="animate-pulse text-center text-text-secondary">
{t('com_mermaid_rendering')}
</div>
)}
{isRendered && svgContent && (
<TransformWrapper
ref={transformRef}
initialScale={1}
minScale={0.1}
maxScale={4}
limitToBounds={false}
centerOnInit={true}
wheel={{ step: 0.1 }}
panning={{ velocityDisabled: true }}
alignmentAnimation={{ disabled: true }}
>
<TransformComponent
wrapperStyle={{
width: '100%',
height: 'auto',
minHeight: '200px',
maxHeight: '600px',
overflow: 'hidden',
}}
>
<div
className="mermaid-container flex min-h-[200px] items-center justify-center"
dangerouslySetInnerHTML={{ __html: svgContent }}
/>
</TransformComponent>
</TransformWrapper>
)}
</div>
</div>
);
});
InlineMermaidDiagram.displayName = 'InlineMermaidDiagram';
export default InlineMermaidDiagram;

View File

@@ -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 = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
<line x1="11" y1="8" x2="11" y2="14"/>
<line x1="8" y1="11" x2="14" y2="11"/>
</svg>
);
const ZoomOutIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
<line x1="8" y1="11" x2="14" y2="11"/>
</svg>
);
const ResetIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="1 4 1 10 7 10"/>
<polyline points="23 20 23 14 17 14"/>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
</svg>
);
interface MermaidDiagramProps {
content: string;
}
const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ content }) => {
const mermaidRef = useRef<HTMLDivElement>(null);
const transformRef = useRef<ReactZoomPanPinchRef>(null);
const [isRendered, setIsRendered] = useState(false);
const [error, setError] = useState<string | null>(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 (
<div style={{ padding: '16px', color: '#ef4444', backgroundColor: '#fee2e2', borderRadius: '8px', border: '1px solid #fecaca' }}>
<strong>Error:</strong> {error}
</div>
);
}
return (
<div style={{ position: 'relative', height: '100%', width: '100%', backgroundColor: '#f9fafb' }}>
<TransformWrapper
ref={transformRef}
initialScale={1}
minScale={0.1}
maxScale={4}
wheel={{ step: 0.1 }}
centerOnInit={true}
>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%",
}}
>
<div
ref={mermaidRef}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '300px',
padding: '20px',
}}
/>
</TransformComponent>
</TransformWrapper>
{isRendered && (
<div style={{ position: 'absolute', bottom: '8px', right: '8px', display: 'flex', gap: '8px' }}>
<button
onClick={handleZoomIn}
style={{
padding: '8px',
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title="Zoom in"
>
<ZoomInIcon />
</button>
<button
onClick={handleZoomOut}
style={{
padding: '8px',
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title="Zoom out"
>
<ZoomOutIcon />
</button>
<button
onClick={handleReset}
style={{
padding: '8px',
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title="Reset zoom"
>
<ResetIcon />
</button>
</div>
)}
</div>
);
};
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 <MermaidDiagram content={content} />;
}
`;
};
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 (
<SandpackProvider files={files} options={sharedOptions} template="react-ts" {...sandpackProps}>
<SandpackPreview
showOpenInCodeSandbox={false}
showRefreshButton={false}
showSandpackErrorOverlay={true}
/>
</SandpackProvider>
);
});
SandpackMermaidDiagram.displayName = 'SandpackMermaidDiagram';
export default SandpackMermaidDiagram;

View File

@@ -300,6 +300,26 @@
"com_error_moderation": "It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We're unable to proceed with this specific topic. If you have any other questions or topics you'd like to explore, please edit your message, or create a new conversation.",
"com_error_no_base_url": "No base URL found. Please provide one and try again.",
"com_error_no_user_key": "No key found. Please provide a key and try again.",
"com_mermaid_auto_fixed": "Auto-fixed",
"com_mermaid_copy": "Copy",
"com_mermaid_copy_potential_fix": "Copy potential fix to clipboard",
"com_mermaid_copy_code": "Copy mermaid code",
"com_mermaid_copied": "Copied",
"com_mermaid_error": "Mermaid Error:",
"com_mermaid_error_fixes_detected": "Potential fixes detected: spacing issues in arrows or labels",
"com_mermaid_error_invalid_syntax": "Invalid diagram syntax - check arrow formatting and node labels",
"com_mermaid_error_invalid_syntax_auto_correct": "Invalid diagram syntax - syntax errors found but unable to auto-correct",
"com_mermaid_error_invalid_type": "Invalid Mermaid syntax - diagram must start with a valid diagram type (flowchart, graph, sequenceDiagram, etc.)",
"com_mermaid_error_no_content": "No diagram content provided",
"com_mermaid_error_no_svg": "No SVG generated - rendering failed unexpectedly",
"com_mermaid_error_rendering_failed": "Rendering failed: {{0}}",
"com_mermaid_fix_copied": "Potential fix copied to clipboard. Common issues found and corrected.",
"com_mermaid_rendering": "Rendering diagram...",
"com_mermaid_suggested_fix": "Suggested Fix:",
"com_mermaid_try_fix": "Try Fix",
"com_mermaid_zoom_in": "Zoom in",
"com_mermaid_zoom_out": "Zoom out",
"com_mermaid_reset_zoom": "Reset zoom",
"com_files_filter": "Filter files...",
"com_files_no_results": "No results.",
"com_files_number_selected": "{{0}} of {{1}} items(s) selected",
@@ -837,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",

View File

@@ -371,4 +371,130 @@ p.whitespace-pre-wrap a, li a {
.dark p.whitespace-pre-wrap a, .dark li a {
color: #52a0ff;
}
}
/* .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;
} */

View File

@@ -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';
@@ -231,6 +231,7 @@ export default defineConfig(({ command }) => ({
resolve: {
alias: {
'~': path.join(__dirname, 'src/'),
lodash: 'lodash-es',
$fonts: path.resolve(__dirname, 'public/fonts'),
'micromark-extension-math': 'micromark-extension-llm-math',
},

8
package-lock.json generated
View File

@@ -2511,7 +2511,7 @@
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"librechat-data-provider": "*",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lucide-react": "^0.394.0",
"match-sorter": "^6.3.4",
"micromark-extension-llm-math": "^3.1.0",
@@ -35295,6 +35295,12 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",