diff --git a/packages/genui/a2ui/README.md b/packages/genui/a2ui/README.md index b15e4c7895..124a6ba9ba 100644 --- a/packages/genui/a2ui/README.md +++ b/packages/genui/a2ui/README.md @@ -107,8 +107,6 @@ generated UI actions through `onAction`. ## More Docs -- [Architecture and exports](./docs/architecture.md) -- [Catalogs and manifests](./docs/catalogs.md) -- [Custom components](./docs/custom-components.md) -- [Built-in catalog composition](./src/catalog/README.md) +- [Overview and architecture](./docs/overview.md) +- [Catalogs, built-ins, and custom components](./docs/catalog-guide.md) - [Open the A2UI playground](https://lynxjs.org/a2ui) diff --git a/packages/genui/a2ui/README_zh.md b/packages/genui/a2ui/README_zh.md index cfad0b1cc0..a5e8d1191e 100644 --- a/packages/genui/a2ui/README_zh.md +++ b/packages/genui/a2ui/README_zh.md @@ -97,8 +97,6 @@ async function sendPrompt(input: string) { ## 更多文档 -- [架构与导出](./docs/architecture_zh.md) -- [Catalog 与 manifests](./docs/catalogs_zh.md) -- [自定义组件](./docs/custom-components_zh.md) -- [内置 Catalog 组合](./src/catalog/readme_zh.md) +- [概览与架构](./docs/overview_zh.md) +- [Catalogs、内置组件与自定义组件](./docs/catalog-guide_zh.md) - [打开 A2UI Playground](https://lynxjs.org/a2ui) diff --git a/packages/genui/a2ui/docs/architecture.md b/packages/genui/a2ui/docs/architecture.md deleted file mode 100644 index a22fe3e6d6..0000000000 --- a/packages/genui/a2ui/docs/architecture.md +++ /dev/null @@ -1,67 +0,0 @@ -# Architecture and exports - -This page is for developers who already finished the quick start and want to -understand which part of the A2UI stack owns each responsibility. - -## Responsibilities - -| Piece | Runs in | Responsibility | -| ----------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Agent service | Server | Turns user prompts and client actions into validated A2UI message arrays. It should prompt the model with the same catalog contract that the client can render. | -| Transport adapter | Client shell | Sends prompts/actions to the Agent service over REST, SSE, WebSocket, or another transport, then writes returned messages into a `MessageStore`. | -| `MessageStore` | Client | Stores raw A2UI protocol messages in arrival order and notifies subscribers. It does not parse or interpret the protocol. | -| `` | Client | Owns a `MessageProcessor` per mount, consumes new messages from `MessageStore`, renders the active surface, and forwards generated UI actions through `onAction`. | -| Catalog API | Client and Agent handshake | Maps protocol component/function names to local implementations and optional JSON schemas. Use `defineCatalog`, `mergeCatalogs`, `serializeCatalog`, and `defineFunction` to compose that contract. | -| Built-ins | Client | Provides A2UI v0.9 basic catalog component renderers, JSON-Schema manifests, and client-side basic-catalog function implementations. | -| `genui a2ui` | Build/setup time | Generates custom catalog artifacts and system prompts. It is not required when both the Agent and renderer use the built-in basic catalog. | - -## Package contents - -- ``: all-in-one component that owns a `MessageProcessor`, subscribes - to a developer-supplied `MessageStore`, and renders the most recent - surface. -- `MessageStore`: an append-only buffer of raw protocol messages the - developer pushes into from any IO transport, such as fetch, SSE, - WebSocket, or an in-process mock. -- `defineCatalog`, `mergeCatalogs`, `serializeCatalog`, and - `defineFunction`: the catalog API. There is no global component catalog; - every consumer composes the component and function entries it wants. -- `catalog/`: built-in component renderers (`Text`, `Image`, `Row`, - `Column`, `List`, `Card`, `Modal`, `Button`, `Divider`, `Icon`, - `CheckBox`, `ChoicePicker`, `DateTimeInput`, `LineChart`, `PieChart`, - `RadioGroup`, `Slider`, `TextField`, and `Tabs`). -- `catalog//catalog.json`: per-component JSON-Schema manifests for - Agent handshakes. -- `basicFunctions`: A2UI v0.9 basic-catalog client function entries, ready - to spread into `catalogs`. - -## Exports - -- `@lynx-js/genui/a2ui`: ``, `createMessageStore`, - `defineCatalog`, built-ins, basic functions, and protocol types. -- `@lynx-js/genui/a2ui/catalog`: re-exports of the catalog API and - built-ins for tree-shake-friendly subpath access. -- `@lynx-js/genui/a2ui/catalog/`: import a single built-in. -- `@lynx-js/genui/a2ui/catalog//catalog.json`: import the - per-component manifest. -- `@lynx-js/genui/a2ui/store`: `MessageStore`, `MessageProcessor`, - `Resource`, payload normalizers — the pure data layer. -- `@lynx-js/genui/a2ui/react`: helpers used by custom catalog - components, including `NodeRenderer`, `useAction`, `useDataBinding`, and - `useChecks`. -- `@lynx-js/genui/a2ui/functions`: basic-catalog function entries and - registration helpers. -- `@lynx-js/genui/a2ui/styles/theme.css`: optional default CSS tokens - for `.a2ui-light` and `.a2ui-dark`. - -## `` lifecycle notes - -- It owns its own `MessageProcessor` per mount. Passing a different - `messageStore` instance does not reset internal state; use a `key` derived - from your turn or session id when you want a fresh session. -- `onAction` is fire-and-forget. The renderer does not wait for a response. - Your Agent pushes follow-up messages back into the same `MessageStore`. -- `className` applies to the surface root view (`surface-${surfaceId}`). -- `wrapSurface` applies an outer wrapper around the rendered surface. -- `className` and `wrapSurface` can both support theme switching; choose the - layer that matches your styling strategy. diff --git a/packages/genui/a2ui/docs/architecture_zh.md b/packages/genui/a2ui/docs/architecture_zh.md deleted file mode 100644 index 5ce52c8e29..0000000000 --- a/packages/genui/a2ui/docs/architecture_zh.md +++ /dev/null @@ -1,63 +0,0 @@ -# 架构和 exports - -这篇文档面向已经完成 quick start、并希望理解 A2UI stack 各部分职责边界的 -开发者。 - -## 职责划分 - -| 部分 | 运行位置 | 职责 | -| -------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Agent 服务 | Server | 把用户 prompt 和客户端 action 转换成经过校验的 A2UI message 数组。它应该使用与客户端可渲染能力一致的 catalog contract 来提示模型。 | -| 传输适配层 | Client shell | 通过 REST、SSE、WebSocket 或其他传输方式把 prompt/action 发给 Agent 服务,再把返回的 messages 写入 `MessageStore`。 | -| `MessageStore` | Client | 按到达顺序保存原始 A2UI protocol messages,并通知订阅者。它不解析也不解释协议语义。 | -| `` | Client | 每次 mount 拥有一个 `MessageProcessor`,从 `MessageStore` 消费新 messages,渲染当前 active surface,并通过 `onAction` 把 generated UI action 转发出去。 | -| Catalog API | Client 和 Agent handshake | 把协议中的 component/function 名称映射到本地实现和可选 JSON schema。使用 `defineCatalog`、`mergeCatalogs`、`serializeCatalog` 和 `defineFunction` 组合这份 contract。 | -| 内置能力 | Client | 提供 A2UI v0.9 basic catalog 的组件 renderer、JSON-Schema manifest,以及客户端 basic-catalog function 实现。 | -| `genui a2ui` | 构建/接入阶段 | 生成自定义 catalog artifacts 和 system prompt。如果 Agent 和 renderer 都使用内置 basic catalog,则不需要它。 | - -## 包含内容 - -- ``:all-in-one 组件。它拥有 `MessageProcessor`,订阅开发者传入的 - `MessageStore`,并渲染最新的 surface。 -- `MessageStore`:原始 protocol messages 的 append-only buffer。开发者可以从 - fetch、SSE、WebSocket、in-process mock 等任意 IO 传输层写入 messages。 -- `defineCatalog`、`mergeCatalogs`、`serializeCatalog` 和 `defineFunction`: - catalog API。这里没有全局 component catalog;每个消费者都要显式组合自己 - 想开放的 component 和 function entries。 -- `catalog/`:内置组件 renderers(`Text`、`Image`、`Row`、`Column`、 - `List`、`Card`、`Modal`、`Button`、`Divider`、`Icon`、`CheckBox`、 - `ChoicePicker`、`DateTimeInput`、`LineChart`、`PieChart`、`RadioGroup`、 - `Slider`、`TextField` 和 `Tabs`)。 -- `catalog//catalog.json`:用于 Agent handshake 的逐组件 JSON-Schema - manifests。 -- `basicFunctions`:A2UI v0.9 basic-catalog 的客户端 function entries,可以 - 直接展开到 `catalogs` 中。 - -## Exports - -- `@lynx-js/genui/a2ui`:``、`createMessageStore`、 - `defineCatalog`、内置组件、basic functions 和 protocol types。 -- `@lynx-js/genui/a2ui/catalog`:catalog API 和内置组件的 re-export, - 适合 tree-shake-friendly 的 subpath 访问。 -- `@lynx-js/genui/a2ui/catalog/`:导入单个内置组件。 -- `@lynx-js/genui/a2ui/catalog//catalog.json`:导入单个组件的 - manifest。 -- `@lynx-js/genui/a2ui/store`:`MessageStore`、`MessageProcessor`、 - `Resource`、payload normalizers 等纯数据层能力。 -- `@lynx-js/genui/a2ui/react`:自定义 catalog 组件会用到的 helper,包括 - `NodeRenderer`、`useAction`、`useDataBinding` 和 `useChecks`。 -- `@lynx-js/genui/a2ui/functions`:basic-catalog function entries 和注册 - helper。 -- `@lynx-js/genui/a2ui/styles/theme.css`:可选的默认 CSS tokens,提供 - `.a2ui-light` 和 `.a2ui-dark`。 - -## `` 生命周期说明 - -- 每次 mount 都拥有自己的 `MessageProcessor`。传入另一个 `messageStore` 实例 - 不会重置内部状态;如果你想开启新的 session/turn,请使用由 turn/session id - 派生出的 `key`。 -- `onAction` 是 fire-and-forget。renderer 不等待响应;你的 Agent 把后续 - messages 写回同一个 `MessageStore`。 -- `className` 会加在 surface root view(`surface-${surfaceId}`)上。 -- `wrapSurface` 会在渲染出的 surface 外面包一层。 -- 两者都可以用于多主题切换;选择与你的样式策略匹配的那一层。 diff --git a/packages/genui/a2ui/docs/catalog-guide.md b/packages/genui/a2ui/docs/catalog-guide.md new file mode 100644 index 0000000000..503a429308 --- /dev/null +++ b/packages/genui/a2ui/docs/catalog-guide.md @@ -0,0 +1,399 @@ +# Catalogs, built-ins, and custom components + +A **catalog** is the contract between your Agent and your client: it lists +the components the renderer is allowed to instantiate and the functions it +can execute, and it carries the optional JSON schemas an Agent reads during +a handshake. This guide covers everything about composing that contract — +from the one-line minimum, through the built-in component set, to shipping +your own components and generating their schemas. + +If you only skim one section, make it [The built-in components](#the-built-in-components) and [Basic-catalog functions](#basic-catalog-functions): those are the vocabulary your Agent +gets to work with. + +## What a catalog is + +A catalog has two kinds of entries: + +- **Components** — map a protocol name (`"Text"`, `"Card"`, your + `"MyChart"`) to a ReactLynx component the client renders. +- **Functions** — map a protocol function name (`"formatDate"`, + `"required"`) to a client-side implementation the renderer calls while + resolving props, actions, and validation checks. + +You build one with `defineCatalog`, then either pass it (or a raw input +array) to `` for rendering, or run it through +`serializeCatalog` to announce the contract to your Agent. + +A component's **protocol name** comes from `displayName ?? component.name`, +unless you pair it with a manifest — in which case the manifest's top-level +key is authoritative. The name is the only thing the Agent references, so +keeping it stable matters (see the minifier warning below). + +## Start small: renderer-only components + +If your app only needs to render, pass bare components. No schemas, no +ceremony. + +```ts +import { defineCatalog, Text, Button } from '@lynx-js/genui/a2ui'; + +const catalog = defineCatalog([Text, Button]); +``` + +You can pass the same array straight to `` without calling +`defineCatalog` yourself — the component composes it internally: + +```tsx +; +``` + +Bundlers tree-shake unused components: pulling in `Text` does **not** drag +`Button`, `Card`, or any other built-in into your bundle. + +> ⚠️ **Production minifiers rename function declarations**, which breaks the +> `component.name` fallback. For production safety, either set an explicit +> `displayName` on every custom component (the string literal survives +> minification) or pair the component with its `catalog.json` manifest using +> the tuple form — the manifest key is authoritative and immune to +> minification. + +## The built-in components + +The package ships 20 A2UI v0.9 basic-catalog renderers. Each is an +independent, tree-shakeable export, available from the root or from +`@lynx-js/genui/a2ui/catalog/`. + +**Layout and containers** + +| Component | What it renders | +| --------- | ------------------------------------------------------------------ | +| `Row` | A horizontal layout container for a `children` list. | +| `Column` | A vertical layout container for a `children` list. | +| `Card` | A padded, elevated surface that wraps a single `child`. | +| `List` | A scrollable collection — the usual target for templated children. | +| `Tabs` | Tabbed sections that switch between child views. | +| `Modal` | An overlay/dialog surface layered above the rest of the UI. | +| `Divider` | A thin separator rule between items. | + +**Content** + +| Component | What it renders | +| --------- | -------------------------------------------------------------- | +| `Text` | A string of text, with a `variant` such as `body` for styling. | +| `Image` | An image from a source URL. | +| `Icon` | A named icon glyph. | + +**Input and actions** + +| Component | What it renders | +| --------------- | ------------------------------------------------- | +| `Button` | A tappable button that dispatches a user action. | +| `TextField` | A single-line text input bound to the data model. | +| `CheckBox` | A boolean toggle. | +| `RadioGroup` | A single-choice set of radio options. | +| `ChoicePicker` | A picker/select for choosing among options. | +| `Slider` | A numeric value selected along a range. | +| `DateTimeInput` | A date and/or time input. | + +**Data visualization** + +| Component | What it renders | +| ----------- | ------------------------------------------ | +| `LineChart` | A line chart over a series of data points. | +| `PieChart` | A pie chart over a set of values. | + +**Feedback** + +| Component | What it renders | +| --------- | ----------------------------------------------------------------------------------------------- | +| `Loading` | A loading indicator. Also the default `renderFallback` for `` while a surface is pending. | + +To learn the exact props each one accepts, read its manifest at +`@lynx-js/genui/a2ui/catalog//catalog.json` — that JSON is the same +schema the Agent sees. + +## Adding manifests for Agent handshakes + +If you want `serializeCatalog(...)` to emit JSON Schema for each component +(so the Agent knows which props to send), pair each component with the JSON +generated at `dist/catalog//catalog.json` using the tuple form: + +```ts +import { Text, defineCatalog, serializeCatalog } from '@lynx-js/genui/a2ui'; +import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' + with { type: 'json' }; + +const catalog = defineCatalog([[Text, textManifest]]); +agentChannel.handshake({ catalog: serializeCatalog(catalog) }); +``` + +The protocol name lives in the JSON as the top-level key, so the runtime +never duplicates it. Components you register without a manifest still +render fine — they just serialize to `{ name }` only, which tells the Agent +the component exists without describing its props. + +## Basic-catalog functions + +A2UI messages can embed function calls in dynamic props, action payloads, +and validation checks — for example `{ call: 'formatDate', args: { … } }`. +These run **on the client at render time**. To make them available, spread +`...basicFunctions` into the same catalog input list: + +```ts +import { Text, basicFunctions, defineCatalog } from '@lynx-js/genui/a2ui'; + +const catalog = defineCatalog([Text, ...basicFunctions]); +``` + +`basicFunctions` is an array of ready-made entries whose implementations +come straight from the upstream `@a2ui/web_core` basic catalog, so the wire +contract stays aligned with the A2UI v0.9 spec for free. It covers 25 +functions: + +| Category | Functions (protocol names) | +| ---------- | --------------------------------------------------------------------------- | +| Arithmetic | `add`, `subtract`, `multiply`, `divide` | +| Comparison | `equals`, `not_equals`, `greater_than`, `less_than` | +| Logic | `and`, `or`, `not` | +| Text | `contains`, `starts_with`, `ends_with`, `length` | +| Validation | `required`, `regex`, `numeric`, `email` | +| Formatting | `formatString`, `formatNumber`, `formatCurrency`, `formatDate`, `pluralize` | +| Action | `openUrl` | + +> Note the mixed casing — comparison/text helpers use `snake_case` +> (`not_equals`, `starts_with`) while formatters use `camelCase` +> (`formatDate`, `openUrl`). These are the upstream A2UI v0.9 names; use +> them verbatim in messages. + +Include `...basicFunctions` whenever your Agent might emit any of these. If +a message references a function the catalog does not contain, that call +resolves to `undefined` rather than throwing. + +If you build your own renderer instead of using ``, call +`registerBasicFunctions()` once to register the same implementations into +the shared `functionRegistry`. + +## Why there is no `catalog/all` + +The package intentionally does **not** ship an "all-in-one" catalog +constant or a `@lynx-js/genui/a2ui/catalog/all` export. A single top-level +array referencing every built-in would defeat tree-shaking — every consumer +of that aggregate would bundle every component, even the ones they never +render. Composition is per-component, and the bundle cost stays visible at +the import site. + +## The paste-able "every built-in" recipe + +When you genuinely want all of them, keep the list at the integration site. +This composes every built-in component with its manifest, plus the basic +functions: + +```tsx +import { + basicFunctions, + defineCatalog, + Button, + Card, + CheckBox, + ChoicePicker, + Column, + DateTimeInput, + Divider, + Icon, + Image, + LineChart, + List, + Loading, + Modal, + PieChart, + RadioGroup, + Row, + Slider, + Tabs, + Text, + TextField, +} from '@lynx-js/genui/a2ui'; +import buttonManifest from '@lynx-js/genui/a2ui/catalog/Button/catalog.json' with { + type: 'json', +}; +import cardManifest from '@lynx-js/genui/a2ui/catalog/Card/catalog.json' with { + type: 'json', +}; +import checkBoxManifest from '@lynx-js/genui/a2ui/catalog/CheckBox/catalog.json' with { + type: 'json', +}; +import choicePickerManifest from '@lynx-js/genui/a2ui/catalog/ChoicePicker/catalog.json' with { + type: 'json', +}; +import columnManifest from '@lynx-js/genui/a2ui/catalog/Column/catalog.json' with { + type: 'json', +}; +import dateTimeInputManifest from '@lynx-js/genui/a2ui/catalog/DateTimeInput/catalog.json' with { + type: 'json', +}; +import dividerManifest from '@lynx-js/genui/a2ui/catalog/Divider/catalog.json' with { + type: 'json', +}; +import iconManifest from '@lynx-js/genui/a2ui/catalog/Icon/catalog.json' with { + type: 'json', +}; +import imageManifest from '@lynx-js/genui/a2ui/catalog/Image/catalog.json' with { + type: 'json', +}; +import lineChartManifest from '@lynx-js/genui/a2ui/catalog/LineChart/catalog.json' with { + type: 'json', +}; +import listManifest from '@lynx-js/genui/a2ui/catalog/List/catalog.json' with { + type: 'json', +}; +import loadingManifest from '@lynx-js/genui/a2ui/catalog/Loading/catalog.json' with { + type: 'json', +}; +import modalManifest from '@lynx-js/genui/a2ui/catalog/Modal/catalog.json' with { + type: 'json', +}; +import pieChartManifest from '@lynx-js/genui/a2ui/catalog/PieChart/catalog.json' with { + type: 'json', +}; +import radioGroupManifest from '@lynx-js/genui/a2ui/catalog/RadioGroup/catalog.json' with { + type: 'json', +}; +import rowManifest from '@lynx-js/genui/a2ui/catalog/Row/catalog.json' with { + type: 'json', +}; +import sliderManifest from '@lynx-js/genui/a2ui/catalog/Slider/catalog.json' with { + type: 'json', +}; +import tabsManifest from '@lynx-js/genui/a2ui/catalog/Tabs/catalog.json' with { + type: 'json', +}; +import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' with { + type: 'json', +}; +import textFieldManifest from '@lynx-js/genui/a2ui/catalog/TextField/catalog.json' with { + type: 'json', +}; + +export const allBuiltins = defineCatalog([ + [Text, textManifest], + [Image, imageManifest], + [Row, rowManifest], + [Column, columnManifest], + [List, listManifest], + [Card, cardManifest], + [Modal, modalManifest], + [Button, buttonManifest], + [Divider, dividerManifest], + [LineChart, lineChartManifest], + [PieChart, pieChartManifest], + [TextField, textFieldManifest], + [CheckBox, checkBoxManifest], + [ChoicePicker, choicePickerManifest], + [DateTimeInput, dateTimeInputManifest], + [Icon, iconManifest], + [RadioGroup, radioGroupManifest], + [Slider, sliderManifest], + [Tabs, tabsManifest], + [Loading, loadingManifest], + ...basicFunctions, +]); +``` + +Drop the manifest import and tuple form for any component whose schema you +do not need to send to the Agent — `defineCatalog([Text, Button])` is +perfectly valid. Keep `...basicFunctions` if your messages use function +calls. + +## Custom components + +A catalog component is _anything_ that takes a single props object and +returns a `ReactNode`. Its function name — or its `displayName` — is the +protocol name the Agent will use: + +```tsx +function MyChart(props: { data: number[] }) { /* … */ } +// Required for production-safe naming: minifiers rewrite `function` names, +// but the `displayName` string literal survives. +MyChart.displayName = 'MyChart'; + +; +// Agent emits `{ component: 'MyChart', data: [...] }` → renders MyChart. +``` + +Custom components receive runtime-shaped props from the protocol stream. For +anything beyond a leaf component, reach into `@lynx-js/genui/a2ui/react`, +the contract custom components plug into: + +- **`useDataBinding`** — resolve a bound value (`{ path }`) against the + surface's data model and get a setter back, so inputs can write user edits + into the model. +- **`useResolvedProps`** — resolve a whole prop bag at once, expanding data + bindings and function calls into concrete values. +- **`useAction`** — turn a protocol action into a `sendAction` callback you + fire on tap/submit; the result loops back out through ``. +- **`useChecks`** — evaluate validation checks (built from basic functions + like `required`/`email`) and report pass/fail with messages. +- **`NodeRenderer`** — render child component ids against the same surface, + the same way the built-ins render their children. + +## Generating a manifest for a custom component + +If the Agent needs to know a custom component's props, generate a manifest +and pair it the same way you would a built-in. + +1. Describe the props as a TypeScript `interface` and annotate it with the + `@a2uiCatalog ` JSDoc tag so the extractor picks it up. +2. Run the public CLI to emit the JSON: + + ```bash + genui a2ui generate catalog --catalog-dir src/catalog --out-dir dist/catalog + ``` + +3. Pair the generated JSON with the component: + + ```tsx + import myChartManifest from './dist/catalog/MyChart/catalog.json' + with { type: 'json' }; + + const catalog = defineCatalog([[MyChart, myChartManifest]]); + ``` + +`genui a2ui generate catalog` is the user-facing command; +`@lynx-js/genui/a2ui-catalog-extractor` is the TypeDoc-powered engine behind +it. Two constraints to keep extraction happy: the component folder name must +match the exported function name (`src/catalog/MyChart/index.tsx` exports +`function MyChart`), and framework-level props are excluded from the emitted +schema. See the +[extractor README](../../a2ui-catalog-extractor/README.md) for details. + +## Catalog API reference + +All of these are exported from `@lynx-js/genui/a2ui` (and from the +`/catalog` subpath). + +- **`defineCatalog(inputs)`** — builds the runtime catalog. `inputs` is an + array that can mix bare components, `[component, manifest]` tuples, + already-resolved entries (e.g. from `mergeCatalogs`), and function + entries. Duplicate names within the same kind are rejected. Function + entries register their impls into `functionRegistry` immediately, so any + `executeFunctionCall` after `defineCatalog` can route to them. +- **`mergeCatalogs(...catalogs)`** — merges catalogs with **last-write-wins** + on duplicate names. Useful for layering: a page catalog overrides a brand + catalog which overrides the built-ins. +- **`serializeCatalog(catalog)`** — emits the JSON manifest for the Agent + handshake. Components without an attached schema serialize to `{ name }` + only; functions serialize with their parameter schema when available. +- **`resolveCatalog(catalog)`** — returns a `name → component` map. The + renderer uses it internally to resolve `{ component: 'Text' }`; exposed for + advanced cases. +- **`defineFunction(impl, manifest?)`** — wraps a function implementation + into a catalog entry. With a manifest, the name comes from the manifest's + key and the schema is announced to the Agent; without one, the name comes + from `impl.displayName ?? impl.name` and the Agent simply won't see the + parameter schema. + +## Where to go next + +- [Overview and architecture](./overview.md) — how a message becomes UI, the + responsibility split, and the export map. +- [Open the A2UI playground](https://lynxjs.org/a2ui) — try it live. diff --git a/packages/genui/a2ui/docs/catalog-guide_zh.md b/packages/genui/a2ui/docs/catalog-guide_zh.md new file mode 100644 index 0000000000..7d24379e70 --- /dev/null +++ b/packages/genui/a2ui/docs/catalog-guide_zh.md @@ -0,0 +1,372 @@ +# Catalogs、内置组件与自定义组件 + +**catalog** 是你的 Agent 和 client 之间的 contract:它列出 renderer 被允许 +实例化的 component、可以执行的 function,并携带 Agent 在 handshake 时读取的 +可选 JSON schema。这篇指南覆盖关于组合这份 contract 的一切——从一行的最小写法, +到内置组件集合,再到发布你自己的组件并生成它们的 schema。 + +如果你只想快速浏览一节,请看[内置组件](#内置组件)和[basic-catalog functions](#basic-catalog-functions):那是你的 Agent 能使用的「词汇表」。 + +## catalog 是什么 + +一个 catalog 有两类 entry: + +- **Component**——把一个协议名(`"Text"`、`"Card"`、你的 `"MyChart"`)映射到 + client 渲染的 ReactLynx 组件。 +- **Function**——把一个协议 function 名(`"formatDate"`、`"required"`)映射到 + 客户端实现,renderer 在解析 props、actions 和 validation checks 时调用它。 + +你用 `defineCatalog` 构建它,然后要么把它(或一个原始 input 数组)传给 +`` 用于渲染,要么用 `serializeCatalog` 把这份 contract +告知你的 Agent。 + +一个组件的**协议名**来自 `displayName ?? component.name`,除非你把它和 manifest +配对——这时 manifest 的顶层 key 是权威的。Agent 只引用这个名字,所以保持它稳定 +很重要(见下面的 minifier 警告)。 + +## 从小开始:renderer-only components + +如果你的应用只需要渲染,直接传 bare components 即可。不需要 schema,也没有任何 +仪式感。 + +```ts +import { defineCatalog, Text, Button } from '@lynx-js/genui/a2ui'; + +const catalog = defineCatalog([Text, Button]); +``` + +你也可以把同一个数组直接传给 `` 而不必自己调用 `defineCatalog`——组件会在 +内部组合它: + +```tsx +; +``` + +打包器会 tree-shake 掉未使用的组件:引入 `Text` **不会**把 `Button`、`Card` 或 +其他任何内置组件拖进你的 bundle。 + +> ⚠️ **生产环境 minifier 会改写 function 声明名**,这会破坏 `component.name` +> 这个回退。为了生产安全,请给每个自定义组件设置显式 `displayName`(字符串 +> 字面量能在 minify 后保留),或用 tuple 形式把组件和它的 `catalog.json` +> manifest 配对——manifest 的 key 是权威的,也不受 minify 影响。 + +## 内置组件 + +这个包提供 20 个 A2UI v0.9 basic-catalog renderer。每个都是独立、可 tree-shake +的导出,既可从根导入,也可从 `@lynx-js/genui/a2ui/catalog/` 导入。 + +**布局与容器** + +| 组件 | 渲染什么 | +| --------- | ------------------------------------------------------ | +| `Row` | 一个用于 `children` 列表的水平布局容器。 | +| `Column` | 一个用于 `children` 列表的垂直布局容器。 | +| `Card` | 一个带 padding、有层次感的 surface,包裹单个 `child`。 | +| `List` | 一个可滚动的集合——通常是 templated children 的目标。 | +| `Tabs` | 在多个子视图之间切换的标签分区。 | +| `Modal` | 一个层叠在其余 UI 之上的 overlay/dialog surface。 | +| `Divider` | item 之间的一条细分隔线。 | + +**内容** + +| 组件 | 渲染什么 | +| ------- | --------------------------------------------- | +| `Text` | 一段文本,带 `variant`(如 `body`)控制样式。 | +| `Image` | 来自 source URL 的图片。 | +| `Icon` | 一个具名 icon 字形。 | + +**输入与动作** + +| 组件 | 渲染什么 | +| --------------- | -------------------------------------------- | +| `Button` | 一个可点击、会 dispatch 用户 action 的按钮。 | +| `TextField` | 一个绑定到 data model 的单行文本输入。 | +| `CheckBox` | 一个布尔开关。 | +| `RadioGroup` | 一组单选的 radio 选项。 | +| `ChoicePicker` | 一个在多个选项中选择的 picker/select。 | +| `Slider` | 在一个范围上选择的数值。 | +| `DateTimeInput` | 一个日期和/或时间输入。 | + +**数据可视化** + +| 组件 | 渲染什么 | +| ----------- | -------------------------- | +| `LineChart` | 基于一系列数据点的折线图。 | +| `PieChart` | 基于一组数值的饼图。 | + +**反馈** + +| 组件 | 渲染什么 | +| --------- | --------------------------------------------------------------------------------- | +| `Loading` | 一个加载指示器。也是 `` 在 surface 处于 pending 时的默认 `renderFallback`。 | + +要了解每个组件接受的确切 props,请阅读它的 manifest +`@lynx-js/genui/a2ui/catalog//catalog.json`——那份 JSON 就是 Agent 看到的 +同一份 schema。 + +## 为 Agent handshake 加入 manifests + +如果你希望 `serializeCatalog(...)` 为每个组件输出 JSON Schema(让 Agent 知道该 +发哪些 props),用 tuple 形式把每个组件和 `dist/catalog//catalog.json` +生成的 JSON 配对: + +```ts +import { Text, defineCatalog, serializeCatalog } from '@lynx-js/genui/a2ui'; +import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' + with { type: 'json' }; + +const catalog = defineCatalog([[Text, textManifest]]); +agentChannel.handshake({ catalog: serializeCatalog(catalog) }); +``` + +协议名作为顶层 key 存在 JSON 里,所以运行时从不重复它。没有 manifest 就注册的 +组件照样能渲染——它们只是序列化成 `{ name }`,告诉 Agent 该组件存在,但不描述 +它的 props。 + +## basic-catalog functions + +A2UI messages 可以在 dynamic props、action payload 和 validation checks 里内嵌 +function call——例如 `{ call: 'formatDate', args: { … } }`。这些在**客户端的渲染 +时刻**运行。要让它们可用,把 `...basicFunctions` 展开进同一个 catalog input 列表: + +```ts +import { Text, basicFunctions, defineCatalog } from '@lynx-js/genui/a2ui'; + +const catalog = defineCatalog([Text, ...basicFunctions]); +``` + +`basicFunctions` 是一组现成 entry 的数组,它们的实现直接来自上游 +`@a2ui/web_core` basic catalog,所以 wire contract 能免费地与 A2UI v0.9 spec +保持一致。它覆盖 25 个 function: + +| 类别 | Functions(协议名) | +| ------ | --------------------------------------------------------------------------- | +| 算术 | `add`、`subtract`、`multiply`、`divide` | +| 比较 | `equals`、`not_equals`、`greater_than`、`less_than` | +| 逻辑 | `and`、`or`、`not` | +| 文本 | `contains`、`starts_with`、`ends_with`、`length` | +| 校验 | `required`、`regex`、`numeric`、`email` | +| 格式化 | `formatString`、`formatNumber`、`formatCurrency`、`formatDate`、`pluralize` | +| 动作 | `openUrl` | + +> 注意大小写不统一——比较/文本类用 `snake_case`(`not_equals`、`starts_with`), +> 而格式化类用 `camelCase`(`formatDate`、`openUrl`)。这些是上游 A2UI v0.9 的 +> 名称;在 message 里请原样使用。 + +只要你的 Agent 可能发出其中任何一个,就加入 `...basicFunctions`。如果某条 +message 引用了 catalog 里没有的 function,那次调用会解析为 `undefined` 而不是 +抛错。 + +如果你构建自己的 renderer 而不用 ``,调用一次 `registerBasicFunctions()` +就能把这些相同的实现注册进共享的 `functionRegistry`。 + +## 为什么没有 `catalog/all` + +这个包刻意**不**提供 all-in-one 的 catalog 常量,也没有 +`@lynx-js/genui/a2ui/catalog/all` 导出。一个引用所有内置组件的顶层数组会破坏 +tree-shaking——任何用到这个聚合的消费者都会打包全部组件,即使它们从不渲染其中 +某些。组合是逐组件的,bundle 成本在 import 处保持可见。 + +## 可粘贴的「全部内置」配方 + +当你确实想要它们全部时,请把列表保留在接入点。下面这段组合了每个内置组件及其 +manifest,外加 basic functions: + +```tsx +import { + basicFunctions, + defineCatalog, + Button, + Card, + CheckBox, + ChoicePicker, + Column, + DateTimeInput, + Divider, + Icon, + Image, + LineChart, + List, + Loading, + Modal, + PieChart, + RadioGroup, + Row, + Slider, + Tabs, + Text, + TextField, +} from '@lynx-js/genui/a2ui'; +import buttonManifest from '@lynx-js/genui/a2ui/catalog/Button/catalog.json' with { + type: 'json', +}; +import cardManifest from '@lynx-js/genui/a2ui/catalog/Card/catalog.json' with { + type: 'json', +}; +import checkBoxManifest from '@lynx-js/genui/a2ui/catalog/CheckBox/catalog.json' with { + type: 'json', +}; +import choicePickerManifest from '@lynx-js/genui/a2ui/catalog/ChoicePicker/catalog.json' with { + type: 'json', +}; +import columnManifest from '@lynx-js/genui/a2ui/catalog/Column/catalog.json' with { + type: 'json', +}; +import dateTimeInputManifest from '@lynx-js/genui/a2ui/catalog/DateTimeInput/catalog.json' with { + type: 'json', +}; +import dividerManifest from '@lynx-js/genui/a2ui/catalog/Divider/catalog.json' with { + type: 'json', +}; +import iconManifest from '@lynx-js/genui/a2ui/catalog/Icon/catalog.json' with { + type: 'json', +}; +import imageManifest from '@lynx-js/genui/a2ui/catalog/Image/catalog.json' with { + type: 'json', +}; +import lineChartManifest from '@lynx-js/genui/a2ui/catalog/LineChart/catalog.json' with { + type: 'json', +}; +import listManifest from '@lynx-js/genui/a2ui/catalog/List/catalog.json' with { + type: 'json', +}; +import loadingManifest from '@lynx-js/genui/a2ui/catalog/Loading/catalog.json' with { + type: 'json', +}; +import modalManifest from '@lynx-js/genui/a2ui/catalog/Modal/catalog.json' with { + type: 'json', +}; +import pieChartManifest from '@lynx-js/genui/a2ui/catalog/PieChart/catalog.json' with { + type: 'json', +}; +import radioGroupManifest from '@lynx-js/genui/a2ui/catalog/RadioGroup/catalog.json' with { + type: 'json', +}; +import rowManifest from '@lynx-js/genui/a2ui/catalog/Row/catalog.json' with { + type: 'json', +}; +import sliderManifest from '@lynx-js/genui/a2ui/catalog/Slider/catalog.json' with { + type: 'json', +}; +import tabsManifest from '@lynx-js/genui/a2ui/catalog/Tabs/catalog.json' with { + type: 'json', +}; +import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' with { + type: 'json', +}; +import textFieldManifest from '@lynx-js/genui/a2ui/catalog/TextField/catalog.json' with { + type: 'json', +}; + +export const allBuiltins = defineCatalog([ + [Text, textManifest], + [Image, imageManifest], + [Row, rowManifest], + [Column, columnManifest], + [List, listManifest], + [Card, cardManifest], + [Modal, modalManifest], + [Button, buttonManifest], + [Divider, dividerManifest], + [LineChart, lineChartManifest], + [PieChart, pieChartManifest], + [TextField, textFieldManifest], + [CheckBox, checkBoxManifest], + [ChoicePicker, choicePickerManifest], + [DateTimeInput, dateTimeInputManifest], + [Icon, iconManifest], + [RadioGroup, radioGroupManifest], + [Slider, sliderManifest], + [Tabs, tabsManifest], + [Loading, loadingManifest], + ...basicFunctions, +]); +``` + +对任何你不需要把 schema 发给 Agent 的组件,去掉它的 manifest 导入和 tuple +形式即可——`defineCatalog([Text, Button])` 完全有效。如果你的 messages 用到 +function call,就保留 `...basicFunctions`。 + +## 自定义组件 + +catalog 组件可以是_任何_接收单个 props 对象并返回 `ReactNode` 的东西。它的 +function 名——或它的 `displayName`——就是 Agent 会使用的协议名: + +```tsx +function MyChart(props: { data: number[] }) { /* … */ } +// 生产安全所必需:minifier 会改写 `function` 名,但 `displayName` 字符串 +// 字面量会保留。 +MyChart.displayName = 'MyChart'; + +; +// Agent emits `{ component: 'MyChart', data: [...] }` → renders MyChart. +``` + +自定义组件从 protocol stream 收到运行时形状的 props。对于任何超出叶子组件的 +情况,请使用 `@lynx-js/genui/a2ui/react`——这是自定义组件接入的 contract: + +- **`useDataBinding`**——把一个 bound 值(`{ path }`)针对 surface 的 data model + 求值,并拿回一个 setter,这样输入组件就能把用户编辑写回 model。 +- **`useResolvedProps`**——一次性解析整个 prop 包,把 data binding 和 function + call 展开成具体值。 +- **`useAction`**——把一个协议 action 变成你在点击/提交时触发的 `sendAction` + 回调;结果会通过 `` 回流出去。 +- **`useChecks`**——求值 validation checks(由 `required`/`email` 等 basic + function 构建),并以 pass/fail 加消息的形式报告。 +- **`NodeRenderer`**——针对同一个 surface 渲染 child component id,方式和内置 + 组件渲染自己的 children 完全一样。 + +## 为自定义组件生成 manifest + +如果 Agent 需要知道某个自定义组件的 props,生成一个 manifest,并像内置组件那样 +把它配对。 + +1. 把 props 描述为一个 TypeScript `interface`,并用 `@a2uiCatalog ` + JSDoc 标签标注它,让 extractor 能识别它。 +2. 运行公开 CLI 输出 JSON: + + ```bash + genui a2ui generate catalog --catalog-dir src/catalog --out-dir dist/catalog + ``` + +3. 把生成的 JSON 与组件配对: + + ```tsx + import myChartManifest from './dist/catalog/MyChart/catalog.json' + with { type: 'json' }; + + const catalog = defineCatalog([[MyChart, myChartManifest]]); + ``` + +`genui a2ui generate catalog` 是面向用户的命令; +`@lynx-js/genui/a2ui-catalog-extractor` 是它背后由 TypeDoc 驱动的引擎。两条约束 +能让抽取顺利进行:组件文件夹名必须与导出的 function 名一致 +(`src/catalog/MyChart/index.tsx` 导出 `function MyChart`),并且框架级 props +会被排除在产出的 schema 之外。详情见 +[extractor README](../../a2ui-catalog-extractor/readme.zh_cn.md)。 + +## Catalog API 参考 + +下面这些都从 `@lynx-js/genui/a2ui`(以及 `/catalog` subpath)导出。 + +- **`defineCatalog(inputs)`**——构建运行时 catalog。`inputs` 是一个数组,可以 + 混合 bare components、`[component, manifest]` tuple、已解析的 entry(例如来自 + `mergeCatalogs`),以及 function entries。同一类里的重名会被拒绝。function + entry 会立即把它的 impl 注册进 `functionRegistry`,所以 `defineCatalog` 之后 + 的任何 `executeFunctionCall` 都能路由到它们。 +- **`mergeCatalogs(...catalogs)`**——合并多个 catalog,重名时**后写覆盖前写**。 + 适合分层:page catalog 覆盖 brand catalog,brand catalog 覆盖内置。 +- **`serializeCatalog(catalog)`**——为 Agent handshake 输出 JSON manifest。没有 + 附带 schema 的组件序列化成 `{ name }`;function 在有 schema 时带上其参数 + schema 一起序列化。 +- **`resolveCatalog(catalog)`**——返回一个 `name → component` 的 map。renderer + 内部用它来解析 `{ component: 'Text' }`;供高级场景暴露。 +- **`defineFunction(impl, manifest?)`**——把一个 function 实现包装成 catalog + entry。带 manifest 时,名字来自 manifest 的 key,schema 会被告知 Agent;不带 + 时,名字来自 `impl.displayName ?? impl.name`,Agent 就看不到参数 schema。 + +## 下一步 + +- [概览与架构](./overview_zh.md)——一条 message 如何变成 UI、职责划分,以及 + export 映射。 +- [打开 A2UI playground](https://lynxjs.org/a2ui)——在线体验。 diff --git a/packages/genui/a2ui/docs/catalogs.md b/packages/genui/a2ui/docs/catalogs.md deleted file mode 100644 index e88e004d18..0000000000 --- a/packages/genui/a2ui/docs/catalogs.md +++ /dev/null @@ -1,59 +0,0 @@ -# Catalogs and manifests - -Catalogs define which protocol components and functions the renderer can use. -They also provide the optional JSON schemas that an Agent can use during a -handshake. - -## Start with renderer-only components - -If your app only needs to render, pass bare components. The protocol name comes -from `displayName ?? component.name`. - -```ts -import { defineCatalog, Text, Button } from '@lynx-js/genui/a2ui'; - -const catalog = defineCatalog([Text, Button]); -``` - -Production minifiers can rewrite function names. For production safety, set an -explicit `displayName` on every custom component, or pair the component with -its `catalog.json` manifest. The manifest key is authoritative. - -## Add manifests for Agent handshakes - -If you want `serializeCatalog(...)` to emit JSON Schema for each component, -pair each component with the JSON emitted at `dist/catalog//catalog.json`. - -```ts -import { Text, defineCatalog, serializeCatalog } from '@lynx-js/genui/a2ui'; -import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' - with { type: 'json' }; - -const catalog = defineCatalog([[Text, textManifest]]); -agentChannel.handshake({ catalog: serializeCatalog(catalog) }); -``` - -## Include basic functions when messages use function calls - -A2UI messages may use basic-catalog function calls such as `formatDate`, -`formatString`, or `required`. Include `...basicFunctions` in the same catalog -input list so the client can execute those calls at render time. - -```ts -import { Text, basicFunctions, defineCatalog } from '@lynx-js/genui/a2ui'; - -const catalog = defineCatalog([ - Text, - ...basicFunctions, -]); -``` - -## No `catalog/all` - -The package intentionally does not ship a `catalog/all` aggregate. A single -top-level array that references every component forces consumers to bundle -every built-in even when they render only a few. Compose the catalog at the -call site so the bundle cost is explicit. - -For the complete "every built-in" recipe, see -[../src/catalog/README.md](../src/catalog/README.md). diff --git a/packages/genui/a2ui/docs/catalogs_zh.md b/packages/genui/a2ui/docs/catalogs_zh.md deleted file mode 100644 index d0bb65ef4e..0000000000 --- a/packages/genui/a2ui/docs/catalogs_zh.md +++ /dev/null @@ -1,57 +0,0 @@ -# Catalogs 和 manifests - -Catalog 定义 renderer 可以使用哪些 protocol components 和 functions。它也可以 -提供 Agent handshake 所需的 JSON schemas。 - -## 从 renderer-only components 开始 - -如果你的应用只负责渲染,可以直接传 bare components。协议名来自 -`displayName ?? component.name`。 - -```ts -import { defineCatalog, Text, Button } from '@lynx-js/genui/a2ui'; - -const catalog = defineCatalog([Text, Button]); -``` - -生产环境 minifier 可能改写 function 名称。为了生产安全,请给每个自定义组件 -设置显式 `displayName`,或者把组件与它的 `catalog.json` manifest 配对使用。 -manifest 的顶层 key 是权威名称。 - -## 为 Agent handshake 加入 manifests - -如果你希望 `serializeCatalog(...)` 为每个组件输出 JSON Schema,请把组件和 -`dist/catalog//catalog.json` 产出的 JSON 配对: - -```ts -import { Text, defineCatalog, serializeCatalog } from '@lynx-js/genui/a2ui'; -import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' - with { type: 'json' }; - -const catalog = defineCatalog([[Text, textManifest]]); -agentChannel.handshake({ catalog: serializeCatalog(catalog) }); -``` - -## Messages 使用 function calls 时加入 basic functions - -A2UI messages 可能使用 `formatDate`、`formatString` 或 `required` 这样的 -basic-catalog function calls。请在同一个 catalog input 列表里加入 -`...basicFunctions`,这样客户端才能在渲染时执行这些 function。 - -```ts -import { Text, basicFunctions, defineCatalog } from '@lynx-js/genui/a2ui'; - -const catalog = defineCatalog([ - Text, - ...basicFunctions, -]); -``` - -## 没有 `catalog/all` - -这个包故意不提供 `catalog/all` 聚合导出。一个引用所有组件的顶层数组会迫使 -消费者打包全部内置组件,即使实际只渲染其中几个。请在接入点显式组合 catalog, -让 bundle 成本保持可见。 - -完整 “every built-in” 配方见 -[../src/catalog/readme_zh.md](../src/catalog/readme_zh.md)。 diff --git a/packages/genui/a2ui/docs/custom-components.md b/packages/genui/a2ui/docs/custom-components.md deleted file mode 100644 index 941e972225..0000000000 --- a/packages/genui/a2ui/docs/custom-components.md +++ /dev/null @@ -1,34 +0,0 @@ -# Custom components - -Any function returning a `ReactNode` can be a catalog component. The function's -name, or its `displayName`, is the protocol name the Agent will use. - -```tsx -function MyChart(props: { data: number[] }) { ... } -MyChart.displayName = 'MyChart'; - -; -// Agent emits `{ component: 'MyChart', data: [...] }` -> renders MyChart. -``` - -Set `displayName` for production-safe naming. Minifiers can rewrite function -names, but string literals survive. - -## Add schema introspection - -If the Agent needs to know the component's props, generate a manifest with -`@lynx-js/genui/a2ui-catalog-extractor` and pair it with the component. - -```tsx -import myChartManifest from './dist/catalog/MyChart/catalog.json' with { - type: 'json', -}; - -const catalog = defineCatalog([ - [MyChart, myChartManifest], -]); -``` - -Use `@a2uiCatalog ` on the TypeScript interface that describes -the props you want the Agent to see. For extractor details, see -[`@lynx-js/genui/a2ui-catalog-extractor`](../../a2ui-catalog-extractor/README.md). diff --git a/packages/genui/a2ui/docs/custom-components_zh.md b/packages/genui/a2ui/docs/custom-components_zh.md deleted file mode 100644 index 554f5a7fd5..0000000000 --- a/packages/genui/a2ui/docs/custom-components_zh.md +++ /dev/null @@ -1,34 +0,0 @@ -# 自定义组件 - -任何返回 `ReactNode` 的函数都可以成为 catalog component。函数名,或它的 -`displayName`,就是 Agent 会使用的协议名。 - -```tsx -function MyChart(props: { data: number[] }) { ... } -MyChart.displayName = 'MyChart'; - -; -// Agent emits `{ component: 'MyChart', data: [...] }` -> renders MyChart. -``` - -为了生产安全,请设置 `displayName`。Minifier 可能改写 function 名称,但字符串 -字面量会保留下来。 - -## 加入 schema introspection - -如果 Agent 需要知道组件 props,请使用 `@lynx-js/genui/a2ui-catalog-extractor` 生成 -manifest,并把 manifest 与组件配对。 - -```tsx -import myChartManifest from './dist/catalog/MyChart/catalog.json' with { - type: 'json', -}; - -const catalog = defineCatalog([ - [MyChart, myChartManifest], -]); -``` - -在描述 Agent 可见 props 的 TypeScript interface 上使用 -`@a2uiCatalog `。Extractor 详情见 -[`@lynx-js/genui/a2ui-catalog-extractor`](../../a2ui-catalog-extractor/readme.zh_cn.md)。 diff --git a/packages/genui/a2ui/docs/overview.md b/packages/genui/a2ui/docs/overview.md new file mode 100644 index 0000000000..9e36fda940 --- /dev/null +++ b/packages/genui/a2ui/docs/overview.md @@ -0,0 +1,310 @@ +# Overview and architecture + +This page explains what `@lynx-js/genui/a2ui` is, the mental model behind +it, and how a server message becomes a rendered UI on the client. It +opens with a runnable [quick start](#quick-start), then works through +the architecture so you understand which part of the stack owns each +responsibility — and why the package is shaped the way it is. + +## What this package is + +`@lynx-js/genui/a2ui` is the ReactLynx **client runtime** for the A2UI +v0.9 protocol. It consumes validated server-to-client JSON messages and +renders trusted ReactLynx components inside your app. + +It is deliberately a renderer and nothing more. The package does **not**: + +- host an Agent or call an LLM, +- own a backend route or chat shell, +- decide _what_ to render — that is the Agent's job. + +Your app owns the transport layer and pushes messages into the renderer. +Use this package when you already have, or plan to build, an Agent service +that returns A2UI messages. + +## Quick start + +Install the package in a ReactLynx app, then render a `MessageStore` with +``. Your transport writes the Agent's messages into the store; the +renderer turns them into UI and hands user actions back through `onAction`. + +```sh +pnpm add @lynx-js/genui @lynx-js/react +``` + +```tsx +import { + A2UI, + basicFunctions, + Button, + createMessageStore, + normalizePayloadToMessages, + Text, +} from '@lynx-js/genui/a2ui'; + +// 1. A buffer your transport writes raw protocol messages into. +const store = createMessageStore(); + +// 2. The components and functions generated UI is allowed to use. +const catalogs = [Text, Button, ...basicFunctions]; + +// 3. Send a prompt and push the Agent's reply into the store. +async function sendPrompt(input: string) { + const res = await fetch('/a2ui/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: [{ role: 'user', content: input }] }), + }); + store.push(normalizePayloadToMessages(await res.json())); +} + +// 4. Render. onAction round-trips user taps back to the Agent. + { + void fetch('/a2ui/action', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(action), + }) + .then((res) => res.json()) + .then((payload) => store.push(normalizePayloadToMessages(payload))); + }} +/>; +``` + +That is the entire client loop: **push messages in, render, send actions +out.** The transport can be REST, SSE, WebSocket, or an in-process mock — +the renderer does not care. The rest of this page explains what happens +between `store.push(...)` and the rendered surface. For install details and +optional theme tokens, see the [README](../README.md). + +## The mental model + +If you have written React, the shift is small but important: + +- In **React**, your code chooses components and passes props. +- In **A2UI**, an _Agent_ chooses from a component catalog that _your app_ + publishes, and sends data describing which approved component to render + and with what props. + +The model never ships executable code. It selects a `component` name and a +prop bag from a contract you defined up front. The client looks that name up +in the catalog and renders the real ReactLynx component you registered. + +```text +Agent output (data, not code): Your catalog (code, trusted): + { component: "Card", Card -> (you wrote this) + child: "t1" } Text -> (you wrote this) + { component: "Text", id: "t1", + text: "Hello" } Result: Hello +``` + +The result is not arbitrary generated markup. It is a ReactLynx UI tree +assembled from a trusted catalog — which is what makes generated UI safe to +mount in a production app. An Agent can only reach the components and +functions you put in the catalog; anything else it emits renders nothing. + +## The end-to-end picture + +A2UI is a round trip between a server that decides and a client that +renders. This package is everything inside the **Client** box below. + +```text + ┌─────────────── Your application ───────────────┐ +user │ │ +input ─┼─► Transport ──prompt/action──► Agent service │ + │ adapter (server) │ + │ ▲ │ │ + │ │ A2UI messages (JSON) │ │ + │ └─────────────────────────────┘ │ + │ │ │ + │ ▼ │ + │ MessageStore ──► ──renders──► surface │ + │ (raw buffer) (this package) (UI tree) │ + │ │ │ + │ └─ onAction ─► back to ───┤ + │ (user taps) transport │ + └────────────────────────────────────────────────┘ +``` + +1. The user prompts, or taps something in already-rendered UI. +2. Your **transport adapter** sends that to your **Agent service**. +3. The Agent calls a model with the A2UI system prompt and your catalog + contract, validates the output, and returns A2UI messages. +4. Your adapter writes those messages into a `MessageStore`. +5. `` consumes them, renders the active surface, and forwards any + user actions through `onAction` — which loops back to step 2. + +Because the loop is just "push messages in, get actions out," the transport +can be REST, SSE, WebSocket, or an in-process mock. The renderer does not +care how messages arrive. + +## Who owns what + +The package draws a hard line between what it provides and what your +application provides. Keeping that line crisp is the reason the runtime +stays transport-agnostic and the catalog stays explicit. + +| Piece | Runs in | Owner | Responsibility | +| ----------------- | ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| Agent service | Server | Your application | Turns prompts and client actions into validated A2UI message arrays. Prompts the model with the same catalog contract the client renders. | +| Transport adapter | Client shell | Your application | Sends prompts/actions to the Agent over REST/SSE/WebSocket, then writes the returned messages into a `MessageStore`. | +| `MessageStore` | Client | This package | Stores raw A2UI messages in arrival order and notifies subscribers. It does not parse or interpret the protocol. | +| `` | Client | This package | Owns a `MessageProcessor` per mount, consumes new messages, renders the active surface, and forwards generated UI actions through `onAction`. | +| Catalog API | Client + Agent handshake | This package | Maps protocol component/function names to local implementations and optional JSON schemas. Compose it with `defineCatalog` and friends. | +| Built-ins | Client | This package | A2UI v0.9 basic-catalog component renderers, per-component JSON-Schema manifests, and client-side basic-catalog function implementations. | +| `genui a2ui` | Build / setup time | GenUI CLI | Generates custom catalog artifacts and A2UI system prompts. Not required when both Agent and renderer use the built-in basic catalog. | + +A useful way to remember it: **the server decides, the client renders, and +the catalog is the contract they agree on.** The catalog is the one piece +that lives on both sides of the wire — your client registers +implementations; your Agent receives the serialized schema during the +handshake. + +## Inside the client: how a message becomes UI + +`` is an all-in-one front door, but underneath it the package is three +independently composable layers. Understanding the path a message takes +through them makes the renderer's behavior — and its lifecycle gotchas — +predictable. + +```text +store.push(msg) + │ + ▼ +MessageStore ──subscribe──► ──► MessageProcessor ──► Surface(s) +(raw buffer) (React) (state machine) │ │ + Resource│ │SignalStore + (pending/ (data model, + success/ signal-backed) + error) + │ + ▼ + NodeRenderer walks the tree, + looks each component up in the + catalog, and renders it. +``` + +- **Store layer** (`@lynx-js/genui/a2ui/store`) — pure data logic, no + React. The `MessageStore` is an append-only buffer with a + `useSyncExternalStore`-friendly `subscribe` / `getSnapshot` API. Your + transport calls `store.push(msg)`; the store stays intentionally dumb + about protocol semantics. +- **`MessageProcessor`** — the protocol brain. It owns every `Surface`, + applies `createSurface` / `updateComponents` / `updateDataModel` / + `deleteSurface` into surface state, and emits typed events + (`beginRendering`, `surfaceUpdate`, `deleteSurface`) for the React layer + to consume. `dispatch({ userAction })` fans actions out to listeners. +- **`Resource`** — a `pending → success → error` state machine, one per + surface root and per component instance. Its snapshot reference changes + on every transition so `useSyncExternalStore` never bails out of a + `pending → error` update. +- **`SignalStore`** — a `@preact/signals` wrapper used as the per-surface + data model, addressed with JSON-pointer-style paths. +- **React layer** (`@lynx-js/genui/a2ui/react`) — `` plus + `NodeRenderer` and the hooks (`useAction`, `useDataBinding`, + `useResolvedProps`, `useChecks`) that turn surface state into a ReactLynx + tree. + +A few runtime behaviors worth knowing because they explain things you will +see while building: + +- **Children by reference.** A component instance references children by id + (`child: "text-1"` or `children: ["a", "b"]`). Catalog components render + their child ids by delegating to `` for the same surface. +- **Data binding.** A bound prop is `{ path: string }` resolved against the + surface's `SignalStore`. Relative paths resolve against the component's + `dataContextPath`, which is what makes templates and repeated lists work. +- **Template expansion.** When `updateComponents` carries a "templated + children" placeholder, the processor stores `__template` metadata. When a + later `updateDataModel` fills the bound path, it clones the template + subtree per item, rewrites child ids, and scopes each clone's + `dataContextPath`. This is why components can appear or disappear when + only the data model changes. +- **Actions loop back as messages.** A tap calls `sendAction`; `useAction` + resolves any dynamic values, builds a `UserActionPayload`, and dispatches + it. `` forwards it to your `onAction`. Responses, if any, return as + new protocol messages that you push into the same `MessageStore`. +- **Unknown components fail soft.** A `component` name not in the catalog + logs a warning once per tag and renders `null`, rather than throwing. + +## Package contents + +The building blocks you compose against: + +- **``** — the all-in-one component. It owns a `MessageProcessor`, + subscribes to a developer-supplied `MessageStore`, and renders the most + recent surface. +- **`MessageStore`** — an append-only buffer of raw protocol messages you + push into from any transport: fetch, SSE, WebSocket, or an in-process + mock. +- **Catalog API** — `defineCatalog`, `mergeCatalogs`, `serializeCatalog`, + `resolveCatalog`, and `defineFunction`. There is no global component + registry; every consumer composes the component and function entries it + wants. +- **Built-in components** — 20 A2UI v0.9 basic-catalog renderers (`Text`, + `Image`, `Button`, `Row`, `Column`, `List`, `Loading`, `Card`, `Modal`, + `Divider`, `Icon`, `CheckBox`, `ChoicePicker`, `DateTimeInput`, + `LineChart`, `PieChart`, `RadioGroup`, `Slider`, `TextField`, and + `Tabs`). See the [catalog guide](./catalog-guide.md) for what each one + does. +- **Per-component manifests** — `catalog//catalog.json`, the + JSON-Schema descriptions used during Agent handshakes. +- **`basicFunctions`** — A2UI v0.9 basic-catalog client function entries, + ready to spread into your `catalogs` array. + +## Exports + +The package is split into subpaths so you import only what you use. + +| Import | What you get | +| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `@lynx-js/genui/a2ui` | The main surface: ``, `createMessageStore`, the catalog API, every built-in component, `basicFunctions`, and protocol types. | +| `@lynx-js/genui/a2ui/catalog` | The catalog API and built-ins again, as a tree-shake-friendly subpath. | +| `@lynx-js/genui/a2ui/catalog/` | A single built-in component (e.g. `.../catalog/Text`). | +| `@lynx-js/genui/a2ui/catalog//catalog.json` | That component's JSON-Schema manifest for the handshake. | +| `@lynx-js/genui/a2ui/store` | The pure data layer: `MessageStore`, `MessageProcessor`, `Resource`, `SignalStore`, and the payload normalizers. | +| `@lynx-js/genui/a2ui/react` | The custom-component contract: `NodeRenderer`, `useAction`, `useDataBinding`, `useResolvedProps`, and `useChecks`. | +| `@lynx-js/genui/a2ui/functions` | `basicFunctions` and the `registerBasicFunctions` escape hatch. | +| `@lynx-js/genui/a2ui/styles/theme.css` | Optional default CSS tokens for `.a2ui-light` and `.a2ui-dark`. | + +Most apps only ever import from `@lynx-js/genui/a2ui`. Reach for `/store` and +`/react` when you build custom catalog components or your own renderer. + +## `` props and lifecycle + +`` takes two required props and a set of optional render hooks. + +| Prop | Type | Required | Purpose | +| ------------------- | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- | +| `messageStore` | `MessageStore` | yes | The raw-message buffer your transport pushes into. `` subscribes and processes new tail messages. | +| `catalogs` | `readonly CatalogInput[]` | yes | The components and function entries the renderer is allowed to instantiate. | +| `onAction` | `(action: UserActionPayload) => void` | no | Fired when a user action occurs in the tree. Forward to your Agent; push responses back into the store. | +| `className` | `string` | no | Applied to the surface root view (`surface-${surfaceId}`). Handy for surface-level theme classes. | +| `wrapSurface` | `(children, { surfaceId }) => ReactNode` | no | Wraps each surface so you can apply an outer theme shell or wrapper styles. | +| `renderEmpty` | `() => ReactNode` | no | Rendered before the first `beginRendering` arrives. Defaults to nothing. | +| `renderFallback` | `() => ReactNode` | no | Rendered while the active resource is pending. Defaults to the built-in ``. | +| `renderError` | `(err: unknown) => ReactNode` | no | Rendered when the active resource fails. | +| `renderUnsupported` | `(info) => ReactNode` | no | Rendered for an unsupported component or data syntax. | + +Lifecycle notes that save debugging time: + +- **One processor per mount.** `` creates its `MessageProcessor` + (surfaces, signals, resources) once per mount. Passing a _different_ + `messageStore` instance later does **not** reset internal state. To start + a fresh session or turn, mount with a different `key` derived from your + turn/session id: ``. +- **`onAction` is fire-and-forget.** The renderer never awaits it. Your + Agent pushes follow-up messages back into the same `MessageStore` to + update the UI. +- **`className` vs `wrapSurface`.** Both can drive theme switching; + `className` styles the surface root, `wrapSurface` adds an outer wrapper. + Choose the layer that matches your styling strategy. + +## Where to go next + +- [Catalogs, built-ins, and custom components](./catalog-guide.md) — compose + the contract, add manifests, and register your own components. +- [Open the A2UI playground](https://lynxjs.org/a2ui) — try it live. diff --git a/packages/genui/a2ui/docs/overview_zh.md b/packages/genui/a2ui/docs/overview_zh.md new file mode 100644 index 0000000000..169711c9c6 --- /dev/null +++ b/packages/genui/a2ui/docs/overview_zh.md @@ -0,0 +1,288 @@ +# 概览与架构 + +这篇文档解释 `@lynx-js/genui/a2ui` 是什么、它背后的心智模型,以及一条 +server message 如何在 client 上变成渲染出来的 UI。它以一个可运行的 +[quick start](#quick-start) 开场,然后逐步讲解架构,帮你理解 stack 各部分的 +职责边界,以及这个包为什么设计成现在这样。 + +## 这个包是什么 + +`@lynx-js/genui/a2ui` 是面向 A2UI v0.9 协议的 ReactLynx **客户端运行时**。 +它消费经过校验的 server-to-client JSON messages,并在你的应用中渲染可信的 +ReactLynx 组件。 + +它刻意只做一件事——渲染。这个包**不会**: + +- 托管 Agent,也不调用 LLM; +- 拥有后端路由或 chat shell; +- 决定_渲染什么_——那是 Agent 的职责。 + +你的应用负责传输层,并把 messages 写入 renderer。当你已经有、或准备构建一个 +返回 A2UI messages 的 Agent 服务时,使用这个包。 + +## Quick start + +在 ReactLynx 应用里安装这个包,然后用 `` 渲染一个 `MessageStore`。你的 +传输层把 Agent 的 messages 写入 store;renderer 把它们变成 UI,并通过 +`onAction` 把用户 action 交还给你。 + +```sh +pnpm add @lynx-js/genui @lynx-js/react +``` + +```tsx +import { + A2UI, + basicFunctions, + Button, + createMessageStore, + normalizePayloadToMessages, + Text, +} from '@lynx-js/genui/a2ui'; + +// 1. 一个 buffer,你的传输层把原始 protocol messages 写进它。 +const store = createMessageStore(); + +// 2. 允许 generated UI 使用的 component 和 function。 +const catalogs = [Text, Button, ...basicFunctions]; + +// 3. 发送 prompt,并把 Agent 的回复推进 store。 +async function sendPrompt(input: string) { + const res = await fetch('/a2ui/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: [{ role: 'user', content: input }] }), + }); + store.push(normalizePayloadToMessages(await res.json())); +} + +// 4. 渲染。onAction 把用户点击回传给 Agent。 + { + void fetch('/a2ui/action', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(action), + }) + .then((res) => res.json()) + .then((payload) => store.push(normalizePayloadToMessages(payload))); + }} +/>; +``` + +这就是 client 的全部循环:**把 messages 推进来、渲染、把 actions 抛出去。** +传输层可以是 REST、SSE、WebSocket,或一个 in-process mock——renderer 并不关心。 +本页接下来会讲清楚在 `store.push(...)` 和渲染出的 surface 之间到底发生了什么。 +安装细节和可选的 theme tokens 见 [README](../README_zh.md)。 + +## 心智模型 + +如果你写过 React,这个转变很小但很关键: + +- 在 **React** 里,是你的代码选择组件并传入 props。 +- 在 **A2UI** 里,是 _Agent_ 从_你的应用_发布的组件 catalog 中选择组件, + 并发送数据,告诉渲染器用哪个已授权组件、传哪些 props。 + +模型从不发送可执行代码。它只是从你预先定义好的 contract 中选出一个 +`component` 名称和一组 props。Client 在 catalog 里查到这个名称,渲染你注册的 +真实 ReactLynx 组件。 + +```text +Agent output (data, not code): Your catalog (code, trusted): + { component: "Card", Card -> (you wrote this) + child: "t1" } Text -> (you wrote this) + { component: "Text", id: "t1", + text: "Hello" } Result: Hello +``` + +最终产物不是任意生成的标记,而是由可信 catalog 组装出来的 ReactLynx UI 树—— +这正是 generated UI 能安全地挂载到生产应用里的原因。Agent 只能触及你放进 +catalog 的 component 和 function;它发出的其他任何东西都渲染为空。 + +## 端到端全貌 + +A2UI 是一次往返:server 负责决策,client 负责渲染。这个包就是下图 **Client** +框里的全部内容。 + +```text + ┌─────────────── Your application ───────────────┐ +user │ │ +input ─┼─► Transport ──prompt/action──► Agent service │ + │ adapter (server) │ + │ ▲ │ │ + │ │ A2UI messages (JSON) │ │ + │ └─────────────────────────────┘ │ + │ │ │ + │ ▼ │ + │ MessageStore ──► ──renders──► surface │ + │ (raw buffer) (this package) (UI tree) │ + │ │ │ + │ └─ onAction ─► back to ───┤ + │ (user taps) transport │ + └────────────────────────────────────────────────┘ +``` + +1. 用户输入 prompt,或在已渲染的 UI 上点击某处。 +2. 你的 **transport adapter** 把它发给你的 **Agent 服务**。 +3. Agent 带着 A2UI system prompt 和你的 catalog contract 调用模型,校验输出, + 然后返回 A2UI messages。 +4. 你的 adapter 把这些 messages 写入 `MessageStore`。 +5. `` 消费它们、渲染 active surface,并通过 `onAction` 把用户 action + 抛出——再回到第 2 步。 + +因为整个循环只是「把 messages 推进来、把 actions 抛出去」,传输层可以是 REST、 +SSE、WebSocket,或一个 in-process mock。renderer 不关心 messages 是怎么来的。 + +## 谁负责什么 + +这个包在「它提供什么」和「你的应用提供什么」之间划了一条硬边界。保持这条边界 +清晰,正是运行时保持传输无关、catalog 保持显式的原因。 + +| 部分 | 运行位置 | 负责人 | 职责 | +| ----------------- | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------- | +| Agent 服务 | Server | 你的应用 | 把 prompt 和 client action 转成经过校验的 A2UI message 数组。使用与 client 可渲染能力一致的 catalog contract 提示模型。 | +| Transport adapter | Client shell | 你的应用 | 通过 REST/SSE/WebSocket 把 prompt/action 发给 Agent,再把返回的 messages 写入 `MessageStore`。 | +| `MessageStore` | Client | 这个包 | 按到达顺序保存原始 A2UI messages 并通知订阅者。它不解析也不解释协议语义。 | +| `` | Client | 这个包 | 每次 mount 拥有一个 `MessageProcessor`,消费新 messages,渲染 active surface,并通过 `onAction` 转发 generated UI action。 | +| Catalog API | Client + Agent handshake | 这个包 | 把协议中的 component/function 名称映射到本地实现和可选 JSON schema。用 `defineCatalog` 等组合它。 | +| 内置能力 | Client | 这个包 | A2UI v0.9 basic-catalog 的组件 renderer、逐组件 JSON-Schema manifest,以及客户端 basic-catalog function 实现。 | +| `genui a2ui` | 构建 / 接入阶段 | GenUI CLI | 生成自定义 catalog artifacts 和 A2UI system prompt。当 Agent 和 renderer 都用内置 basic catalog 时不需要它。 | + +一个好记的方式:**server 决策,client 渲染,catalog 是两边达成一致的 +contract。** catalog 是唯一同时活在 wire 两侧的部分——你的 client 注册实现, +你的 Agent 在 handshake 时收到序列化后的 schema。 + +## client 内部:一条 message 如何变成 UI + +`` 是一个 all-in-one 的入口,但它底下其实是三个可独立组合的 layer。理解 +一条 message 在它们之间走过的路径,能让 renderer 的行为——以及它的生命周期 +坑——变得可预测。 + +```text +store.push(msg) + │ + ▼ +MessageStore ──subscribe──► ──► MessageProcessor ──► Surface(s) +(raw buffer) (React) (state machine) │ │ + Resource│ │SignalStore + (pending/ (data model, + success/ signal-backed) + error) + │ + ▼ + NodeRenderer walks the tree, + looks each component up in the + catalog, and renders it. +``` + +- **Store layer**(`@lynx-js/genui/a2ui/store`)——纯数据逻辑,没有 React。 + `MessageStore` 是一个 append-only buffer,带有对 `useSyncExternalStore` + 友好的 `subscribe` / `getSnapshot` API。你的传输层调用 `store.push(msg)`; + store 刻意对协议语义保持「无知」。 +- **`MessageProcessor`**——协议大脑。它拥有每一个 `Surface`,把 + `createSurface` / `updateComponents` / `updateDataModel` / `deleteSurface` + 应用进 surface 状态,并发出带类型的事件(`beginRendering`、`surfaceUpdate`、 + `deleteSurface`)供 React layer 消费。`dispatch({ userAction })` 把 action + 分发给监听者。 +- **`Resource`**——一个 `pending → success → error` 状态机,每个 surface root + 和每个 component 实例各一个。它的 snapshot 引用在每次状态转换时都会改变, + 这样 `useSyncExternalStore` 永远不会在 `pending → error` 更新上「bail out」。 +- **`SignalStore`**——一个 `@preact/signals` 封装,作为每个 surface 的 data + model,用 JSON-pointer 风格的 path 寻址。 +- **React layer**(`@lynx-js/genui/a2ui/react`)——`` 加上 + `NodeRenderer` 和那些把 surface 状态变成 ReactLynx 树的 hooks(`useAction`、 + `useDataBinding`、`useResolvedProps`、`useChecks`)。 + +有几个运行时行为值得了解,因为它们能解释你在开发中会看到的现象: + +- **children 通过引用。** 一个 component 实例用 id 引用 children + (`child: "text-1"` 或 `children: ["a", "b"]`)。catalog 组件通过对同一个 + surface 委托 `` 来渲染它的 child id。 +- **data binding。** 一个 bound prop 是 `{ path: string }`,它针对 surface 的 + `SignalStore` 求值。相对 path 针对组件的 `dataContextPath` 求值——这正是 + templates 和重复列表能工作的原因。 +- **template 展开。** 当 `updateComponents` 带有「templated children」占位时, + processor 会存下 `__template` 元数据。当之后的 `updateDataModel` 填充被绑定 + 的 path 时,它会按每个 item 克隆 template 子树、重写 child id,并 scope 每个 + 克隆的 `dataContextPath`。这就是为什么只改 data model 也能让组件出现或消失。 +- **action 以 message 形式回流。** 一次点击调用 `sendAction`;`useAction` 解析 + 所有动态值,构建一个 `UserActionPayload` 并 dispatch。`` 把它转发给你 + 的 `onAction`。响应(如果有)会作为新的 protocol messages 回来,你把它们推回 + 同一个 `MessageStore`。 +- **未知 component 软失败。** 不在 catalog 里的 `component` 名称会按 tag 打印 + 一次警告并渲染 `null`,而不是抛错。 + +## 包含内容 + +你会用来组合的构建块: + +- **``**——all-in-one 组件。它拥有 `MessageProcessor`,订阅开发者传入的 + `MessageStore`,并渲染最新的 surface。 +- **`MessageStore`**——原始 protocol messages 的 append-only buffer。你可以从 + fetch、SSE、WebSocket 或 in-process mock 等任意传输层写入。 +- **Catalog API**——`defineCatalog`、`mergeCatalogs`、`serializeCatalog`、 + `resolveCatalog` 和 `defineFunction`。这里没有全局 component registry;每个 + 消费者都显式组合自己想开放的 component 和 function entries。 +- **内置组件**——20 个 A2UI v0.9 basic-catalog renderer(`Text`、`Image`、 + `Button`、`Row`、`Column`、`List`、`Loading`、`Card`、`Modal`、`Divider`、 + `Icon`、`CheckBox`、`ChoicePicker`、`DateTimeInput`、`LineChart`、 + `PieChart`、`RadioGroup`、`Slider`、`TextField` 和 `Tabs`)。每个的用途见 + [catalog 指南](./catalog-guide_zh.md)。 +- **逐组件 manifest**——`catalog//catalog.json`,用于 Agent handshake 的 + JSON-Schema 描述。 +- **`basicFunctions`**——A2UI v0.9 basic-catalog 的客户端 function entries, + 可以直接展开进你的 `catalogs` 数组。 + +## Exports + +这个包按 subpath 拆分,让你只导入用到的部分。 + +| 导入 | 你得到什么 | +| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `@lynx-js/genui/a2ui` | 主入口:``、`createMessageStore`、catalog API、所有内置组件、`basicFunctions`,以及 protocol types。 | +| `@lynx-js/genui/a2ui/catalog` | catalog API 和内置组件的再导出,作为 tree-shake-friendly 的 subpath。 | +| `@lynx-js/genui/a2ui/catalog/` | 单个内置组件(例如 `.../catalog/Text`)。 | +| `@lynx-js/genui/a2ui/catalog//catalog.json` | 该组件用于 handshake 的 JSON-Schema manifest。 | +| `@lynx-js/genui/a2ui/store` | 纯数据层:`MessageStore`、`MessageProcessor`、`Resource`、`SignalStore`,以及 payload normalizers。 | +| `@lynx-js/genui/a2ui/react` | 自定义组件的 contract:`NodeRenderer`、`useAction`、`useDataBinding`、`useResolvedProps` 和 `useChecks`。 | +| `@lynx-js/genui/a2ui/functions` | `basicFunctions` 和 `registerBasicFunctions` 这个 escape hatch。 | +| `@lynx-js/genui/a2ui/styles/theme.css` | 可选的默认 CSS tokens,提供 `.a2ui-light` 和 `.a2ui-dark`。 | + +大多数应用只会从 `@lynx-js/genui/a2ui` 导入。当你构建自定义 catalog 组件或自己 +的 renderer 时,才需要 `/store` 和 `/react`。 + +## `` props 与生命周期 + +`` 接收两个必填 prop 和一组可选的 render hooks。 + +| Prop | 类型 | 必填 | 用途 | +| ------------------- | ---------------------------------------- | ---- | --------------------------------------------------------------------------------- | +| `messageStore` | `MessageStore` | 是 | 你的传输层写入的 raw-message buffer。`` 订阅它并处理新的 tail messages。 | +| `catalogs` | `readonly CatalogInput[]` | 是 | renderer 被允许实例化的 component 和 function entries。 | +| `onAction` | `(action: UserActionPayload) => void` | 否 | 树中发生用户 action 时触发。转发给你的 Agent;把响应推回 store。 | +| `className` | `string` | 否 | 加在 surface root view(`surface-${surfaceId}`)上。适合做 surface 级主题 class。 | +| `wrapSurface` | `(children, { surfaceId }) => ReactNode` | 否 | 包裹每个 surface,便于套一层外部主题壳或 wrapper 样式。 | +| `renderEmpty` | `() => ReactNode` | 否 | 在第一条 `beginRendering` 到达前渲染。默认什么都不渲染。 | +| `renderFallback` | `() => ReactNode` | 否 | 在 active resource 处于 pending 时渲染。默认是内置的 ``。 | +| `renderError` | `(err: unknown) => ReactNode` | 否 | 在 active resource 失败时渲染。 | +| `renderUnsupported` | `(info) => ReactNode` | 否 | 在遇到不支持的 component 或数据语法时渲染。 | + +能省下调试时间的生命周期说明: + +- **每次 mount 一个 processor。** `` 在每次 mount 时创建一次自己的 + `MessageProcessor`(surfaces、signals、resources)。之后传入_另一个_ + `messageStore` 实例**不会**重置内部状态。要开启新的 session 或 turn,请用一个 + 由 turn/session id 派生的 `key` 来 mount: + ``。 +- **`onAction` 是 fire-and-forget。** renderer 从不 await 它。你的 Agent 把后续 + messages 推回同一个 `MessageStore` 来更新 UI。 +- **`className` vs `wrapSurface`。** 两者都能驱动主题切换;`className` 给 surface + root 加样式,`wrapSurface` 在外面加一层 wrapper。选择与你的样式策略匹配的那层。 + +## 下一步 + +- [Catalogs、内置组件与自定义组件](./catalog-guide_zh.md)——组合 contract、加入 + manifest、注册你自己的组件。 +- [打开 A2UI playground](https://lynxjs.org/a2ui)——在线体验。 diff --git a/packages/genui/a2ui/src/catalog/README.md b/packages/genui/a2ui/src/catalog/README.md index 4b45e03732..34f8bdaa53 100644 --- a/packages/genui/a2ui/src/catalog/README.md +++ b/packages/genui/a2ui/src/catalog/README.md @@ -1,211 +1,12 @@ # Catalog composition -The package intentionally **does not** ship an "all-in-one" catalog -constant. A top-level array referencing every built-in defeats -tree-shaking — every consumer of such an aggregate would bundle every -component, even the components you don't use. Composition is per-component, -and the cost is visible at the import site. +The catalog documentation now lives in one place: -## The minimum a renderer needs +➡️ **[docs/catalog-guide.md](../../docs/catalog-guide.md)** -If your app only renders, names alone are enough. Pass bare components — -the protocol name comes from `displayName ?? component.name`. +It covers composing a catalog, the built-in components, adding manifests for +the Agent handshake, the basic-catalog functions, why there is no +`catalog/all` aggregate, the paste-able "every built-in" recipe, custom +components, and the full catalog API reference. -> ⚠️ **Production minifiers will rename function declarations**, which breaks -> the `component.name` fallback. For production safety, set an explicit -> `displayName` on every custom component (the string literal survives -> minification), or pair the component with its `catalog.json` manifest -> using the tuple form below — the manifest key is authoritative. - -```tsx -import { A2UI, Text, Button, createMessageStore } from '@lynx-js/genui/a2ui'; - -const store = createMessageStore(); - -// Push raw protocol messages from your IO module (fetch, SSE, ...). -// async function streamFromAgent(input) { -// for await (const msg of myAgent.stream(input)) store.push(msg); -// } - - { - /* forward to your agent and push response messages back */ - }} -/>; -``` - -Bundlers tree-shake unused components — pulling `Text` does not drag in -`Button`, `Card`, etc. - -## Adding schemas for the agent handshake - -If you want `serializeCatalog(...)` to emit JSON Schema for each component -(for the agent to know what props to send), pair each component with the -JSON the extractor emitted at `dist/catalog//catalog.json`: - -```tsx -import { Text } from '@lynx-js/genui/a2ui/catalog/Text'; -import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' - with { type: 'json' }; - -const catalog = defineCatalog([[Text, textManifest]]); -agentChannel.handshake({ catalog: serializeCatalog(catalog) }); -``` - -The protocol name lives in the JSON as the top-level key, so the runtime -never duplicates it. - -## "I really want every built-in" - the paste-able recipe - -This includes every built-in component and the A2UI v0.9 basic-catalog -function entries. The package intentionally does not export this as -`catalog/all`; keep the list at the integration site so the bundle cost stays -visible. - -```tsx -import { - basicFunctions, - defineCatalog, - Button, - Card, - CheckBox, - ChoicePicker, - DateTimeInput, - Column, - Divider, - Icon, - Image, - LineChart, - PieChart, - List, - Modal, - RadioGroup, - Row, - Slider, - Tabs, - Text, - TextField, -} from '@lynx-js/genui/a2ui'; -import buttonManifest from '@lynx-js/genui/a2ui/catalog/Button/catalog.json' with { - type: 'json', -}; -import cardManifest from '@lynx-js/genui/a2ui/catalog/Card/catalog.json' with { - type: 'json', -}; -import checkBoxManifest from '@lynx-js/genui/a2ui/catalog/CheckBox/catalog.json' with { - type: 'json', -}; -import choicePickerManifest from '@lynx-js/genui/a2ui/catalog/ChoicePicker/catalog.json' with { - type: 'json', -}; -import dateTimeInputManifest from '@lynx-js/genui/a2ui/catalog/DateTimeInput/catalog.json' with { - type: 'json', -}; -import columnManifest from '@lynx-js/genui/a2ui/catalog/Column/catalog.json' with { - type: 'json', -}; -import dividerManifest from '@lynx-js/genui/a2ui/catalog/Divider/catalog.json' with { - type: 'json', -}; -import iconManifest from '@lynx-js/genui/a2ui/catalog/Icon/catalog.json' with { - type: 'json', -}; -import imageManifest from '@lynx-js/genui/a2ui/catalog/Image/catalog.json' with { - type: 'json', -}; -import lineChartManifest from '@lynx-js/genui/a2ui/catalog/LineChart/catalog.json' with { - type: 'json', -}; -import pieChartManifest from '@lynx-js/genui/a2ui/catalog/PieChart/catalog.json' with { - type: 'json', -}; -import listManifest from '@lynx-js/genui/a2ui/catalog/List/catalog.json' with { - type: 'json', -}; -import modalManifest from '@lynx-js/genui/a2ui/catalog/Modal/catalog.json' with { - type: 'json', -}; -import radioGroupManifest from '@lynx-js/genui/a2ui/catalog/RadioGroup/catalog.json' with { - type: 'json', -}; -import rowManifest from '@lynx-js/genui/a2ui/catalog/Row/catalog.json' with { - type: 'json', -}; -import sliderManifest from '@lynx-js/genui/a2ui/catalog/Slider/catalog.json' with { - type: 'json', -}; -import tabsManifest from '@lynx-js/genui/a2ui/catalog/Tabs/catalog.json' with { - type: 'json', -}; -import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' with { - type: 'json', -}; -import textFieldManifest from '@lynx-js/genui/a2ui/catalog/TextField/catalog.json' with { - type: 'json', -}; - -export const allBuiltins = defineCatalog([ - [Text, textManifest], - [Image, imageManifest], - [Row, rowManifest], - [Column, columnManifest], - [List, listManifest], - [Card, cardManifest], - [Modal, modalManifest], - [Button, buttonManifest], - [Divider, dividerManifest], - [LineChart, lineChartManifest], - [PieChart, pieChartManifest], - [TextField, textFieldManifest], - [CheckBox, checkBoxManifest], - [ChoicePicker, choicePickerManifest], - [DateTimeInput, dateTimeInputManifest], - [Icon, iconManifest], - [RadioGroup, radioGroupManifest], - [Slider, sliderManifest], - [Tabs, tabsManifest], - ...basicFunctions, -]); -``` - -Drop the `manifest` import + tuple form for any component whose schema you -don't need to ship to the agent. Keep `...basicFunctions` if your A2UI -messages use function calls in dynamic props, actions, or validation checks. - -## Custom components - -A component is anything that takes a single props object and returns a -ReactNode. The function's name (or `displayName`) is the protocol name the -agent will use: - -```tsx -function MyChart(props: { data: number[] }) { ... } -// Required for production-safe naming — minifiers rewrite `function` -// names, but the `displayName` string literal survives. -MyChart.displayName = 'MyChart'; - - -// Agent sends `{ component: 'MyChart', data: [...] }` → renders MyChart. -``` - -If you want schema introspection for a custom component, generate the -manifest with `@lynx-js/genui/a2ui-catalog-extractor` against your interface and -pair it the same way: - -```tsx -defineCatalog([[MyChart, myChartManifest]]); -``` - -## API surface - -- `defineCatalog(inputs)` — builds the runtime catalog. Inputs can mix bare - components, `[component, manifest]` tuples, and already-resolved entries - (e.g. from `mergeCatalogs`). -- `mergeCatalogs(...catalogs)` — last-write-wins on duplicate names. -- `serializeCatalog(catalog)` — emits the JSON manifest for the agent - handshake. Components without an attached schema serialize to `{ name }` - only. -- `resolveCatalog(catalog)` — name → component map (used internally by the - renderer; exposed for advanced cases). +中文版见 [docs/catalog-guide_zh.md](../../docs/catalog-guide_zh.md)。 diff --git a/packages/genui/a2ui/src/catalog/readme_zh.md b/packages/genui/a2ui/src/catalog/readme_zh.md index 7ebcc592d1..4ded417449 100644 --- a/packages/genui/a2ui/src/catalog/readme_zh.md +++ b/packages/genui/a2ui/src/catalog/readme_zh.md @@ -1,203 +1,11 @@ # Catalog 组合 -这个包故意不提供 “all-in-one” catalog 常量。一个引用所有内置组件的顶层数组 -会破坏 tree-shaking:只要消费者引用这个聚合,就会把全部组件打进 bundle,即使 -实际只用其中几个。Catalog 应该按组件组合,成本应当在 import 处可见。 +catalog 文档现在统一收在一处: -## Renderer 最少需要什么 +➡️ **[docs/catalog-guide_zh.md](../../docs/catalog-guide_zh.md)** -如果你的应用只负责渲染,组件名称就足够了。直接传 bare components;协议名来自 -`displayName ?? component.name`。 +它覆盖了组合 catalog、内置组件、为 Agent handshake 加入 manifest、basic-catalog +functions、为什么没有 `catalog/all` 聚合、可粘贴的「全部内置」配方、自定义组件, +以及完整的 catalog API 参考。 -> 生产环境 minifier 会重写 function declaration 名称,这会破坏 -> `component.name` fallback。为了生产安全,请给每个自定义组件设置显式 -> `displayName`(字符串字面量能在 minification 后保留下来),或者用下面的 -> tuple 形式把组件和 `catalog.json` manifest 配对;manifest key 是权威名称。 - -```tsx -import { A2UI, Button, Text, createMessageStore } from '@lynx-js/genui/a2ui'; - -const store = createMessageStore(); - -// 从你的 IO 模块(fetch、SSE 等)写入原始 protocol messages。 -// async function streamFromAgent(input) { -// for await (const msg of myAgent.stream(input)) store.push(msg); -// } - - { - /* 转发给你的 Agent,并把 response messages 写回 store */ - }} -/>; -``` - -Bundler 可以 tree-shake 未使用组件;导入 `Text` 不会自动带上 `Button`、`Card` -等组件。 - -## 为 Agent handshake 加入 schemas - -如果你希望 `serializeCatalog(...)` 为每个组件输出 JSON Schema,让 Agent 知道 -可以发送哪些 props,请把组件和 extractor 在 `dist/catalog//catalog.json` -产出的 JSON 配对: - -```tsx -import { Text } from '@lynx-js/genui/a2ui/catalog/Text'; -import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' - with { type: 'json' }; - -const catalog = defineCatalog([[Text, textManifest]]); -agentChannel.handshake({ catalog: serializeCatalog(catalog) }); -``` - -协议名存在于 JSON 的顶层 key 中,runtime 不需要再复制一份名称。 - -## “我就是想用所有内置组件” - 可复制配方 - -这个配方包含所有内置组件和 A2UI v0.9 basic-catalog function entries。包本身 -故意不导出 `catalog/all`;请把列表保留在接入点,让 bundle 成本保持可见。 - -```tsx -import { - basicFunctions, - defineCatalog, - Button, - Card, - CheckBox, - ChoicePicker, - DateTimeInput, - Column, - Divider, - Icon, - Image, - LineChart, - PieChart, - List, - Modal, - RadioGroup, - Row, - Slider, - Tabs, - Text, - TextField, -} from '@lynx-js/genui/a2ui'; -import buttonManifest from '@lynx-js/genui/a2ui/catalog/Button/catalog.json' with { - type: 'json', -}; -import cardManifest from '@lynx-js/genui/a2ui/catalog/Card/catalog.json' with { - type: 'json', -}; -import checkBoxManifest from '@lynx-js/genui/a2ui/catalog/CheckBox/catalog.json' with { - type: 'json', -}; -import choicePickerManifest from '@lynx-js/genui/a2ui/catalog/ChoicePicker/catalog.json' with { - type: 'json', -}; -import dateTimeInputManifest from '@lynx-js/genui/a2ui/catalog/DateTimeInput/catalog.json' with { - type: 'json', -}; -import columnManifest from '@lynx-js/genui/a2ui/catalog/Column/catalog.json' with { - type: 'json', -}; -import dividerManifest from '@lynx-js/genui/a2ui/catalog/Divider/catalog.json' with { - type: 'json', -}; -import iconManifest from '@lynx-js/genui/a2ui/catalog/Icon/catalog.json' with { - type: 'json', -}; -import imageManifest from '@lynx-js/genui/a2ui/catalog/Image/catalog.json' with { - type: 'json', -}; -import lineChartManifest from '@lynx-js/genui/a2ui/catalog/LineChart/catalog.json' with { - type: 'json', -}; -import pieChartManifest from '@lynx-js/genui/a2ui/catalog/PieChart/catalog.json' with { - type: 'json', -}; -import listManifest from '@lynx-js/genui/a2ui/catalog/List/catalog.json' with { - type: 'json', -}; -import modalManifest from '@lynx-js/genui/a2ui/catalog/Modal/catalog.json' with { - type: 'json', -}; -import radioGroupManifest from '@lynx-js/genui/a2ui/catalog/RadioGroup/catalog.json' with { - type: 'json', -}; -import rowManifest from '@lynx-js/genui/a2ui/catalog/Row/catalog.json' with { - type: 'json', -}; -import sliderManifest from '@lynx-js/genui/a2ui/catalog/Slider/catalog.json' with { - type: 'json', -}; -import tabsManifest from '@lynx-js/genui/a2ui/catalog/Tabs/catalog.json' with { - type: 'json', -}; -import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json' with { - type: 'json', -}; -import textFieldManifest from '@lynx-js/genui/a2ui/catalog/TextField/catalog.json' with { - type: 'json', -}; - -export const allBuiltins = defineCatalog([ - [Text, textManifest], - [Image, imageManifest], - [Row, rowManifest], - [Column, columnManifest], - [List, listManifest], - [Card, cardManifest], - [Modal, modalManifest], - [Button, buttonManifest], - [Divider, dividerManifest], - [LineChart, lineChartManifest], - [PieChart, pieChartManifest], - [TextField, textFieldManifest], - [CheckBox, checkBoxManifest], - [ChoicePicker, choicePickerManifest], - [DateTimeInput, dateTimeInputManifest], - [Icon, iconManifest], - [RadioGroup, radioGroupManifest], - [Slider, sliderManifest], - [Tabs, tabsManifest], - ...basicFunctions, -]); -``` - -如果某个组件的 schema 不需要发给 Agent,可以去掉对应的 `manifest` import 和 -tuple 形式。只要你的 A2UI messages 在 dynamic props、actions 或 validation -checks 中使用 function calls,就保留 `...basicFunctions`。 - -## 自定义组件 - -组件可以是任何接收单个 props object 并返回 `ReactNode` 的函数。函数名(或 -`displayName`)就是 Agent 使用的协议名: - -```tsx -function MyChart(props: { data: number[] }) { ... } -// 生产安全命名需要 displayName。minifier 会重写 function 名称,但字符串 -// 字面量会保留下来。 -MyChart.displayName = 'MyChart'; - - -// Agent sends `{ component: 'MyChart', data: [...] }` -> renders MyChart. -``` - -如果自定义组件需要 schema introspection,请用 -`@lynx-js/genui/a2ui-catalog-extractor` 基于 interface 生成 manifest,然后用同样方式 -配对: - -```tsx -defineCatalog([[MyChart, myChartManifest]]); -``` - -## API surface - -- `defineCatalog(inputs)`:构建 runtime catalog。输入可以混合 bare - components、`[component, manifest]` tuples,以及已经 resolved 的 entries - (例如来自 `mergeCatalogs`)。 -- `mergeCatalogs(...catalogs)`:重复名称采用 last-write-wins。 -- `serializeCatalog(catalog)`:输出发给 Agent handshake 的 JSON manifest。 - 没有关联 schema 的 component 会序列化成 `{ name }`。 -- `resolveCatalog(catalog)`:生成 name -> component map。Renderer 内部使用, - 也开放给高级场景。 +English version: [docs/catalog-guide.md](../../docs/catalog-guide.md)。 diff --git a/packages/genui/a2ui/src/index.ts b/packages/genui/a2ui/src/index.ts index 246c4cd5a6..dd3cf3039c 100644 --- a/packages/genui/a2ui/src/index.ts +++ b/packages/genui/a2ui/src/index.ts @@ -84,7 +84,7 @@ export type { // // // There is intentionally no all-in-one aggregate — see -// `packages/genui/a2ui/src/catalog/README.md`. +// `packages/genui/a2ui/docs/catalog-guide.md`. export { Button, Card, diff --git a/packages/genui/a2ui/test/catalog.test.ts b/packages/genui/a2ui/test/catalog.test.ts index 9850c58d48..cf43080f84 100644 --- a/packages/genui/a2ui/test/catalog.test.ts +++ b/packages/genui/a2ui/test/catalog.test.ts @@ -164,8 +164,8 @@ describe('serializeCatalog', () => { describe('user composes their own all-builtins catalog', () => { // Snapshot of the recipe documented in - // packages/genui/a2ui/src/catalog/README.md. If you change this list, - // update the README too. + // packages/genui/a2ui/docs/catalog-guide.md. If you change this list, + // update that guide too. test('paste-able recipe builds the expected manifest', () => { const all = defineCatalog([ [Text, TEXT_MANIFEST], diff --git a/website/sidebars/genui.ts b/website/sidebars/genui.ts index 99a42d8b1a..a3930907a7 100644 --- a/website/sidebars/genui.ts +++ b/website/sidebars/genui.ts @@ -46,44 +46,24 @@ export function createGenUIGuideReadmeDocs(options: { }); syncDoc({ - outFile: path.join(enGuideRoot, 'a2ui/architecture.md'), + outFile: path.join(enGuideRoot, 'a2ui/overview.md'), replacements: A2UI_EN_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'docs/architecture.md'), + sourceFile: path.join(a2uiPackageRoot, 'docs/overview.md'), }); syncDoc({ - outFile: path.join(zhGuideRoot, 'a2ui/architecture.md'), + outFile: path.join(zhGuideRoot, 'a2ui/overview.md'), replacements: A2UI_ZH_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'docs/architecture_zh.md'), + sourceFile: path.join(a2uiPackageRoot, 'docs/overview_zh.md'), }); syncDoc({ - outFile: path.join(enGuideRoot, 'a2ui/catalogs.md'), + outFile: path.join(enGuideRoot, 'a2ui/catalog-guide.md'), replacements: A2UI_EN_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'docs/catalogs.md'), + sourceFile: path.join(a2uiPackageRoot, 'docs/catalog-guide.md'), }); syncDoc({ - outFile: path.join(zhGuideRoot, 'a2ui/catalogs.md'), + outFile: path.join(zhGuideRoot, 'a2ui/catalog-guide.md'), replacements: A2UI_ZH_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'docs/catalogs_zh.md'), - }); - syncDoc({ - outFile: path.join(enGuideRoot, 'a2ui/custom-components.md'), - replacements: A2UI_EN_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'docs/custom-components.md'), - }); - syncDoc({ - outFile: path.join(zhGuideRoot, 'a2ui/custom-components.md'), - replacements: A2UI_ZH_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'docs/custom-components_zh.md'), - }); - syncDoc({ - outFile: path.join(enGuideRoot, 'a2ui/catalog.md'), - replacements: A2UI_EN_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'src/catalog/README.md'), - }); - syncDoc({ - outFile: path.join(zhGuideRoot, 'a2ui/catalog.md'), - replacements: A2UI_ZH_LINK_REPLACEMENTS, - sourceFile: path.join(a2uiPackageRoot, 'src/catalog/readme_zh.md'), + sourceFile: path.join(a2uiPackageRoot, 'docs/catalog-guide_zh.md'), }); removeGeneratedDoc( @@ -123,24 +103,16 @@ export function createGenUIGuideReadmeDocs(options: { export const A2UI_EN_NAV_ITEMS = [ { - text: 'Overview README', + text: 'Introduction README', link: '/guide/genui/a2ui', }, { - text: 'Architecture', - link: '/guide/genui/a2ui/architecture', + text: 'Overview & Architecture', + link: '/guide/genui/a2ui/overview', }, { - text: 'Catalogs', - link: '/guide/genui/a2ui/catalogs', - }, - { - text: 'Custom Components', - link: '/guide/genui/a2ui/custom-components', - }, - { - text: 'Built-in Catalog README', - link: '/guide/genui/a2ui/catalog', + text: 'Catalogs & Components', + link: '/guide/genui/a2ui/catalog-guide', }, { text: 'Playground', @@ -150,24 +122,16 @@ export const A2UI_EN_NAV_ITEMS = [ export const A2UI_ZH_NAV_ITEMS = [ { - text: '概览 README', + text: '简介 README', link: '/zh/guide/genui/a2ui', }, { - text: '架构与导出', - link: '/zh/guide/genui/a2ui/architecture', - }, - { - text: 'Catalog 与 Manifests', - link: '/zh/guide/genui/a2ui/catalogs', + text: '概览与架构', + link: '/zh/guide/genui/a2ui/overview', }, { - text: '自定义组件', - link: '/zh/guide/genui/a2ui/custom-components', - }, - { - text: '内置 Catalog README', - link: '/zh/guide/genui/a2ui/catalog', + text: 'Catalogs 与组件', + link: '/zh/guide/genui/a2ui/catalog-guide', }, { text: 'Playground', @@ -186,35 +150,27 @@ const A2UI_ZH_SIDEBAR_ITEMS = A2UI_ZH_NAV_ITEMS.map(item => ({ })); const A2UI_EN_LINK_REPLACEMENTS = [ - ['./docs/architecture.md', '/guide/genui/a2ui/architecture'], - ['./docs/catalogs.md', '/guide/genui/a2ui/catalogs'], - ['./docs/custom-components.md', '/guide/genui/a2ui/custom-components'], - [ - '[../src/catalog/README.md](../src/catalog/README.md)', - '[built-in catalog README](/guide/genui/a2ui/catalog)', - ], - ['../src/catalog/README.md', '/guide/genui/a2ui/catalog'], - ['./src/catalog/README.md', '/guide/genui/a2ui/catalog'], + ['./docs/overview.md', '/guide/genui/a2ui/overview'], + ['./docs/catalog-guide.md', '/guide/genui/a2ui/catalog-guide'], + ['./overview.md', '/guide/genui/a2ui/overview'], + ['./catalog-guide.md', '/guide/genui/a2ui/catalog-guide'], [ '../../a2ui-catalog-extractor/README.md', 'https://github.com/lynx-family/lynx-stack/tree/main/packages/genui/a2ui-catalog-extractor#readme', ], + ['../README.md', '/guide/genui/a2ui'], ] as const; const A2UI_ZH_LINK_REPLACEMENTS = [ - ['./docs/architecture_zh.md', '/zh/guide/genui/a2ui/architecture'], - ['./docs/catalogs_zh.md', '/zh/guide/genui/a2ui/catalogs'], - ['./docs/custom-components_zh.md', '/zh/guide/genui/a2ui/custom-components'], - [ - '[../src/catalog/readme_zh.md](../src/catalog/readme_zh.md)', - '[内置 Catalog README](/zh/guide/genui/a2ui/catalog)', - ], - ['../src/catalog/readme_zh.md', '/zh/guide/genui/a2ui/catalog'], - ['./src/catalog/readme_zh.md', '/zh/guide/genui/a2ui/catalog'], + ['./docs/overview_zh.md', '/zh/guide/genui/a2ui/overview'], + ['./docs/catalog-guide_zh.md', '/zh/guide/genui/a2ui/catalog-guide'], + ['./overview_zh.md', '/zh/guide/genui/a2ui/overview'], + ['./catalog-guide_zh.md', '/zh/guide/genui/a2ui/catalog-guide'], [ '../../a2ui-catalog-extractor/readme.zh_cn.md', 'https://github.com/lynx-family/lynx-stack/blob/main/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md', ], + ['../README_zh.md', '/zh/guide/genui/a2ui'], ] as const; function syncReadme(options: {