import { useEffect, useRef, useState } from 'react' import { createFileRoute } from '@tanstack/react-router' import { Send, Square, Mic, MicOff, Volume2, VolumeX, Loader2, } from 'lucide-react' import { Streamdown } from 'streamdown' import { useGuitarRecommendationChat } from '#/lib/demo-ai-hook' import type { ChatMessages } from '#/lib/demo-ai-hook' import { useAudioRecorder } from '#/hooks/demo-useAudioRecorder' import { useTTS } from '#/hooks/demo-useTTS' import GuitarRecommendation from '#/components/demo-GuitarRecommendation' import './ai-chat.css' function InitialLayout({ children }: { children: React.ReactNode }) { return (

TanStack Chat

You can ask me about anything, I might or might not have a good answer, but you can still ask.

{children}
) } function ChattingLayout({ children }: { children: React.ReactNode }) { return (
{children}
) } function Messages({ messages, playingId, onSpeak, onStopSpeak, }: { messages: ChatMessages playingId: string | null onSpeak: (text: string, id: string) => void onStopSpeak: () => void }) { const messagesContainerRef = useRef(null) useEffect(() => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight } }, [messages]) if (!messages.length) { return null } // Extract text content from message parts const getTextContent = ( parts: ChatMessages[number]['parts'], ): string | null => { for (const part of parts) { if (part.type === 'text' && part.content) { return part.content } } return null } return (
{messages.map((message) => { const textContent = getTextContent(message.parts) const isPlaying = playingId === message.id return (
{message.role === 'assistant' ? (
AI
) : (
Y
)}
{message.parts.map((part, index) => { if (part.type === 'text' && part.content) { return (
{part.content}
) } // Guitar recommendation card if ( part.type === 'tool-call' && part.name === 'recommendGuitar' && part.output ) { return (
) } return null })}
{/* TTS button for assistant messages */} {message.role === 'assistant' && textContent && ( )}
) })}
) } function ChatPage() { const [input, setInput] = useState('') const { isRecording, isTranscribing, startRecording, stopRecording } = useAudioRecorder() const { playingId, speak, stop: stopTTS } = useTTS() const { messages, sendMessage, isLoading, stop } = useGuitarRecommendationChat() const handleMicClick = async () => { if (isRecording) { const transcribedText = await stopRecording() if (transcribedText) { setInput((prev) => prev ? `${prev} ${transcribedText}` : transcribedText, ) } } else { await startRecording() } } const Layout = messages.length ? ChattingLayout : InitialLayout return (
{isLoading && (
)}
{ e.preventDefault() if (input.trim()) { sendMessage(input) setInput('') } }} >