diff --git a/src/components/LibraryStatusBadge.tsx b/src/components/LibraryStatusBadge.tsx index 8fc1d04f3..5cedd6e42 100644 --- a/src/components/LibraryStatusBadge.tsx +++ b/src/components/LibraryStatusBadge.tsx @@ -11,15 +11,18 @@ export function LibraryStatusBadge({ badge: LibraryBadge className?: string }) { - // Status badges are intentionally neutral: the word (ALPHA / BETA / RC…) - // carries the meaning, color carries none. A single quiet chip never competes - // with a library's brand color and reads consistently across every surface. + const isRc = badge.toUpperCase() === 'RC' + + // ALPHA / BETA stay quiet. RC is the current ship state, so it uses the + // library accent and a slow glow. Reduced-motion users still get the fill. return ( diff --git a/src/components/landing/AiCampaignHero.css b/src/components/landing/AiCampaignHero.css new file mode 100644 index 000000000..c017aa2c2 --- /dev/null +++ b/src/components/landing/AiCampaignHero.css @@ -0,0 +1,561 @@ +.ai-campaign-hero { + min-width: 0; + overflow: hidden; + border: 1px solid var(--color-border-default); + border-radius: 14px; + background: var(--color-background-surface); + box-shadow: 0 12px 30px -24px rgb(0 0 0 / 0.3); +} +.ai-campaign-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 19px 20px; +} +.ai-campaign-heading > span:first-child { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; +} +.ai-campaign-heading svg { + color: var(--landing-accent-bright); +} +.ai-campaign-heading > span:last-child { + font-size: 10px; + color: var(--color-text-secondary); +} +.ai-campaign-steps { + display: flex; + align-items: center; + padding: 0 12px 12px; + border-bottom: 1px solid var(--color-border-subtle); + gap: 8px; +} +.ai-campaign-steps button { + display: flex; + flex: 1 1 0; + min-width: 0; + min-height: 40px; + align-items: center; + justify-content: center; + gap: 7px; + padding: 10px 12px; + border: 1px solid var(--color-border-default); + border-radius: 8px; + background: var(--color-background-default); + color: var(--color-text-primary); + font-size: 12px; + font-weight: 600; + cursor: pointer; +} +.ai-campaign-steps button:hover { + border-color: var(--landing-accent); + background: rgb(var(--landing-glow) / 0.08); + color: var(--landing-accent-bright); +} +.ai-campaign-steps button[aria-pressed='true'] { + border-color: transparent; + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--landing-accent) 84%, black), + color-mix(in srgb, var(--landing-accent) 52%, black) + ); + color: #fff; +} +.ai-campaign-steps button[aria-pressed='true']:hover { + border-color: transparent; + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--landing-accent) 84%, black), + color-mix(in srgb, var(--landing-accent) 52%, black) + ); + color: #fff; +} +.ai-campaign-step-arrow { + flex-shrink: 0; + color: var(--color-text-secondary); + opacity: 0.45; +} +@media (prefers-reduced-motion: no-preference) { + .ai-campaign-steps button { + transition-property: color, background-color, border-color, transform; + transition-duration: 150ms; + transition-timing-function: cubic-bezier(0.2, 0, 0, 1); + } + .ai-campaign-steps button:active { + transform: scale(0.96); + } + .ai-campaign-msg { + animation: ai-campaign-msg-in 280ms cubic-bezier(0.2, 0, 0, 1) both; + } + .ai-campaign-typing > span { + animation: ai-campaign-dot 1s ease-in-out infinite; + } + .ai-campaign-typing > span:nth-child(2) { + animation-delay: 150ms; + } + .ai-campaign-typing > span:nth-child(3) { + animation-delay: 300ms; + } +} +@keyframes ai-campaign-msg-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes ai-campaign-dot { + 0%, + 80%, + 100% { + opacity: 0.25; + transform: translateY(0); + } + 40% { + opacity: 1; + transform: translateY(-2px); + } +} +.ai-campaign-hero button:focus-visible, +.ai-campaign-hero select:focus-visible { + outline: 2px solid var(--landing-accent-bright); + outline-offset: 2px; +} +.ai-campaign-workspace { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + grid-template-rows: minmax(28rem, auto) auto; +} +.ai-campaign-code, +.ai-campaign-chat { + display: grid; + grid-template-rows: subgrid; + grid-row: 1 / span 2; + min-width: 0; +} +.ai-campaign-chat { + background: var(--color-background-surface); +} +.ai-campaign-code-main { + min-width: 0; +} +.ai-campaign-chat-main { + display: flex; + flex-direction: column; + min-width: 0; + height: 0; + min-height: 100%; + overflow: hidden; +} +.ai-campaign-chat-header { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid var(--color-border-subtle); +} +.ai-campaign-chat-avatar { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 999px; + background: rgb(var(--landing-glow) / 0.16); + color: var(--landing-accent-bright); + flex-shrink: 0; +} +.ai-campaign-row-assistant > .ai-campaign-chat-avatar { + width: 28px; + height: 28px; + margin-top: 2px; +} +.ai-campaign-chat-title { + min-width: 0; + flex: 1; +} +.ai-campaign-chat-title > span:first-child { + display: block; + font-size: 14px; + font-weight: 600; +} +.ai-campaign-chat-title > span:last-child { + display: block; + font-size: 11px; + color: var(--color-text-secondary); +} +.ai-campaign-chat-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--color-text-secondary); +} +.ai-campaign-chat-status > span:first-child { + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--landing-accent-bright); +} +.ai-campaign-messages { + flex: 1 1 0%; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + display: flex; + flex-direction: column; + gap: 14px; + padding: 16px; + scroll-padding-bottom: 12px; +} +.ai-campaign-bubble-user { + align-self: flex-end; + max-width: 85%; + border-radius: 18px 18px 6px 18px; + padding: 10px 14px; + background: rgb(var(--landing-glow) / 0.12); + font-size: 13px; + line-height: 1.55; +} +.ai-campaign-row-assistant { + display: flex; + align-items: flex-start; + gap: 10px; + max-width: 100%; +} +.ai-campaign-assistant-body { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; + font-size: 13px; + line-height: 1.55; +} +.ai-campaign-typing { + display: flex; + align-items: center; + gap: 5px; + padding: 10px 4px; +} +.ai-campaign-typing > span { + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--landing-accent-bright); + opacity: 0.35; +} +.ai-campaign-composer { + display: flex; + align-items: center; + margin: 0; + padding: 14px 15px; + border-top: 1px solid var(--color-border-subtle); + background: var(--color-background-surface); +} +.ai-campaign-composer-field { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 10px 8px 14px; + border: 1px solid var(--color-border-default); + border-radius: 12px; + background: var(--color-background-default); + color: var(--color-text-secondary); + font-size: 13px; +} +.ai-campaign-composer-field > span:first-child { + min-width: 0; + flex: 1; +} +.ai-campaign-composer-field > span:last-child { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + background: rgb(var(--landing-glow) / 0.12); + color: var(--color-text-secondary); +} +.ai-campaign-attachment { + display: flex; + align-items: center; + gap: 7px; + margin-top: 9px; + border-top: 1px solid var(--color-border-subtle); + padding-top: 8px; + font-size: 11px; + color: var(--color-text-secondary); +} +.ai-campaign-attachment img { + width: 32px; + height: 28px; + object-fit: cover; + border-radius: 4px; +} +.ai-campaign-output { + overflow: hidden; + border: 1px solid var(--color-border-default); + border-radius: 12px; +} +.ai-campaign-media { + position: relative; + width: 100%; + height: 12rem; + overflow: hidden; + background: #d5e9f4; +} +.ai-campaign-media > video, +.ai-campaign-media > img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + object-position: 15% center; +} +.ai-campaign-art-direction { + pointer-events: none; + position: absolute; + inset: 18px 16px auto; + color: #162f37; +} +.ai-campaign-art-direction > span { + font-size: 10px; + font-weight: 600; +} +.ai-campaign-art-direction > p { + margin-top: 13px; + max-width: 175px; + font-size: 25px; + font-weight: 600; + line-height: 1.05; + letter-spacing: -1px; +} +.ai-campaign-output-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 9px 10px; + font-size: 9px; + color: var(--color-text-secondary); +} +.ai-campaign-output-meta > span:first-child { + display: flex; + align-items: center; + gap: 4px; +} +.ai-campaign-output-meta svg { + color: var(--landing-accent-bright); +} +.ai-campaign-brief { + border: 1px solid var(--color-border-default); + border-radius: 12px; + padding: 16px 14px; +} +.ai-campaign-brief > span { + font-size: 10px; + color: var(--color-text-secondary); +} +.ai-campaign-brief h3 { + margin: 16px 0 12px; + font-size: 27px; + letter-spacing: -0.8px; + line-height: 1.1; +} +.ai-campaign-brief p { + font-size: 11px; + line-height: 1.7; + color: var(--color-text-secondary); +} +.ai-campaign-brief > div { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 17px; +} +.ai-campaign-brief > div > span { + padding: 4px 7px; + background: rgb(var(--landing-glow) / 0.08); + border-radius: 4px; + font-size: 9px; + color: var(--landing-accent-bright); +} +.ai-campaign-brief hr { + margin: 19px 0 13px; + border-color: var(--color-border-subtle); +} +.ai-campaign-code { + border-right: 1px solid var(--color-border-subtle); + background: var(--color-background-surface); +} +.ai-campaign-code-heading { + display: flex; + align-items: center; + gap: 7px; + padding: 17px 15px 12px; + font-family: var(--font-ds-mono); + font-size: 10px; + color: var(--color-text-secondary); +} +.ai-campaign-code-tabs { + display: flex; + gap: 14px; + margin: 0 15px; + border-bottom: 1px solid var(--color-border-subtle); +} +.ai-campaign-code-tabs button { + padding: 6px 0 9px; + font-size: 10px; + color: var(--color-text-secondary); + cursor: pointer; + border-bottom: 1px solid transparent; +} +.ai-campaign-code-tabs button[aria-pressed='true'] { + color: var(--landing-accent-bright); + border-bottom-color: var(--landing-accent-bright); +} +.ai-campaign-code pre { + max-height: 28rem; + overflow: auto; + background: var(--color-background-default); +} +.ai-campaign-hook-note { + padding: 0 16px 19px; +} +.ai-campaign-hook-note > span { + font-family: var(--font-ds-mono); + font-size: 11px; + color: var(--landing-accent-bright); +} +.ai-campaign-hook-note p { + margin-top: 7px; + font-size: 11px; + line-height: 1.65; + color: var(--color-text-secondary); +} +.ai-campaign-providers { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + padding: 14px 15px; + border-top: 1px solid var(--color-border-subtle); + background: rgb(var(--landing-glow) / 0.08); +} +.ai-campaign-providers > p:first-child { + font-size: 12px; + font-weight: 600; + color: var(--color-text-primary); +} +.ai-campaign-provider-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.ai-campaign-provider-chips button { + padding: 7px 10px; + border: 1px solid var(--color-border-default); + border-radius: 8px; + background: var(--color-background-surface); + color: var(--color-text-primary); + font-size: 11px; + font-weight: 600; + cursor: pointer; +} +.ai-campaign-provider-chips button:hover { + border-color: var(--landing-accent); + color: var(--landing-accent-bright); +} +.ai-campaign-provider-chips button[aria-pressed='true'] { + border-color: transparent; + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--landing-accent) 84%, black), + color-mix(in srgb, var(--landing-accent) 52%, black) + ); + color: #fff; +} +.ai-campaign-providers > p:last-child { + font-size: 11px; + color: var(--color-text-secondary); +} +.ai-campaign-caption { + display: flex; + justify-content: space-between; + gap: 10px; + padding: 13px 18px; + border-top: 1px solid var(--color-border-subtle); + font-size: 10px; +} +.ai-campaign-caption > span:last-child { + color: var(--color-text-secondary); +} +@media (max-width: 639px) { + .ai-campaign-heading { + padding: 16px 13px; + } + .ai-campaign-heading > span:first-child { + font-size: 13px; + } + .ai-campaign-heading > span:last-child { + font-size: 9px; + } + .ai-campaign-steps { + padding-inline: 7px; + gap: 4px; + } + .ai-campaign-steps button { + font-size: 10px; + gap: 5px; + padding-inline: 6px; + } + .ai-campaign-steps button > svg:first-child { + width: 13px; + } + .ai-campaign-workspace { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto; + } + .ai-campaign-code, + .ai-campaign-chat { + display: flex; + flex-direction: column; + grid-row: auto; + } + .ai-campaign-code { + border-right: 0; + border-bottom: 1px solid var(--color-border-subtle); + } + .ai-campaign-chat-main { + height: 24rem; + min-height: 24rem; + } + .ai-campaign-media { + height: 10rem; + } + .ai-campaign-art-direction > p { + font-size: 25px; + } + .ai-campaign-code pre { + max-height: 22rem; + } + .ai-campaign-provider-chips button { + padding: 6px 8px; + font-size: 10px; + } + .ai-campaign-caption { + padding-inline: 13px; + font-size: 9px; + } +} diff --git a/src/components/landing/AiCampaignHero.tsx b/src/components/landing/AiCampaignHero.tsx new file mode 100644 index 000000000..f6a0f2531 --- /dev/null +++ b/src/components/landing/AiCampaignHero.tsx @@ -0,0 +1,728 @@ +import * as React from 'react' +import { + ArrowRightIcon, + CheckIcon, + CodeIcon, + FilmSlateIcon, + ImageIcon, + PencilLineIcon, + SparkleIcon, +} from '@phosphor-icons/react' +import { CodeBlock } from '~/components/markdown/CodeBlock' +import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' +import './AiCampaignHero.css' + +type CampaignProvider = { + adapter: string + model: string + name: string + pkg: string + provider: string +} + +type CampaignStep = { + activity: 'chat' | 'generateImage' | 'generateVideo' + clientCode: string + file: string + hook: string + icon: typeof PencilLineIcon + prompt: string + providers: Array + reply: string + route: '/api/chat' | '/api/image' | '/api/video' + serverFile: string + title: string +} + +const campaignSteps: Array = [ + { + title: 'The brief', + icon: PencilLineIcon, + hook: 'useChat', + prompt: + 'A launch campaign for TanStack AI. Something that feels like possibility, not another robot.', + reply: 'Let’s give your next big idea a little room to grow.', + file: 'src/components/campaign.tsx', + serverFile: 'src/routes/api/chat.ts', + activity: 'chat', + route: '/api/chat', + providers: [ + { + name: 'Anthropic', + adapter: 'anthropicText', + model: 'claude-fable-5-1', + pkg: '@tanstack/ai-anthropic', + provider: 'Anthropic', + }, + { + name: 'Grok', + adapter: 'grokText', + model: 'grok-4.6', + pkg: '@tanstack/ai-grok', + provider: 'xAI', + }, + { + name: 'OpenAI', + adapter: 'openaiText', + model: 'gpt-6-astra', + pkg: '@tanstack/ai-openai', + provider: 'OpenAI', + }, + ], + clientCode: `import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' + +const brief = + 'A launch campaign for TanStack AI. ' + + 'Something that feels like possibility, not another robot.' + +export function Campaign() { + const { messages, sendMessage, isLoading } = useChat({ + connection: fetchServerSentEvents('/api/chat'), + }) + + return ( +
+ {messages.map((message) => ( +

+ {message.parts + .filter((part) => part.type === 'text') + .map((part) => part.content) + .join('')} +

+ ))} + +
+ ) +}`, + }, + { + title: 'The artwork', + icon: ImageIcon, + hook: 'useGenerateImage', + prompt: + 'Give it a visual. A single palm tree against a pastel sky. Leave room for the idea.', + reply: 'One tree. An open sky. A world of possibility.', + file: 'src/components/artwork.tsx', + serverFile: 'src/routes/api/image.ts', + activity: 'generateImage', + route: '/api/image', + providers: [ + { + name: 'GPT Image 2.5', + adapter: 'openaiImage25', + model: 'gpt-image-2.5-sunburst', + pkg: '@tanstack/ai-openai', + provider: 'OpenAI', + }, + { + name: 'Nano Banana 2', + adapter: 'geminiImage', + model: 'gemini-3.1-flash-image', + pkg: '@tanstack/ai-gemini', + provider: 'Google', + }, + { + name: 'Grok Imagine', + adapter: 'grokImage', + model: 'grok-imagine-image-2.0', + pkg: '@tanstack/ai-grok', + provider: 'xAI', + }, + ], + clientCode: `import { fetchServerSentEvents, useGenerateImage } from '@tanstack/ai-react' + +const prompt = + 'Give it a visual. A single palm tree against a pastel sky. ' + + 'Leave room for the idea.' + +export function Artwork() { + const { generate, result, isLoading } = useGenerateImage({ + connection: fetchServerSentEvents('/api/image'), + }) + + return ( +
+ + {result?.images.map((image, index) => ( + + ))} +
+ ) +}`, + }, + { + title: 'The launch film', + icon: FilmSlateIcon, + hook: 'useGenerateVideo', + prompt: + 'Now make it move. A short launch film, with the palm swaying in a soft breeze.', + reply: 'Same idea. A little more life.', + file: 'src/components/launch-film.tsx', + serverFile: 'src/routes/api/video.ts', + activity: 'generateVideo', + route: '/api/video', + providers: [ + { + name: 'Seedance 2.5', + adapter: 'byteplusVideo', + model: 'dreamina-seedance-2-5-260628', + pkg: '@tanstack/ai-byteplus', + provider: 'BytePlus', + }, + { + name: 'Google Omni', + adapter: 'geminiVideo', + model: 'gemini-omni-1.1-flash', + pkg: '@tanstack/ai-gemini', + provider: 'Google', + }, + { + name: 'MiniMax H3', + adapter: 'falVideo', + model: 'minimax/h3/image-to-video', + pkg: '@tanstack/ai-fal', + provider: 'fal', + }, + ], + clientCode: `import { fetchServerSentEvents, useGenerateVideo } from '@tanstack/ai-react' + +const prompt = + 'Now make it move. A short launch film, ' + + 'with the palm swaying in a soft breeze.' + +export function LaunchFilm() { + const { generate, result, videoStatus, isLoading } = useGenerateVideo({ + connection: fetchServerSentEvents('/api/video'), + }) + + return ( +
+ + {result?.url ?
+ ) +}`, + }, +] + +function campaignServerCode(step: CampaignStep, provider: CampaignProvider) { + const isImage25 = provider.adapter === 'openaiImage25' + const adapterCall = isImage25 + ? `openaiImage25('${provider.model}')` + : `${provider.adapter}('${provider.model}')` + + const imports = isImage25 + ? `import { + createModel, + extendAdapter, + generateImage, + toServerSentEventsResponse, +} from '@tanstack/ai' +import { openaiImage } from '@tanstack/ai-openai' +import { createFileRoute } from '@tanstack/react-router'` + : `import { ${step.activity}, toServerSentEventsResponse } from '@tanstack/ai' +import { ${provider.adapter} } from '${provider.pkg}' +import { createFileRoute } from '@tanstack/react-router'` + + const setup = isImage25 + ? ` +const openaiImage25 = extendAdapter( + openaiImage, + [createModel('gpt-image-2.5-sunburst', ['text', 'image'])], +) +` + : '' + + let bodyAndCall = '' + if (step.activity === 'chat') { + bodyAndCall = ` const { messages } = await request.json() + + const stream = chat({ + adapter: ${adapterCall}, + messages, + })` + } else if (step.activity === 'generateVideo') { + bodyAndCall = ` const { prompt, artworkUrl } = await request.json() + + const stream = generateVideo({ + adapter: ${adapterCall}, + prompt: [ + { type: 'text', content: prompt }, + { + type: 'image', + source: { + type: 'url', + value: artworkUrl, + }, + }, + ], + stream: true, + })` + } else { + bodyAndCall = ` const { prompt } = await request.json() + + const stream = generateImage({ + adapter: ${adapterCall}, + prompt, + stream: true, + })` + } + + return `${imports} +${setup} +export const Route = createFileRoute('${step.route}')({ + server: { + handlers: { + POST: async ({ request }) => { +${bodyAndCall} + + return toServerSentEventsResponse(stream) + }, + }, + }, +})` +} + +function campaignNote(step: CampaignStep, showServer: boolean) { + if (showServer) { + return 'A TanStack Start server route. Swap the adapter, keep the handler.' + } + if (step.activity === 'generateVideo') { + return 'A React component. From job progress to a playable video.' + } + if (step.activity === 'generateImage') { + return 'A React component. Prompt in. Image out. Loading state included.' + } + return 'A React component. Messages, streaming, and send. Ready for your UI.' +} + +type ChatPhase = 'user' | 'pending' | 'result' + +function stageView( + index: number, + stage: number, + phase: ChatPhase, +): ChatPhase | 'hidden' { + if (index < stage) { + return 'result' + } + if (index > stage) { + return 'hidden' + } + return phase +} + +function CampaignTyping() { + return ( +
+ + + +
+ ) +} + +function CampaignStageOutput({ + reducedMotion, + stage, +}: { + reducedMotion: boolean | null + stage: number +}) { + if (stage === 0) { + return ( +
+ Campaign idea +

+ Build something +
+ that moves you. +

+

+ The tools for whatever’s next. +
+ The freedom to make it yours. +

+
+ Open skies + Endless possibilities +
+
+ ) + } + + return ( +
+
+ {stage === 2 ? ( + + ) : ( + A palm tree against a blue and pink sky, with the campaign headline Build something that moves you + )} + +
+
+ + {' '} + {stage === 2 ? 'Launch film' : 'Campaign artwork'} + + {stage === 2 ? '0:05 · MP4' : 'Art direction · WebP'} +
+
+ ) +} + +function CampaignChatPanel({ + phase, + reducedMotion, + stage, +}: { + phase: ChatPhase + reducedMotion: boolean | null + stage: number +}) { + const scrollerRef = React.useRef(null) + + React.useEffect(() => { + const node = scrollerRef.current + if (!node) { + return + } + + const scrollToLatest = () => { + node.scrollTo({ + top: node.scrollHeight, + behavior: reducedMotion ? 'auto' : 'smooth', + }) + } + + scrollToLatest() + const frame = window.requestAnimationFrame(scrollToLatest) + const timeoutId = window.setTimeout(scrollToLatest, 320) + const media = [...node.querySelectorAll('img, video')] + media.forEach((item) => { + item.addEventListener('loadeddata', scrollToLatest) + item.addEventListener('load', scrollToLatest) + }) + + return () => { + window.cancelAnimationFrame(frame) + window.clearTimeout(timeoutId) + media.forEach((item) => { + item.removeEventListener('loadeddata', scrollToLatest) + item.removeEventListener('load', scrollToLatest) + }) + } + }, [phase, reducedMotion, stage]) + + const composer = + stage === 0 && phase !== 'result' + ? 'Write a launch brief…' + : stage === 0 + ? 'Give it a visual…' + : stage === 1 && phase === 'result' + ? 'Now make it move…' + : stage === 1 + ? 'Give it a visual…' + : 'Ask a follow-up' + + return ( +
+
+
+ + + + Campaign studio + TanStack AI + + + + Online + +
+
+ {campaignSteps.map((item, index) => { + const view = stageView(index, stage, phase) + if (view === 'hidden') { + return null + } + + return ( + +
+

{item.prompt}

+ {index === 2 ? ( +
+ Palm tree campaign artwork + campaign-artwork.webp +
+ ) : null} +
+ {view === 'pending' ? ( +
+ + +
+ ) : null} + {view === 'result' ? ( +
+ +
+

{item.reply}

+ +
+
+ ) : null} +
+ ) + })} +
+
+
+
+ {composer} + +
+
+
+ ) +} + +export function AiCampaignHero() { + const [step, setStep] = React.useState(0) + const [phase, setPhase] = React.useState('user') + const [pinned, setPinned] = React.useState(false) + const [showServer, setShowServer] = React.useState(false) + const [providerIndices, setProviderIndices] = React.useState([0, 0, 0]) + const reducedMotion = usePrefersReducedMotion() + const current = campaignSteps[step] ?? campaignSteps[0]! + const provider = + current.providers[providerIndices[step]] ?? current.providers[0]! + const serverCode = campaignServerCode(current, provider) + const sample = showServer ? serverCode : current.clientCode + + React.useEffect(() => { + if (reducedMotion === true && !pinned) { + setStep(2) + setPhase('result') + } + }, [pinned, reducedMotion]) + + React.useEffect(() => { + if (pinned || reducedMotion !== false) { + return + } + + const delay = phase === 'user' ? 900 : phase === 'pending' ? 1600 : 2600 + const timeoutId = window.setTimeout(() => { + if (phase === 'user') { + setPhase('pending') + return + } + if (phase === 'pending') { + setPhase('result') + return + } + if (step < campaignSteps.length - 1) { + setStep(step + 1) + setPhase('user') + setShowServer(false) + return + } + setStep(0) + setPhase('user') + setShowServer(false) + }, delay) + + return () => window.clearTimeout(timeoutId) + }, [phase, pinned, reducedMotion, step]) + + const stopOnStage = (index: number) => { + setPinned(true) + setStep(index) + setPhase('result') + setShowServer(false) + } + + return ( +
+
+ + Campaign studio + + Built with TanStack AI +
+
+ {campaignSteps.map((item, index) => ( + + {index > 0 ? ( + + ))} +
+ +
+
+
+
+ + {showServer ? current.serverFile : current.file} +
+
+ + +
+ + + {sample} + + +
+ {showServer ? current.activity : current.hook} +

{campaignNote(current, showServer)}

+
+
+
+

Swap the model

+
+ {current.providers.map((item, index) => ( + + ))} +
+

Via {provider.provider}. The client component stays the same.

+
+
+ + +
+
+ One campaign. More than chat. + Text → image → video +
+
+ ) +} diff --git a/src/components/landing/AiLanding.tsx b/src/components/landing/AiLanding.tsx index daf8bcac4..cd5688eb5 100644 --- a/src/components/landing/AiLanding.tsx +++ b/src/components/landing/AiLanding.tsx @@ -23,7 +23,7 @@ import { import { getLibrary } from '~/libraries' import { CodeBlock } from '~/components/markdown/CodeBlock' -import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' +import { AiCampaignHero } from './AiCampaignHero' import { LandingPromptBox } from './LandingPromptBox' import { LandingSection, @@ -32,9 +32,6 @@ import { LibraryLandingShell, } from './LibraryLanding' -const aiPrompt = - 'Install the agent skills from the skills folder of https://github.com/TanStack/ai for my user, read them, then ask me what AI features I want to build, or suggest some.' - // 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 = @@ -46,8 +43,9 @@ export default function AiLanding() { libraryId="ai" headline="AI building blocks for TypeScript. We build the hard parts, you keep your stack." description="TanStack AI is a TypeScript library for building AI features and agents. It ships the agent loop, provider adapters, durability, interrupts, sandboxes, and tools, and plugs into the server, database, and UI you already have." - hero={} - beforeActions={} + hero={} + beforeActions={} + stackHero > -
+