From 16386a952413e6dc97fe93af47f91ce027dc7274 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 4 May 2025 19:39:24 +0530 Subject: [PATCH] (feat:mermaid) zoom on hover --- frontend/src/components/MermaidRenderer.tsx | 187 +++++++++++++----- .../src/conversation/ConversationMessages.tsx | 29 +-- 2 files changed, 152 insertions(+), 64 deletions(-) diff --git a/frontend/src/components/MermaidRenderer.tsx b/frontend/src/components/MermaidRenderer.tsx index bd1e5b03..d2ca1276 100644 --- a/frontend/src/components/MermaidRenderer.tsx +++ b/frontend/src/components/MermaidRenderer.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useRef, useState } from 'react'; import mermaid from 'mermaid'; import CopyButton from './CopyButton'; -import { useSelector } from 'react-redux'; -import { selectStatus } from '../conversation/conversationSlice'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneLight, vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { MermaidRendererProps } from './types'; const MermaidRenderer: React.FC = ({ @@ -10,47 +10,90 @@ const MermaidRenderer: React.FC = ({ isDarkTheme, }) => { const diagramId = useRef(`mermaid-${crypto.randomUUID()}`); - const status = useSelector(selectStatus); + // const status = useSelector(selectStatus); + const [localStatus, setLocalStatus] = useState<'loading' | 'idle'>('loading'); const [svgContent, setSvgContent] = useState(''); const [error, setError] = useState(null); const [showCode, setShowCode] = useState(false); const [showDownloadMenu, setShowDownloadMenu] = useState(false); + const [zoomLevel, setZoomLevel] = useState(1); const downloadMenuRef = useRef(null); + const containerRef = useRef(null); + const [hoverPosition, setHoverPosition] = useState<{ x: number, y: number } | null>(null); + const [isHovering, setIsHovering] = useState(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 (!code) { + setLocalStatus('idle'); + return; + } + setLocalStatus('loading'); + + mermaid.initialize({ + startOnLoad: true, + theme: isDarkTheme ? 'dark' : 'default', + securityLevel: 'loose', + suppressErrorRendering: true, + }); + + const renderDiagram = async (): Promise => { + try { + const element = document.getElementById(diagramId.current); + if (element) { + element.removeAttribute('data-processed'); + element.innerHTML = code; + 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'); + + setSvgContent(svgElement.outerHTML); + } + setError(null); + setLocalStatus('idle'); + } + } catch (err) { + setError( + `Failed to render Mermaid diagram: ${err instanceof Error ? err.message : String(err)}` + ); + setSvgContent(''); + setLocalStatus('idle'); + } + }; + + renderDiagram(); + + }, [code, isDarkTheme]); useEffect(() => { - if (status === 'loading' || !code) return; - - mermaid.initialize({ - startOnLoad: true, - theme: isDarkTheme ? 'dark' : 'default', - securityLevel: 'loose', - suppressErrorRendering: true - }); - - const renderDiagram = async (): Promise => { - try { - const element = document.getElementById(diagramId.current); - if (element) { - element.removeAttribute('data-processed'); - element.innerHTML = code; - mermaid.contentLoaded(); - - const svgElement = element.querySelector('svg'); - if (svgElement) { - setSvgContent(svgElement.outerHTML); - } - setError(null); - } - } catch (err) { - setError( - `Failed to render Mermaid diagram: ${err instanceof Error ? err.message : String(err)}` - ); - setSvgContent(''); - } - }; - - renderDiagram(); - }, [code, isDarkTheme, status]); + setZoomLevel(1); + }, [code]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -68,6 +111,7 @@ const MermaidRenderer: React.FC = ({ }; }, [showDownloadMenu]); + const downloadSvg = (): void => { const element = document.getElementById(diagramId.current); if (!element) return; @@ -189,25 +233,26 @@ const MermaidRenderer: React.FC = ({ 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 showDiagramOptions = localStatus !== 'loading' && !error; + const errorRender = localStatus !== 'loading' && error; return ( -
+
mermaid
- + {showDiagramOptions && (
- {status === 'loading' ? ( + {localStatus === 'loading' ? (
Loading diagram...
- ) : errorRender ? ( + ) : errorRender ? (
{error}
) : ( -
-
-
+ <> +
+
{code}
{showCode && ( -
-              {code}
-            
+
+
+ + Mermaid Code + +
+ + {code} + +
)} -
+ )}
); diff --git a/frontend/src/conversation/ConversationMessages.tsx b/frontend/src/conversation/ConversationMessages.tsx index 941b9682..50c79394 100644 --- a/frontend/src/conversation/ConversationMessages.tsx +++ b/frontend/src/conversation/ConversationMessages.tsx @@ -1,4 +1,4 @@ -import { Fragment, useEffect, useRef, useState } from 'react'; +import { Fragment, useEffect, useRef, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ArrowDown from '../assets/arrow-down.svg'; @@ -47,25 +47,30 @@ export default function ConversationMessages({ } }; + // Remove Mermaid tracking code that was here + const scrollIntoView = () => { if (!conversationRef?.current || eventInterrupt) return; - if (status === 'idle' || !queries[queries.length - 1]?.response) { - conversationRef.current.scrollTo({ - behavior: 'smooth', - top: conversationRef.current.scrollHeight, - }); - } else { - conversationRef.current.scrollTop = conversationRef.current.scrollHeight; - } + setTimeout(() => { + if (!conversationRef?.current) return; + + if (status === 'idle' || !queries[queries.length - 1]?.response) { + conversationRef.current.scrollTo({ + behavior: 'smooth', + top: conversationRef.current.scrollHeight, + }); + } else { + conversationRef.current.scrollTop = conversationRef.current.scrollHeight; + } + }, 100); // Small timeout to allow images to render }; const checkScroll = () => { const el = conversationRef.current; if (!el) return; const isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10; - - atLast.current = isBottom + atLast.current = isBottom; }; useEffect(() => { @@ -182,4 +187,4 @@ export default function ConversationMessages({
); -} \ No newline at end of file +}