import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { getConversationHistory } from "@/services/get-conversation-history"; import { createFileRoute, notFound } from "@tanstack/react-router"; import { LoaderCircleIcon, SendHorizontalIcon } from "lucide-react"; import React, { useEffect, useRef, useState } from "react"; import { streamingFetch } from "@/utils/streaming-fetch"; import { env } from "@/env"; import ConversationBubble from "@/components/conversation-bubble"; interface ConversationData { key: React.Key; content: string; type: "question" | "response"; } export const Route = createFileRoute("/conversation/$sessionId")({ component: RouteComponent, loader: async ({ params }) => { const data = await getConversationHistory(params.sessionId); if (!data) { throw notFound(); } return data; }, }); function RouteComponent() { const { history, title } = Route.useLoaderData(); const { sessionId } = Route.useParams(); const [inputValue, setInputValue] = useState(""); const [isBusy, setIsBusy] = useState(false); const [conversationPieces, setConversationPieces] = useState< ConversationData[] >([]); const scrollContainer = useRef(null); const scrollToBottom = () => { setTimeout(() => { if (scrollContainer.current) { scrollContainer.current.scrollTo({ top: scrollContainer.current.scrollHeight, behavior: "smooth", }); } }, 100); }; // Auto-scroll when conversation pieces change useEffect(() => { scrollToBottom(); }, [conversationPieces]); const handleSendQuestion = async () => { if (!inputValue || isBusy) { return; } const question = inputValue.trim(); setIsBusy(true); setInputValue(""); setConversationPieces((state) => [ ...state, { key: new Date().getTime(), content: question, type: "question", }, ]); try { setConversationPieces((state) => [ ...state, { key: new Date().getTime(), content: "", type: "response", }, ]); scrollToBottom(); let idx = 0; for await (const chunk of streamingFetch(() => fetch(`${env.VITE_API_URL}/ask-conversation`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ question, session_id: sessionId }), }) )) { setConversationPieces((state) => { const stateCopy = [...state]; const lastItem = stateCopy.pop()!; return [ ...stateCopy, { key: new Date().getTime(), content: idx++ === 0 ? chunk : lastItem.content + chunk, type: "response", }, ]; }); scrollToBottom(); } } catch (err) { console.error("There was an error receiving chunks", err); } finally { setIsBusy(false); } }; return (
{title}
{history.map((h, idx) => ( ))} {!!history.length && (
End of history
)} {!!conversationPieces.length && ( <> {conversationPieces.map((c) => ( ))} )}
setInputValue(e.target.value)} disabled={isBusy} />
); }