بلاک‌ها · پشتیبانی و محتوا

چتِ پشتیبانیِ زنده

پیش‌نمایش تمام‌صفحه

ویجتِ گفت‌وگو با پشتیبان: سربرگِ تیره با عکس و وضعیتِ آنلاین، پیام‌ها با ساعت، پاسخ‌های آماده، در حالِ نوشتن و ورودیِ پیام با پیوست.

۱. نصب کامپوننت‌ها

این بلاک از ۶ آیتم دیگ استفاده می‌کند: avatarbuttonchat-messagecontrol-sizeiconsprompt-input

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/prompt-input.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/prompt-input.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/prompt-input.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/prompt-input.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

live-chat.tsx
"use client"

import * as React from "react"

import { XIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ChatMessage, ChatMessageAvatar, ChatMessageBubble, ChatMessageContent } from "@/components/ui/chat-message"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { PromptInput, PromptInputSubmitButton, PromptInputTextarea, PromptInputToolbar, PromptInputTools, PromptInputAttachButton } from "@/components/ui/prompt-input"
import { stockImages } from "@/lib/stock-images"

type Message = { id: number; role: "user" | "assistant"; text: string; time: string }

const INITIAL: Message[] = [
  { id: 1, role: "assistant", text: "سلام، من امیرم از پشتیبانی آوند. چطور کمکت کنم؟", time: "۱۰:۱۲" },
  { id: 2, role: "user", text: "فاکتوری که دیروز فرستادم برای مشتری پیامک نشده.", time: "۱۰:۱۳" },
  { id: 3, role: "assistant", text: "بررسی کردم؛ شمارهٔ مشتری یک رقم کم داشت. اصلاحش کردم و دوباره فرستادم.", time: "۱۰:۱۵" },
]
const QUICK = ["ممنون، حل شد", "یک سؤالِ دیگر دارم", "می‌خواهم تماس بگیرید"]

/**
 * چتِ پشتیبانی زنده (ویجت): سربرگِ تیره با عکسِ پشتیبان، وضعیتِ آنلاین و زمانِ پاسخ؛
 * پیام‌ها با ساعت، پاسخ‌های آماده به‌صورتِ دکمه و ورودی پیام با پیوست. پاسخِ نمونه
 * بعد از «در حالِ نوشتن» می‌آید.
 */
export default function LiveChatBlock() {
  const [messages, setMessages] = React.useState(INITIAL)
  const [typing, setTyping] = React.useState(false)
  const endRef = React.useRef<HTMLDivElement>(null)

  React.useEffect(() => {
    endRef.current?.scrollIntoView({ block: "nearest" })
  }, [messages, typing])

  function send(text: string) {
    if (!text.trim()) return
    setMessages((list) => [...list, { id: list.length + 1, role: "user", text, time: "۱۰:۱۶" }])
    setTyping(true)
    window.setTimeout(() => {
      setTyping(false)
      setMessages((list) => [...list, { id: list.length + 1, role: "assistant", text: "خواهش می‌کنم! اگر کارِ دیگری داشتی همین‌جا بنویس.", time: "۱۰:۱۶" }])
    }, 1200)
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <div className="flex min-h-svh items-end justify-center bg-subtle p-4 sm:items-center">
        <h1 className="sr-only">گفت‌وگو با پشتیبانی</h1>
        <section aria-label="گفت‌وگو با پشتیبانی" className="flex h-[40rem] w-full max-w-md flex-col overflow-hidden rounded-xl border border-border bg-card shadow-card">
          {/* سربرگِ برند با گرادیانِ ملایم، نه سیاه. */}
          <header className="flex items-center gap-3 bg-[linear-gradient(135deg,var(--dig-violet-700),var(--dig-indigo-700))] p-4 text-[var(--dig-white)]">
            <span className="relative">
              <Avatar alt="امیر رضایی" src={stockImages.personMan1.src} />
              <span aria-hidden className="absolute -end-0.5 -bottom-0.5 size-3 rounded-full bg-[var(--dig-green-400)] ring-2 ring-[var(--dig-violet-700)]" />
            </span>
            <span className="flex flex-1 flex-col">
              <span className="font-semibold">امیر رضایی</span>
              <span className="text-xs text-[var(--dig-violet-100)]">آنلاین · معمولاً زیرِ ۵ دقیقه جواب می‌دهد</span>
            </span>
            <Button variant="ghost" size="icon" aria-label="بستنِ گفت‌وگو" className="text-[var(--dig-white)] hover:bg-[color-mix(in_oklab,var(--dig-white)_15%,var(--dig-violet-700))]">
              <XIcon />
            </Button>
          </header>

          <div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4" aria-live="polite">
            <span className="self-center rounded-full bg-subtle px-3 py-1 text-xs text-muted-foreground">امروز</span>
            {messages.map((message) => (
              <ChatMessage key={message.id} role={message.role}>
                {message.role === "assistant" && <ChatMessageAvatar alt="امیر رضایی" src={stockImages.personMan1.src} />}
                <ChatMessageContent>
                  <ChatMessageBubble>{message.text}</ChatMessageBubble>
                  <span className="px-1 text-xs text-muted-foreground">{message.time}</span>
                </ChatMessageContent>
              </ChatMessage>
            ))}
            {typing && <span className="text-sm text-muted-foreground">امیر در حالِ نوشتن است…</span>}
            <div ref={endRef} />
          </div>

          <div className="flex flex-col gap-3 border-t border-border p-4">
            <div className="flex flex-wrap gap-2" aria-label="پاسخ‌های آماده">
              {QUICK.map((item) => (
                <Button key={item} variant="outline" onClick={() => send(item)} disabled={typing}>
                  {item}
                </Button>
              ))}
            </div>
            <PromptInput onSubmit={send} disabled={typing}>
              <PromptInputTextarea placeholder="پیامت را بنویس…" aria-label="پیام" />
              <PromptInputToolbar>
                <PromptInputTools>
                  <PromptInputAttachButton />
                </PromptInputTools>
                <PromptInputSubmitButton loading={typing} />
              </PromptInputToolbar>
            </PromptInput>
          </div>
        </section>
      </div>
    </ControlSizeProvider>
  )
}