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 (
)
}
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 (
)
}
export const Route = createFileRoute('/demo/ai/chat')({
component: ChatPage,
})