@@ -3,10 +3,8 @@ import { LinkProps } from '@tanstack/react-router'
33import type { Library } from '~/libraries'
44import { useIsDark } from '~/hooks/useIsDark'
55import { ChatPanel } from './ChatPanel'
6- import {
7- useAILibraryHeroAnimationStore ,
8- AnimationPhase ,
9- } from '~/stores/aiLibraryHeroAnimation'
6+ import { AnimationPhase } from '~/stores/aiLibraryHeroAnimation'
7+ import { useAILibraryHeroAnimation } from '~/hooks/useAILibraryHeroAnimation'
108import { AILibraryHeroCard } from './AILibraryHeroCard'
119import { AILibraryHeroBox } from './AILibraryHeroBox'
1210import { AILibraryHeroServiceCard } from './AILibraryHeroServiceCard'
@@ -30,7 +28,6 @@ import {
3028 BOX_FONT_SIZE ,
3129 BOX_FONT_WEIGHT ,
3230 SERVICE_WIDTH ,
33- SERVICE_GUTTER ,
3431 SERVICE_LOCATIONS ,
3532 SERVICE_Y_OFFSET ,
3633 SERVICE_Y_CENTER ,
@@ -44,9 +41,6 @@ import {
4441 SERVER_CARD_HEIGHT ,
4542} from '~/stores/aiLibraryHeroAnimation'
4643
47- // Get the store instance for accessing getState in closures
48- const getStoreState = ( ) => useAILibraryHeroAnimationStore . getState ( )
49-
5044type AILibraryHeroProps = {
5145 project : Library
5246 cta ?: {
@@ -57,48 +51,14 @@ type AILibraryHeroProps = {
5751 actions ?: React . ReactNode
5852}
5953
60- const FRAMEWORKS = [ 'vanilla' , 'react' , 'solid' , '?' ] as const
61- const SERVICES = [ 'ollama' , 'openai' , 'anthropic' , 'gemini' ] as const
62- const SERVERS = [ 'typescript' , 'php' , 'python' , '?' ] as const
63-
64- const MESSAGES = [
65- {
66- user : 'What makes TanStack AI different?' ,
67- assistant :
68- '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.' ,
69- } ,
70- {
71- user : 'Do you support tools?' ,
72- assistant :
73- '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.' ,
74- } ,
75- {
76- user : 'What about thinking models?' ,
77- assistant :
78- "We fully support thinking and reasoning models. All thinking and reasoning tokens are sent to the client, giving you complete visibility into the model's reasoning process." ,
79- } ,
80- {
81- user : 'How type-safe is it?' ,
82- assistant :
83- 'We have total type safety across providers, models, and model options. Every interaction is fully typed from end to end, catching errors at compile time.' ,
84- } ,
85- {
86- user : 'What about developer experience?' ,
87- assistant :
88- '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.' ,
89- } ,
90- {
91- user : 'Is this a service I have to pay for?' ,
92- assistant :
93- "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." ,
94- } ,
95- ]
96-
9754export function AILibraryHero ( { } : AILibraryHeroProps ) {
9855 const isDark = useIsDark ( )
9956 const strokeColor = isDark ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)'
10057 const textColor = isDark ? '#ffffff' : '#000000'
10158
59+ // Use the animation hook - handles all animation state and orchestration
60+ const { store } = useAILibraryHeroAnimation ( )
61+
10262 const {
10363 phase,
10464 selectedFramework,
@@ -111,258 +71,7 @@ export function AILibraryHero({}: AILibraryHeroProps) {
11171 messages,
11272 typingUserMessage,
11373 connectionPulseDirection,
114- setPhase,
115- setSelectedFramework,
116- setSelectedService,
117- setSelectedServer,
118- setRotatingFramework,
119- setRotatingServer,
120- setRotatingService,
121- setServiceOffset,
122- addMessage,
123- updateCurrentAssistantMessage,
124- setCurrentMessageStreaming,
125- clearMessages,
126- setTypingUserMessage,
127- clearTypingUserMessage,
128- setConnectionPulseDirection,
129- addTimeout,
130- clearTimeouts,
131- } = useAILibraryHeroAnimationStore ( )
132-
133- React . useEffect ( ( ) => {
134- const addTimeoutHelper = ( fn : ( ) => void , delay : number ) => {
135- const timeout = setTimeout ( fn , delay )
136- addTimeout ( timeout )
137- return timeout
138- }
139-
140- const getRandomIndex = ( length : number , exclude ?: number ) => {
141- let index
142- do {
143- index = Math . floor ( Math . random ( ) * length )
144- } while ( exclude !== undefined && index === exclude )
145- return index
146- }
147-
148- const selectFrameworkServiceServer = ( onComplete : ( ) => void ) => {
149- // Phase 2: DESELECTING
150- setPhase ( AnimationPhase . DESELECTING )
151- addTimeoutHelper ( ( ) => {
152- // Phase 3: SELECTING_FRAMEWORK
153- setPhase ( AnimationPhase . SELECTING_FRAMEWORK )
154- const targetFramework = getRandomIndex ( FRAMEWORKS . length )
155- let currentIndex = Math . floor ( Math . random ( ) * FRAMEWORKS . length )
156- const rotationCount = 8 + Math . floor ( Math . random ( ) * 4 ) // 8-11 rotations
157-
158- const rotateFramework = ( iteration : number ) => {
159- if ( iteration < rotationCount - 1 ) {
160- setRotatingFramework ( currentIndex )
161- currentIndex = ( currentIndex + 1 ) % FRAMEWORKS . length
162- const delay =
163- iteration < rotationCount - 4
164- ? 100
165- : 150 + ( iteration - ( rotationCount - 4 ) ) * 50
166- addTimeoutHelper ( ( ) => rotateFramework ( iteration + 1 ) , delay )
167- } else {
168- // Final iteration - ensure we land on target
169- setRotatingFramework ( targetFramework )
170- addTimeoutHelper ( ( ) => {
171- setSelectedFramework ( targetFramework )
172- setRotatingFramework ( null )
173- addTimeoutHelper ( ( ) => {
174- // Phase 4: SELECTING_SERVICE
175- setPhase ( AnimationPhase . SELECTING_SERVICE )
176- // Always pick a different service so it has to scroll
177- const currentSelectedService = getStoreState ( ) . selectedService
178- const targetService = getRandomIndex (
179- SERVICES . length ,
180- currentSelectedService ?? undefined ,
181- )
182- let currentServiceIndex = Math . floor (
183- Math . random ( ) * SERVICES . length ,
184- )
185- const serviceRotationCount = 6 + Math . floor ( Math . random ( ) * 3 )
186-
187- const rotateService = ( iteration : number ) => {
188- if ( iteration < serviceRotationCount - 1 ) {
189- setRotatingService ( currentServiceIndex )
190- currentServiceIndex =
191- ( currentServiceIndex + 1 ) % SERVICES . length
192- const delay =
193- iteration < serviceRotationCount - 3
194- ? 120
195- : 180 + ( iteration - ( serviceRotationCount - 3 ) ) * 60
196- addTimeoutHelper ( ( ) => rotateService ( iteration + 1 ) , delay )
197- } else {
198- // Final iteration - ensure we land on target
199- setRotatingService ( targetService )
200- addTimeoutHelper ( ( ) => {
201- setSelectedService ( targetService )
202- setRotatingService ( null )
203- const targetX =
204- 0 -
205- SERVICE_WIDTH / 2 -
206- SERVICE_GUTTER / 2 -
207- targetService * ( SERVICE_WIDTH + SERVICE_GUTTER )
208- setServiceOffset ( targetX )
209-
210- addTimeoutHelper ( ( ) => {
211- // Phase 5: SELECTING_SERVER
212- setPhase ( AnimationPhase . SELECTING_SERVER )
213- const targetServer = getRandomIndex ( SERVERS . length )
214- let currentServerIndex = Math . floor (
215- Math . random ( ) * SERVERS . length ,
216- )
217- const serverRotationCount =
218- 8 + Math . floor ( Math . random ( ) * 4 )
219-
220- const rotateServer = ( iteration : number ) => {
221- if ( iteration < serverRotationCount - 1 ) {
222- setRotatingServer ( currentServerIndex )
223- currentServerIndex =
224- ( currentServerIndex + 1 ) % SERVERS . length
225- const delay =
226- iteration < serverRotationCount - 4
227- ? 100
228- : 150 +
229- ( iteration - ( serverRotationCount - 4 ) ) * 50
230- addTimeoutHelper (
231- ( ) => rotateServer ( iteration + 1 ) ,
232- delay ,
233- )
234- } else {
235- // Final iteration - ensure we land on target
236- setRotatingServer ( targetServer )
237- addTimeoutHelper ( ( ) => {
238- setSelectedServer ( targetServer )
239- setRotatingServer ( null )
240- addTimeoutHelper ( ( ) => {
241- // Selection complete, call callback
242- onComplete ( )
243- } , 800 )
244- } , 1000 )
245- }
246- }
247- rotateServer ( 0 )
248- } , 1000 )
249- } , 800 )
250- }
251- }
252- rotateService ( 0 )
253- } , 800 )
254- } , 500 )
255- }
256- }
257- rotateFramework ( 0 )
258- } , 500 )
259- }
260-
261- const processNextMessage = ( messageIndex : number ) => {
262- if ( messageIndex >= MESSAGES . length ) {
263- // All messages shown, clear and restart
264- clearMessages ( )
265- addTimeoutHelper ( ( ) => {
266- // Phase 1: STARTING (initial state)
267- setPhase ( AnimationPhase . STARTING )
268- addTimeoutHelper ( ( ) => {
269- // Start first message with selection
270- selectFrameworkServiceServer ( ( ) => {
271- processNextMessage ( 0 )
272- } )
273- } , 1000 )
274- } , 1000 )
275- return
276- }
277-
278- const message = MESSAGES [ messageIndex ]
279-
280- // Phase 6: SHOWING_CHAT - Type user message in input field first
281- setPhase ( AnimationPhase . SHOWING_CHAT )
282- clearTypingUserMessage ( )
283-
284- // Type the user message character by character in the input field
285- let typingIndex = 0
286- const typeUserMessage = ( ) => {
287- if ( typingIndex < message . user . length ) {
288- setTypingUserMessage ( message . user . slice ( 0 , typingIndex + 1 ) )
289- typingIndex ++
290- const delay = 30 + Math . floor ( Math . random ( ) * 40 ) // 30-70ms per character
291- addTimeoutHelper ( typeUserMessage , delay )
292- } else {
293- // Typing complete, wait a moment then clear input and show as bubble
294- addTimeoutHelper ( ( ) => {
295- clearTypingUserMessage ( )
296- addMessage ( message . user )
297- addTimeoutHelper ( ( ) => {
298- // Phase 7: PULSING_CONNECTIONS
299- setPhase ( AnimationPhase . PULSING_CONNECTIONS )
300- setConnectionPulseDirection ( 'down' )
301- addTimeoutHelper ( ( ) => {
302- // Phase 8: STREAMING_RESPONSE
303- setPhase ( AnimationPhase . STREAMING_RESPONSE )
304- setConnectionPulseDirection ( 'up' )
305- const fullMessage = message . assistant
306- setCurrentMessageStreaming ( true )
307- let currentIndex = 0
308-
309- const streamChunk = ( ) => {
310- if ( currentIndex < fullMessage . length ) {
311- // Random chunk size between 2 and 8 characters
312- const chunkSize = 2 + Math . floor ( Math . random ( ) * 7 )
313- const nextIndex = Math . min (
314- currentIndex + chunkSize ,
315- fullMessage . length ,
316- )
317- updateCurrentAssistantMessage (
318- fullMessage . slice ( 0 , nextIndex ) ,
319- )
320- currentIndex = nextIndex
321- // Random delay between 20ms and 80ms
322- const delay = 20 + Math . floor ( Math . random ( ) * 60 )
323- addTimeoutHelper ( streamChunk , delay )
324- } else {
325- setCurrentMessageStreaming ( false )
326- addTimeoutHelper ( ( ) => {
327- // Phase 9: HOLDING - brief pause before next message
328- setPhase ( AnimationPhase . HOLDING )
329- addTimeoutHelper ( ( ) => {
330- // Select new combination for next message
331- selectFrameworkServiceServer ( ( ) => {
332- // Move to next message
333- processNextMessage ( messageIndex + 1 )
334- } )
335- } , 2000 )
336- } , 500 )
337- }
338- }
339- streamChunk ( )
340- } , 2000 )
341- } , 500 )
342- } , 300 )
343- }
344- }
345- typeUserMessage ( )
346- }
347-
348- const startAnimationSequence = ( ) => {
349- // Phase 1: STARTING (initial state)
350- setPhase ( AnimationPhase . STARTING )
351- addTimeoutHelper ( ( ) => {
352- // Start first message with selection
353- selectFrameworkServiceServer ( ( ) => {
354- processNextMessage ( 0 )
355- } )
356- } , 1000 )
357- }
358-
359- startAnimationSequence ( )
360-
361- return ( ) => {
362- clearTimeouts ( )
363- }
364- // eslint-disable-next-line react-hooks/exhaustive-deps
365- } , [ ] )
74+ } = store
36675
36776 const getOpacity = (
36877 index : number ,
0 commit comments