import * as React from 'react'; import { useQuery } from '@tanstack/react-query'; import { useShallow } from 'zustand/react/shallow'; import type { SxProps } from '@mui/joy/styles/types'; import { Box, ButtonGroup, IconButton, Sheet, styled, 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 NumbersRoundedIcon from '@mui/icons-material/NumbersRounded'; import SchemaIcon from '@mui/icons-material/Schema'; import ShapeLineOutlinedIcon from '@mui/icons-material/ShapeLineOutlined'; import WrapTextIcon from '@mui/icons-material/WrapText'; import { copyToClipboard } from '~/common/util/clipboardUtils'; import { frontendSideFetch } from '~/common/util/clientFetchers'; import { useUIPreferencesStore } from '~/common/state/store-ui'; import type { CodeBlock } from '../blocks'; import { ButtonCodePen, isCodePenSupported } from './ButtonCodePen'; import { ButtonJsFiddle, isJSFiddleSupported } from './ButtonJSFiddle'; import { ButtonStackBlitz, isStackBlitzSupported } from './ButtonStackBlitz'; import { heuristicIsBlockTextHTML, IFrameComponent } from '../RenderHtml'; import { patchSvgString, RenderCodeMermaid } from './RenderCodeMermaid'; // style for line-numbers import './RenderCode.css'; 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 OverlayButton = styled(IconButton)(({ theme, variant }) => ({ backgroundColor: variant === 'outlined' ? theme.palette.background.surface : undefined, '--Icon-fontSize': theme.fontSize.lg, })) as typeof IconButton; 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.2s cubic-bezier(.17,.84,.44,1)', // buttongroup: background '& > div > button': { // backgroundColor: 'background.surface', // backdropFilter: 'blur(12px)', }, }; interface RenderCodeBaseProps { codeBlock: CodeBlock, fitScreen?: boolean, noCopyButton?: boolean, optimizeLightweight?: boolean, initialShowHTML?: boolean, sx?: SxProps, } interface RenderCodeImplProps extends RenderCodeBaseProps { highlightCode: (inferredCodeLanguage: string | null, blockCode: string, addLineNumbers: boolean) => 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(props.initialShowHTML === true); const [showMermaid, setShowMermaid] = React.useState(true); const [showPlantUML, setShowPlantUML] = React.useState(true); const [showSVG, setShowSVG] = React.useState(true); const { showLineNumbers, showSoftWrap, setShowLineNumbers, setShowSoftWrap } = useUIPreferencesStore(useShallow(state => ({ showLineNumbers: state.renderCodeLineNumbers, showSoftWrap: state.renderCodeSoftWrap, setShowLineNumbers: state.setRenderCodeLineNumbers, setShowSoftWrap: state.setRenderCodeSoftWrap, }))); // derived props const { codeBlock: { blockTitle, blockCode, complete: blockComplete }, highlightCode, inferCodeLanguage, optimizeLightweight, } = props; const canRenderLineNumbers = !showSoftWrap; const renderLineNumbers = showLineNumbers && canRenderLineNumbers; // heuristic for language, and syntax highlight const { highlightedCode, inferredCodeLanguage } = React.useMemo(() => { const inferredCodeLanguage = inferCodeLanguage(blockTitle, blockCode); const highlightedCode = highlightCode(inferredCodeLanguage, blockCode, renderLineNumbers); return { highlightedCode, inferredCodeLanguage }; }, [inferCodeLanguage, blockTitle, blockCode, highlightCode, renderLineNumbers]); // heuristics for specialized rendering const isHTML = heuristicIsBlockTextHTML(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('\n'); const renderSVG = isSVG && showSVG; const canScaleSVG = renderSVG && blockCode.includes('viewBox="'); const renderCode = !renderHTML && !renderMermaid && !renderPlantUML && !renderSVG; const canCodePen = blockComplete && isCodePenSupported(inferredCodeLanguage, isSVG); const canJSFiddle = blockComplete && isJSFiddleSupported(inferredCodeLanguage, blockCode); const canStackBlitz = blockComplete && isStackBlitzSupported(inferredCodeLanguage); const showBlockTitle = blockTitle != inferredCodeLanguage && (blockTitle.includes('.') || blockTitle.includes('://')); const handleCopyToClipboard = (e: React.MouseEvent) => { e.stopPropagation(); copyToClipboard(blockCode, 'Code'); }; return ( {/* Code render */} .overlay-buttons': { opacity: 1 }, // lots more style, incl font, background, embossing, radius, etc. ...props.sx, }}> {/* Markdown Title (File/Type) */} {showBlockTitle && ( {blockTitle} {/*{inferredCodeLanguage}*/} )} {/* Renders HTML, or inline SVG, inline plantUML rendered, or highlighted code */} {renderHTML ? : renderMermaid ? : } {/* Buttons */} {/* Show HTML */} {isHTML && ( setShowHTML(!showHTML)}> )} {/* Show SVG */} {isSVG && ( setShowSVG(!showSVG)}> )} {/* Show Diagrams */} {(isMermaid || isPlantUML) && ( {/* Toggle rendering */} { if (isMermaid) setShowMermaid(on => !on); if (isPlantUML) setShowPlantUML(on => !on); }}> {/* Fit-To-Screen */} {((isMermaid && showMermaid) || (isPlantUML && showPlantUML && !plantUmlError) || (isSVG && showSVG && canScaleSVG)) && ( setFitScreen(on => !on)}> )} )} {/* New Code Window Buttons */} {(canJSFiddle || canCodePen || canStackBlitz) && ( {canJSFiddle && } {canCodePen && } {canStackBlitz && } )} {/* Soft Wrap toggle */} {renderCode && ( setShowSoftWrap(!showSoftWrap)}> )} {/* Line Numbers toggle */} {renderCode && ( setShowLineNumbers(!showLineNumbers)}> )} {/* Copy */} {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);