From e773b52ff266b6f807dbaf8525bcd8c50c8e157b Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 22 May 2026 17:40:33 +0200 Subject: [PATCH] feat(ai): refresh landing ecosystem animation --- src/components/AILibraryHero.tsx | 862 +++++++----------- src/components/AILibraryHeroBox.tsx | 85 +- src/components/FrameworkCard.tsx | 16 +- src/components/landing/AiLanding.tsx | 83 +- src/hooks/useAILibraryHeroAnimation.ts | 138 +-- src/images/ag-ui-dark.svg | 5 + src/images/ag-ui-light.svg | 5 + src/images/elevenlabs-dark.svg | 4 + src/images/elevenlabs-light.svg | 4 + src/images/fal-ai-dark.svg | 3 + src/images/fal-ai-light.svg | 3 + src/images/groq-dark.svg | 3 + src/images/groq-light.svg | 3 + src/images/xai-dark.svg | 3 + src/images/xai-light.svg | 3 + src/libraries/ai.tsx | 23 +- src/libraries/frameworkSupport.ts | 55 ++ src/libraries/libraries.ts | 18 +- src/libraries/types.ts | 2 + .../$version.docs.framework.index.tsx | 20 +- src/stores/aiLibraryHeroAnimation.ts | 53 +- src/utils/documents.server.ts | 15 +- tests/ai-framework-doc-links.test.ts | 56 ++ 23 files changed, 747 insertions(+), 715 deletions(-) create mode 100644 src/images/ag-ui-dark.svg create mode 100644 src/images/ag-ui-light.svg create mode 100644 src/images/elevenlabs-dark.svg create mode 100644 src/images/elevenlabs-light.svg create mode 100644 src/images/fal-ai-dark.svg create mode 100644 src/images/fal-ai-light.svg create mode 100644 src/images/groq-dark.svg create mode 100644 src/images/groq-light.svg create mode 100644 src/images/xai-dark.svg create mode 100644 src/images/xai-light.svg create mode 100644 src/libraries/frameworkSupport.ts create mode 100644 tests/ai-framework-doc-links.test.ts diff --git a/src/components/AILibraryHero.tsx b/src/components/AILibraryHero.tsx index 933546f01..20a9d3f17 100644 --- a/src/components/AILibraryHero.tsx +++ b/src/components/AILibraryHero.tsx @@ -7,9 +7,13 @@ import { useAILibraryHeroAnimation } from '~/hooks/useAILibraryHeroAnimation' import { AILibraryHeroCard } from './AILibraryHeroCard' import { AILibraryHeroBox } from './AILibraryHeroBox' import { AILibraryHeroServiceCard } from './AILibraryHeroServiceCard' +import jsLogo from '~/images/js-logo.svg' import tsLogo from '~/images/ts-logo.svg' import reactLogo from '~/images/react-logo.svg' +import vueLogo from '~/images/vue-logo.svg' import solidLogo from '~/images/solid-logo.svg' +import svelteLogo from '~/images/svelte-logo.svg' +import preactLogo from '~/images/preact-logo.svg' import pythonLogo from '~/images/python.svg' import phpLightLogo from '~/images/php-light.svg' import phpDarkLogo from '~/images/php-dark.svg' @@ -20,6 +24,18 @@ import openaiDarkLogo from '~/images/openai-dark.svg' import anthropicLightLogo from '~/images/anthropic-light.svg' import anthropicDarkLogo from '~/images/anthropic-dark.svg' import geminiLogo from '~/images/gemini.svg' +import openrouterBlackLogo from '~/images/openrouter-black.svg' +import openrouterWhiteLogo from '~/images/openrouter-white.svg' +import agUiLightLogo from '~/images/ag-ui-light.svg' +import agUiDarkLogo from '~/images/ag-ui-dark.svg' +import xaiLightLogo from '~/images/xai-light.svg' +import xaiDarkLogo from '~/images/xai-dark.svg' +import falAiLightLogo from '~/images/fal-ai-light.svg' +import falAiDarkLogo from '~/images/fal-ai-dark.svg' +import elevenLabsLightLogo from '~/images/elevenlabs-light.svg' +import elevenLabsDarkLogo from '~/images/elevenlabs-dark.svg' +import groqLightLogo from '~/images/groq-light.svg' +import groqDarkLogo from '~/images/groq-dark.svg' import { SVG_WIDTH, @@ -33,6 +49,7 @@ import { SERVICE_HEIGHT, LIBRARY_CARD_WIDTH, LIBRARY_CARD_HEIGHT, + LIBRARY_CARD_Y_OFFSET, LIBRARY_CARD_LOCATIONS, SERVER_CARD_Y_OFFSET, SERVER_CARD_LOCATIONS, @@ -50,14 +67,94 @@ type AILibraryHeroProps = { actions?: React.ReactNode } -const HIGHLIGHT_COLOR = 'rgba(255, 255, 240, 0.95)' +type HeroCardDefinition = { + label: string + logo?: string + logoLight?: string + logoDark?: string + fontSize?: number + logoSize?: number +} + +const HIGHLIGHT_COLOR = 'var(--hero-active-stroke)' +const TANSTACK_LOGO = '/images/logos/logo-color-100.png' + +const CLIENT_FRAMEWORKS: Array = [ + { label: 'Vanilla', logo: jsLogo, fontSize: 13, logoSize: 16 }, + { label: 'React', logo: reactLogo, fontSize: 13, logoSize: 16 }, + { label: 'Vue', logo: vueLogo, fontSize: 13, logoSize: 16 }, + { label: 'Solid', logo: solidLogo, fontSize: 13, logoSize: 16 }, + { label: 'Svelte', logo: svelteLogo, fontSize: 13, logoSize: 16 }, + { label: 'Preact', logo: preactLogo, fontSize: 13, logoSize: 16 }, +] + +const SERVER_LANGUAGES: Array = [ + { label: 'TypeScript', logo: tsLogo, fontSize: 15 }, + { label: 'Python', logo: pythonLogo }, + { label: 'PHP', logoLight: phpLightLogo, logoDark: phpDarkLogo }, +] + +const AI_PROVIDERS: Array = [ + { + label: 'OpenRouter', + logoLight: openrouterBlackLogo, + logoDark: openrouterWhiteLogo, + fontSize: 15, + }, + { + label: 'OpenAI', + logoLight: openaiLightLogo, + logoDark: openaiDarkLogo, + fontSize: 15, + }, + { + label: 'Anthropic', + logoLight: anthropicLightLogo, + logoDark: anthropicDarkLogo, + fontSize: 15, + }, + { label: 'Gemini', logo: geminiLogo, fontSize: 15 }, + { + label: 'Ollama', + logoLight: ollamaLightLogo, + logoDark: ollamaDarkLogo, + fontSize: 15, + }, + { + label: 'Groq', + logoLight: groqLightLogo, + logoDark: groqDarkLogo, + fontSize: 15, + }, + { + label: 'Grok/xAI', + logoLight: xaiLightLogo, + logoDark: xaiDarkLogo, + fontSize: 15, + }, + { + label: 'ElevenLabs', + logoLight: elevenLabsLightLogo, + logoDark: elevenLabsDarkLogo, + fontSize: 15, + }, + { + label: 'fal.ai', + logoLight: falAiLightLogo, + logoDark: falAiDarkLogo, + fontSize: 15, + }, +] + +const CLIENT_BOX = { x: 156, y: 134, width: 320, height: 56 } +const AG_UI_BOX = { x: 226, y: 240, width: 180, height: 48 } +const TANSTACK_AI_BOX = { x: 156, y: 344, width: 320, height: 56 } export function AILibraryHero(_props: AILibraryHeroProps) { const strokeColor = 'var(--hero-stroke)' const textColor = 'var(--hero-text)' const glassGradientStart = 'var(--hero-glass-start)' const glassGradientEnd = 'var(--hero-glass-end)' - // Use the animation hook - handles all animation state and orchestration const { store } = useAILibraryHeroAnimation() const { @@ -74,6 +171,20 @@ export function AILibraryHero(_props: AILibraryHeroProps) { connectionPulseDirection, } = store + const isHighlighting = + phase === AnimationPhase.SHOWING_CHAT || + phase === AnimationPhase.PULSING_CONNECTIONS || + phase === AnimationPhase.STREAMING_RESPONSE + + const hasActivePath = + isHighlighting && + selectedFramework !== null && + selectedServer !== null && + selectedService !== null + + const clientCenterX = CLIENT_BOX.x + CLIENT_BOX.width / 2 + const serverBoxCenterX = TANSTACK_AI_BOX.x + TANSTACK_AI_BOX.width / 2 + const getOpacity = ( index: number, selectedIndex: number | null, @@ -98,69 +209,42 @@ export function AILibraryHero(_props: AILibraryHeroProps) { return 0.3 } - const getConnectionOpacity = ( - frameworkIndex: number, - serverIndex: number, - ) => { - const isFrameworkSelected = - selectedFramework !== null && selectedFramework === frameworkIndex - const isServerSelected = - selectedServer !== null && selectedServer === serverIndex - const isHighlighting = - phase === AnimationPhase.SHOWING_CHAT || + const getConnectionPulse = () => { + if ( phase === AnimationPhase.PULSING_CONNECTIONS || phase === AnimationPhase.STREAMING_RESPONSE - - // Active path: selected framework -> client -> ai -> selected server - if (isHighlighting && isFrameworkSelected && isServerSelected) { - return 1.0 + ) { + return connectionPulseDirection === 'down' ? 'down' : 'up' } - // Unused lines should be low opacity - return 0.3 + return null } - const getConnectionStrokeColor = ( - frameworkIndex: number, - serverIndex: number, - ) => { - // If no selections, ALWAYS return original stroke color (highest priority check) - if (selectedFramework === null || selectedServer === null) { - return strokeColor + const getPathOpacity = (isActive: boolean) => { + if (!hasActivePath) { + return 0.42 } + return isActive ? 1 : 0.22 + } - // Only highlight during specific phases - const isHighlighting = - phase === AnimationPhase.SHOWING_CHAT || - phase === AnimationPhase.PULSING_CONNECTIONS || - phase === AnimationPhase.STREAMING_RESPONSE - - // If not in a highlighting phase, always return original stroke color - if (!isHighlighting) { - return strokeColor + const getPathStrokeColor = (isActive: boolean) => { + if (hasActivePath && isActive) { + return HIGHLIGHT_COLOR } + return strokeColor + } - // Now check if this is the active path - const isFrameworkSelected = selectedFramework === frameworkIndex - const isServerSelected = selectedServer === serverIndex + const getPulseClass = (isActive: boolean) => { + const pulse = getConnectionPulse() - // Active path: selected framework -> client -> ai -> selected server - // Only return off-white if we're in a highlighting phase AND this is the active path - if (isFrameworkSelected && isServerSelected) { - return HIGHLIGHT_COLOR + if (!pulse || !hasActivePath || !isActive) { + return '' } - // Not the active path, return original color - return strokeColor + return pulse === 'down' ? 'animate-pulse-down' : 'animate-pulse-up' } - const getConnectionPulse = () => { - if ( - phase === AnimationPhase.PULSING_CONNECTIONS || - phase === AnimationPhase.STREAMING_RESPONSE - ) { - return connectionPulseDirection === 'down' ? 'down' : 'up' - } - return null + const getProviderOpacity = (index: number) => { + return getServiceOpacity(index) } const getScaleTransform = ( @@ -181,16 +265,18 @@ export function AILibraryHero(_props: AILibraryHeroProps) { dangerouslySetInnerHTML={{ __html: ` :root { - --hero-stroke: rgba(0, 0, 0, 0.6); - --hero-text: #000000; - --hero-glass-start: rgba(255, 255, 255, 0.8); - --hero-glass-end: rgba(255, 255, 255, 0.6); + --hero-stroke: rgba(24, 24, 27, 0.36); + --hero-active-stroke: rgba(236, 72, 153, 0.95); + --hero-text: #18181b; + --hero-glass-start: rgba(255, 255, 255, 0.98); + --hero-glass-end: rgba(253, 242, 248, 0.9); } :root.dark { - --hero-stroke: rgba(255, 255, 255, 0.8); + --hero-stroke: rgba(255, 255, 255, 0.32); + --hero-active-stroke: rgba(255, 255, 255, 0.95); --hero-text: #ffffff; - --hero-glass-start: rgba(255, 255, 255, 0.55); - --hero-glass-end: rgba(255, 255, 255, 0.55); + --hero-glass-start: rgba(24, 24, 27, 0.92); + --hero-glass-end: rgba(39, 39, 42, 0.78); } @keyframes pulse-down { 0% { @@ -231,24 +317,21 @@ export function AILibraryHero(_props: AILibraryHeroProps) { `, }} /> -
- {/* Diagram and Chat Panel Container */} +
- {/* SVG Diagram */}
- {/* Glass effect filter with blur and opacity */} - {/* Subtle glow for lines */} - {/* Glass gradient */} - {/* Glass gradient for larger boxes */} + + + + - {/* Lines from frameworks to ai-client */} - - - - + {CLIENT_FRAMEWORKS.map((framework, index) => { + const startX = + LIBRARY_CARD_LOCATIONS[index] + LIBRARY_CARD_WIDTH / 2 + const isActive = selectedFramework === index + + return ( + + ) + })} - {/* Lines from TanStack AI to servers */} - - + - - {/* Top layer: Frameworks */} - - - - + {SERVER_LANGUAGES.map((server, index) => { + const endX = + SERVER_CARD_LOCATIONS[index] + SERVER_CARD_WIDTH / 2 + const isActive = selectedServer === index + + return ( + + ) + })} + + {SERVER_LANGUAGES.map((server, index) => { + const startX = + SERVER_CARD_LOCATIONS[index] + SERVER_CARD_WIDTH / 2 + const isActive = selectedServer === index + + return ( + + ) + })} + + {CLIENT_FRAMEWORKS.map((framework, index) => ( + + ))} - - {/* @tanstack/ai-client box */} - {/* Large TanStack AI container box */} - {/* Line from ai-client to @tanstack/ai - drawn after boxes to be on top */} - - - {/* Provider layer */} - - ( + + ))} - - - - - + + + {AI_PROVIDERS.map((provider, index) => ( + + ))} + - - {/* Server layer */} - - - - - - -
- {/* Chat Panel */}
@@ -63,21 +72,69 @@ export function AILibraryHeroBox({ fontFamily="Helvetica" fontSize={fontSize} fontWeight={fontWeight} - textAnchor="start" + textAnchor={centerText ? 'middle' : 'start'} opacity={opacity * 1.05} > {label} )} - + {showLogo && logo ? ( + + ) : null} + {showLogo && hasSeparateLogos ? ( + <> + + + + ) : null} + {showLogo && !logo && !hasSeparateLogos && (logoLight || logoDark) ? ( + + ) : null} + {showLogo && !hasCustomLogo ? ( + + ) : null} ) } diff --git a/src/components/FrameworkCard.tsx b/src/components/FrameworkCard.tsx index 36b31a31d..b0733aeed 100644 --- a/src/components/FrameworkCard.tsx +++ b/src/components/FrameworkCard.tsx @@ -6,6 +6,10 @@ import { useCopyButton } from '~/components/CopyMarkdownButton' import { useToast } from '~/components/ToastProvider' import { Check, Copy } from 'lucide-react' import { Card } from '~/components/Card' +import { + getFrameworkDocsHash, + getFrameworkDocsPath, +} from '~/libraries/frameworkSupport' export function FrameworkCard({ framework, @@ -35,16 +39,8 @@ export function FrameworkCard({ ) }) - const hasCustomInstallPath = !!library.installPath - const installationPath = library.installPath - ? library.installPath - .replace('$framework', framework.value) - .replace('$libraryId', libraryId) - : 'installation' - - // Add framework hash fragment only for default installation pages (when installPath is not defined) - // Link component adds the # automatically, so we just pass the value without # - const installationHash = !hasCustomInstallPath ? framework.value : undefined + const installationPath = getFrameworkDocsPath(framework.value, library) + const installationHash = getFrameworkDocsHash(framework.value, library) return ( - +
+ +
@@ -54,68 +57,68 @@ export default function AiLanding() { A complete AI ecosystem, not a vendor platform

- TanStack AI is a pure open-source ecosystem of libraries and - standards, not a service. We connect you directly to the AI - providers you choose, with no middleman, no service fees, and no - vendor lock-in. Just powerful, type-safe tools built by and for the - community. + TanStack AI is open-source libraries and AG-UI-compatible standards, + not a hosted gateway. Bring your client framework, your server + runtime, and the AI providers you trust. There is no middleman, no + service fee, and no vendor lock-in, just composable tools built for + teams that want to own their AI stack.

-

Server Agnostic

+

Client Agnostic

- Use any backend server you want. Well-documented protocol with - libraries for TypeScript, PHP, Python, and more. + Use the headless client directly or framework bindings for React, + Vue, Solid, Svelte, and Preact.

-

Client Agnostic

+

AG-UI Native

- Vanilla client library (@tanstack/ai-client) or framework - integrations for React, Solid, and more coming soon. + Client-to-server requests and server-to-client streams use AG-UI, + so compatible clients and servers can interoperate.

-

Service Agnostic

+

Server Agnostic

- Connect to OpenAI, Anthropic, Gemini, and Ollama out of the box. - Create custom adapters for any provider. + Build endpoints in TypeScript, Python, or PHP with portable + helpers for AG-UI events, SSE, and provider message formats.

-

Full Tooling Support

+

Provider Agnostic

- Complete support for client and server tools, including tool - approvals and execution control. + Official adapters cover OpenRouter, OpenAI, Anthropic, Gemini, + Ollama, Groq, Grok/xAI, ElevenLabs, and fal.ai.

-

Thinking & Reasoning

+

Typed Tools

- Full support for thinking and reasoning models with - thinking/reasoning tokens streamed to clients. + Define isomorphic tools once, run them on the client or server, + gate them with approvals, and use provider-native tools safely.

-

Fully Type-Safe

+

Model-Aware Types

- Complete type safety across providers, models, and model options - from end to end. + Provider and model choices narrow options, tools, modalities, and + structured outputs at compile time.

-

Next-Gen DevTools

+

Media Generation

- Amazing developer tools that show you everything happening with - your AI connections in real-time. + Use stable APIs for image, video, speech, transcription, realtime + voice, summarization, and generation hooks.

-

Pure Open Source

+

Observable Runtime

- No hidden service, no fees, no upsells. Community-supported - software connecting you directly to your chosen providers. + Devtools, debug logging, middleware, and observability hooks show + what happened across your AI pipeline.

diff --git a/src/hooks/useAILibraryHeroAnimation.ts b/src/hooks/useAILibraryHeroAnimation.ts index 52f1f425f..deaf1857c 100644 --- a/src/hooks/useAILibraryHeroAnimation.ts +++ b/src/hooks/useAILibraryHeroAnimation.ts @@ -6,20 +6,23 @@ import { SERVICE_GUTTER, } from '~/stores/aiLibraryHeroAnimation' -const FRAMEWORKS_COUNT = 4 -const SERVICES_COUNT = 4 -const SERVERS_COUNT = 4 +const FRAMEWORKS_COUNT = 6 +const SERVICES_COUNT = 9 +const SERVERS_COUNT = 3 + +const getServiceOffset = (serviceIndex: number) => + -(serviceIndex * (SERVICE_WIDTH + SERVICE_GUTTER)) const MESSAGES = [ { user: 'What makes TanStack AI different?', assistant: - 'TanStack AI is completely agnostic - server agnostic, client agnostic, and service agnostic. Use any backend (TypeScript, PHP, Python), any client (vanilla JS, React, Solid), and any AI service (OpenAI, Anthropic, Gemini, Ollama). We provide the libraries and standards, you choose your stack.', + 'TanStack AI is completely agnostic - client agnostic, protocol agnostic, server agnostic, and provider agnostic. Use Vanilla, React, Vue, Solid, Svelte, or Preact clients, speak AG-UI on the wire, run TypeScript, Python, or PHP servers, and connect to the providers you choose.', }, { user: 'Do you support tools?', assistant: - 'Yes! We have full support for both client and server tooling, including tool approvals. You can execute tools on either side with complete type safety and control.', + 'Yes. Define tools once, run them on the client or server, require approvals when needed, and use provider-native tools like web search or code execution when the selected model supports them.', }, { user: 'What about thinking models?', @@ -34,12 +37,12 @@ const MESSAGES = [ { user: 'What about developer experience?', assistant: - 'We have next-generation dev tools that show you everything happening with your AI connection in real-time. Debug, inspect, and optimize with complete visibility.', + 'TanStack AI includes devtools, debug logging, middleware, observability hooks, and AG-UI event streams so you can inspect what happened instead of guessing.', }, { user: 'Is this a service I have to pay for?', assistant: - "No! TanStack AI is pure open source software. We don't have a service to promote or charge for. This is an ecosystem of libraries and standards connecting you with the services you choose - completely community supported.", + 'No. TanStack AI is open-source software and standards, not a hosted gateway. There is no middleman, no service fee, and no provider lock-in.', }, ] @@ -151,66 +154,67 @@ export function useAILibraryHeroAnimation() { : TIMING.rotationSlowBase + (i - (total - 4)) * TIMING.rotationSlowIncrement, onComplete: () => { - // Phase: Select service - store.setPhase(AnimationPhase.SELECTING_SERVICE) - const currentService = - useAILibraryHeroAnimationStore.getState().selectedService - const targetService = getRandomIndex( - SERVICES_COUNT, - currentService ?? undefined, - ) - const serviceRotations = getRandomRotationCount(6, 3) - - let currentServiceIndex = Math.floor(Math.random() * SERVICES_COUNT) - - const rotateService = (iteration: number) => { - if (iteration < serviceRotations - 1) { - store.setRotatingService(currentServiceIndex) - currentServiceIndex = (currentServiceIndex + 1) % SERVICES_COUNT - const delay = - iteration < serviceRotations - 3 - ? TIMING.serviceRotationFast - : TIMING.serviceRotationSlowBase + - (iteration - (serviceRotations - 3)) * - TIMING.serviceRotationSlowIncrement - addTimeout(() => rotateService(iteration + 1), delay) - } else { - store.setRotatingService(targetService) - addTimeout(() => { - store.setSelectedService(targetService) - store.setRotatingService(null) - const targetX = -( - SERVICE_WIDTH / 2 + - SERVICE_GUTTER / 2 + - targetService * (SERVICE_WIDTH + SERVICE_GUTTER) - ) - store.setServiceOffset(targetX) - - addTimeout(() => { - // Phase: Select server - store.setPhase(AnimationPhase.SELECTING_SERVER) - const targetServer = getRandomIndex(SERVERS_COUNT) - const serverRotations = getRandomRotationCount(8, 4) - - runRotation({ - count: serverRotations, - setRotating: store.setRotatingServer, - setSelected: store.setSelectedServer, - targetIndex: targetServer, - itemCount: SERVERS_COUNT, - getDelay: (i, total) => - i < total - 4 - ? TIMING.rotationFast - : TIMING.rotationSlowBase + - (i - (total - 4)) * TIMING.rotationSlowIncrement, - onComplete, - }) - }, TIMING.afterServiceSelection) - }, TIMING.afterSelection) - } - } - - rotateService(0) + // Phase: Select server language + store.setPhase(AnimationPhase.SELECTING_SERVER) + const targetServer = getRandomIndex(SERVERS_COUNT) + const serverRotations = getRandomRotationCount(8, 4) + + runRotation({ + count: serverRotations, + setRotating: store.setRotatingServer, + setSelected: store.setSelectedServer, + targetIndex: targetServer, + itemCount: SERVERS_COUNT, + getDelay: (i, total) => + i < total - 4 + ? TIMING.rotationFast + : TIMING.rotationSlowBase + + (i - (total - 4)) * TIMING.rotationSlowIncrement, + onComplete: () => { + // Phase: Select provider + store.setPhase(AnimationPhase.SELECTING_SERVICE) + const currentService = + useAILibraryHeroAnimationStore.getState().selectedService + const targetService = getRandomIndex( + SERVICES_COUNT, + currentService ?? undefined, + ) + const serviceRotations = getRandomRotationCount(8, 4) + + let currentServiceIndex = Math.floor( + Math.random() * SERVICES_COUNT, + ) + + const rotateService = (iteration: number) => { + if (iteration < serviceRotations - 1) { + store.setRotatingService(currentServiceIndex) + store.setServiceOffset( + getServiceOffset(currentServiceIndex), + ) + currentServiceIndex = + (currentServiceIndex + 1) % SERVICES_COUNT + const delay = + iteration < serviceRotations - 4 + ? TIMING.serviceRotationFast + : TIMING.serviceRotationSlowBase + + (iteration - (serviceRotations - 4)) * + TIMING.serviceRotationSlowIncrement + addTimeout(() => rotateService(iteration + 1), delay) + } else { + store.setRotatingService(targetService) + store.setServiceOffset(getServiceOffset(targetService)) + addTimeout(() => { + store.setSelectedService(targetService) + store.setRotatingService(null) + store.setServiceOffset(getServiceOffset(targetService)) + addTimeout(onComplete, TIMING.afterServiceSelection) + }, TIMING.afterSelection) + } + } + + rotateService(0) + }, + }) }, }) }, TIMING.phaseTransition) diff --git a/src/images/ag-ui-dark.svg b/src/images/ag-ui-dark.svg new file mode 100644 index 000000000..f42df842e --- /dev/null +++ b/src/images/ag-ui-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/ag-ui-light.svg b/src/images/ag-ui-light.svg new file mode 100644 index 000000000..26dcf3b51 --- /dev/null +++ b/src/images/ag-ui-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/elevenlabs-dark.svg b/src/images/elevenlabs-dark.svg new file mode 100644 index 000000000..ee1bc55fa --- /dev/null +++ b/src/images/elevenlabs-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/images/elevenlabs-light.svg b/src/images/elevenlabs-light.svg new file mode 100644 index 000000000..6d7567e29 --- /dev/null +++ b/src/images/elevenlabs-light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/images/fal-ai-dark.svg b/src/images/fal-ai-dark.svg new file mode 100644 index 000000000..2b275de7f --- /dev/null +++ b/src/images/fal-ai-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/fal-ai-light.svg b/src/images/fal-ai-light.svg new file mode 100644 index 000000000..501523a46 --- /dev/null +++ b/src/images/fal-ai-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/groq-dark.svg b/src/images/groq-dark.svg new file mode 100644 index 000000000..9d7d5972f --- /dev/null +++ b/src/images/groq-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/groq-light.svg b/src/images/groq-light.svg new file mode 100644 index 000000000..dc013d75e --- /dev/null +++ b/src/images/groq-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/xai-dark.svg b/src/images/xai-dark.svg new file mode 100644 index 000000000..6e0f201a8 --- /dev/null +++ b/src/images/xai-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/xai-light.svg b/src/images/xai-light.svg new file mode 100644 index 000000000..f52def290 --- /dev/null +++ b/src/images/xai-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/libraries/ai.tsx b/src/libraries/ai.tsx index 967b6a5b5..8b38314a9 100644 --- a/src/libraries/ai.tsx +++ b/src/libraries/ai.tsx @@ -15,35 +15,34 @@ export const aiProject = { defaultDocs: 'getting-started/overview', featureHighlights: [ { - title: 'Multi-Provider Support', + title: 'Provider Agnostic', icon: , description: (
- Support for OpenAI, Anthropic, Ollama, and Google Gemini. Switch - providers at runtime without code changes. No vendor lock-in, just - clean TypeScript. + Official adapters for OpenRouter, OpenAI, Anthropic, Gemini, Ollama, + Groq, Grok/xAI, ElevenLabs, and fal.ai. Import only the adapters your + app needs.
), }, { - title: 'Unified API', + title: 'AG-UI Native Clients', icon: , description: (
- Same interface across all providers. Standalone functions with - automatic type inference from adapters. Framework-agnostic client for - any JavaScript environment. + A headless client plus React, Vue, Solid, Svelte, and Preact bindings + all speak the same AG-UI request and event protocol.
), }, { - title: 'Tool/Function Calling', + title: 'Typed Tools & Media', icon: , description: (
- Automatic execution loop with no manual tool management needed. - Type-safe tool definitions with structured outputs and streaming - support. + Type-safe client/server tools, provider-native tools, structured + output, reasoning streams, image, speech, transcription, realtime + voice, and video generation.
), }, diff --git a/src/libraries/frameworkSupport.ts b/src/libraries/frameworkSupport.ts new file mode 100644 index 000000000..f6edacfbe --- /dev/null +++ b/src/libraries/frameworkSupport.ts @@ -0,0 +1,55 @@ +import type { Framework, LibraryId, LibrarySlim } from './types' + +export function getFrameworkPackageName( + framework: Framework, + libraryId: LibraryId, + library: LibrarySlim, +) { + const packageName = library.frameworkPackageNames?.[framework] + + if (packageName) { + return packageName + } + + if (framework === 'vanilla') { + return library.corePackageName ?? `@tanstack/${libraryId}` + } + + if (framework === 'angular' && libraryId === 'query') { + return '@tanstack/angular-query-experimental' + } + + return `@tanstack/${framework}-${libraryId}` +} + +export function getFrameworkDocsPath( + framework: Framework, + library: LibrarySlim, +) { + const docsPath = library.frameworkDocs?.[framework] + + if (docsPath) { + return docsPath + } + + if (library.installPath) { + return library.installPath + .replace('$framework', framework) + .replace('$libraryId', library.id) + } + + return 'installation' +} + +export function getFrameworkDocsHash( + framework: Framework, + library: LibrarySlim, +) { + const hasCustomDocsPath = Boolean(library.frameworkDocs?.[framework]) + + if (hasCustomDocsPath || library.installPath) { + return undefined + } + + return framework +} diff --git a/src/libraries/libraries.ts b/src/libraries/libraries.ts index f3d305fd7..0f0634751 100644 --- a/src/libraries/libraries.ts +++ b/src/libraries/libraries.ts @@ -612,11 +612,27 @@ export const ai: LibrarySlim = { colorTo: 'to-pink-700', bgRadial: 'from-pink-500 via-pink-700/50 to-transparent', repo: 'tanstack/ai', - frameworks: ['react', 'solid', 'vanilla'], + frameworks: ['react', 'vue', 'solid', 'svelte', 'preact', 'vanilla'], latestVersion: 'v0', latestBranch: 'main', availableVersions: ['v0'], defaultDocs: 'getting-started/overview', + frameworkPackageNames: { + react: '@tanstack/ai-react', + vue: '@tanstack/ai-vue', + solid: '@tanstack/ai-solid', + svelte: '@tanstack/ai-svelte', + preact: '@tanstack/ai-preact', + vanilla: '@tanstack/ai-client', + }, + frameworkDocs: { + react: 'getting-started/quick-start', + vue: 'getting-started/quick-start-vue', + solid: 'api/ai-solid', + svelte: 'getting-started/quick-start-svelte', + preact: 'api/ai-preact', + vanilla: 'api/ai-client', + }, sitemap: { includeLandingPage: true, includeDocsPages: true, diff --git a/src/libraries/types.ts b/src/libraries/types.ts index 081cf2f4e..20478a550 100644 --- a/src/libraries/types.ts +++ b/src/libraries/types.ts @@ -72,6 +72,8 @@ export type LibrarySlim = { legacyPackages?: string[] installPath?: string corePackageName?: string + frameworkPackageNames?: Partial> + frameworkDocs?: Partial> handleRedirects?: (href: string) => void /** * If false, the library is hidden from sidebar navigation and pages have noindex meta tag. diff --git a/src/routes/$libraryId/$version.docs.framework.index.tsx b/src/routes/$libraryId/$version.docs.framework.index.tsx index 9a02b7476..a3171e647 100644 --- a/src/routes/$libraryId/$version.docs.framework.index.tsx +++ b/src/routes/$libraryId/$version.docs.framework.index.tsx @@ -8,28 +8,12 @@ import { FrameworkCard } from '~/components/FrameworkCard' import { GithubIcon } from '~/components/icons/GithubIcon' import { DiscordIcon } from '~/components/icons/DiscordIcon' import { Card } from '~/components/Card' +import { getFrameworkPackageName } from '~/libraries/frameworkSupport' export const Route = createFileRoute('/$libraryId/$version/docs/framework/')({ component: RouteComponent, }) -function getPackageName( - frameworkValue: string, - libraryId: string, - library: ReturnType, -): string { - if (frameworkValue === 'vanilla') { - // For vanilla, use corePackageName if provided, otherwise just libraryId - return library.corePackageName ?? `@tanstack/${libraryId}` - } - // Special case: Angular Query uses experimental package - if (frameworkValue === 'angular' && libraryId === 'query') { - return `@tanstack/angular-query-experimental` - } - // For other frameworks, use {framework}-{libraryId} pattern (e.g., @tanstack/react-table) - return `@tanstack/${frameworkValue}-${libraryId}` -} - function RouteComponent() { const { libraryId, version } = Route.useParams() const library = getLibrary(libraryId) @@ -58,7 +42,7 @@ function RouteComponent() { )} > {frameworks.map((framework, i) => { - const packageName = getPackageName( + const packageName = getFrameworkPackageName( framework.value, libraryId, library, diff --git a/src/stores/aiLibraryHeroAnimation.ts b/src/stores/aiLibraryHeroAnimation.ts index 0971c69d2..3e59b29a5 100644 --- a/src/stores/aiLibraryHeroAnimation.ts +++ b/src/stores/aiLibraryHeroAnimation.ts @@ -4,8 +4,8 @@ export enum AnimationPhase { STARTING = 'STARTING', DESELECTING = 'DESELECTING', SELECTING_FRAMEWORK = 'SELECTING_FRAMEWORK', - SELECTING_SERVICE = 'SELECTING_SERVICE', SELECTING_SERVER = 'SELECTING_SERVER', + SELECTING_SERVICE = 'SELECTING_SERVICE', SHOWING_CHAT = 'SHOWING_CHAT', PULSING_CONNECTIONS = 'PULSING_CONNECTIONS', STREAMING_RESPONSE = 'STREAMING_RESPONSE', @@ -13,43 +13,42 @@ export enum AnimationPhase { } export const SVG_WIDTH = 632 -export const SVG_HEIGHT = 432 +export const SVG_HEIGHT = 760 export const BOX_FONT_SIZE = 18 export const BOX_FONT_WEIGHT = 700 -export const SERVICE_WIDTH = 160 -export const SERVICE_GUTTER = 20 -export const SERVICE_LOCATIONS = [0, 1, 2, 3].map( +export const SERVICE_WIDTH = 142 +export const SERVICE_GUTTER = 16 +export const SERVICE_LOCATIONS = [0, 1, 2, 3, 4, 5, 6, 7, 8].map( (index) => - SERVICE_WIDTH * 2 + - index * (SERVICE_WIDTH + SERVICE_GUTTER) + - SERVICE_GUTTER / 2, + SVG_WIDTH / 2 - + SERVICE_WIDTH / 2 + + index * (SERVICE_WIDTH + SERVICE_GUTTER), ) -export const SERVICE_Y_OFFSET = 265 +export const SERVICE_Y_OFFSET = 670 export const SERVICE_HEIGHT = 40 export const SERVICE_Y_CENTER = SERVICE_Y_OFFSET + SERVICE_HEIGHT / 2 -export const LIBRARY_CARD_WIDTH = 140 -export const LIBRARY_CARD_HEIGHT = 60 -export const LIBRARY_CARD_GUTTER = 20 -const LIBRARY_CARD_START_X = -20 -export const LIBRARY_CARD_LOCATIONS = [0, 1, 2, 3].map( +export const LIBRARY_CARD_WIDTH = 88 +export const LIBRARY_CARD_HEIGHT = 52 +export const LIBRARY_CARD_GUTTER = 14 +export const LIBRARY_CARD_Y_OFFSET = 24 +const LIBRARY_CARD_START_X = + (SVG_WIDTH - LIBRARY_CARD_WIDTH * 6 - LIBRARY_CARD_GUTTER * 5) / 2 +export const LIBRARY_CARD_LOCATIONS = [0, 1, 2, 3, 4, 5].map( (index) => - LIBRARY_CARD_START_X + - index * (LIBRARY_CARD_WIDTH + LIBRARY_CARD_GUTTER) + - LIBRARY_CARD_GUTTER / 2, + LIBRARY_CARD_START_X + index * (LIBRARY_CARD_WIDTH + LIBRARY_CARD_GUTTER), ) -export const SERVER_CARD_WIDTH = 140 -export const SERVER_CARD_HEIGHT = 60 -export const SERVER_CARD_GUTTER = 20 -const SERVER_CARD_START_X = -20 -export const SERVER_CARD_LOCATIONS = [0, 1, 2, 3].map( +export const SERVER_CARD_WIDTH = 146 +export const SERVER_CARD_HEIGHT = 54 +export const SERVER_CARD_GUTTER = 24 +const SERVER_CARD_START_X = + (SVG_WIDTH - SERVER_CARD_WIDTH * 3 - SERVER_CARD_GUTTER * 2) / 2 +export const SERVER_CARD_LOCATIONS = [0, 1, 2].map( (index) => - SERVER_CARD_START_X + - index * (SERVER_CARD_WIDTH + SERVER_CARD_GUTTER) + - SERVER_CARD_GUTTER / 2, + SERVER_CARD_START_X + index * (SERVER_CARD_WIDTH + SERVER_CARD_GUTTER), ) -export const SERVER_CARD_Y_OFFSET = 370 +export const SERVER_CARD_Y_OFFSET = 500 export type ChatMessage = { id: string @@ -107,7 +106,7 @@ export const useAILibraryHeroAnimationStore = create< rotatingFramework: null, rotatingServer: null, rotatingService: null, - serviceOffset: 0 - SERVICE_WIDTH / 2 - SERVICE_GUTTER / 2, + serviceOffset: 0, messages: [], currentMessageIndex: -1, typingUserMessage: '', diff --git a/src/utils/documents.server.ts b/src/utils/documents.server.ts index dfe920858..d446dd8cf 100644 --- a/src/utils/documents.server.ts +++ b/src/utils/documents.server.ts @@ -1,6 +1,7 @@ import fs from 'node:fs' import fsp from 'node:fs/promises' import path from 'node:path' +import { fileURLToPath } from 'node:url' import * as graymatter from 'gray-matter' import { fetchCached } from '~/utils/cache.server' import { @@ -125,6 +126,14 @@ function isValidFilepath(filepath: string): boolean { ) } +function getLocalRepoBaseDir(repo: string) { + return path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../..', + repo, + ) +} + /** * Return text content of file from local file system */ @@ -134,8 +143,7 @@ async function fetchFs(repo: string, filepath: string) { return '' } - const dirname = import.meta.url.split('://').at(-1)! - const baseDir = path.resolve(dirname, `../../../../${repo}`) + const baseDir = getLocalRepoBaseDir(repo) const localFilePath = path.resolve(baseDir, filepath) if (!localFilePath.startsWith(baseDir)) { @@ -855,9 +863,8 @@ async function fetchApiContentsFs( startingPath: string, ): Promise | null> { const [_, repo] = repoPair.split('/') - const dirname = import.meta.url.split('://').at(-1)! - const base = path.resolve(dirname, `../../../../${repo}`) + const base = getLocalRepoBaseDir(repo) const fsStartPath = path.join(base, removeLeadingSlash(startingPath)) const dirsAndFilesToIgnore = [ diff --git a/tests/ai-framework-doc-links.test.ts b/tests/ai-framework-doc-links.test.ts new file mode 100644 index 000000000..846ea331f --- /dev/null +++ b/tests/ai-framework-doc-links.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict' +import { ai } from '../src/libraries/libraries' +import type { Framework } from '../src/libraries/types' +import { + getFrameworkDocsPath, + getFrameworkPackageName, +} from '../src/libraries/frameworkSupport' + +const expectedFrameworks: Framework[] = [ + 'react', + 'vue', + 'solid', + 'svelte', + 'preact', + 'vanilla', +] + +const expectedPackages: Partial> = { + react: '@tanstack/ai-react', + vue: '@tanstack/ai-vue', + solid: '@tanstack/ai-solid', + svelte: '@tanstack/ai-svelte', + preact: '@tanstack/ai-preact', + vanilla: '@tanstack/ai-client', +} + +const expectedDocsPaths: Partial> = { + react: 'getting-started/quick-start', + vue: 'getting-started/quick-start-vue', + solid: 'api/ai-solid', + svelte: 'getting-started/quick-start-svelte', + preact: 'api/ai-preact', + vanilla: 'api/ai-client', +} + +assert.deepEqual( + ai.frameworks, + expectedFrameworks, + 'AI framework list reflects shipped framework packages', +) + +for (const framework of expectedFrameworks) { + assert.equal( + getFrameworkPackageName(framework, ai.id, ai), + expectedPackages[framework], + `${framework} package name points to the shipped AI package`, + ) + + assert.equal( + getFrameworkDocsPath(framework, ai), + expectedDocsPaths[framework], + `${framework} framework card links to an existing AI docs page`, + ) +} + +console.log('ai framework doc link tests passed')