(fix/mermaid loading): load only the diagrams which stream

This commit is contained in:
ManishMadan2882
2025-05-06 15:16:14 +05:30
parent 72e51bb072
commit f37ca95c10
4 changed files with 73 additions and 65 deletions

View File

@@ -10,6 +10,7 @@ import { useDarkTheme } from '../hooks';
const MermaidRenderer: React.FC<MermaidRendererProps> = ({
code,
isLoading,
}) => {
const [isDarkTheme] = useDarkTheme();
const diagramId = useRef(`mermaid-${crypto.randomUUID()}`);
@@ -21,70 +22,70 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
const [hoverPosition, setHoverPosition] = useState<{ x: number, y: number } | null>(null);
const [isHovering, setIsHovering] = useState<boolean>(false);
const handleMouseMove = (event: React.MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width;
const y = (event.clientY - rect.top) / rect.height;
setHoverPosition({ x, y });
};
const handleMouseEnter = () => setIsHovering(true);
const handleMouseLeave = () => {
setIsHovering(false);
setHoverPosition(null);
};
const getTransformOrigin = () => {
if (!hoverPosition) return 'center center';
return `${hoverPosition.x * 100}% ${hoverPosition.y * 100}%`;
};
useEffect(() => {
if (status === 'loading' || !code) return;
if ((isLoading !== undefined ? isLoading : status === 'loading') || !code) return;
mermaid.initialize({
startOnLoad: true,
theme: isDarkTheme ? 'dark' : 'default',
securityLevel: 'loose',
suppressErrorRendering: true,
});
const renderDiagram = async (): Promise<void> => {
try {
await mermaid.parse(code); //throws syntax errors
const element = document.getElementById(diagramId.current);
if (element) {
element.removeAttribute('data-processed');
mermaid.contentLoaded();
const svgElement = element.querySelector('svg');
if (svgElement) {
svgElement.setAttribute('width', '100%');
svgElement.setAttribute('height', 'auto');
svgElement.style.maxWidth = '100%';
svgElement.style.width = '100%';
svgElement.removeAttribute('viewBox');
}
setError(null);
}
} catch (err) {
setError(
`Failed to render Mermaid diagram: ${err instanceof Error ? err.message : String(err)}`
);
}
};
renderDiagram();
}, [code, isDarkTheme]);
}, [code, isDarkTheme, isLoading]);
useEffect(() => {
@@ -109,13 +110,13 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
if (!element) return;
const svgElement = element.querySelector('svg');
if (!svgElement) return;
const svgClone = svgElement.cloneNode(true) as SVGElement;
if (!svgClone.hasAttribute('xmlns')) {
svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
if (!svgClone.hasAttribute('width') || !svgClone.hasAttribute('height')) {
const viewBox = svgClone.getAttribute('viewBox')?.split(' ') || [];
if (viewBox.length === 4) {
@@ -123,15 +124,15 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
svgClone.setAttribute('height', viewBox[3]);
}
}
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgClone);
const svgBlob = new Blob(
[`<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n${svgString}`],
[`<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n${svgString}`],
{ type: 'image/svg+xml' }
);
const url = URL.createObjectURL(svgBlob);
const link = document.createElement('a');
link.href = url;
@@ -145,19 +146,19 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
const downloadPng = (): void => {
const element = document.getElementById(diagramId.current);
if (!element) return;
const svgElement = element.querySelector('svg');
if (!svgElement) return;
const svgClone = svgElement.cloneNode(true) as SVGElement;
if (!svgClone.hasAttribute('xmlns')) {
svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
let width = parseInt(svgClone.getAttribute('width') || '0');
let height = parseInt(svgClone.getAttribute('height') || '0');
if (!width || !height) {
const viewBox = svgClone.getAttribute('viewBox')?.split(' ') || [];
if (viewBox.length === 4) {
@@ -172,30 +173,30 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
svgClone.setAttribute('height', height.toString());
}
}
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgClone);
const svgBase64 = btoa(unescape(encodeURIComponent(svgString)));
const dataUrl = `data:image/svg+xml;base64,${svgBase64}`;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function(): void {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Could not get canvas context');
return;
}
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
try {
const pngUrl = canvas.toDataURL('image/png');
const link = document.createElement('a');
@@ -210,7 +211,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
downloadSvg();
}
};
img.src = dataUrl;
};
@@ -225,16 +226,17 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const downloadOptions = [
{ label: 'Download as SVG', action: downloadSvg },
{ label: 'Download as PNG', action: downloadPng },
{ label: 'Download as MMD', action: downloadMmd },
];
const showDiagramOptions = status !== 'loading' && !error;
const errorRender = status !== 'loading' && error;
const isCurrentlyLoading = isLoading !== undefined ? isLoading : status === 'loading';
const showDiagramOptions = !isCurrentlyLoading && !error;
const errorRender = !isCurrentlyLoading && error;
@@ -246,7 +248,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
</span>
<div className="flex items-center gap-2">
<CopyButton text={String(code).replace(/\n$/, '')} />
{showDiagramOptions && (
<div className="relative" ref={downloadMenuRef}>
<button
@@ -277,7 +279,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
)}
</div>
)}
{showDiagramOptions && (
<button
onClick={() => setShowCode(!showCode)}
@@ -293,8 +295,8 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
)}
</div>
</div>
{status === 'loading' ? (
{isCurrentlyLoading ? (
<div className="p-4 bg-white dark:bg-eerie-black flex justify-center items-center">
<div className="text-sm text-gray-500 dark:text-gray-400">
Loading diagram...
@@ -308,24 +310,24 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
</div>
) : (
<>
<div
<div
ref={containerRef}
className=" no-scrollbar p-4 block w-full bg-white dark:bg-eerie-black "
style={{
style={{
overflow: 'auto',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
width: '100%',
}}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<pre
className="mermaid select-none w-full"
<pre
className="mermaid select-none w-full"
id={diagramId.current}
style={{
style={{
transform: isHovering ? `scale(2)` : `scale(1)`,
transformOrigin: getTransformOrigin(),
transition: 'transform 0.2s ease',
@@ -338,7 +340,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
{code}
</pre>
</div>
{showCode && (
<div className="border-t border-light-silver dark:border-raisin-black">
<div className="p-2 bg-platinum dark:bg-eerie-black-2">

View File

@@ -26,4 +26,5 @@ export type InputProps = {
export type MermaidRendererProps = {
code: string;
isLoading?: boolean;
};

View File

@@ -53,6 +53,7 @@ const ConversationBubble = forwardRef<
toolCalls?: ToolCallsType[];
retryBtn?: React.ReactElement;
questionNumber?: number;
isStreaming?: boolean;
handleUpdatedQuestionSubmission?: (
updatedquestion?: string,
updated?: boolean,
@@ -71,6 +72,7 @@ const ConversationBubble = forwardRef<
toolCalls,
retryBtn,
questionNumber,
isStreaming,
handleUpdatedQuestionSubmission,
},
ref,
@@ -195,29 +197,29 @@ const ConversationBubble = forwardRef<
};
const processMarkdownContent = (content: string) => {
const processedContent = preprocessLaTeX(content);
const contentSegments: Array<{type: 'text' | 'mermaid', content: string}> = [];
let lastIndex = 0;
const regex = /```mermaid\n([\s\S]*?)```/g;
let match;
while ((match = regex.exec(processedContent)) !== null) {
const textBefore = processedContent.substring(lastIndex, match.index);
if (textBefore) {
contentSegments.push({ type: 'text', content: textBefore });
}
contentSegments.push({ type: 'mermaid', content: match[1].trim() });
lastIndex = match.index + match[0].length;
}
const textAfter = processedContent.substring(lastIndex);
if (textAfter) {
contentSegments.push({ type: 'text', content: textAfter });
}
return contentSegments;
};
bubble = (
@@ -404,7 +406,7 @@ const ConversationBubble = forwardRef<
const { children, className, node, ref, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
return match ? (
<div className="group relative rounded-[14px] overflow-hidden border border-light-silver dark:border-raisin-black">
<div className="flex justify-between items-center px-2 py-1 bg-platinum dark:bg-eerie-black-2">
@@ -491,6 +493,7 @@ const ConversationBubble = forwardRef<
<div className="my-4 w-full" style={{ minWidth: '100%' }}>
<MermaidRenderer
code={segment.content}
isLoading={isStreaming}
/>
</div>
)}
@@ -505,7 +508,7 @@ const ConversationBubble = forwardRef<
{message && (
<div className="my-2 ml-2 flex justify-start">
<div
className={`relative mr-2 block items-center justify-center lg:invisible
className={`relative mr-2 block items-center justify-center lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : 'hidden'}`}
>
<div>
@@ -513,7 +516,7 @@ const ConversationBubble = forwardRef<
</div>
</div>
<div
className={`relative mr-2 block items-center justify-center lg:invisible
className={`relative mr-2 block items-center justify-center lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : 'hidden'}`}
>
<div>
@@ -544,7 +547,7 @@ const ConversationBubble = forwardRef<
}`}
>
<Like
className={`cursor-pointer
className={`cursor-pointer
${
isLikeClicked || feedback === 'LIKE'
? 'fill-white-3000 stroke-purple-30 dark:fill-transparent'

View File

@@ -40,7 +40,7 @@ export default function ConversationMessages({
const conversationRef = useRef<HTMLDivElement>(null);
const atLast = useRef(true);
const [eventInterrupt, setEventInterrupt] = useState(false);
const handleUserInterruption = () => {
if (!eventInterrupt && status === 'loading') {
setEventInterrupt(true);
@@ -54,7 +54,7 @@ export default function ConversationMessages({
setTimeout(() => {
if (!conversationRef?.current) return;
if (status === 'idle' || !queries[queries.length - 1]?.response) {
conversationRef.current.scrollTo({
behavior: 'smooth',
@@ -93,6 +93,7 @@ export default function ConversationMessages({
const prepResponseView = (query: Query, index: number) => {
let responseView;
if (query.thought || query.response) {
const isCurrentlyStreaming = status === 'loading' && index === queries.length - 1;
responseView = (
<ConversationBubble
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
@@ -103,6 +104,7 @@ export default function ConversationMessages({
sources={query.sources}
toolCalls={query.tool_calls}
feedback={query.feedback}
isStreaming={isCurrentlyStreaming}
handleFeedback={
handleFeedback
? (feedback) => handleFeedback(query, feedback, index)