From 7d38600d1dc94434c6cd1449e94a97306bcd3067 Mon Sep 17 00:00:00 2001 From: Sherry-hue <37186915+Sherry-hue@users.noreply.github.com> Date: Thu, 21 May 2026 20:12:40 +0800 Subject: [PATCH] feat(a2ui): support playground provider settings --- .../a2ui-playground/src/pages/AIChatPage.css | 95 +++++++++++ .../a2ui-playground/src/pages/AIChatPage.tsx | 156 +++++++++++++++++- 2 files changed, 249 insertions(+), 2 deletions(-) diff --git a/packages/genui/a2ui-playground/src/pages/AIChatPage.css b/packages/genui/a2ui-playground/src/pages/AIChatPage.css index 6b4ba8f7af..a39f49e57f 100644 --- a/packages/genui/a2ui-playground/src/pages/AIChatPage.css +++ b/packages/genui/a2ui-playground/src/pages/AIChatPage.css @@ -417,6 +417,90 @@ line-height: 1.5; } +.chatProviderToggle { + display: inline-flex; + align-items: center; + height: 28px; + margin-left: 4px; + padding: 0 10px; + border: 1px solid var(--geist-border); + border-radius: var(--geist-radius-md); + background: var(--geist-surface); + color: var(--geist-foreground); + font-size: 12px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} + +.chatProviderToggle:hover { + border-color: var(--geist-foreground); +} + +.chatProviderPanel { + display: grid; + grid-template-columns: + minmax(150px, 1.2fr) minmax(180px, 1fr) minmax(130px, 0.8fr) auto; + gap: 8px; + align-items: end; + padding: 10px 20px 12px; + border-bottom: 1px solid var(--geist-border); + background: var(--geist-background); + flex-shrink: 0; +} + +.chatProviderField { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.chatProviderLabel { + color: var(--geist-secondary); + font-size: 11px; + font-weight: 600; +} + +.chatProviderInput { + width: 100%; + height: 32px; + min-width: 0; + padding: 0 10px; + border: 1px solid var(--geist-border); + border-radius: var(--geist-radius-md); + background: var(--geist-background); + color: var(--geist-foreground); + font-family: var(--geist-mono); + font-size: 12px; + outline: none; +} + +.chatProviderInput:focus { + border-color: var(--geist-foreground); +} + +.chatProviderInput::placeholder { + color: var(--geist-secondary); +} + +.chatProviderClearButton { + height: 32px; + padding: 0 12px; + border: 1px solid var(--geist-border); + border-radius: var(--geist-radius-md); + background: var(--geist-surface); + color: var(--geist-secondary); + font-size: 12px; + font-weight: 600; + cursor: pointer; +} + +.chatProviderClearButton:hover { + color: var(--geist-foreground); + border-color: var(--geist-foreground); +} + .chatMessages { flex: 1; min-height: 0; @@ -843,4 +927,15 @@ border-left: none; border-top: 1px solid var(--geist-border); } + + .chatTokenUsageBadge { + order: 3; + width: 100%; + margin-left: 0; + overflow-x: auto; + } + + .chatProviderPanel { + grid-template-columns: 1fr; + } } diff --git a/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx b/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx index e9b08c170a..62d256436d 100644 --- a/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx @@ -53,6 +53,23 @@ interface TokenUsage { totalTokens: number; } +interface ProviderSettings { + apiKey: string; + baseURL: string; + model: string; +} + +interface ProviderRequestOptions { + apiKey?: string; + baseURL?: string; + model?: string; +} + +interface PersistedProviderSettings { + baseURL: string; + model: string; +} + function parseUsage(value: unknown): TokenUsage | null { if (!value || typeof value !== 'object') return null; const record = value as Record; @@ -119,7 +136,15 @@ 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 PROVIDER_SETTINGS_STORAGE_KEY = 'a2ui-playground-provider-settings'; const jsonExtensions = [json(), EditorView.lineWrapping]; + +const EMPTY_PROVIDER_SETTINGS: ProviderSettings = { + apiKey: '', + baseURL: '', + model: '', +}; + function isDevHost(hostname: string): boolean { return ( hostname === 'localhost' @@ -231,6 +256,35 @@ function safeStringifyPayload(value: unknown): string { } } +function readProviderSettings(): ProviderSettings { + if (typeof window === 'undefined') return EMPTY_PROVIDER_SETTINGS; + try { + const raw = window.localStorage.getItem(PROVIDER_SETTINGS_STORAGE_KEY); + if (!raw) return EMPTY_PROVIDER_SETTINGS; + const parsed = JSON.parse(raw) as Partial; + return { + apiKey: '', + baseURL: typeof parsed.baseURL === 'string' ? parsed.baseURL : '', + model: typeof parsed.model === 'string' ? parsed.model : '', + }; + } catch { + return EMPTY_PROVIDER_SETTINGS; + } +} + +function toProviderRequestOptions( + settings: ProviderSettings, +): ProviderRequestOptions { + const apiKey = settings.apiKey.trim(); + const baseURL = settings.baseURL.trim(); + const model = settings.model.trim(); + return { + ...(apiKey ? { apiKey } : {}), + ...(baseURL ? { baseURL } : {}), + ...(model ? { model } : {}), + }; +} + function parseSseFrame(frame: string): SseEvent | null { const lines = frame.split(/\r?\n/u); let event = 'message'; @@ -491,6 +545,12 @@ export function AIChatPage( completionTokens: 0, totalTokens: 0, }); + const [providerSettingsOpen, setProviderSettingsOpen] = useState( + false, + ); + const [providerSettings, setProviderSettings] = useState( + readProviderSettings, + ); const messagesEndRef = useRef(null); const chatMessagesRef = useRef(null); const followBottomRef = useRef(true); @@ -516,6 +576,29 @@ export function AIChatPage( initialSecondarySize: 480, }); + const providerRequestOptions = useMemo( + () => toProviderRequestOptions(providerSettings), + [providerSettings], + ); + + const hasProviderOverride = Object.keys(providerRequestOptions).length > 0; + + useEffect(() => { + try { + window.localStorage.setItem( + PROVIDER_SETTINGS_STORAGE_KEY, + JSON.stringify( + { + baseURL: providerSettings.baseURL, + model: providerSettings.model, + } satisfies PersistedProviderSettings, + ), + ); + } catch { + // Keep the in-memory settings usable even when browser storage is off. + } + }, [providerSettings]); + useEffect(() => { // Re-run on every render so streaming text growth & async editor mounts // both keep the chat pinned to the latest message. @@ -697,6 +780,7 @@ export function AIChatPage( body: JSON.stringify({ messages: [userMessage], conversation: requestConversation, + ...providerRequestOptions, }), signal: controller.signal, }); @@ -783,6 +867,7 @@ export function AIChatPage( inputValue, isGenerating, publishPreviewMessages, + providerRequestOptions, recordTurn, ]); @@ -876,6 +961,7 @@ export function AIChatPage( surfaceId: payload.surfaceId, action, conversation: requestConversation, + ...providerRequestOptions, }), signal, }); @@ -1046,7 +1132,7 @@ export function AIChatPage( actionAbortRef.current?.abort(); actionAbortRef.current = null; }; - }, [buildConversationContext, recordTurn]); + }, [buildConversationContext, providerRequestOptions, recordTurn]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -1118,7 +1204,9 @@ export function AIChatPage( description='Describe the UI you want to build. Share the structure, interactions, or visual style you want to explore.' topContent={ <> - Online Agent + + {hasProviderOverride ? 'Custom Provider' : 'Online Agent'} + {tokenUsage.totalTokens > 0 ? ( ) : null} + } /> + {providerSettingsOpen + ? ( +
+ + + + +
+ ) + : null}