mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
feat(x): inline charts in chat via a charts skill
The chat's markdown fence override now renders ```chart blocks (the same ChartBlockSchema notes use) as interactive recharts inline — the mermaid pattern. A new ChartRenderer handles line/bar/pie with multi-series support, light/dark theming, and a quiet placeholder while the fence body is still streaming. ChartBlockSchema.y widens to string | string[] for multi-series charts (notes chart block updated to map series too), with a chartSeries() helper in shared. A new 'charts' skill teaches the model the fence format, form selection (line/bar/pie, no mixed scales), and data honesty rules; loaded on demand like any skill.
This commit is contained in:
parent
efa6a0523c
commit
5acd825903
6 changed files with 256 additions and 4 deletions
|
|
@ -0,0 +1,74 @@
|
|||
export const skill = String.raw`
|
||||
# Charts
|
||||
|
||||
Load this skill when the user asks for a chart, graph, plot, trend, comparison, or "visualize" — or when data you've gathered (prices over time, counts per category, proportions) would land better as a picture than a table.
|
||||
|
||||
## How it works
|
||||
|
||||
Emit a fenced code block with language \`chart\` anywhere in your reply. The app renders it as an interactive chart inline (tooltips, legend, light/dark theming are handled for you — never pick colors yourself). Everything outside the fence is normal markdown.
|
||||
|
||||
\`\`\`\`
|
||||
\`\`\`chart
|
||||
{ ...JSON config... }
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
|
||||
## Config schema
|
||||
|
||||
- **\`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
|
||||
- **\`title\`** (optional): short heading shown above the chart
|
||||
|
||||
## Picking the form
|
||||
|
||||
- **line** — change over time (prices, counts by date). X is the time field.
|
||||
- **bar** — compare magnitudes across categories (issues per label, revenue per region).
|
||||
- **pie** — proportions of a whole; only with ≤ 6 slices, otherwise use a bar.
|
||||
- Two measures with very different scales (e.g. a $600 stock vs a $5 one): do NOT mix them on one chart — either normalize to % change and say so in the title, or emit two chart blocks.
|
||||
|
||||
## Examples
|
||||
|
||||
Multi-series line (comparison over time, normalized):
|
||||
|
||||
\`\`\`chart
|
||||
{
|
||||
"chart": "line",
|
||||
"title": "5-Day % Change",
|
||||
"x": "date",
|
||||
"y": ["AAPL", "NVDA", "SPY"],
|
||||
"data": [
|
||||
{ "date": "Jul 15", "AAPL": 0, "NVDA": 0, "SPY": 0 },
|
||||
{ "date": "Jul 16", "AAPL": -0.4, "NVDA": 1.2, "SPY": 0.1 },
|
||||
{ "date": "Jul 17", "AAPL": -1.1, "NVDA": 0.8, "SPY": -0.3 }
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Single-series bar:
|
||||
|
||||
\`\`\`chart
|
||||
{
|
||||
"chart": "bar",
|
||||
"title": "Open issues by area",
|
||||
"x": "area",
|
||||
"y": "count",
|
||||
"data": [
|
||||
{ "area": "sync", "count": 14 },
|
||||
{ "area": "editor", "count": 9 },
|
||||
{ "area": "billing", "count": 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.
|
||||
- Keep it readable: ≤ ~30 rows per chart, ≤ 6 series. Round values sensibly.
|
||||
- Numbers must be JSON numbers, not strings ("3.1", "5%" won't plot).
|
||||
- Follow the chart with one sentence of takeaway — what the reader should see in it.
|
||||
- One chart per distinct question; don't emit several variants of the same data.
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
@ -29,6 +29,7 @@ import backgroundTaskSkill from "./background-task/skill.js";
|
|||
import notifyUserSkill from "./notify-user/skill.js";
|
||||
import appsSkill from "./apps/skill.js";
|
||||
import slackSkill from "./slack/skill.js";
|
||||
import chartsSkill from "./charts/skill.js";
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CATALOG_PREFIX = "src/runtime/assembly/skills";
|
||||
|
|
@ -77,6 +78,12 @@ const definitions: SkillDefinition[] = [
|
|||
summary: "Prepare for meetings by gathering context about attendees from the knowledge base.",
|
||||
content: meetingPrepSkill,
|
||||
},
|
||||
{
|
||||
id: "charts",
|
||||
title: "Charts",
|
||||
summary: "Render interactive charts (line, bar, pie) inline in the chat reply. Use when the user asks for a chart/graph/visualization or when gathered data is clearer as a picture.",
|
||||
content: chartsSkill,
|
||||
},
|
||||
{
|
||||
id: "organize-files",
|
||||
title: "Organize Files",
|
||||
|
|
|
|||
|
|
@ -47,11 +47,18 @@ export const ChartBlockSchema = z.object({
|
|||
data: z.array(z.record(z.string(), z.unknown())).optional(),
|
||||
source: z.string().optional(),
|
||||
x: z.string(),
|
||||
y: z.string(),
|
||||
// One series (string) or several (array of data keys). Pie ignores all
|
||||
// but the first.
|
||||
y: z.union([z.string(), z.array(z.string()).min(1)]),
|
||||
});
|
||||
|
||||
export type ChartBlock = z.infer<typeof ChartBlockSchema>;
|
||||
|
||||
/** The y series list regardless of which form the block used. */
|
||||
export function chartSeries(block: ChartBlock): string[] {
|
||||
return Array.isArray(block.y) ? block.y : [block.y];
|
||||
}
|
||||
|
||||
export const TableBlockSchema = z.object({
|
||||
columns: z.array(z.string()),
|
||||
data: z.array(z.record(z.string(), z.unknown())),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue