mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Will be replaced.
This commit is contained in:
@@ -22,26 +22,6 @@ interface CodeFix {
|
||||
|
||||
const CodeFixes: Record<string, CodeFix> = {
|
||||
|
||||
// See `RenderCodeChartJS`
|
||||
'chartjs-issue': {
|
||||
description: 'Provides the corrected Chart.js configuration code.',
|
||||
systemMessage: `You are an AI assistant that fixes invalid Chart.js configuration JSON.
|
||||
When provided with invalid Chart.js code, you analyze it, identify errors, especially remove all functions if any, and output a corrected version in valid JSON format.
|
||||
Respond first with a very brief analysis of where the error is and exactly what to change, and then call the \`{{functionName}}\` function.`,
|
||||
userInstructionTemplate: `The following Chart.js ChartOptions JSON is invalid and cannot be parsed:
|
||||
\`\`\`json
|
||||
{{codeToFix}}
|
||||
\`\`\`
|
||||
|
||||
{{errorMessageSection}}
|
||||
Please analyze the JSON, correct any errors (REMOVE FUNCTIONS!!!), and provide a valid JSON-only ChartOptions that can be parsed by Chart.js.`,
|
||||
functionName: 'provide_corrected_chartjs_code',
|
||||
functionPolicy: 'think-then-invoke',
|
||||
outputSchema: z.object({
|
||||
corrected_code: z.string().describe('The corrected Chart.js ChartOptions in valid JSON format.'),
|
||||
}),
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { Diff as SanityTextDiff } from '@sanity/diff-match-patch';
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import type { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { BLOCK_CODE_VND_AGI_CHARTJS, renderCodeMemoOrNot } from './code/RenderCode';
|
||||
import { BlocksContainer } from './BlocksContainers';
|
||||
import { EnhancedRenderCode } from './enhanced-code/EnhancedRenderCode';
|
||||
import { RenderDangerousHtml } from './danger-html/RenderDangerousHtml';
|
||||
@@ -13,6 +12,7 @@ import { RenderMarkdown, RenderMarkdownMemo } from './markdown/RenderMarkdown';
|
||||
import { RenderPlainText } from './plaintext/RenderPlainText';
|
||||
import { RenderTextDiff } from './textdiff/RenderTextDiff';
|
||||
import { ToggleExpansionButton } from './ToggleExpansionButton';
|
||||
import { renderCodeMemoOrNot } from './code/RenderCode';
|
||||
import { useAutoBlocksMemoSemiStable, useTextCollapser } from './blocks.hooks';
|
||||
import { useScaledCodeSx, useScaledImageSx, useScaledTypographySx, useToggleExpansionButtonSx } from './blocks.styles';
|
||||
|
||||
@@ -146,11 +146,6 @@ export function AutoBlocksRenderer(props: {
|
||||
// Custom handling for some of our blocks
|
||||
let disableEnhancedRender = bkInput.isPartial;
|
||||
let enhancedStartCollapsed = false;
|
||||
if (bkInput.title === BLOCK_CODE_VND_AGI_CHARTJS) {
|
||||
disableEnhancedRender = false;
|
||||
// For Chart.js charts, at the moment, we use the 'unwanted' refresh at the end of the message to start (that block) without collapse
|
||||
enhancedStartCollapsed = bkInput.isPartial;
|
||||
}
|
||||
|
||||
return (props.codeRenderVariant === 'enhanced' && !disableEnhancedRender) ? (
|
||||
<EnhancedRenderCode
|
||||
|
||||
@@ -3,11 +3,9 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, ButtonGroup, Dropdown, ListItem, Menu, MenuButton, Sheet, Tooltip, Typography } from '@mui/joy';
|
||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
||||
import ChangeHistoryTwoToneIcon from '@mui/icons-material/ChangeHistoryTwoTone';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import FitScreenIcon from '@mui/icons-material/FitScreen';
|
||||
import FullscreenRoundedIcon from '@mui/icons-material/FullscreenRounded';
|
||||
import HtmlIcon from '@mui/icons-material/Html';
|
||||
@@ -15,14 +13,11 @@ import NumbersRoundedIcon from '@mui/icons-material/NumbersRounded';
|
||||
import SquareTwoToneIcon from '@mui/icons-material/SquareTwoTone';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
|
||||
import { copyBlobPromiseToClipboard, copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { downloadBlob } from '~/common/util/downloadUtils';
|
||||
import { prettyTimestampForFilenames } from '~/common/util/timeUtils';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { useFullscreenElement } from '~/common/components/useFullscreenElement';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { BUTTON_RADIUS, OverlayButton, overlayButtonsActiveSx, overlayButtonsClassName, overlayButtonsTopRightSx, overlayGroupWithShadowSx, StyledOverlayButton, } from '../OverlayButton';
|
||||
import { RenderCodeChartJS, RenderCodeChartJSHandle } from './code-renderers/RenderCodeChartJS';
|
||||
import { BUTTON_RADIUS, OverlayButton, overlayButtonsActiveSx, overlayButtonsClassName, overlayButtonsTopRightSx, overlayGroupWithShadowSx, StyledOverlayButton } from '../OverlayButton';
|
||||
import { RenderCodeHtmlIFrame } from './code-renderers/RenderCodeHtmlIFrame';
|
||||
import { RenderCodeMermaid } from './code-renderers/RenderCodeMermaid';
|
||||
import { RenderCodeSVG } from './code-renderers/RenderCodeSVG';
|
||||
@@ -37,7 +32,6 @@ import './RenderCode.css';
|
||||
|
||||
// configuration
|
||||
const ALWAYS_SHOW_OVERLAY = true;
|
||||
export const BLOCK_CODE_VND_AGI_CHARTJS = 'chartjs';
|
||||
|
||||
|
||||
// RenderCode
|
||||
@@ -118,9 +112,7 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
|
||||
const [showMermaid, setShowMermaid] = React.useState(true);
|
||||
const [showPlantUML, setShowPlantUML] = React.useState(true);
|
||||
const [showSVG, setShowSVG] = React.useState(true);
|
||||
const [showChartJS, setShowChartJS] = React.useState(true);
|
||||
const fullScreenElementRef = React.useRef<HTMLDivElement>(null);
|
||||
const chartJSRef = React.useRef<RenderCodeChartJSHandle>(null);
|
||||
|
||||
// external state
|
||||
const { isFullscreen, enterFullscreen, exitFullscreen } = useFullscreenElement(fullScreenElementRef);
|
||||
@@ -155,24 +147,6 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
|
||||
copyToClipboard(code, 'Code');
|
||||
}, [code]);
|
||||
|
||||
const handleChartCopyToClipboard = React.useCallback(async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
copyBlobPromiseToClipboard('image/png', new Promise(async (resolve, reject) => {
|
||||
const blob = await chartJSRef.current?.getChartPNG(e.shiftKey);
|
||||
if (blob) resolve(blob);
|
||||
else if (blob === undefined) reject('Chart not ready yet.')
|
||||
else reject('Failed to generate chart image.');
|
||||
}), `Chart Image${e.shiftKey ? ' with transparent background' : ''}`);
|
||||
}, []);
|
||||
|
||||
const handleChartDownload = React.useCallback(async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
chartJSRef.current?.getChartPNG(e.shiftKey).then((blob) => {
|
||||
if (blob) return downloadBlob(blob, `chart_${prettyTimestampForFilenames()}.png`);
|
||||
alert('Chart not ready yet.');
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// heuristics for specialized rendering
|
||||
|
||||
@@ -193,11 +167,8 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
|
||||
const renderSVG = isSVGCode && showSVG;
|
||||
const canScaleSVG = renderSVG && code.includes('viewBox="');
|
||||
|
||||
const isChartJSCode = lcBlockTitle === BLOCK_CODE_VND_AGI_CHARTJS && !blockIsPartial;
|
||||
const renderChartJS = isChartJSCode && showChartJS;
|
||||
|
||||
const renderSyntaxHighlight = !renderHTML && !renderMermaid && !renderPlantUML && !renderSVG && !renderChartJS;
|
||||
const cannotRenderLineNumbers = !renderSyntaxHighlight || showSoftWrap || renderChartJS;
|
||||
const renderSyntaxHighlight = !renderHTML && !renderMermaid && !renderPlantUML && !renderSVG;
|
||||
const cannotRenderLineNumbers = !renderSyntaxHighlight || showSoftWrap;
|
||||
const renderLineNumbers = !cannotRenderLineNumbers && ((showLineNumbers && uiComplexityMode === 'extra') || isFullscreen);
|
||||
|
||||
|
||||
@@ -263,7 +234,7 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
|
||||
ref={fullScreenElementRef}
|
||||
component='code'
|
||||
className={`language-${inferredCodeLanguage || 'unknown'}${renderLineNumbers ? ' line-numbers' : ''}`}
|
||||
sx={!isFullscreen ? codeSx : {...codeSx, backgroundColor: 'background.surface' }}
|
||||
sx={!isFullscreen ? codeSx : { ...codeSx, backgroundColor: 'background.surface' }}
|
||||
>
|
||||
|
||||
{/* Markdown Title (File/Type) */}
|
||||
@@ -281,8 +252,7 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
|
||||
: renderMermaid ? <RenderCodeMermaid mermaidCode={code} fitScreen={fitScreen} />
|
||||
: renderSVG ? <RenderCodeSVG svgCode={code} fitScreen={fitScreen} />
|
||||
: (renderPlantUML && (plantUmlSvgData || plantUmlError)) ? <RenderCodePlantUML svgCode={plantUmlSvgData ?? null} error={plantUmlError} fitScreen={fitScreen} />
|
||||
: renderChartJS ? <RenderCodeChartJS ref={chartJSRef} chartJSCode={code} onReplaceInCode={props.onReplaceInCode} />
|
||||
: <RenderCodeSyntax highlightedSyntaxAsHtml={highlightedCode} presenterMode={isFullscreen} />}
|
||||
: <RenderCodeSyntax highlightedSyntaxAsHtml={highlightedCode} presenterMode={isFullscreen} />}
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -300,28 +270,25 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
|
||||
</OverlayButton>
|
||||
)}
|
||||
|
||||
{/* SVG, Chart.js, Mermaid, PlantUML -- including a max-out button */}
|
||||
{(isSVGCode || isChartJSCode || isMermaidCode || isPlantUMLCode) && (
|
||||
{/* SVG, Mermaid, PlantUML -- including a max-out button */}
|
||||
{(isSVGCode || isMermaidCode || isPlantUMLCode) && (
|
||||
<ButtonGroup aria-label='Diagram' sx={overlayGroupWithShadowSx}>
|
||||
{/* Toggle rendering */}
|
||||
<OverlayButton
|
||||
tooltip={noTooltips ? null
|
||||
: (renderSVG || renderMermaid || renderPlantUML) ? 'Show Code'
|
||||
: renderChartJS ? 'Show Data'
|
||||
: isSVGCode ? 'Render SVG'
|
||||
: isChartJSCode ? 'Show Chart'
|
||||
: isMermaidCode ? 'Mermaid Diagram'
|
||||
: 'PlantUML Diagram'
|
||||
: isSVGCode ? 'Render SVG'
|
||||
: isMermaidCode ? 'Mermaid Diagram'
|
||||
: 'PlantUML Diagram'
|
||||
}
|
||||
variant={(renderChartJS || renderMermaid || renderPlantUML) ? 'solid' : 'outlined'}
|
||||
color={isSVGCode ? 'warning' : isChartJSCode ? 'primary' : undefined}
|
||||
variant={(renderMermaid || renderPlantUML) ? 'solid' : 'outlined'}
|
||||
color={isSVGCode ? 'warning' : undefined}
|
||||
onClick={() => {
|
||||
if (isSVGCode) setShowSVG(on => !on);
|
||||
if (isChartJSCode) setShowChartJS(on => !on);
|
||||
if (isMermaidCode) setShowMermaid(on => !on);
|
||||
if (isPlantUMLCode) setShowPlantUML(on => !on);
|
||||
}}>
|
||||
{isSVGCode ? <ChangeHistoryTwoToneIcon /> : isChartJSCode ? <BarChartIcon /> : <SquareTwoToneIcon />}
|
||||
{isSVGCode ? <ChangeHistoryTwoToneIcon /> : <SquareTwoToneIcon />}
|
||||
</OverlayButton>
|
||||
|
||||
{/* Fit-Content */}
|
||||
@@ -376,30 +343,13 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
|
||||
)}
|
||||
|
||||
{/* Copy */}
|
||||
{props.noCopyButton !== true && !renderChartJS && (
|
||||
{props.noCopyButton !== true && (
|
||||
<OverlayButton tooltip={noTooltips ? null : 'Copy Code'} variant='outlined' onClick={handleCopyToClipboard}>
|
||||
<ContentCopyIcon />
|
||||
</OverlayButton>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
{/* Special Group: ChartJS */}
|
||||
{props.noCopyButton !== true && renderChartJS && (
|
||||
<ButtonGroup aria-label='Chart Actions' sx={overlayGroupWithShadowSx}>
|
||||
|
||||
{/* Download Chart PNG */}
|
||||
<OverlayButton tooltip={noTooltips ? null : <>Download PNG<Box sx={{ fontSize: 'xs', m: 0.5 }}>hold ⇧ for transparent</Box></>} onClick={handleChartDownload}>
|
||||
<FileDownloadOutlinedIcon />
|
||||
</OverlayButton>
|
||||
|
||||
{/* Copy Chart PNG */}
|
||||
<OverlayButton tooltip={noTooltips ? null : <>Copy PNG<Box sx={{ fontSize: 'xs', m: 0.5 }}>hold ⇧ for transparent</Box></>} onClick={handleChartCopyToClipboard}>
|
||||
<ContentCopyIcon />
|
||||
</OverlayButton>
|
||||
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* DISABLED: Converted to a Dropdown */}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { GoogleColabIcon } from '~/common/components/icons/3rdparty/GoogleColabI
|
||||
import { JSFiddleIcon } from '~/common/components/icons/3rdparty/JSFiddleIcon';
|
||||
import { StackBlitzIcon } from '~/common/components/icons/3rdparty/StackBlitzIcon';
|
||||
|
||||
import { BLOCK_CODE_VND_AGI_CHARTJS } from '../RenderCode';
|
||||
import { isCodePenSupported, openInCodePen } from './openInCodePen';
|
||||
import { isGoogleColabSupported, openInGoogleColab } from './openInGoogleColab';
|
||||
import { isJSFiddleSupported, openInJsFiddle } from './openInJsFiddle';
|
||||
@@ -25,8 +24,6 @@ export function useOpenInWebEditors(
|
||||
return React.useMemo(() => {
|
||||
if (blockIsPartial) return stableNoButtons;
|
||||
|
||||
if (blockTitle === BLOCK_CODE_VND_AGI_CHARTJS) return stableNoButtons;
|
||||
|
||||
const mayExternal = code?.indexOf('\n') > 0;
|
||||
if (!mayExternal) return stableNoButtons;
|
||||
|
||||
|
||||
@@ -36,9 +36,6 @@ export function inferCodeLanguage(blockTitle: string, code: string): string | nu
|
||||
|
||||
// if we have a block title, use it to infer the language
|
||||
if (blockTitle) {
|
||||
// vnd.agi - we tell how to format these blocks, so we know what's the language inside
|
||||
if (blockTitle.trim().toLowerCase() === 'chartjs')
|
||||
return 'json'; // {{RenderChartJS}}
|
||||
|
||||
// single word: assume it's the syntax highlight language
|
||||
if (!blockTitle.includes('.'))
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, Typography, useColorScheme } from '@mui/joy';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
|
||||
import { useAgiFixupCode } from '~/modules/aifn/agicodefixup/useAgiFixupCode';
|
||||
|
||||
import { asyncCanvasToBlob } from '~/common/util/canvasUtils';
|
||||
import { themeFontFamilyCss } from '~/common/app.theme';
|
||||
|
||||
import { ChartConfiguration, ChartInstanceType, chartJSApplyTheme, chartJSFixupGeneratedObject, chartJSPixelRatio, useDynamicChartJS } from './useDynamicChartJS';
|
||||
|
||||
|
||||
const chartContainerSx: SxProps = {
|
||||
// required by Chart.js
|
||||
position: 'relative',
|
||||
|
||||
// to try to regain the chart size after shrinking
|
||||
width: '100%',
|
||||
// to better get resized when fullscreen
|
||||
flex: 1,
|
||||
|
||||
// limit height of the canvas or it can too large easily
|
||||
'& canvas': {
|
||||
// width: '100% !important',
|
||||
// height: '100%',
|
||||
// minHeight: '320px',
|
||||
maxHeight: '640px',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Exposed API
|
||||
export type RenderCodeChartJSHandle = {
|
||||
getChartPNG: (transparentBackground: boolean) => Promise<Blob | null>;
|
||||
};
|
||||
|
||||
|
||||
export const RenderCodeChartJS = React.forwardRef(function RenderCodeChartJS(props: {
|
||||
chartJSCode: string;
|
||||
onReplaceInCode?: (search: string, replace: string) => boolean;
|
||||
}, ref: React.Ref<RenderCodeChartJSHandle>) {
|
||||
|
||||
// state
|
||||
const [renderError, setRenderError] = React.useState<string | null>(null);
|
||||
const [fixupError, setFixupError] = React.useState<string | null>(null);
|
||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||
const chartInstanceRef = React.useRef<ChartInstanceType | null>(null);
|
||||
|
||||
// external state
|
||||
const isDarkMode = useColorScheme().mode === 'dark';
|
||||
const { chartJS, loadingError, isLoading: isLibraryLoading } = useDynamicChartJS();
|
||||
|
||||
// immediate parsing (note, this could be done with useEffect and state, but we save a render cycle)
|
||||
const parseResult = React.useMemo(() => {
|
||||
try {
|
||||
const config = JSON.parse(props.chartJSCode) as ChartConfiguration;
|
||||
chartJSFixupGeneratedObject(config);
|
||||
return { chartConfig: config, parseError: null };
|
||||
} catch (error: any) {
|
||||
return { chartConfig: null, parseError: error.message as string || 'Unknown error.' };
|
||||
}
|
||||
}, [props.chartJSCode]);
|
||||
|
||||
// AI functions
|
||||
const { isFetching, refetch } = useAgiFixupCode('chartjs-issue', false, props.chartJSCode, parseResult.parseError);
|
||||
|
||||
|
||||
// Rendering
|
||||
React.useEffect(() => {
|
||||
if (!chartJS || !parseResult.chartConfig || !canvasRef.current) return;
|
||||
|
||||
try {
|
||||
// Destroy previous chart instance if it exists
|
||||
chartInstanceRef.current?.destroy();
|
||||
|
||||
// React to the theme
|
||||
chartJSApplyTheme(chartJS, isDarkMode);
|
||||
|
||||
// Create new chart instance
|
||||
chartInstanceRef.current = new chartJS(canvasRef.current, parseResult.chartConfig);
|
||||
setRenderError(null);
|
||||
|
||||
} catch (error: any) {
|
||||
setRenderError('Error rendering chart: ' + (error.message || 'Unknown error.'));
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
chartInstanceRef.current?.destroy();
|
||||
chartInstanceRef.current = null;
|
||||
};
|
||||
}, [chartJS, parseResult.chartConfig, isDarkMode]);
|
||||
|
||||
|
||||
// Expose control methods
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getChartPNG: async (transparentBackground: boolean) => {
|
||||
const chartCanvas = canvasRef.current;
|
||||
if (!chartCanvas) return null;
|
||||
|
||||
// Create a new canvas
|
||||
const pngCanvas = document.createElement('canvas');
|
||||
pngCanvas.width = chartCanvas.width;
|
||||
pngCanvas.height = chartCanvas.height;
|
||||
const ctx = pngCanvas.getContext('2d', { alpha: true });
|
||||
if (!ctx)
|
||||
return await asyncCanvasToBlob(chartCanvas, 'image/png');
|
||||
|
||||
// Omit the background layer
|
||||
if (!transparentBackground) {
|
||||
// ctx.fillStyle = isDarkMode ? '#171A1C' : '#F0F4F8';
|
||||
ctx.fillStyle = isDarkMode ? '#000' : '#FFF';
|
||||
ctx.fillRect(0, 0, pngCanvas.width, pngCanvas.height);
|
||||
}
|
||||
|
||||
// Draw the chart
|
||||
ctx.drawImage(chartCanvas, 0, 0);
|
||||
|
||||
// Great work Big-AGI!
|
||||
const pr = chartJSPixelRatio();
|
||||
ctx.font = `${10 * pr}px ${themeFontFamilyCss}`;
|
||||
ctx.fillStyle = isDarkMode ? '#9FA6AD' : '#555E68';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.fillText('Big-AGI.com', 7 * pr, pngCanvas.height - 6 * pr);
|
||||
return await asyncCanvasToBlob(pngCanvas, 'image/png');
|
||||
},
|
||||
}), [isDarkMode]);
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
const { onReplaceInCode } = props;
|
||||
|
||||
const handleChartRegenerate = React.useCallback(async () => {
|
||||
if (!onReplaceInCode) return;
|
||||
setFixupError(null);
|
||||
refetch().then((result) => {
|
||||
if (result.data)
|
||||
onReplaceInCode(props.chartJSCode, result.data);
|
||||
else if (result.error)
|
||||
setFixupError(result.error.message || 'Unknown error.');
|
||||
else
|
||||
setFixupError('Unknown Fixup error.');
|
||||
});
|
||||
}, [onReplaceInCode, props.chartJSCode, refetch]);
|
||||
|
||||
|
||||
// Handle all the non-chart states
|
||||
switch (true) {
|
||||
case isLibraryLoading:
|
||||
// DISABLED: reduce visual noise
|
||||
// return <Typography level='body-xs'>Loading Chart.js...</Typography>;
|
||||
return null;
|
||||
case !!loadingError:
|
||||
return <Typography level='body-sm' color='danger'>{loadingError}</Typography>;
|
||||
case !!parseResult.parseError || !!fixupError:
|
||||
return (
|
||||
<Box sx={{ display: 'grid', gap: 1, justifyItems: 'start' }}>
|
||||
{/* Here we play like if we won't get the callback, but we will */}
|
||||
{/*{props.onReplaceInCode && (*/}
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='success'
|
||||
disabled={!props.onReplaceInCode}
|
||||
onClick={handleChartRegenerate}
|
||||
loading={isFetching}
|
||||
loadingPosition='end'
|
||||
sx={{
|
||||
minWidth: 160,
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'xs',
|
||||
}}
|
||||
endDecorator={props.onReplaceInCode ? <AutoAwesomeIcon /> : undefined}
|
||||
>
|
||||
{isFetching ? 'Fixing Chart... ' : props.onReplaceInCode ? 'Attempt Fix' : 'Detected Issue'}
|
||||
</Button>
|
||||
{/*)}*/}
|
||||
{fixupError ? (
|
||||
<Typography level='body-sm' color='warning' sx={{ ml: 0.5 }}>
|
||||
Error fixing chart: {fixupError}
|
||||
</Typography>
|
||||
) : (parseResult.parseError && !isFetching) && (
|
||||
<Typography level='body-xs' sx={{ ml: 0.5 }}>
|
||||
Invalid Chart.js input: {parseResult.parseError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
case !!renderError:
|
||||
return <Typography level='body-sm' color='warning' variant='plain'>{renderError}</Typography>;
|
||||
}
|
||||
|
||||
// Render the chart
|
||||
return (
|
||||
<Box sx={chartContainerSx}>
|
||||
<canvas ref={canvasRef} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Enrico Ros
|
||||
*
|
||||
* Hooks, state centralizer and utility functions to load Chart.js dynamically
|
||||
* from CDN instead of bundling it with the app.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { themeFontFamilyCss } from '~/common/app.theme';
|
||||
|
||||
|
||||
// Configuration
|
||||
const CHARTJS_VERSION = '4.4.4';
|
||||
const CHARTJS_CDN_URL = `https://cdn.jsdelivr.net/npm/chart.js@${CHARTJS_VERSION}/dist/chart.umd.js`;
|
||||
const CHARTJS_SCRIPT_ID = 'chartjs-cdn';
|
||||
|
||||
|
||||
// Minimal type definitions for Chart.js - as of 4.4.4
|
||||
|
||||
interface ChartConstructorType {
|
||||
defaults: ChartDefaults;
|
||||
|
||||
new(context: CanvasRenderingContext2D | HTMLCanvasElement, config: ChartConfiguration): ChartInstanceType;
|
||||
}
|
||||
|
||||
interface ChartDefaults {
|
||||
color?: string;
|
||||
devicePixelRatio?: number;
|
||||
font?: {
|
||||
family: string;
|
||||
size: number;
|
||||
};
|
||||
maintainAspectRatio?: boolean;
|
||||
responsive?: boolean;
|
||||
plugins?: any,
|
||||
|
||||
// [key: string]: any;
|
||||
}
|
||||
|
||||
export interface ChartConfiguration {
|
||||
type?: string;
|
||||
data?: any;
|
||||
options?: ChartDefaults;
|
||||
|
||||
// [key: string]: any;
|
||||
}
|
||||
|
||||
export interface ChartInstanceType {
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
|
||||
// Code manipulation functions
|
||||
|
||||
function _chartJSInitializeDefaults(Chart: ChartConstructorType): ChartConstructorType {
|
||||
|
||||
// Use the application fonts
|
||||
if (Chart.defaults.font) {
|
||||
Chart.defaults.font.family = themeFontFamilyCss;
|
||||
Chart.defaults.font.size = 13;
|
||||
}
|
||||
|
||||
// Responsive defaults, to autosize the chart while keeping the aspect ratios
|
||||
Chart.defaults.maintainAspectRatio = true; // defaults to 1 for polar and so, 2 for bars and more
|
||||
Chart.defaults.responsive = true; // re-draw on resize
|
||||
|
||||
// Set devicePixelRatio to double, to enable downloading/zooming of charts
|
||||
// FIXME: there's an issue here, by overriding the default (which invokes getDevicePixelRatio) we stop
|
||||
// the re-render when a window is moved to a different screen with different DPI. In some sense
|
||||
// we are anchoring the DPR to the first screen's x 2.
|
||||
if (window.devicePixelRatio)
|
||||
Chart.defaults.devicePixelRatio = chartJSPixelRatio();
|
||||
|
||||
// Change the default padding for the title
|
||||
Chart.defaults.plugins.title.padding = { top: 8, bottom: 16 };
|
||||
|
||||
return Chart;
|
||||
}
|
||||
|
||||
export function chartJSPixelRatio() {
|
||||
return 2 * (window.devicePixelRatio || 1);
|
||||
}
|
||||
|
||||
export function chartJSApplyTheme(Chart: ChartConstructorType, isDarkMode: boolean) {
|
||||
// responsive color
|
||||
Chart.defaults.color = isDarkMode ? '#CDD7E1' : '#32383E';
|
||||
}
|
||||
|
||||
export function chartJSFixupGeneratedObject(chartConfig: ChartConfiguration): void {
|
||||
// Do not remove Font, allow for override
|
||||
// delete chartConfig?.options?.font;
|
||||
// Remove responsive options - we handle this ourselves by default
|
||||
delete chartConfig?.options?.responsive;
|
||||
delete chartConfig?.options?.maintainAspectRatio;
|
||||
delete chartConfig?.options?.devicePixelRatio;
|
||||
}
|
||||
|
||||
|
||||
// Singleton promise for loading Chart.js
|
||||
let chartJSPromise: Promise<ChartConstructorType> | null = null;
|
||||
|
||||
function loadCDNScript(): Promise<ChartConstructorType> {
|
||||
// Resolve immediately if already loaded
|
||||
if ((window as any).Chart)
|
||||
return Promise.resolve(_chartJSInitializeDefaults((window as any).Chart));
|
||||
|
||||
// If loading has already started, return the existing promise
|
||||
if (chartJSPromise) return chartJSPromise;
|
||||
|
||||
// Ensure the API definitions from package.json match the CDN loaded version
|
||||
// NOTE: Disabled because we are not using the package.json version anymore, we replaced the API
|
||||
// if (devDependencies['chart.js'] !== CHARTJS_VERSION)
|
||||
// return Promise.reject(new Error(`Chart.js version mismatch: loaded ${CHARTJS_VERSION}, expected ${devDependencies['chart.js']}.`));
|
||||
|
||||
chartJSPromise = new Promise((resolve, reject) => {
|
||||
|
||||
// Create or reuse a script DOM element
|
||||
const script = document.createElement('script');
|
||||
script.id = CHARTJS_SCRIPT_ID;
|
||||
script.src = CHARTJS_CDN_URL;
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => {
|
||||
if ((window as any).Chart) resolve(_chartJSInitializeDefaults((window as any).Chart));
|
||||
else reject(new Error('Chart.js failed to load.'));
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
console.log(`[DEV] error loading Chart.js from: ${CHARTJS_CDN_URL}`);
|
||||
reject(new Error('Failed to load Chart.js from CDN.'));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return chartJSPromise;
|
||||
}
|
||||
|
||||
|
||||
// Store: we share the state across multiple useChartJS hooks
|
||||
interface ChartApiStore {
|
||||
|
||||
// state
|
||||
chartJS: ChartConstructorType | null;
|
||||
loadingError: string | null;
|
||||
isLoading: boolean;
|
||||
|
||||
// actions
|
||||
loadChartJS: () => void;
|
||||
|
||||
}
|
||||
|
||||
const useChartApiStore = create<ChartApiStore>((set, get) => ({
|
||||
|
||||
// initial state
|
||||
chartJS: null,
|
||||
loadingError: null,
|
||||
isLoading: false,
|
||||
|
||||
// actions
|
||||
loadChartJS: () => {
|
||||
|
||||
// Prevent redundant calls
|
||||
const { chartJS, loadingError, isLoading } = get();
|
||||
if (chartJS || loadingError || isLoading) return;
|
||||
set({ isLoading: true });
|
||||
|
||||
// Load and save the constructor to the store
|
||||
loadCDNScript()
|
||||
.then((Chart) =>
|
||||
set({ chartJS: Chart, loadingError: null, isLoading: false }),
|
||||
)
|
||||
.catch((error) =>
|
||||
set({ chartJS: null, loadingError: error.message, isLoading: false }),
|
||||
);
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
|
||||
/**
|
||||
* Hook to load Chart.js and make it available to the component.
|
||||
*/
|
||||
export function useDynamicChartJS() {
|
||||
const { chartJS, loadingError, isLoading } = useChartApiStore();
|
||||
|
||||
// Load the library upon first access
|
||||
const needsLoading = !chartJS && !loadingError && !isLoading;
|
||||
React.useEffect(() => {
|
||||
if (needsLoading)
|
||||
useChartApiStore.getState().loadChartJS();
|
||||
}, [needsLoading]);
|
||||
|
||||
return { chartJS, loadingError, isLoading };
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, ColorPaletteProp, IconButton, Typography } from '@mui/joy';
|
||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
@@ -10,8 +9,8 @@ import type { ContentScaling } from '~/common/app.theme';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
|
||||
import { BLOCK_CODE_VND_AGI_CHARTJS, RenderCodeMemo } from '../code/RenderCode';
|
||||
import { EnhancedRenderCodeMenu } from './EnhancedRenderCodeMenu';
|
||||
import { RenderCodeMemo } from '../code/RenderCode';
|
||||
import { enhancedCodePanelTitleTooltipSx, RenderCodePanelFrame } from '../code/RenderCodePanelFrame';
|
||||
import { getCodeCollapseManager } from './codeCollapseManager';
|
||||
import { useLiveFilePatch } from './livefile-patch/useLiveFilePatch';
|
||||
@@ -117,8 +116,7 @@ export function EnhancedRenderCode(props: {
|
||||
), [props.code, props.semiStableId, props.title]);
|
||||
|
||||
const headerRow = React.useMemo(() => {
|
||||
const isChart = props.title === BLOCK_CODE_VND_AGI_CHARTJS;
|
||||
const Icon = (isChart && !isCodeCollapsed) ? BarChartIcon : CodeIcon;
|
||||
const Icon = CodeIcon;
|
||||
return <>
|
||||
{/* Icon and Title */}
|
||||
<TooltipOutlined placement='top-start' color='neutral' title={headerTooltipContents}>
|
||||
@@ -127,14 +125,13 @@ export function EnhancedRenderCode(props: {
|
||||
aria-hidden
|
||||
onClick={handleToggleCodeCollapse}
|
||||
sx={{
|
||||
transform: (isCodeCollapsed && !isChart) ? 'rotate(-90deg)' : 'none',
|
||||
transform: isCodeCollapsed ? 'rotate(-90deg)' : 'none',
|
||||
transition: 'transform 0.2s cubic-bezier(.17,.84,.44,1)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<Typography level={!isChart ? 'title-sm' : 'body-sm'}>
|
||||
{isChart ? 'Chart ' + (props.isPartial ? '.'.repeat(Math.round(props.code.length / 100) % 4) : '')
|
||||
: props.title || 'Code'}
|
||||
<Typography level={'title-sm'}>
|
||||
{props.title || 'Code'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TooltipOutlined>
|
||||
@@ -153,7 +150,7 @@ export function EnhancedRenderCode(props: {
|
||||
</IconButton>
|
||||
|
||||
</>;
|
||||
}, [handleToggleCodeCollapse, handleToggleContextMenu, headerTooltipContents, isCodeCollapsed, liveFileButton, props.code.length, props.isPartial, props.title]);
|
||||
}, [handleToggleCodeCollapse, handleToggleContextMenu, headerTooltipContents, isCodeCollapsed, liveFileButton, props.title]);
|
||||
|
||||
// const toolbarRow = React.useMemo(() => <>
|
||||
// {props.onLiveFileCreate && (
|
||||
|
||||
Reference in New Issue
Block a user