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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ target/

# rslib
.rslib

# next
.next
.vercel
12 changes: 12 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export default tseslint.config(

// Outputs
'**/.rslib/**',
'**/.next/**',
'**/.turbo/**',
'**/.vercel/**',
'**/coverage/**',
'output/**',
'target/**',
Expand Down Expand Up @@ -74,6 +76,7 @@ export default tseslint.config(
'packages/react/transform/index.d.ts',
'packages/react/transform/index.cjs',
'packages/react/transform/**/index.d.ts',
'packages/genui/server/next-env.d.ts',

// REPL examples use Lynx platform globals and are not subject to lint rules
'packages/repl/src/examples/**',
Expand Down Expand Up @@ -453,4 +456,13 @@ export default tseslint.config(
'headers/header-format': 'off',
},
},
{
files: [
'packages/genui/server/**/*.{ts,tsx}',
],
rules: {
'n/file-extension-in-import': 'off',
'n/no-unsupported-features/node-builtins': 'off',
},
},
Comment thread
Sherry-hue marked this conversation as resolved.
);
72 changes: 29 additions & 43 deletions packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import {
Row,
Text,
createMessageStore,
normalizePayloadToMessages as normalizeProtocolMessages,
} from '@lynx-js/a2ui-reactlynx';
import type {
CatalogComponent,
CatalogInput,
CatalogManifest,
MessageStore,
ServerToClientMessage,
UserActionPayload,
Expand Down Expand Up @@ -57,18 +60,25 @@ const DEFAULT_STREAM_DELAY_MS = 800;
// agent handshake. To include schemas, pair each component with its
// `catalog.json` manifest — see
// `packages/genui/a2ui/src/catalog/README.md`.
function manifestEntry(
component: unknown,
manifest: CatalogManifest,
): readonly [CatalogComponent, CatalogManifest] {
return [component as CatalogComponent, manifest];
}

const ALL_BUILTINS: readonly CatalogInput[] = [
[Text, textManifest],
[Image, imageManifest],
[Row, rowManifest],
[Column, columnManifest],
[List, listManifest],
[Card, cardManifest],
[Button, buttonManifest],
[Divider, dividerManifest],
[Icon, iconManifest],
[CheckBox, checkBoxManifest],
[RadioGroup, radioGroupManifest],
manifestEntry(Text, textManifest),
manifestEntry(Image, imageManifest),
manifestEntry(Row, rowManifest),
manifestEntry(Column, columnManifest),
manifestEntry(List, listManifest),
manifestEntry(Card, cardManifest),
manifestEntry(Button, buttonManifest),
manifestEntry(Divider, dividerManifest),
manifestEntry(Icon, iconManifest),
manifestEntry(CheckBox, checkBoxManifest),
manifestEntry(RadioGroup, radioGroupManifest),
];

interface InitData {
Expand All @@ -83,9 +93,6 @@ interface InitData {
}

type Theme = 'light' | 'dark';

type A2uiMessage = Record<string, unknown> & { messageId?: string };
type ResponseMessages = A2uiMessage[];
type ActionMocks = Record<
string,
| ServerToClientMessage[]
Expand Down Expand Up @@ -184,40 +191,21 @@ function mergeInitDataPreferLeft(a: InitData, b: InitData): InitData {
};
}

function normalizePayloadToMessages(payload: unknown): ResponseMessages {
if (payload === null || payload === undefined) return [];
if (Array.isArray(payload)) return payload as ResponseMessages;
if (typeof payload === 'string') {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const parsed = JSON.parse(payload);
return normalizePayloadToMessages(parsed);
} catch {
return [];
}
}
if (
typeof payload === 'object'
&& Array.isArray((payload as Record<string, unknown>).messages)
) {
return (payload as Record<string, unknown>).messages as ResponseMessages;
}
return [];
}

async function loadMessages(initData: InitData): Promise<ResponseMessages> {
async function loadMessages(
initData: InitData,
): Promise<ServerToClientMessage[]> {
if (initData.messagesUrl) {
// eslint-disable-next-line n/no-unsupported-features/node-builtins
const res = await fetch(initData.messagesUrl, { cache: 'no-store' });
const text = await res.text();
try {
return normalizePayloadToMessages(JSON.parse(text));
return normalizeProtocolMessages(JSON.parse(text));
} catch {
return normalizePayloadToMessages(text);
return normalizeProtocolMessages(text);
}
}
if (initData.messages !== undefined) {
return normalizePayloadToMessages(initData.messages);
return normalizeProtocolMessages(initData.messages);
}
return [];
}
Expand Down Expand Up @@ -414,12 +402,10 @@ export function App() {
loadActionMocks(streamConfig as InitData),
]);

const initialMessages = rawMessages as ServerToClientMessage[];
const initialMessages = rawMessages;
const actionMocks: ActionMocks = {};
for (const [name, value] of Object.entries(rawActionMocks)) {
actionMocks[name] = normalizePayloadToMessages(
value,
) as ServerToClientMessage[];
actionMocks[name] = normalizeProtocolMessages(value);
}

const next = createMessageStore();
Expand Down
18 changes: 16 additions & 2 deletions packages/genui/a2ui-playground/src/components/MobilePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
// 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.
export function MobilePreview(props: { src: string }) {
import type { ComponentPropsWithoutRef, Ref } from 'react';

interface MobilePreviewProps {
src: string;
iframeRef?: Ref<HTMLIFrameElement>;
onLoad?: ComponentPropsWithoutRef<'iframe'>['onLoad'];
}

export function MobilePreview(props: MobilePreviewProps) {
return (
<div className='phoneWrap'>
<div className='phoneFrame'>
Expand All @@ -10,7 +18,13 @@ export function MobilePreview(props: { src: string }) {
<div className='phoneSpeaker' />
</div>
<div className='phoneScreen'>
<iframe className='phoneIframe' title='preview' src={props.src} />
<iframe
ref={props.iframeRef}
className='phoneIframe'
title='preview'
src={props.src}
onLoad={props.onLoad}
/>
</div>
<div className='phoneHomeIndicator' />
</div>
Expand Down
Loading
Loading