💬 Customer Support Chat

PreviousNext

A professional, customizable chat widget for customer support with real-time typing indicators and smart responses.

Docs
react-marketcomponent

Preview

Loading preview…
customer-support-chat.tsx
"use client"

import * as React from "react"
import { AnimatePresence, motion } from "framer-motion"
import { ArrowUpCircle, ChevronDown, Clock, File, ImageIcon, Paperclip, Send, X } from "lucide-react"
import { format } from "date-fns"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Textarea } from "@/components/ui/textarea"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"

// Types for the chat component
type MessageType = "user" | "agent" | "system"

interface Message {
  id: string
  content: string
  timestamp: Date
  type: MessageType
  attachments?: Attachment[]
  status?: "sent" | "delivered" | "read"
  isTyping?: boolean
}

interface Attachment {
  id: string
  name: string
  type: "image" | "file"
  url: string
  size?: string
}

interface CustomerSupportChatProps {
  agentName?: string
  agentAvatar?: string
  companyName?: string
  companyLogo?: string
  primaryColor?: string
  initialMessage?: string
  position?: "bottom-right" | "bottom-left"
  className?: string
  defaultOpen?: boolean
}

export function CustomerSupportChat({
  agentName = "Support Agent",
  agentAvatar = "/diverse-avatars.png",
  companyName = "Customer Support",
  companyLogo = "/abstract-logo.png",
  initialMessage = "Hello! How can I help you today?",
  position = "bottom-right",
  className,
  defaultOpen = true,
}: CustomerSupportChatProps) {
  const [isOpen, setIsOpen] = React.useState(defaultOpen)
  const [isAgentTyping, setIsAgentTyping] = React.useState(false)
  const [isAgentOnline, setIsAgentOnline] = React.useState(true)
  const [newMessage, setNewMessage] = React.useState("")
  const [messages, setMessages] = React.useState<Message[]>([
    {
      id: "1",
      content: initialMessage,
      timestamp: new Date(),
      type: "agent",
      status: "read",
    },
  ])
  const messagesEndRef = React.useRef<HTMLDivElement>(null)

  // Simulate agent coming online/offline
  React.useEffect(() => {
    const interval = setInterval(() => {
      setIsAgentOnline((prev) => !prev)
    }, 60000) // Toggle every minute for demo purposes
    return () => clearInterval(interval)
  }, [])

  // Auto-scroll to bottom when messages change
  React.useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
  }, [messages, isAgentTyping])

  // Simulate agent typing and response when user sends a message
  const simulateAgentResponse = (userMessage: string) => {
    // Start typing indicator
    setIsAgentTyping(true)

    // Simulate typing delay (1-3 seconds)
    const typingDelay = Math.floor(Math.random() * 2000) + 1000

    setTimeout(() => {
      setIsAgentTyping(false)

      // Generate a response based on user message
      let response = "Thank you for your message. Our team will get back to you shortly."

      if (userMessage.toLowerCase().includes("hello") || userMessage.toLowerCase().includes("hi")) {
        response = "Hello there! How can I assist you today?"
      } else if (userMessage.toLowerCase().includes("help")) {
        response = "I'd be happy to help. Could you please provide more details about your issue?"
      } else if (userMessage.toLowerCase().includes("price") || userMessage.toLowerCase().includes("cost")) {
        response = "Our pricing information can be found on our pricing page. Would you like me to send you the link?"
      } else if (userMessage.toLowerCase().includes("thank")) {
        response = "You're welcome! Is there anything else I can help you with?"
      }

      // Add agent response
      setMessages((prev) => [
        ...prev,
        {
          id: Date.now().toString(),
          content: response,
          timestamp: new Date(),
          type: "agent",
          status: "sent",
        },
      ])
    }, typingDelay)
  }

  const handleSendMessage = () => {
    if (newMessage.trim() === "") return

    // Add user message
    const userMessage = {
      id: Date.now().toString(),
      content: newMessage,
      timestamp: new Date(),
      type: "user" as MessageType,
      status: "sent",
    }

    setMessages((prev) => [...prev, userMessage])
    setNewMessage("")

    // Simulate agent response
    simulateAgentResponse(newMessage)
  }

  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault()
      handleSendMessage()
    }
  }

  return (
    <div className={cn("z-50", position === "bottom-right" ? "bottom-4 right-4" : "bottom-4 left-4", className)}>
      <AnimatePresence>
        {isOpen ? (
          <motion.div
            initial={{ opacity: 0, y: 20, scale: 0.95 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={{ opacity: 0, y: 20, scale: 0.95 }}
            transition={{ duration: 0.2 }}
            className="flex flex-col w-80 sm:w-96 h-[500px] rounded-lg shadow-lg bg-background border"
          >
            <Card className="flex flex-col h-full">
              <CardHeader className="flex flex-row items-center justify-between space-y-0 p-4">
                <div className="flex items-center gap-2">
                  <Avatar className="h-8 w-8">
                    <AvatarImage src={companyLogo || "/placeholder.svg"} alt={companyName} />
                    <AvatarFallback>{companyName.substring(0, 2)}</AvatarFallback>
                  </Avatar>
                  <div>
                    <h3 className="font-semibold text-sm">{companyName}</h3>
                    <div className="flex items-center gap-1.5">
                      <div
                        className={cn("h-1.5 w-1.5 rounded-full", isAgentOnline ? "bg-green-500" : "bg-amber-500")}
                      />
                      <p className="text-xs text-muted-foreground">{isAgentOnline ? "Online" : "Away"}</p>
                    </div>
                  </div>
                </div>
                <Button variant="ghost" size="icon" onClick={() => setIsOpen(false)}>
                  <X className="h-4 w-4" />
                </Button>
              </CardHeader>
              <CardContent className="flex-1 p-0 overflow-hidden">
                <ScrollArea className="h-full p-4">
                  <div className="flex flex-col gap-3">
                    {messages.map((message) => (
                      <div
                        key={message.id}
                        className={cn(
                          "flex flex-col max-w-[80%] rounded-lg p-3",
                          message.type === "user"
                            ? "ml-auto bg-primary text-primary-foreground"
                            : message.type === "system"
                              ? "mx-auto bg-muted text-muted-foreground text-xs"
                              : "mr-auto bg-muted",
                        )}
                      >
                        {message.type === "agent" && (
                          <div className="flex items-center gap-2 mb-1">
                            <Avatar className="h-5 w-5">
                              <AvatarImage src={agentAvatar || "/placeholder.svg"} alt={agentName} />
                              <AvatarFallback>{agentName.substring(0, 2)}</AvatarFallback>
                            </Avatar>
                            <span className="text-xs font-medium">{agentName}</span>
                          </div>
                        )}
                        <div className="text-sm">{message.content}</div>
                        {message.attachments && message.attachments.length > 0 && (
                          <div className="flex flex-wrap gap-2 mt-2">
                            {message.attachments.map((attachment) => (
                              <div
                                key={attachment.id}
                                className={cn(
                                  "flex items-center gap-1 text-xs p-1.5 rounded",
                                  message.type === "user" ? "bg-primary/20" : "bg-background",
                                )}
                              >
                                {attachment.type === "image" ? (
                                  <ImageIcon className="h-3.5 w-3.5" />
                                ) : (
                                  <File className="h-3.5 w-3.5" />
                                )}
                                <span className="max-w-[100px] truncate">{attachment.name}</span>
                              </div>
                            ))}
                          </div>
                        )}
                        <div
                          className={cn(
                            "flex items-center gap-1 text-xs mt-1",
                            message.type === "user" ? "ml-auto" : "mr-auto",
                          )}
                        >
                          <span className="opacity-70">{format(message.timestamp, "h:mm a")}</span>
                          {message.type === "user" && message.status && (
                            <span>
                              {message.status === "sent" && "✓"}
                              {message.status === "delivered" && "✓✓"}
                              {message.status === "read" && "✓✓"}
                            </span>
                          )}
                        </div>
                      </div>
                    ))}
                    {isAgentTyping && (
                      <div className="flex items-center gap-2 max-w-[80%] mr-auto">
                        <Avatar className="h-5 w-5">
                          <AvatarImage src={agentAvatar || "/placeholder.svg"} alt={agentName} />
                          <AvatarFallback>{agentName.substring(0, 2)}</AvatarFallback>
                        </Avatar>
                        <div className="bg-muted p-3 rounded-lg">
                          <div className="flex gap-1 items-center">
                            <div className="h-1.5 w-1.5 bg-foreground/70 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
                            <div className="h-1.5 w-1.5 bg-foreground/70 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
                            <div className="h-1.5 w-1.5 bg-foreground/70 rounded-full animate-bounce"></div>
                          </div>
                        </div>
                      </div>
                    )}
                    <div ref={messagesEndRef} />
                  </div>
                </ScrollArea>
              </CardContent>
              <CardFooter className="p-3 pt-0">
                <div className="flex flex-col w-full gap-3">
                  <div className="flex items-center gap-1 text-xs text-muted-foreground">
                    <Clock className="h-3 w-3" />
                    <span>Typical reply time: ~5 minutes</span>
                  </div>
                  <div className="relative">
                    <Textarea
                      value={newMessage}
                      onChange={(e) => setNewMessage(e.target.value)}
                      onKeyDown={handleKeyDown}
                      placeholder="Type your message..."
                      className="min-h-10 resize-none pr-12"
                    />
                    <div className="absolute right-2 bottom-2 flex items-center gap-1">
                      <TooltipProvider>
                        <Tooltip>
                          <TooltipTrigger asChild>
                            <Button variant="ghost" size="icon" className="h-7 w-7">
                              <Paperclip className="h-4 w-4" />
                            </Button>
                          </TooltipTrigger>
                          <TooltipContent>Attach file</TooltipContent>
                        </Tooltip>
                      </TooltipProvider>
                      <Button
                        size="icon"
                        className="h-7 w-7"
                        onClick={handleSendMessage}
                        disabled={newMessage.trim() === ""}
                      >
                        <Send className="h-4 w-4" />
                      </Button>
                    </div>
                  </div>
                </div>
              </CardFooter>
            </Card>
          </motion.div>
        ) : (
          <motion.div
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.8 }}
            transition={{ duration: 0.2 }}
            className="flex flex-col items-end gap-2"
          >
            <AnimatePresence>
              {!isOpen && messages.length > 1 && (
                <motion.div
                  initial={{ opacity: 0, y: 10, scale: 0.9 }}
                  animate={{ opacity: 1, y: 0, scale: 1 }}
                  exit={{ opacity: 0, y: 10, scale: 0.9 }}
                  className="max-w-xs bg-background rounded-lg p-3 shadow-lg border mb-2"
                >
                  <div className="flex items-center gap-2 mb-1">
                    <Avatar className="h-6 w-6">
                      <AvatarImage src={agentAvatar || "/placeholder.svg"} alt={agentName} />
                      <AvatarFallback>{agentName.substring(0, 2)}</AvatarFallback>
                    </Avatar>
                    <span className="text-xs font-medium">{agentName}</span>
                    <Button
                      variant="ghost"
                      size="icon"
                      className="h-5 w-5 ml-auto"
                      onClick={() => setMessages([messages[0]])}
                    >
                      <X className="h-3 w-3" />
                    </Button>
                  </div>
                  <p className="text-sm">{messages[messages.length - 1].content}</p>
                </motion.div>
              )}
            </AnimatePresence>
            <Button onClick={() => setIsOpen(true)} size="icon" className="h-12 w-12 rounded-full shadow-lg">
              {isOpen ? <ChevronDown className="h-6 w-6" /> : <ArrowUpCircle className="h-6 w-6" />}
            </Button>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  )
}

export default CustomerSupportChat

Installation

npx shadcn@latest add @react-market/customer-support-chat

Usage

import { CustomerSupportChat } from "@/components/customer-support-chat"
<CustomerSupportChat />