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.