mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
More Mermaids (#1833)
* wip * install dependencies * rendering mermaid graphs * fix render failure on streaming * fix bouncy screen by debouncing mermaid parsing * add loading state; clean up styling * replace tag symbols when rendering code * better mermaid theme for visibility * added changeset * remove rehype-mermaid * update webview package.lock * clean up * fix package-lock * remove regular markdown background styling for mermaid blocks
This commit is contained in:
parent
220603e530
commit
cb02277b45
6 changed files with 1275 additions and 3 deletions
5
.changeset/cyan-bags-work.md
Normal file
5
.changeset/cyan-bags-work.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add support for rendering mermaid graphs in the chat.
|
||||
1110
webview-ui/package-lock.json
generated
1110
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,7 @@
|
|||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^7.0.0",
|
||||
"fzf": "^0.5.2",
|
||||
"mermaid": "^11.4.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { memo, useEffect } from "react"
|
||||
import React, { memo, useEffect } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import rehypeHighlight, { Options } from "rehype-highlight"
|
||||
import styled from "styled-components"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import MermaidBlock from "./MermaidBlock"
|
||||
|
||||
interface MarkdownBlockProps {
|
||||
markdown?: string
|
||||
|
|
@ -220,7 +221,27 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
|||
],
|
||||
rehypeReactOptions: {
|
||||
components: {
|
||||
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
|
||||
pre: ({ node, children, ...preProps }: any) => {
|
||||
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
|
||||
const child = children[0] as React.ReactElement<{ className?: string }>
|
||||
if (child.props?.className?.includes("language-mermaid")) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return (
|
||||
<StyledPre {...preProps} theme={theme}>
|
||||
{children}
|
||||
</StyledPre>
|
||||
)
|
||||
},
|
||||
code: (props: any) => {
|
||||
const className = props.className || ""
|
||||
if (className.includes("language-mermaid")) {
|
||||
const codeText = String(props.children || "")
|
||||
return <MermaidBlock code={codeText} />
|
||||
}
|
||||
return <code {...props} />
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
95
webview-ui/src/components/common/MermaidBlock.tsx
Normal file
95
webview-ui/src/components/common/MermaidBlock.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { useEffect, useRef, useState } from "react"
|
||||
import mermaid from "mermaid"
|
||||
import { useDebounceEffect } from "../../utils/useDebounceEffect"
|
||||
import styled from "styled-components"
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "loose",
|
||||
theme: "dark",
|
||||
themeVariables: {
|
||||
background: "#1e1e1e",
|
||||
textColor: "#ffffff", // make text much brighter
|
||||
mainBkg: "#2d2d2d",
|
||||
lineColor: "#cccccc", // light enough for contrast
|
||||
fontSize: "16px",
|
||||
primaryColor: "#3c3c3c", // node fill color, etc.
|
||||
},
|
||||
})
|
||||
|
||||
interface MermaidBlockProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
export default function MermaidBlock({ code }: MermaidBlockProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 1) Whenever `code` changes, mark that we need to re-render a new chart
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
}, [code])
|
||||
|
||||
// 2) Debounce the actual parse/render
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = ""
|
||||
}
|
||||
mermaid
|
||||
.parse(code, { suppressErrors: true })
|
||||
.then((isValid) => {
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid or incomplete Mermaid code")
|
||||
}
|
||||
const id = `mermaid-${Math.random().toString(36).substring(2)}`
|
||||
return mermaid.render(id, code)
|
||||
})
|
||||
.then(({ svg }) => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = svg
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("Mermaid parse/render failed:", err)
|
||||
containerRef.current!.innerHTML = code.replace(/</g, "<").replace(/>/g, ">")
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
},
|
||||
500, // Delay 500ms
|
||||
[code], // Dependencies for scheduling
|
||||
)
|
||||
|
||||
return (
|
||||
<MermaidBlockContainer>
|
||||
{isLoading && <LoadingMessage>Creating mermaid chart...</LoadingMessage>}
|
||||
|
||||
{/* The container for the final <svg> or raw code. */}
|
||||
<SvgContainer ref={containerRef} $isLoading={isLoading} />
|
||||
</MermaidBlockContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const MermaidBlockContainer = styled.div`
|
||||
position: relative;
|
||||
margin: 8px 0;
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div`
|
||||
padding: 8px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
`
|
||||
|
||||
interface SvgContainerProps {
|
||||
$isLoading: boolean
|
||||
}
|
||||
|
||||
const SvgContainer = styled.div<SvgContainerProps>`
|
||||
opacity: ${(props) => (props.$isLoading ? 0.3 : 1)};
|
||||
min-height: 20px;
|
||||
transition: opacity 0.2s ease;
|
||||
`
|
||||
42
webview-ui/src/utils/useDebounceEffect.ts
Normal file
42
webview-ui/src/utils/useDebounceEffect.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { useEffect, useRef } from "react"
|
||||
|
||||
type VoidFn = () => void
|
||||
|
||||
/**
|
||||
* Runs `effectRef.current()` after `delay` ms whenever any of the `deps` change,
|
||||
* but cancels/re-schedules if they change again before the delay.
|
||||
*/
|
||||
export function useDebounceEffect(effect: VoidFn, delay: number, deps: any[]) {
|
||||
const callbackRef = useRef<VoidFn>(effect)
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Keep callbackRef current
|
||||
useEffect(() => {
|
||||
callbackRef.current = effect
|
||||
}, [effect])
|
||||
|
||||
useEffect(() => {
|
||||
// Clear any queued call
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
|
||||
// Schedule a new call
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
// always call the *latest* version of effect
|
||||
callbackRef.current()
|
||||
}, delay)
|
||||
|
||||
// Cleanup on unmount or next effect
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
}
|
||||
|
||||
// We want to re‐schedule if any item in `deps` changed,
|
||||
// or if `delay` changed.
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [delay, ...deps])
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue