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
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface PreviewQrItem {
variant?: 'default' | 'alt';
placeholder?: ReactNode;
errorDescription?: ReactNode;
showQrCode?: boolean;
}

interface A2UIPreviewSource {
Expand Down Expand Up @@ -445,6 +446,8 @@ export function PreviewPanel(props: PreviewPanelProps) {
return [];
}

const showQrCode = previewSource.kind !== 'a2ui' || !!previewSource.demoId;

const cards: Array<{ key: string; item: PreviewQrItem }> = [];
if (renderShareUrl) {
cards.push({
Expand All @@ -455,6 +458,7 @@ export function PreviewPanel(props: PreviewPanelProps) {
url: renderShareUrl,
urlTitle: formatUrlForDisplay(renderShareUrl),
copyButtonTitle: 'Copy render URL',
showQrCode,
},
});
}
Expand All @@ -468,6 +472,7 @@ export function PreviewPanel(props: PreviewPanelProps) {
urlTitle: formatUrlForDisplay(lynxDevUrl),
copyButtonTitle: 'Copy Lynx dev bundle URL',
variant: 'alt',
showQrCode,
},
});
}
Expand Down Expand Up @@ -655,7 +660,7 @@ export function PreviewPanel(props: PreviewPanelProps) {
</button>
</div>
</div>
{item.url
{item.url && item.showQrCode !== false
? (
<QrCode
value={item.url}
Expand Down
63 changes: 63 additions & 0 deletions packages/genui/a2ui-playground/src/pages/AIChatPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,69 @@
border-bottom-left-radius: 4px;
}

.chatGeneratedJson {
width: 100%;
max-width: 100%;
align-self: stretch;
border: 1px solid var(--geist-border);
border-radius: var(--geist-radius-md);
overflow: hidden;
background: var(--geist-code-bg);
flex-shrink: 0;
}

.chatGeneratedJsonTitle {
display: flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 0 12px;
border-bottom: 1px solid var(--geist-border);
background: var(--geist-background);
color: var(--geist-secondary);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}

.chatGeneratedJsonBadge {
padding: 1px 6px;
border: 1px solid var(--geist-border);
border-radius: 4px;
background: var(--geist-surface);
color: var(--geist-secondary);
font-size: 10px;
font-weight: 500;
letter-spacing: 0;
}

.chatGeneratedJsonEditor .cm-editor {
height: auto;
max-height: 400px;
font-family: var(--geist-mono);
font-size: 13px;
background: var(--geist-code-bg);
color: var(--geist-code-fg);
}

.chatGeneratedJsonEditor .cm-scroller {
max-height: 400px;
overflow: auto;
}

.chatGeneratedJsonEditor .cm-gutters {
background: color-mix(in srgb, var(--geist-code-bg) 88%, black);
color: color-mix(in srgb, var(--geist-code-fg) 58%, white);
border-right: 1px solid
color-mix(in srgb, var(--geist-border) 82%, transparent);
}

.chatGeneratedJsonEditor .cm-activeLine,
.chatGeneratedJsonEditor .cm-activeLineGutter {
background: color-mix(in srgb, var(--geist-background) 12%, transparent);
}

.chatInputArea {
padding: 12px 20px;
border-top: 1px solid var(--geist-border);
Expand Down
77 changes: 44 additions & 33 deletions packages/genui/a2ui-playground/src/pages/AIChatPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +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.
import { json } from '@codemirror/lang-json';
import CodeMirror from '@uiw/react-codemirror';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import './AIChatPage.css';

import { PanelResizeHandle } from '../components/PanelResizeHandle.js';
import { PreviewPanel } from '../components/PreviewPanel.js';
import { PreviewViewport } from '../components/PreviewViewport.js';
import { QrCode } from '../components/QrCode.js';
import { useResizablePanels } from '../hooks/useResizablePanels.js';
import { DEFAULT_A2UI_DEMO_URL } from '../utils/demoUrl.js';
import type { Protocol } from '../utils/protocol.js';
Expand Down Expand Up @@ -71,6 +72,7 @@ const RESIZE_BREAKPOINT = 980;
const ONLINE_A2UI_SERVER_ORIGIN = 'https://genui-server.vercel.app';
const ONLINE_A2UI_CHAT_URL = `${ONLINE_A2UI_SERVER_ORIGIN}/a2ui/stream`;
const LOCAL_A2UI_SERVER_PORT = '3060';
const jsonExtensions = [json()];

function isDevHost(hostname: string): boolean {
return (
Expand Down Expand Up @@ -338,6 +340,9 @@ export function AIChatPage(props: { protocol: Protocol }) {
const [inputValue, setInputValue] = useState<string>('');
const [renderUrl, setRenderUrl] = useState<string>('');
const [generatedJson, setGeneratedJson] = useState<string>('');
const [previewMessages, setPreviewMessages] = useState<unknown[] | null>(
null,
);
const [isGenerating, setIsGenerating] = useState<boolean>(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const previewFrameRef = useRef<HTMLIFrameElement | null>(null);
Expand Down Expand Up @@ -366,11 +371,22 @@ export function AIChatPage(props: { protocol: Protocol }) {
});

const baseUrl = useMemo(() => window.location.href.replace(/#.*$/, ''), []);
const previewSource = useMemo(() => {
if (!previewMessages) return undefined;
return {
kind: 'a2ui' as const,
protocol,
demoUrl: DEFAULT_A2UI_DEMO_URL,
theme: 'light' as const,
messages: previewMessages,
};
}, [previewMessages, protocol]);

const publishPreviewMessages = useCallback(
(nextMessages: unknown[]) => {
if (nextMessages.length === 0) return;
latestPreviewMessagesRef.current = nextMessages;
setPreviewMessages(nextMessages);

const initData = {
protocol,
Expand Down Expand Up @@ -425,6 +441,8 @@ export function AIChatPage(props: { protocol: Protocol }) {
]);
setInputValue('');
setGeneratedJson('');
setPreviewMessages(null);
latestPreviewMessagesRef.current = [];
setIsGenerating(true);
Comment thread
Sherry-hue marked this conversation as resolved.

void (async () => {
Expand Down Expand Up @@ -533,6 +551,30 @@ export function AIChatPage(props: { protocol: Protocol }) {
{msg.content}
</div>
))}
{generatedJson
? (
<div className='chatGeneratedJson'>
<div className='chatGeneratedJsonTitle'>
Generated Output
<span className='chatGeneratedJsonBadge'>JSON</span>
</div>
<CodeMirror
className='chatGeneratedJsonEditor'
value={generatedJson}
extensions={jsonExtensions}
theme='dark'
editable={false}
basicSetup={{
lineNumbers: true,
foldGutter: true,
bracketMatching: true,
closeBrackets: false,
autocompletion: false,
}}
/>
</div>
)
: null}
<div ref={messagesEndRef} />
</div>

Expand Down Expand Up @@ -569,38 +611,7 @@ export function AIChatPage(props: { protocol: Protocol }) {
style={previewPanelStyle}
title='Lynx Preview'
showPreviewModeSwitch
afterBody={
<>
{generatedJson
? (
<div className='chatGeneratedJson'>
<div className='chatGeneratedJsonTitle'>Generated JSON</div>
<pre>{generatedJson}</pre>
</div>
)
: null}

<div className='previewQrSection'>
<div className='previewQrContent'>
<div className='previewQrInfo'>
<div className='previewQrTitle'>View on Device</div>
<div className='previewQrDesc'>
Scan the QR code to preview on your mobile device.
</div>
</div>
{renderUrl
? <QrCode value={renderUrl} size={80} />
: (
<div className='previewQrPlaceholder'>
<span className='previewQrPlaceholderText'>
No render
</span>
</div>
)}
</div>
</div>
</>
}
previewSource={previewSource}
>
<PreviewViewport
src={renderUrl}
Expand Down
Loading