diff --git a/src/components/landing/AiLanding.tsx b/src/components/landing/AiLanding.tsx index 4fdda9f87..b8e5e3ca9 100644 --- a/src/components/landing/AiLanding.tsx +++ b/src/components/landing/AiLanding.tsx @@ -1,10 +1,15 @@ import * as React from 'react' import { BracketsCurly, + Bug, + Code, + Cube, + Database, Microphone, Plug, Radio, Robot, + Terminal, Waveform, type Icon, } from '@phosphor-icons/react' @@ -17,12 +22,18 @@ import { } from './LibraryLanding' const aiPrompt = [ - 'Build a TanStack AI feature for a TypeScript app.', - 'Use the headless client or framework adapter, AG-UI-compatible request and event streams, provider adapters, typed client and server tools, structured output, and observable runtime state without requiring a hosted gateway.', - 'Show provider-specific capabilities honestly, make tool approval explicit, and include media or realtime primitives only where the selected model supports them.', + 'Build an agent with TanStack AI, the headless agent framework for TypeScript.', + 'Drive the agent loop with chat(): isomorphic tools via toolDefinition().server() / .client(), composable (state) => boolean stop strategies, needsApproval interrupts resolved on the client, and native AG-UI request and event streams consumed by the headless client or a framework adapter.', + 'Reach for the rest of the stack only when the task needs it: Code Mode in an isolate for multi-tool orchestration, a sandboxed coding-agent harness, @tanstack/ai-mcp for MCP servers, memoryMiddleware for cross-session recall, @tanstack/ai-persistence for durable threads and resumable streams.', + 'Never introduce a hosted gateway, a prescribed UI kit, or a provider-specific wire format. Keep provider capabilities honest: model options, tool support, and modality-specific results stay typed at the adapter boundary, and media or realtime primitives appear only where the selected model supports them.', ].join(' ') const providers = [ + { + name: 'OpenRouter', + model: 'any of 300+ models', + capabilities: ['text', 'reasoning', 'tools', 'image'], + }, { name: 'OpenAI', model: 'gpt-5', @@ -65,27 +76,35 @@ type GraphPoint = { y: number } -const aiHeroClients = ['Vanilla', 'React', 'Vue', 'Solid', 'Svelte', 'Preact'] +const aiHeroClients = [ + 'Vanilla', + 'React', + 'Vue', + 'Solid', + 'Svelte', + 'Preact', + 'Angular', + 'Octane', +] const aiHeroServers: Array = [ - { label: 'TanStack AI', detail: 'TypeScript', kind: 'tanstack' }, + { label: 'TanStack AI', detail: 'Server', kind: 'tanstack' }, { label: 'Python', dotted: true }, { label: 'Go', dotted: true }, { label: 'PHP', dotted: true }, ] const aiHeroProviders = ['OpenRouter', 'OpenAI', 'Anthropic', 'Gemini'] +// ponytail: 8 clients on a fixed 4x2 grid; recompute the columns if the list changes length const graphClientNodes = aiHeroClients.map((label, index) => ({ label, - x: [34, 154, 274][index % 3] ?? 154, - y: index < 3 ? 44 : 90, - width: 86, - height: 34, + x: [10, 112, 214, 316][index % 4] ?? 112, + y: index < 4 ? 36 : 84, + width: 94, + height: 36, })) const graphAgUiNode: GraphNodePosition & { - detail: string kind: 'tanstack' } = { label: 'TanStack AI Client', - detail: 'AG-UI', kind: 'tanstack', x: 142, y: 138, @@ -108,22 +127,27 @@ const graphProviderNodes = aiHeroProviders.map((label, index) => ({ })) const aiHeroMessages = [ { - user: 'Build the invoice assistant on our stack.', + user: 'Build the invoice agent on our stack, not yours.', assistant: - 'Using TanStack AI for TypeScript, AG-UI events, a server tool for invoices, and OpenRouter for model routing.', + 'Done. Headless client in your app, the agent loop on your server, AG-UI between them. No gateway, no hosted state.', }, { - user: 'Ask before it charges a card.', + user: 'It should ask before it charges a card.', assistant: - 'Tool approval added. The UI will pause on chargeCard until your app confirms it.', + 'chargeCard is marked needsApproval, so the run ends as an interrupt. Resolve it and the loop continues from that exact step.', }, { - user: 'Can we switch providers later?', + user: 'And if we move off this provider?', assistant: - 'Yes. The provider stays behind an adapter; the app keeps the same event stream and typed tools.', + 'Swap the adapter. Your tools, events, and UI never learn the difference.', }, ] +// ponytail: the shared --landing-accent-ink is pure black, which reads badly on the +// orange accent fill. Darken the fill instead and use white text on it. +const accentFillClass = + 'bg-[linear-gradient(135deg,color-mix(in_srgb,var(--landing-accent)_84%,black),color-mix(in_srgb,var(--landing-accent)_52%,black))] text-white' + type AiHeroChatMessage = { assistant: string id: string @@ -135,32 +159,43 @@ export default function AiLanding() { return ( } prompt={aiPrompt} promptLabel="Copy AI prompt" > + + +
- +
@@ -168,37 +203,229 @@ export default function AiLanding() { + +
+
+
+
+
+ + +
+
) } +function CodeLine({ + children, + indent = 0, +}: { + children?: React.ReactNode + indent?: number +}) { + return

{children || ' '}

+} + +function Kw({ children }: { children: React.ReactNode }) { + return {children} +} + +// ponytail: the code surface is always dark, so these use fixed token colors. +// --landing-accent-bright resolves to a dark terracotta in light mode and is +// unreadable here. +function Fn({ children }: { children: React.ReactNode }) { + return {children} +} + +function Str({ children }: { children: React.ReactNode }) { + return {children} +} + +function Cmt({ children }: { children: React.ReactNode }) { + return {children} +} + +function CodeSurface({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function QuickStart() { + return ( +
+ + + + import {'{ chat, toServerSentEventsResponse }'}{' '} + from '@tanstack/ai' + + + import {'{ openRouterText }'} from{' '} + '@tanstack/ai-openrouter' + + + import {'{ createFileRoute }'} from{' '} + '@tanstack/react-router' + + + + export const Route = createFileRoute( + '/api/chat')({'{'} + + server: {'{'} + handlers: {'{'} + + POST: async ({'{ request }'}) => {'{'} + + + const {'{ messages }'} = await request. + json() + + + + const stream = chat({'{'} + + + adapter: openRouterText( + 'anthropic/claude-sonnet-4.5'), + + messages, + tools: [lookupInvoice], + {'})'} + + + // your route, your auth, your deploy target + + + return toServerSentEventsResponse(stream) + + {'},'} + {'},'} + {'},'} + {'})'} + + + + + + + import {'{ useChat, fetchServerSentEvents }'} from{' '} + '@tanstack/ai-react' + + + + export function Chat() {'{'} + + + const {'{ messages, sendMessage, interrupts }'} ={' '} + useChat({'{'} + + + connection: fetchServerSentEvents('/api/chat'), + + {'})'} + + + // typed state and events. no components, no styles. + + + return ( + + <> + + {'{'}messages.map((message) => ( + + + <Bubble key={'{'}message.id{'}'} {'{'}...message{'}'}{' '} + /> + + )){'}'} + + + + {'{/* the loop paused. you decide when it continues. */}'} + + + + {'{'}interrupts.map((interrupt) => ( + + + <button key={'{'}interrupt.id{'}'} + + + onClick={'{'}() => interrupt.resolveInterrupt( + true){'}'}> + + + Approve {'{'}interrupt.toolName{'}'} + + + </button> + + )){'}'} + </> + ) + {'}'} + + + +

+ Swap ai-react for ai-vue, ai-solid, ai-svelte, ai-preact, ai-angular, or + the framework-free ai-client. The server route never changes. +

+
+ ) +} + function AiGraphChatHero() { const [activeClient, setActiveClient] = React.useState(0) + const [activeServer, setActiveServer] = React.useState(0) const [activeProvider, setActiveProvider] = React.useState(0) const [chatMessages, setChatMessages] = React.useState< Array >([]) const [typingUserMessage, setTypingUserMessage] = React.useState('') - const primaryServerNode = graphServerNodes[0] + const activeServerNode = graphServerNodes[activeServer] ?? graphServerNodes[0] const chatScrollRef = React.useRef(null) const chatLockedToBottomRef = React.useRef(true) @@ -210,12 +437,16 @@ function AiGraphChatHero() { const clientIntervalId = window.setInterval(() => { setActiveClient((current) => (current + 1) % aiHeroClients.length) }, 2300) + const serverIntervalId = window.setInterval(() => { + setActiveServer((current) => (current + 1) % aiHeroServers.length) + }, 3300) const providerIntervalId = window.setInterval(() => { setActiveProvider((current) => (current + 1) % aiHeroProviders.length) }, 4100) return () => { window.clearInterval(clientIntervalId) + window.clearInterval(serverIntervalId) window.clearInterval(providerIntervalId) } }, []) @@ -379,9 +610,9 @@ function AiGraphChatHero() { return (
- A client graph shows six UI adapters converging on the TanStack AI - Client over AG-UI, then reaching a TypeScript runtime and - interchangeable model providers. + A client graph shows eight UI adapters converging on the TanStack AI + Client over AG-UI, then reaching an agent runtime in TypeScript, Python, + Go, or PHP, and interchangeable model providers. @@ -408,7 +639,7 @@ function AiGraphChatHero() { {graphServerNodes.map((node, index) => ( ( {chatMessages.map((message) => ( -
+
{message.user}
{message.assistant || message.isStreaming ? ( @@ -506,7 +738,10 @@ function AiGraphChatHero() { ['event', 'text content'], ['tool', 'approval gate'], ['provider', aiHeroProviders[activeProvider]], - ['runtime', 'TanStack AI'], + [ + 'runtime', + aiHeroServers[activeServer]?.label ?? 'TanStack AI', + ], ].map(([label, value]) => (
{boundary === 'client' - ? 'Runs beside the UI and can update local application state.' - : 'Runs behind your server boundary with private credentials and data.'} + ? 'Runs beside the UI and can update local application state. The loop waits for it and feeds the result back to the model.' + : 'Runs behind your server boundary with private credentials and data. The model never sees them.'}

@@ -760,8 +995,9 @@ function ProviderWorkbench() { )}

- Adapter-specific types expose the options and outputs available for - this model. + Types narrow to this exact model: its options, its capabilities, its + input modalities. Pass an image to a text-only model and it fails at + compile time, not in production.

@@ -773,7 +1009,7 @@ function ProtocolMap() { const nodes = [ ['UI', 'headless client'], ['AG-UI', 'request + events'], - ['Runtime', 'your server'], + ['Agent loop', 'your server'], ['Provider', 'typed adapter'], ] @@ -799,49 +1035,203 @@ function ProtocolMap() { ) } -function ModalityRail() { - const modalities: Array<{ detail: string; icon: Icon; label: string }> = [ - { - label: 'Text + objects', - detail: 'chat · outputSchema', - icon: Robot, - }, - { - label: 'Speech + transcription', - detail: 'generateSpeech · generateTranscription', - icon: Microphone, - }, - { - label: 'Realtime voice', - detail: 'realtimeToken · RealtimeClient', - icon: Waveform, - }, - { - label: 'Images + video', - detail: 'generateImage · generateVideo', - icon: Radio, - }, - ] +type RailItem = { + body: string + detail: string + icon: Icon + label: string +} +const agentStack: Array = [ + { + label: 'Code Mode', + detail: '@tanstack/ai-code-mode', + body: 'The model writes one TypeScript program that calls your tools with loops and Promise.all, instead of a round trip per call. It runs in a V8 isolate, QuickJS WASM, or a Cloudflare Worker, with no host filesystem, network, or process.', + icon: Code, + }, + { + label: 'Coding-agent harnesses', + detail: '@tanstack/ai-sandbox', + body: 'Run Claude Code, Codex, OpenCode, Grok Build, or any ACP agent as a chat backend, inside a local process, Docker, Daytona, Vercel, Sprites, or Cloudflare sandbox. Their tool activity streams back as AG-UI events your UI already renders.', + icon: Terminal, + }, + { + label: 'MCP + MCP Apps', + detail: '@tanstack/ai-mcp', + body: 'A host-side MCP client with a type-generating CLI, provider-routed mcpTool(), and interactive ui:// widgets rendered from tool results across multiple servers.', + icon: Cube, + }, + { + label: 'Memory + persistence', + detail: '@tanstack/ai-memory · -persistence', + body: 'memoryMiddleware recalls across sessions through Redis, mem0, Honcho, or Hindsight adapters. Persistence keeps an authoritative server thread, resumes a stream through a dropped connection, and survives a reload.', + icon: Database, + }, +] + +const modalities: Array = [ + { + label: 'Text, objects, reasoning', + detail: 'chat · outputSchema · summarize', + body: 'Structured output streams as a typed message part beside tool calls and is preserved per turn in history, not a separate one-shot call.', + icon: Robot, + }, + { + label: 'Speech, transcription, music', + detail: 'generateSpeech · generateTranscription · generateAudio', + body: 'Six speech formats with speed control, transcription with word timestamps and diarization, plus music and sound effects.', + icon: Microphone, + }, + { + label: 'Realtime voice', + detail: 'openaiRealtimeToken · RealtimeClient', + body: 'OpenAI, Grok, and ElevenLabs with VAD modes and tool calling inside a live session.', + icon: Waveform, + }, + { + label: 'Images + video', + detail: 'generateImage · generateVideo', + body: 'Per-model typed options across OpenAI, Gemini, Grok, OpenRouter, and fal.ai, with an async job lifecycle for video.', + icon: Radio, + }, +] + +const devtoolsHooks = [ + { detail: 'useChat · 12 msgs', name: 'Support Chat', selected: true }, + { detail: 'useGenerateImage', name: 'Image Studio' }, + { detail: 'useObject', name: 'Invoice Extract' }, + { detail: 'useTranscription', name: 'Call Notes' }, +] + +const devtoolsTimeline: Array<{ + detail: string + label: string + tone: 'accent' | 'muted' | 'warn' +}> = [ + { + label: 'user turn', + detail: '"refund the duplicate charge"', + tone: 'muted', + }, + { + label: 'memory recall', + detail: '3 facts injected · 214 tokens', + tone: 'accent', + }, + { + label: 'tool call', + detail: 'lookupInvoice { id: "inv_8841" }', + tone: 'accent', + }, + { + label: 'tool result', + detail: '{ total: 4200, status: "paid" }', + tone: 'accent', + }, + { + label: 'interrupt', + detail: 'chargeCard · awaiting approval', + tone: 'warn', + }, + { + label: 'finish reason', + detail: 'interrupt · run resumable', + tone: 'muted', + }, +] + +function DevtoolsPanel() { + return ( + +
+
+

+ hooks +

+ {devtoolsHooks.map((hook) => ( +
+

+ {hook.name} +

+

+ {hook.detail} +

+
+ ))} +
+ +
+
+

+ run timeline +

+

+ thread_7f2 · run_3 +

+
+
+ {devtoolsTimeline.map((event) => ( +
+ + {event.label} + + + {event.detail} + +
+ ))} +
+
+
+
+ ) +} + +function FeatureRail({ items }: { items: Array }) { return (
- {modalities.map((modality, index) => { - const Icon = modality.icon + {items.map((item, index) => { + const Icon = item.icon return (
-

- {modality.label} -

+

{item.label}

- {modality.detail} + {item.detail} +

+

+ {item.body}

diff --git a/src/libraries/ai.tsx b/src/libraries/ai.tsx index 64f98babe..c84d1bd44 100644 --- a/src/libraries/ai.tsx +++ b/src/libraries/ai.tsx @@ -7,39 +7,46 @@ const textStyles = `text-category-data` export const aiProject = { ...ai, - description: `A powerful, open-source AI SDK with a unified interface across multiple providers. No vendor lock-in, no proprietary formats, just clean TypeScript and honest open source.`, + description: `The headless agent framework for TypeScript. TanStack AI runs the agent loop as typed primitives you compose yourself: tool calls, reasoning, human-in-the-loop interrupts, memory, and streaming state. Bring your own UI framework, model provider, server, and transport. Native AG-UI over the wire, MIT licensed, no hosted gateway and no platform to buy into.`, latestBranch: 'main', defaultDocs: 'getting-started/overview', featureHighlights: [ { - title: 'Provider Agnostic', - icon: , + title: 'A Real Agent Loop', + icon: , description: (
- Official adapters for OpenRouter, OpenAI, Anthropic, Gemini, Ollama, - Groq, Grok/xAI, ElevenLabs, and fal.ai. Import only the adapters your - app needs. + chat() drives the loop and you control every part of it: + isomorphic tools you place on the client or the server, composable{' '} + {`(state) => boolean`} stop strategies, and interrupts + that pause a run for human approval and resume exactly where it + stopped, with no database required.
), }, { - title: 'AG-UI Native Clients', - icon: , + title: 'Bring Your Own Everything', + icon: , description: (
- A headless client plus React, Vue, Solid, Svelte, and Preact bindings - all speak the same AG-UI request and event protocol. + Your provider, server, transport, auth, and deploy target. Adapters + for OpenRouter, OpenAI, Anthropic, Gemini, Bedrock, Mistral, Groq, + Grok/xAI, Ollama, ElevenLabs, and fal.ai, plus{' '} + openaiCompatible for anything else. Import only what you + use: every activity is a separate, tree-shakeable module.
), }, { - title: 'Typed Tools & Media', - icon: , + title: 'Headless, Not Opinionated', + icon: , description: (
- Type-safe client/server tools, provider-native tools, structured - output, reasoning streams, image, speech, transcription, realtime - voice, and video generation. + A framework-free core with React, Vue, Solid, Svelte, Preact, Angular, + and React Native bindings on top, plus official Octane bindings from + the Octane team. All of them speak native AG-UI over SSE, HTTP + streams, XHR, RPC, or your own transport. No components to fight, no + styles to override.
), }, diff --git a/src/libraries/libraries.ts b/src/libraries/libraries.ts index 0b27366b3..09bb239b2 100644 --- a/src/libraries/libraries.ts +++ b/src/libraries/libraries.ts @@ -650,13 +650,20 @@ export const ai: LibrarySlim = { ...categoryStyles.data, name: 'TanStack AI', to: '/ai', - tagline: - 'A powerful, open-source AI SDK with a unified interface across multiple providers', + tagline: 'The headless agent framework for TypeScript. Bring your own stack', description: - 'A powerful, open-source AI SDK with a unified interface across multiple providers. No vendor lock-in, no proprietary formats, just clean TypeScript and honest open source.', + 'The headless agent framework for TypeScript. TanStack AI runs the agent loop as typed primitives you compose yourself: tool calls, reasoning, human-in-the-loop interrupts, memory, and streaming state. Eleven provider adapters, seven UI framework bindings, sandboxed code execution, MCP, and coding-agent harnesses behind one interface. Native AG-UI over the wire, MIT licensed, no hosted gateway and no platform to buy into.', badge: 'beta', repo: 'tanstack/ai', - frameworks: ['react', 'vue', 'solid', 'svelte', 'preact', 'vanilla'], + frameworks: [ + 'react', + 'vue', + 'solid', + 'svelte', + 'preact', + 'angular', + 'vanilla', + ], corePackageName: '@tanstack/ai-client', npmPackageNames: ['@tanstack/ai-client'], latestVersion: 'v0', @@ -669,6 +676,7 @@ export const ai: LibrarySlim = { solid: '@tanstack/ai-solid', svelte: '@tanstack/ai-svelte', preact: '@tanstack/ai-preact', + angular: '@tanstack/ai-angular', vanilla: '@tanstack/ai-client', }, frameworkDocs: { @@ -677,6 +685,7 @@ export const ai: LibrarySlim = { solid: 'api/ai-solid', svelte: 'getting-started/quick-start-svelte', preact: 'api/ai-preact', + angular: 'getting-started/quick-start-angular', vanilla: 'api/ai-client', }, sitemap: { diff --git a/tests/ai-framework-doc-links.test.ts b/tests/ai-framework-doc-links.test.ts index 846ea331f..bab7cc194 100644 --- a/tests/ai-framework-doc-links.test.ts +++ b/tests/ai-framework-doc-links.test.ts @@ -12,6 +12,7 @@ const expectedFrameworks: Framework[] = [ 'solid', 'svelte', 'preact', + 'angular', 'vanilla', ] @@ -21,6 +22,7 @@ const expectedPackages: Partial> = { solid: '@tanstack/ai-solid', svelte: '@tanstack/ai-svelte', preact: '@tanstack/ai-preact', + angular: '@tanstack/ai-angular', vanilla: '@tanstack/ai-client', } @@ -30,6 +32,7 @@ const expectedDocsPaths: Partial> = { solid: 'api/ai-solid', svelte: 'getting-started/quick-start-svelte', preact: 'api/ai-preact', + angular: 'getting-started/quick-start-angular', vanilla: 'api/ai-client', }