asafefe.dev

Support Chat

New

A floating support widget. The launcher morphs into a chat panel with an agent header, bubbles that spring in, a typing indicator, quick reply chips and a send button that flies its plane.

motionlucide-reactutilsavatarbadgebuttoncardinput-groupitemseparator@soultel/icon-send@soultel/use-reduced-motion-safe

Installation

$ pnpm dlx shadcn@latest add @soultel/support-chat

No namespace set up? npx shadcn@latest add https://asafefe.dev/r/support-chat.json works too.

Manual installation
  1. 1. Install the dependencies.
    terminal1 lines
    npm install motion lucide-react
  2. 2. Copy the source from the Code tab into components/support-chat.tsx.

Usage

example.tsx24 lines
import { SupportChat, type ChatMessage } from "@/components/support-chat"

export function Support() {
  const [messages, setMessages] = useState<ChatMessage[]>([
    { id: "hi", from: "agent", text: "Hi! How can we help today?", replies: ["Track my order", "Return an item"] },
  ])
  const [typing, setTyping] = useState(false)

  return (
    <SupportChat
      team="Northside support"
      agent={{ name: "Maya", role: "Typically replies in 2 min", avatar: "/team/maya.jpg" }}
      messages={messages}
      onMessagesChange={setMessages}
      typing={typing}
      onSend={async (text) => {
        setTyping(true)
        const reply = await fetch("/api/support", { method: "POST", body: JSON.stringify({ text }) }).then((r) => r.json())
        setTyping(false)
        setMessages((m) => [...m, { id: reply.id, from: "agent", text: reply.text }])
      }}
    />
  )
}

Props

PropTypeDefaultDescription
agentChatAgent-name, and optional role, avatar (photo URL, initials as fallback) and status ("online" | "away" | "offline").
teamstring-Header title, e.g. "Northside support". Defaults to the agent's name.
messages / defaultMessagesChatMessage[]-Controlled or initial messages: { id, from: "agent" | "user", text, time, replies }. replies shows chips under the latest agent message.
onMessagesChange(messages: ChatMessage[]) => void-Called when the visitor sends a message.
onSend(text: string) => void | Promise<void>-Called with each sent message. A promise dims the bubble until it settles; a rejection offers a retry.
typingbooleanfalseShows the typing indicator.
quickRepliesstring[]-Chips shown before the visitor has written anything.
open / defaultOpenbooleanfalseControlled or initial open state.
onOpenChange(open: boolean) => void-Called by the launcher, the minimize button and Escape.
unreadnumber0Count on the launcher while closed.
position"fixed" | "absolute""fixed"Pin to the viewport corner, or to the nearest positioned parent.
placeholderstring"Write a message"Input placeholder.