Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/a2ui-catalog.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ When a GenUI package builds a CLI or other generated artifact that another works
When implementing A2UI v0.9 functions in `packages/genui/a2ui`, keep function resolution scoped to the active catalog first, with the global `FunctionRegistry` only as an escape hatch. Dynamic component props, checks, and function-call actions should all go through the same `resolveDynamicValue` / `executeFunctionCall` path so data bindings, nested function calls, zod argument coercion from `@a2ui/web_core`, and `formatString` data-context interpolation stay consistent.

When verifying `packages/genui/a2ui-playground`, remember that `pnpm -F @lynx-js/genui-a2ui build` first runs `tsc --project tsconfig.build.json` and then regenerates catalog JSON through `build:catalog`. The playground consumes `@lynx-js/genui/a2ui` through package exports under `dist/**`, so you normally do not need a separate `tsc` step unless you intentionally skipped the package `build` step.
When streaming A2UI server responses, emit a root `Loading` component immediately after `createSurface` so the new surface has visible content before the model streams real components. Do not send `Image` components with unresolved search-query `url` strings directly to the renderer. Emit a same-id `Loading` component while `packages/genui/server/agent/image-resolver.ts` resolves the query to a loadable image URL, then emit an `updateComponents` message with the resolved `Image` so parent component references stay stable.

When verifying `packages/genui/a2ui-playground`, remember that `pnpm -F @lynx-js/a2ui-reactlynx build` regenerates catalog JSON only. The playground consumes `@lynx-js/a2ui-reactlynx` through package exports under `dist/**`, so run `pnpm -F @lynx-js/a2ui-reactlynx exec tsc -p tsconfig.build.json` before rebuilding the playground if runtime TypeScript changed.

For known A2UI playground examples, keep the web preview URL on `?demo=<id>` instead of swapping it to the payload-store `messagesUrl`. `render.html` intentionally fetches known demo JSON in the browser shell and passes resolved messages into Lynx, avoiding fetch differences in the Lynx worker runtime; use payload-store URLs for custom edited JSON.

Expand Down
2 changes: 2 additions & 0 deletions packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Image,
LineChart,
List,
Loading,
Modal,
PieChart,
RadioGroup,
Expand Down Expand Up @@ -76,6 +77,7 @@ const ALL_BUILTINS: readonly CatalogInput[] = [
manifestEntry(DateTimeInput, catalogManifests.DateTimeInput),
manifestEntry(LineChart, catalogManifests.LineChart),
manifestEntry(PieChart, catalogManifests.PieChart),
manifestEntry(Loading, catalogManifests.Loading),
manifestEntry(RadioGroup, catalogManifests.RadioGroup),
manifestEntry(Slider, catalogManifests.Slider),
manifestEntry(TextField, catalogManifests.TextField),
Expand Down
35 changes: 35 additions & 0 deletions packages/genui/a2ui-playground/src/catalog/a2ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,41 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
openui: [],
},
},
{
name: 'Loading',
category: 'Display',
description: 'Shows an animated skeleton placeholder for pending content.',
props: schemaToProps(catalogManifests.Loading),
usage: {
a2ui: {
id: 'loading-state',
component: 'Loading',
variant: 'block',
},
openui: {},
},
usageExamples: {
a2ui: [
{
label: 'Inline',
value: {
id: 'inline-loading',
component: 'Loading',
variant: 'inline',
},
},
{
label: 'Block',
value: {
id: 'block-loading',
component: 'Loading',
variant: 'block',
},
},
],
openui: [],
},
},
{
name: 'Icon',
category: 'Display',
Expand Down
13 changes: 11 additions & 2 deletions packages/genui/a2ui-playground/src/hooks/useConversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ function cloneDataModel(
}
}

function cloneDataValue(value: unknown): unknown {
if (value === null || typeof value !== 'object') return value;
try {
return JSON.parse(JSON.stringify(value)) as unknown;
} catch {
return value;
}
}
Comment thread
Sherry-hue marked this conversation as resolved.

function truncateConversationHistory(
history: ModelChatMessage[],
): ModelChatMessage[] {
Expand Down Expand Up @@ -108,7 +117,7 @@ function applyDataModel(
delete model[key];
}
if (value && typeof value === 'object' && !Array.isArray(value)) {
Object.assign(model, value as Record<string, unknown>);
Object.assign(model, cloneDataValue(value) as Record<string, unknown>);
}
return;
}
Expand All @@ -129,7 +138,7 @@ function applyDataModel(
if (value === undefined) {
delete cursor[last];
} else {
cursor[last] = value;
cursor[last] = cloneDataValue(value);
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/genui/a2ui/etc/genui-a2ui.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ export function LineChart(props: LineChartProps): ReactNode;
// @public (undocumented)
export function List(props: ListProps): ReactNode;

// Warning: (ae-forgotten-export) The symbol "LoadingProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "Loading" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function Loading(props: LoadingProps): ReactNode;

// Warning: (ae-missing-release-tag) "mergeCatalogs" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
Expand Down Expand Up @@ -611,6 +617,7 @@ export function useResolvedProps(properties: Record<string, unknown>, surface: S
// dist/catalog/Image/index.d.ts:4:4 - (tsdoc-undefined-tag) The TSDoc tag "@a2uiCatalog" is not defined in this configuration
// dist/catalog/LineChart/index.d.ts:9:4 - (tsdoc-undefined-tag) The TSDoc tag "@a2uiCatalog" is not defined in this configuration
// dist/catalog/List/index.d.ts:4:4 - (tsdoc-undefined-tag) The TSDoc tag "@a2uiCatalog" is not defined in this configuration
// dist/catalog/Loading/index.d.ts:4:4 - (tsdoc-undefined-tag) The TSDoc tag "@a2uiCatalog" is not defined in this configuration
// dist/catalog/Modal/index.d.ts:4:4 - (tsdoc-undefined-tag) The TSDoc tag "@a2uiCatalog" is not defined in this configuration
// dist/catalog/PieChart/index.d.ts:9:4 - (tsdoc-undefined-tag) The TSDoc tag "@a2uiCatalog" is not defined in this configuration
// dist/catalog/RadioGroup/index.d.ts:4:4 - (tsdoc-undefined-tag) The TSDoc tag "@a2uiCatalog" is not defined in this configuration
Expand Down
5 changes: 5 additions & 0 deletions packages/genui/a2ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
"default": "./dist/catalog/List/index.js"
},
"./catalog/List/catalog.json": "./dist/catalog/List/catalog.json",
"./catalog/Loading": {
"types": "./dist/catalog/Loading/index.d.ts",
"default": "./dist/catalog/Loading/index.js"
},
"./catalog/Loading/catalog.json": "./dist/catalog/Loading/catalog.json",
"./catalog/Card": {
"types": "./dist/catalog/Card/index.d.ts",
"default": "./dist/catalog/Card/index.js"
Expand Down
29 changes: 29 additions & 0 deletions packages/genui/a2ui/src/catalog/Loading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2026 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import type { GenericComponentProps } from '../../store/types.js';

import '../../../styles/catalog/Loading.css';

/**
* @a2uiCatalog Loading
*/
export interface LoadingProps extends GenericComponentProps {
/** Visual density for the skeleton placeholder. */
variant?: 'inline' | 'block';
}

export function Loading(
props: LoadingProps,
): import('@lynx-js/react').ReactNode {
const variant = props.variant ?? 'inline';

return (
<view className={`loading loading-${variant}`}>
<view className='loading-skeleton loading-skeleton-primary' />
{variant === 'block'
? <view className='loading-skeleton loading-skeleton-secondary' />
: null}
</view>
);
}
1 change: 1 addition & 0 deletions packages/genui/a2ui/src/catalog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export { Divider } from './Divider/index.jsx';
export { Icon } from './Icon/index.jsx';
export { Image } from './Image/index.jsx';
export { List } from './List/index.jsx';
export { Loading } from './Loading/index.jsx';
export { Modal } from './Modal/index.jsx';
export { RadioGroup } from './RadioGroup/index.jsx';
export { Row } from './Row/index.jsx';
Expand Down
1 change: 1 addition & 0 deletions packages/genui/a2ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export {
LineChart,
PieChart,
List,
Loading,
Modal,
RadioGroup,
Row,
Expand Down
21 changes: 4 additions & 17 deletions packages/genui/a2ui/src/react/A2UIRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useA2UIContext } from './useA2UIContext.js';
import { useAction } from './useAction.js';
import { useCatalog } from './useCatalog.js';
import { splitUnsupportedProps, useResolvedProps } from './useDataBinding.js';
import { Loading } from '../catalog/Loading/index.jsx';
import type { LoadingProps } from '../catalog/Loading/index.jsx';
import type { ComponentInstance, Resource, Surface } from '../store/types.js';

const noop = () => {
Expand All @@ -29,23 +31,8 @@ export interface UnsupportedInfo {
fields?: string[];
}

function DefaultLoading(props: { id: string }) {
const content = `loading ${props.id}...`;
return (
<view
style={{
width: '100%',
minHeight: '20px',
padding: '10px',
border: '1px solid var(--a2ui-color-border)',
borderRadius: '4px',
backgroundColor: 'var(--a2ui-color-surface-muted)',
color: 'var(--a2ui-color-text-muted)',
}}
>
<text style={{ color: 'inherit' }}>{content}</text>
</view>
);
function DefaultLoading(_props: { id: string }) {
return Loading({ variant: 'block' } as LoadingProps);
}

function DefaultUnsupportedNotice(props: UnsupportedInfo) {
Expand Down
10 changes: 5 additions & 5 deletions packages/genui/a2ui/styles/catalog/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
}

.button-primary {
background-color: var(--a2ui-color-primary);
border-color: var(--a2ui-color-primary);
color: var(--a2ui-color-on-primary);
background-color: var(--a2ui-color-surface-strong);
border-color: var(--a2ui-color-border-strong);
color: var(--a2ui-color-on-surface);
box-shadow:
0 10px 24px rgba(0, 85, 217, 0.16),
0 1px 0 rgba(255, 255, 255, 0.22) inset;
0 10px 24px var(--a2ui-color-overlay),
0 1px 0 rgba(255, 255, 255, 0.4) inset;
}

.button-borderless {
Expand Down
61 changes: 61 additions & 0 deletions packages/genui/a2ui/styles/catalog/Loading.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@import "../theme.css";

.loading {
--a2ui-loading-base: var(--a2ui-color-surface-muted);
--a2ui-loading-glow: rgba(255, 255, 255, 0.34);
--a2ui-loading-highlight: var(--a2ui-color-border-strong);

display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
flex-shrink: 0;
gap: 10px;
}

.loading-inline {
width: 100%;
min-height: 14px;
}

.loading-block {
width: 100%;
min-height: 48px;
padding: 6px 0;
}

.loading-skeleton {
height: 14px;
border-radius: 999px;
background: linear-gradient(
90deg,
var(--a2ui-loading-base) 0%,
var(--a2ui-loading-glow) 30%,
var(--a2ui-loading-highlight) 50%,
var(--a2ui-loading-glow) 70%,
var(--a2ui-loading-base) 100%
);
background-size: 220% 100%;
animation-name: a2ui-loading-shimmer;
animation-duration: 1200ms;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}

.loading-skeleton-primary {
width: 96%;
}

.loading-skeleton-secondary {
width: 68%;
}

@keyframes a2ui-loading-shimmer {
0% {
background-position: 120% 0;
}

100% {
background-position: -120% 0;
}
}
3 changes: 3 additions & 0 deletions packages/genui/server/agent/a2ui-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import iconManifest from './catalog/Icon/catalog.json';
import imageManifest from './catalog/Image/catalog.json';
import lineChartManifest from './catalog/LineChart/catalog.json';
import listManifest from './catalog/List/catalog.json';
import loadingManifest from './catalog/Loading/catalog.json';
import modalManifest from './catalog/Modal/catalog.json';
import radioGroupManifest from './catalog/RadioGroup/catalog.json';
import rowManifest from './catalog/Row/catalog.json';
Expand Down Expand Up @@ -89,6 +90,7 @@ const CATALOG_MANIFESTS = [
rowManifest,
columnManifest,
listManifest,
loadingManifest,
cardManifest,
tabsManifest,
modalManifest,
Expand Down Expand Up @@ -117,6 +119,7 @@ const COMPONENT_SUMMARIES: Record<string, string> = {
Image: 'Display an image by URL.',
LineChart: 'Display one or more numeric line series over shared labels.',
List: 'Repeating layout container, commonly bound to a data path.',
Loading: 'Animated progress indicator for pending content.',
Modal:
'Modal dialog with a trigger component and a content component. The trigger opens the modal locally when tapped.',
RadioGroup: 'Single-choice selector for a list of string options.',
Expand Down
Loading
Loading