import { ChatContainerContent, ChatContainerRoot, ChatContainerScrollAnchor, } from "@/components/prompt-kit/chat-container"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Message, MessageAvatar, MessageContent } from "@/components/prompt-kit/message"; import { PromptSuggestion } from "@/components/prompt-kit/prompt-suggestion"; import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/prompt-kit/reasoning"; import { ScrollButton } from "@/components/prompt-kit/scroll-button"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useAgentChat } from "@cloudflare/ai-chat/react"; import { useAgent } from "agents/react"; import type { UIMessage } from "ai"; import { ArrowUp, Square } from "lucide-react"; import { type KeyboardEvent, useEffect, useRef, useState } from "react"; const SUGGESTIONS = [ "What can you help me with?", "Write a haiku about coding", "Explain quantum computing simply", "Tell me a fun fact", ]; interface ChatProps { roomId: string; username: string; loadHistory?: boolean; } export default function Chat({ roomId, username, loadHistory }: ChatProps) { const agent = useAgent({ agent: "chat", name: roomId }); const { messages, sendMessage, status, stop } = useAgentChat({ agent, getInitialMessages: loadHistory ? undefined : null, }); const [input, setInput] = useState(""); const textareaRef = useRef(null); const isStreaming = status === "streaming"; const isLoading = status === "submitted"; // Auto-resize textarea const inputLength = input.length; // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on input change useEffect(() => { const textarea = textareaRef.current; if (textarea) { textarea.style.height = "auto"; textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; } }, [inputLength]); const send = (text?: string) => { const msg = text ?? input.trim(); if (!msg || isStreaming || isLoading) return; sendMessage({ text: msg }); setInput(""); if (textareaRef.current) { textareaRef.current.style.height = "auto"; } }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }; function getMessageText(message: UIMessage): string { return message.parts .filter((part) => part.type === "text") .map((part) => part.text) .join(""); } function getReasoningText(message: UIMessage): string | null { const texts: string[] = []; for (const part of message.parts) { if (part.type === "reasoning") { texts.push(part.text); } } return texts.length > 0 ? texts.join("") : null; } const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; const isWaitingForResponse = isLoading || (isStreaming && lastMessage?.role === "user"); const userInitials = username.slice(0, 2).toUpperCase(); return (
{messages.length === 0 && (

jack-template

Real-time AI chat with persistent rooms. Share the link — anyone who opens it joins this conversation live. No database, no WebSocket server, just code.

{SUGGESTIONS.map((s) => ( send(s)}> {s} ))}
)} {messages.map((message: UIMessage, index: number) => { const isUser = message.role === "user"; const text = getMessageText(message); const reasoning = isUser ? null : getReasoningText(message); const isLast = index === messages.length - 1; const isStreamingThis = isLast && !isUser && isStreaming; if (isUser) { return (
{username}
{text}
); } return (
AI
{reasoning && ( Reasoning {reasoning} )} {isStreamingThis ? `${text}▍` : text}
); })} {isWaitingForResponse && (
)}
{/* Input area */}