@agent-native/core 0.128.3 → 0.128.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/chat/legacy-chart-shorthand.tsx +479 -0
- package/corpus/core/src/client/chat/markdown-renderer.tsx +23 -3
- package/corpus/core/src/extensions/actions.ts +1 -1
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +12 -0
- package/corpus/templates/analytics/actions/generate-chart.ts +2 -2
- package/corpus/templates/chat/README.md +2 -0
- package/dist/client/chat/legacy-chart-shorthand.d.ts +29 -0
- package/dist/client/chat/legacy-chart-shorthand.d.ts.map +1 -0
- package/dist/client/chat/legacy-chart-shorthand.js +330 -0
- package/dist/client/chat/legacy-chart-shorthand.js.map +1 -0
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +8 -2
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.js +1 -1
- package/dist/extensions/actions.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/README.md +2 -0
- package/package.json +1 -1
- package/src/client/chat/legacy-chart-shorthand.tsx +479 -0
- package/src/client/chat/markdown-renderer.tsx +23 -3
- package/src/extensions/actions.ts +1 -1
- package/src/templates/chat/README.md +2 -0
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.128.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e22b660: Render a best-effort inline chart when an agent emits a hallucinated `/word ... labels=[...] data=[...]` line in chat instead of the documented ```embed fence. Previously that line rendered as inert literal text with no chart. Detection is generic (not tied to any single template's tool name), rejects malformed input (mismatched lengths, negative values, oversized arrays) by falling back to plain text, and correctly skips content inside fenced/indented code blocks.
|
|
8
|
+
- f261c10: Warn in the `update-extension` tool description against inlining large static datasets into extension HTML/JS and against oversized single edit/replace payloads, since a payload over roughly 8KB risks the model truncating its own `payloadJson` mid-generation and arriving as an empty or malformed call that stalls the run.
|
|
9
|
+
|
|
3
10
|
## 0.128.3
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.128.
|
|
3
|
+
"version": "0.128.4",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// Some tool schemas expose chart parameters named `type`/`title`/`labels`/
|
|
2
|
+
// `data` (e.g. a chart-generation action). Models occasionally regress to
|
|
3
|
+
// typing those parameter names as a bare chat line instead of calling the
|
|
4
|
+
// tool or emitting a real ```embed fence, e.g.:
|
|
5
|
+
// /chart type=bar title="..." labels=["Mon","Tue"] data=[5,8]
|
|
6
|
+
// That has no real markdown syntax and would otherwise render as inert text.
|
|
7
|
+
// This module detects that generic shape (any "/word ... labels=[...]
|
|
8
|
+
// data=[...]" line) and renders a best-effort inline chart so the user still
|
|
9
|
+
// sees something useful, without hardcoding any single template's tool name.
|
|
10
|
+
|
|
11
|
+
import React from "react";
|
|
12
|
+
|
|
13
|
+
export const LEGACY_CHART_SHORTHAND_LANG = "chart-shorthand";
|
|
14
|
+
|
|
15
|
+
const LEGACY_CHART_PALETTE = [
|
|
16
|
+
"#6366f1",
|
|
17
|
+
"#22c55e",
|
|
18
|
+
"#f59e0b",
|
|
19
|
+
"#ef4444",
|
|
20
|
+
"#0ea5e9",
|
|
21
|
+
"#a855f7",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const SAFE_HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
|
|
25
|
+
const BACKSLASH = String.fromCharCode(92);
|
|
26
|
+
const MAX_LINE_LENGTH = 20_000;
|
|
27
|
+
const MAX_LABELS = 60;
|
|
28
|
+
const MAX_SERIES = 6;
|
|
29
|
+
|
|
30
|
+
export interface LegacyChartShorthand {
|
|
31
|
+
type: "bar" | "line" | "area";
|
|
32
|
+
title: string;
|
|
33
|
+
labels: string[];
|
|
34
|
+
series: { label: string; data: number[]; color?: string }[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Extracts a balanced JSON array/object starting exactly at `startIndex`
|
|
38
|
+
// (which must point at "["), tracking string/escape state so brackets or
|
|
39
|
+
// braces inside JSON string values don't confuse the boundary.
|
|
40
|
+
function extractBalancedArrayAt(
|
|
41
|
+
text: string,
|
|
42
|
+
startIndex: number,
|
|
43
|
+
): string | null {
|
|
44
|
+
if (text[startIndex] !== "[") return null;
|
|
45
|
+
let depth = 0;
|
|
46
|
+
let inString = false;
|
|
47
|
+
for (let idx = startIndex; idx < text.length; idx++) {
|
|
48
|
+
const ch = text[idx];
|
|
49
|
+
if (inString) {
|
|
50
|
+
if (ch === "\\")
|
|
51
|
+
idx++; // skip escaped character, including \"
|
|
52
|
+
else if (ch === '"') inString = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '"') inString = true;
|
|
56
|
+
else if (ch === "[" || ch === "{") depth++;
|
|
57
|
+
else if (ch === "]" || ch === "}") {
|
|
58
|
+
depth--;
|
|
59
|
+
if (depth === 0) return text.slice(startIndex, idx + 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Marks which character indices fall inside a JSON/quoted-string literal so
|
|
66
|
+
// key-token scanning can ignore false matches such as a key= that's part of
|
|
67
|
+
// a title string, or embedded in a label's own text, rather than a real
|
|
68
|
+
// assignment in the line.
|
|
69
|
+
function computeStringMask(text: string): boolean[] {
|
|
70
|
+
const mask = new Array<boolean>(text.length).fill(false);
|
|
71
|
+
let inString = false;
|
|
72
|
+
for (let i = 0; i < text.length; i++) {
|
|
73
|
+
mask[i] = inString;
|
|
74
|
+
const ch = text[i];
|
|
75
|
+
if (inString) {
|
|
76
|
+
if (ch === BACKSLASH) {
|
|
77
|
+
i++;
|
|
78
|
+
if (i < text.length) mask[i] = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '"') inString = false;
|
|
82
|
+
} else if (ch === '"') {
|
|
83
|
+
inString = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return mask;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Finds the array value immediately following a `key=` token, scanning every
|
|
90
|
+
// occurrence of `key=` outside of quoted strings (not just the first) so a
|
|
91
|
+
// `key=` that appears inside an unrelated quoted string (e.g.
|
|
92
|
+
// title="data=quality", or a label literally containing "data=[1,2]") is
|
|
93
|
+
// skipped in favor of the real one. Only whitespace is allowed between
|
|
94
|
+
// `key=` and the `[`.
|
|
95
|
+
function extractArrayForKey(
|
|
96
|
+
text: string,
|
|
97
|
+
key: "labels" | "data",
|
|
98
|
+
): string | null {
|
|
99
|
+
const mask = computeStringMask(text);
|
|
100
|
+
const re = new RegExp(`\\b${key}=`, "g");
|
|
101
|
+
let match: RegExpExecArray | null;
|
|
102
|
+
while ((match = re.exec(text))) {
|
|
103
|
+
if (mask[match.index]) continue; // this key= is inside a string literal
|
|
104
|
+
let idx = match.index + match[0].length;
|
|
105
|
+
while (idx < text.length && /\s/.test(text[idx])) idx++;
|
|
106
|
+
const arr = extractBalancedArrayAt(text, idx);
|
|
107
|
+
if (arr) return arr;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extracts the value of a `title="..."`/`title='...'` token, honoring
|
|
113
|
+
// backslash-escaped quotes and backslashes inside the string (e.g.
|
|
114
|
+
// title="Sales \"Metrics\"") instead of stopping at the first escaped quote.
|
|
115
|
+
function extractLegacyChartTitle(text: string): string {
|
|
116
|
+
const match = /\btitle=/.exec(text);
|
|
117
|
+
if (!match) return "";
|
|
118
|
+
let idx = match.index + match[0].length;
|
|
119
|
+
const quoteChar = text[idx];
|
|
120
|
+
if (quoteChar !== '"' && quoteChar !== "'") return "";
|
|
121
|
+
idx++;
|
|
122
|
+
let result = "";
|
|
123
|
+
while (idx < text.length) {
|
|
124
|
+
const ch = text[idx];
|
|
125
|
+
if (ch === BACKSLASH && idx + 1 < text.length) {
|
|
126
|
+
const next = text[idx + 1];
|
|
127
|
+
if (next === quoteChar || next === BACKSLASH) {
|
|
128
|
+
result += next;
|
|
129
|
+
idx += 2;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (ch === quoteChar) return result;
|
|
134
|
+
result += ch;
|
|
135
|
+
idx++;
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Cheap-ish pre-check so callers can skip the full parse on ordinary text.
|
|
142
|
+
* Requires `labels=`/`data=` to actually resolve to bracketed arrays (not
|
|
143
|
+
* just appear as substrings) so unrelated slash commands or API paths that
|
|
144
|
+
* happen to mention both words aren't diverted out of normal markdown
|
|
145
|
+
* rendering.
|
|
146
|
+
*/
|
|
147
|
+
export function looksLikeLegacyChartShorthand(line: string): boolean {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
if (trimmed.length === 0 || trimmed.length > MAX_LINE_LENGTH) return false;
|
|
150
|
+
if (!/^\/[a-zA-Z][\w-]*\b/.test(trimmed)) return false;
|
|
151
|
+
if (!/\blabels=/.test(trimmed) || !/\bdata=/.test(trimmed)) return false;
|
|
152
|
+
return (
|
|
153
|
+
extractArrayForKey(trimmed, "labels") !== null &&
|
|
154
|
+
extractArrayForKey(trimmed, "data") !== null
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function parseLegacyChartShorthand(
|
|
159
|
+
rawLine: string,
|
|
160
|
+
): LegacyChartShorthand | null {
|
|
161
|
+
const trimmed = rawLine.trim();
|
|
162
|
+
if (!looksLikeLegacyChartShorthand(trimmed)) return null;
|
|
163
|
+
|
|
164
|
+
const typeMatch = trimmed.match(/\btype=(bar|line|area)\b/i);
|
|
165
|
+
const chartType =
|
|
166
|
+
(typeMatch?.[1].toLowerCase() as LegacyChartShorthand["type"]) || "bar";
|
|
167
|
+
|
|
168
|
+
const chartTitle = extractLegacyChartTitle(trimmed);
|
|
169
|
+
|
|
170
|
+
const labelsRaw = extractArrayForKey(trimmed, "labels");
|
|
171
|
+
const dataRaw = extractArrayForKey(trimmed, "data");
|
|
172
|
+
if (!labelsRaw || !dataRaw) return null;
|
|
173
|
+
|
|
174
|
+
let parsedLabels: unknown;
|
|
175
|
+
let parsedData: unknown;
|
|
176
|
+
try {
|
|
177
|
+
parsedLabels = JSON.parse(labelsRaw);
|
|
178
|
+
parsedData = JSON.parse(dataRaw);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
!Array.isArray(parsedLabels) ||
|
|
184
|
+
parsedLabels.length === 0 ||
|
|
185
|
+
parsedLabels.length > MAX_LABELS ||
|
|
186
|
+
// Reject nested arrays/objects as labels — String(nestedArray) recurses
|
|
187
|
+
// through Array.prototype.join and can throw on deeply nested input.
|
|
188
|
+
!parsedLabels.every((l) =>
|
|
189
|
+
["string", "number", "boolean"].includes(typeof l),
|
|
190
|
+
)
|
|
191
|
+
) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const safeLabels = parsedLabels.map((l) => String(l));
|
|
195
|
+
|
|
196
|
+
const isValidSeriesData = (values: unknown): values is number[] =>
|
|
197
|
+
Array.isArray(values) &&
|
|
198
|
+
values.length === safeLabels.length &&
|
|
199
|
+
values.every((v) => typeof v === "number" && Number.isFinite(v) && v >= 0);
|
|
200
|
+
|
|
201
|
+
const colorMatch = trimmed.match(/(?:^|\s)color=(#[0-9a-fA-F]{3,8})\b/);
|
|
202
|
+
const topLevelColor =
|
|
203
|
+
colorMatch && SAFE_HEX_COLOR.test(colorMatch[1])
|
|
204
|
+
? colorMatch[1]
|
|
205
|
+
: undefined;
|
|
206
|
+
|
|
207
|
+
let chartSeries: LegacyChartShorthand["series"];
|
|
208
|
+
if (isValidSeriesData(parsedData)) {
|
|
209
|
+
chartSeries = [
|
|
210
|
+
{ label: chartTitle || "Value", data: parsedData, color: topLevelColor },
|
|
211
|
+
];
|
|
212
|
+
} else if (
|
|
213
|
+
Array.isArray(parsedData) &&
|
|
214
|
+
parsedData.length > 0 &&
|
|
215
|
+
// Reject rather than silently truncate an over-limit series count:
|
|
216
|
+
// presenting a partial chart as complete is worse than plain text.
|
|
217
|
+
parsedData.length <= MAX_SERIES &&
|
|
218
|
+
parsedData.every(
|
|
219
|
+
(d) =>
|
|
220
|
+
d &&
|
|
221
|
+
typeof d === "object" &&
|
|
222
|
+
isValidSeriesData((d as { data?: unknown }).data),
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
225
|
+
chartSeries = (
|
|
226
|
+
parsedData as { label?: unknown; data: number[]; color?: unknown }[]
|
|
227
|
+
).map((d, idx) => ({
|
|
228
|
+
label: typeof d.label === "string" ? d.label : `Series ${idx + 1}`,
|
|
229
|
+
data: d.data,
|
|
230
|
+
color:
|
|
231
|
+
typeof d.color === "string" && SAFE_HEX_COLOR.test(d.color)
|
|
232
|
+
? d.color
|
|
233
|
+
: undefined,
|
|
234
|
+
}));
|
|
235
|
+
} else {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (chartSeries.length === 0) return null;
|
|
239
|
+
return {
|
|
240
|
+
type: chartType,
|
|
241
|
+
title: chartTitle,
|
|
242
|
+
labels: safeLabels,
|
|
243
|
+
series: chartSeries,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Wraps any line matching the legacy shorthand shape in a fenced code block
|
|
248
|
+
// tagged `chart-shorthand`, so it routes through markdownComponents.pre()
|
|
249
|
+
// like any other language fence instead of rendering as inert prose.
|
|
250
|
+
// Fence/indented-code tracking mirrors ../../shared/markdown-block-split.ts
|
|
251
|
+
// (marker char + length, indented-code detection) so real code blocks —
|
|
252
|
+
// including ~~~ fences, longer-than-3 fences, and 4-space/tab indented code —
|
|
253
|
+
// are never mistaken for chat prose and rewritten.
|
|
254
|
+
export function wrapLegacyChartShorthandLines(markdown: string): string {
|
|
255
|
+
if (!markdown.includes("labels=") || !markdown.includes("data=")) {
|
|
256
|
+
return markdown;
|
|
257
|
+
}
|
|
258
|
+
let fenceMarker = ""; // non-empty while inside a ``` or ~~~ fence
|
|
259
|
+
const lines = markdown.split("\n");
|
|
260
|
+
const out: string[] = [];
|
|
261
|
+
for (const line of lines) {
|
|
262
|
+
// CommonMark: a line indented 4+ spaces (or a tab) is an indented code
|
|
263
|
+
// block, not a fence marker. Check this first so a literal four-space
|
|
264
|
+
// " ```" example in a message doesn't toggle fence state.
|
|
265
|
+
if (/^(?: {4}|\t)/.test(line)) {
|
|
266
|
+
out.push(line);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const trimmed = line.trimStart();
|
|
270
|
+
if (fenceMarker) {
|
|
271
|
+
out.push(line);
|
|
272
|
+
const closeMatch = /^(`{3,}|~{3,})\s*$/.exec(trimmed);
|
|
273
|
+
if (
|
|
274
|
+
closeMatch &&
|
|
275
|
+
closeMatch[1].charAt(0) === fenceMarker.charAt(0) &&
|
|
276
|
+
closeMatch[1].length >= fenceMarker.length
|
|
277
|
+
) {
|
|
278
|
+
fenceMarker = "";
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const openMatch = /^(`{3,}|~{3,})/.exec(trimmed);
|
|
283
|
+
if (openMatch) {
|
|
284
|
+
fenceMarker = openMatch[1].charAt(0).repeat(openMatch[1].length);
|
|
285
|
+
out.push(line);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (looksLikeLegacyChartShorthand(line)) {
|
|
289
|
+
// Preserve the line's own indentation on the emitted fence so
|
|
290
|
+
// shorthand inside a list item or blockquote continuation stays part
|
|
291
|
+
// of that container instead of being dedented to a top-level block.
|
|
292
|
+
const leadingWs = line.match(/^\s*/)?.[0] ?? "";
|
|
293
|
+
out.push(
|
|
294
|
+
leadingWs + "```" + LEGACY_CHART_SHORTHAND_LANG,
|
|
295
|
+
leadingWs + line.trim(),
|
|
296
|
+
leadingWs + "```",
|
|
297
|
+
);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
out.push(line);
|
|
301
|
+
}
|
|
302
|
+
return out.join("\n");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function LegacyChartShorthandFallback({ text }: { text: string }) {
|
|
306
|
+
return <span style={{ whiteSpace: "pre-wrap" }}>{text}</span>;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function LegacyChartShorthandChart({
|
|
310
|
+
parsed,
|
|
311
|
+
}: {
|
|
312
|
+
parsed: LegacyChartShorthand;
|
|
313
|
+
}) {
|
|
314
|
+
const { title, labels, series, type } = parsed;
|
|
315
|
+
const width = 640;
|
|
316
|
+
const height = 280;
|
|
317
|
+
const padding = { top: 16, right: 16, bottom: 44, left: 48 };
|
|
318
|
+
const innerW = width - padding.left - padding.right;
|
|
319
|
+
const innerH = height - padding.top - padding.bottom;
|
|
320
|
+
const maxVal = Math.max(
|
|
321
|
+
1,
|
|
322
|
+
series.reduce(
|
|
323
|
+
(acc, s) => s.data.reduce((seriesAcc, v) => Math.max(seriesAcc, v), acc),
|
|
324
|
+
0,
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
const groupW = innerW / labels.length;
|
|
328
|
+
const gridLineCount = 4;
|
|
329
|
+
const gridLines = Array.from({ length: gridLineCount + 1 }, (_, g) => {
|
|
330
|
+
const y = padding.top + (innerH * g) / gridLineCount;
|
|
331
|
+
const val = Math.round(maxVal - (maxVal * g) / gridLineCount);
|
|
332
|
+
return { y, val };
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const colorFor = (s: LegacyChartShorthand["series"][number], si: number) =>
|
|
336
|
+
s.color || LEGACY_CHART_PALETTE[si % LEGACY_CHART_PALETTE.length];
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div className="my-4 rounded-lg border border-border bg-muted/20 p-3">
|
|
340
|
+
{title && (
|
|
341
|
+
<div className="mb-1 text-sm font-medium text-foreground">{title}</div>
|
|
342
|
+
)}
|
|
343
|
+
<svg
|
|
344
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
345
|
+
className="w-full text-muted-foreground"
|
|
346
|
+
role="img"
|
|
347
|
+
aria-label={title || "Chart"}
|
|
348
|
+
>
|
|
349
|
+
{gridLines.map(({ y, val }, i) => (
|
|
350
|
+
<React.Fragment key={i}>
|
|
351
|
+
<line
|
|
352
|
+
x1={padding.left}
|
|
353
|
+
y1={y}
|
|
354
|
+
x2={width - padding.right}
|
|
355
|
+
y2={y}
|
|
356
|
+
stroke="currentColor"
|
|
357
|
+
strokeOpacity={0.1}
|
|
358
|
+
/>
|
|
359
|
+
<text
|
|
360
|
+
x={padding.left - 8}
|
|
361
|
+
y={y + 4}
|
|
362
|
+
textAnchor="end"
|
|
363
|
+
fontSize={10}
|
|
364
|
+
fill="currentColor"
|
|
365
|
+
fillOpacity={0.6}
|
|
366
|
+
>
|
|
367
|
+
{val}
|
|
368
|
+
</text>
|
|
369
|
+
</React.Fragment>
|
|
370
|
+
))}
|
|
371
|
+
|
|
372
|
+
{type === "bar" &&
|
|
373
|
+
labels.map((_, li) => {
|
|
374
|
+
const groupPad = groupW * 0.15;
|
|
375
|
+
const barsW = groupW - groupPad * 2;
|
|
376
|
+
const barW = barsW / series.length;
|
|
377
|
+
return series.map((s, si) => {
|
|
378
|
+
const value = s.data[li] ?? 0;
|
|
379
|
+
const barH = (Math.max(0, value) / maxVal) * innerH;
|
|
380
|
+
const x = padding.left + li * groupW + groupPad + si * barW;
|
|
381
|
+
const y = padding.top + (innerH - barH);
|
|
382
|
+
return (
|
|
383
|
+
<rect
|
|
384
|
+
key={`${li}-${si}`}
|
|
385
|
+
x={x}
|
|
386
|
+
y={y}
|
|
387
|
+
width={Math.max(1, barW - 2)}
|
|
388
|
+
height={Math.max(0, barH)}
|
|
389
|
+
fill={colorFor(s, si)}
|
|
390
|
+
rx={2}
|
|
391
|
+
/>
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
})}
|
|
395
|
+
|
|
396
|
+
{type !== "bar" &&
|
|
397
|
+
series.map((s, si) => {
|
|
398
|
+
const coords = labels.map((_, li) => {
|
|
399
|
+
const value = s.data[li] ?? 0;
|
|
400
|
+
const x = padding.left + groupW * li + groupW / 2;
|
|
401
|
+
const y =
|
|
402
|
+
padding.top + innerH - (Math.max(0, value) / maxVal) * innerH;
|
|
403
|
+
return { x, y };
|
|
404
|
+
});
|
|
405
|
+
if (coords.length === 1) {
|
|
406
|
+
// A single point has no line segment to draw — render a marker
|
|
407
|
+
// instead of an invisible zero-length polyline.
|
|
408
|
+
return (
|
|
409
|
+
<circle
|
|
410
|
+
key={si}
|
|
411
|
+
cx={coords[0].x}
|
|
412
|
+
cy={coords[0].y}
|
|
413
|
+
r={4}
|
|
414
|
+
fill={colorFor(s, si)}
|
|
415
|
+
/>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const points = coords
|
|
419
|
+
.map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`)
|
|
420
|
+
.join(" ");
|
|
421
|
+
const baseY = padding.top + innerH;
|
|
422
|
+
const firstX = padding.left + groupW / 2;
|
|
423
|
+
const lastX =
|
|
424
|
+
padding.left + groupW * (labels.length - 1) + groupW / 2;
|
|
425
|
+
return (
|
|
426
|
+
<React.Fragment key={si}>
|
|
427
|
+
{type === "area" && (
|
|
428
|
+
<polygon
|
|
429
|
+
points={`${firstX.toFixed(1)},${baseY.toFixed(1)} ${points} ${lastX.toFixed(1)},${baseY.toFixed(1)}`}
|
|
430
|
+
fill={colorFor(s, si)}
|
|
431
|
+
fillOpacity={0.15}
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
<polyline
|
|
435
|
+
points={points}
|
|
436
|
+
fill="none"
|
|
437
|
+
stroke={colorFor(s, si)}
|
|
438
|
+
strokeWidth={2}
|
|
439
|
+
/>
|
|
440
|
+
</React.Fragment>
|
|
441
|
+
);
|
|
442
|
+
})}
|
|
443
|
+
|
|
444
|
+
{labels.map((label, li) => {
|
|
445
|
+
const x = padding.left + groupW * li + groupW / 2;
|
|
446
|
+
const y = height - padding.bottom + 16;
|
|
447
|
+
const truncated =
|
|
448
|
+
label.length > 12 ? `${label.slice(0, 11)}...` : label;
|
|
449
|
+
return (
|
|
450
|
+
<text
|
|
451
|
+
key={li}
|
|
452
|
+
x={x}
|
|
453
|
+
y={y}
|
|
454
|
+
textAnchor="middle"
|
|
455
|
+
fontSize={10}
|
|
456
|
+
fill="currentColor"
|
|
457
|
+
fillOpacity={0.7}
|
|
458
|
+
>
|
|
459
|
+
{truncated}
|
|
460
|
+
</text>
|
|
461
|
+
);
|
|
462
|
+
})}
|
|
463
|
+
</svg>
|
|
464
|
+
{series.length > 1 && (
|
|
465
|
+
<div className="mt-2 flex flex-wrap gap-3 text-xs text-muted-foreground">
|
|
466
|
+
{series.map((s, si) => (
|
|
467
|
+
<span key={si} className="inline-flex items-center gap-1">
|
|
468
|
+
<span
|
|
469
|
+
className="inline-block h-2 w-2 rounded-full"
|
|
470
|
+
style={{ background: colorFor(s, si) }}
|
|
471
|
+
/>
|
|
472
|
+
{s.label}
|
|
473
|
+
</span>
|
|
474
|
+
))}
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
@@ -32,6 +32,13 @@ import { NEW_CHAT_ACTION_HREF } from "../error-format.js";
|
|
|
32
32
|
import { HighlightedCodeBlock as SharedHighlightedCodeBlock } from "../HighlightedCodeBlock.js";
|
|
33
33
|
import { IframeEmbed, parseEmbedBody } from "../IframeEmbed.js";
|
|
34
34
|
import { cn } from "../utils.js";
|
|
35
|
+
import {
|
|
36
|
+
LEGACY_CHART_SHORTHAND_LANG,
|
|
37
|
+
LegacyChartShorthandChart,
|
|
38
|
+
LegacyChartShorthandFallback,
|
|
39
|
+
parseLegacyChartShorthand,
|
|
40
|
+
wrapLegacyChartShorthandLines,
|
|
41
|
+
} from "./legacy-chart-shorthand.js";
|
|
35
42
|
|
|
36
43
|
// ─── Lazy markdown loader ────────────────────────────────────────────────────
|
|
37
44
|
// react-markdown + remark-gfm are deferred so they stay off the critical path
|
|
@@ -284,6 +291,19 @@ export const markdownComponents = {
|
|
|
284
291
|
<IframeEmbed {...(parsed as Parameters<typeof IframeEmbed>[0])} />
|
|
285
292
|
);
|
|
286
293
|
}
|
|
294
|
+
if (
|
|
295
|
+
new RegExp(`\\blanguage-${LEGACY_CHART_SHORTHAND_LANG}\\b`).test(
|
|
296
|
+
className,
|
|
297
|
+
)
|
|
298
|
+
) {
|
|
299
|
+
const body = extractCodeText(childProps.children).replace(/\n$/, "");
|
|
300
|
+
const parsed = parseLegacyChartShorthand(body);
|
|
301
|
+
return parsed ? (
|
|
302
|
+
<LegacyChartShorthandChart parsed={parsed} />
|
|
303
|
+
) : (
|
|
304
|
+
<LegacyChartShorthandFallback text={body} />
|
|
305
|
+
);
|
|
306
|
+
}
|
|
287
307
|
const langMatch = className.match(/\blanguage-([\w+-]+)\b/);
|
|
288
308
|
if (langMatch) {
|
|
289
309
|
const code = extractCodeText(childProps.children).replace(/\n$/, "");
|
|
@@ -589,7 +609,7 @@ export const MemoizedMarkdownBlock = React.memo(function MemoizedMarkdownBlock({
|
|
|
589
609
|
components={markdownComponents}
|
|
590
610
|
urlTransform={markdownUrlTransform}
|
|
591
611
|
>
|
|
592
|
-
{blockText}
|
|
612
|
+
{wrapLegacyChartShorthandLines(blockText)}
|
|
593
613
|
</ReactMarkdown>
|
|
594
614
|
);
|
|
595
615
|
});
|
|
@@ -642,7 +662,7 @@ export function SmoothMarkdownText({
|
|
|
642
662
|
components={markdownComponents}
|
|
643
663
|
urlTransform={markdownUrlTransform}
|
|
644
664
|
>
|
|
645
|
-
{split.tail}
|
|
665
|
+
{wrapLegacyChartShorthandLines(split.tail)}
|
|
646
666
|
</ReactMarkdown>
|
|
647
667
|
) : null}
|
|
648
668
|
</>
|
|
@@ -653,7 +673,7 @@ export function SmoothMarkdownText({
|
|
|
653
673
|
components={markdownComponents}
|
|
654
674
|
urlTransform={markdownUrlTransform}
|
|
655
675
|
>
|
|
656
|
-
{visibleText}
|
|
676
|
+
{wrapLegacyChartShorthandLines(visibleText)}
|
|
657
677
|
</ReactMarkdown>
|
|
658
678
|
)
|
|
659
679
|
) : (
|
|
@@ -623,7 +623,7 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
|
|
|
623
623
|
"update-extension": {
|
|
624
624
|
tool: {
|
|
625
625
|
description:
|
|
626
|
-
'Update an existing sandboxed Alpine.js mini-app extension. Pass exactly three fields: `id`, `operation`, and `payloadJson`. `payloadJson` is a JSON object encoded as a string so model gateways cannot fill its optional members with invalid empty placeholders. For operation="edit", pass {"edits":[...]} or {"patches":[...]} and optional format=true. For operation="replace", pass exactly one of content, contentFromAttachment, or contentFromWorkspaceFile; use this operation only for a user-requested broad visual rewrite or complete replacement body. For operation="metadata", pass name, description, or icon. Change sharing through set-resource-visibility instead. If the user is viewing the extension, use the extensionId from <current-screen> or <current-url> directly.',
|
|
626
|
+
'Update an existing sandboxed Alpine.js mini-app extension. Pass exactly three fields: `id`, `operation`, and `payloadJson`. `payloadJson` is a JSON object encoded as a string so model gateways cannot fill its optional members with invalid empty placeholders. For operation="edit", pass {"edits":[...]} or {"patches":[...]} and optional format=true — prefer several small, targeted edits over one large one; a single edit/replace body over roughly 8KB risks the model truncating its own payloadJson mid-generation, which arrives here as an empty or malformed call that must be retried from scratch. For operation="replace", pass exactly one of content, contentFromAttachment, or contentFromWorkspaceFile; use this operation only for a user-requested broad visual rewrite or complete replacement body. Do not inline large static datasets (full metric histories, big arrays) into the HTML/JS body — store them with extension-data-set and fetch them at runtime via extensionData.list()/get() instead, so edits stay small regardless of dataset size. For operation="metadata", pass name, description, or icon. Change sharing through set-resource-visibility instead. If the user is viewing the extension, use the extensionId from <current-screen> or <current-url> directly.',
|
|
627
627
|
parameters: {
|
|
628
628
|
type: "object",
|
|
629
629
|
properties: {
|
|
@@ -52,6 +52,18 @@ height: 320
|
|
|
52
52
|
|
|
53
53
|
Fence keys: `src` (required, same-origin path), `title`, and either `height` (px) or `aspect` (`16/9`, `4/3`, `1/1`, `21/9`, `3/2`, `2/1`; default `16/9`). A cross-origin `src` renders an "Embed blocked" notice instead of a chart.
|
|
54
54
|
|
|
55
|
+
**This fence is the only supported syntax.** Never write a bare line like
|
|
56
|
+
`` `/chart type=bar title="..." labels=[...] data=[...] color=#...` `` in chat
|
|
57
|
+
text — that pattern comes from confusing `generate-chart`'s tool parameters
|
|
58
|
+
(`title`, `labels`, `data`, `type`) with markdown; those are arguments to a
|
|
59
|
+
tool call, not something to type into a chat message. The chat renderer has a
|
|
60
|
+
best-effort compatibility fallback that tries to recover a chart from that
|
|
61
|
+
exact shorthand shape, but it is not the contract: it rejects mismatched
|
|
62
|
+
lengths, negative values, and malformed input (falling back to plain text),
|
|
63
|
+
and it does not re-query live data the way the embed does. If you catch
|
|
64
|
+
yourself typing `label` or `data` followed by `=` in a chat reply, stop and
|
|
65
|
+
build the ` ```embed ` fence above instead.
|
|
66
|
+
|
|
55
67
|
**Panel JSON.** The `/chart` route decodes `panel` into a `SqlPanel` (`app/pages/adhoc/sql-dashboard/types.ts`):
|
|
56
68
|
|
|
57
69
|
- `sql` — required, non-empty.
|
|
@@ -71,11 +71,11 @@ function errorMessage(error: unknown): string {
|
|
|
71
71
|
* those flows have full data in hand before they call here.
|
|
72
72
|
*/
|
|
73
73
|
const CHART_FALLBACK_HINT =
|
|
74
|
-
"If you're answering an in-chat data question, do not retry generate-chart. Switch to the live /chart embed instead — it accepts a SqlPanel object directly and doesn't require pre-stringified JSON params. See the data-querying skill's \"Inline Charts In Chat\" section for the embed syntax. Only use generate-chart when you're building a save-analysis artifact.";
|
|
74
|
+
"If you're answering an in-chat data question, do not retry generate-chart, and do not type this action's title/labels/data/type parameters as plain chat text (e.g. `/chart type=bar title=... labels=[...] data=[...]`) — that is not the supported syntax and only a best-effort compatibility fallback may recover a chart from it. Switch to the live /chart embed instead — it accepts a SqlPanel object directly and doesn't require pre-stringified JSON params. See the data-querying skill's \"Inline Charts In Chat\" section for the exact ```embed fence syntax. Only use generate-chart when you're building a save-analysis artifact.";
|
|
75
75
|
|
|
76
76
|
export default defineAction({
|
|
77
77
|
description:
|
|
78
|
-
|
|
78
|
+
"Render a static chart image to the media directory **for save-analysis artifacts only**. Uses PNG when the native renderer is available and a portable SVG fallback otherwise. For an in-chat answer to a data question, do NOT call this — emit a live `/chart` embed instead (see the data-querying skill's \"Inline Charts In Chat\" section for the embed syntax). Never write this action's `title`/`labels`/`data`/`type` parameters as literal chat text (e.g. `/chart type=bar title=... labels=[...] data=[...]`) — that is not the supported syntax; a best-effort compatibility fallback may recover a simple chart from it, but it rejects malformed input and never re-queries live data like the embed does. The static image path exists for analyses that need to render outside this app (exports, archived reports). If validation here fails, switch to the live embed rather than retrying.",
|
|
79
79
|
schema: z.object({
|
|
80
80
|
title: z.string().optional().describe("Chart title (required)"),
|
|
81
81
|
labels: z.string().optional().describe("JSON array of x-axis labels"),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export declare const LEGACY_CHART_SHORTHAND_LANG = "chart-shorthand";
|
|
3
|
+
export interface LegacyChartShorthand {
|
|
4
|
+
type: "bar" | "line" | "area";
|
|
5
|
+
title: string;
|
|
6
|
+
labels: string[];
|
|
7
|
+
series: {
|
|
8
|
+
label: string;
|
|
9
|
+
data: number[];
|
|
10
|
+
color?: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Cheap-ish pre-check so callers can skip the full parse on ordinary text.
|
|
15
|
+
* Requires `labels=`/`data=` to actually resolve to bracketed arrays (not
|
|
16
|
+
* just appear as substrings) so unrelated slash commands or API paths that
|
|
17
|
+
* happen to mention both words aren't diverted out of normal markdown
|
|
18
|
+
* rendering.
|
|
19
|
+
*/
|
|
20
|
+
export declare function looksLikeLegacyChartShorthand(line: string): boolean;
|
|
21
|
+
export declare function parseLegacyChartShorthand(rawLine: string): LegacyChartShorthand | null;
|
|
22
|
+
export declare function wrapLegacyChartShorthandLines(markdown: string): string;
|
|
23
|
+
export declare function LegacyChartShorthandFallback({ text }: {
|
|
24
|
+
text: string;
|
|
25
|
+
}): React.JSX.Element;
|
|
26
|
+
export declare function LegacyChartShorthandChart({ parsed, }: {
|
|
27
|
+
parsed: LegacyChartShorthand;
|
|
28
|
+
}): React.JSX.Element;
|
|
29
|
+
//# sourceMappingURL=legacy-chart-shorthand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy-chart-shorthand.d.ts","sourceRoot":"","sources":["../../../src/client/chat/legacy-chart-shorthand.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,eAAO,MAAM,2BAA2B,oBAAoB,CAAC;AAiB7D,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC7D;AAyGD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASnE;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,GACd,oBAAoB,GAAG,IAAI,CAqF7B;AASD,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiDtE;AAED,wBAAgB,4BAA4B,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,qBAEtE;AAED,wBAAgB,yBAAyB,CAAC,EACxC,MAAM,GACP,EAAE;IACD,MAAM,EAAE,oBAAoB,CAAC;CAC9B,qBAsKA"}
|