components/chat.tsx (108 lines of code) (raw):

"use client"; import React, { useCallback, useEffect, useRef, useState } from "react"; import ToolCall from "./tool-call"; import Message from "./message"; import Annotations from "./annotations"; import { Item } from "@/lib/assistant"; interface ChatProps { items: Item[]; onSendMessage: (message: string) => void; } const Chat: React.FC<ChatProps> = ({ items, onSendMessage }) => { const itemsEndRef = useRef<HTMLDivElement>(null); const [inputMessageText, setinputMessageText] = useState<string>(""); // This state is used to provide better user experience for non-English IMEs such as Japanese const [isComposing, setIsComposing] = useState(false); const scrollToBottom = () => { itemsEndRef.current?.scrollIntoView({ behavior: "instant" }); }; const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => { if (event.key === "Enter" && !event.shiftKey && !isComposing) { event.preventDefault(); onSendMessage(inputMessageText); setinputMessageText(""); } }, [onSendMessage, inputMessageText]); useEffect(() => { scrollToBottom(); }, [items]); return ( <div className="flex justify-center items-center size-full"> <div className="flex grow flex-col h-full max-w-[750px] gap-2"> <div className="h-[90vh] overflow-y-scroll px-10 flex flex-col"> <div className="mt-auto space-y-5 pt-4"> {items.map((item, index) => ( <React.Fragment key={index}> {item.type === "tool_call" ? ( <ToolCall toolCall={item} /> ) : item.type === "message" ? ( <div className="flex flex-col gap-1"> <Message message={item} /> {item.content && item.content[0].annotations && item.content[0].annotations.length > 0 && ( <Annotations annotations={item.content[0].annotations} /> )} </div> ) : null} </React.Fragment> ))} <div ref={itemsEndRef} /> </div> </div> <div className="flex-1 p-4 px-10"> <div className="flex items-center"> <div className="flex w-full items-center pb-4 md:pb-1"> <div className="flex w-full flex-col gap-1.5 rounded-[20px] p-2.5 pl-1.5 transition-colors bg-white border border-stone-200 shadow-sm"> <div className="flex items-end gap-1.5 md:gap-2 pl-4"> <div className="flex min-w-0 flex-1 flex-col"> <textarea id="prompt-textarea" tabIndex={0} dir="auto" rows={2} placeholder="Message..." className="mb-2 resize-none border-0 focus:outline-none text-sm bg-transparent px-0 pb-6 pt-2" value={inputMessageText} onChange={(e) => setinputMessageText(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={() => setIsComposing(true)} onCompositionEnd={() => setIsComposing(false)} /> </div> <button disabled={!inputMessageText} data-testid="send-button" className="flex size-8 items-end justify-center rounded-full bg-black text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100" onClick={() => { onSendMessage(inputMessageText); setinputMessageText(""); }} > <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32" className="icon-2xl" > <path fill="currentColor" fillRule="evenodd" d="M15.192 8.906a1.143 1.143 0 0 1 1.616 0l5.143 5.143a1.143 1.143 0 0 1-1.616 1.616l-3.192-3.192v9.813a1.143 1.143 0 0 1-2.286 0v-9.813l-3.192 3.192a1.143 1.143 0 1 1-1.616-1.616z" clipRule="evenodd" /> </svg> </button> </div> </div> </div> </div> </div> </div> </div> ); }; export default Chat;