import * as React from 'react'; import { useQuery } from '@tanstack/react-query'; 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 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'; 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) => 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 [softWrap, setSoftWrap] = useUIPreferencesStore(state => [state.renderCodeSoftWrap, state.setRenderCodeSoftWrap]); // 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 = 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 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 }, ...(props.sx || {}), // fix for SVG diagrams over dark mode: https://github.com/enricoros/big-AGI/issues/520 '[data-joy-color-scheme="dark"] &': (renderPlantUML || renderMermaid) ? { backgroundColor: 'neutral.300', } : {}, }}> {/* 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 */} {(!renderHTML && !renderMermaid && !renderPlantUML && !renderSVG) && ( setSoftWrap(!softWrap)}> )} {/* 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);