Will be replaced.

This commit is contained in:
Enrico Ros
2024-10-04 11:22:13 -07:00
parent f37cdcb20c
commit 8754bbada9
8 changed files with 22 additions and 506 deletions
@@ -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.'),
}),
},
};
+1 -6
View File
@@ -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
+15 -65
View File
@@ -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 && (