Chatbot
Open the chatbot by clicking the button at the bottom right corner.
import Chatbot, { type Message,} from "@/registry/new-york/chatbot/components/chatbot/chatbot"import { MessageCircleQuestionMark } from "lucide-react"import { useEffect, useState } from "react"
const title = "Chatbot Assistant"const promptSuggestions = [ "What's the weather like today?", "Tell me a joke.", "How do I bake a chocolate cake?", "Explain quantum computing in simple terms.",]const endpoint = "http://localhost:8000/ask"
const icon = <MessageCircleQuestionMark className="size-6" />
export default function Default() { const [messages, setMessages] = useState<Message[]>([ { role: "bot", message: "Hello! I'm your Chatbot Assistant. How can I help you today?", pending: false, sources: [], }, ])
const [currentChatId, setCurrentChatId] = useState<string | null>(null) const chatEndpoint = `${endpoint}/${currentChatId}`
useEffect(() => { const chatId = crypto.randomUUID() setCurrentChatId(chatId) }, [])
const clearMessages = async () => { setMessages([]) }
if (!currentChatId) { return null }
return ( <Chatbot endpoint={chatEndpoint} strings={{ title, promptSuggestions }} messages={messages} setMessages={setMessages} icon={icon} newChat={clearMessages} /> )}Installation
Section titled “Installation”Install the following dependencies.
Copy and paste the following code into your project.
src/components/chatbot.tsx
import { Button } from "@/components/ui/button"import { ChatBubble, ChatBubbleMessage,} from "@/registry/new-york/chat/components/chat/chat-bubble"import { ChatInput } from "@/registry/new-york/chat/components/chat/chat-input"import { ChatMessageList } from "@/registry/new-york/chat/components/chat/chat-message-list"import { ExpandableChat, ExpandableChatBody, ExpandableChatFooter, ExpandableChatHeader,} from "@/registry/new-york/chat/components/chat/expandable-chat"import Markdown from "@/registry/new-york/chat/components/chat/markdown/markdown"import PromptSuggestions from "@/registry/new-york/chatbot/components/chatbot/prompt-suggestions"import { Send, Trash } from "lucide-react"import { useState } from "react"
export type Message = UserMessage | BotMessage
export interface UserMessage { role: "user" message: string}export interface BotMessage { role: "bot" message: string pending: boolean sources: string[]}
export type ChatStrings = { title: string promptSuggestions: string[] placeholder: string interruptedStreamingError: string genericErrorAnswer: string}
export interface ChatbotProps { endpoint?: string strings?: Partial<ChatStrings> messages: Message[] icon?: React.ReactNode setMessages: React.Dispatch<React.SetStateAction<Message[]>> newChat?: () => Promise<void>}
export default function Chatbot({ endpoint = "http://localhost:8000/ask", strings, messages, icon, setMessages, newChat,}: ChatbotProps) { const title = strings?.title ?? "Chatbot Assistant" const promptSuggestions = strings?.promptSuggestions ?? [] const placeholder = strings?.placeholder ?? "Ask me anything..." const interruptedStreamingError = strings?.interruptedStreamingError ?? "(Response was interrupted)" const genericErrorAnswer = strings?.genericErrorAnswer ?? "Sorry, at this moment I am not able to help you. Try again later."
const [input, setInput] = useState("") const [isLoading, setIsLoading] = useState(false) const [isStreaming, setIsStreaming] = useState(false)
const handleSendMessage = async (input: string) => { setInput("")
const userMessage = { role: "user", message: input, } as UserMessage
const botMessage = { role: "bot", message: "", pending: true, } as BotMessage
setMessages(messages => [...messages, userMessage, botMessage])
setIsLoading(true) setIsStreaming(true)
try { const response = await fetch(endpoint, { method: "post", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ question: input }), })
if (!response.ok || !response.body) { throw response.statusText }
const reader = response.body.getReader() const decoder = new TextDecoder()
let buffer = "" let content = "" let sources: string[] = []
while (true) { const { value, done } = await reader.read() if (done) { setMessages(prev => { const lastIndex = prev.length - 1 return prev.map((msg, i) => i === lastIndex ? { ...msg, pending: false, } : msg, ) }) break } buffer += decoder.decode(value, { stream: true }) const lines = buffer.split("\n") buffer = lines.pop() || ""
for (const line of lines) { const message = line.replace(/^data: /, "").trim() if (!message) continue
try { const parsed = JSON.parse(message)
switch (parsed.type) { case "sources": sources = parsed.data break case "token": content += parsed.data break } } catch (e) { console.error("Error parsing JSON chunk", e) } }
if (content) { setMessages(prev => { const lastIndex = prev.length - 1 return prev.map((msg, i) => { const isLastMessage = i === lastIndex if (!isLastMessage || msg.role !== "bot") return msg
return { ...msg, message: content, sources: [...(msg.sources || []), ...sources], } }) }) setIsLoading(false) } } } catch (error) { console.error("Error during fetch:", error) setMessages(prev => { const lastIndex = prev.length - 1 return prev.map((msg, i) => i === lastIndex ? { ...msg, message: genericErrorAnswer, pending: false, } : msg, ) }) } finally { setIsLoading(false) setIsStreaming(false) } }
return ( <ExpandableChat icon={icon}> <ExpandableChatHeader> <h2 className="m-0! w-full text-center text-xl font-semibold"> {title} </h2> </ExpandableChatHeader> <ExpandableChatBody style={{ scrollbarGutter: "stable", }} > {messages.length === 0 && ( <PromptSuggestions suggestions={promptSuggestions} onClickSuggestion={handleSendMessage} /> )} <ChatMessageList className="w-full max-w-3xl"> {messages.map((message, index) => { if (message.role === "bot") { const showLoading = isLoading && index === messages.length - 1 const isLastMessage = index === messages.length - 1 const interruptedStreaming = message.pending && (!isLastMessage || !isStreaming)
return ( <ChatBubble key={index} variant="received"> <ChatBubbleMessage isLoading={showLoading}> <Markdown references={message.sources}> {message.message} </Markdown> {interruptedStreaming && ( <p className="mt-2 text-sm text-muted-foreground"> {interruptedStreamingError} </p> )} </ChatBubbleMessage> </ChatBubble> ) }
return ( <ChatBubble key={index} variant="sent"> <ChatBubbleMessage>{message.message}</ChatBubbleMessage> </ChatBubble> ) })} </ChatMessageList> </ExpandableChatBody> <ExpandableChatFooter> <div className="flex flex-col gap-2 rounded-2xl border p-2"> <ChatInput value={input} onChange={e => setInput(e.currentTarget.value)} placeholder={placeholder} onKeyDown={e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() handleSendMessage(input) } else if (e.key === "Enter" && e.shiftKey) { e.preventDefault() setInput(input => input + "\n") } }} /> <div className="flex gap-2"> <Button variant="destructive" size="icon-sm" className="cursor-pointer" onClick={newChat} disabled={messages.length === 0} > <Trash className="size-4" /> </Button> <div className="flex-grow" /> <Button type="submit" size="icon-lg" className="cursor-pointer" onClick={() => handleSendMessage(input)} disabled={!input || isLoading} > <Send className="size-4" /> </Button> </div> </div> </ExpandableChatFooter> </ExpandableChat> )}src/components/chatbot.tsx
import { Button } from "@/components/ui/button"
interface Props { suggestions: string[] onClickSuggestion: (prompt: string) => void}
export default function PromptSuggestions({ suggestions, onClickSuggestion,}: Props) { if (!suggestions.length) return null
return ( <div className="flex h-full w-full items-center justify-center"> <div className="flex flex-wrap items-center justify-center gap-4 py-2"> {suggestions.map((prompt, index) => ( <Button key={index} variant="secondary" className="inline-block h-auto max-w-full cursor-pointer rounded-xl px-4 py-2 text-left leading-snug whitespace-normal" onClick={() => onClickSuggestion(prompt)} > {prompt} </Button> ))} </div> </div> )}Update the import paths to match your project setup.
import { Chatbot } from "@chatbot-tools/chatbot"