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 { 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 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 clearMessages = async () => { setMessages([]) }
return ( <Chatbot 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/chatbot/components/chatbot/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) { 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 => prev.map((msg, i) => { const isLastMessage = i === prev.length - 1 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) botMessage.message = genericErrorAnswer } finally { setMessages(prev => prev.map((msg, i) => i === prev.length - 1 ? { ...msg, pending: false } : msg, ), ) 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> )}src/components/chatbot.tsx
import styles from "./markdown.module.css"import { cn } from "@/lib/utils"import ReactMarkdown from "react-markdown"import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"import darkStyle from "react-syntax-highlighter/dist/esm/styles/prism/one-dark"import lightStyle from "react-syntax-highlighter/dist/esm/styles/prism/one-light"import remarkGfm from "remark-gfm"
interface Props { theme?: "light" | "dark" children?: string className?: string references?: Array<string>}
export default function Markdown({ theme = "light", children, className, references = [],}: Props) { const hlStyle = theme === "dark" ? darkStyle : lightStyle
return ( <div className={cn( "prose dark:prose-invert flex max-h-fit w-full flex-col", styles.markdown, className, )} > <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[]} urlTransform={uri => uri} components={{ code(props) { const { children, className, ...rest } = props const match = /language-(\w+)/.exec(className ?? "") return match ? ( <SyntaxHighlighter {...rest} PreTag="div" ref={undefined} language={match[1]} style={hlStyle} customStyle={{ margin: 0, }} > {String(children).replace(/\n$/, "")} </SyntaxHighlighter> ) : ( <code {...rest} className={className}> {children} </code> ) }, a: ({ href, children }) => { if (href?.startsWith("ref:")) { const id = parseInt(href.slice(4)) const url = references[id]
if (!url) return <span>{children}</span>
return ( <a href={url} target="_blank" rel="noopener noreferrer" className="ref-button" > {children} </a> ) }
return <a href={href}>{children}</a> }, }} > {children} </ReactMarkdown> </div> )}src/components/chatbot.css
.markdown ul { display: flex; flex-direction: column; margin-top: 0.25rem; margin-bottom: 0.25rem; list-style-type: disc; padding-left: 1rem;}.markdown ol { margin-top: 0.25rem; margin-bottom: 0.75rem; list-style-type: decimal; padding-left: 1rem;}.markdown a { color: #3b82f6; /* Tailwind's blue-500 */}.markdown pre { margin: 0; border: 0; background-color: var(--background); padding: 0;}.markdown p { margin-bottom: 0.75rem;}.markdown h1 { margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.25;}.markdown h2 { margin-bottom: 1.5rem; font-size: 1.125rem; line-height: 1.25;}.markdown h3 { margin-bottom: 1rem; font-size: 1rem; font-weight: bold; line-height: 1.25;}.markdown h4 { margin-bottom: 1rem; font-size: 1rem; line-height: 1.25;}.markdown h5 { margin-bottom: 0.5rem; font-size: 0.875rem; line-height: 1.25;}.markdown h6 { margin-bottom: 0.5rem; font-size: 0.75rem; line-height: 1.25;}.markdown hr { margin: 0;}Update the import paths to match your project setup.
import { Chatbot } from "@chatbot-tools/chatbot"