@ai-gui/plugin-bigscreen 0.36.3 → 0.37.1
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 +64 -0
- package/README.md +20 -3
- package/dist/index.cjs +478 -14
- package/dist/index.d.cts +634 -4
- package/dist/index.d.ts +634 -4
- package/dist/index.js +416 -6
- package/dist/{mount-DPyPP9Ha.cjs → mount-BXSCLn67.cjs} +164 -27
- package/dist/{mount-CbzNLMoI.js → mount-ChcoxgHd.js} +147 -10
- package/dist/positions-CHtRx0fb.cjs +1933 -0
- package/dist/positions-CN8aP4-S.js +1621 -0
- package/package.json +2 -2
- package/dist/options-BpHnWRpI.cjs +0 -499
- package/dist/options-C087Peeh.js +0 -415
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chart3dOption, chartOption, formatNumber, gaugeColour, gaugeOption, globeOption, palette, withAlpha } from "./
|
|
1
|
+
import { EDGE_VERTICES, GRAPH_LABELS, GRAPH_LEGEND_ROWS, GRAPH_MAX_STEPS, GRAPH_SETTLE_STEPS, LAYOUT_GRAVITY, LAYOUT_SPRING, ORBIT_BOX, ORBIT_DISTANCE, ORBIT_EXTENT, ORBIT_FOV, REMEMBERED_GRAPHS, TIMELINE_LABELS, chart3dOption, chartOption, countriesTexture, createLayout, degrees, earthColours, earthTexture, forgetPositions, formatNumber, gaugeColour, gaugeOption, globeOption, globeTexture, graph3dOption, graphKey, graphLegend, graphOrbitData, graphOrbitOption, graphTooltip, hash, layoutChunk, layoutRadius, layoutSteps, orbitFrame, palette, recallPositions, rememberPositions, seedPositions, spacedItems, timelineHeight, timelineOption, timelineWindow, typeColour, withAlpha } from "./positions-CN8aP4-S.js";
|
|
2
2
|
import { translate } from "@ai-gui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/parse.ts
|
|
@@ -51,6 +51,21 @@ const FIELDS = {
|
|
|
51
51
|
"arcs",
|
|
52
52
|
"points",
|
|
53
53
|
"rotate"
|
|
54
|
+
],
|
|
55
|
+
timeline: [
|
|
56
|
+
"lanes",
|
|
57
|
+
"items",
|
|
58
|
+
"links",
|
|
59
|
+
"from",
|
|
60
|
+
"to"
|
|
61
|
+
],
|
|
62
|
+
graph3d: [
|
|
63
|
+
"nodes",
|
|
64
|
+
"edges",
|
|
65
|
+
"types",
|
|
66
|
+
"focus",
|
|
67
|
+
"mode",
|
|
68
|
+
"rotate"
|
|
54
69
|
]
|
|
55
70
|
};
|
|
56
71
|
const KINDS = new Set([
|
|
@@ -59,7 +74,9 @@ const KINDS = new Set([
|
|
|
59
74
|
"rank",
|
|
60
75
|
"chart",
|
|
61
76
|
"chart3d",
|
|
62
|
-
"globe"
|
|
77
|
+
"globe",
|
|
78
|
+
"timeline",
|
|
79
|
+
"graph3d"
|
|
63
80
|
]);
|
|
64
81
|
const CHART3D_TYPES = new Set([
|
|
65
82
|
"bar3D",
|
|
@@ -67,10 +84,67 @@ const CHART3D_TYPES = new Set([
|
|
|
67
84
|
"surface",
|
|
68
85
|
"line3D"
|
|
69
86
|
]);
|
|
87
|
+
const LINK_KINDS = new Set([
|
|
88
|
+
"contradicts",
|
|
89
|
+
"follows",
|
|
90
|
+
"same"
|
|
91
|
+
]);
|
|
92
|
+
const LANE_FIELDS = new Set([
|
|
93
|
+
"id",
|
|
94
|
+
"name",
|
|
95
|
+
"color"
|
|
96
|
+
]);
|
|
97
|
+
const TIMELINE_ITEM_FIELDS = new Set([
|
|
98
|
+
"id",
|
|
99
|
+
"lane",
|
|
100
|
+
"at",
|
|
101
|
+
"label",
|
|
102
|
+
"detail",
|
|
103
|
+
"url",
|
|
104
|
+
"value"
|
|
105
|
+
]);
|
|
106
|
+
const LINK_FIELDS = new Set([
|
|
107
|
+
"from",
|
|
108
|
+
"to",
|
|
109
|
+
"kind"
|
|
110
|
+
]);
|
|
111
|
+
const NODE_FIELDS = new Set([
|
|
112
|
+
"id",
|
|
113
|
+
"name",
|
|
114
|
+
"type",
|
|
115
|
+
"value"
|
|
116
|
+
]);
|
|
117
|
+
const EDGE_FIELDS = new Set([
|
|
118
|
+
"from",
|
|
119
|
+
"to",
|
|
120
|
+
"type"
|
|
121
|
+
]);
|
|
70
122
|
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
123
|
+
/**
|
|
124
|
+
* Every limit the parser enforces, named.
|
|
125
|
+
*
|
|
126
|
+
* Exported because a limit the parser checks and nothing states is a trap: the prompt spec has to
|
|
127
|
+
* name the same numbers, and a host building a fence of its own needs to know where the cliff is.
|
|
128
|
+
* Overrunning any of them voids the whole block, so they are all deliberately generous — generous
|
|
129
|
+
* enough that the biggest of them are not reachable under the default 64 KiB source cap: five
|
|
130
|
+
* thousand edges is around a hundred kilobytes of JSON, so a host that really wants a graph that
|
|
131
|
+
* size has to raise `maxSourceBytes` as well.
|
|
132
|
+
*/
|
|
71
133
|
const MAX_POINTS = 5e3;
|
|
72
134
|
const MAX_ITEMS = 50;
|
|
73
135
|
const MAX_ARCS = 300;
|
|
136
|
+
const MAX_LANES = 24;
|
|
137
|
+
const MAX_TIMELINE_ITEMS = 500;
|
|
138
|
+
const MAX_LINKS = 500;
|
|
139
|
+
const MAX_NODES = 2e3;
|
|
140
|
+
const MAX_EDGES = 5e3;
|
|
141
|
+
const MAX_TYPES = 32;
|
|
142
|
+
const MAX_LANE_NAME = 40;
|
|
143
|
+
const MAX_ITEM_LABEL = 120;
|
|
144
|
+
const MAX_ITEM_DETAIL = 400;
|
|
145
|
+
const MAX_URL = 400;
|
|
146
|
+
const MAX_NODE_NAME = 80;
|
|
147
|
+
const MAX_TYPE_NAME = 32;
|
|
74
148
|
const bad = (message) => ({
|
|
75
149
|
ok: false,
|
|
76
150
|
error: {
|
|
@@ -91,6 +165,23 @@ const text = (value, max = 120) => typeof value === "string" && value.length <=
|
|
|
91
165
|
* model that had never been told there was a limit.
|
|
92
166
|
*/
|
|
93
167
|
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`;
|
|
168
|
+
/**
|
|
169
|
+
* A moment, as far as `Date.parse` is concerned — which is what the chart will read it with.
|
|
170
|
+
*
|
|
171
|
+
* Deliberately the platform's own parser rather than a stricter regular expression: a model that
|
|
172
|
+
* writes `2026-09-01T08:00:00+02:00` or `2026-09-01` has said something a timeline can place, and
|
|
173
|
+
* refusing the whole block over the shape of a suffix helps nobody.
|
|
174
|
+
*/
|
|
175
|
+
const instant = (value) => typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
|
|
176
|
+
/**
|
|
177
|
+
* A link a click may follow.
|
|
178
|
+
*
|
|
179
|
+
* Only `http` and `https`: this URL ends up in `window.open`, and `javascript:` there is script
|
|
180
|
+
* the page runs because a model asked it to.
|
|
181
|
+
*/
|
|
182
|
+
const httpUrl = (value) => typeof value === "string" && value.length <= MAX_URL && /^https?:\/\/\S+$/i.test(value);
|
|
183
|
+
/** The first key that is not in `allowed`, or undefined. */
|
|
184
|
+
const stray = (raw, allowed) => Object.keys(raw).find((key) => !allowed.has(key));
|
|
94
185
|
const lngLat = (value) => Array.isArray(value) && value.length === 2 && value.every(finite) && Math.abs(value[0]) <= 180 && Math.abs(value[1]) <= 90;
|
|
95
186
|
function parseKpi(raw, at) {
|
|
96
187
|
if (!finite(raw.value)) return bad(`${at}.value must be a number`);
|
|
@@ -264,6 +355,181 @@ function parseGlobe(raw, at) {
|
|
|
264
355
|
value: panel
|
|
265
356
|
};
|
|
266
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* A timeline, checked against itself.
|
|
360
|
+
*
|
|
361
|
+
* The references are the part worth the code: a claim on a lane nobody declared, or a
|
|
362
|
+
* contradiction between an id and nothing, is dropped silently by every charting library there
|
|
363
|
+
* is — and a timeline missing the very link it was drawn for is worse than no timeline at all.
|
|
364
|
+
*/
|
|
365
|
+
function parseTimeline(raw, at) {
|
|
366
|
+
if (!Array.isArray(raw.lanes) || raw.lanes.length === 0 || raw.lanes.length > MAX_LANES) return bad(`${at}.lanes must be 1 to ${MAX_LANES} entries`);
|
|
367
|
+
const lanes = [];
|
|
368
|
+
const laneIds = new Set();
|
|
369
|
+
for (const [index, lane] of raw.lanes.entries()) {
|
|
370
|
+
const where = `${at}.lanes[${index}]`;
|
|
371
|
+
if (!isRecord(lane) || !text(lane.id, 64) || typeof lane.name !== "string") return bad(`${where} must be {id, name}`);
|
|
372
|
+
const key = stray(lane, LANE_FIELDS);
|
|
373
|
+
if (key) return bad(`${where}.${key} is not a field of a timeline lane`);
|
|
374
|
+
if (!text(lane.name, MAX_LANE_NAME)) return bad(tooLong(`${where}.name`, MAX_LANE_NAME, lane.name));
|
|
375
|
+
if (laneIds.has(lane.id)) return bad(`${where}.id is a duplicate lane id`);
|
|
376
|
+
laneIds.add(lane.id);
|
|
377
|
+
const entry = {
|
|
378
|
+
id: lane.id,
|
|
379
|
+
name: lane.name
|
|
380
|
+
};
|
|
381
|
+
if (lane.color !== void 0) {
|
|
382
|
+
if (typeof lane.color !== "string" || !HEX.test(lane.color)) return bad(`${where}.color must be a hex colour like #22d3ee`);
|
|
383
|
+
entry.color = lane.color;
|
|
384
|
+
}
|
|
385
|
+
lanes.push(entry);
|
|
386
|
+
}
|
|
387
|
+
if (!Array.isArray(raw.items) || raw.items.length === 0 || raw.items.length > MAX_TIMELINE_ITEMS) return bad(`${at}.items must be 1 to ${MAX_TIMELINE_ITEMS} entries`);
|
|
388
|
+
const items = [];
|
|
389
|
+
const itemIds = new Set();
|
|
390
|
+
for (const [index, item] of raw.items.entries()) {
|
|
391
|
+
const where = `${at}.items[${index}]`;
|
|
392
|
+
if (!isRecord(item) || typeof item.lane !== "string" || typeof item.label !== "string" || item.at === void 0) return bad(`${where} must be {lane, at, label}`);
|
|
393
|
+
const key = stray(item, TIMELINE_ITEM_FIELDS);
|
|
394
|
+
if (key) return bad(`${where}.${key} is not a field of a timeline item`);
|
|
395
|
+
if (!laneIds.has(item.lane)) return bad(`${where}.lane is not one of the panel's lane ids`);
|
|
396
|
+
if (!instant(item.at)) return bad(`${where}.at must be an ISO 8601 date-time, like 2026-09-01T08:00:00Z`);
|
|
397
|
+
if (!text(item.label, MAX_ITEM_LABEL)) return bad(tooLong(`${where}.label`, MAX_ITEM_LABEL, item.label));
|
|
398
|
+
const entry = {
|
|
399
|
+
lane: item.lane,
|
|
400
|
+
at: item.at,
|
|
401
|
+
label: item.label
|
|
402
|
+
};
|
|
403
|
+
if (item.id !== void 0) {
|
|
404
|
+
if (!text(item.id, 64)) return bad(tooLong(`${where}.id`, 64, item.id));
|
|
405
|
+
if (itemIds.has(item.id)) return bad(`${where}.id is a duplicate item id`);
|
|
406
|
+
itemIds.add(item.id);
|
|
407
|
+
entry.id = item.id;
|
|
408
|
+
}
|
|
409
|
+
if (item.detail !== void 0) {
|
|
410
|
+
if (!text(item.detail, MAX_ITEM_DETAIL)) return bad(tooLong(`${where}.detail`, MAX_ITEM_DETAIL, item.detail));
|
|
411
|
+
entry.detail = item.detail;
|
|
412
|
+
}
|
|
413
|
+
if (item.url !== void 0) {
|
|
414
|
+
if (!httpUrl(item.url)) return bad(`${where}.url must be an http or https URL`);
|
|
415
|
+
entry.url = item.url;
|
|
416
|
+
}
|
|
417
|
+
if (item.value !== void 0) {
|
|
418
|
+
if (!finite(item.value) || item.value < 0) return bad(`${where}.value must be zero or a positive number`);
|
|
419
|
+
entry.value = item.value;
|
|
420
|
+
}
|
|
421
|
+
items.push(entry);
|
|
422
|
+
}
|
|
423
|
+
const panel = {
|
|
424
|
+
kind: "timeline",
|
|
425
|
+
lanes,
|
|
426
|
+
items
|
|
427
|
+
};
|
|
428
|
+
if (raw.links !== void 0) {
|
|
429
|
+
if (!Array.isArray(raw.links) || raw.links.length > MAX_LINKS) return bad(`${at}.links must be up to ${MAX_LINKS} entries`);
|
|
430
|
+
panel.links = [];
|
|
431
|
+
for (const [index, link] of raw.links.entries()) {
|
|
432
|
+
const where = `${at}.links[${index}]`;
|
|
433
|
+
if (!isRecord(link) || typeof link.from !== "string" || typeof link.to !== "string") return bad(`${where} must be {from, to}`);
|
|
434
|
+
const key = stray(link, LINK_FIELDS);
|
|
435
|
+
if (key) return bad(`${where}.${key} is not a field of a timeline link`);
|
|
436
|
+
for (const end of ["from", "to"]) if (!itemIds.has(link[end])) return bad(`${where}.${end} is not an item id`);
|
|
437
|
+
const entry = {
|
|
438
|
+
from: link.from,
|
|
439
|
+
to: link.to
|
|
440
|
+
};
|
|
441
|
+
if (link.kind !== void 0) {
|
|
442
|
+
if (typeof link.kind !== "string" || !LINK_KINDS.has(link.kind)) return bad(`${where}.kind must be contradicts, follows or same`);
|
|
443
|
+
entry.kind = link.kind;
|
|
444
|
+
}
|
|
445
|
+
panel.links.push(entry);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const edge of ["from", "to"]) {
|
|
449
|
+
if (raw[edge] === void 0) continue;
|
|
450
|
+
if (!instant(raw[edge])) return bad(`${at}.${edge} must be an ISO 8601 date-time, like 2026-09-01T08:00:00Z`);
|
|
451
|
+
panel[edge] = raw[edge];
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
value: panel
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/** A knowledge graph, checked against itself: every edge and the focus must land on a real node. */
|
|
459
|
+
function parseGraph3d(raw, at) {
|
|
460
|
+
if (!Array.isArray(raw.nodes) || raw.nodes.length === 0 || raw.nodes.length > MAX_NODES) return bad(`${at}.nodes must be 1 to ${MAX_NODES} entries`);
|
|
461
|
+
const nodes = [];
|
|
462
|
+
const ids = new Set();
|
|
463
|
+
for (const [index, node] of raw.nodes.entries()) {
|
|
464
|
+
const where = `${at}.nodes[${index}]`;
|
|
465
|
+
if (!isRecord(node) || !text(node.id, 64) || typeof node.name !== "string") return bad(`${where} must be {id, name}`);
|
|
466
|
+
const key = stray(node, NODE_FIELDS);
|
|
467
|
+
if (key) return bad(`${where}.${key} is not a field of a graph3d node`);
|
|
468
|
+
if (!text(node.name, MAX_NODE_NAME)) return bad(tooLong(`${where}.name`, MAX_NODE_NAME, node.name));
|
|
469
|
+
if (ids.has(node.id)) return bad(`${where}.id is a duplicate node id`);
|
|
470
|
+
ids.add(node.id);
|
|
471
|
+
const entry = {
|
|
472
|
+
id: node.id,
|
|
473
|
+
name: node.name
|
|
474
|
+
};
|
|
475
|
+
if (node.type !== void 0) {
|
|
476
|
+
if (!text(node.type, MAX_TYPE_NAME)) return bad(tooLong(`${where}.type`, MAX_TYPE_NAME, node.type));
|
|
477
|
+
entry.type = node.type;
|
|
478
|
+
}
|
|
479
|
+
if (node.value !== void 0) {
|
|
480
|
+
if (!finite(node.value) || node.value < 0) return bad(`${where}.value must be zero or a positive number`);
|
|
481
|
+
entry.value = node.value;
|
|
482
|
+
}
|
|
483
|
+
nodes.push(entry);
|
|
484
|
+
}
|
|
485
|
+
if (!Array.isArray(raw.edges) || raw.edges.length > MAX_EDGES) return bad(`${at}.edges must be up to ${MAX_EDGES} entries`);
|
|
486
|
+
const edges = [];
|
|
487
|
+
for (const [index, edge] of raw.edges.entries()) {
|
|
488
|
+
const where = `${at}.edges[${index}]`;
|
|
489
|
+
if (!isRecord(edge) || typeof edge.from !== "string" || typeof edge.to !== "string") return bad(`${where} must be {from, to}`);
|
|
490
|
+
const key = stray(edge, EDGE_FIELDS);
|
|
491
|
+
if (key) return bad(`${where}.${key} is not a field of a graph3d edge`);
|
|
492
|
+
for (const end of ["from", "to"]) if (!ids.has(edge[end])) return bad(`${where}.${end} is not a node id`);
|
|
493
|
+
const entry = {
|
|
494
|
+
from: edge.from,
|
|
495
|
+
to: edge.to
|
|
496
|
+
};
|
|
497
|
+
if (edge.type !== void 0) {
|
|
498
|
+
if (!text(edge.type, MAX_TYPE_NAME)) return bad(tooLong(`${where}.type`, MAX_TYPE_NAME, edge.type));
|
|
499
|
+
entry.type = edge.type;
|
|
500
|
+
}
|
|
501
|
+
edges.push(entry);
|
|
502
|
+
}
|
|
503
|
+
const panel = {
|
|
504
|
+
kind: "graph3d",
|
|
505
|
+
nodes,
|
|
506
|
+
edges
|
|
507
|
+
};
|
|
508
|
+
if (raw.types !== void 0) {
|
|
509
|
+
if (!isRecord(raw.types) || Object.keys(raw.types).length > MAX_TYPES) return bad(`${at}.types must be up to ${MAX_TYPES} entries`);
|
|
510
|
+
const types = {};
|
|
511
|
+
for (const [name, colour] of Object.entries(raw.types)) {
|
|
512
|
+
if (!text(name, MAX_TYPE_NAME)) return bad(tooLong(`${at}.types key`, MAX_TYPE_NAME, name));
|
|
513
|
+
if (typeof colour !== "string" || !HEX.test(colour)) return bad(`${at}.types.${name} must be a hex colour like #22d3ee`);
|
|
514
|
+
types[name] = colour;
|
|
515
|
+
}
|
|
516
|
+
panel.types = types;
|
|
517
|
+
}
|
|
518
|
+
if (raw.focus !== void 0) {
|
|
519
|
+
if (typeof raw.focus !== "string" || !ids.has(raw.focus)) return bad(`${at}.focus is not a node id`);
|
|
520
|
+
panel.focus = raw.focus;
|
|
521
|
+
}
|
|
522
|
+
if (raw.mode !== void 0 && raw.mode !== "orbit" && raw.mode !== "flat") return bad(`${at}.mode must be orbit or flat`);
|
|
523
|
+
panel.mode = raw.mode ?? "orbit";
|
|
524
|
+
if (raw.rotate !== void 0) {
|
|
525
|
+
if (typeof raw.rotate !== "boolean") return bad(`${at}.rotate must be true or false`);
|
|
526
|
+
panel.rotate = raw.rotate;
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
value: panel
|
|
531
|
+
};
|
|
532
|
+
}
|
|
267
533
|
function parsePanel(raw, index, columns) {
|
|
268
534
|
const at = `panels[${index}]`;
|
|
269
535
|
if (!isRecord(raw)) return bad(`${at} must be an object`);
|
|
@@ -298,6 +564,12 @@ function parsePanel(raw, index, columns) {
|
|
|
298
564
|
case "globe":
|
|
299
565
|
parsed = parseGlobe(raw, at);
|
|
300
566
|
break;
|
|
567
|
+
case "timeline":
|
|
568
|
+
parsed = parseTimeline(raw, at);
|
|
569
|
+
break;
|
|
570
|
+
case "graph3d":
|
|
571
|
+
parsed = parseGraph3d(raw, at);
|
|
572
|
+
break;
|
|
301
573
|
}
|
|
302
574
|
if (!parsed.ok) return parsed;
|
|
303
575
|
const panel = parsed.value;
|
|
@@ -418,6 +690,8 @@ const ZH = `数据大屏(围栏代码块):\`\`\`bigscreen 开头,块内
|
|
|
418
690
|
- "chart":option(必填)——完整的 ECharts option,颜色、字体、动画由大屏统一
|
|
419
691
|
- "chart3d":type "bar3D" | "scatter3D" | "surface" | "line3D"、data([[x, y, z], ...])、xAxis / yAxis(类目名列表,给了的话 x、y 就是下标)、rotate(默认 true)
|
|
420
692
|
- "globe":arcs([{"from": [经度, 纬度], "to": [经度, 纬度], "label"}])、points([{"coord": [经度, 纬度], "label", "value"}])、rotate(默认 true)。至少给一个。适合洲际、跨国的流向;同一个省内的线路在地球上只有几个像素,那种用普通图表
|
|
693
|
+
- "timeline":一条泳道一个来源,一个点一条说法,说法之间可以连线。lanes(必填,1–24 条,[{"id", "name"(最多 40 字符), "color"(十六进制)}],从上到下就是你写的顺序);items(必填,1–500 条,[{"id", "lane"(必须是上面某个 lane 的 id), "at"(ISO 8601,如 "2026-09-01T08:00:00Z"), "label"(最多 120 字符), "detail"(最多 400 字符,只在悬停时出现), "url"(只能是 http/https,点击打开), "value"(点的大小)}]);links(最多 500 条,[{"from", "to", "kind"}],from 和 to 必须是上面写了 id 的 item,kind 是 "contradicts"(红线,两句话不可能同时为真)| "follows"(默认,带箭头)| "same"(点线,同一件事));from、to(时间窗口,默认取所有 item 的范围再各留 5%)。lane 或 item 的 id 引用不到就整个块作废
|
|
694
|
+
- "graph3d":实体和带类型的边组成的知识图谱。nodes(必填,1–2000 个,[{"id", "name"(最多 80 字符), "type"(最多 32 字符,同名的 type 在整面墙上颜色一致), "value"(大小,默认按度数)}]);edges(必填,最多 5000 条,[{"from", "to", "type"(最多 32 字符)}],两端必须是上面的 node id);types(最多 32 项,把某个 type 指定成某个十六进制颜色);focus(某个 node id,会高亮并且永远带标签);mode("orbit" 默认 | "flat");rotate(默认 true)。mode "orbit" 是真正的三维模型:实体在空间里由力导向布局散开,镜头绕着它转,rotate false 就直接给最终模型且不转;mode "flat" 是原来的 GPU 平面布局,一两千个节点以上、三维反而看不清时才用它。度数最高的 20 个带标签,其余悬停可见,右下角有类型图例
|
|
421
695
|
|
|
422
696
|
布局建议:第一行放 3–4 个 kpi(各 span 3),然后大图 span 8 配一个 rank 或 gauge span 4,3D 和地球 span 6。
|
|
423
697
|
|
|
@@ -440,6 +714,70 @@ const ZH = `数据大屏(围栏代码块):\`\`\`bigscreen 开头,块内
|
|
|
440
714
|
}
|
|
441
715
|
\`\`\`
|
|
442
716
|
|
|
717
|
+
例子——一条时间线,三家媒体,四条说法,其中两条互相矛盾:
|
|
718
|
+
|
|
719
|
+
\`\`\`bigscreen
|
|
720
|
+
{
|
|
721
|
+
"title": "边境车队",
|
|
722
|
+
"panels": [
|
|
723
|
+
{
|
|
724
|
+
"kind": "timeline",
|
|
725
|
+
"title": "各家说法",
|
|
726
|
+
"span": 12,
|
|
727
|
+
"lanes": [
|
|
728
|
+
{ "id": "reuters", "name": "路透社" },
|
|
729
|
+
{ "id": "tass", "name": "塔斯社" },
|
|
730
|
+
{ "id": "ap", "name": "美联社" }
|
|
731
|
+
],
|
|
732
|
+
"items": [
|
|
733
|
+
{ "id": "c1", "lane": "reuters", "at": "2026-09-01T08:12:00Z", "label": "车队于清晨越境", "detail": "两名现场消息人士", "url": "https://example.com/reuters-1", "value": 3 },
|
|
734
|
+
{ "id": "c2", "lane": "tass", "at": "2026-09-01T09:30:00Z", "label": "没有任何车队越境" },
|
|
735
|
+
{ "id": "c3", "lane": "ap", "at": "2026-09-01T11:05:00Z", "label": "卫星影像确认越境" },
|
|
736
|
+
{ "id": "c4", "lane": "reuters", "at": "2026-09-01T14:40:00Z", "label": "第二支车队" }
|
|
737
|
+
],
|
|
738
|
+
"links": [
|
|
739
|
+
{ "from": "c1", "to": "c2", "kind": "contradicts" },
|
|
740
|
+
{ "from": "c1", "to": "c4", "kind": "follows" },
|
|
741
|
+
{ "from": "c1", "to": "c3", "kind": "same" }
|
|
742
|
+
]
|
|
743
|
+
}
|
|
744
|
+
]
|
|
745
|
+
}
|
|
746
|
+
\`\`\`
|
|
747
|
+
|
|
748
|
+
例子——一张知识图谱,六个实体三种类型,七条边:
|
|
749
|
+
|
|
750
|
+
\`\`\`bigscreen
|
|
751
|
+
{
|
|
752
|
+
"title": "实体关系",
|
|
753
|
+
"panels": [
|
|
754
|
+
{
|
|
755
|
+
"kind": "graph3d",
|
|
756
|
+
"title": "谁报道了什么",
|
|
757
|
+
"span": 12,
|
|
758
|
+
"focus": "convoy",
|
|
759
|
+
"nodes": [
|
|
760
|
+
{ "id": "kyiv", "name": "基辅", "type": "place" },
|
|
761
|
+
{ "id": "moscow", "name": "莫斯科", "type": "place" },
|
|
762
|
+
{ "id": "reuters", "name": "路透社", "type": "outlet" },
|
|
763
|
+
{ "id": "tass", "name": "塔斯社", "type": "outlet" },
|
|
764
|
+
{ "id": "convoy", "name": "车队越境", "type": "event" },
|
|
765
|
+
{ "id": "denial", "name": "否认越境", "type": "event" }
|
|
766
|
+
],
|
|
767
|
+
"edges": [
|
|
768
|
+
{ "from": "reuters", "to": "convoy", "type": "reported" },
|
|
769
|
+
{ "from": "tass", "to": "denial", "type": "reported" },
|
|
770
|
+
{ "from": "convoy", "to": "kyiv", "type": "located" },
|
|
771
|
+
{ "from": "denial", "to": "moscow", "type": "located" },
|
|
772
|
+
{ "from": "convoy", "to": "denial", "type": "contradicts" },
|
|
773
|
+
{ "from": "reuters", "to": "kyiv", "type": "located" },
|
|
774
|
+
{ "from": "tass", "to": "moscow", "type": "located" }
|
|
775
|
+
]
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
}
|
|
779
|
+
\`\`\`
|
|
780
|
+
|
|
443
781
|
这一版画不了的(遇到就退回普通图表或文字):实时刷新、可交互筛选、表格、地图着色(省份热力图)、视频。需要宿主提供真实数据的 BI 看板用 dashboard 块,不用这个。`;
|
|
444
782
|
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.
|
|
445
783
|
|
|
@@ -451,6 +789,10 @@ Top level: title (at most 80 characters), subtitle (at most 120), theme ("dark"
|
|
|
451
789
|
|
|
452
790
|
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).
|
|
453
791
|
|
|
792
|
+
"timeline" — one swim-lane per source, one point per claim, lines between claims. lanes (required, 1 to 24, as {id, name at most 40 characters, color as hex}; drawn top-down in the order you write them); items (required, 1 to 500, as {id, lane — an id from lanes, at — ISO 8601 like "2026-09-01T08:00:00Z", label at most 120 characters, detail at most 400 and shown only on hover, url — http or https only, opened on click, value — how big the point is}); links (at most 500, as {from, to, kind}, where from and to are ids of items you gave an id to, and kind is "contradicts" (a red line: the two cannot both be true), "follows" (the default, with an arrow) or "same" (dotted, one thing said twice)); from and to for the window, which otherwise takes the claims' own range with 5% either side. A lane id or an item id that resolves to nothing voids the whole block.
|
|
793
|
+
|
|
794
|
+
"graph3d" — entities and typed edges as a knowledge graph. nodes (required, 1 to 2000, as {id, name at most 80 characters, type at most 32 characters — one type is one colour everywhere on the wall, value — how big, defaulting to its degree}); edges (required, at most 5000, as {from, to, type at most 32 characters}, both ends being node ids); types (at most 32 entries, pinning a type to a hex colour); focus (a node id, highlighted and always labelled); mode ("orbit", the default, or "flat"); rotate (default true). In "orbit" the graph is a real three-dimensional model: the entities settle into space in front of the reader and the camera turns around them, and rotate false hands over the settled model without turning it. "flat" is the older GPU layout on a plane — reach for it only when a graph is big enough that depth hides more than it shows. The twenty busiest entities carry a label, the rest are one hover away, and the types are keyed in the corner.
|
|
795
|
+
|
|
454
796
|
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.
|
|
455
797
|
|
|
456
798
|
Example:
|
|
@@ -469,6 +811,70 @@ Example:
|
|
|
469
811
|
}
|
|
470
812
|
\`\`\`
|
|
471
813
|
|
|
814
|
+
A timeline — three outlets, four claims, one of which contradicts another:
|
|
815
|
+
|
|
816
|
+
\`\`\`bigscreen
|
|
817
|
+
{
|
|
818
|
+
"title": "Border convoy",
|
|
819
|
+
"panels": [
|
|
820
|
+
{
|
|
821
|
+
"kind": "timeline",
|
|
822
|
+
"title": "Who said what",
|
|
823
|
+
"span": 12,
|
|
824
|
+
"lanes": [
|
|
825
|
+
{ "id": "reuters", "name": "Reuters" },
|
|
826
|
+
{ "id": "tass", "name": "TASS" },
|
|
827
|
+
{ "id": "ap", "name": "AP" }
|
|
828
|
+
],
|
|
829
|
+
"items": [
|
|
830
|
+
{ "id": "c1", "lane": "reuters", "at": "2026-09-01T08:12:00Z", "label": "Convoy crossed at dawn", "detail": "Two sources on the ground.", "url": "https://example.com/reuters-1", "value": 3 },
|
|
831
|
+
{ "id": "c2", "lane": "tass", "at": "2026-09-01T09:30:00Z", "label": "No convoy crossed" },
|
|
832
|
+
{ "id": "c3", "lane": "ap", "at": "2026-09-01T11:05:00Z", "label": "Crossing confirmed by satellite" },
|
|
833
|
+
{ "id": "c4", "lane": "reuters", "at": "2026-09-01T14:40:00Z", "label": "A second convoy" }
|
|
834
|
+
],
|
|
835
|
+
"links": [
|
|
836
|
+
{ "from": "c1", "to": "c2", "kind": "contradicts" },
|
|
837
|
+
{ "from": "c1", "to": "c4", "kind": "follows" },
|
|
838
|
+
{ "from": "c1", "to": "c3", "kind": "same" }
|
|
839
|
+
]
|
|
840
|
+
}
|
|
841
|
+
]
|
|
842
|
+
}
|
|
843
|
+
\`\`\`
|
|
844
|
+
|
|
845
|
+
A knowledge graph — six entities of three types, seven edges:
|
|
846
|
+
|
|
847
|
+
\`\`\`bigscreen
|
|
848
|
+
{
|
|
849
|
+
"title": "Entities",
|
|
850
|
+
"panels": [
|
|
851
|
+
{
|
|
852
|
+
"kind": "graph3d",
|
|
853
|
+
"title": "Who reported what",
|
|
854
|
+
"span": 12,
|
|
855
|
+
"focus": "convoy",
|
|
856
|
+
"nodes": [
|
|
857
|
+
{ "id": "kyiv", "name": "Kyiv", "type": "place" },
|
|
858
|
+
{ "id": "moscow", "name": "Moscow", "type": "place" },
|
|
859
|
+
{ "id": "reuters", "name": "Reuters", "type": "outlet" },
|
|
860
|
+
{ "id": "tass", "name": "TASS", "type": "outlet" },
|
|
861
|
+
{ "id": "convoy", "name": "Convoy crossing", "type": "event" },
|
|
862
|
+
{ "id": "denial", "name": "Denial of crossing", "type": "event" }
|
|
863
|
+
],
|
|
864
|
+
"edges": [
|
|
865
|
+
{ "from": "reuters", "to": "convoy", "type": "reported" },
|
|
866
|
+
{ "from": "tass", "to": "denial", "type": "reported" },
|
|
867
|
+
{ "from": "convoy", "to": "kyiv", "type": "located" },
|
|
868
|
+
{ "from": "denial", "to": "moscow", "type": "located" },
|
|
869
|
+
{ "from": "convoy", "to": "denial", "type": "contradicts" },
|
|
870
|
+
{ "from": "reuters", "to": "kyiv", "type": "located" },
|
|
871
|
+
{ "from": "tass", "to": "moscow", "type": "located" }
|
|
872
|
+
]
|
|
873
|
+
}
|
|
874
|
+
]
|
|
875
|
+
}
|
|
876
|
+
\`\`\`
|
|
877
|
+
|
|
472
878
|
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.`;
|
|
473
879
|
const PROMPT = {
|
|
474
880
|
en: { spec: EN },
|
|
@@ -488,7 +894,7 @@ const bigscreenCss = [
|
|
|
488
894
|
".aigui-bs-subtitle{margin:.25rem 0 0;font-size:.85rem;color:var(--aigui-bs-muted);letter-spacing:.12em}",
|
|
489
895
|
".aigui-bs-rule{height:1px;margin:.8rem auto 0;width:70%}",
|
|
490
896
|
".aigui-bs-grid{display:grid;gap:.9rem}",
|
|
491
|
-
".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)}",
|
|
897
|
+
".aigui-bs-panel{position:relative;display:flex;flex-direction:column;min-width:0;border:1px solid;border-radius:10px;padding:.7rem .85rem .85rem;backdrop-filter:blur(6px)}",
|
|
492
898
|
"[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)}",
|
|
493
899
|
"[data-aigui-bigscreen='light'] .aigui-bs-panel{background:rgba(255,255,255,.9);box-shadow:0 6px 20px rgba(15,23,42,.06)}",
|
|
494
900
|
".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)}",
|
|
@@ -508,6 +914,10 @@ const bigscreenCss = [
|
|
|
508
914
|
".aigui-bs-rank-track{display:block;height:8px;border-radius:4px;overflow:hidden}",
|
|
509
915
|
".aigui-bs-rank-fill{display:block;height:100%;border-radius:4px;transition:width 1.2s cubic-bezier(.22,1,.36,1)}",
|
|
510
916
|
".aigui-bs-rank-value{color:var(--aigui-bs-muted);font-variant-numeric:tabular-nums}",
|
|
917
|
+
".aigui-bs-graph-legend{position:absolute;right:.6rem;bottom:.6rem;z-index:2;display:flex;flex-direction:column;gap:.15rem;max-width:48%;padding:.3rem .45rem;border-radius:6px;background:color-mix(in srgb,currentColor 8%,transparent);font-size:.68rem;line-height:1.5;letter-spacing:.02em;color:var(--aigui-bs-muted);pointer-events:none}",
|
|
918
|
+
".aigui-bs-graph-legend-row{display:flex;align-items:center;gap:.35rem;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
919
|
+
".aigui-bs-graph-legend-dot{flex:none;width:8px;height:8px;border-radius:50%}",
|
|
920
|
+
".aigui-bs-graph-legend-line{flex:none;width:12px;height:2px;border-radius:1px}",
|
|
511
921
|
".aigui-bs-note{display:flex;align-items:center;justify-content:center;height:100%;min-height:4rem;font-size:.85rem;color:var(--aigui-bs-muted)}",
|
|
512
922
|
"[data-aigui-bigscreen-loading]{min-height:8rem;border-radius:14px;background:currentColor;opacity:.06}",
|
|
513
923
|
":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)}",
|
|
@@ -549,9 +959,9 @@ function bigscreen(options = {}) {
|
|
|
549
959
|
mount: (el) => {
|
|
550
960
|
let destroy;
|
|
551
961
|
let disposed = false;
|
|
552
|
-
import("./mount-
|
|
962
|
+
import("./mount-ChcoxgHd.js").then(({ mountScreen }) => {
|
|
553
963
|
if (disposed) return;
|
|
554
|
-
destroy = mountScreen(el, definition, options.animate !== false);
|
|
964
|
+
destroy = mountScreen(el, definition, options.animate !== false, options.globe, options.events);
|
|
555
965
|
}).catch(() => {
|
|
556
966
|
const error = document.createElement("div");
|
|
557
967
|
error.setAttribute("data-aigui-bigscreen-error", "");
|
|
@@ -586,4 +996,4 @@ function bigscreen(options = {}) {
|
|
|
586
996
|
}
|
|
587
997
|
|
|
588
998
|
//#endregion
|
|
589
|
-
export { bigscreen, bigscreenCss, bigscreenPromptSpec, chart3dOption, chartOption, formatNumber, gaugeColour, gaugeOption, globeOption, palette, parseBigscreen, withAlpha };
|
|
999
|
+
export { EDGE_VERTICES, GRAPH_LABELS, GRAPH_LEGEND_ROWS, GRAPH_MAX_STEPS, GRAPH_SETTLE_STEPS, LAYOUT_GRAVITY, LAYOUT_SPRING, MAX_ARCS, MAX_EDGES, MAX_ITEMS, MAX_ITEM_DETAIL, MAX_ITEM_LABEL, MAX_LANES, MAX_LANE_NAME, MAX_LINKS, MAX_NODES, MAX_NODE_NAME, MAX_POINTS, MAX_TIMELINE_ITEMS, MAX_TYPES, MAX_TYPE_NAME, MAX_URL, ORBIT_BOX, ORBIT_DISTANCE, ORBIT_EXTENT, ORBIT_FOV, REMEMBERED_GRAPHS, TIMELINE_LABELS, bigscreen, bigscreenCss, bigscreenPromptSpec, chart3dOption, chartOption, countriesTexture, createLayout, degrees, earthColours, earthTexture, forgetPositions, formatNumber, gaugeColour, gaugeOption, globeOption, globeTexture, graph3dOption, graphKey, graphLegend, graphOrbitData, graphOrbitOption, graphTooltip, hash, layoutChunk, layoutRadius, layoutSteps, orbitFrame, palette, parseBigscreen, recallPositions, rememberPositions, seedPositions, spacedItems, timelineHeight, timelineOption, timelineWindow, typeColour, withAlpha };
|