AI Chat

A full-featured AI chatbot ported feature-by-feature from the official Nuxt AI Chatbot template (Nuxt UI + Vercel AI SDK) — probably the most demanding "real-world" validation of Vue Lynx so far: streaming responses with thinking/reasoning, markdown with highlighted code blocks and tables, tool-call cards (weather, charts, web-search sources), chat history with date grouping, votes, message editing, share/rename/delete flows, a model picker, persisted runtime theming with light/dark mode, attachments, fuzzy command-palette search, and a responsive mobile layout with a slide-over sidebar.

Try the quick prompts below — "What is the weather in Bordeaux?" streams a reasoning section and a weather card; "Show me a chart of sales data" renders an SVG line chart; "Help me create a Vue composable" shows markdown with highlighted code. Scan the QR code in the native tab to run the same bundle in LynxExplorer.

The playground above runs fully self-contained: the app probes for its API server and, when unreachable, falls back to an in-app demo backend with the same seeded history and deterministic mock AI streams. When you run the example locally with pnpm dev:server, the same UI talks to a standalone Node server that reimplements the Nuxt template's API routes — and streams real models through the Vercel AI Gateway if you set AI_GATEWAY_API_KEY.

Native chat techniques

A polished chat send is not one animation. It is a handoff between the composer, message list, keyboard, and streaming response. Vercel’s account of building the v0 iOS app inspired us to describe the work through those visible moments, then trace each moment back to the system that makes it possible.

The first send establishes the stage

Before an assistant message exists, the app captures the composer and viewport geometry. The outgoing text stays in a temporary animation layer while the app commits the real message, scroll position, and response space. The assistant reveal begins only after that update, so there is no target-position flash.

First send: the bubble leaves the composer, settles into its anchor, and makes room for the response.

The launch distance depends on the bubble height, viewport, composer, and keyboard. We stage the bubble at the captured composer position, commit layout, then start its transform on a later frame. Layout and animation never compete for the same first frame.

The second send is the real test

Repeat sends expose mistakes that the empty state hides. Native aligns the new user turn at the top, but keeps the previous turn stable until the moving bubble reaches its target. Vue Lynx nextTick() waits for pending operations to reach the main thread before scrollIntoView performs the final alignment. Only then does the assistant stream appear.

Second send: the old turn remains stable while the next bubble travels to the top anchor.

Web uses a different policy because it does not have the same native positioning guarantees: observed scroll-top updates keep the message list pinned to the bottom during sends and streaming. The interaction stays familiar even when the mechanism changes.

The keyboard participates in layout

Lynx <input> elements do not avoid the keyboard automatically. The composer remains absolutely positioned at the bottom and listens for the keyboardstatuschanged global event. It applies the reported keyboard height with setNativeProps; the message list follows only if it was already following new output.

Keyboard follow: the composer tracks the keyboard while the current turn stays readable.

Gesture feedback stays on the main thread

Drawer gestures and press feedback use Vue Lynx Main Thread Script. Functions marked with 'main thread' update element styles from main-thread-bindtouch* events without a background-thread round trip. The regular tap handler still performs the application action on Vue’s background thread.

Porting notes

The port keeps the original's data model (AI SDK v5 UIMessage parts over the UI-message-stream protocol) and nearly all of its composable/page logic, while re-implementing the DOM-coupled layers with Lynx elements:

AspectNuxt originalVue Lynx
UI kit@nuxt/ui v4 (UChat*, UDashboard*, Reka UI portals)Hand-built Lynx components (view/text/image/scroll-view/input) with Nuxt UI's semantic design tokens
Chat state@ai-sdk/vue useChatCustom useChat speaking the same protocol (SSE on web; incremental polling + cancellation on native)
MarkdownComark + Shiki (HTML)Markdown, including tables, → native-node renderer + lightweight code tokenizer
Chartsnuxt-charts / Unovis (SVG DOM)Chart generated as an inline SVG string for the native <svg> element
Dropdowns / modalsAnchored popovers, portalsAction sheets + centered modals at app root
ThemingTailwind v4 + app.configRuntime --ui-* CSS variables via inline vars (17 primary × 5 neutral picker included)
ResponsiveTailwind lg: breakpointSystemInfo.pixelWidth / pixelRatio branch (no media queries on Lynx)

The example directory contains a complete PRD with per-feature parity status (65 features ported/adapted, 10 skipped with reasons), a PORTING.md documenting what was reused vs rewritten plus the platform quirks discovered along the way, and side-by-side screenshot comparisons against the live original captured on Lynx for Web.