@ai-gui/plugin-bigscreen 0.34.0
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 +11 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/index.cjs +588 -0
- package/dist/index.d.cts +205 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +574 -0
- package/dist/mount-BPBXhD2D.cjs +298 -0
- package/dist/mount-DVHPS1xP.js +297 -0
- package/dist/options-BpHnWRpI.cjs +499 -0
- package/dist/options-C087Peeh.js +415 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { chart3dOption, chartOption, formatNumber, gaugeColour, gaugeOption, globeOption, palette, withAlpha } from "./options-C087Peeh.js";
|
|
2
|
+
import { translate } from "@ai-gui/core";
|
|
3
|
+
|
|
4
|
+
//#region src/parse.ts
|
|
5
|
+
const SCREEN_FIELDS = new Set([
|
|
6
|
+
"title",
|
|
7
|
+
"subtitle",
|
|
8
|
+
"theme",
|
|
9
|
+
"accent",
|
|
10
|
+
"columns",
|
|
11
|
+
"panels"
|
|
12
|
+
]);
|
|
13
|
+
const COMMON = [
|
|
14
|
+
"kind",
|
|
15
|
+
"title",
|
|
16
|
+
"span",
|
|
17
|
+
"height"
|
|
18
|
+
];
|
|
19
|
+
const FIELDS = {
|
|
20
|
+
kpi: [
|
|
21
|
+
"value",
|
|
22
|
+
"unit",
|
|
23
|
+
"prefix",
|
|
24
|
+
"decimals",
|
|
25
|
+
"delta",
|
|
26
|
+
"upIsGood",
|
|
27
|
+
"trend",
|
|
28
|
+
"label"
|
|
29
|
+
],
|
|
30
|
+
gauge: [
|
|
31
|
+
"value",
|
|
32
|
+
"max",
|
|
33
|
+
"unit",
|
|
34
|
+
"style",
|
|
35
|
+
"thresholds"
|
|
36
|
+
],
|
|
37
|
+
rank: [
|
|
38
|
+
"items",
|
|
39
|
+
"unit",
|
|
40
|
+
"top"
|
|
41
|
+
],
|
|
42
|
+
chart: ["option"],
|
|
43
|
+
chart3d: [
|
|
44
|
+
"type",
|
|
45
|
+
"data",
|
|
46
|
+
"xAxis",
|
|
47
|
+
"yAxis",
|
|
48
|
+
"rotate"
|
|
49
|
+
],
|
|
50
|
+
globe: [
|
|
51
|
+
"arcs",
|
|
52
|
+
"points",
|
|
53
|
+
"rotate"
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
const KINDS = new Set([
|
|
57
|
+
"kpi",
|
|
58
|
+
"gauge",
|
|
59
|
+
"rank",
|
|
60
|
+
"chart",
|
|
61
|
+
"chart3d",
|
|
62
|
+
"globe"
|
|
63
|
+
]);
|
|
64
|
+
const CHART3D_TYPES = new Set([
|
|
65
|
+
"bar3D",
|
|
66
|
+
"scatter3D",
|
|
67
|
+
"surface",
|
|
68
|
+
"line3D"
|
|
69
|
+
]);
|
|
70
|
+
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
71
|
+
const MAX_POINTS = 5e3;
|
|
72
|
+
const MAX_ITEMS = 50;
|
|
73
|
+
const MAX_ARCS = 300;
|
|
74
|
+
const bad = (message) => ({
|
|
75
|
+
ok: false,
|
|
76
|
+
error: {
|
|
77
|
+
code: "invalid-definition",
|
|
78
|
+
message
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
83
|
+
const text = (value, max = 120) => typeof value === "string" && value.length <= max;
|
|
84
|
+
const lngLat = (value) => Array.isArray(value) && value.length === 2 && value.every(finite) && Math.abs(value[0]) <= 180 && Math.abs(value[1]) <= 90;
|
|
85
|
+
function parseKpi(raw, at) {
|
|
86
|
+
if (!finite(raw.value)) return bad(`${at}.value must be a number`);
|
|
87
|
+
const panel = {
|
|
88
|
+
kind: "kpi",
|
|
89
|
+
value: raw.value
|
|
90
|
+
};
|
|
91
|
+
if (raw.unit !== void 0) {
|
|
92
|
+
if (!text(raw.unit, 16)) return bad(`${at}.unit must be a short string`);
|
|
93
|
+
panel.unit = raw.unit;
|
|
94
|
+
}
|
|
95
|
+
if (raw.prefix !== void 0) {
|
|
96
|
+
if (!text(raw.prefix, 8)) return bad(`${at}.prefix must be a short string`);
|
|
97
|
+
panel.prefix = raw.prefix;
|
|
98
|
+
}
|
|
99
|
+
if (raw.decimals !== void 0) {
|
|
100
|
+
if (!Number.isInteger(raw.decimals) || raw.decimals < 0 || raw.decimals > 6) return bad(`${at}.decimals must be a whole number from 0 to 6`);
|
|
101
|
+
panel.decimals = raw.decimals;
|
|
102
|
+
}
|
|
103
|
+
if (raw.delta !== void 0) {
|
|
104
|
+
if (!finite(raw.delta)) return bad(`${at}.delta must be a number (a fraction: 0.12 is +12%)`);
|
|
105
|
+
panel.delta = raw.delta;
|
|
106
|
+
}
|
|
107
|
+
if (raw.upIsGood !== void 0) {
|
|
108
|
+
if (typeof raw.upIsGood !== "boolean") return bad(`${at}.upIsGood must be true or false`);
|
|
109
|
+
panel.upIsGood = raw.upIsGood;
|
|
110
|
+
}
|
|
111
|
+
if (raw.trend !== void 0) {
|
|
112
|
+
if (!Array.isArray(raw.trend) || raw.trend.length < 2 || raw.trend.length > 200 || !raw.trend.every(finite)) return bad(`${at}.trend must be 2 to 200 numbers`);
|
|
113
|
+
panel.trend = raw.trend;
|
|
114
|
+
}
|
|
115
|
+
if (raw.label !== void 0) {
|
|
116
|
+
if (!text(raw.label, 40)) return bad(`${at}.label must be a short string`);
|
|
117
|
+
panel.label = raw.label;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
value: panel
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function parseGauge(raw, at) {
|
|
125
|
+
if (!finite(raw.value)) return bad(`${at}.value must be a number`);
|
|
126
|
+
const panel = {
|
|
127
|
+
kind: "gauge",
|
|
128
|
+
value: raw.value
|
|
129
|
+
};
|
|
130
|
+
if (raw.max !== void 0) {
|
|
131
|
+
if (!finite(raw.max) || raw.max <= 0) return bad(`${at}.max must be a positive number`);
|
|
132
|
+
panel.max = raw.max;
|
|
133
|
+
}
|
|
134
|
+
if (raw.unit !== void 0) {
|
|
135
|
+
if (!text(raw.unit, 16)) return bad(`${at}.unit must be a short string`);
|
|
136
|
+
panel.unit = raw.unit;
|
|
137
|
+
}
|
|
138
|
+
if (raw.style !== void 0) {
|
|
139
|
+
if (raw.style !== "dial" && raw.style !== "ring") return bad(`${at}.style must be dial or ring`);
|
|
140
|
+
panel.style = raw.style;
|
|
141
|
+
}
|
|
142
|
+
if (raw.thresholds !== void 0) {
|
|
143
|
+
const t = raw.thresholds;
|
|
144
|
+
if (!Array.isArray(t) || t.length !== 2 || !t.every(finite) || !(t[0] >= 0 && t[0] <= t[1] && t[1] <= 1)) return bad(`${at}.thresholds must be two fractions in order, e.g. [0.6, 0.85]`);
|
|
145
|
+
panel.thresholds = [t[0], t[1]];
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
value: panel
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function parseRank(raw, at) {
|
|
153
|
+
if (!Array.isArray(raw.items) || raw.items.length === 0 || raw.items.length > MAX_ITEMS) return bad(`${at}.items must be 1 to ${MAX_ITEMS} entries`);
|
|
154
|
+
const items = [];
|
|
155
|
+
for (const [index, item] of raw.items.entries()) {
|
|
156
|
+
if (!isRecord(item) || !text(item.name, 40) || !finite(item.value)) return bad(`${at}.items[${index}] must be {name, value}`);
|
|
157
|
+
for (const key of Object.keys(item)) if (key !== "name" && key !== "value") return bad(`${at}.items[${index}].${key} is not a field of a rank item`);
|
|
158
|
+
items.push({
|
|
159
|
+
name: item.name,
|
|
160
|
+
value: item.value
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const panel = {
|
|
164
|
+
kind: "rank",
|
|
165
|
+
items
|
|
166
|
+
};
|
|
167
|
+
if (raw.unit !== void 0) {
|
|
168
|
+
if (!text(raw.unit, 16)) return bad(`${at}.unit must be a short string`);
|
|
169
|
+
panel.unit = raw.unit;
|
|
170
|
+
}
|
|
171
|
+
if (raw.top !== void 0) {
|
|
172
|
+
if (!Number.isInteger(raw.top) || raw.top < 1 || raw.top > MAX_ITEMS) return bad(`${at}.top must be a whole number from 1 to ${MAX_ITEMS}`);
|
|
173
|
+
panel.top = raw.top;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
value: panel
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function parseChart3d(raw, at) {
|
|
181
|
+
if (typeof raw.type !== "string" || !CHART3D_TYPES.has(raw.type)) return bad(`${at}.type must be one of ${[...CHART3D_TYPES].join(", ")}`);
|
|
182
|
+
if (!Array.isArray(raw.data) || raw.data.length === 0 || raw.data.length > MAX_POINTS) return bad(`${at}.data must be 1 to ${MAX_POINTS} points`);
|
|
183
|
+
const data = [];
|
|
184
|
+
for (const [index, point] of raw.data.entries()) {
|
|
185
|
+
if (!Array.isArray(point) || point.length !== 3 || !point.every(finite)) return bad(`${at}.data[${index}] must be [x, y, z]`);
|
|
186
|
+
data.push([
|
|
187
|
+
point[0],
|
|
188
|
+
point[1],
|
|
189
|
+
point[2]
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
const panel = {
|
|
193
|
+
kind: "chart3d",
|
|
194
|
+
type: raw.type,
|
|
195
|
+
data
|
|
196
|
+
};
|
|
197
|
+
for (const axis of ["xAxis", "yAxis"]) {
|
|
198
|
+
const categories = raw[axis];
|
|
199
|
+
if (categories === void 0) continue;
|
|
200
|
+
if (!Array.isArray(categories) || categories.length === 0 || categories.length > 200 || !categories.every((c) => text(c, 40))) return bad(`${at}.${axis} must be a list of category names`);
|
|
201
|
+
panel[axis] = categories;
|
|
202
|
+
}
|
|
203
|
+
if (raw.rotate !== void 0) {
|
|
204
|
+
if (typeof raw.rotate !== "boolean") return bad(`${at}.rotate must be true or false`);
|
|
205
|
+
panel.rotate = raw.rotate;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
value: panel
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function parseGlobe(raw, at) {
|
|
213
|
+
const panel = { kind: "globe" };
|
|
214
|
+
if (raw.arcs !== void 0) {
|
|
215
|
+
if (!Array.isArray(raw.arcs) || raw.arcs.length > MAX_ARCS) return bad(`${at}.arcs must be up to ${MAX_ARCS} entries`);
|
|
216
|
+
panel.arcs = [];
|
|
217
|
+
for (const [index, arc] of raw.arcs.entries()) {
|
|
218
|
+
if (!isRecord(arc) || !lngLat(arc.from) || !lngLat(arc.to)) return bad(`${at}.arcs[${index}] must be {from: [lng, lat], to: [lng, lat]}`);
|
|
219
|
+
const entry = {
|
|
220
|
+
from: arc.from,
|
|
221
|
+
to: arc.to
|
|
222
|
+
};
|
|
223
|
+
if (arc.label !== void 0) {
|
|
224
|
+
if (!text(arc.label, 40)) return bad(`${at}.arcs[${index}].label must be a short string`);
|
|
225
|
+
entry.label = arc.label;
|
|
226
|
+
}
|
|
227
|
+
panel.arcs.push(entry);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (raw.points !== void 0) {
|
|
231
|
+
if (!Array.isArray(raw.points) || raw.points.length > MAX_ARCS) return bad(`${at}.points must be up to ${MAX_ARCS} entries`);
|
|
232
|
+
panel.points = [];
|
|
233
|
+
for (const [index, point] of raw.points.entries()) {
|
|
234
|
+
if (!isRecord(point) || !lngLat(point.coord)) return bad(`${at}.points[${index}] must be {coord: [lng, lat]}`);
|
|
235
|
+
const entry = { coord: point.coord };
|
|
236
|
+
if (point.label !== void 0) {
|
|
237
|
+
if (!text(point.label, 40)) return bad(`${at}.points[${index}].label must be a short string`);
|
|
238
|
+
entry.label = point.label;
|
|
239
|
+
}
|
|
240
|
+
if (point.value !== void 0) {
|
|
241
|
+
if (!finite(point.value) || point.value < 0) return bad(`${at}.points[${index}].value must be zero or a positive number`);
|
|
242
|
+
entry.value = point.value;
|
|
243
|
+
}
|
|
244
|
+
panel.points.push(entry);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!panel.arcs?.length && !panel.points?.length) return bad(`${at} needs arcs or points`);
|
|
248
|
+
if (raw.rotate !== void 0) {
|
|
249
|
+
if (typeof raw.rotate !== "boolean") return bad(`${at}.rotate must be true or false`);
|
|
250
|
+
panel.rotate = raw.rotate;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
value: panel
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function parsePanel(raw, index, columns) {
|
|
258
|
+
const at = `panels[${index}]`;
|
|
259
|
+
if (!isRecord(raw)) return bad(`${at} must be an object`);
|
|
260
|
+
if (typeof raw.kind !== "string" || !KINDS.has(raw.kind)) return bad(`${at}.kind must be one of ${[...KINDS].join(", ")}`);
|
|
261
|
+
const kind = raw.kind;
|
|
262
|
+
const allowed = new Set([...COMMON, ...FIELDS[kind]]);
|
|
263
|
+
for (const key of Object.keys(raw)) if (!allowed.has(key)) return bad(`${at}.${key} is not a field of a ${kind} panel`);
|
|
264
|
+
let parsed;
|
|
265
|
+
switch (kind) {
|
|
266
|
+
case "kpi":
|
|
267
|
+
parsed = parseKpi(raw, at);
|
|
268
|
+
break;
|
|
269
|
+
case "gauge":
|
|
270
|
+
parsed = parseGauge(raw, at);
|
|
271
|
+
break;
|
|
272
|
+
case "rank":
|
|
273
|
+
parsed = parseRank(raw, at);
|
|
274
|
+
break;
|
|
275
|
+
case "chart":
|
|
276
|
+
if (!isRecord(raw.option)) return bad(`${at}.option must be an ECharts option object`);
|
|
277
|
+
parsed = {
|
|
278
|
+
ok: true,
|
|
279
|
+
value: {
|
|
280
|
+
kind: "chart",
|
|
281
|
+
option: raw.option
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
break;
|
|
285
|
+
case "chart3d":
|
|
286
|
+
parsed = parseChart3d(raw, at);
|
|
287
|
+
break;
|
|
288
|
+
case "globe":
|
|
289
|
+
parsed = parseGlobe(raw, at);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (!parsed.ok) return parsed;
|
|
293
|
+
const panel = parsed.value;
|
|
294
|
+
if (raw.title !== void 0) {
|
|
295
|
+
if (!text(raw.title, 80)) return bad(`${at}.title must be a short string`);
|
|
296
|
+
panel.title = raw.title;
|
|
297
|
+
}
|
|
298
|
+
if (raw.span !== void 0) {
|
|
299
|
+
if (!Number.isInteger(raw.span) || raw.span < 1 || raw.span > columns) return bad(`${at}.span must be a whole number from 1 to ${columns}`);
|
|
300
|
+
panel.span = raw.span;
|
|
301
|
+
}
|
|
302
|
+
if (raw.height !== void 0) {
|
|
303
|
+
if (!finite(raw.height) || raw.height < 80 || raw.height > 900) return bad(`${at}.height must be from 80 to 900 pixels`);
|
|
304
|
+
panel.height = raw.height;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
ok: true,
|
|
308
|
+
value: panel
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Validate one `bigscreen` fence, or explain why it cannot be shown. */
|
|
312
|
+
function parseBigscreen(source, options = {}) {
|
|
313
|
+
const maxPanels = options.maxPanels ?? 24;
|
|
314
|
+
const maxSourceBytes = options.maxSourceBytes ?? 64 * 1024;
|
|
315
|
+
if (new TextEncoder().encode(source).byteLength > maxSourceBytes) return {
|
|
316
|
+
ok: false,
|
|
317
|
+
error: {
|
|
318
|
+
code: "too-large",
|
|
319
|
+
message: "Screen definition is too large."
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
let raw;
|
|
323
|
+
try {
|
|
324
|
+
raw = JSON.parse(source);
|
|
325
|
+
} catch {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
error: {
|
|
329
|
+
code: "invalid-json",
|
|
330
|
+
message: "Screen definition is not valid JSON."
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (!isRecord(raw)) return bad("A screen definition must be a JSON object");
|
|
335
|
+
for (const key of Object.keys(raw)) if (!SCREEN_FIELDS.has(key)) return bad(`${key} is not a field of a screen definition`);
|
|
336
|
+
const definition = {
|
|
337
|
+
theme: options.theme ?? "dark",
|
|
338
|
+
columns: 12,
|
|
339
|
+
panels: []
|
|
340
|
+
};
|
|
341
|
+
if (raw.title !== void 0) {
|
|
342
|
+
if (!text(raw.title, 80)) return bad("title must be a short string");
|
|
343
|
+
definition.title = raw.title;
|
|
344
|
+
}
|
|
345
|
+
if (raw.subtitle !== void 0) {
|
|
346
|
+
if (!text(raw.subtitle, 120)) return bad("subtitle must be a short string");
|
|
347
|
+
definition.subtitle = raw.subtitle;
|
|
348
|
+
}
|
|
349
|
+
if (raw.theme !== void 0) {
|
|
350
|
+
if (raw.theme !== "dark" && raw.theme !== "light") return bad("theme must be dark or light");
|
|
351
|
+
definition.theme = raw.theme;
|
|
352
|
+
}
|
|
353
|
+
if (raw.accent !== void 0) {
|
|
354
|
+
if (typeof raw.accent !== "string" || !HEX.test(raw.accent)) return bad("accent must be a hex colour like #22d3ee");
|
|
355
|
+
definition.accent = raw.accent;
|
|
356
|
+
}
|
|
357
|
+
if (raw.columns !== void 0) {
|
|
358
|
+
if (!Number.isInteger(raw.columns) || raw.columns < 1 || raw.columns > 24) return bad("columns must be a whole number from 1 to 24");
|
|
359
|
+
definition.columns = raw.columns;
|
|
360
|
+
}
|
|
361
|
+
if (!Array.isArray(raw.panels) || raw.panels.length === 0) return bad("panels must be a non-empty array");
|
|
362
|
+
if (raw.panels.length > maxPanels) return bad(`panels has more than ${maxPanels} entries`);
|
|
363
|
+
for (const [index, entry] of raw.panels.entries()) {
|
|
364
|
+
const panel = parsePanel(entry, index, definition.columns);
|
|
365
|
+
if (!panel.ok) return panel;
|
|
366
|
+
definition.panels.push(panel.value);
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
value: definition
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/prompt.ts
|
|
376
|
+
/**
|
|
377
|
+
* The model-facing rules for a data wall.
|
|
378
|
+
*
|
|
379
|
+
* The rule that must survive editing is "只用对话里已有的数字": a screen full of counting
|
|
380
|
+
* numbers looks like evidence, and a model that fills gaps with plausible figures produces a
|
|
381
|
+
* convincing lie. The examples show every panel kind once, because that is what a model copies,
|
|
382
|
+
* and the numbers in them are obviously placeholders.
|
|
383
|
+
*
|
|
384
|
+
* You rarely want this directly: `buildSystemPrompt({ registry, plugins, locale })` from
|
|
385
|
+
* `@ai-gui/core` collects the card specs and every enabled plugin's spec in one call, in the
|
|
386
|
+
* product's language. Reach for this only to inspect or override one plugin's rules.
|
|
387
|
+
*/
|
|
388
|
+
function bigscreenPromptSpec(locale) {
|
|
389
|
+
return translate(PROMPT, locale, "spec");
|
|
390
|
+
}
|
|
391
|
+
const ZH = `数据大屏(围栏代码块):\`\`\`bigscreen 开头,块内是一个 JSON 对象。当用户要一个"大屏 / 看板 / dashboard / 总览",或者要把一组指标、排行、趋势、地域分布放在一起漂亮地展示时,输出一个 bigscreen 块。它会画成深色发光的数据墙:数字滚动增长、仪表盘扫过、排行条生长、图表逐步绘出、3D 图和地球缓慢旋转。文字说明照常写在块外。
|
|
392
|
+
|
|
393
|
+
只用对话里已有的数字。没有的数据就不要放这个面板,或者在块外说明缺什么。绝对不要编造一个看起来合理的数字来填空——大屏上的数字看起来像证据。
|
|
394
|
+
|
|
395
|
+
顶层字段:
|
|
396
|
+
- title、subtitle:大屏标题、副标题
|
|
397
|
+
- theme:"dark"(默认)| "light"
|
|
398
|
+
- accent:主色,十六进制,如 "#22d3ee"
|
|
399
|
+
- columns:网格列数,默认 12
|
|
400
|
+
- panels(必填):面板数组,最多 24 个
|
|
401
|
+
|
|
402
|
+
每个面板:kind(必填)、title、span(占几列,默认 4)、height(像素)。按 kind 填:
|
|
403
|
+
- "kpi":value(必填)、unit、prefix(如 "¥")、decimals、delta(变化率,小数:0.12 就是 +12%)、upIsGood(默认 true)、trend(一列数,画成迷你折线)、label
|
|
404
|
+
- "gauge":value(必填)、max(默认 100)、unit、style "dial"(默认)| "ring"、thresholds [0.6, 0.85](告警线:超过第一个变黄,超过第二个变红——只给"越高越危险"的指标用,如 CPU 占用、库存水位;完成率这类越高越好的不要给)
|
|
405
|
+
- "rank":items(必填,[{"name","value"}])、unit、top(默认 8)
|
|
406
|
+
- "chart":option(必填)——完整的 ECharts option,颜色、字体、动画由大屏统一
|
|
407
|
+
- "chart3d":type "bar3D" | "scatter3D" | "surface" | "line3D"、data([[x, y, z], ...])、xAxis / yAxis(类目名列表,给了的话 x、y 就是下标)、rotate(默认 true)
|
|
408
|
+
- "globe":arcs([{"from": [经度, 纬度], "to": [经度, 纬度], "label"}])、points([{"coord": [经度, 纬度], "label", "value"}])、rotate(默认 true)。至少给一个。适合洲际、跨国的流向;同一个省内的线路在地球上只有几个像素,那种用普通图表
|
|
409
|
+
|
|
410
|
+
布局建议:第一行放 3–4 个 kpi(各 span 3),然后大图 span 8 配一个 rank 或 gauge span 4,3D 和地球 span 6。
|
|
411
|
+
|
|
412
|
+
例子——销售大屏:
|
|
413
|
+
|
|
414
|
+
\`\`\`bigscreen
|
|
415
|
+
{
|
|
416
|
+
"title": "华东区销售大屏",
|
|
417
|
+
"subtitle": "2026 年 8 月",
|
|
418
|
+
"panels": [
|
|
419
|
+
{ "kind": "kpi", "title": "本月营收", "value": 12843000, "prefix": "¥", "delta": 0.124, "trend": [8.1, 8.6, 9.2, 9.0, 10.4, 11.9, 12.8], "span": 3 },
|
|
420
|
+
{ "kind": "kpi", "title": "订单数", "value": 48210, "unit": "单", "delta": 0.051, "span": 3 },
|
|
421
|
+
{ "kind": "kpi", "title": "客单价", "value": 266.4, "prefix": "¥", "decimals": 1, "delta": -0.018, "span": 3 },
|
|
422
|
+
{ "kind": "gauge", "title": "目标完成率", "value": 82, "unit": "%", "span": 3 },
|
|
423
|
+
{ "kind": "chart", "title": "月度趋势", "span": 8, "option": { "xAxis": { "type": "category", "data": ["3月", "4月", "5月", "6月", "7月", "8月"] }, "yAxis": { "type": "value" }, "series": [{ "type": "line", "smooth": true, "areaStyle": {}, "data": [820, 932, 901, 934, 1290, 1330] }] } },
|
|
424
|
+
{ "kind": "rank", "title": "门店排行", "span": 4, "unit": "万", "items": [{ "name": "上海", "value": 320 }, { "name": "杭州", "value": 245 }, { "name": "南京", "value": 198 }, { "name": "苏州", "value": 176 }, { "name": "宁波", "value": 121 }] },
|
|
425
|
+
{ "kind": "chart3d", "title": "品类 × 月份", "span": 6, "type": "bar3D", "xAxis": ["6月", "7月", "8月"], "yAxis": ["家电", "服饰", "食品"], "data": [[0, 0, 120], [1, 0, 150], [2, 0, 180], [0, 1, 90], [1, 1, 110], [2, 1, 140], [0, 2, 60], [1, 2, 75], [2, 2, 95]] },
|
|
426
|
+
{ "kind": "globe", "title": "出口流向", "span": 6, "arcs": [{ "from": [121.47, 31.23], "to": [8.68, 50.11], "label": "上海→法兰克福" }, { "from": [121.47, 31.23], "to": [-74.01, 40.71], "label": "上海→纽约" }, { "from": [121.47, 31.23], "to": [151.21, -33.87], "label": "上海→悉尼" }], "points": [{ "coord": [121.47, 31.23], "label": "上海", "value": 320 }, { "coord": [-74.01, 40.71], "label": "纽约", "value": 120 }] }
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
\`\`\`
|
|
430
|
+
|
|
431
|
+
这一版画不了的(遇到就退回普通图表或文字):实时刷新、可交互筛选、表格、地图着色(省份热力图)、视频。需要宿主提供真实数据的 BI 看板用 dashboard 块,不用这个。`;
|
|
432
|
+
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
|
+
|
|
434
|
+
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
|
+
|
|
436
|
+
Top level: title, subtitle, theme ("dark" default or "light"), accent (hex), columns (default 12), panels (required, at most 24).
|
|
437
|
+
|
|
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).
|
|
439
|
+
|
|
440
|
+
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
|
+
|
|
442
|
+
Example:
|
|
443
|
+
|
|
444
|
+
\`\`\`bigscreen
|
|
445
|
+
{
|
|
446
|
+
"title": "East region sales",
|
|
447
|
+
"subtitle": "August 2026",
|
|
448
|
+
"panels": [
|
|
449
|
+
{ "kind": "kpi", "title": "Revenue", "value": 12843000, "prefix": "$", "delta": 0.124, "trend": [8.1, 8.6, 9.2, 9.0, 10.4, 11.9, 12.8], "span": 4 },
|
|
450
|
+
{ "kind": "gauge", "title": "Target", "value": 82, "unit": "%", "span": 4 },
|
|
451
|
+
{ "kind": "rank", "title": "Stores", "span": 4, "items": [{ "name": "Shanghai", "value": 320 }, { "name": "Hangzhou", "value": 245 }, { "name": "Nanjing", "value": 198 }] },
|
|
452
|
+
{ "kind": "chart", "title": "Trend", "span": 6, "option": { "xAxis": { "type": "category", "data": ["Mar", "Apr", "May", "Jun", "Jul", "Aug"] }, "yAxis": { "type": "value" }, "series": [{ "type": "line", "smooth": true, "areaStyle": {}, "data": [820, 932, 901, 934, 1290, 1330] }] } },
|
|
453
|
+
{ "kind": "chart3d", "title": "Category × month", "span": 6, "type": "bar3D", "xAxis": ["Jun", "Jul", "Aug"], "yAxis": ["Appliances", "Apparel", "Food"], "data": [[0, 0, 120], [1, 0, 150], [2, 0, 180], [0, 1, 90], [1, 1, 110], [2, 1, 140], [0, 2, 60], [1, 2, 75], [2, 2, 95]] }
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
\`\`\`
|
|
457
|
+
|
|
458
|
+
Not supported — fall back to ordinary charts or prose: live refresh, interactive filters, tables, choropleth maps, video. A BI board whose numbers must come from the host's own queries is a dashboard block, not this.`;
|
|
459
|
+
const PROMPT = {
|
|
460
|
+
en: { spec: EN },
|
|
461
|
+
"zh-CN": { spec: ZH }
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/index.ts
|
|
466
|
+
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
467
|
+
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}",
|
|
469
|
+
"[data-aigui-bigscreen] *{box-sizing:border-box}",
|
|
470
|
+
"[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
|
+
"[data-aigui-bigscreen='light']{background:linear-gradient(180deg,#f8fafc,#eef2f7);box-shadow:inset 0 0 0 1px rgba(15,23,42,.06)}",
|
|
472
|
+
".aigui-bs-head{text-align:center;margin-bottom:1rem}",
|
|
473
|
+
".aigui-bs-title{margin:0;font-size:1.5rem;font-weight:700;letter-spacing:.06em}",
|
|
474
|
+
".aigui-bs-subtitle{margin:.25rem 0 0;font-size:.85rem;color:var(--aigui-bs-muted);letter-spacing:.12em}",
|
|
475
|
+
".aigui-bs-rule{height:1px;margin:.8rem auto 0;width:70%}",
|
|
476
|
+
".aigui-bs-grid{display:grid;gap:.9rem}",
|
|
477
|
+
".aigui-bs-panel{display:flex;flex-direction:column;min-width:0;border:1px solid;border-radius:10px;padding:.7rem .85rem .85rem;backdrop-filter:blur(6px)}",
|
|
478
|
+
"[data-aigui-bigscreen='dark'] .aigui-bs-panel{background:linear-gradient(180deg,rgba(17,27,50,.82),rgba(10,18,36,.82));box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 10px 30px rgba(0,0,0,.35)}",
|
|
479
|
+
"[data-aigui-bigscreen='light'] .aigui-bs-panel{background:rgba(255,255,255,.9);box-shadow:0 6px 20px rgba(15,23,42,.06)}",
|
|
480
|
+
".aigui-bs-panel-title{display:flex;align-items:center;gap:.5rem;margin:0 0 .5rem;font-size:.8rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--aigui-bs-muted)}",
|
|
481
|
+
".aigui-bs-panel-mark{display:inline-block;width:4px;height:14px;border-radius:2px}",
|
|
482
|
+
".aigui-bs-panel-body{position:relative;flex:1 1 auto;width:100%;min-width:0;container-type:inline-size}",
|
|
483
|
+
".aigui-bs-kpi{display:flex;flex-direction:column;gap:.35rem;justify-content:center;height:100%}",
|
|
484
|
+
".aigui-bs-kpi-row{display:flex;align-items:baseline;gap:.35rem;min-width:0}",
|
|
485
|
+
".aigui-bs-kpi-value{font-size:2rem;font-size:clamp(1.2rem,13cqi,2.6rem);font-weight:800;line-height:1.05;letter-spacing:-.01em;white-space:nowrap}",
|
|
486
|
+
".aigui-bs-kpi-unit{font-size:.95rem;color:var(--aigui-bs-muted)}",
|
|
487
|
+
".aigui-bs-kpi-meta{display:flex;gap:.7rem;font-size:.8rem;color:var(--aigui-bs-muted)}",
|
|
488
|
+
".aigui-bs-kpi-delta{font-weight:600}",
|
|
489
|
+
".aigui-bs-kpi-spark{margin-top:.2rem;opacity:.9}",
|
|
490
|
+
".aigui-bs-rank{display:flex;flex-direction:column;gap:.45rem;overflow:hidden}",
|
|
491
|
+
".aigui-bs-rank-row{display:grid;grid-template-columns:1.4rem minmax(3rem,auto) 1fr auto;align-items:center;gap:.5rem;font-size:.85rem}",
|
|
492
|
+
".aigui-bs-rank-n{font-weight:700;color:var(--aigui-bs-muted);text-align:center}",
|
|
493
|
+
".aigui-bs-rank-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
494
|
+
".aigui-bs-rank-track{display:block;height:8px;border-radius:4px;overflow:hidden}",
|
|
495
|
+
".aigui-bs-rank-fill{display:block;height:100%;border-radius:4px;transition:width 1.2s cubic-bezier(.22,1,.36,1)}",
|
|
496
|
+
".aigui-bs-rank-value{color:var(--aigui-bs-muted);font-variant-numeric:tabular-nums}",
|
|
497
|
+
".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
|
+
"[data-aigui-bigscreen-loading]{min-height:8rem;border-radius:14px;background:currentColor;opacity:.06}",
|
|
499
|
+
"[data-aigui-bigscreen-error]{padding:.5rem .75rem;border-radius:.5rem;font-size:.875rem;opacity:.8;background:currentColor}",
|
|
500
|
+
"@media (max-width:640px){.aigui-bs-grid{grid-template-columns:1fr!important}.aigui-bs-panel{grid-column:span 1!important}}"
|
|
501
|
+
].join("");
|
|
502
|
+
function failed(message) {
|
|
503
|
+
const text$1 = escapeHtml(message);
|
|
504
|
+
return {
|
|
505
|
+
kind: "html",
|
|
506
|
+
html: `<div data-aigui-bigscreen-error role="img" aria-label="${text$1}">${text$1}</div>`,
|
|
507
|
+
trusted: true
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* A data wall: KPIs that count up, gauges that sweep, ranks that grow, charts that draw
|
|
512
|
+
* themselves, and 3D bars and globes that turn — laid out on a grid the model chooses.
|
|
513
|
+
*
|
|
514
|
+
* The screen is presentation, not evidence. Where the numbers must be the host's — a BI board
|
|
515
|
+
* over real queries — `@ai-gui/plugin-dashboard` is the block, and it refuses to let the model
|
|
516
|
+
* write a row. This one lets the model lay out the numbers it was given, which is what a
|
|
517
|
+
* summary, a demo or a briefing needs, and the prompt spec tells it never to invent them.
|
|
518
|
+
*/
|
|
519
|
+
function bigscreen(options = {}) {
|
|
520
|
+
const render = (node, context) => {
|
|
521
|
+
if (node.complete === false) return {
|
|
522
|
+
kind: "html",
|
|
523
|
+
html: "<div data-aigui-bigscreen-loading aria-label=\"Loading screen\"></div>"
|
|
524
|
+
};
|
|
525
|
+
const theme = options.theme ?? (context?.theme === "light" ? "light" : context?.theme === "dark" ? "dark" : void 0);
|
|
526
|
+
const parsed = parseBigscreen(node.content ?? "", {
|
|
527
|
+
...options,
|
|
528
|
+
theme
|
|
529
|
+
});
|
|
530
|
+
if (!parsed.ok) return failed(parsed.error.message);
|
|
531
|
+
const definition = parsed.value;
|
|
532
|
+
return {
|
|
533
|
+
kind: "mount",
|
|
534
|
+
mount: (el) => {
|
|
535
|
+
let destroy;
|
|
536
|
+
let disposed = false;
|
|
537
|
+
import("./mount-DVHPS1xP.js").then(({ mountScreen }) => {
|
|
538
|
+
if (disposed) return;
|
|
539
|
+
destroy = mountScreen(el, definition, options.animate !== false);
|
|
540
|
+
}).catch(() => {
|
|
541
|
+
const error = document.createElement("div");
|
|
542
|
+
error.setAttribute("data-aigui-bigscreen-error", "");
|
|
543
|
+
error.setAttribute("role", "img");
|
|
544
|
+
error.setAttribute("aria-label", "Screen could not be drawn.");
|
|
545
|
+
error.textContent = "Screen could not be drawn.";
|
|
546
|
+
el.replaceChildren(error);
|
|
547
|
+
});
|
|
548
|
+
return () => {
|
|
549
|
+
disposed = true;
|
|
550
|
+
destroy?.();
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
};
|
|
555
|
+
return {
|
|
556
|
+
name: "bigscreen",
|
|
557
|
+
css: bigscreenCss,
|
|
558
|
+
nodeRenderers: { bigscreen: render },
|
|
559
|
+
isBlockComplete: (_type, raw) => {
|
|
560
|
+
const text$1 = raw.trim();
|
|
561
|
+
if (!text$1.startsWith("{") || !text$1.endsWith("}")) return false;
|
|
562
|
+
try {
|
|
563
|
+
JSON.parse(text$1);
|
|
564
|
+
return true;
|
|
565
|
+
} catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
promptSpec: (locale) => bigscreenPromptSpec(locale)
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
//#endregion
|
|
574
|
+
export { bigscreen, bigscreenCss, bigscreenPromptSpec, chart3dOption, chartOption, formatNumber, gaugeColour, gaugeOption, globeOption, palette, parseBigscreen, withAlpha };
|