From 099a47ac36e7b14e572053760108b42d4117081f Mon Sep 17 00:00:00 2001 From: Gagan Date: Wed, 22 Jul 2026 01:38:08 +0530 Subject: [PATCH] fix(x): chart fences with alias fields no longer hang on the placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live testing showed models emitting pie configs with label/value instead of x/y, and long-format line data with a series column — schema-invalid fences sat on 'Preparing chart…' forever. The renderer now maps the label/value aliases before validating, distinguishes complete-but-invalid JSON (explicit 'config invalid' state) from still-streaming JSON (placeholder), and the skill spells out the exact field list, pie's x/y meaning, and wide-vs-long data format with a wrong/right example. --- .../src/components/chart-renderer.tsx | 33 ++++++++++++++++--- .../runtime/assembly/skills/charts/skill.ts | 27 +++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/apps/x/apps/renderer/src/components/chart-renderer.tsx b/apps/x/apps/renderer/src/components/chart-renderer.tsx index 2edb83c1..7ecc217a 100644 --- a/apps/x/apps/renderer/src/components/chart-renderer.tsx +++ b/apps/x/apps/renderer/src/components/chart-renderer.tsx @@ -33,15 +33,38 @@ export function ChartRenderer({ source }: ChartRendererProps) { const gridStroke = resolvedTheme === 'dark' ? '#3a3a38' : '#e4e4e0' const textColor = resolvedTheme === 'dark' ? '#c3c2b7' : '#52514e' - const config = useMemo(() => { + const { config, invalid } = useMemo(() => { + let parsed: unknown try { - return blocks.ChartBlockSchema.parse(JSON.parse(source)) + parsed = JSON.parse(source) } catch { - return null + // Incomplete JSON — the fence body is still streaming in. + return { config: null, invalid: false } } + // Models occasionally reach for pie-flavored field names despite the + // skill's schema; map the predictable aliases before validating. + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const obj = parsed as Record + if (obj.x === undefined && typeof obj.label === 'string') obj.x = obj.label + if (obj.y === undefined && typeof obj.value === 'string') obj.y = obj.value + } + const result = blocks.ChartBlockSchema.safeParse(parsed) + return result.success + ? { config: result.data, invalid: false } + : { config: null, invalid: true } }, [source]) - if (!config || !config.data || config.data.length === 0) { + // Complete JSON that fails the schema is a real error — say so instead of + // showing the streaming placeholder forever. + if (invalid || (config && (!config.data || config.data.length === 0))) { + return ( +
+ + {invalid ? 'Chart config invalid — ask me to redraw it' : 'Chart has no data'} +
+ ) + } + if (!config) { return (
@@ -50,7 +73,7 @@ export function ChartRenderer({ source }: ChartRendererProps) { ) } - const data = config.data + const data = config.data ?? [] const series = blocks.chartSeries(config) const axisProps = { stroke: textColor, diff --git a/apps/x/packages/core/src/runtime/assembly/skills/charts/skill.ts b/apps/x/packages/core/src/runtime/assembly/skills/charts/skill.ts index 4e071609..71ac3110 100644 --- a/apps/x/packages/core/src/runtime/assembly/skills/charts/skill.ts +++ b/apps/x/packages/core/src/runtime/assembly/skills/charts/skill.ts @@ -15,12 +15,19 @@ Emit a fenced code block with language \`chart\` anywhere in your reply. The app ## Config schema +The ONLY fields are \`chart\`, \`data\`, \`x\`, \`y\`, \`title\` — nothing else exists (no \`label\`, \`value\`, \`series\`, \`color\`, etc.). Unknown fields are ignored; a missing \`x\` or \`y\` breaks the chart. + - **\`chart\`** (required): \`"line"\` | \`"bar"\` | \`"pie"\` - **\`data\`** (required): array of flat objects — the rows to plot. Put REAL values you gathered this turn here; never invent numbers. -- **\`x\`** (required): key of the label/category field in each row -- **\`y\`** (required): key of the value field — a string for one series, or an array of keys for several series on one chart +- **\`x\`** (required): key of the label/category field in each row. For pie: the slice-name key. +- **\`y\`** (required): key of the value field — a string for one series, or an array of keys for several series on one chart. For pie: the slice-value key. - **\`title\`** (optional): short heading shown above the chart +**Data must be wide-format**: one row per x value, one key per series. Never long/tidy format (a row per series-point with a series-name column) — multiple series means multiple keys in the SAME row: + +WRONG: \`[{ "day": "Mon", "index": "S&P", "pct": 0.1 }, { "day": "Mon", "index": "Dow", "pct": 0.2 }]\` +RIGHT: \`[{ "day": "Mon", "S&P": 0.1, "Dow": 0.2 }]\` with \`"y": ["S&P", "Dow"]\` + ## Picking the form - **line** — change over time (prices, counts by date). X is the time field. @@ -62,6 +69,22 @@ Single-series bar: } \`\`\` +Pie (x names the slice, y is its value — same keys as every other chart): + +\`\`\`chart +{ + "chart": "pie", + "title": "Time spent by project", + "x": "project", + "y": "hours", + "data": [ + { "project": "Alpha", "hours": 14 }, + { "project": "Beta", "hours": 6 }, + { "project": "Internal", "hours": 3 } + ] +} +\`\`\` + ## Rules - Data must come from what you actually fetched or computed this turn — never fabricate points to make a chart possible. Too little real data? Say so instead of charting.