@webskill/sdk 0.3.0 → 0.5.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/dist/agent.d.ts +2 -0
- package/dist/agent.js +867 -0
- package/dist/browser.d.ts +137 -4
- package/dist/browser.js +458 -22
- package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-DV7cPpUm-C77AEEx9.js} +477 -157
- package/dist/{dist-rorEJsNi.js → dist-6C03DShK.js} +654 -298
- package/dist/{dist-ZKaM8j06.js → dist-bewtXYlO.js} +1061 -807
- package/dist/governance.d.ts +87 -10
- package/dist/governance.js +194 -24
- package/dist/{index-wiV5X8Rz.d.ts → index-Bsqg4ftU.d.ts} +151 -143
- package/dist/index-D_7ZZjkl.d.ts +411 -0
- package/dist/{index-8d-oEDww.d.ts → index-vBz_FC9w.d.ts} +289 -24
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
- package/dist/node.d.ts +8 -4
- package/dist/node.js +1 -1
- package/dist/{openUiLibrary-B8-Cvou9-BbpNTXS3.js → openUiLibrary-W3Ce896k-ClFTRZFs.js} +6 -5
- package/dist/{skillVersionStore-DOEI9ptb-BxbYL70B.d.ts → skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts} +43 -11
- package/dist/{testing-CsrG3XLz.js → testing-DDCJWvgA.js} +7 -5
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/{types-AmKCKJn_-VGabeXK4.d.ts → types-D_hoCri8-BnNPiZCi.d.ts} +111 -72
- package/dist/ui-react.d.ts +352 -20
- package/dist/ui-react.js +3804 -3478
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +25 -6
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-CNaUpasU-BslMcxRZ.js → webskillLitCatalog-_mugzRHx-DiuJpCuf.js} +398 -122
- package/package.json +6 -1
- package/dist/jsonRenderRegistry-9GrWP_hE-U6Do3Kid.js +0 -2468
- package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +0 -48
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { m as WebSkillError } from "./dist-8oQRa8Xz.js";
|
|
2
2
|
import { toJSONSchema, z } from "zod";
|
|
3
3
|
|
|
4
|
-
//#region ../ui/dist/webskillCatalog-
|
|
4
|
+
//#region ../ui/dist/webskillCatalog-B-IQxOKs.js
|
|
5
5
|
/**
|
|
6
6
|
* 最小子集 markdown → DOM:标题(#..###)、无序列表、代码块(```)、加粗、链接。
|
|
7
7
|
* 全部内容经 textContent 写入,用户内容天然转义防 HTML 注入。
|
|
@@ -84,214 +84,112 @@ function renderMiniMarkdown(text, doc) {
|
|
|
84
84
|
}
|
|
85
85
|
return root;
|
|
86
86
|
}
|
|
87
|
+
const KINDS = /* @__PURE__ */ new Set([
|
|
88
|
+
"bar",
|
|
89
|
+
"line",
|
|
90
|
+
"pie"
|
|
91
|
+
]);
|
|
92
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
87
93
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
94
|
+
* catalog `Chart` 节点 props → `ChartSpec`。四档共用这一份转换:
|
|
95
|
+
* 此前 native / OpenUI / json-render / A2UI 各抄了一遍,改一处就会漂移。
|
|
96
|
+
* catalog 的 `area` 在 `ChartSpec` 里没有对应枚举,按折线渲染——
|
|
97
|
+
* 原先的裸 cast 会把非法枚举一路带进渲染器,画出空图。
|
|
90
98
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
];
|
|
103
|
-
const WIDTH = 320;
|
|
104
|
-
const HEIGHT = 200;
|
|
105
|
-
function el(doc, name, attrs) {
|
|
106
|
-
const node = doc.createElementNS(SVG_NS, name);
|
|
107
|
-
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
108
|
-
return node;
|
|
109
|
-
}
|
|
110
|
-
function text$1(doc, x, y, content, attrs = {}) {
|
|
111
|
-
const node = el(doc, "text", {
|
|
112
|
-
x: String(x),
|
|
113
|
-
y: String(y),
|
|
114
|
-
"font-size": "9",
|
|
115
|
-
fill: "#555",
|
|
116
|
-
...attrs
|
|
117
|
-
});
|
|
118
|
-
node.textContent = content;
|
|
119
|
-
return node;
|
|
120
|
-
}
|
|
121
|
-
const fmt = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
122
|
-
function maxValue(chart) {
|
|
123
|
-
let max = 0;
|
|
124
|
-
for (const s of chart.series) for (const v of s.data) if (v > max) max = v;
|
|
125
|
-
return max;
|
|
126
|
-
}
|
|
127
|
-
function renderBar(chart, svg, doc) {
|
|
128
|
-
const plot = {
|
|
129
|
-
x0: 30,
|
|
130
|
-
y0: 24,
|
|
131
|
-
x1: 312,
|
|
132
|
-
y1: 164
|
|
99
|
+
function chartSpecFromProps(props) {
|
|
100
|
+
const type = typeof props["type"] === "string" ? props["type"] : "bar";
|
|
101
|
+
const series = list(props["series"]);
|
|
102
|
+
return {
|
|
103
|
+
kind: KINDS.has(type) ? type : "line",
|
|
104
|
+
labels: list(props["labels"]),
|
|
105
|
+
series: series.map((item) => ({
|
|
106
|
+
...typeof item.name === "string" ? { name: item.name } : {},
|
|
107
|
+
data: list(item.values)
|
|
108
|
+
})),
|
|
109
|
+
...typeof props["title"] === "string" ? { title: props["title"] } : {}
|
|
133
110
|
};
|
|
134
|
-
const max = maxValue(chart);
|
|
135
|
-
svg.appendChild(el(doc, "line", {
|
|
136
|
-
x1: String(plot.x0),
|
|
137
|
-
y1: String(plot.y0),
|
|
138
|
-
x2: String(plot.x0),
|
|
139
|
-
y2: String(plot.y1),
|
|
140
|
-
stroke: "#999"
|
|
141
|
-
}));
|
|
142
|
-
svg.appendChild(el(doc, "line", {
|
|
143
|
-
x1: String(plot.x0),
|
|
144
|
-
y1: String(plot.y1),
|
|
145
|
-
x2: String(plot.x1),
|
|
146
|
-
y2: String(plot.y1),
|
|
147
|
-
stroke: "#999"
|
|
148
|
-
}));
|
|
149
|
-
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
150
|
-
const groupWidth = (plot.x1 - plot.x0) / chart.labels.length;
|
|
151
|
-
const barWidth = Math.max(1, groupWidth / chart.series.length * .7);
|
|
152
|
-
chart.series.forEach((series, si) => {
|
|
153
|
-
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
154
|
-
series.data.forEach((value, li) => {
|
|
155
|
-
if (li >= chart.labels.length) return;
|
|
156
|
-
const height = (plot.y1 - plot.y0) * value / max;
|
|
157
|
-
const x = plot.x0 + li * groupWidth + (groupWidth - barWidth * chart.series.length) / 2 + si * barWidth;
|
|
158
|
-
const y = plot.y1 - height;
|
|
159
|
-
svg.appendChild(el(doc, "rect", {
|
|
160
|
-
x: String(x),
|
|
161
|
-
y: String(y),
|
|
162
|
-
width: String(barWidth),
|
|
163
|
-
height: String(height),
|
|
164
|
-
fill: color,
|
|
165
|
-
"data-series": series.name ?? String(si)
|
|
166
|
-
}));
|
|
167
|
-
if (chart.series.length === 1) svg.appendChild(text$1(doc, x + barWidth / 2, y - 2, fmt(value), { "text-anchor": "middle" }));
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
chart.labels.forEach((label, li) => {
|
|
171
|
-
svg.appendChild(text$1(doc, plot.x0 + li * groupWidth + groupWidth / 2, plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
172
|
-
});
|
|
173
111
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
112
|
+
const seriesNames = (chart) => chart.series.map((series) => series.name ?? "");
|
|
113
|
+
/** echarts 的图例条目来源:饼图按标签分项,其余按系列名 */
|
|
114
|
+
const legendEntries = (chart) => chart.kind === "pie" ? chart.labels : seriesNames(chart);
|
|
115
|
+
/** `ChartSpec` → ECharts option:图表类型、数据系列、图例的唯一定义处 */
|
|
116
|
+
function toEchartsOption(chart) {
|
|
117
|
+
return {
|
|
118
|
+
animation: false,
|
|
119
|
+
title: chart.title ? {
|
|
120
|
+
text: chart.title,
|
|
121
|
+
left: "center",
|
|
122
|
+
textStyle: { fontSize: 14 }
|
|
123
|
+
} : void 0,
|
|
124
|
+
tooltip: { trigger: chart.kind === "pie" ? "item" : "axis" },
|
|
125
|
+
legend: { bottom: 0 },
|
|
126
|
+
xAxis: chart.kind === "pie" ? void 0 : {
|
|
127
|
+
type: "category",
|
|
128
|
+
data: chart.labels
|
|
129
|
+
},
|
|
130
|
+
yAxis: chart.kind === "pie" ? void 0 : { type: "value" },
|
|
131
|
+
series: chart.kind === "pie" ? chart.series.map((series) => ({
|
|
132
|
+
type: "pie",
|
|
133
|
+
name: series.name,
|
|
134
|
+
data: chart.labels.map((label, index) => ({
|
|
135
|
+
name: label,
|
|
136
|
+
value: series.data[index] ?? 0
|
|
137
|
+
}))
|
|
138
|
+
})) : chart.series.map((series) => ({
|
|
139
|
+
type: chart.kind,
|
|
140
|
+
name: series.name,
|
|
141
|
+
data: series.data
|
|
142
|
+
}))
|
|
180
143
|
};
|
|
181
|
-
const max = maxValue(chart);
|
|
182
|
-
svg.appendChild(el(doc, "line", {
|
|
183
|
-
x1: String(plot.x0),
|
|
184
|
-
y1: String(plot.y0),
|
|
185
|
-
x2: String(plot.x0),
|
|
186
|
-
y2: String(plot.y1),
|
|
187
|
-
stroke: "#999"
|
|
188
|
-
}));
|
|
189
|
-
svg.appendChild(el(doc, "line", {
|
|
190
|
-
x1: String(plot.x0),
|
|
191
|
-
y1: String(plot.y1),
|
|
192
|
-
x2: String(plot.x1),
|
|
193
|
-
y2: String(plot.y1),
|
|
194
|
-
stroke: "#999"
|
|
195
|
-
}));
|
|
196
|
-
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
197
|
-
const step = chart.labels.length > 1 ? (plot.x1 - plot.x0) / (chart.labels.length - 1) : 0;
|
|
198
|
-
const px = (li) => chart.labels.length > 1 ? plot.x0 + li * step : (plot.x0 + plot.x1) / 2;
|
|
199
|
-
const py = (v) => plot.y1 - (plot.y1 - plot.y0) * v / max;
|
|
200
|
-
chart.series.forEach((series, si) => {
|
|
201
|
-
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
202
|
-
const points = series.data.slice(0, chart.labels.length).map((v, li) => `${px(li)},${py(v)}`).join(" ");
|
|
203
|
-
if (points !== "") svg.appendChild(el(doc, "polyline", {
|
|
204
|
-
points,
|
|
205
|
-
fill: "none",
|
|
206
|
-
stroke: color,
|
|
207
|
-
"stroke-width": "1.5"
|
|
208
|
-
}));
|
|
209
|
-
series.data.slice(0, chart.labels.length).forEach((v, li) => {
|
|
210
|
-
svg.appendChild(el(doc, "circle", {
|
|
211
|
-
cx: String(px(li)),
|
|
212
|
-
cy: String(py(v)),
|
|
213
|
-
r: "2",
|
|
214
|
-
fill: color
|
|
215
|
-
}));
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
chart.labels.forEach((label, li) => {
|
|
219
|
-
svg.appendChild(text$1(doc, px(li), plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
220
|
-
});
|
|
221
144
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let angle = -Math.PI / 2;
|
|
233
|
-
data.forEach((value, i) => {
|
|
234
|
-
const label = chart.labels[i] ?? String(i);
|
|
235
|
-
const color = CHART_PALETTE[i % CHART_PALETTE.length];
|
|
236
|
-
const slice = Math.max(value, 0) / total * Math.PI * 2;
|
|
237
|
-
const x0 = cx + r * Math.cos(angle);
|
|
238
|
-
const y0 = cy + r * Math.sin(angle);
|
|
239
|
-
const x1 = cx + r * Math.cos(angle + slice);
|
|
240
|
-
const y1 = cy + r * Math.sin(angle + slice);
|
|
241
|
-
const path = el(doc, "path", {
|
|
242
|
-
d: `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${slice > Math.PI ? "1" : "0"} 1 ${x1} ${y1} Z`,
|
|
243
|
-
fill: color,
|
|
244
|
-
"data-label": label
|
|
245
|
-
});
|
|
246
|
-
svg.appendChild(path);
|
|
247
|
-
angle += slice;
|
|
248
|
-
const ly = 40 + i * 14;
|
|
249
|
-
svg.appendChild(el(doc, "rect", {
|
|
250
|
-
x: "190",
|
|
251
|
-
y: String(ly - 7),
|
|
252
|
-
width: "8",
|
|
253
|
-
height: "8",
|
|
254
|
-
fill: color
|
|
255
|
-
}));
|
|
256
|
-
svg.appendChild(text$1(doc, 202, ly, `${label} (${fmt(value)})`));
|
|
257
|
-
});
|
|
145
|
+
/**
|
|
146
|
+
* 图表的 DOM 契约。echarts 画在 canvas 上,DOM 里读不到图表类型/系列/图例,
|
|
147
|
+
* 「同一份 ChartSpec 在各档下一致」只能靠宿主元素上的这三个属性对拍。
|
|
148
|
+
*/
|
|
149
|
+
function applyChartDomContract(container, chart) {
|
|
150
|
+
container.setAttribute("data-webskill-chart", chart.kind);
|
|
151
|
+
container.setAttribute("data-webskill-chart-series", seriesNames(chart).join("|"));
|
|
152
|
+
container.setAttribute("data-webskill-chart-legend", legendEntries(chart).join("|"));
|
|
153
|
+
if (chart.title) container.setAttribute("aria-label", chart.title);
|
|
154
|
+
else container.removeAttribute("aria-label");
|
|
258
155
|
}
|
|
259
156
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
157
|
+
* 框架无关的 echarts 挂载点:三档的 React 组件与 A2UI 档的 Lit 元素共用同一实现,
|
|
158
|
+
* 图表不再因档位而异。echarts 走动态 import,不打进首屏。
|
|
262
159
|
*/
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
160
|
+
function mountEchart(container) {
|
|
161
|
+
let disposed = false;
|
|
162
|
+
let instance;
|
|
163
|
+
let queued;
|
|
164
|
+
let observer;
|
|
165
|
+
const apply = (chart) => {
|
|
166
|
+
instance?.setOption(toEchartsOption(chart), {
|
|
167
|
+
notMerge: true,
|
|
168
|
+
lazyUpdate: true
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
import("./echarts-DhNm2ene.js").then((echarts) => {
|
|
172
|
+
if (disposed) return;
|
|
173
|
+
instance = echarts.init(container);
|
|
174
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
175
|
+
observer = new ResizeObserver(() => instance?.resize());
|
|
176
|
+
observer.observe(container);
|
|
177
|
+
}
|
|
178
|
+
if (queued) apply(queued);
|
|
270
179
|
});
|
|
271
|
-
if (chart.title) svg.appendChild(text$1(doc, WIDTH / 2, 14, chart.title, {
|
|
272
|
-
"text-anchor": "middle",
|
|
273
|
-
"font-size": "11",
|
|
274
|
-
fill: "#333"
|
|
275
|
-
}));
|
|
276
|
-
switch (chart.kind) {
|
|
277
|
-
case "bar":
|
|
278
|
-
renderBar(chart, svg, doc);
|
|
279
|
-
break;
|
|
280
|
-
case "line":
|
|
281
|
-
renderLine(chart, svg, doc);
|
|
282
|
-
break;
|
|
283
|
-
case "pie":
|
|
284
|
-
renderPie(chart, svg, doc);
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
return svg;
|
|
288
|
-
}
|
|
289
|
-
/** A2UI/OpenUI 降级:chart → table(labels 为首列,series 各为一列) */
|
|
290
|
-
function chartToTable(chart) {
|
|
291
180
|
return {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
181
|
+
setChart(chart) {
|
|
182
|
+
applyChartDomContract(container, chart);
|
|
183
|
+
queued = chart;
|
|
184
|
+
apply(chart);
|
|
185
|
+
},
|
|
186
|
+
dispose() {
|
|
187
|
+
disposed = true;
|
|
188
|
+
observer?.disconnect();
|
|
189
|
+
observer = void 0;
|
|
190
|
+
instance?.dispose();
|
|
191
|
+
instance = void 0;
|
|
192
|
+
}
|
|
295
193
|
};
|
|
296
194
|
}
|
|
297
195
|
function componentSchema(def) {
|
|
@@ -407,6 +305,7 @@ function validateNode(catalog, byName, node, path, issues) {
|
|
|
407
305
|
return;
|
|
408
306
|
}
|
|
409
307
|
const allowed = def.children;
|
|
308
|
+
const singletons = /* @__PURE__ */ new Map();
|
|
410
309
|
children.forEach((child, index) => {
|
|
411
310
|
const childPath = `${path}.children[${index}]`;
|
|
412
311
|
if (allowed !== "any" && isRecord$1(child) && typeof child["component"] === "string") {
|
|
@@ -418,6 +317,17 @@ function validateNode(catalog, byName, node, path, issues) {
|
|
|
418
317
|
return;
|
|
419
318
|
}
|
|
420
319
|
}
|
|
320
|
+
if (isRecord$1(child) && typeof child["component"] === "string") {
|
|
321
|
+
const childName = child["component"];
|
|
322
|
+
if (byName.get(childName)?.singletonPerContainer === true) {
|
|
323
|
+
const seen = (singletons.get(childName) ?? 0) + 1;
|
|
324
|
+
singletons.set(childName, seen);
|
|
325
|
+
if (seen > 1) issues.push({
|
|
326
|
+
path: childPath,
|
|
327
|
+
message: `At most one "${childName}" per container; "${name}" already contains one`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
421
331
|
validateNode(catalog, byName, child, childPath, issues);
|
|
422
332
|
});
|
|
423
333
|
}
|
|
@@ -481,6 +391,7 @@ const DATA = [
|
|
|
481
391
|
"Metric",
|
|
482
392
|
"Table",
|
|
483
393
|
"Chart",
|
|
394
|
+
"Timeline",
|
|
484
395
|
"FileLink"
|
|
485
396
|
];
|
|
486
397
|
const INPUT = [
|
|
@@ -488,8 +399,10 @@ const INPUT = [
|
|
|
488
399
|
"Field",
|
|
489
400
|
"Button"
|
|
490
401
|
];
|
|
402
|
+
/** Tabs 的面板与 Grid 的格子必须自成容器,否则「每容器至多一个表单」就没有落脚点 */
|
|
403
|
+
const PANEL = ["Card", "Stack"];
|
|
491
404
|
/**
|
|
492
|
-
* v3 首发 catalog
|
|
405
|
+
* v3 首发 catalog:覆盖旧六类 surface(metric / chart / table / form / file / custom)的全部表达力,先窄后宽。
|
|
493
406
|
* 只依赖 zod —— 不 import 任何 UI framework(21 号文档 §5.1)。
|
|
494
407
|
*/
|
|
495
408
|
const uiCatalog = defineUiCatalog({
|
|
@@ -503,8 +416,13 @@ const uiCatalog = defineUiCatalog({
|
|
|
503
416
|
"- Ask for user input with `Form` + `Field`; never describe an input in `Text`.",
|
|
504
417
|
"- A `Button`'s `action` must be one of the catalog actions.",
|
|
505
418
|
"- Never emit HTML, scripts or remote images.",
|
|
506
|
-
"- Prefer `Table` for many rows, `Chart` for trends, `Metric` for a single number
|
|
507
|
-
"
|
|
419
|
+
"- Prefer `Table` for many rows, `Chart` for trends, `Metric` for a single number,",
|
|
420
|
+
" `Timeline` for ordered steps and `Progress` for how far a long task has come.",
|
|
421
|
+
"- Match `Field.type` to the question instead of using `text` for everything:",
|
|
422
|
+
" `select` / `multi-select` for a fixed set of options, `date` for dates,",
|
|
423
|
+
" `toggle` for yes/no, `number` for quantities, `textarea` for several sentences.",
|
|
424
|
+
"- Group independent sections with `Tabs` or `Grid` instead of one long column.",
|
|
425
|
+
"- At most one `Form` per container; a `Tabs` panel and a `Grid` cell are separate containers."
|
|
508
426
|
].join("\n"),
|
|
509
427
|
components: [
|
|
510
428
|
{
|
|
@@ -566,6 +484,67 @@ const uiCatalog = defineUiCatalog({
|
|
|
566
484
|
props: z.object({}),
|
|
567
485
|
example: { component: "Separator" }
|
|
568
486
|
},
|
|
487
|
+
{
|
|
488
|
+
name: "Tabs",
|
|
489
|
+
group: "layout",
|
|
490
|
+
description: "Switchable panels. Each child is one panel and its own form container.",
|
|
491
|
+
props: z.object({ labels: z.array(z.string()).min(2) }),
|
|
492
|
+
children: PANEL,
|
|
493
|
+
constraints: ["One child per label, in the same order"],
|
|
494
|
+
example: {
|
|
495
|
+
component: "Tabs",
|
|
496
|
+
props: { labels: ["Filters", "Export"] },
|
|
497
|
+
children: [{
|
|
498
|
+
component: "Stack",
|
|
499
|
+
children: [{
|
|
500
|
+
component: "Text",
|
|
501
|
+
props: { text: "Pick a date range." }
|
|
502
|
+
}]
|
|
503
|
+
}, {
|
|
504
|
+
component: "Stack",
|
|
505
|
+
children: [{
|
|
506
|
+
component: "Text",
|
|
507
|
+
props: { text: "Choose a file format." }
|
|
508
|
+
}]
|
|
509
|
+
}]
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "Grid",
|
|
514
|
+
group: "layout",
|
|
515
|
+
description: "Dashboard grid. Each child is one cell and its own form container.",
|
|
516
|
+
props: z.object({ columns: z.union([
|
|
517
|
+
z.literal(2),
|
|
518
|
+
z.literal(3),
|
|
519
|
+
z.literal(4)
|
|
520
|
+
]).default(2) }),
|
|
521
|
+
children: PANEL,
|
|
522
|
+
example: {
|
|
523
|
+
component: "Grid",
|
|
524
|
+
props: { columns: 2 },
|
|
525
|
+
children: [{
|
|
526
|
+
component: "Card",
|
|
527
|
+
props: { title: "Revenue" },
|
|
528
|
+
children: [{
|
|
529
|
+
component: "Metric",
|
|
530
|
+
props: {
|
|
531
|
+
label: "MRR",
|
|
532
|
+
value: "12,400"
|
|
533
|
+
}
|
|
534
|
+
}]
|
|
535
|
+
}, {
|
|
536
|
+
component: "Card",
|
|
537
|
+
props: { title: "Churn" },
|
|
538
|
+
children: [{
|
|
539
|
+
component: "Metric",
|
|
540
|
+
props: {
|
|
541
|
+
label: "Rate",
|
|
542
|
+
value: "2.1%"
|
|
543
|
+
}
|
|
544
|
+
}]
|
|
545
|
+
}]
|
|
546
|
+
}
|
|
547
|
+
},
|
|
569
548
|
{
|
|
570
549
|
name: "Heading",
|
|
571
550
|
group: "content",
|
|
@@ -701,6 +680,33 @@ const uiCatalog = defineUiCatalog({
|
|
|
701
680
|
}
|
|
702
681
|
}
|
|
703
682
|
},
|
|
683
|
+
{
|
|
684
|
+
name: "Timeline",
|
|
685
|
+
group: "data",
|
|
686
|
+
description: "Ordered steps or events, earliest first.",
|
|
687
|
+
props: z.object({ items: z.array(z.object({
|
|
688
|
+
title: z.string(),
|
|
689
|
+
time: z.string().optional(),
|
|
690
|
+
description: z.string().optional(),
|
|
691
|
+
state: z.enum([
|
|
692
|
+
"done",
|
|
693
|
+
"active",
|
|
694
|
+
"pending"
|
|
695
|
+
]).default("pending")
|
|
696
|
+
})).min(1) }),
|
|
697
|
+
example: {
|
|
698
|
+
component: "Timeline",
|
|
699
|
+
props: { items: [{
|
|
700
|
+
title: "Plan",
|
|
701
|
+
time: "Mon",
|
|
702
|
+
state: "done"
|
|
703
|
+
}, {
|
|
704
|
+
title: "Build",
|
|
705
|
+
time: "Tue",
|
|
706
|
+
state: "active"
|
|
707
|
+
}] }
|
|
708
|
+
}
|
|
709
|
+
},
|
|
704
710
|
{
|
|
705
711
|
name: "FileLink",
|
|
706
712
|
group: "data",
|
|
@@ -708,13 +714,18 @@ const uiCatalog = defineUiCatalog({
|
|
|
708
714
|
props: z.object({
|
|
709
715
|
path: z.string(),
|
|
710
716
|
label: z.string().optional(),
|
|
711
|
-
mimeType: z.string().optional()
|
|
717
|
+
mimeType: z.string().optional(),
|
|
718
|
+
/** 字节数;人类可读格式化是渲染层的事,不进 props */
|
|
719
|
+
size: z.number().int().nonnegative().optional(),
|
|
720
|
+
actions: z.array(actionRef).optional()
|
|
712
721
|
}),
|
|
722
|
+
constraints: ["size is a byte count, not a formatted string"],
|
|
713
723
|
example: {
|
|
714
724
|
component: "FileLink",
|
|
715
725
|
props: {
|
|
716
726
|
path: "report.csv",
|
|
717
|
-
label: "Download report"
|
|
727
|
+
label: "Download report",
|
|
728
|
+
size: 2411724
|
|
718
729
|
}
|
|
719
730
|
}
|
|
720
731
|
},
|
|
@@ -728,7 +739,8 @@ const uiCatalog = defineUiCatalog({
|
|
|
728
739
|
cancelLabel: z.string().optional()
|
|
729
740
|
}),
|
|
730
741
|
children: ["Field"],
|
|
731
|
-
|
|
742
|
+
singletonPerContainer: true,
|
|
743
|
+
constraints: ["At most one Form per container", "Every Field name must be unique inside the Form"],
|
|
732
744
|
example: {
|
|
733
745
|
component: "Form",
|
|
734
746
|
props: { title: "Schedule report" },
|
|
@@ -792,6 +804,27 @@ const uiCatalog = defineUiCatalog({
|
|
|
792
804
|
}
|
|
793
805
|
}
|
|
794
806
|
},
|
|
807
|
+
{
|
|
808
|
+
name: "Progress",
|
|
809
|
+
group: "feedback",
|
|
810
|
+
description: "How far a long-running task has come, in percent.",
|
|
811
|
+
props: z.object({
|
|
812
|
+
label: z.string().optional(),
|
|
813
|
+
value: z.number().min(0).max(100),
|
|
814
|
+
tone: z.enum([
|
|
815
|
+
"neutral",
|
|
816
|
+
"success",
|
|
817
|
+
"warning"
|
|
818
|
+
]).default("neutral")
|
|
819
|
+
}),
|
|
820
|
+
example: {
|
|
821
|
+
component: "Progress",
|
|
822
|
+
props: {
|
|
823
|
+
label: "Indexing",
|
|
824
|
+
value: 42
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
},
|
|
795
828
|
{
|
|
796
829
|
name: "Alert",
|
|
797
830
|
group: "feedback",
|
|
@@ -860,6 +893,12 @@ const UI_CATALOG_GROUPS = {
|
|
|
860
893
|
data: DATA,
|
|
861
894
|
input: INPUT
|
|
862
895
|
};
|
|
896
|
+
/**
|
|
897
|
+
* catalog 系统提示的体积上限(UTF-8 字节,FR-6.6)。
|
|
898
|
+
* 描述每次请求都要重发,所以上限必须是可判定的数字而不是「尽量精简」。
|
|
899
|
+
* 要改这个数字,先想清楚是否值得让每次请求都多付这些 token。
|
|
900
|
+
*/
|
|
901
|
+
const UI_CATALOG_PROMPT_BUDGET_BYTES = 20480;
|
|
863
902
|
/** A2UI 协议公共类型(与 @a2ui/web_core 的 common-types 同一套定义) */
|
|
864
903
|
const A2UI_COMMON_TYPES = "https://a2ui.org/specification/v0_9/common_types.json";
|
|
865
904
|
const CHILD_LIST_REF = `${A2UI_COMMON_TYPES}#/$defs/ChildList`;
|
|
@@ -993,6 +1032,7 @@ function interactionToFormModel(request) {
|
|
|
993
1032
|
...f.required ? { required: true } : {},
|
|
994
1033
|
...f.description ? { description: f.description } : {},
|
|
995
1034
|
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
1035
|
+
...f.suggestion ? { suggestion: f.suggestion } : {},
|
|
996
1036
|
...f.options ? { options: f.options } : {}
|
|
997
1037
|
})),
|
|
998
1038
|
submitLabel: "Submit",
|
|
@@ -1067,16 +1107,247 @@ function collectValues(controls, container) {
|
|
|
1067
1107
|
missingRequired
|
|
1068
1108
|
};
|
|
1069
1109
|
}
|
|
1070
|
-
/**
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1110
|
+
/**
|
|
1111
|
+
* 采纳建议值(FR-5.9):把 `control.suggestion` 写进已渲染的控件。
|
|
1112
|
+
* 与 `collectValues` 共用同一套选择器与编码约定——两边分开实现的话,
|
|
1113
|
+
* select 的 JSON 编码值迟早会对不上。
|
|
1114
|
+
* @experimental
|
|
1115
|
+
*/
|
|
1116
|
+
function applySuggestion(control, container) {
|
|
1117
|
+
if (control.suggestion === void 0) return;
|
|
1118
|
+
const el = container.querySelector(controlSelector(control.name));
|
|
1119
|
+
if (el === null) return;
|
|
1120
|
+
const { value } = control.suggestion;
|
|
1121
|
+
if (control.control === "boolean") {
|
|
1122
|
+
el.checked = value === true;
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (control.control === "select") {
|
|
1126
|
+
el.value = JSON.stringify(value);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
el.value = value === void 0 || value === null ? "" : String(value);
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* 轻量 SVG 图表自绘(bar/line/pie,零依赖,三端复用单一来源)。
|
|
1133
|
+
* 全部经 doc.createElementNS 构建(内容来自技能输出,不拼 innerHTML)。
|
|
1134
|
+
*/
|
|
1135
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
1136
|
+
/** 固定 8 色循环调色板 */
|
|
1137
|
+
const CHART_PALETTE = [
|
|
1138
|
+
"#4e79a7",
|
|
1139
|
+
"#f28e2b",
|
|
1140
|
+
"#e15759",
|
|
1141
|
+
"#76b7b2",
|
|
1142
|
+
"#59a14f",
|
|
1143
|
+
"#edc948",
|
|
1144
|
+
"#b07aa1",
|
|
1145
|
+
"#ff9da7"
|
|
1146
|
+
];
|
|
1147
|
+
const WIDTH = 320;
|
|
1148
|
+
const HEIGHT = 200;
|
|
1149
|
+
function el(doc, name, attrs) {
|
|
1150
|
+
const node = doc.createElementNS(SVG_NS, name);
|
|
1151
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
1152
|
+
return node;
|
|
1153
|
+
}
|
|
1154
|
+
function text(doc, x, y, content, attrs = {}) {
|
|
1155
|
+
const node = el(doc, "text", {
|
|
1156
|
+
x: String(x),
|
|
1157
|
+
y: String(y),
|
|
1158
|
+
"font-size": "9",
|
|
1159
|
+
fill: "#555",
|
|
1160
|
+
...attrs
|
|
1161
|
+
});
|
|
1162
|
+
node.textContent = content;
|
|
1163
|
+
return node;
|
|
1164
|
+
}
|
|
1165
|
+
const fmt = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
1166
|
+
function maxValue(chart) {
|
|
1167
|
+
let max = 0;
|
|
1168
|
+
for (const s of chart.series) for (const v of s.data) if (v > max) max = v;
|
|
1169
|
+
return max;
|
|
1170
|
+
}
|
|
1171
|
+
function renderBar(chart, svg, doc) {
|
|
1172
|
+
const plot = {
|
|
1173
|
+
x0: 30,
|
|
1174
|
+
y0: 24,
|
|
1175
|
+
x1: 312,
|
|
1176
|
+
y1: 164
|
|
1177
|
+
};
|
|
1178
|
+
const max = maxValue(chart);
|
|
1179
|
+
svg.appendChild(el(doc, "line", {
|
|
1180
|
+
x1: String(plot.x0),
|
|
1181
|
+
y1: String(plot.y0),
|
|
1182
|
+
x2: String(plot.x0),
|
|
1183
|
+
y2: String(plot.y1),
|
|
1184
|
+
stroke: "#999"
|
|
1185
|
+
}));
|
|
1186
|
+
svg.appendChild(el(doc, "line", {
|
|
1187
|
+
x1: String(plot.x0),
|
|
1188
|
+
y1: String(plot.y1),
|
|
1189
|
+
x2: String(plot.x1),
|
|
1190
|
+
y2: String(plot.y1),
|
|
1191
|
+
stroke: "#999"
|
|
1192
|
+
}));
|
|
1193
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
1194
|
+
const groupWidth = (plot.x1 - plot.x0) / chart.labels.length;
|
|
1195
|
+
const barWidth = Math.max(1, groupWidth / chart.series.length * .7);
|
|
1196
|
+
chart.series.forEach((series, si) => {
|
|
1197
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
1198
|
+
series.data.forEach((value, li) => {
|
|
1199
|
+
if (li >= chart.labels.length) return;
|
|
1200
|
+
const height = (plot.y1 - plot.y0) * value / max;
|
|
1201
|
+
const x = plot.x0 + li * groupWidth + (groupWidth - barWidth * chart.series.length) / 2 + si * barWidth;
|
|
1202
|
+
const y = plot.y1 - height;
|
|
1203
|
+
svg.appendChild(el(doc, "rect", {
|
|
1204
|
+
x: String(x),
|
|
1205
|
+
y: String(y),
|
|
1206
|
+
width: String(barWidth),
|
|
1207
|
+
height: String(height),
|
|
1208
|
+
fill: color,
|
|
1209
|
+
"data-series": series.name ?? String(si)
|
|
1210
|
+
}));
|
|
1211
|
+
if (chart.series.length === 1) svg.appendChild(text(doc, x + barWidth / 2, y - 2, fmt(value), { "text-anchor": "middle" }));
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
chart.labels.forEach((label, li) => {
|
|
1215
|
+
svg.appendChild(text(doc, plot.x0 + li * groupWidth + groupWidth / 2, plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
function renderLine(chart, svg, doc) {
|
|
1219
|
+
const plot = {
|
|
1220
|
+
x0: 30,
|
|
1221
|
+
y0: 24,
|
|
1222
|
+
x1: 312,
|
|
1223
|
+
y1: 164
|
|
1224
|
+
};
|
|
1225
|
+
const max = maxValue(chart);
|
|
1226
|
+
svg.appendChild(el(doc, "line", {
|
|
1227
|
+
x1: String(plot.x0),
|
|
1228
|
+
y1: String(plot.y0),
|
|
1229
|
+
x2: String(plot.x0),
|
|
1230
|
+
y2: String(plot.y1),
|
|
1231
|
+
stroke: "#999"
|
|
1232
|
+
}));
|
|
1233
|
+
svg.appendChild(el(doc, "line", {
|
|
1234
|
+
x1: String(plot.x0),
|
|
1235
|
+
y1: String(plot.y1),
|
|
1236
|
+
x2: String(plot.x1),
|
|
1237
|
+
y2: String(plot.y1),
|
|
1238
|
+
stroke: "#999"
|
|
1239
|
+
}));
|
|
1240
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
1241
|
+
const step = chart.labels.length > 1 ? (plot.x1 - plot.x0) / (chart.labels.length - 1) : 0;
|
|
1242
|
+
const px = (li) => chart.labels.length > 1 ? plot.x0 + li * step : (plot.x0 + plot.x1) / 2;
|
|
1243
|
+
const py = (v) => plot.y1 - (plot.y1 - plot.y0) * v / max;
|
|
1244
|
+
chart.series.forEach((series, si) => {
|
|
1245
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
1246
|
+
const points = series.data.slice(0, chart.labels.length).map((v, li) => `${px(li)},${py(v)}`).join(" ");
|
|
1247
|
+
if (points !== "") svg.appendChild(el(doc, "polyline", {
|
|
1248
|
+
points,
|
|
1249
|
+
fill: "none",
|
|
1250
|
+
stroke: color,
|
|
1251
|
+
"stroke-width": "1.5"
|
|
1252
|
+
}));
|
|
1253
|
+
series.data.slice(0, chart.labels.length).forEach((v, li) => {
|
|
1254
|
+
svg.appendChild(el(doc, "circle", {
|
|
1255
|
+
cx: String(px(li)),
|
|
1256
|
+
cy: String(py(v)),
|
|
1257
|
+
r: "2",
|
|
1258
|
+
fill: color
|
|
1259
|
+
}));
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
chart.labels.forEach((label, li) => {
|
|
1263
|
+
svg.appendChild(text(doc, px(li), plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
function renderPie(chart, svg, doc) {
|
|
1267
|
+
const data = (chart.series[0]?.data ?? []).slice(0, chart.labels.length);
|
|
1268
|
+
const total = data.reduce((sum, v) => sum + (v > 0 ? v : 0), 0);
|
|
1269
|
+
const cx = 90;
|
|
1270
|
+
const cy = 104;
|
|
1271
|
+
const r = 72;
|
|
1272
|
+
if (total <= 0) {
|
|
1273
|
+
svg.appendChild(text(doc, cx, cy, "No data", { "text-anchor": "middle" }));
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
let angle = -Math.PI / 2;
|
|
1277
|
+
data.forEach((value, i) => {
|
|
1278
|
+
const label = chart.labels[i] ?? String(i);
|
|
1279
|
+
const color = CHART_PALETTE[i % CHART_PALETTE.length];
|
|
1280
|
+
const slice = Math.max(value, 0) / total * Math.PI * 2;
|
|
1281
|
+
const x0 = cx + r * Math.cos(angle);
|
|
1282
|
+
const y0 = cy + r * Math.sin(angle);
|
|
1283
|
+
const x1 = cx + r * Math.cos(angle + slice);
|
|
1284
|
+
const y1 = cy + r * Math.sin(angle + slice);
|
|
1285
|
+
const path = el(doc, "path", {
|
|
1286
|
+
d: `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${slice > Math.PI ? "1" : "0"} 1 ${x1} ${y1} Z`,
|
|
1287
|
+
fill: color,
|
|
1288
|
+
"data-label": label
|
|
1289
|
+
});
|
|
1290
|
+
svg.appendChild(path);
|
|
1291
|
+
angle += slice;
|
|
1292
|
+
const ly = 40 + i * 14;
|
|
1293
|
+
svg.appendChild(el(doc, "rect", {
|
|
1294
|
+
x: "190",
|
|
1295
|
+
y: String(ly - 7),
|
|
1296
|
+
width: "8",
|
|
1297
|
+
height: "8",
|
|
1298
|
+
fill: color
|
|
1299
|
+
}));
|
|
1300
|
+
svg.appendChild(text(doc, 202, ly, `${label} (${fmt(value)})`));
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* ChartSpec → SVG(bar 等宽柱 + 值标签;line 折线 + 点;pie 扇形 + 图例;空数据容错)
|
|
1305
|
+
* @stable
|
|
1306
|
+
*/
|
|
1307
|
+
function renderMiniChart(chart, doc) {
|
|
1308
|
+
const svg = el(doc, "svg", {
|
|
1309
|
+
viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
|
|
1310
|
+
width: String(WIDTH),
|
|
1311
|
+
height: String(HEIGHT),
|
|
1312
|
+
class: `webskill-chart webskill-chart--${chart.kind}`,
|
|
1313
|
+
role: "img"
|
|
1314
|
+
});
|
|
1315
|
+
if (chart.title) svg.appendChild(text(doc, WIDTH / 2, 14, chart.title, {
|
|
1316
|
+
"text-anchor": "middle",
|
|
1317
|
+
"font-size": "11",
|
|
1318
|
+
fill: "#333"
|
|
1319
|
+
}));
|
|
1320
|
+
switch (chart.kind) {
|
|
1321
|
+
case "bar":
|
|
1322
|
+
renderBar(chart, svg, doc);
|
|
1323
|
+
break;
|
|
1324
|
+
case "line":
|
|
1325
|
+
renderLine(chart, svg, doc);
|
|
1326
|
+
break;
|
|
1327
|
+
case "pie":
|
|
1328
|
+
renderPie(chart, svg, doc);
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
return svg;
|
|
1332
|
+
}
|
|
1333
|
+
/** A2UI/OpenUI 降级:chart → table(labels 为首列,series 各为一列) */
|
|
1334
|
+
function chartToTable(chart) {
|
|
1335
|
+
return {
|
|
1336
|
+
type: "table",
|
|
1337
|
+
columns: ["label", ...chart.series.map((s, i) => s.name ?? `series ${i + 1}`)],
|
|
1338
|
+
rows: chart.labels.map((label, li) => [label, ...chart.series.map((s) => s.data[li] ?? null)])
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
/** RenderBlock → DOM(markdown/json/table/image/file/chart 六种) */
|
|
1342
|
+
function renderBlocks(container, blocks, doc) {
|
|
1343
|
+
for (const block of blocks) switch (block.type) {
|
|
1344
|
+
case "chart":
|
|
1345
|
+
container.appendChild(renderMiniChart(block.chart, doc));
|
|
1346
|
+
break;
|
|
1347
|
+
case "markdown":
|
|
1348
|
+
container.appendChild(renderMiniMarkdown(block.text, doc));
|
|
1349
|
+
break;
|
|
1350
|
+
case "json": {
|
|
1080
1351
|
const pre = doc.createElement("pre");
|
|
1081
1352
|
pre.className = "webskill-result__json";
|
|
1082
1353
|
pre.textContent = JSON.stringify(block.data, null, 2);
|
|
@@ -1157,6 +1428,8 @@ const WEBSKILL_STYLES_CSS = `
|
|
|
1157
1428
|
.webskill-form__error { font-size: 12px; color: #d33; display: none; }
|
|
1158
1429
|
.webskill-form__control--invalid .webskill-form__error { display: block; }
|
|
1159
1430
|
.webskill-form__input { padding: 6px 8px; border: 1px solid #bbb; border-radius: 4px; font-size: 14px; font-family: inherit; }
|
|
1431
|
+
.webskill-form__suggestion { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #666; }
|
|
1432
|
+
.webskill-form__suggestion-use { padding: 1px 8px; border-radius: 4px; border: 1px solid #bbb; background: #fff; cursor: pointer; font-size: 12px; }
|
|
1160
1433
|
.webskill-form__actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
1161
1434
|
.webskill-form__button { padding: 6px 14px; border-radius: 4px; border: 1px solid #bbb; background: #fff; cursor: pointer; font-size: 14px; }
|
|
1162
1435
|
.webskill-form__button--primary { background: #2563eb; border-color: #2563eb; color: #fff; }
|
|
@@ -1350,6 +1623,7 @@ var WebFormBridge = class {
|
|
|
1350
1623
|
input.id = controlId;
|
|
1351
1624
|
input.setAttribute("aria-describedby", errorId);
|
|
1352
1625
|
wrapper.appendChild(input);
|
|
1626
|
+
if (control.suggestion !== void 0) wrapper.appendChild(this.#renderSuggestion(control, wrapper));
|
|
1353
1627
|
const error = doc.createElement("div");
|
|
1354
1628
|
error.className = "webskill-form__error";
|
|
1355
1629
|
error.id = errorId;
|
|
@@ -1358,65 +1632,371 @@ var WebFormBridge = class {
|
|
|
1358
1632
|
wrapper.appendChild(error);
|
|
1359
1633
|
return wrapper;
|
|
1360
1634
|
}
|
|
1635
|
+
/** 历史值以**建议**形态出现(FR-5.9):控件初始值仍为空,点「使用」才写入 */
|
|
1636
|
+
#renderSuggestion(control, container) {
|
|
1637
|
+
const doc = this.#doc;
|
|
1638
|
+
const row = doc.createElement("div");
|
|
1639
|
+
row.className = "webskill-form__suggestion";
|
|
1640
|
+
row.setAttribute("data-webskill-suggestion", control.name);
|
|
1641
|
+
const preview = doc.createElement("span");
|
|
1642
|
+
preview.className = "webskill-form__suggestion-value";
|
|
1643
|
+
preview.textContent = `Last entered: ${String(control.suggestion?.value)}`;
|
|
1644
|
+
row.appendChild(preview);
|
|
1645
|
+
const use = doc.createElement("button");
|
|
1646
|
+
use.type = "button";
|
|
1647
|
+
use.className = "webskill-form__suggestion-use";
|
|
1648
|
+
use.textContent = "Use";
|
|
1649
|
+
use.setAttribute("data-webskill-suggestion-use", control.name);
|
|
1650
|
+
use.addEventListener("click", () => applySuggestion(control, container));
|
|
1651
|
+
row.appendChild(use);
|
|
1652
|
+
return row;
|
|
1653
|
+
}
|
|
1361
1654
|
};
|
|
1655
|
+
function fieldNamesOf(form) {
|
|
1656
|
+
const names = [];
|
|
1657
|
+
const walk = (node) => {
|
|
1658
|
+
const name = node.props?.["name"];
|
|
1659
|
+
if (node.component === "Field" && typeof name === "string") names.push(name);
|
|
1660
|
+
for (const child of node.children ?? []) walk(child);
|
|
1661
|
+
};
|
|
1662
|
+
walk(form);
|
|
1663
|
+
return names;
|
|
1664
|
+
}
|
|
1362
1665
|
/**
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1666
|
+
* 按出现序列出声明树里的每个 `Form`。
|
|
1667
|
+
*
|
|
1668
|
+
* 单表单时 action id 保持 `submit` / `cancel`,回传形状与容器级放宽前逐字节一致——
|
|
1669
|
+
* 向后兼容是 FR-6.3 的硬约束,不能让既有单表单卡片跟着改。
|
|
1670
|
+
* 多表单时每个表单拿到独立的 action id 与 `scopeId`,渲染层据此只收集本表单子树内的字段。
|
|
1366
1671
|
*/
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
if (typeof node.props?.["cancelLabel"] === "string") out.add("cancel");
|
|
1373
|
-
}
|
|
1374
|
-
for (const child of node.children ?? []) collectActions(child, out);
|
|
1375
|
-
}
|
|
1376
|
-
const INTENTS = /* @__PURE__ */ new Set([
|
|
1377
|
-
"submit",
|
|
1378
|
-
"cancel",
|
|
1379
|
-
"select",
|
|
1380
|
-
"download",
|
|
1381
|
-
"refresh"
|
|
1382
|
-
]);
|
|
1383
|
-
/** 声明树 → `UiSurface`。id 缺省时由调用方给(工具源用 runtime 生成的 id)。 */
|
|
1384
|
-
function toSurface(catalog, spec, id) {
|
|
1385
|
-
const names = /* @__PURE__ */ new Set();
|
|
1386
|
-
collectActions(spec, names);
|
|
1387
|
-
const actions = [...names].filter((name) => INTENTS.has(name)).map((intent) => ({
|
|
1388
|
-
id: intent,
|
|
1389
|
-
label: catalog.actions.find((action) => action.name === intent)?.name ?? intent,
|
|
1390
|
-
intent,
|
|
1391
|
-
...intent === "submit" ? { awaitResponse: true } : {}
|
|
1392
|
-
}));
|
|
1393
|
-
const props = {
|
|
1394
|
-
catalogId: catalog.id,
|
|
1395
|
-
catalogVersion: catalog.version,
|
|
1396
|
-
spec,
|
|
1397
|
-
actions: [...names]
|
|
1398
|
-
};
|
|
1399
|
-
return {
|
|
1400
|
-
kind: "custom",
|
|
1401
|
-
id,
|
|
1402
|
-
component: UI_SPEC_COMPONENT,
|
|
1403
|
-
props,
|
|
1404
|
-
...actions.length > 0 ? { actions } : {}
|
|
1672
|
+
function collectFormScopes(spec) {
|
|
1673
|
+
const forms = [];
|
|
1674
|
+
const walk = (node) => {
|
|
1675
|
+
if (node.component === "Form") forms.push(node);
|
|
1676
|
+
for (const child of node.children ?? []) walk(child);
|
|
1405
1677
|
};
|
|
1678
|
+
walk(spec);
|
|
1679
|
+
if (forms.length === 1) {
|
|
1680
|
+
const form = forms[0];
|
|
1681
|
+
return [{
|
|
1682
|
+
scopeId: form.id ?? "form",
|
|
1683
|
+
submitActionId: "submit",
|
|
1684
|
+
cancelActionId: "cancel",
|
|
1685
|
+
fieldNames: fieldNamesOf(form),
|
|
1686
|
+
form
|
|
1687
|
+
}];
|
|
1688
|
+
}
|
|
1689
|
+
return forms.map((form, index) => {
|
|
1690
|
+
const scopeId = form.id ?? `form-${index}`;
|
|
1691
|
+
return {
|
|
1692
|
+
scopeId,
|
|
1693
|
+
submitActionId: `${scopeId}:submit`,
|
|
1694
|
+
cancelActionId: `${scopeId}:cancel`,
|
|
1695
|
+
fieldNames: fieldNamesOf(form),
|
|
1696
|
+
form
|
|
1697
|
+
};
|
|
1698
|
+
});
|
|
1406
1699
|
}
|
|
1407
|
-
/**
|
|
1408
|
-
|
|
1409
|
-
|
|
1700
|
+
/**
|
|
1701
|
+
* 多表单时字段值在渲染层的存储键。
|
|
1702
|
+
*
|
|
1703
|
+
* 跨表单重名过去不可能发生,容器级放宽后它变成合法输入:两个表单各有一个 `email`,
|
|
1704
|
+
* 共用一张扁平表就是后写的覆盖先写的,用户在 A 里输入会串到 B。
|
|
1705
|
+
* 因此多表单时按作用域限定键;单表单不加前缀,存储与草稿形状保持不变。
|
|
1706
|
+
*/
|
|
1707
|
+
function qualifyFieldName(name, scopeId) {
|
|
1708
|
+
return scopeId === void 0 ? name : `${scopeId}.${name}`;
|
|
1410
1709
|
}
|
|
1411
1710
|
/**
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1711
|
+
* 提交时回传的字段值。
|
|
1712
|
+
*
|
|
1713
|
+
* 不给 `scope` → 整张 surface 的扁平值,与容器级放宽前完全一致(FR-6.3 第 3 条)。
|
|
1714
|
+
* 给了 `scope` → 只取该表单子树内的字段,键回落成裸字段名,
|
|
1715
|
+
* 因此接收端看到的载荷形状与单表单时相同。
|
|
1414
1716
|
*/
|
|
1415
|
-
function
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
const
|
|
1419
|
-
const key =
|
|
1717
|
+
function collectScopedValues(values, scope) {
|
|
1718
|
+
if (!scope) return { ...values };
|
|
1719
|
+
const out = {};
|
|
1720
|
+
for (const name of scope.fieldNames) {
|
|
1721
|
+
const key = qualifyFieldName(name, scope.scopeId);
|
|
1722
|
+
if (key in values) out[name] = values[key];
|
|
1723
|
+
else if (name in values) out[name] = values[name];
|
|
1724
|
+
}
|
|
1725
|
+
return out;
|
|
1726
|
+
}
|
|
1727
|
+
/** 五类场景预设(FR-6.4)。@experimental */
|
|
1728
|
+
const UI_PRESETS = [
|
|
1729
|
+
{
|
|
1730
|
+
name: "charts",
|
|
1731
|
+
summary: "One question, one chart, with the takeaway spelled out above it.",
|
|
1732
|
+
guidance: [
|
|
1733
|
+
"Layout: `Card` → `Heading` (the takeaway, not \"Chart\") → `Chart` → optional `Text` for the caveat.",
|
|
1734
|
+
"Pick the chart type from the question: `line` / `area` for time, `bar` for comparison, `pie` for shares.",
|
|
1735
|
+
"Every series needs a name; labels and values must be the same length.",
|
|
1736
|
+
"Never repeat the same numbers in a `Table` right under the chart."
|
|
1737
|
+
].join("\n"),
|
|
1738
|
+
example: {
|
|
1739
|
+
component: "Card",
|
|
1740
|
+
props: { title: "Revenue" },
|
|
1741
|
+
children: [
|
|
1742
|
+
{
|
|
1743
|
+
component: "Heading",
|
|
1744
|
+
props: {
|
|
1745
|
+
level: 3,
|
|
1746
|
+
text: "Revenue grew 27% in Q4"
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
component: "Chart",
|
|
1751
|
+
props: {
|
|
1752
|
+
type: "line",
|
|
1753
|
+
labels: [
|
|
1754
|
+
"Q1",
|
|
1755
|
+
"Q2",
|
|
1756
|
+
"Q3",
|
|
1757
|
+
"Q4"
|
|
1758
|
+
],
|
|
1759
|
+
series: [{
|
|
1760
|
+
name: "Revenue",
|
|
1761
|
+
values: [
|
|
1762
|
+
8,
|
|
1763
|
+
9,
|
|
1764
|
+
9.8,
|
|
1765
|
+
12.4
|
|
1766
|
+
]
|
|
1767
|
+
}]
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
component: "Text",
|
|
1772
|
+
props: {
|
|
1773
|
+
text: "Figures in millions, unaudited.",
|
|
1774
|
+
tone: "muted"
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
]
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
name: "cards",
|
|
1782
|
+
summary: "A short list of comparable items, one `Card` each.",
|
|
1783
|
+
guidance: [
|
|
1784
|
+
"Layout: `Grid` (2 or 3 columns) → one `Card` per item.",
|
|
1785
|
+
"Keep every card the same shape: title, one `Metric` or `Text`, at most one `Badge` for status.",
|
|
1786
|
+
"Use `Grid` rather than a vertical `Stack` as soon as there are three or more items.",
|
|
1787
|
+
"Put an action on a card only when the user can act on that single item."
|
|
1788
|
+
].join("\n"),
|
|
1789
|
+
example: {
|
|
1790
|
+
component: "Grid",
|
|
1791
|
+
props: { columns: 2 },
|
|
1792
|
+
children: [{
|
|
1793
|
+
component: "Card",
|
|
1794
|
+
props: { title: "Checkout" },
|
|
1795
|
+
children: [{
|
|
1796
|
+
component: "Metric",
|
|
1797
|
+
props: {
|
|
1798
|
+
label: "Errors",
|
|
1799
|
+
value: 12,
|
|
1800
|
+
trend: "down"
|
|
1801
|
+
}
|
|
1802
|
+
}, {
|
|
1803
|
+
component: "Badge",
|
|
1804
|
+
props: {
|
|
1805
|
+
text: "healthy",
|
|
1806
|
+
tone: "success"
|
|
1807
|
+
}
|
|
1808
|
+
}]
|
|
1809
|
+
}, {
|
|
1810
|
+
component: "Card",
|
|
1811
|
+
props: { title: "Search" },
|
|
1812
|
+
children: [{
|
|
1813
|
+
component: "Metric",
|
|
1814
|
+
props: {
|
|
1815
|
+
label: "Errors",
|
|
1816
|
+
value: 143,
|
|
1817
|
+
trend: "up"
|
|
1818
|
+
}
|
|
1819
|
+
}, {
|
|
1820
|
+
component: "Badge",
|
|
1821
|
+
props: {
|
|
1822
|
+
text: "degraded",
|
|
1823
|
+
tone: "warning"
|
|
1824
|
+
}
|
|
1825
|
+
}]
|
|
1826
|
+
}]
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
{
|
|
1830
|
+
name: "dashboards",
|
|
1831
|
+
summary: "Headline numbers first, then detail, optionally split across `Tabs`.",
|
|
1832
|
+
guidance: [
|
|
1833
|
+
"Layout: `Stack` → `Grid` of `Metric` cards → `Chart` or `Table` for the detail.",
|
|
1834
|
+
"Use `Tabs` when the detail splits into independent groups (filters, exports, alerts).",
|
|
1835
|
+
"Each `Tabs` panel and each `Grid` cell is its own form container: at most one `Form` inside.",
|
|
1836
|
+
"Show at most four headline metrics; anything more belongs in a `Table`."
|
|
1837
|
+
].join("\n"),
|
|
1838
|
+
example: {
|
|
1839
|
+
component: "Stack",
|
|
1840
|
+
children: [{
|
|
1841
|
+
component: "Grid",
|
|
1842
|
+
props: { columns: 2 },
|
|
1843
|
+
children: [{
|
|
1844
|
+
component: "Card",
|
|
1845
|
+
props: { title: "MRR" },
|
|
1846
|
+
children: [{
|
|
1847
|
+
component: "Metric",
|
|
1848
|
+
props: {
|
|
1849
|
+
label: "This month",
|
|
1850
|
+
value: "12,400",
|
|
1851
|
+
trend: "up"
|
|
1852
|
+
}
|
|
1853
|
+
}]
|
|
1854
|
+
}, {
|
|
1855
|
+
component: "Card",
|
|
1856
|
+
props: { title: "Churn" },
|
|
1857
|
+
children: [{
|
|
1858
|
+
component: "Metric",
|
|
1859
|
+
props: {
|
|
1860
|
+
label: "This month",
|
|
1861
|
+
value: "2.1%",
|
|
1862
|
+
trend: "down"
|
|
1863
|
+
}
|
|
1864
|
+
}]
|
|
1865
|
+
}]
|
|
1866
|
+
}, {
|
|
1867
|
+
component: "Tabs",
|
|
1868
|
+
props: { labels: ["Trend", "Breakdown"] },
|
|
1869
|
+
children: [{
|
|
1870
|
+
component: "Stack",
|
|
1871
|
+
children: [{
|
|
1872
|
+
component: "Chart",
|
|
1873
|
+
props: {
|
|
1874
|
+
type: "bar",
|
|
1875
|
+
labels: ["Q3", "Q4"],
|
|
1876
|
+
series: [{
|
|
1877
|
+
name: "Revenue",
|
|
1878
|
+
values: [9800, 12400]
|
|
1879
|
+
}]
|
|
1880
|
+
}
|
|
1881
|
+
}]
|
|
1882
|
+
}, {
|
|
1883
|
+
component: "Stack",
|
|
1884
|
+
children: [{
|
|
1885
|
+
component: "Table",
|
|
1886
|
+
props: {
|
|
1887
|
+
columns: ["Region", "Revenue"],
|
|
1888
|
+
rows: [["EMEA", 5400], ["AMER", 7e3]]
|
|
1889
|
+
}
|
|
1890
|
+
}]
|
|
1891
|
+
}]
|
|
1892
|
+
}]
|
|
1893
|
+
}
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
name: "slides",
|
|
1897
|
+
summary: "One idea per `Tabs` panel, headline plus at most three supporting lines.",
|
|
1898
|
+
guidance: [
|
|
1899
|
+
"Layout: `Tabs` → one `Stack` per slide, labelled with the slide title.",
|
|
1900
|
+
"Each slide: one `Heading` (level 2) stating the point, then a `Chart`, `Metric` or short `Markdown` list.",
|
|
1901
|
+
"Never put more than one idea on a slide; add a panel instead.",
|
|
1902
|
+
"No `Form` inside slides — a deck is for reading, not for input."
|
|
1903
|
+
].join("\n"),
|
|
1904
|
+
example: {
|
|
1905
|
+
component: "Tabs",
|
|
1906
|
+
props: { labels: ["Where we are", "What we do next"] },
|
|
1907
|
+
children: [{
|
|
1908
|
+
component: "Stack",
|
|
1909
|
+
children: [{
|
|
1910
|
+
component: "Heading",
|
|
1911
|
+
props: {
|
|
1912
|
+
level: 2,
|
|
1913
|
+
text: "Revenue grew 27% in Q4"
|
|
1914
|
+
}
|
|
1915
|
+
}, {
|
|
1916
|
+
component: "Metric",
|
|
1917
|
+
props: {
|
|
1918
|
+
label: "Q4 revenue",
|
|
1919
|
+
value: "12,400",
|
|
1920
|
+
trend: "up"
|
|
1921
|
+
}
|
|
1922
|
+
}]
|
|
1923
|
+
}, {
|
|
1924
|
+
component: "Stack",
|
|
1925
|
+
children: [{
|
|
1926
|
+
component: "Heading",
|
|
1927
|
+
props: {
|
|
1928
|
+
level: 2,
|
|
1929
|
+
text: "Double down on EMEA"
|
|
1930
|
+
}
|
|
1931
|
+
}, {
|
|
1932
|
+
component: "Markdown",
|
|
1933
|
+
props: { text: "- hire two AEs\n- localise pricing" }
|
|
1934
|
+
}]
|
|
1935
|
+
}]
|
|
1936
|
+
}
|
|
1937
|
+
},
|
|
1938
|
+
{
|
|
1939
|
+
name: "reports",
|
|
1940
|
+
summary: "A long-form document: sections, tables and an optional `Timeline`.",
|
|
1941
|
+
guidance: [
|
|
1942
|
+
"Layout: `Stack` → `Heading` (level 2) → body → `Separator` between sections.",
|
|
1943
|
+
"Use `Table` for figures, `Timeline` for what happened when, `Markdown` for prose.",
|
|
1944
|
+
"Open with a one-paragraph summary before any detail.",
|
|
1945
|
+
"Attach generated files with `FileLink` at the end rather than inlining raw data."
|
|
1946
|
+
].join("\n"),
|
|
1947
|
+
example: {
|
|
1948
|
+
component: "Stack",
|
|
1949
|
+
children: [
|
|
1950
|
+
{
|
|
1951
|
+
component: "Heading",
|
|
1952
|
+
props: {
|
|
1953
|
+
level: 2,
|
|
1954
|
+
text: "Q4 incident review"
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
component: "Text",
|
|
1959
|
+
props: { text: "Three incidents, all recovered within the hour." }
|
|
1960
|
+
},
|
|
1961
|
+
{ component: "Separator" },
|
|
1962
|
+
{
|
|
1963
|
+
component: "Timeline",
|
|
1964
|
+
props: { items: [{
|
|
1965
|
+
title: "Search latency spike",
|
|
1966
|
+
time: "12 Nov",
|
|
1967
|
+
state: "done"
|
|
1968
|
+
}, {
|
|
1969
|
+
title: "Checkout timeouts",
|
|
1970
|
+
time: "03 Dec",
|
|
1971
|
+
state: "done"
|
|
1972
|
+
}] }
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
component: "FileLink",
|
|
1976
|
+
props: {
|
|
1977
|
+
path: "q4-incidents.csv",
|
|
1978
|
+
label: "Full log",
|
|
1979
|
+
size: 24117
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
]
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
];
|
|
1986
|
+
const UI_PRESET_NAMES = UI_PRESETS.map((preset) => preset.name);
|
|
1987
|
+
/** Returns undefined for names outside the catalog presets. @experimental */
|
|
1988
|
+
function uiPreset(name) {
|
|
1989
|
+
return UI_PRESETS.find((preset) => preset.name === name);
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* catalog 声明树 → json-render 扁平 spec。纯数据变换,不 import json-render
|
|
1993
|
+
* (`@webskill/ui` 保持环境无关)。
|
|
1994
|
+
*/
|
|
1995
|
+
function toJsonRenderSpec(spec) {
|
|
1996
|
+
const elements = {};
|
|
1997
|
+
let counter = 0;
|
|
1998
|
+
const walk = (node) => {
|
|
1999
|
+
const key = node.id ?? `${node.component.toLowerCase()}-${counter++}`;
|
|
1420
2000
|
const children = (node.children ?? []).map(walk);
|
|
1421
2001
|
elements[key] = {
|
|
1422
2002
|
type: node.component,
|
|
@@ -1430,34 +2010,186 @@ function toJsonRenderSpec(spec) {
|
|
|
1430
2010
|
elements
|
|
1431
2011
|
};
|
|
1432
2012
|
}
|
|
2013
|
+
/** ControlModel 的控件词汇 → catalog `Field.type`(catalog 的 8 种是超集) */
|
|
2014
|
+
const FIELD_TYPE = {
|
|
2015
|
+
text: "text",
|
|
2016
|
+
number: "number",
|
|
2017
|
+
boolean: "toggle",
|
|
2018
|
+
select: "select",
|
|
2019
|
+
textarea: "textarea"
|
|
2020
|
+
};
|
|
2021
|
+
function toOptions(options) {
|
|
2022
|
+
return (options ?? []).map((option) => ({
|
|
2023
|
+
label: option.label,
|
|
2024
|
+
value: typeof option.value === "number" ? option.value : String(option.value)
|
|
2025
|
+
}));
|
|
2026
|
+
}
|
|
2027
|
+
function toField(control) {
|
|
2028
|
+
return {
|
|
2029
|
+
component: "Field",
|
|
2030
|
+
props: {
|
|
2031
|
+
name: control.name,
|
|
2032
|
+
label: control.label,
|
|
2033
|
+
type: FIELD_TYPE[control.control],
|
|
2034
|
+
...control.required ? { required: true } : {},
|
|
2035
|
+
...control.description ? { description: control.description } : {},
|
|
2036
|
+
...control.defaultValue !== void 0 ? { defaultValue: control.defaultValue } : {},
|
|
2037
|
+
...control.suggestion ? { suggestion: control.suggestion } : {},
|
|
2038
|
+
...control.options ? { options: toOptions(control.options) } : {}
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* 五类 `InteractionRequest` → catalog 声明树。
|
|
2044
|
+
*
|
|
2045
|
+
* 路径 A(runtime 发起的交互请求)与路径 B(模型产出的 surface)的差别只在「谁产出这棵树」;
|
|
2046
|
+
* 产出之后四档走的是同一条 spec 渲染路径,因此这份生成器全仓只写一份。
|
|
2047
|
+
*/
|
|
2048
|
+
function interactionToUiSpec(request, labels = {}) {
|
|
2049
|
+
const model = interactionToFormModel(request);
|
|
2050
|
+
const title = labels.title ?? model.title;
|
|
2051
|
+
const cancelLabel = labels.cancelLabel ?? model.cancelLabel;
|
|
2052
|
+
const form = {
|
|
2053
|
+
component: "Form",
|
|
2054
|
+
props: {
|
|
2055
|
+
...title ? { title } : {},
|
|
2056
|
+
submitLabel: labels.submitLabel ?? model.submitLabel,
|
|
2057
|
+
...cancelLabel ? { cancelLabel } : {}
|
|
2058
|
+
},
|
|
2059
|
+
children: model.controls.map(toField)
|
|
2060
|
+
};
|
|
2061
|
+
if (model.controls.length > 0 || !model.message) return form;
|
|
2062
|
+
return {
|
|
2063
|
+
component: "Stack",
|
|
2064
|
+
children: [{
|
|
2065
|
+
component: "Text",
|
|
2066
|
+
props: { text: model.message }
|
|
2067
|
+
}, form]
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
1433
2070
|
const RENDER_UI_TOOL = "render_ui";
|
|
2071
|
+
const DESCRIBE_UI_PRESET_TOOL = "describe_ui_preset";
|
|
1434
2072
|
const defaultId = () => `ui-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1435
2073
|
/**
|
|
2074
|
+
* 工具描述只留一句指针:catalog 本体走 system 消息(C4)。
|
|
2075
|
+
* 工具 `description` 的长度上限与截断在各 provider 上不一致且是静默的,
|
|
2076
|
+
* 近 5000 字符的 catalog 放在那里,被截断时模型拿到的是半份规格而请求照样成功。
|
|
2077
|
+
*/
|
|
2078
|
+
const RENDER_UI_DESCRIPTION = [
|
|
2079
|
+
"Render a UI for the answer instead of prose.",
|
|
2080
|
+
"Use it for forms, tables, charts, metrics or file downloads.",
|
|
2081
|
+
"The component catalog is in the system prompt."
|
|
2082
|
+
].join(" ");
|
|
2083
|
+
const INTENTS = /* @__PURE__ */ new Set([
|
|
2084
|
+
"submit",
|
|
2085
|
+
"cancel",
|
|
2086
|
+
"select",
|
|
2087
|
+
"download",
|
|
2088
|
+
"refresh"
|
|
2089
|
+
]);
|
|
2090
|
+
function isIntent(value) {
|
|
2091
|
+
return typeof value === "string" && INTENTS.has(value);
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* 从节点树抽出 action 能力表。能力表走事件外壳而非树内:
|
|
2095
|
+
* 树是模型可写的,nonce 放进去就等于让模型自己签发能力。
|
|
2096
|
+
* action id 取节点 id;Button 未声明 id 时退回 intent 名(同 intent 只留一条)。
|
|
2097
|
+
* 表单的提交/取消取自作用域表:容器级放宽后一张 surface 可能有多个表单。
|
|
2098
|
+
*/
|
|
2099
|
+
function collectActions(node, out, scopes) {
|
|
2100
|
+
if (node.component === "Button" && isIntent(node.props?.["action"])) {
|
|
2101
|
+
const intent = node.props["action"];
|
|
2102
|
+
const id = node.id ?? intent;
|
|
2103
|
+
if (!out.has(id)) out.set(id, {
|
|
2104
|
+
id,
|
|
2105
|
+
intent,
|
|
2106
|
+
...intent === "submit" ? { awaitResponse: true } : {}
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
const scope = scopes.get(node);
|
|
2110
|
+
if (scope) {
|
|
2111
|
+
if (!out.has(scope.submitActionId)) out.set(scope.submitActionId, {
|
|
2112
|
+
id: scope.submitActionId,
|
|
2113
|
+
intent: "submit",
|
|
2114
|
+
awaitResponse: true
|
|
2115
|
+
});
|
|
2116
|
+
if (typeof node.props?.["cancelLabel"] === "string" && !out.has(scope.cancelActionId)) out.set(scope.cancelActionId, {
|
|
2117
|
+
id: scope.cancelActionId,
|
|
2118
|
+
intent: "cancel"
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
for (const child of node.children ?? []) collectActions(child, out, scopes);
|
|
2122
|
+
}
|
|
2123
|
+
/** 节点树声明的 action 能力表(renderer 据此生成按钮,runtime 据此签发 nonce)。@experimental */
|
|
2124
|
+
function collectSpecActions(spec) {
|
|
2125
|
+
const actions = /* @__PURE__ */ new Map();
|
|
2126
|
+
collectActions(spec, actions, new Map(collectFormScopes(spec).map((scope) => [scope.form, scope])));
|
|
2127
|
+
return [...actions.values()];
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
1436
2130
|
* 把 catalog 变成一个工具的入参 Schema,让模型直接生成声明式 UI(21 号文档 §3):
|
|
1437
|
-
* agentLoop / UiBridge /
|
|
2131
|
+
* agentLoop / UiBridge / extractUiSpecEvents 全部零改动,装配处只多一个 externalTools 条目。
|
|
1438
2132
|
*/
|
|
1439
2133
|
function createUiCatalogToolSource(options = {}) {
|
|
1440
2134
|
const catalog = options.catalog ?? uiCatalog;
|
|
1441
2135
|
const newSurfaceId = options.newSurfaceId ?? defaultId;
|
|
2136
|
+
const presets = options.presets ? options.presets.flatMap((name) => uiPreset(name) ?? []) : UI_PRESETS;
|
|
2137
|
+
const presetChoices = presets.map((preset) => `${preset.name} — ${preset.summary}`).join("\n");
|
|
2138
|
+
const presetTool = () => ({
|
|
2139
|
+
name: DESCRIBE_UI_PRESET_TOOL,
|
|
2140
|
+
description: `Fetch the layout guidance for one scenario preset before calling ${RENDER_UI_TOOL}.\n${presetChoices}`,
|
|
2141
|
+
inputSchema: {
|
|
2142
|
+
type: "object",
|
|
2143
|
+
properties: { preset: {
|
|
2144
|
+
type: "string",
|
|
2145
|
+
enum: presets.map((preset) => preset.name)
|
|
2146
|
+
} },
|
|
2147
|
+
required: ["preset"],
|
|
2148
|
+
additionalProperties: false
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
const renderTool = () => ({
|
|
2152
|
+
name: RENDER_UI_TOOL,
|
|
2153
|
+
description: RENDER_UI_DESCRIPTION,
|
|
2154
|
+
inputSchema: {
|
|
2155
|
+
type: "object",
|
|
2156
|
+
properties: {
|
|
2157
|
+
spec: catalog.toJsonSchema(),
|
|
2158
|
+
...presets.length > 0 ? { preset: {
|
|
2159
|
+
type: "string",
|
|
2160
|
+
enum: presets.map((preset) => preset.name),
|
|
2161
|
+
description: `Scenario preset this surface follows; call ${DESCRIBE_UI_PRESET_TOOL} first for its layout guidance.`
|
|
2162
|
+
} } : {}
|
|
2163
|
+
},
|
|
2164
|
+
required: ["spec"],
|
|
2165
|
+
additionalProperties: false
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
1442
2168
|
return {
|
|
1443
2169
|
kind: "ui-catalog",
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
""
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
2170
|
+
systemPrompt: () => Promise.resolve(catalog.toPrompt()),
|
|
2171
|
+
listToolSpecs: () => Promise.resolve(presets.length > 0 ? [renderTool(), presetTool()] : [renderTool()]),
|
|
2172
|
+
canHandle: (name) => name === "render_ui" || presets.length > 0 && name === "describe_ui_preset",
|
|
2173
|
+
call: (name, args) => {
|
|
2174
|
+
if (name === "describe_ui_preset") {
|
|
2175
|
+
const requested = args["preset"];
|
|
2176
|
+
const preset = presets.find((candidate) => candidate.name === requested);
|
|
2177
|
+
if (!preset) return Promise.resolve({
|
|
2178
|
+
ok: false,
|
|
2179
|
+
content: [],
|
|
2180
|
+
error: {
|
|
2181
|
+
code: "VALIDATION_FAILED",
|
|
2182
|
+
message: `Unknown UI preset "${String(requested)}"; available presets: ${presets.map((candidate) => candidate.name).join(", ")}`
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
return Promise.resolve({
|
|
2186
|
+
ok: true,
|
|
2187
|
+
content: [{
|
|
2188
|
+
type: "text",
|
|
2189
|
+
text: `${preset.guidance}\n\nExample spec:\n${JSON.stringify(preset.example)}`
|
|
2190
|
+
}]
|
|
2191
|
+
});
|
|
1457
2192
|
}
|
|
1458
|
-
}]),
|
|
1459
|
-
canHandle: (name) => name === RENDER_UI_TOOL,
|
|
1460
|
-
call: (_name, args) => {
|
|
1461
2193
|
const spec = args["spec"];
|
|
1462
2194
|
const result = catalog.validate(spec);
|
|
1463
2195
|
if (!result.ok) return Promise.resolve({
|
|
@@ -1468,65 +2200,28 @@ function createUiCatalogToolSource(options = {}) {
|
|
|
1468
2200
|
message: `The UI spec does not match the catalog: ${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`
|
|
1469
2201
|
}
|
|
1470
2202
|
});
|
|
1471
|
-
const
|
|
2203
|
+
const node = spec;
|
|
2204
|
+
const surfaceId = newSurfaceId();
|
|
2205
|
+
const actions = collectSpecActions(node);
|
|
1472
2206
|
return Promise.resolve({
|
|
1473
2207
|
ok: true,
|
|
1474
2208
|
content: [{
|
|
1475
2209
|
type: "json",
|
|
1476
2210
|
data: { $surface: {
|
|
1477
2211
|
type: "open",
|
|
1478
|
-
|
|
2212
|
+
id: surfaceId,
|
|
2213
|
+
node,
|
|
2214
|
+
...actions.length > 0 ? { actions } : {}
|
|
1479
2215
|
} }
|
|
1480
2216
|
}, {
|
|
1481
2217
|
type: "text",
|
|
1482
|
-
text: `Rendered surface ${
|
|
2218
|
+
text: `Rendered surface ${surfaceId}.`
|
|
1483
2219
|
}]
|
|
1484
2220
|
});
|
|
1485
2221
|
}
|
|
1486
2222
|
};
|
|
1487
2223
|
}
|
|
1488
|
-
const WEBSKILL_SURFACE_ACTION = "webskill:surface-action";
|
|
1489
|
-
function toUiSurfaceDescriptor(snapshot, provenance) {
|
|
1490
|
-
return {
|
|
1491
|
-
type: "webskill-surface",
|
|
1492
|
-
snapshot,
|
|
1493
|
-
provenance
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
function toUiSurfaceActionDispatch(snapshot, action, value) {
|
|
1497
|
-
return {
|
|
1498
|
-
...snapshot.runId ? { runId: snapshot.runId } : {},
|
|
1499
|
-
surfaceId: snapshot.surface.id,
|
|
1500
|
-
actionId: action.id,
|
|
1501
|
-
intent: action.intent,
|
|
1502
|
-
...action.nonce ? { nonce: action.nonce } : {},
|
|
1503
|
-
...value ? { value } : {}
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
/** Returns undefined for malformed or non-WebSkill renderer events. @experimental */
|
|
1507
|
-
function fromUiSurfaceActionDispatch(value) {
|
|
1508
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
1509
|
-
const event = value;
|
|
1510
|
-
if (typeof event["surfaceId"] !== "string" || typeof event["actionId"] !== "string" || !isSurfaceActionIntent(event["intent"])) return;
|
|
1511
|
-
return {
|
|
1512
|
-
...typeof event["runId"] === "string" ? { runId: event["runId"] } : {},
|
|
1513
|
-
surfaceId: event["surfaceId"],
|
|
1514
|
-
actionId: event["actionId"],
|
|
1515
|
-
intent: event["intent"],
|
|
1516
|
-
...typeof event["nonce"] === "string" ? { nonce: event["nonce"] } : {},
|
|
1517
|
-
...isRecord(event["value"]) ? { value: event["value"] } : {},
|
|
1518
|
-
...event["cancelled"] === true ? { cancelled: true } : {}
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
|
-
function isSurfaceActionIntent(value) {
|
|
1522
|
-
return value === "submit" || value === "cancel" || value === "select" || value === "download" || value === "refresh";
|
|
1523
|
-
}
|
|
1524
|
-
function isRecord(value) {
|
|
1525
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1526
|
-
}
|
|
1527
2224
|
const VERCEL_INTERACTION_TOOL_NAME = "webskill_interaction";
|
|
1528
|
-
const VERCEL_SURFACE_DATA_PART_TYPE = "data-webskill-surface";
|
|
1529
|
-
const VERCEL_SURFACE_ACTION_DATA_PART_TYPE = WEBSKILL_SURFACE_ACTION;
|
|
1530
2225
|
/** InteractionRequest → SDK tool UI 结构(应用聊天 UI 直接消费) */
|
|
1531
2226
|
function toVercelToolInvocation(request) {
|
|
1532
2227
|
return {
|
|
@@ -1549,20 +2244,6 @@ function fromVercelToolResult(part) {
|
|
|
1549
2244
|
value: p["output"] ?? p["result"] ?? p["value"]
|
|
1550
2245
|
};
|
|
1551
2246
|
}
|
|
1552
|
-
/** UiSurfaceSnapshot → AI SDK data part; the SurfaceHost owns renderer selection. @experimental */
|
|
1553
|
-
function toVercelSurfaceDataPart(snapshot) {
|
|
1554
|
-
return {
|
|
1555
|
-
type: VERCEL_SURFACE_DATA_PART_TYPE,
|
|
1556
|
-
id: `webskill-surface-${snapshot.surface.id}`,
|
|
1557
|
-
data: toUiSurfaceDescriptor(snapshot, "vercel-surface-host")
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
/** Decodes a nonce-bound action data part emitted by a Vercel SurfaceHost. @experimental */
|
|
1561
|
-
function fromVercelSurfaceAction(part) {
|
|
1562
|
-
const event = part;
|
|
1563
|
-
if (event?.type !== "webskill:surface-action") return void 0;
|
|
1564
|
-
return fromUiSurfaceActionDispatch(event.data);
|
|
1565
|
-
}
|
|
1566
2247
|
/**
|
|
1567
2248
|
* 薄桥接类:request → SDK tool UI 结构 → 应用的聊天 UI(sendInteraction 回调实现)
|
|
1568
2249
|
* → SDK 回传 → InteractionResponse。
|
|
@@ -1576,154 +2257,6 @@ var VercelUiBridge = class {
|
|
|
1576
2257
|
return fromVercelToolResult(await this.#sendInteraction(toVercelToolInvocation(input)));
|
|
1577
2258
|
}
|
|
1578
2259
|
};
|
|
1579
|
-
/**
|
|
1580
|
-
* InteractionRequest ↔ openui-lang 双向转换。
|
|
1581
|
-
* openui-lang 语法(@openuidev/react-lang 实测):
|
|
1582
|
-
* - 每行一条语句 `identifier = Expression`,`root = Component(...)` 为入口
|
|
1583
|
-
* - 组件调用参数为位置参数;字符串双引号反斜杠转义
|
|
1584
|
-
* - 组件词汇(与应用 Library 约定):Form(title, children)、
|
|
1585
|
-
* Text(content)(纯展示文本,authorize 的能力描述用)、
|
|
1586
|
-
* TextField(name, label, required?, defaultValue?)、NumberField、BooleanField(name, label, default?)、
|
|
1587
|
-
* SelectField(name, label, options)、SubmitButton(label, actionType, requestId)、
|
|
1588
|
-
* CancelButton(label, actionType, requestId)
|
|
1589
|
-
*/
|
|
1590
|
-
const OPENUI_SUBMIT_ACTION = "webskill:submit";
|
|
1591
|
-
const OPENUI_CANCEL_ACTION = "webskill:cancel";
|
|
1592
|
-
/** authorize 的 Allow 专用动作(提交即批准 → value:true;Deny 走 cancel → cancelled:true) */
|
|
1593
|
-
const OPENUI_AUTHORIZE_ACTION = "webskill:authorize";
|
|
1594
|
-
const OPENUI_SURFACE_ACTION = WEBSKILL_SURFACE_ACTION;
|
|
1595
|
-
const str = (s) => JSON.stringify(s);
|
|
1596
|
-
const val = (v) => JSON.stringify(v ?? null);
|
|
1597
|
-
function fieldStatements(fields) {
|
|
1598
|
-
return fields.map((f, i) => {
|
|
1599
|
-
const id = `f${i}`;
|
|
1600
|
-
const required = f.required ? ", true" : "";
|
|
1601
|
-
switch (f.control) {
|
|
1602
|
-
case "number": return `${id} = NumberField(${str(f.name)}, ${str(f.label)}${required})`;
|
|
1603
|
-
case "boolean": return `${id} = BooleanField(${str(f.name)}, ${str(f.label)}${f.defaultValue !== void 0 ? `, ${val(f.defaultValue)}` : ""})`;
|
|
1604
|
-
case "select": return `${id} = SelectField(${str(f.name)}, ${str(f.label)}, ${val(f.options ?? [])})`;
|
|
1605
|
-
case "textarea": return `${id} = TextField(${str(f.name)}, ${str(f.label)}${required})`;
|
|
1606
|
-
default: {
|
|
1607
|
-
const args = [str(f.name), str(f.label)];
|
|
1608
|
-
if (f.required || f.defaultValue !== void 0) args.push(f.required ? "true" : "null");
|
|
1609
|
-
if (f.defaultValue !== void 0) args.push(val(f.defaultValue));
|
|
1610
|
-
return `${id} = TextField(${args.join(", ")})`;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
/** @experimental */
|
|
1616
|
-
function toOpenUiLang(request) {
|
|
1617
|
-
const lines = [];
|
|
1618
|
-
let fields = [];
|
|
1619
|
-
let title = null;
|
|
1620
|
-
switch (request.type) {
|
|
1621
|
-
case "ask":
|
|
1622
|
-
title = "Question";
|
|
1623
|
-
fields = [{
|
|
1624
|
-
name: "answer",
|
|
1625
|
-
label: request.message,
|
|
1626
|
-
control: "text",
|
|
1627
|
-
required: true
|
|
1628
|
-
}];
|
|
1629
|
-
break;
|
|
1630
|
-
case "confirm":
|
|
1631
|
-
title = "Confirm";
|
|
1632
|
-
fields = [{
|
|
1633
|
-
name: "confirmed",
|
|
1634
|
-
label: request.message,
|
|
1635
|
-
control: "boolean",
|
|
1636
|
-
defaultValue: request.defaultValue ?? true
|
|
1637
|
-
}];
|
|
1638
|
-
break;
|
|
1639
|
-
case "form":
|
|
1640
|
-
title = request.title ?? "Form";
|
|
1641
|
-
fields = request.fields.map((f) => ({
|
|
1642
|
-
name: f.name,
|
|
1643
|
-
label: f.label,
|
|
1644
|
-
control: f.type,
|
|
1645
|
-
...f.required ? { required: true } : {},
|
|
1646
|
-
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
1647
|
-
...f.options ? { options: f.options } : {}
|
|
1648
|
-
}));
|
|
1649
|
-
break;
|
|
1650
|
-
case "select":
|
|
1651
|
-
title = "Select";
|
|
1652
|
-
fields = [{
|
|
1653
|
-
name: "selected",
|
|
1654
|
-
label: request.message,
|
|
1655
|
-
control: "select",
|
|
1656
|
-
options: request.options
|
|
1657
|
-
}];
|
|
1658
|
-
break;
|
|
1659
|
-
case "authorize":
|
|
1660
|
-
title = "Authorization required";
|
|
1661
|
-
fields = [];
|
|
1662
|
-
break;
|
|
1663
|
-
}
|
|
1664
|
-
lines.push(...fieldStatements(fields));
|
|
1665
|
-
const children = fields.map((_, i) => `f${i}`);
|
|
1666
|
-
const isAuthorize = request.type === "authorize";
|
|
1667
|
-
if (isAuthorize) {
|
|
1668
|
-
lines.push(`message = Text(${str(request.message)})`);
|
|
1669
|
-
children.push("message");
|
|
1670
|
-
}
|
|
1671
|
-
const submitAction = isAuthorize ? OPENUI_AUTHORIZE_ACTION : OPENUI_SUBMIT_ACTION;
|
|
1672
|
-
lines.push(`submit = SubmitButton(${str(isAuthorize ? "Allow" : "Submit")}, ${str(submitAction)}, ${str(request.id)})`);
|
|
1673
|
-
lines.push(`cancel = CancelButton(${str(isAuthorize ? "Deny" : "Cancel")}, ${str(OPENUI_CANCEL_ACTION)}, ${str(request.id)})`);
|
|
1674
|
-
lines.push(`root = Form(${val(title)}, [${[
|
|
1675
|
-
...children,
|
|
1676
|
-
"submit",
|
|
1677
|
-
"cancel"
|
|
1678
|
-
].join(", ")}])`);
|
|
1679
|
-
return lines.join("\n");
|
|
1680
|
-
}
|
|
1681
|
-
/**
|
|
1682
|
-
* UiSurfaceSnapshot → OpenUI library program. OpenUI applications provide WebSkillSurfaceHost;
|
|
1683
|
-
* its descriptor tells the host whether it is a native extension rather than an OpenUI-native component.
|
|
1684
|
-
* @experimental
|
|
1685
|
-
*/
|
|
1686
|
-
function toOpenUiSurfaceLang(snapshot) {
|
|
1687
|
-
const descriptor = toUiSurfaceDescriptor(snapshot, snapshot.surface.kind === "chart" || snapshot.surface.kind === "table" || snapshot.surface.kind === "custom" ? "openui-extension" : "openui-library");
|
|
1688
|
-
return `root = WebSkillSurfaceHost(${val(descriptor)})`;
|
|
1689
|
-
}
|
|
1690
|
-
/** Decodes a nonce-bound action emitted by an OpenUI WebSkillSurfaceHost. @experimental */
|
|
1691
|
-
function fromOpenUiSurfaceAction(action) {
|
|
1692
|
-
const event = action;
|
|
1693
|
-
if (event?.type !== "webskill:surface-action") return void 0;
|
|
1694
|
-
return fromUiSurfaceActionDispatch(event.params);
|
|
1695
|
-
}
|
|
1696
|
-
/**
|
|
1697
|
-
* OpenUI ActionEvent → InteractionResponse(formState 优先,取消单独分支)
|
|
1698
|
-
* @experimental
|
|
1699
|
-
*/
|
|
1700
|
-
function fromOpenUiAction(action) {
|
|
1701
|
-
const a = action ?? {};
|
|
1702
|
-
const id = String(a.params?.["id"] ?? "");
|
|
1703
|
-
if (a.type === "webskill:cancel") return {
|
|
1704
|
-
id,
|
|
1705
|
-
cancelled: true
|
|
1706
|
-
};
|
|
1707
|
-
if (a.type === "webskill:authorize") return {
|
|
1708
|
-
id,
|
|
1709
|
-
value: true
|
|
1710
|
-
};
|
|
1711
|
-
if (a.type === "webskill:submit") {
|
|
1712
|
-
const formState = a.formState;
|
|
1713
|
-
if (formState) return {
|
|
1714
|
-
id,
|
|
1715
|
-
value: Object.fromEntries(Object.entries(formState).map(([key, v]) => [key, typeof v === "object" && v !== null && "value" in v ? v.value : v]))
|
|
1716
|
-
};
|
|
1717
|
-
return {
|
|
1718
|
-
id,
|
|
1719
|
-
value: a.params?.["values"]
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
return {
|
|
1723
|
-
id,
|
|
1724
|
-
value: a.params
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
2260
|
const literal = (value) => JSON.stringify(value ?? null);
|
|
1728
2261
|
/** 组件的位置参数顺序 = catalog props 的 schema key 顺序(OpenUI 的 param map 同源) */
|
|
1729
2262
|
function propOrder(catalog) {
|
|
@@ -1756,194 +2289,52 @@ function toOpenUiSpecLang(spec, catalog = uiCatalog) {
|
|
|
1756
2289
|
const rootId = walk(spec);
|
|
1757
2290
|
return [...lines.filter((line) => !line.startsWith(`${rootId} =`)), `root = ${lines.find((line) => line.startsWith(`${rootId} =`)).slice(rootId.length + 3)}`].join("\n");
|
|
1758
2291
|
}
|
|
2292
|
+
const WEBSKILL_SURFACE_ACTION = "webskill:surface-action";
|
|
2293
|
+
function toUiSurfaceActionDispatch(snapshot, action, value, scopeId) {
|
|
2294
|
+
return {
|
|
2295
|
+
...snapshot.runId ? { runId: snapshot.runId } : {},
|
|
2296
|
+
surfaceId: snapshot.id,
|
|
2297
|
+
actionId: action.id,
|
|
2298
|
+
intent: action.intent,
|
|
2299
|
+
...action.nonce ? { nonce: action.nonce } : {},
|
|
2300
|
+
...scopeId ? { scopeId } : {},
|
|
2301
|
+
...value ? { value } : {}
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
/** Returns undefined for malformed or non-WebSkill renderer events. @experimental */
|
|
2305
|
+
function fromUiSurfaceActionDispatch(value) {
|
|
2306
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2307
|
+
const event = value;
|
|
2308
|
+
if (typeof event["surfaceId"] !== "string" || typeof event["actionId"] !== "string" || !isSurfaceActionIntent(event["intent"])) return;
|
|
2309
|
+
return {
|
|
2310
|
+
...typeof event["runId"] === "string" ? { runId: event["runId"] } : {},
|
|
2311
|
+
surfaceId: event["surfaceId"],
|
|
2312
|
+
actionId: event["actionId"],
|
|
2313
|
+
intent: event["intent"],
|
|
2314
|
+
...typeof event["nonce"] === "string" ? { nonce: event["nonce"] } : {},
|
|
2315
|
+
...typeof event["scopeId"] === "string" ? { scopeId: event["scopeId"] } : {},
|
|
2316
|
+
...isRecord(event["value"]) ? { value: event["value"] } : {},
|
|
2317
|
+
...event["cancelled"] === true ? { cancelled: true } : {}
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
function isSurfaceActionIntent(value) {
|
|
2321
|
+
return value === "submit" || value === "cancel" || value === "select" || value === "download" || value === "refresh";
|
|
2322
|
+
}
|
|
2323
|
+
function isRecord(value) {
|
|
2324
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2325
|
+
}
|
|
1759
2326
|
/**
|
|
1760
|
-
*
|
|
2327
|
+
* A2UI 协议常量与 surface 动作编解码(v0.9.1)。
|
|
1761
2328
|
* 依据 https://a2ui.org/specification/v0.9-a2ui/:
|
|
1762
2329
|
* - 信封 {version, createSurface|updateComponents|updateDataModel|deleteSurface}
|
|
1763
2330
|
* - 组件为扁平邻接表(id 引用),root 必需
|
|
1764
2331
|
* - 输入组件与数据模型经 JSON Pointer 双向绑定;action.context 回传
|
|
1765
|
-
* - createSurface.sendDataModel: true → 客户端动作时携带完整数据模型
|
|
1766
2332
|
*
|
|
1767
|
-
*
|
|
1768
|
-
* cancel → {name: 'webskill:cancel', context: {requestId}}。
|
|
2333
|
+
* 声明树 → A2UI 消息的转换在 `specToA2ui.ts`(四档共用的单一渲染路径)。
|
|
1769
2334
|
*/
|
|
1770
2335
|
const A2UI_VERSION = "v0.9.1";
|
|
1771
2336
|
const A2UI_BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json";
|
|
1772
|
-
const A2UI_SUBMIT_ACTION = "webskill:submit";
|
|
1773
|
-
const A2UI_CANCEL_ACTION = "webskill:cancel";
|
|
1774
2337
|
const A2UI_SURFACE_ACTION = WEBSKILL_SURFACE_ACTION;
|
|
1775
|
-
const text = (id, content, variant) => ({
|
|
1776
|
-
id,
|
|
1777
|
-
component: "Text",
|
|
1778
|
-
text: content,
|
|
1779
|
-
...variant ? { variant } : {}
|
|
1780
|
-
});
|
|
1781
|
-
/** @experimental */
|
|
1782
|
-
function toA2uiMessages(request) {
|
|
1783
|
-
const surfaceId = `webskill-${request.id}`;
|
|
1784
|
-
const components = [];
|
|
1785
|
-
const children = [];
|
|
1786
|
-
const defaults = {};
|
|
1787
|
-
if (request.type === "form" && request.title) {
|
|
1788
|
-
components.push(text("title", request.title, "h3"));
|
|
1789
|
-
children.push("title");
|
|
1790
|
-
}
|
|
1791
|
-
if (request.type === "authorize") {
|
|
1792
|
-
components.push(text("title", "Authorization required", "h3"));
|
|
1793
|
-
children.push("title");
|
|
1794
|
-
}
|
|
1795
|
-
const message = request.type === "ask" || request.type === "confirm" || request.type === "select" || request.type === "authorize" ? request.message : void 0;
|
|
1796
|
-
if (message) {
|
|
1797
|
-
components.push(text("message", message));
|
|
1798
|
-
children.push("message");
|
|
1799
|
-
}
|
|
1800
|
-
const fields = request.type === "form" ? request.fields.map((f) => ({ ...f })) : request.type === "ask" ? [{
|
|
1801
|
-
name: "answer",
|
|
1802
|
-
label: request.message,
|
|
1803
|
-
type: "text",
|
|
1804
|
-
required: true
|
|
1805
|
-
}] : request.type === "confirm" ? [{
|
|
1806
|
-
name: "confirmed",
|
|
1807
|
-
label: request.message,
|
|
1808
|
-
type: "boolean",
|
|
1809
|
-
defaultValue: request.defaultValue ?? true
|
|
1810
|
-
}] : request.type === "authorize" ? [] : [{
|
|
1811
|
-
name: "selected",
|
|
1812
|
-
label: request.message,
|
|
1813
|
-
type: "select",
|
|
1814
|
-
options: request.options
|
|
1815
|
-
}];
|
|
1816
|
-
for (const field of fields) {
|
|
1817
|
-
const path = `/form/${field.name}`;
|
|
1818
|
-
if (field.defaultValue !== void 0) defaults[field.name] = field.defaultValue;
|
|
1819
|
-
const checks = field.required ? [{
|
|
1820
|
-
call: "required",
|
|
1821
|
-
args: { value: { path } },
|
|
1822
|
-
message: `${field.label} is required`
|
|
1823
|
-
}] : void 0;
|
|
1824
|
-
if (field.type === "boolean") components.push({
|
|
1825
|
-
id: `field_${field.name}`,
|
|
1826
|
-
component: "CheckBox",
|
|
1827
|
-
label: field.label,
|
|
1828
|
-
value: { path },
|
|
1829
|
-
...checks ? { checks } : {}
|
|
1830
|
-
});
|
|
1831
|
-
else if (field.type === "select") components.push({
|
|
1832
|
-
id: `field_${field.name}`,
|
|
1833
|
-
component: "ChoicePicker",
|
|
1834
|
-
variant: "mutuallyExclusive",
|
|
1835
|
-
label: field.label,
|
|
1836
|
-
options: (field.options ?? []).map((o) => ({
|
|
1837
|
-
label: o.label,
|
|
1838
|
-
value: typeof o.value === "string" ? o.value : JSON.stringify(o.value)
|
|
1839
|
-
})),
|
|
1840
|
-
value: { path },
|
|
1841
|
-
...checks ? { checks } : {}
|
|
1842
|
-
});
|
|
1843
|
-
else components.push({
|
|
1844
|
-
id: `field_${field.name}`,
|
|
1845
|
-
component: "TextField",
|
|
1846
|
-
label: field.label,
|
|
1847
|
-
value: { path },
|
|
1848
|
-
variant: field.type === "textarea" ? "longText" : "shortText",
|
|
1849
|
-
...checks ? { checks } : {}
|
|
1850
|
-
});
|
|
1851
|
-
children.push(`field_${field.name}`);
|
|
1852
|
-
}
|
|
1853
|
-
components.push(text("submit_label", request.type === "authorize" ? "Allow" : "Submit"));
|
|
1854
|
-
components.push({
|
|
1855
|
-
id: "submit",
|
|
1856
|
-
component: "Button",
|
|
1857
|
-
child: "submit_label",
|
|
1858
|
-
variant: "primary",
|
|
1859
|
-
action: { event: {
|
|
1860
|
-
name: A2UI_SUBMIT_ACTION,
|
|
1861
|
-
context: { requestId: request.id }
|
|
1862
|
-
} }
|
|
1863
|
-
});
|
|
1864
|
-
components.push(text("cancel_label", request.type === "authorize" ? "Deny" : "Cancel"));
|
|
1865
|
-
components.push({
|
|
1866
|
-
id: "cancel",
|
|
1867
|
-
component: "Button",
|
|
1868
|
-
child: "cancel_label",
|
|
1869
|
-
variant: "borderless",
|
|
1870
|
-
action: { event: {
|
|
1871
|
-
name: A2UI_CANCEL_ACTION,
|
|
1872
|
-
context: { requestId: request.id }
|
|
1873
|
-
} }
|
|
1874
|
-
});
|
|
1875
|
-
children.push("submit", "cancel");
|
|
1876
|
-
components.unshift({
|
|
1877
|
-
id: "root",
|
|
1878
|
-
component: "Column",
|
|
1879
|
-
children,
|
|
1880
|
-
justify: "start",
|
|
1881
|
-
align: "stretch"
|
|
1882
|
-
});
|
|
1883
|
-
const messages = [{
|
|
1884
|
-
version: A2UI_VERSION,
|
|
1885
|
-
createSurface: {
|
|
1886
|
-
surfaceId,
|
|
1887
|
-
catalogId: A2UI_BASIC_CATALOG_ID,
|
|
1888
|
-
sendDataModel: true
|
|
1889
|
-
}
|
|
1890
|
-
}, {
|
|
1891
|
-
version: A2UI_VERSION,
|
|
1892
|
-
updateComponents: {
|
|
1893
|
-
surfaceId,
|
|
1894
|
-
components
|
|
1895
|
-
}
|
|
1896
|
-
}];
|
|
1897
|
-
if (Object.keys(defaults).length > 0) messages.push({
|
|
1898
|
-
version: A2UI_VERSION,
|
|
1899
|
-
updateDataModel: {
|
|
1900
|
-
surfaceId,
|
|
1901
|
-
path: "/form",
|
|
1902
|
-
value: defaults
|
|
1903
|
-
}
|
|
1904
|
-
});
|
|
1905
|
-
return messages;
|
|
1906
|
-
}
|
|
1907
|
-
/**
|
|
1908
|
-
* UiSurfaceSnapshot → A2UI extension-host messages.
|
|
1909
|
-
* Basic Catalog has no chart/table vocabulary, so a labelled WebSkill host owns these controlled renderers.
|
|
1910
|
-
* @experimental
|
|
1911
|
-
*/
|
|
1912
|
-
function toA2uiSurfaceMessages(snapshot) {
|
|
1913
|
-
const surfaceId = `webskill-surface-${snapshot.surface.id}`;
|
|
1914
|
-
const standard = snapshot.surface.kind === "metric" || snapshot.surface.kind === "file";
|
|
1915
|
-
const descriptor = toUiSurfaceDescriptor(snapshot, standard ? "a2ui-standard" : "a2ui-extension");
|
|
1916
|
-
const component = standard ? {
|
|
1917
|
-
id: "content",
|
|
1918
|
-
component: "Text",
|
|
1919
|
-
text: snapshot.surface.kind === "metric" ? `${snapshot.surface.label}: ${snapshot.surface.value}` : snapshot.surface.kind === "file" ? snapshot.surface.path : ""
|
|
1920
|
-
} : {
|
|
1921
|
-
id: "content",
|
|
1922
|
-
component: "WebSkillSurfaceHost",
|
|
1923
|
-
descriptor,
|
|
1924
|
-
provenance: "WebSkill extension host"
|
|
1925
|
-
};
|
|
1926
|
-
return [{
|
|
1927
|
-
version: A2UI_VERSION,
|
|
1928
|
-
createSurface: {
|
|
1929
|
-
surfaceId,
|
|
1930
|
-
catalogId: A2UI_BASIC_CATALOG_ID,
|
|
1931
|
-
sendDataModel: true
|
|
1932
|
-
}
|
|
1933
|
-
}, {
|
|
1934
|
-
version: A2UI_VERSION,
|
|
1935
|
-
updateComponents: {
|
|
1936
|
-
surfaceId,
|
|
1937
|
-
components: [{
|
|
1938
|
-
id: "root",
|
|
1939
|
-
component: "Column",
|
|
1940
|
-
children: ["content"],
|
|
1941
|
-
justify: "start",
|
|
1942
|
-
align: "stretch"
|
|
1943
|
-
}, component]
|
|
1944
|
-
}
|
|
1945
|
-
}];
|
|
1946
|
-
}
|
|
1947
2338
|
/** Decodes a nonce-bound action emitted by a WebSkill A2UI extension host. @experimental */
|
|
1948
2339
|
function fromA2uiSurfaceAction(event) {
|
|
1949
2340
|
const action = event;
|
|
@@ -1951,80 +2342,10 @@ function fromA2uiSurfaceAction(event) {
|
|
|
1951
2342
|
return fromUiSurfaceActionDispatch(action.context);
|
|
1952
2343
|
}
|
|
1953
2344
|
/** Builds the A2UI event payload an extension host sends after a user action. @experimental */
|
|
1954
|
-
function toA2uiSurfaceAction(snapshot, action, value) {
|
|
2345
|
+
function toA2uiSurfaceAction(snapshot, action, value, scopeId) {
|
|
1955
2346
|
return {
|
|
1956
2347
|
name: A2UI_SURFACE_ACTION,
|
|
1957
|
-
context: toUiSurfaceActionDispatch(snapshot, action, value)
|
|
1958
|
-
};
|
|
1959
|
-
}
|
|
1960
|
-
/**
|
|
1961
|
-
* 按 InteractionRequest.type 解码提交值(绑定模型原样回传的是表单对象):
|
|
1962
|
-
* confirm 仅 confirmed===true 批准;select 还原原始 option 值(非字符串值经 JSON 编码比对);
|
|
1963
|
-
* form 的 number 字段转 number(NaN 保留原值);ask 取 answer;authorize 提交即批准。
|
|
1964
|
-
* 纯转换器:任何直接使用 fromA2uiAction 的消费者都应经此拿到正确类型。
|
|
1965
|
-
* @experimental
|
|
1966
|
-
*/
|
|
1967
|
-
function decodeInteractionResponse(request, response) {
|
|
1968
|
-
if (response.cancelled) return response;
|
|
1969
|
-
const values = response.value;
|
|
1970
|
-
switch (request.type) {
|
|
1971
|
-
case "confirm": return {
|
|
1972
|
-
...response,
|
|
1973
|
-
value: values?.confirmed === true
|
|
1974
|
-
};
|
|
1975
|
-
case "ask": return {
|
|
1976
|
-
...response,
|
|
1977
|
-
value: values?.answer
|
|
1978
|
-
};
|
|
1979
|
-
case "select": {
|
|
1980
|
-
const raw = values?.selected;
|
|
1981
|
-
const option = request.options.find((o) => o.value === raw || JSON.stringify(o.value) === raw);
|
|
1982
|
-
return {
|
|
1983
|
-
...response,
|
|
1984
|
-
value: option ? option.value : raw
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
case "form": {
|
|
1988
|
-
if (typeof values !== "object" || values === null) return response;
|
|
1989
|
-
const out = { ...values };
|
|
1990
|
-
for (const field of request.fields) if (field.type === "number" && out[field.name] !== void 0) {
|
|
1991
|
-
const n = Number(out[field.name]);
|
|
1992
|
-
if (!Number.isNaN(n)) out[field.name] = n;
|
|
1993
|
-
}
|
|
1994
|
-
return {
|
|
1995
|
-
...response,
|
|
1996
|
-
value: out
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
case "authorize": return {
|
|
2000
|
-
...response,
|
|
2001
|
-
value: true
|
|
2002
|
-
};
|
|
2003
|
-
default: return response;
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
/**
|
|
2007
|
-
* A2UI client→server action 事件 → InteractionResponse。
|
|
2008
|
-
* 传入 request 时按类型解码提交值(见 decodeInteractionResponse)。
|
|
2009
|
-
* @experimental
|
|
2010
|
-
*/
|
|
2011
|
-
function fromA2uiAction(event, request) {
|
|
2012
|
-
const e = event ?? {};
|
|
2013
|
-
const id = String(e.context?.["requestId"] ?? "");
|
|
2014
|
-
if (e.name === "webskill:cancel") return {
|
|
2015
|
-
id,
|
|
2016
|
-
cancelled: true
|
|
2017
|
-
};
|
|
2018
|
-
if (e.name === "webskill:submit") {
|
|
2019
|
-
const response = {
|
|
2020
|
-
id,
|
|
2021
|
-
value: e.context?.["values"] ?? e.context?.["form"] ?? e.context
|
|
2022
|
-
};
|
|
2023
|
-
return request ? decodeInteractionResponse(request, response) : response;
|
|
2024
|
-
}
|
|
2025
|
-
return {
|
|
2026
|
-
id,
|
|
2027
|
-
value: e.context
|
|
2348
|
+
context: toUiSurfaceActionDispatch(snapshot, action, value, scopeId)
|
|
2028
2349
|
};
|
|
2029
2350
|
}
|
|
2030
2351
|
/** 声明树里按钮触发的事件名(宿主据 context.actionId 回传 surface action) */
|
|
@@ -2122,73 +2443,6 @@ function fromA2uiSpecAction(event) {
|
|
|
2122
2443
|
context
|
|
2123
2444
|
};
|
|
2124
2445
|
}
|
|
2125
|
-
let loading;
|
|
2126
|
-
function loadA2uiRuntime() {
|
|
2127
|
-
loading ??= (async () => {
|
|
2128
|
-
try {
|
|
2129
|
-
const [core, lit] = await Promise.all([import("@a2ui/web_core/v0_9"), import("@a2ui/lit/v0_9")]);
|
|
2130
|
-
lit.A2uiSurface;
|
|
2131
|
-
return {
|
|
2132
|
-
MessageProcessor: core.MessageProcessor,
|
|
2133
|
-
basicCatalog: lit.basicCatalog
|
|
2134
|
-
};
|
|
2135
|
-
} catch (e) {
|
|
2136
|
-
throw new WebSkillError("UI_UNAVAILABLE", "The A2UI Lit runtime is unavailable; install @a2ui/lit and @a2ui/web_core to use LitRendererBridge", e);
|
|
2137
|
-
}
|
|
2138
|
-
})();
|
|
2139
|
-
return loading;
|
|
2140
|
-
}
|
|
2141
|
-
/** @experimental */
|
|
2142
|
-
var LitRendererBridge = class {
|
|
2143
|
-
#mount;
|
|
2144
|
-
#doc;
|
|
2145
|
-
/** 进行中的交互:id → resolve 与 surface 元素(cancel 时清理并以 cancelled resolve) */
|
|
2146
|
-
#pending = /* @__PURE__ */ new Map();
|
|
2147
|
-
constructor(deps) {
|
|
2148
|
-
this.#mount = deps.mount;
|
|
2149
|
-
this.#doc = deps.document ?? deps.mount.ownerDocument;
|
|
2150
|
-
}
|
|
2151
|
-
async request(input) {
|
|
2152
|
-
const { MessageProcessor, basicCatalog } = await loadA2uiRuntime();
|
|
2153
|
-
const messages = toA2uiMessages(input);
|
|
2154
|
-
const surfaceId = `webskill-${input.id}`;
|
|
2155
|
-
return new Promise((resolve) => {
|
|
2156
|
-
const entry = { resolve };
|
|
2157
|
-
this.#pending.set(input.id, entry);
|
|
2158
|
-
const processor = new MessageProcessor([basicCatalog], (action) => {
|
|
2159
|
-
const surfaces = processor.getClientDataModel("v0.9.1")?.surfaces;
|
|
2160
|
-
const formValues = typeof surfaces?.[surfaceId] === "object" && surfaces[surfaceId] !== null ? surfaces[surfaceId]["form"] ?? action.context : action.context;
|
|
2161
|
-
this.#pending.delete(input.id);
|
|
2162
|
-
entry.surfaceEl?.remove();
|
|
2163
|
-
resolve(fromA2uiAction({
|
|
2164
|
-
...action,
|
|
2165
|
-
context: {
|
|
2166
|
-
...action.context,
|
|
2167
|
-
values: formValues
|
|
2168
|
-
}
|
|
2169
|
-
}, input));
|
|
2170
|
-
}, { version: "v0.9.1" });
|
|
2171
|
-
processor.onSurfaceCreated((surface) => {
|
|
2172
|
-
const el = this.#doc.createElement("a2ui-surface");
|
|
2173
|
-
el.surface = surface;
|
|
2174
|
-
entry.surfaceEl = el;
|
|
2175
|
-
this.#mount.appendChild(el);
|
|
2176
|
-
});
|
|
2177
|
-
processor.processMessages(messages);
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
|
-
/** runtime 交互超时/取消:移除 surface 并以 cancelled resolve pending Promise(防悬挂 + DOM 残留) */
|
|
2181
|
-
cancel(id) {
|
|
2182
|
-
const entry = this.#pending.get(id);
|
|
2183
|
-
if (!entry) return;
|
|
2184
|
-
this.#pending.delete(id);
|
|
2185
|
-
entry.surfaceEl?.remove();
|
|
2186
|
-
entry.resolve({
|
|
2187
|
-
id,
|
|
2188
|
-
cancelled: true
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
};
|
|
2192
2446
|
/**
|
|
2193
2447
|
* BYOC Lit 元素的加载入口。
|
|
2194
2448
|
*
|
|
@@ -2198,7 +2452,7 @@ var LitRendererBridge = class {
|
|
|
2198
2452
|
*/
|
|
2199
2453
|
async function loadWebSkillLitCatalog() {
|
|
2200
2454
|
try {
|
|
2201
|
-
const { webskillLitCatalog } = await import("./webskillLitCatalog-
|
|
2455
|
+
const { webskillLitCatalog } = await import("./webskillLitCatalog-_mugzRHx-DiuJpCuf.js");
|
|
2202
2456
|
return webskillLitCatalog();
|
|
2203
2457
|
} catch (cause) {
|
|
2204
2458
|
throw new WebSkillError("UI_UNAVAILABLE", "The WebSkill A2UI catalog is unavailable; install @a2ui/lit, @a2ui/web_core and lit to render catalog surfaces with A2UI", cause);
|
|
@@ -2223,4 +2477,4 @@ async function loadOpenUiPeers() {
|
|
|
2223
2477
|
}
|
|
2224
2478
|
|
|
2225
2479
|
//#endregion
|
|
2226
|
-
export {
|
|
2480
|
+
export { defineUiCatalog as $, loadOpenUiPeers as A, toOpenUiSpecLang as B, ensureStyles as C, fromVercelToolResult as D, fromUiSurfaceActionDispatch as E, renderRenderResult as F, A2UI_COMMON_TYPES as G, toVercelToolInvocation as H, shapeInteractionValue as I, WEBSKILL_A2UI_CATALOG_ID as J, UI_CATALOG_GROUPS as K, toA2uiSpecMessages as L, qualifyFieldName as M, renderBlocks as N, interactionToFormModel as O, renderMiniChart as P, chartSpecFromProps as Q, toA2uiSurfaceAction as R, createUiCatalogToolSource as S, fromA2uiSurfaceAction as T, uiPreset as U, toUiSurfaceActionDispatch as V, A2UI_CHILDREN_PROP as W, a2uiComponentShapes as X, a2uiComponentSchema as Y, buildA2uiCatalogDefinition as Z, chartToTable as _, A2UI_VERSION as a, collectSpecActions as b, RENDER_UI_TOOL as c, VERCEL_INTERACTION_TOOL_NAME as d, mountEchart as et, VercelUiBridge as f, applySuggestion as g, WebFormBridge as h, A2UI_SURFACE_ACTION as i, loadWebSkillLitCatalog as j, interactionToUiSpec as k, UI_PRESETS as l, WEBSKILL_SURFACE_ACTION as m, A2UI_SPEC_ACTION as n, uiCatalog as nt, CHART_PALETTE as o, WEBSKILL_STYLES_CSS as p, UI_CATALOG_PROMPT_BUDGET_BYTES as q, A2UI_SPEC_FORM_PATH as r, DESCRIBE_UI_PRESET_TOOL as s, A2UI_BASIC_CATALOG_ID as t, renderMiniMarkdown as tt, UI_PRESET_NAMES as u, collectFormScopes as v, fromA2uiSpecAction as w, collectValues as x, collectScopedValues as y, toJsonRenderSpec as z };
|