diff --git a/package.json b/package.json
index d4c20f651..aeb8688ab 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"@types/react-katex": "^3.0.4",
"@types/react-timeago": "^4.1.7",
"@types/turndown": "^5.0.5",
+ "chart.js": "^4.4.4",
"eslint": "^8.57.1",
"eslint-config-next": "^14.2.13",
"prettier": "^3.3.3",
diff --git a/src/data.ts b/src/data.ts
index 7068645d9..377e4f9f6 100644
--- a/src/data.ts
+++ b/src/data.ts
@@ -100,6 +100,7 @@ Current date: {{LocaleNow}}
{{RenderHTML}}
{{RenderSVG}}
{{PreferTables}}
+{{RenderChartJS}}
`.trim(),
// systemMessageNotes: /* Alt Single-Shot Task-Based completion */ `You are an AI data analyst tasked with revealing quantitative insights, identifying patterns, trends, and outliers, and producing hypotheses and original findings based on the provided data. Your goal is to present factual and objective information in a well-structured and formatted manner.
//
diff --git a/src/modules/blocks/code/RenderCode.tsx b/src/modules/blocks/code/RenderCode.tsx
index e7d4ffa6a..67b393ac1 100644
--- a/src/modules/blocks/code/RenderCode.tsx
+++ b/src/modules/blocks/code/RenderCode.tsx
@@ -3,6 +3,7 @@ import { useShallow } from 'zustand/react/shallow';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, ButtonGroup, Sheet, 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 FitScreenIcon from '@mui/icons-material/FitScreen';
@@ -15,6 +16,7 @@ import { copyToClipboard } from '~/common/util/clipboardUtils';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { BUTTON_RADIUS, OverlayButton, overlayButtonsActiveSx, overlayButtonsClassName, overlayButtonsTopRightSx, overlayGroupWithShadowSx } from '../OverlayButton';
+import { RenderCodeChartJS } from './code-renderers/RenderCodeChartJS';
import { RenderCodeHtmlIFrame } from './code-renderers/RenderCodeHtmlIFrame';
import { RenderCodeMermaid } from './code-renderers/RenderCodeMermaid';
import { RenderCodeSVG } from './code-renderers/RenderCodeSVG';
@@ -108,6 +110,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 { showLineNumbers, showSoftWrap, setShowLineNumbers, setShowSoftWrap } = useUIPreferencesStore(useShallow(state => ({
showLineNumbers: state.renderCodeLineNumbers,
showSoftWrap: state.renderCodeSoftWrap,
@@ -141,10 +144,12 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
// heuristics for specialized rendering
+ const lcBlockTitle = blockTitle.trim().toLowerCase();
+
const isHTMLCode = heuristicIsBlockPureHTML(code);
const renderHTML = isHTMLCode && showHTML;
- const isMermaidCode = blockTitle === 'mermaid' && !blockIsPartial;
+ const isMermaidCode = lcBlockTitle === 'mermaid' && !blockIsPartial;
const renderMermaid = isMermaidCode && showMermaid;
const isPlantUMLCode = heuristicIsCodePlantUML(code);
@@ -156,10 +161,11 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
const renderSVG = isSVGCode && showSVG;
const canScaleSVG = renderSVG && code.includes('viewBox="');
- const renderSyntaxHighlight = !renderHTML && !renderMermaid && !renderPlantUML && !renderSVG;
+ const isChartJSCode = lcBlockTitle === 'chartjs' && !blockIsPartial;
+ const renderChartJS = isChartJSCode && showChartJS;
-
- const cannotRenderLineNumbers = !renderSyntaxHighlight || showSoftWrap;
+ const renderSyntaxHighlight = !renderHTML && !renderMermaid && !renderPlantUML && !renderSVG && !renderChartJS;
+ const cannotRenderLineNumbers = !renderSyntaxHighlight || showSoftWrap || renderChartJS;
const renderLineNumbers = showLineNumbers && !cannotRenderLineNumbers;
@@ -242,7 +248,8 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
: renderMermaid ?
: renderSVG ?
: (renderPlantUML && (plantUmlSvgData || plantUmlError)) ?
- : }
+ : renderChartJS ?
+ : }
@@ -288,6 +295,19 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
)}
+ {/* Show ChartJS */}
+ {isChartJSCode && (
+ setShowChartJS(on => !on)}
+ >
+
+
+ )}
+
{/* Group: Text Options */}
{/* Soft Wrap toggle */}
diff --git a/src/modules/blocks/code/code-highlight/codePrism.ts b/src/modules/blocks/code/code-highlight/codePrism.ts
index d9c94a234..305066c3d 100644
--- a/src/modules/blocks/code/code-highlight/codePrism.ts
+++ b/src/modules/blocks/code/code-highlight/codePrism.ts
@@ -36,6 +36,10 @@ 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('.'))
return hFileExtensionsMap.hasOwnProperty(blockTitle) ? hFileExtensionsMap[blockTitle] : blockTitle;
diff --git a/src/modules/blocks/code/code-renderers/RenderCodeChartJS.tsx b/src/modules/blocks/code/code-renderers/RenderCodeChartJS.tsx
new file mode 100644
index 000000000..3f3191e00
--- /dev/null
+++ b/src/modules/blocks/code/code-renderers/RenderCodeChartJS.tsx
@@ -0,0 +1,160 @@
+import * as React from 'react';
+import { create } from 'zustand';
+import type { Chart as ChartType } from 'chart.js/auto';
+import { useQuery } from '@tanstack/react-query';
+import { Box, Typography } from '@mui/joy';
+import { patchSvgString } from '~/modules/blocks/code/code-renderers/RenderCodeSVG';
+import { diagramErrorSx, diagramSx } from '~/modules/blocks/code/code-renderers/RenderCodePlantUML';
+
+// configuration
+/**
+ * We are loading Chart.js from the CDN (and spending all the work to dynamically load it
+ * and strong type it), because the Chart.js dependencies (npm i chart.js) are too heavy
+ * and would slow down development for everyone.
+ */
+const CHARTJS_CDN_URL = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.js';
+const CHARTJS_ERROR_PREFIX = '[Chart.js]';
+
+
+// Store to react to loading of the library
+
+interface ChartJSApiState {
+ chartJSAPI: any /* FIXME */ | null;
+ loadingError: string | null;
+}
+
+export const useChartJSStore = create(() => ({
+ chartJSAPI: null,
+ loadingError: null,
+}));
+
+
+// Dynamic loading of the Chart.js library
+
+// extend the Window interface, to allow for the mermaid API to be found
+// declare global {
+// // noinspection JSUnusedGlobalSymbols
+// interface Window {
+// Chart: ;
+// }
+// }
+
+let loadingStarted = false;
+
+export function useChartJSLoader() {
+ const { chartJSAPI, loadingError } = useChartJSStore();
+
+ React.useEffect(() => {
+ if (!chartJSAPI && !loadingError && !loadingStarted) {
+ loadingStarted = true;
+ const script = document.createElement('script');
+ script.src = CHARTJS_CDN_URL;
+ script.async = true;
+ script.onload = () => {
+ if (window.Chart) {
+ useChartJSStore.setState({ chartJSAPI: window.Chart, loadingError: null });
+ } else {
+ useChartJSStore.setState({ chartJSAPI: null, loadingError: 'Chart.js failed to load.' });
+ }
+ };
+ script.onerror = () => {
+ useChartJSStore.setState({ chartJSAPI: null, loadingError: 'Failed to load Chart.js library.' });
+ };
+ document.head.appendChild(script);
+ }
+ }, [chartJSAPI, loadingError]);
+
+ return { chartJSAPI, loadingError };
+}
+
+
+
+
+
+interface RenderCodeChartJSProps {
+ chartJSCode: string;
+ fitScreen: boolean;
+}
+
+export function RenderCodeChartJS({ chartJSCode, fitScreen }: RenderCodeChartJSProps) {
+ const canvasRef = React.useRef(null);
+ const chartInstanceRef = React.useRef(null);
+
+ const { chartJSAPI, loadingError } = useChartJSLoader();
+
+ const [chartConfig, setChartConfig] = React.useState(null);
+ const [parseError, setParseError] = React.useState(null);
+ const [renderError, setRenderError] = React.useState(null);
+
+ // Parse the Chart.js configuration
+ React.useEffect(() => {
+ try {
+ const config = JSON.parse(chartJSCode) as import('chart.js').ChartConfiguration;
+ setChartConfig(config);
+ setParseError(null);
+ } catch (error: any) {
+ console.error('Chart.js configuration parse error:', error);
+ setChartConfig(null);
+ setParseError('Invalid Chart.js configuration: ' + (error.message || 'Unknown error.'));
+ }
+ }, [chartJSCode]);
+
+ // Render the chart
+ React.useEffect(() => {
+ if (chartJSAPI && chartConfig && canvasRef.current) {
+ try {
+ // Destroy previous chart instance
+ if (chartInstanceRef.current) {
+ chartInstanceRef.current.destroy();
+ }
+
+ // Create new chart instance
+ chartInstanceRef.current = new chartJSAPI(canvasRef.current, chartConfig);
+ setRenderError(null);
+ } catch (error: any) {
+ console.error('Chart.js rendering error:', error);
+ setRenderError('Error rendering chart: ' + (error.message || 'Unknown error.'));
+ }
+ }
+
+ return () => {
+ // Cleanup on unmount
+ if (chartInstanceRef.current) {
+ chartInstanceRef.current.destroy();
+ chartInstanceRef.current = null;
+ }
+ };
+ }, [chartJSAPI, chartConfig]);
+
+ // Handle error messages
+ if (loadingError) {
+ return (
+
+ {loadingError}
+
+ );
+ }
+
+ if (parseError) {
+ return (
+
+ {parseError}
+
+ );
+ }
+
+ if (renderError) {
+ return (
+
+ {renderError}
+
+ );
+ }
+
+ // Render the chart
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/modules/persona/pmix/pmix.ts b/src/modules/persona/pmix/pmix.ts
index 7af8955a2..55a0db983 100644
--- a/src/modules/persona/pmix/pmix.ts
+++ b/src/modules/persona/pmix/pmix.ts
@@ -83,6 +83,15 @@ export function bareBonesPromptMixer(_template: string, assistantLlmId: DLLMId |
// {{Prefer...}}
mixed = mixed.replace('{{PreferTables}}', 'Data presentation: prefer tables (auto-columns)');
// {{Render...}}
+ mixed = mixed.replace('{{RenderChartJS}}', `
+When presenting data that would be better visualized as a chart, output a ChartJS configuration object in this format:
+\`\`\`chartjs
+{
+ // Valid and complete ChartJS configuration JSON object (DO NOT USE FUNCTIONS)
+}
+\`\`\`
+Choose the most suitable chart type based on the data and context. Include only the JSON configuration, without any explanatory text. Ensure the JSON is valid and complete.
+`.trim());
mixed = mixed.replace('{{RenderMermaid}}', 'Mermaid rendering: Enabled for diagrams and pie charts and no other charts');
mixed = mixed.replace('{{RenderPlantUML}}', 'PlantUML rendering: Enabled');
mixed = mixed.replace('{{RenderHTML}}', `HTML in markdown rendering: Sleek HTML5 for ${Is.Desktop ? 'desktop' : 'mobile'} screens (self-contained with CSS/JS, leverage top libraries, external links OK)`);