labs/aicharts: fast mode (Clef draws first, LLM checks) as the demo default; clearer series/filter questions
Bench: Clef-flash 9/12 at ~0.5 s with uninformative confidence, Llama 12/12 at ~2.6 s. brain=fast draws Clef's chart at once and redraws only if the LLM's differs. Series and filter questions reworded around the three misses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDkN7ukK1kXkqov8FaZxva
This commit is contained in:
3 files changed
+38
-9
No files matched your search
@@ -169,7 +169,8 @@ export function attach(chart, { data, schema, endpoint, dataset }) {
|
|||||||
|
|
||||||
const Base = globalThis.HTMLElement ?? class {};
|
const Base = globalThis.HTMLElement ?? class {};
|
||||||
|
|
||||||
// <ai-chart endpoint="https://..." dataset="orders" brain="jev"></ai-chart> brain: llm | jev | clef (server default if unset)
|
// <ai-chart endpoint="https://..." dataset="orders" brain="fast"></ai-chart>
|
||||||
|
// brain: fast (decision model first, LLM checks) | llm | jev | clef (server default if unset)
|
||||||
// Needs Chart.js on the page (window.Chart). Fires "spec" after each answer and "save" when Save is pressed; set
|
// Needs Chart.js on the page (window.Chart). Fires "spec" after each answer and "save" when Save is pressed; set
|
||||||
// element.onSave = async ({ spec, prompt, dataset }) => ... to store charts yourself (the demo stores them in D1).
|
// element.onSave = async ({ spec, prompt, dataset }) => ... to store charts yourself (the demo stores them in D1).
|
||||||
export class AiChart extends Base {
|
export class AiChart extends Base {
|
||||||
@@ -202,7 +203,9 @@ export class AiChart extends Base {
|
|||||||
try {
|
try {
|
||||||
await this.load();
|
await this.load();
|
||||||
const brain = this.getAttribute("brain");
|
const brain = this.getAttribute("brain");
|
||||||
const { spec, ...info } = await fetchSpec(this.endpoint, { dataset: this.dataset, prompt, prior: this._spec, ...(brain ? { brain } : {}) });
|
const body = { dataset: this.dataset, prompt, prior: this._spec };
|
||||||
|
if (brain === "fast") return await this.askFast(body);
|
||||||
|
const { spec, ...info } = await fetchSpec(this.endpoint, { ...body, ...(brain ? { brain } : {}) });
|
||||||
await this.render(spec, prompt);
|
await this.render(spec, prompt);
|
||||||
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt, info } }));
|
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt, info } }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -210,6 +213,29 @@ export class AiChart extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// brain="fast": a decision model draws first (under a second), an LLM answers in the background, and the chart is
|
||||||
|
// redrawn only if the LLM's chart differs. Whichever comes back alone still draws.
|
||||||
|
async askFast(body) {
|
||||||
|
const ask = ++this._asks || (this._asks = 1);
|
||||||
|
const current = () => ask === this._asks;
|
||||||
|
const same = (a, b) => JSON.stringify({ ...a, title: 0 }) === JSON.stringify({ ...b, title: 0 });
|
||||||
|
const llm = fetchSpec(this.endpoint, { ...body, brain: "llm" }).catch((e) => ({ error: e }));
|
||||||
|
let drawn = null;
|
||||||
|
try {
|
||||||
|
const { spec, ...info } = await fetchSpec(this.endpoint, { ...body, brain: "clef", floor: 0 });
|
||||||
|
if (!current()) return;
|
||||||
|
await this.render(spec, body.prompt);
|
||||||
|
drawn = spec;
|
||||||
|
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt: body.prompt, info: { ...info, stage: "fast" } } }));
|
||||||
|
} catch { /* the LLM will answer */ }
|
||||||
|
const out = await llm;
|
||||||
|
if (!current() || out.error) { if (!drawn && out.error) throw out.error; return; }
|
||||||
|
const { spec, ...info } = out;
|
||||||
|
const changed = !drawn || !same(drawn, spec);
|
||||||
|
if (changed) await this.render(spec, body.prompt);
|
||||||
|
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt: body.prompt, info: { ...info, stage: drawn ? (changed ? "corrected" : "confirmed") : "llm" } } }));
|
||||||
|
}
|
||||||
|
|
||||||
async render(spec, prompt) {
|
async render(spec, prompt) {
|
||||||
await this.load();
|
await this.load();
|
||||||
const v = validateSpec(spec, this._schema);
|
const v = validateSpec(spec, this._schema);
|
||||||
|
|||||||
@@ -52,16 +52,18 @@ export function questions(info) {
|
|||||||
x: choice("Which column goes on the x axis (the categories or time the chart is broken down by)?", axis),
|
x: choice("Which column goes on the x axis (the categories or time the chart is broken down by)?", axis),
|
||||||
measure: choice("Which quantity does the chart measure?", measure),
|
measure: choice("Which quantity does the chart measure?", measure),
|
||||||
agg: choice("How should that quantity be combined within each group?", AGG_MENU),
|
agg: choice("How should that quantity be combined within each group?", AGG_MENU),
|
||||||
series: choice("Should the chart split into one coloured series per value of another column? Pick none unless the request compares groups against each other (\"web vs store\", \"by region over time\").",
|
series: choice("Does the request compare two or more values of a column side by side (\"web vs store\", \"split by channel\", \"each region over time\")? If so, pick that column; it becomes one coloured line or bar per value. Otherwise none.",
|
||||||
{ none: "One series only.", ...Object.fromEntries(text.map((c) => [c.name, `one series per ${c.name}`])) }),
|
{ none: "No comparison asked for: a single series.",
|
||||||
|
...Object.fromEntries(text.map((c) => [c.name, `Compare ${c.name}s (${(info.aggregates[c.name]?.values || []).join(" vs ")}): one series per ${c.name}.`])) }),
|
||||||
order: choice("How should the groups be ordered?", ORDER_MENU),
|
order: choice("How should the groups be ordered?", ORDER_MENU),
|
||||||
top: choice("Does the request limit how many groups to show?", TOP_MENU),
|
top: choice("Does the request limit how many groups to show?", TOP_MENU),
|
||||||
};
|
};
|
||||||
for (const c of text) {
|
for (const c of text) {
|
||||||
const vals = info.aggregates[c.name]?.values;
|
const vals = info.aggregates[c.name]?.values;
|
||||||
if (vals?.length && vals.length <= 30)
|
if (vals?.length && vals.length <= 30)
|
||||||
q[`only.${c.name}`] = choice(`Does the request restrict the data to one ${c.name}? Pick any unless it names one.`,
|
q[`only.${c.name}`] = choice(`Does the request name exactly one ${c.name} to look at (for example "${vals[0]} sales" or "in the ${vals[0]}")? Pick that value; pick any if it names none, or names several to compare.`,
|
||||||
{ any: `No restriction on ${c.name}.`, ...Object.fromEntries(vals.map((v) => [v, `Only ${c.name} = ${v}.`])) });
|
{ any: `The request names no single ${c.name}.`,
|
||||||
|
...Object.fromEntries(vals.map((v) => [v, `The request mentions "${v}": keep only rows where ${c.name} is ${v}.`])) });
|
||||||
}
|
}
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,8 +287,8 @@ h2{font-size:18px;margin:28px 0 8px}.row{display:flex;flex-wrap:wrap;gap:8px;ali
|
|||||||
</style></head><body><main>
|
</style></head><body><main>
|
||||||
<h1>AICharts</h1>
|
<h1>AICharts</h1>
|
||||||
<p class="lede">Ask for a chart in plain words. The model writes a small spec that is checked before anything draws, and never sees the rows: only column names, five sample rows and value ranges. Data: a sample tea shop, 24 months of orders.</p>
|
<p class="lede">Ask for a chart in plain words. The model writes a small spec that is checked before anything draws, and never sees the rows: only column names, five sample rows and value ranges. Data: a sample tea shop, 24 months of orders.</p>
|
||||||
<div class="row"><div class="chips" id="chips"></div><label class="brain">Brain <select id="brain"><option value="">default</option><option value="jev">Jev</option><option value="clef">Clef</option><option value="llm">LLM</option></select></label></div>
|
<div class="row"><div class="chips" id="chips"></div><label class="brain">Brain <select id="brain"><option value="fast" selected>Fast (Clef, LLM checks)</option><option value="clef">Clef only</option><option value="jev">Jev</option><option value="llm">LLM only</option></select></label></div>
|
||||||
<ai-chart dataset="orders"></ai-chart>
|
<ai-chart dataset="orders" brain="fast"></ai-chart>
|
||||||
<div class="meta" id="meta"></div>
|
<div class="meta" id="meta"></div>
|
||||||
<details><summary>Spec</summary><pre id="spec">Ask something first.</pre></details>
|
<details><summary>Spec</summary><pre id="spec">Ask something first.</pre></details>
|
||||||
<h2>Saved charts</h2><ul id="saved"><li>None yet.</li></ul>
|
<h2>Saved charts</h2><ul id="saved"><li>None yet.</li></ul>
|
||||||
@@ -310,7 +310,8 @@ brainSel.onchange = () => brainSel.value ? el.setAttribute("brain", brainSel.val
|
|||||||
el.addEventListener("spec", (e) => {
|
el.addEventListener("spec", (e) => {
|
||||||
const { spec, info = {} } = e.detail;
|
const { spec, info = {} } = e.detail;
|
||||||
document.getElementById("spec").textContent = JSON.stringify({ ...info, spec }, null, 2);
|
document.getElementById("spec").textContent = JSON.stringify({ ...info, spec }, null, 2);
|
||||||
const bits = [info.brain && "via " + info.brain + (info.handoff ? " (handed off: " + info.handoff.reason + ")" : ""),
|
const stage = { fast: "first draw", confirmed: "LLM agrees", corrected: "corrected by the LLM" }[info.stage];
|
||||||
|
const bits = [stage, info.brain && "via " + info.brain + (info.handoff ? " (handed off: " + info.handoff.reason + ")" : ""),
|
||||||
info.ms != null && info.ms + " ms", info.confidence != null && "confidence " + info.confidence].filter(Boolean);
|
info.ms != null && info.ms + " ms", info.confidence != null && "confidence " + info.confidence].filter(Boolean);
|
||||||
meta.replaceChildren(document.createTextNode(bits.join(" · ")));
|
meta.replaceChildren(document.createTextNode(bits.join(" · ")));
|
||||||
for (const alt of info.alternatives || []) {
|
for (const alt of info.alternatives || []) {
|
||||||
|
|||||||
Reference in new issue
Block a user