fix(x): chart fences with alias fields no longer hang on the placeholder

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.
This commit is contained in:
Gagan 2026-07-22 01:38:08 +05:30
parent 5acd825903
commit 099a47ac36
2 changed files with 53 additions and 7 deletions

View file

@ -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<string, unknown>
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 (
<div className="my-2 flex h-24 items-center justify-center gap-2 rounded-md border border-border bg-muted/30 text-xs text-muted-foreground">
<BarChart3 className="size-3.5" />
{invalid ? 'Chart config invalid — ask me to redraw it' : 'Chart has no data'}
</div>
)
}
if (!config) {
return (
<div className="my-2 flex h-24 items-center justify-center gap-2 rounded-md border border-border bg-muted/30 text-xs text-muted-foreground">
<BarChart3 className="size-3.5" />
@ -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,

View file

@ -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.