@ai-gui/plugin-bigscreen 0.36.0 → 0.36.2
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# @ai-gui/plugin-bigscreen
|
|
2
2
|
|
|
3
|
+
## 0.36.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c57eae2: A data wall now folds on its own width, not the window's.
|
|
8
|
+
|
|
9
|
+
The wall laid out twelve columns and fell back to one at `@media
|
|
10
|
+
(max-width: 640px)` — a query about the _viewport_. Put the same wall in a
|
|
11
|
+
330px side panel of a 1900px window and the query never fires: four KPI cards
|
|
12
|
+
share three hundred pixels and the numbers wrap one character per line.
|
|
13
|
+
|
|
14
|
+
The wall is now a container (`container-type: inline-size`) and the fallbacks
|
|
15
|
+
are `@container` queries, so what decides the layout is the only thing that was
|
|
16
|
+
ever relevant — how wide the wall is. That also covers the case the old query
|
|
17
|
+
was written for, since a full-width wall in a small window has a small
|
|
18
|
+
container.
|
|
19
|
+
|
|
20
|
+
Two steps rather than one. At 900px it folds to two columns; at 520px to one.
|
|
21
|
+
And a panel the model gave more than half the grid to keeps both columns in the
|
|
22
|
+
two-column state — a chart is wide because it needs to be, and collapsing it to
|
|
23
|
+
the width of a KPI card is how a readable chart becomes a smear.
|
|
24
|
+
|
|
25
|
+
- @ai-gui/core@0.36.2
|
|
26
|
+
|
|
27
|
+
## 0.36.1
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 6183537: A refused block now says why, and bigscreen says how long its strings may be.
|
|
32
|
+
|
|
33
|
+
Two faults, found by asking a real agent for a real dashboard — a portfolio
|
|
34
|
+
wall — and getting a grey bar.
|
|
35
|
+
|
|
36
|
+
**The wall was refused for a caption.** `parseKpi` allows a `label` of 40
|
|
37
|
+
characters and the prompt spec listed the field with no hint of that, or of
|
|
38
|
+
what the field is for. Asked for holdings, the model wrote
|
|
39
|
+
|
|
40
|
+
"持仓 14股 | 成本 $699.19 | 现价 $703.41 | 盈亏 +$59.07 (+0.60%)"
|
|
41
|
+
|
|
42
|
+
— 54 characters, and lost the whole screen. It did it again on the next
|
|
43
|
+
generation, so this is not luck; it is a limit that was enforced and never
|
|
44
|
+
stated. The spec now gives every length (unit 16, prefix 8, label and rank
|
|
45
|
+
names 40, panel title 80, screen title 80, subtitle 120), says what `label` is
|
|
46
|
+
for, and says the part that makes lengths matter: overrunning one throws the
|
|
47
|
+
whole block away rather than trimming the string.
|
|
48
|
+
|
|
49
|
+
The parser's messages carry the numbers too. "must be a short string" is true
|
|
50
|
+
and useless — it tells neither a model rewriting its block nor a person reading
|
|
51
|
+
the message whether they are two characters over or twenty. It now reads
|
|
52
|
+
`panels[0].label must be at most 40 characters (got 54)`.
|
|
53
|
+
|
|
54
|
+
**And the reason was invisible.** Seven plugins shipped
|
|
55
|
+
|
|
56
|
+
[data-aigui-<name>-error] { …; opacity: .8; background: currentColor }
|
|
57
|
+
|
|
58
|
+
which paints the box in the very colour its text is written in. The message
|
|
59
|
+
reached the DOM and the `aria-label`, and on screen it was an unexplained slab —
|
|
60
|
+
indistinguishable from a renderer that had broken. It is now a tint of the host's
|
|
61
|
+
own colour with the text readable over it, in the shape `@ai-gui/plugin-dashboard`
|
|
62
|
+
already used. Wrapped in `:where()` so a host restyling it wins without a
|
|
63
|
+
specificity fight, since plugin CSS is injected at runtime and takes every tie.
|
|
64
|
+
|
|
65
|
+
- @ai-gui/core@0.36.1
|
|
66
|
+
|
|
3
67
|
## 0.36.0
|
|
4
68
|
|
|
5
69
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -82,6 +82,16 @@ const bad = (message) => ({
|
|
|
82
82
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
83
|
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
84
84
|
const text = (value, max = 120) => typeof value === "string" && value.length <= max;
|
|
85
|
+
/**
|
|
86
|
+
* Why a string was rejected, with the number in it.
|
|
87
|
+
*
|
|
88
|
+
* "must be a short string" is true and useless: it does not say how short, so
|
|
89
|
+
* neither a model rewriting its own block nor a person reading the message can
|
|
90
|
+
* tell whether they are two characters over or twenty. Observed in the wild —
|
|
91
|
+
* a KPI caption of 54 characters against a limit of 40, twice in a row, from a
|
|
92
|
+
* model that had never been told there was a limit.
|
|
93
|
+
*/
|
|
94
|
+
const tooLong = (at, max, value) => typeof value === "string" ? `${at} must be at most ${max} characters (got ${value.length})` : `${at} must be a string of at most ${max} characters`;
|
|
85
95
|
const lngLat = (value) => Array.isArray(value) && value.length === 2 && value.every(finite) && Math.abs(value[0]) <= 180 && Math.abs(value[1]) <= 90;
|
|
86
96
|
function parseKpi(raw, at) {
|
|
87
97
|
if (!finite(raw.value)) return bad(`${at}.value must be a number`);
|
|
@@ -90,11 +100,11 @@ function parseKpi(raw, at) {
|
|
|
90
100
|
value: raw.value
|
|
91
101
|
};
|
|
92
102
|
if (raw.unit !== void 0) {
|
|
93
|
-
if (!text(raw.unit, 16)) return bad(`${at}.unit
|
|
103
|
+
if (!text(raw.unit, 16)) return bad(tooLong(`${at}.unit`, 16, raw.unit));
|
|
94
104
|
panel.unit = raw.unit;
|
|
95
105
|
}
|
|
96
106
|
if (raw.prefix !== void 0) {
|
|
97
|
-
if (!text(raw.prefix, 8)) return bad(`${at}.prefix
|
|
107
|
+
if (!text(raw.prefix, 8)) return bad(tooLong(`${at}.prefix`, 8, raw.prefix));
|
|
98
108
|
panel.prefix = raw.prefix;
|
|
99
109
|
}
|
|
100
110
|
if (raw.decimals !== void 0) {
|
|
@@ -114,7 +124,7 @@ function parseKpi(raw, at) {
|
|
|
114
124
|
panel.trend = raw.trend;
|
|
115
125
|
}
|
|
116
126
|
if (raw.label !== void 0) {
|
|
117
|
-
if (!text(raw.label, 40)) return bad(`${at}.label
|
|
127
|
+
if (!text(raw.label, 40)) return bad(tooLong(`${at}.label`, 40, raw.label));
|
|
118
128
|
panel.label = raw.label;
|
|
119
129
|
}
|
|
120
130
|
return {
|
|
@@ -133,7 +143,7 @@ function parseGauge(raw, at) {
|
|
|
133
143
|
panel.max = raw.max;
|
|
134
144
|
}
|
|
135
145
|
if (raw.unit !== void 0) {
|
|
136
|
-
if (!text(raw.unit, 16)) return bad(`${at}.unit
|
|
146
|
+
if (!text(raw.unit, 16)) return bad(tooLong(`${at}.unit`, 16, raw.unit));
|
|
137
147
|
panel.unit = raw.unit;
|
|
138
148
|
}
|
|
139
149
|
if (raw.style !== void 0) {
|
|
@@ -166,7 +176,7 @@ function parseRank(raw, at) {
|
|
|
166
176
|
items
|
|
167
177
|
};
|
|
168
178
|
if (raw.unit !== void 0) {
|
|
169
|
-
if (!text(raw.unit, 16)) return bad(`${at}.unit
|
|
179
|
+
if (!text(raw.unit, 16)) return bad(tooLong(`${at}.unit`, 16, raw.unit));
|
|
170
180
|
panel.unit = raw.unit;
|
|
171
181
|
}
|
|
172
182
|
if (raw.top !== void 0) {
|
|
@@ -222,7 +232,7 @@ function parseGlobe(raw, at) {
|
|
|
222
232
|
to: arc.to
|
|
223
233
|
};
|
|
224
234
|
if (arc.label !== void 0) {
|
|
225
|
-
if (!text(arc.label, 40)) return bad(`${at}.arcs[${index}].label
|
|
235
|
+
if (!text(arc.label, 40)) return bad(tooLong(`${at}.arcs[${index}].label`, 40, arc.label));
|
|
226
236
|
entry.label = arc.label;
|
|
227
237
|
}
|
|
228
238
|
panel.arcs.push(entry);
|
|
@@ -235,7 +245,7 @@ function parseGlobe(raw, at) {
|
|
|
235
245
|
if (!isRecord(point) || !lngLat(point.coord)) return bad(`${at}.points[${index}] must be {coord: [lng, lat]}`);
|
|
236
246
|
const entry = { coord: point.coord };
|
|
237
247
|
if (point.label !== void 0) {
|
|
238
|
-
if (!text(point.label, 40)) return bad(`${at}.points[${index}].label
|
|
248
|
+
if (!text(point.label, 40)) return bad(tooLong(`${at}.points[${index}].label`, 40, point.label));
|
|
239
249
|
entry.label = point.label;
|
|
240
250
|
}
|
|
241
251
|
if (point.value !== void 0) {
|
|
@@ -293,7 +303,7 @@ function parsePanel(raw, index, columns) {
|
|
|
293
303
|
if (!parsed.ok) return parsed;
|
|
294
304
|
const panel = parsed.value;
|
|
295
305
|
if (raw.title !== void 0) {
|
|
296
|
-
if (!text(raw.title, 80)) return bad(`${at}.title
|
|
306
|
+
if (!text(raw.title, 80)) return bad(tooLong(`${at}.title`, 80, raw.title));
|
|
297
307
|
panel.title = raw.title;
|
|
298
308
|
}
|
|
299
309
|
if (raw.span !== void 0) {
|
|
@@ -340,11 +350,11 @@ function parseBigscreen(source, options = {}) {
|
|
|
340
350
|
panels: []
|
|
341
351
|
};
|
|
342
352
|
if (raw.title !== void 0) {
|
|
343
|
-
if (!text(raw.title, 80)) return bad("title
|
|
353
|
+
if (!text(raw.title, 80)) return bad(tooLong("title", 80, raw.title));
|
|
344
354
|
definition.title = raw.title;
|
|
345
355
|
}
|
|
346
356
|
if (raw.subtitle !== void 0) {
|
|
347
|
-
if (!text(raw.subtitle, 120)) return bad("subtitle
|
|
357
|
+
if (!text(raw.subtitle, 120)) return bad(tooLong("subtitle", 120, raw.subtitle));
|
|
348
358
|
definition.subtitle = raw.subtitle;
|
|
349
359
|
}
|
|
350
360
|
if (raw.theme !== void 0) {
|
|
@@ -391,17 +401,19 @@ function bigscreenPromptSpec(locale) {
|
|
|
391
401
|
}
|
|
392
402
|
const ZH = `数据大屏(围栏代码块):\`\`\`bigscreen 开头,块内是一个 JSON 对象。当用户要一个"大屏 / 看板 / dashboard / 总览",或者要把一组指标、排行、趋势、地域分布放在一起漂亮地展示时,输出一个 bigscreen 块。它会画成深色发光的数据墙:数字滚动增长、仪表盘扫过、排行条生长、图表逐步绘出、3D 图和地球缓慢旋转。文字说明照常写在块外。
|
|
393
403
|
|
|
404
|
+
字数超了整个块就作废——一个太长的 label 会让整面墙消失,而不是那一句被截断。所有文字字段都短:unit 最多 16、prefix 最多 8、label 和排行项名字最多 40、面板 title 最多 80。它们是标签,不是句子。
|
|
405
|
+
|
|
394
406
|
只用对话里已有的数字。没有的数据就不要放这个面板,或者在块外说明缺什么。绝对不要编造一个看起来合理的数字来填空——大屏上的数字看起来像证据。
|
|
395
407
|
|
|
396
408
|
顶层字段:
|
|
397
|
-
- title、subtitle
|
|
409
|
+
- title、subtitle:大屏标题(最多 80 字符)、副标题(最多 120 字符)
|
|
398
410
|
- theme:"dark"(默认)| "light"
|
|
399
411
|
- accent:主色,十六进制,如 "#22d3ee"
|
|
400
412
|
- columns:网格列数,默认 12
|
|
401
413
|
- panels(必填):面板数组,最多 24 个
|
|
402
414
|
|
|
403
|
-
每个面板:kind(必填)、title
|
|
404
|
-
- "kpi":value(必填)、unit、prefix(如 "¥")、decimals、delta(变化率,小数:0.12 就是 +12%)、upIsGood(默认 true)、trend(一列数,画成迷你折线)、label
|
|
415
|
+
每个面板:kind(必填)、title(最多 80 字符)、span(占几列,默认 4)、height(像素)。按 kind 填:
|
|
416
|
+
- "kpi":value(必填)、unit、prefix(如 "¥")、decimals、delta(变化率,小数:0.12 就是 +12%)、upIsGood(默认 true)、trend(一列数,画成迷你折线)、label(数字下面的一行小字,**最多 40 字符**——写"较上月""同比"这种一眼看完的说明;明细放不下,要写明细就再开一个面板或者写在块外)
|
|
405
417
|
- "gauge":value(必填)、max(默认 100)、unit、style "dial"(默认)| "ring"、thresholds [0.6, 0.85](告警线:超过第一个变黄,超过第二个变红——只给"越高越危险"的指标用,如 CPU 占用、库存水位;完成率这类越高越好的不要给)
|
|
406
418
|
- "rank":items(必填,[{"name","value"}])、unit、top(默认 8)
|
|
407
419
|
- "chart":option(必填)——完整的 ECharts option,颜色、字体、动画由大屏统一
|
|
@@ -432,11 +444,13 @@ const ZH = `数据大屏(围栏代码块):\`\`\`bigscreen 开头,块内
|
|
|
432
444
|
这一版画不了的(遇到就退回普通图表或文字):实时刷新、可交互筛选、表格、地图着色(省份热力图)、视频。需要宿主提供真实数据的 BI 看板用 dashboard 块,不用这个。`;
|
|
433
445
|
const EN = `Data walls (fenced): \`\`\`bigscreen with a JSON object inside. Emit one when the user asks for a dashboard, an overview, a "big screen", or wants a set of metrics, rankings, trends and places shown together and shown well. It draws a dark, glowing wall: numbers count up, gauges sweep, rank bars grow, charts draw themselves, 3D charts and globes turn slowly. Keep the explanation itself outside the block.
|
|
434
446
|
|
|
447
|
+
Overrunning a length throws the whole block away — one label two characters too long loses the entire wall, rather than being trimmed. Every text field is short: unit at most 16, prefix 8, label and rank item names 40, panel title 80. They are labels, not sentences.
|
|
448
|
+
|
|
435
449
|
Use only numbers the conversation already contains. Leave out a panel whose data you do not have, or say outside the block what is missing. Never invent a plausible figure to fill a gap — a number on a wall looks like evidence.
|
|
436
450
|
|
|
437
|
-
Top level: title, subtitle, theme ("dark" default or "light"), accent (hex), columns (default 12), panels (required, at most 24).
|
|
451
|
+
Top level: title (at most 80 characters), subtitle (at most 120), theme ("dark" default or "light"), accent (hex), columns (default 12), panels (required, at most 24).
|
|
438
452
|
|
|
439
|
-
Each panel: kind (required), title, span (columns, default 4), height (px). By kind: "kpi" (value, unit, prefix, decimals, delta as a fraction, upIsGood, trend as a short number list, label); "gauge" (value, max default 100, unit, style "dial" or "ring", thresholds [0.6, 0.85] as alarm levels — amber past the first, red past the second — only for metrics where higher is worse, never for a completion rate); "rank" (items as {name, value}, unit, top default 8); "chart" (option: a full ECharts option — colours, fonts and animation come from the wall); "chart3d" (type "bar3D" | "scatter3D" | "surface" | "line3D", data as [x, y, z] points, optional xAxis and yAxis category lists, rotate); "globe" (arcs as {from: [lng, lat], to: [lng, lat], label}, points as {coord: [lng, lat], label, value}, rotate; at least one of arcs or points; for intercontinental flows — a route within one region is a few pixels on a globe, so use an ordinary chart for those).
|
|
453
|
+
Each panel: kind (required), title (at most 80 characters), span (columns, default 4), height (px). By kind: "kpi" (value, unit, prefix, decimals, delta as a fraction, upIsGood, trend as a short number list, label — one short line under the number, **at most 40 characters**, for a caption like "vs last month"; detail does not fit there, so put it in its own panel or outside the block); "gauge" (value, max default 100, unit, style "dial" or "ring", thresholds [0.6, 0.85] as alarm levels — amber past the first, red past the second — only for metrics where higher is worse, never for a completion rate); "rank" (items as {name, value}, unit, top default 8); "chart" (option: a full ECharts option — colours, fonts and animation come from the wall); "chart3d" (type "bar3D" | "scatter3D" | "surface" | "line3D", data as [x, y, z] points, optional xAxis and yAxis category lists, rotate); "globe" (arcs as {from: [lng, lat], to: [lng, lat], label}, points as {coord: [lng, lat], label, value}, rotate; at least one of arcs or points; for intercontinental flows — a route within one region is a few pixels on a globe, so use an ordinary chart for those).
|
|
440
454
|
|
|
441
455
|
Layout: a first row of three or four kpi panels at span 3, then a wide chart at span 8 beside a rank or gauge at span 4, and 3D or globe panels at span 6.
|
|
442
456
|
|
|
@@ -466,7 +480,7 @@ const PROMPT = {
|
|
|
466
480
|
//#region src/index.ts
|
|
467
481
|
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
468
482
|
const bigscreenCss = [
|
|
469
|
-
"[data-aigui-bigscreen]{--aigui-bs-accent:#22d3ee;--aigui-bs-text:#e2e8f0;--aigui-bs-muted:#94a3b8;box-sizing:border-box;margin-block:1rem;padding:1.1rem 1.2rem 1.3rem;border-radius:14px;color:var(--aigui-bs-text);font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'PingFang SC','Noto Sans CJK SC',sans-serif;font-variant-numeric:tabular-nums}",
|
|
483
|
+
"[data-aigui-bigscreen]{--aigui-bs-accent:#22d3ee;--aigui-bs-text:#e2e8f0;--aigui-bs-muted:#94a3b8;box-sizing:border-box;margin-block:1rem;padding:1.1rem 1.2rem 1.3rem;border-radius:14px;color:var(--aigui-bs-text);font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'PingFang SC','Noto Sans CJK SC',sans-serif;font-variant-numeric:tabular-nums;container-type:inline-size}",
|
|
470
484
|
"[data-aigui-bigscreen] *{box-sizing:border-box}",
|
|
471
485
|
"[data-aigui-bigscreen='dark']{background:radial-gradient(120% 90% at 50% 0%,#0f1f3d 0%,#0a1428 45%,#050b18 100%);box-shadow:inset 0 0 0 1px rgba(56,189,248,.12),0 20px 60px rgba(0,0,0,.45)}",
|
|
472
486
|
"[data-aigui-bigscreen='light']{background:linear-gradient(180deg,#f8fafc,#eef2f7);box-shadow:inset 0 0 0 1px rgba(15,23,42,.06)}",
|
|
@@ -497,8 +511,9 @@ const bigscreenCss = [
|
|
|
497
511
|
".aigui-bs-rank-value{color:var(--aigui-bs-muted);font-variant-numeric:tabular-nums}",
|
|
498
512
|
".aigui-bs-note{display:flex;align-items:center;justify-content:center;height:100%;min-height:4rem;font-size:.85rem;color:var(--aigui-bs-muted)}",
|
|
499
513
|
"[data-aigui-bigscreen-loading]{min-height:8rem;border-radius:14px;background:currentColor;opacity:.06}",
|
|
500
|
-
"[data-aigui-bigscreen-error]{padding
|
|
501
|
-
"@
|
|
514
|
+
":where([data-aigui-bigscreen-error]){padding:0.5rem 0.75rem;border-radius:0.5rem;font-size:0.875rem;background:color-mix(in srgb,currentColor 8%,transparent);border:1px solid color-mix(in srgb,currentColor 25%,transparent)}",
|
|
515
|
+
"@container (max-width:900px){.aigui-bs-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}.aigui-bs-panel{grid-column:span 1!important}.aigui-bs-panel[data-aigui-bigscreen-wide]{grid-column:span 2!important}}",
|
|
516
|
+
"@container (max-width:520px){.aigui-bs-grid{grid-template-columns:1fr!important}.aigui-bs-panel,.aigui-bs-panel[data-aigui-bigscreen-wide]{grid-column:span 1!important}}"
|
|
502
517
|
].join("");
|
|
503
518
|
function failed(message) {
|
|
504
519
|
const text$1 = escapeHtml(message);
|
|
@@ -536,7 +551,7 @@ function bigscreen(options = {}) {
|
|
|
536
551
|
let destroy;
|
|
537
552
|
let disposed = false;
|
|
538
553
|
Promise.resolve().then(function() {
|
|
539
|
-
return require("./mount-
|
|
554
|
+
return require("./mount-DPyPP9Ha.cjs");
|
|
540
555
|
}).then(({ mountScreen }) => {
|
|
541
556
|
if (disposed) return;
|
|
542
557
|
destroy = mountScreen(el, definition, options.animate !== false);
|
package/dist/index.js
CHANGED
|
@@ -81,6 +81,16 @@ const bad = (message) => ({
|
|
|
81
81
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
82
|
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
83
83
|
const text = (value, max = 120) => typeof value === "string" && value.length <= max;
|
|
84
|
+
/**
|
|
85
|
+
* Why a string was rejected, with the number in it.
|
|
86
|
+
*
|
|
87
|
+
* "must be a short string" is true and useless: it does not say how short, so
|
|
88
|
+
* neither a model rewriting its own block nor a person reading the message can
|
|
89
|
+
* tell whether they are two characters over or twenty. Observed in the wild —
|
|
90
|
+
* a KPI caption of 54 characters against a limit of 40, twice in a row, from a
|
|
91
|
+
* model that had never been told there was a limit.
|
|
92
|
+
*/
|
|
93
|
+
const tooLong = (at, max, value) => typeof value === "string" ? `${at} must be at most ${max} characters (got ${value.length})` : `${at} must be a string of at most ${max} characters`;
|
|
84
94
|
const lngLat = (value) => Array.isArray(value) && value.length === 2 && value.every(finite) && Math.abs(value[0]) <= 180 && Math.abs(value[1]) <= 90;
|
|
85
95
|
function parseKpi(raw, at) {
|
|
86
96
|
if (!finite(raw.value)) return bad(`${at}.value must be a number`);
|
|
@@ -89,11 +99,11 @@ function parseKpi(raw, at) {
|
|
|
89
99
|
value: raw.value
|
|
90
100
|
};
|
|
91
101
|
if (raw.unit !== void 0) {
|
|
92
|
-
if (!text(raw.unit, 16)) return bad(`${at}.unit
|
|
102
|
+
if (!text(raw.unit, 16)) return bad(tooLong(`${at}.unit`, 16, raw.unit));
|
|
93
103
|
panel.unit = raw.unit;
|
|
94
104
|
}
|
|
95
105
|
if (raw.prefix !== void 0) {
|
|
96
|
-
if (!text(raw.prefix, 8)) return bad(`${at}.prefix
|
|
106
|
+
if (!text(raw.prefix, 8)) return bad(tooLong(`${at}.prefix`, 8, raw.prefix));
|
|
97
107
|
panel.prefix = raw.prefix;
|
|
98
108
|
}
|
|
99
109
|
if (raw.decimals !== void 0) {
|
|
@@ -113,7 +123,7 @@ function parseKpi(raw, at) {
|
|
|
113
123
|
panel.trend = raw.trend;
|
|
114
124
|
}
|
|
115
125
|
if (raw.label !== void 0) {
|
|
116
|
-
if (!text(raw.label, 40)) return bad(`${at}.label
|
|
126
|
+
if (!text(raw.label, 40)) return bad(tooLong(`${at}.label`, 40, raw.label));
|
|
117
127
|
panel.label = raw.label;
|
|
118
128
|
}
|
|
119
129
|
return {
|
|
@@ -132,7 +142,7 @@ function parseGauge(raw, at) {
|
|
|
132
142
|
panel.max = raw.max;
|
|
133
143
|
}
|
|
134
144
|
if (raw.unit !== void 0) {
|
|
135
|
-
if (!text(raw.unit, 16)) return bad(`${at}.unit
|
|
145
|
+
if (!text(raw.unit, 16)) return bad(tooLong(`${at}.unit`, 16, raw.unit));
|
|
136
146
|
panel.unit = raw.unit;
|
|
137
147
|
}
|
|
138
148
|
if (raw.style !== void 0) {
|
|
@@ -165,7 +175,7 @@ function parseRank(raw, at) {
|
|
|
165
175
|
items
|
|
166
176
|
};
|
|
167
177
|
if (raw.unit !== void 0) {
|
|
168
|
-
if (!text(raw.unit, 16)) return bad(`${at}.unit
|
|
178
|
+
if (!text(raw.unit, 16)) return bad(tooLong(`${at}.unit`, 16, raw.unit));
|
|
169
179
|
panel.unit = raw.unit;
|
|
170
180
|
}
|
|
171
181
|
if (raw.top !== void 0) {
|
|
@@ -221,7 +231,7 @@ function parseGlobe(raw, at) {
|
|
|
221
231
|
to: arc.to
|
|
222
232
|
};
|
|
223
233
|
if (arc.label !== void 0) {
|
|
224
|
-
if (!text(arc.label, 40)) return bad(`${at}.arcs[${index}].label
|
|
234
|
+
if (!text(arc.label, 40)) return bad(tooLong(`${at}.arcs[${index}].label`, 40, arc.label));
|
|
225
235
|
entry.label = arc.label;
|
|
226
236
|
}
|
|
227
237
|
panel.arcs.push(entry);
|
|
@@ -234,7 +244,7 @@ function parseGlobe(raw, at) {
|
|
|
234
244
|
if (!isRecord(point) || !lngLat(point.coord)) return bad(`${at}.points[${index}] must be {coord: [lng, lat]}`);
|
|
235
245
|
const entry = { coord: point.coord };
|
|
236
246
|
if (point.label !== void 0) {
|
|
237
|
-
if (!text(point.label, 40)) return bad(`${at}.points[${index}].label
|
|
247
|
+
if (!text(point.label, 40)) return bad(tooLong(`${at}.points[${index}].label`, 40, point.label));
|
|
238
248
|
entry.label = point.label;
|
|
239
249
|
}
|
|
240
250
|
if (point.value !== void 0) {
|
|
@@ -292,7 +302,7 @@ function parsePanel(raw, index, columns) {
|
|
|
292
302
|
if (!parsed.ok) return parsed;
|
|
293
303
|
const panel = parsed.value;
|
|
294
304
|
if (raw.title !== void 0) {
|
|
295
|
-
if (!text(raw.title, 80)) return bad(`${at}.title
|
|
305
|
+
if (!text(raw.title, 80)) return bad(tooLong(`${at}.title`, 80, raw.title));
|
|
296
306
|
panel.title = raw.title;
|
|
297
307
|
}
|
|
298
308
|
if (raw.span !== void 0) {
|
|
@@ -339,11 +349,11 @@ function parseBigscreen(source, options = {}) {
|
|
|
339
349
|
panels: []
|
|
340
350
|
};
|
|
341
351
|
if (raw.title !== void 0) {
|
|
342
|
-
if (!text(raw.title, 80)) return bad("title
|
|
352
|
+
if (!text(raw.title, 80)) return bad(tooLong("title", 80, raw.title));
|
|
343
353
|
definition.title = raw.title;
|
|
344
354
|
}
|
|
345
355
|
if (raw.subtitle !== void 0) {
|
|
346
|
-
if (!text(raw.subtitle, 120)) return bad("subtitle
|
|
356
|
+
if (!text(raw.subtitle, 120)) return bad(tooLong("subtitle", 120, raw.subtitle));
|
|
347
357
|
definition.subtitle = raw.subtitle;
|
|
348
358
|
}
|
|
349
359
|
if (raw.theme !== void 0) {
|
|
@@ -390,17 +400,19 @@ function bigscreenPromptSpec(locale) {
|
|
|
390
400
|
}
|
|
391
401
|
const ZH = `数据大屏(围栏代码块):\`\`\`bigscreen 开头,块内是一个 JSON 对象。当用户要一个"大屏 / 看板 / dashboard / 总览",或者要把一组指标、排行、趋势、地域分布放在一起漂亮地展示时,输出一个 bigscreen 块。它会画成深色发光的数据墙:数字滚动增长、仪表盘扫过、排行条生长、图表逐步绘出、3D 图和地球缓慢旋转。文字说明照常写在块外。
|
|
392
402
|
|
|
403
|
+
字数超了整个块就作废——一个太长的 label 会让整面墙消失,而不是那一句被截断。所有文字字段都短:unit 最多 16、prefix 最多 8、label 和排行项名字最多 40、面板 title 最多 80。它们是标签,不是句子。
|
|
404
|
+
|
|
393
405
|
只用对话里已有的数字。没有的数据就不要放这个面板,或者在块外说明缺什么。绝对不要编造一个看起来合理的数字来填空——大屏上的数字看起来像证据。
|
|
394
406
|
|
|
395
407
|
顶层字段:
|
|
396
|
-
- title、subtitle
|
|
408
|
+
- title、subtitle:大屏标题(最多 80 字符)、副标题(最多 120 字符)
|
|
397
409
|
- theme:"dark"(默认)| "light"
|
|
398
410
|
- accent:主色,十六进制,如 "#22d3ee"
|
|
399
411
|
- columns:网格列数,默认 12
|
|
400
412
|
- panels(必填):面板数组,最多 24 个
|
|
401
413
|
|
|
402
|
-
每个面板:kind(必填)、title
|
|
403
|
-
- "kpi":value(必填)、unit、prefix(如 "¥")、decimals、delta(变化率,小数:0.12 就是 +12%)、upIsGood(默认 true)、trend(一列数,画成迷你折线)、label
|
|
414
|
+
每个面板:kind(必填)、title(最多 80 字符)、span(占几列,默认 4)、height(像素)。按 kind 填:
|
|
415
|
+
- "kpi":value(必填)、unit、prefix(如 "¥")、decimals、delta(变化率,小数:0.12 就是 +12%)、upIsGood(默认 true)、trend(一列数,画成迷你折线)、label(数字下面的一行小字,**最多 40 字符**——写"较上月""同比"这种一眼看完的说明;明细放不下,要写明细就再开一个面板或者写在块外)
|
|
404
416
|
- "gauge":value(必填)、max(默认 100)、unit、style "dial"(默认)| "ring"、thresholds [0.6, 0.85](告警线:超过第一个变黄,超过第二个变红——只给"越高越危险"的指标用,如 CPU 占用、库存水位;完成率这类越高越好的不要给)
|
|
405
417
|
- "rank":items(必填,[{"name","value"}])、unit、top(默认 8)
|
|
406
418
|
- "chart":option(必填)——完整的 ECharts option,颜色、字体、动画由大屏统一
|
|
@@ -431,11 +443,13 @@ const ZH = `数据大屏(围栏代码块):\`\`\`bigscreen 开头,块内
|
|
|
431
443
|
这一版画不了的(遇到就退回普通图表或文字):实时刷新、可交互筛选、表格、地图着色(省份热力图)、视频。需要宿主提供真实数据的 BI 看板用 dashboard 块,不用这个。`;
|
|
432
444
|
const EN = `Data walls (fenced): \`\`\`bigscreen with a JSON object inside. Emit one when the user asks for a dashboard, an overview, a "big screen", or wants a set of metrics, rankings, trends and places shown together and shown well. It draws a dark, glowing wall: numbers count up, gauges sweep, rank bars grow, charts draw themselves, 3D charts and globes turn slowly. Keep the explanation itself outside the block.
|
|
433
445
|
|
|
446
|
+
Overrunning a length throws the whole block away — one label two characters too long loses the entire wall, rather than being trimmed. Every text field is short: unit at most 16, prefix 8, label and rank item names 40, panel title 80. They are labels, not sentences.
|
|
447
|
+
|
|
434
448
|
Use only numbers the conversation already contains. Leave out a panel whose data you do not have, or say outside the block what is missing. Never invent a plausible figure to fill a gap — a number on a wall looks like evidence.
|
|
435
449
|
|
|
436
|
-
Top level: title, subtitle, theme ("dark" default or "light"), accent (hex), columns (default 12), panels (required, at most 24).
|
|
450
|
+
Top level: title (at most 80 characters), subtitle (at most 120), theme ("dark" default or "light"), accent (hex), columns (default 12), panels (required, at most 24).
|
|
437
451
|
|
|
438
|
-
Each panel: kind (required), title, span (columns, default 4), height (px). By kind: "kpi" (value, unit, prefix, decimals, delta as a fraction, upIsGood, trend as a short number list, label); "gauge" (value, max default 100, unit, style "dial" or "ring", thresholds [0.6, 0.85] as alarm levels — amber past the first, red past the second — only for metrics where higher is worse, never for a completion rate); "rank" (items as {name, value}, unit, top default 8); "chart" (option: a full ECharts option — colours, fonts and animation come from the wall); "chart3d" (type "bar3D" | "scatter3D" | "surface" | "line3D", data as [x, y, z] points, optional xAxis and yAxis category lists, rotate); "globe" (arcs as {from: [lng, lat], to: [lng, lat], label}, points as {coord: [lng, lat], label, value}, rotate; at least one of arcs or points; for intercontinental flows — a route within one region is a few pixels on a globe, so use an ordinary chart for those).
|
|
452
|
+
Each panel: kind (required), title (at most 80 characters), span (columns, default 4), height (px). By kind: "kpi" (value, unit, prefix, decimals, delta as a fraction, upIsGood, trend as a short number list, label — one short line under the number, **at most 40 characters**, for a caption like "vs last month"; detail does not fit there, so put it in its own panel or outside the block); "gauge" (value, max default 100, unit, style "dial" or "ring", thresholds [0.6, 0.85] as alarm levels — amber past the first, red past the second — only for metrics where higher is worse, never for a completion rate); "rank" (items as {name, value}, unit, top default 8); "chart" (option: a full ECharts option — colours, fonts and animation come from the wall); "chart3d" (type "bar3D" | "scatter3D" | "surface" | "line3D", data as [x, y, z] points, optional xAxis and yAxis category lists, rotate); "globe" (arcs as {from: [lng, lat], to: [lng, lat], label}, points as {coord: [lng, lat], label, value}, rotate; at least one of arcs or points; for intercontinental flows — a route within one region is a few pixels on a globe, so use an ordinary chart for those).
|
|
439
453
|
|
|
440
454
|
Layout: a first row of three or four kpi panels at span 3, then a wide chart at span 8 beside a rank or gauge at span 4, and 3D or globe panels at span 6.
|
|
441
455
|
|
|
@@ -465,7 +479,7 @@ const PROMPT = {
|
|
|
465
479
|
//#region src/index.ts
|
|
466
480
|
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
467
481
|
const bigscreenCss = [
|
|
468
|
-
"[data-aigui-bigscreen]{--aigui-bs-accent:#22d3ee;--aigui-bs-text:#e2e8f0;--aigui-bs-muted:#94a3b8;box-sizing:border-box;margin-block:1rem;padding:1.1rem 1.2rem 1.3rem;border-radius:14px;color:var(--aigui-bs-text);font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'PingFang SC','Noto Sans CJK SC',sans-serif;font-variant-numeric:tabular-nums}",
|
|
482
|
+
"[data-aigui-bigscreen]{--aigui-bs-accent:#22d3ee;--aigui-bs-text:#e2e8f0;--aigui-bs-muted:#94a3b8;box-sizing:border-box;margin-block:1rem;padding:1.1rem 1.2rem 1.3rem;border-radius:14px;color:var(--aigui-bs-text);font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'PingFang SC','Noto Sans CJK SC',sans-serif;font-variant-numeric:tabular-nums;container-type:inline-size}",
|
|
469
483
|
"[data-aigui-bigscreen] *{box-sizing:border-box}",
|
|
470
484
|
"[data-aigui-bigscreen='dark']{background:radial-gradient(120% 90% at 50% 0%,#0f1f3d 0%,#0a1428 45%,#050b18 100%);box-shadow:inset 0 0 0 1px rgba(56,189,248,.12),0 20px 60px rgba(0,0,0,.45)}",
|
|
471
485
|
"[data-aigui-bigscreen='light']{background:linear-gradient(180deg,#f8fafc,#eef2f7);box-shadow:inset 0 0 0 1px rgba(15,23,42,.06)}",
|
|
@@ -496,8 +510,9 @@ const bigscreenCss = [
|
|
|
496
510
|
".aigui-bs-rank-value{color:var(--aigui-bs-muted);font-variant-numeric:tabular-nums}",
|
|
497
511
|
".aigui-bs-note{display:flex;align-items:center;justify-content:center;height:100%;min-height:4rem;font-size:.85rem;color:var(--aigui-bs-muted)}",
|
|
498
512
|
"[data-aigui-bigscreen-loading]{min-height:8rem;border-radius:14px;background:currentColor;opacity:.06}",
|
|
499
|
-
"[data-aigui-bigscreen-error]{padding
|
|
500
|
-
"@
|
|
513
|
+
":where([data-aigui-bigscreen-error]){padding:0.5rem 0.75rem;border-radius:0.5rem;font-size:0.875rem;background:color-mix(in srgb,currentColor 8%,transparent);border:1px solid color-mix(in srgb,currentColor 25%,transparent)}",
|
|
514
|
+
"@container (max-width:900px){.aigui-bs-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}.aigui-bs-panel{grid-column:span 1!important}.aigui-bs-panel[data-aigui-bigscreen-wide]{grid-column:span 2!important}}",
|
|
515
|
+
"@container (max-width:520px){.aigui-bs-grid{grid-template-columns:1fr!important}.aigui-bs-panel,.aigui-bs-panel[data-aigui-bigscreen-wide]{grid-column:span 1!important}}"
|
|
501
516
|
].join("");
|
|
502
517
|
function failed(message) {
|
|
503
518
|
const text$1 = escapeHtml(message);
|
|
@@ -534,7 +549,7 @@ function bigscreen(options = {}) {
|
|
|
534
549
|
mount: (el) => {
|
|
535
550
|
let destroy;
|
|
536
551
|
let disposed = false;
|
|
537
|
-
import("./mount-
|
|
552
|
+
import("./mount-CbzNLMoI.js").then(({ mountScreen }) => {
|
|
538
553
|
if (disposed) return;
|
|
539
554
|
destroy = mountScreen(el, definition, options.animate !== false);
|
|
540
555
|
}).catch(() => {
|
|
@@ -218,7 +218,9 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
218
218
|
class: "aigui-bs-panel",
|
|
219
219
|
"data-aigui-bigscreen-panel": panel.kind
|
|
220
220
|
});
|
|
221
|
-
|
|
221
|
+
const span = Math.min(panel.span ?? 4, definition.columns);
|
|
222
|
+
node.style.gridColumn = `span ${span}`;
|
|
223
|
+
if (span * 2 > definition.columns) node.setAttribute("data-aigui-bigscreen-wide", "");
|
|
222
224
|
node.style.borderColor = withAlpha(c.accent, definition.theme === "dark" ? .22 : .18);
|
|
223
225
|
if (panel.title) {
|
|
224
226
|
const head = el("header", { class: "aigui-bs-panel-title" }, panel.title);
|
|
@@ -219,7 +219,9 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
219
219
|
class: "aigui-bs-panel",
|
|
220
220
|
"data-aigui-bigscreen-panel": panel.kind
|
|
221
221
|
});
|
|
222
|
-
|
|
222
|
+
const span = Math.min(panel.span ?? 4, definition.columns);
|
|
223
|
+
node.style.gridColumn = `span ${span}`;
|
|
224
|
+
if (span * 2 > definition.columns) node.setAttribute("data-aigui-bigscreen-wide", "");
|
|
223
225
|
node.style.borderColor = require_options.withAlpha(c.accent, definition.theme === "dark" ? .22 : .18);
|
|
224
226
|
if (panel.title) {
|
|
225
227
|
const head = el("header", { class: "aigui-bs-panel-title" }, panel.title);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/plugin-bigscreen",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.2",
|
|
4
4
|
"description": "Animated data walls for AIGUI — counting KPIs, sweeping gauges, growing ranks, self-drawing charts, turning 3D bars and globes, on a grid the model lays out.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"echarts": "^5.5.1",
|
|
58
|
-
"@ai-gui/core": "0.36.
|
|
58
|
+
"@ai-gui/core": "0.36.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"echarts-gl": "^2.0.9"
|