feat(chat): adiciona interface de lista de chats estilo WhatsApp
- Cria ChatSessionList e ChatSessionItem para listar sessões ativas - Refatora ChatWidget para usar viewMode (list/chat) - Ordena por não lidos primeiro, depois por última atividade - Adiciona botão de voltar quando há múltiplos chats - Persiste viewMode no localStorage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc5ba0c73a
commit
95ab1b5f0c
3 changed files with 359 additions and 107 deletions
107
src/components/chat/chat-session-item.tsx
Normal file
107
src/components/chat/chat-session-item.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { MessageCircle, WifiOff } from "lucide-react"
|
||||||
|
|
||||||
|
type ChatSession = {
|
||||||
|
ticketId: string
|
||||||
|
ticketRef: number
|
||||||
|
ticketSubject: string
|
||||||
|
sessionId: string
|
||||||
|
agentId: string
|
||||||
|
unreadCount: number
|
||||||
|
lastActivityAt: number
|
||||||
|
machineHostname?: string | null
|
||||||
|
machineOnline?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatSessionItemProps = {
|
||||||
|
session: ChatSession
|
||||||
|
isActive?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(timestamp: number) {
|
||||||
|
const now = Date.now()
|
||||||
|
const diff = now - timestamp
|
||||||
|
const minutes = Math.floor(diff / 60000)
|
||||||
|
const hours = Math.floor(diff / 3600000)
|
||||||
|
const days = Math.floor(diff / 86400000)
|
||||||
|
|
||||||
|
if (minutes < 1) return "Agora"
|
||||||
|
if (minutes < 60) return `${minutes}min`
|
||||||
|
if (hours < 24) return `${hours}h`
|
||||||
|
if (days === 1) return "Ontem"
|
||||||
|
|
||||||
|
return new Date(timestamp).toLocaleDateString("pt-BR", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatSessionItem({ session, isActive, onClick }: ChatSessionItemProps) {
|
||||||
|
const hasUnread = session.unreadCount > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-start gap-3 border-b border-slate-100 px-4 py-3 text-left transition-colors hover:bg-slate-50",
|
||||||
|
isActive && "bg-slate-100",
|
||||||
|
hasUnread && "bg-red-50/50 hover:bg-red-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Avatar/Icone */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex size-10 shrink-0 items-center justify-center rounded-full",
|
||||||
|
hasUnread ? "bg-red-100 text-red-600" : "bg-slate-100 text-slate-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MessageCircle className="size-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conteudo */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={cn("text-sm font-semibold", hasUnread ? "text-red-700" : "text-slate-900")}>
|
||||||
|
#{session.ticketRef}
|
||||||
|
</span>
|
||||||
|
{/* Indicador online/offline */}
|
||||||
|
{session.machineOnline !== undefined && (
|
||||||
|
session.machineOnline ? (
|
||||||
|
<span className="size-2 rounded-full bg-emerald-500" title="Online" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="size-3 text-slate-400" title="Offline" />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
{formatTime(session.lastActivityAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-0.5 truncate text-sm text-slate-600">
|
||||||
|
{session.ticketSubject}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{session.machineHostname && (
|
||||||
|
<p className="mt-0.5 truncate text-xs text-slate-400">
|
||||||
|
{session.machineHostname}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badge de nao lidos */}
|
||||||
|
{hasUnread && (
|
||||||
|
<div className="flex shrink-0 items-center justify-center">
|
||||||
|
<span className="flex size-5 items-center justify-center rounded-full bg-red-500 text-xs font-bold text-white">
|
||||||
|
{session.unreadCount > 9 ? "9+" : session.unreadCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
112
src/components/chat/chat-session-list.tsx
Normal file
112
src/components/chat/chat-session-list.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { MessageCircle, X } from "lucide-react"
|
||||||
|
import { ChatSessionItem } from "./chat-session-item"
|
||||||
|
|
||||||
|
type ChatSession = {
|
||||||
|
ticketId: string
|
||||||
|
ticketRef: number
|
||||||
|
ticketSubject: string
|
||||||
|
sessionId: string
|
||||||
|
agentId: string
|
||||||
|
unreadCount: number
|
||||||
|
lastActivityAt: number
|
||||||
|
machineHostname?: string | null
|
||||||
|
machineOnline?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatSessionListProps = {
|
||||||
|
sessions: ChatSession[]
|
||||||
|
activeTicketId?: string | null
|
||||||
|
onSelectSession: (ticketId: string) => void
|
||||||
|
onClose: () => void
|
||||||
|
onMinimize: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatSessionList({
|
||||||
|
sessions,
|
||||||
|
activeTicketId,
|
||||||
|
onSelectSession,
|
||||||
|
onClose,
|
||||||
|
onMinimize,
|
||||||
|
}: ChatSessionListProps) {
|
||||||
|
// Ordenar: nao lidos primeiro, depois por ultima atividade (desc)
|
||||||
|
const sortedSessions = useMemo(() => {
|
||||||
|
return [...sessions].sort((a, b) => {
|
||||||
|
// Nao lidos primeiro
|
||||||
|
if (a.unreadCount > 0 && b.unreadCount === 0) return -1
|
||||||
|
if (a.unreadCount === 0 && b.unreadCount > 0) return 1
|
||||||
|
// Depois por ultima atividade (mais recente primeiro)
|
||||||
|
return b.lastActivityAt - a.lastActivityAt
|
||||||
|
})
|
||||||
|
}, [sessions])
|
||||||
|
|
||||||
|
const totalUnread = sessions.reduce((sum, s) => sum + s.unreadCount, 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-200 bg-white px-4 py-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-10 items-center justify-center rounded-full bg-black text-white">
|
||||||
|
<MessageCircle className="size-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-slate-900">Chats</p>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
{sessions.length} conversa{sessions.length !== 1 ? "s" : ""} ativa{sessions.length !== 1 ? "s" : ""}
|
||||||
|
{totalUnread > 0 && (
|
||||||
|
<span className="ml-1 text-red-500">
|
||||||
|
({totalUnread} nao lida{totalUnread !== 1 ? "s" : ""})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={onMinimize}
|
||||||
|
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||||
|
title="Minimizar"
|
||||||
|
>
|
||||||
|
<svg className="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||||
|
title="Fechar"
|
||||||
|
>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de sessoes */}
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white">
|
||||||
|
{sortedSessions.length === 0 ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center p-4 text-center">
|
||||||
|
<div className="flex size-12 items-center justify-center rounded-full bg-slate-100">
|
||||||
|
<MessageCircle className="size-6 text-slate-400" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm font-medium text-slate-600">Nenhum chat ativo</p>
|
||||||
|
<p className="mt-1 text-xs text-slate-400">
|
||||||
|
Inicie um chat em um ticket para comecar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
sortedSessions.map((session) => (
|
||||||
|
<ChatSessionItem
|
||||||
|
key={session.ticketId}
|
||||||
|
session={session}
|
||||||
|
isActive={session.ticketId === activeTicketId}
|
||||||
|
onClick={() => onSelectSession(session.ticketId)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,6 @@ import { api } from "@/convex/_generated/api"
|
||||||
import { useAuth } from "@/lib/auth-client"
|
import { useAuth } from "@/lib/auth-client"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Spinner } from "@/components/ui/spinner"
|
import { Spinner } from "@/components/ui/spinner"
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select"
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,6 +17,7 @@ import {
|
||||||
Minimize2,
|
Minimize2,
|
||||||
User,
|
User,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
WifiOff,
|
WifiOff,
|
||||||
XCircle,
|
XCircle,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
|
|
@ -34,16 +28,20 @@ import {
|
||||||
Eye,
|
Eye,
|
||||||
Check,
|
Check,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
import { ChatSessionList } from "./chat-session-list"
|
||||||
|
|
||||||
const MAX_MESSAGE_LENGTH = 4000
|
const MAX_MESSAGE_LENGTH = 4000
|
||||||
const MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024 // 5MB
|
const MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024 // 5MB
|
||||||
const MAX_ATTACHMENTS = 5
|
const MAX_ATTACHMENTS = 5
|
||||||
const STORAGE_KEY = "chat-widget-state"
|
const STORAGE_KEY = "chat-widget-state"
|
||||||
|
|
||||||
|
type ViewMode = "list" | "chat"
|
||||||
|
|
||||||
type ChatWidgetState = {
|
type ChatWidgetState = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
isMinimized: boolean
|
isMinimized: boolean
|
||||||
activeTicketId: string | null
|
activeTicketId: string | null
|
||||||
|
viewMode: ViewMode
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(timestamp: number) {
|
function formatTime(timestamp: number) {
|
||||||
|
|
@ -315,6 +313,17 @@ export function ChatWidget() {
|
||||||
} catch {}
|
} catch {}
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
||||||
|
if (typeof window === "undefined") return "list"
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (saved) {
|
||||||
|
const state = JSON.parse(saved) as ChatWidgetState
|
||||||
|
return state.viewMode ?? "list"
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return "list"
|
||||||
|
})
|
||||||
const [draft, setDraft] = useState("")
|
const [draft, setDraft] = useState("")
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
const [isEndingChat, setIsEndingChat] = useState(false)
|
const [isEndingChat, setIsEndingChat] = useState(false)
|
||||||
|
|
@ -369,6 +378,7 @@ export function ChatWidget() {
|
||||||
const state = JSON.parse(event.newValue) as ChatWidgetState
|
const state = JSON.parse(event.newValue) as ChatWidgetState
|
||||||
setIsOpen(state.isOpen)
|
setIsOpen(state.isOpen)
|
||||||
setIsMinimized(state.isMinimized)
|
setIsMinimized(state.isMinimized)
|
||||||
|
setViewMode(state.viewMode ?? "list")
|
||||||
if (state.activeTicketId) {
|
if (state.activeTicketId) {
|
||||||
setActiveTicketId(state.activeTicketId)
|
setActiveTicketId(state.activeTicketId)
|
||||||
}
|
}
|
||||||
|
|
@ -387,20 +397,32 @@ export function ChatWidget() {
|
||||||
isOpen,
|
isOpen,
|
||||||
isMinimized,
|
isMinimized,
|
||||||
activeTicketId,
|
activeTicketId,
|
||||||
|
viewMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Salvar no localStorage (isso dispara evento storage em outras abas automaticamente)
|
// Salvar no localStorage (isso dispara evento storage em outras abas automaticamente)
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
||||||
} catch {}
|
} catch {}
|
||||||
}, [isOpen, isMinimized, activeTicketId])
|
}, [isOpen, isMinimized, activeTicketId, viewMode])
|
||||||
|
|
||||||
// Auto-selecionar primeira sessão se nenhuma selecionada
|
// Auto-selecionar modo e sessao baseado na quantidade de sessoes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeTicketId && activeSessions && activeSessions.length > 0) {
|
if (!activeSessions) return
|
||||||
|
|
||||||
|
if (activeSessions.length === 0) {
|
||||||
|
// Sem sessoes, limpar estado
|
||||||
|
setActiveTicketId(null)
|
||||||
|
setViewMode("list")
|
||||||
|
} else if (activeSessions.length === 1) {
|
||||||
|
// Apenas 1 sessao, ir direto para chat
|
||||||
setActiveTicketId(activeSessions[0].ticketId)
|
setActiveTicketId(activeSessions[0].ticketId)
|
||||||
|
setViewMode("chat")
|
||||||
|
} else if (!activeTicketId) {
|
||||||
|
// Multiplas sessoes mas nenhuma selecionada, mostrar lista
|
||||||
|
setViewMode("list")
|
||||||
}
|
}
|
||||||
}, [activeTicketId, activeSessions])
|
}, [activeSessions, activeTicketId])
|
||||||
|
|
||||||
// Auto-abrir o widget quando ESTE agente iniciar uma nova sessão de chat.
|
// Auto-abrir o widget quando ESTE agente iniciar uma nova sessão de chat.
|
||||||
// Nao roda na montagem inicial para nao sobrescrever o estado do localStorage.
|
// Nao roda na montagem inicial para nao sobrescrever o estado do localStorage.
|
||||||
|
|
@ -648,6 +670,16 @@ export function ChatWidget() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handlers para navegacao lista/chat
|
||||||
|
const handleSelectSession = (ticketId: string) => {
|
||||||
|
setActiveTicketId(ticketId)
|
||||||
|
setViewMode("chat")
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBackToList = () => {
|
||||||
|
setViewMode("list")
|
||||||
|
}
|
||||||
|
|
||||||
// Nao mostrar se esta no Tauri (usa o chat nativo)
|
// Nao mostrar se esta no Tauri (usa o chat nativo)
|
||||||
if (isTauriContext) return null
|
if (isTauriContext) return null
|
||||||
|
|
||||||
|
|
@ -670,15 +702,38 @@ export function ChatWidget() {
|
||||||
{/* Widget aberto */}
|
{/* Widget aberto */}
|
||||||
{isOpen && !isMinimized && (
|
{isOpen && !isMinimized && (
|
||||||
<div className="flex h-[520px] w-[400px] flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl">
|
<div className="flex h-[520px] w-[400px] flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl">
|
||||||
{/* Header - Estilo card da aplicação */}
|
{/* Modo Lista - mostra quando viewMode === "list" e ha multiplas sessoes */}
|
||||||
|
{viewMode === "list" && activeSessions.length > 1 ? (
|
||||||
|
<ChatSessionList
|
||||||
|
sessions={activeSessions}
|
||||||
|
activeTicketId={activeTicketId}
|
||||||
|
onSelectSession={handleSelectSession}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
onMinimize={() => setIsMinimized(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Header - Modo Chat */}
|
||||||
<div className="flex items-center justify-between border-b border-slate-200 bg-white px-4 py-3">
|
<div className="flex items-center justify-between border-b border-slate-200 bg-white px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Botao voltar para lista (quando ha multiplas sessoes) */}
|
||||||
|
{activeSessions.length > 1 && (
|
||||||
|
<button
|
||||||
|
onClick={handleBackToList}
|
||||||
|
className="flex size-8 items-center justify-center rounded-lg text-slate-500 hover:bg-slate-100"
|
||||||
|
title="Voltar para lista"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="flex size-10 items-center justify-center rounded-full bg-black text-white">
|
<div className="flex size-10 items-center justify-center rounded-full bg-black text-white">
|
||||||
<MessageCircle className="size-5" />
|
<MessageCircle className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-sm font-semibold text-slate-900">Chat</p>
|
<p className="text-sm font-semibold text-slate-900">
|
||||||
|
{activeSession ? `#${activeSession.ticketRef}` : "Chat"}
|
||||||
|
</p>
|
||||||
{/* Indicador online/offline */}
|
{/* Indicador online/offline */}
|
||||||
{liveChat?.hasMachine && (
|
{liveChat?.hasMachine && (
|
||||||
machineOnline ? (
|
machineOnline ? (
|
||||||
|
|
@ -702,7 +757,7 @@ export function ChatWidget() {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group flex items-center gap-1 text-xs text-slate-500 hover:text-slate-900 transition-colors"
|
className="group flex items-center gap-1 text-xs text-slate-500 hover:text-slate-900 transition-colors"
|
||||||
>
|
>
|
||||||
<span>#{activeSession.ticketRef}</span>
|
<span className="max-w-[160px] truncate">{activeSession.ticketSubject}</span>
|
||||||
<ExternalLink className="size-3 opacity-0 group-hover:opacity-100 transition-opacity" />
|
<ExternalLink className="size-3 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
</a>
|
</a>
|
||||||
{machineHostname && (
|
{machineHostname && (
|
||||||
|
|
@ -715,7 +770,7 @@ export function ChatWidget() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{/* Botão encerrar chat */}
|
{/* Botao encerrar chat */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -747,30 +802,6 @@ export function ChatWidget() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Seletor de sessões (se mais de uma) */}
|
|
||||||
{activeSessions.length > 1 && (
|
|
||||||
<div className="border-b border-slate-100 bg-slate-50 px-3 py-2">
|
|
||||||
<Select
|
|
||||||
value={activeTicketId ?? ""}
|
|
||||||
onValueChange={setActiveTicketId}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 w-full border-slate-200 bg-white text-sm">
|
|
||||||
<SelectValue placeholder="Selecione uma conversa" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{activeSessions.map((session) => (
|
|
||||||
<SelectItem key={session.ticketId} value={session.ticketId}>
|
|
||||||
#{session.ticketRef} - {session.ticketSubject.slice(0, 25)}
|
|
||||||
{session.unreadCount > 0 && (
|
|
||||||
<span className="ml-1 text-red-500">({session.unreadCount})</span>
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Aviso de máquina offline */}
|
{/* Aviso de máquina offline */}
|
||||||
{liveChat?.hasMachine && !machineOnline && (
|
{liveChat?.hasMachine && !machineOnline && (
|
||||||
<div className="border-b border-amber-200 bg-amber-50 px-3 py-2">
|
<div className="border-b border-amber-200 bg-amber-50 px-3 py-2">
|
||||||
|
|
@ -956,6 +987,8 @@ export function ChatWidget() {
|
||||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.csv"
|
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.csv"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue