import * as React from 'react'; import { useQuery } from '@tanstack/react-query'; import type { SxProps } from '@mui/joy/styles/types'; import { Box, ButtonGroup, IconButton, Sheet, Tooltip, Typography } from '@mui/joy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import FitScreenIcon from '@mui/icons-material/FitScreen'; import HtmlIcon from '@mui/icons-material/Html'; import SchemaIcon from '@mui/icons-material/Schema'; import ShapeLineOutlinedIcon from '@mui/icons-material/ShapeLineOutlined'; import { copyToClipboard } from '~/common/util/clipboardUtils'; import { frontendSideFetch } from '~/common/util/clientFetchers'; import type { CodeBlock } from '../blocks'; import { ButtonCodePen, isCodePenSupported } from './ButtonCodePen'; import { ButtonJsFiddle, isJSFiddleSupported } from './ButtonJSFiddle'; import { ButtonStackBlitz, isStackBlitzSupported } from './ButtonStackBlitz'; import { heuristicIsHtml, IFrameComponent } from '../RenderHtml'; import { patchSvgString, RenderCodeMermaid } from './RenderCodeMermaid'; export function getPlantUmlServerUrl(): string { // set at nextjs build time return process.env.NEXT_PUBLIC_PLANTUML_SERVER_URL || 'https://www.plantuml.com/plantuml/svg/'; } async function fetchPlantUmlSvg(plantUmlCode: string): Promise { // Get the PlantUML server from inline env var let plantUmlServerUrl = getPlantUmlServerUrl(); if (!plantUmlServerUrl.endsWith('/')) plantUmlServerUrl += '/'; // fetch the PlantUML SVG let text: string = ''; try { // Dynamically import the PlantUML encoder - it's a large library that slows down app loading const { encode: plantUmlEncode } = await import('plantuml-encoder'); // retrieve and manually adapt the SVG, to remove the background const encodedPlantUML: string = plantUmlEncode(plantUmlCode); const response = await frontendSideFetch(`${plantUmlServerUrl}${encodedPlantUML}`); text = await response.text(); } catch (error) { console.error('Error rendering PlantUML on server:', plantUmlServerUrl, error); return null; } // validate/extract the SVG const start = text.indexOf(''); if (start < 0 || end <= start) throw new Error('Could not render PlantUML'); // remove the background color const svg = text .slice(start, end + 6) // .replace('background:#FFFFFF;', ''); // check for syntax errors if (svg.includes('>Syntax Error?')) throw new Error('llm syntax issue (it happens!). Please regenerate or change the language model.'); return svg; } export const overlayButtonsSx: SxProps = { position: 'absolute', top: 0, right: 0, zIndex: 2, /* top of message and its chips */ display: 'flex', flexDirection: 'row', gap: 1, opacity: 0, transition: 'opacity 0.15s', // buttongroup: background '& > div > button': { backgroundColor: 'background.surface', // backdropFilter: 'blur(12px)', }, }; interface RenderCodeBaseProps { codeBlock: CodeBlock, fitScreen?: boolean, noCopyButton?: boolean, optimizeLightweight?: boolean, sx?: SxProps, } interface RenderCodeImplProps extends RenderCodeBaseProps { highlightCode: (inferredCodeLanguage: string | null, blockCode: string) => string, inferCodeLanguage: (blockTitle: string, code: string) => string | null, } function RenderCodeImpl(props: RenderCodeImplProps) { // state const [fitScreen, setFitScreen] = React.useState(!!props.fitScreen); const [showHTML, setShowHTML] = React.useState(false); const [showMermaid, setShowMermaid] = React.useState(true); const [showPlantUML, setShowPlantUML] = React.useState(true); const [showSVG, setShowSVG] = React.useState(true); // derived props const { codeBlock: { blockTitle, blockCode, complete: blockComplete }, highlightCode, inferCodeLanguage, optimizeLightweight, } = props; // heuristic for language, and syntax highlight const { highlightedCode, inferredCodeLanguage } = React.useMemo(() => { const inferredCodeLanguage = inferCodeLanguage(blockTitle, blockCode); const highlightedCode = highlightCode(inferredCodeLanguage, blockCode); return { highlightedCode, inferredCodeLanguage }; }, [inferCodeLanguage, blockTitle, blockCode, highlightCode]); // heuristics for specialized rendering const isHTML = heuristicIsHtml(blockCode); const renderHTML = isHTML && showHTML; const isMermaid = blockTitle === 'mermaid' && blockComplete; const renderMermaid = isMermaid && showMermaid; const isPlantUML = (blockCode.startsWith('@startuml') && blockCode.endsWith('@enduml')) || (blockCode.startsWith('@startmindmap') && blockCode.endsWith('@endmindmap')) || (blockCode.startsWith('@startsalt') && blockCode.endsWith('@endsalt')) || (blockCode.startsWith('@startwbs') && blockCode.endsWith('@endwbs')) || (blockCode.startsWith('@startgantt') && blockCode.endsWith('@endgantt')); let renderPlantUML = isPlantUML && showPlantUML; const { data: plantUmlHtmlData, error: plantUmlError } = useQuery({ enabled: renderPlantUML, queryKey: ['plantuml', blockCode], queryFn: () => fetchPlantUmlSvg(blockCode), staleTime: 24 * 60 * 60 * 1000, // 1 day }); renderPlantUML = renderPlantUML && (!!plantUmlHtmlData || !!plantUmlError); const isSVG = blockCode.startsWith(''); const renderSVG = isSVG && showSVG; const canScaleSVG = renderSVG && blockCode.includes('viewBox="'); const canCodePen = blockComplete && isCodePenSupported(inferredCodeLanguage, isSVG); const canJSFiddle = blockComplete && isJSFiddleSupported(inferredCodeLanguage, blockCode); const canStackBlitz = blockComplete && isStackBlitzSupported(inferredCodeLanguage); const handleCopyToClipboard = (e: React.MouseEvent) => { e.stopPropagation(); copyToClipboard(blockCode, 'Code'); }; return ( {/* Code render */} .overlay-buttons': { opacity: 1 }, ...(props.sx || {}), }}> {/* Markdown Title (File/Type) */} {blockTitle != inferredCodeLanguage && blockTitle.includes('.') && ( {blockTitle} {/*{inferredCodeLanguage}*/} )} {/* Renders HTML, or inline SVG, inline plantUML rendered, or highlighted code */} {renderHTML ? : renderMermaid ? : } {/* Buttons */} {isHTML && ( setShowHTML(!showHTML)}> )} {isMermaid && ( setShowMermaid(!showMermaid)}> )} {isPlantUML && ( setShowPlantUML(!showPlantUML)}> )} {isSVG && ( setShowSVG(!showSVG)}> )} {((isMermaid && showMermaid) || (isPlantUML && showPlantUML && !plantUmlError) || (isSVG && showSVG && canScaleSVG)) && ( setFitScreen(on => !on)}> )} {(canJSFiddle || canCodePen || canStackBlitz) && ( {canJSFiddle && } {canCodePen && } {canStackBlitz && } )} {props.noCopyButton !== true && ( )} ); } // Dynamically import the heavy prism functions const RenderCodeDynamic = React.lazy(async () => { // Dynamically import the code highlight functions const { highlightCode, inferCodeLanguage } = await import('./codePrism'); return { default: (props: RenderCodeBaseProps) => , }; }); export function RenderCode(props: RenderCodeBaseProps) { return ( }> ); } export const RenderCodeMemo = React.memo(RenderCode);