diff --git a/src/components/chat/chat-session-item.tsx b/src/components/chat/chat-session-item.tsx
new file mode 100644
index 0000000..9abd94c
--- /dev/null
+++ b/src/components/chat/chat-session-item.tsx
@@ -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 (
+
+ )
+}
diff --git a/src/components/chat/chat-session-list.tsx b/src/components/chat/chat-session-list.tsx
new file mode 100644
index 0000000..e84f592
--- /dev/null
+++ b/src/components/chat/chat-session-list.tsx
@@ -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 (
+
+ {/* Header */}
+
+
+
+
+
+
+
Chats
+
+ {sessions.length} conversa{sessions.length !== 1 ? "s" : ""} ativa{sessions.length !== 1 ? "s" : ""}
+ {totalUnread > 0 && (
+
+ ({totalUnread} nao lida{totalUnread !== 1 ? "s" : ""})
+
+ )}
+
+
+
+
+
+
+ {/* Lista de sessoes */}
+
+ {sortedSessions.length === 0 ? (
+
+
+
+
+
Nenhum chat ativo
+
+ Inicie um chat em um ticket para comecar
+
+
+ ) : (
+ sortedSessions.map((session) => (
+
onSelectSession(session.ticketId)}
+ />
+ ))
+ )}
+
+
+ )
+}
diff --git a/src/components/chat/chat-widget.tsx b/src/components/chat/chat-widget.tsx
index 5083ab5..ece49d7 100644
--- a/src/components/chat/chat-widget.tsx
+++ b/src/components/chat/chat-widget.tsx
@@ -8,13 +8,6 @@ import { api } from "@/convex/_generated/api"
import { useAuth } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
import { cn } from "@/lib/utils"
import { toast } from "sonner"
import {
@@ -24,6 +17,7 @@ import {
Minimize2,
User,
ChevronDown,
+ ChevronLeft,
WifiOff,
XCircle,
Paperclip,
@@ -34,16 +28,20 @@ import {
Eye,
Check,
} from "lucide-react"
+import { ChatSessionList } from "./chat-session-list"
const MAX_MESSAGE_LENGTH = 4000
const MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024 // 5MB
const MAX_ATTACHMENTS = 5
const STORAGE_KEY = "chat-widget-state"
+type ViewMode = "list" | "chat"
+
type ChatWidgetState = {
isOpen: boolean
isMinimized: boolean
activeTicketId: string | null
+ viewMode: ViewMode
}
function formatTime(timestamp: number) {
@@ -315,6 +313,17 @@ export function ChatWidget() {
} catch {}
return null
})
+ const [viewMode, setViewMode] = useState(() => {
+ 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 [isSending, setIsSending] = useState(false)
const [isEndingChat, setIsEndingChat] = useState(false)
@@ -369,6 +378,7 @@ export function ChatWidget() {
const state = JSON.parse(event.newValue) as ChatWidgetState
setIsOpen(state.isOpen)
setIsMinimized(state.isMinimized)
+ setViewMode(state.viewMode ?? "list")
if (state.activeTicketId) {
setActiveTicketId(state.activeTicketId)
}
@@ -387,20 +397,32 @@ export function ChatWidget() {
isOpen,
isMinimized,
activeTicketId,
+ viewMode,
}
// Salvar no localStorage (isso dispara evento storage em outras abas automaticamente)
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
} 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(() => {
- 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)
+ 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.
// 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)
if (isTauriContext) return null
@@ -670,106 +702,105 @@ export function ChatWidget() {
{/* Widget aberto */}
{isOpen && !isMinimized && (
- {/* Header - Estilo card da aplicação */}
-
-
-
-
-
-
-
-
Chat
- {/* Indicador online/offline */}
- {liveChat?.hasMachine && (
- machineOnline ? (
-
-
- Online
-
- ) : (
-
-
- Offline
-
- )
- )}
-
- {activeSession && (
-
-
- {/* Botão encerrar chat */}
-
-
-
-
-
-
- {/* Seletor de sessões (se mais de uma) */}
- {activeSessions.length > 1 && (
-
-
-
- )}
{/* Aviso de máquina offline */}
{liveChat?.hasMachine && !machineOnline && (
@@ -956,6 +987,8 @@ export function ChatWidget() {
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.csv"
/>
+ >
+ )}
)}