mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
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:
parent
5acd825903
commit
099a47ac36
2 changed files with 53 additions and 7 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue