Add chat widget improvements and chat history component

Widget improvements:
- Pulsating badge with unread message count on floating button
- Clickable ticket reference link in chat header
- ExternalLink icon on hover

Desktop (Raven) improvements:
- Track previous unread count for new message detection
- Send native Windows notifications for new messages
- Focus chat window when new messages arrive

Chat history:
- New query getTicketChatHistory for fetching chat sessions and messages
- New component TicketChatHistory displaying chat sessions
- Sessions can be expanded/collapsed to view messages
- Pagination support for long conversations
- Added to both dashboard and portal ticket views

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-07 03:20:22 -03:00
parent b194d77d57
commit d766de4fda
6 changed files with 453 additions and 9 deletions

View file

@ -594,3 +594,93 @@ export const listAgentSessions = query({
return result.sort((a, b) => b.lastActivityAt - a.lastActivityAt)
},
})
// Historico de sessoes de chat de um ticket (para exibicao no painel)
export const getTicketChatHistory = query({
args: {
ticketId: v.id("tickets"),
viewerId: v.id("users"),
},
handler: async (ctx, { ticketId, viewerId }) => {
const ticket = await ctx.db.get(ticketId)
if (!ticket) {
return { sessions: [], totalMessages: 0 }
}
const viewer = await ctx.db.get(viewerId)
if (!viewer || viewer.tenantId !== ticket.tenantId) {
return { sessions: [], totalMessages: 0 }
}
// Buscar todas as sessoes do ticket (ativas e finalizadas)
const sessions = await ctx.db
.query("liveChatSessions")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
.collect()
if (sessions.length === 0) {
return { sessions: [], totalMessages: 0 }
}
// Buscar todas as mensagens do ticket
const allMessages = await ctx.db
.query("ticketChatMessages")
.withIndex("by_ticket_created", (q) => q.eq("ticketId", ticketId))
.collect()
// Agrupar mensagens por sessao (baseado no timestamp)
// Mensagens entre startedAt e endedAt pertencem a sessao
const sessionResults = await Promise.all(
sessions
.sort((a, b) => b.startedAt - a.startedAt) // Mais recente primeiro
.map(async (session) => {
const sessionMessages = allMessages.filter((msg) => {
// Mensagem criada durante a sessao
if (msg.createdAt < session.startedAt) return false
if (session.endedAt && msg.createdAt > session.endedAt) return false
return true
})
// Obter nome da maquina
let machineName = "Cliente"
if (ticket.machineId) {
const machine = await ctx.db.get(ticket.machineId)
if (machine?.hostname) {
machineName = machine.hostname
}
}
return {
sessionId: session._id,
agentName: session.agentSnapshot?.name ?? "Agente",
agentEmail: session.agentSnapshot?.email ?? null,
agentAvatarUrl: session.agentSnapshot?.avatarUrl ?? null,
machineName,
status: session.status,
startedAt: session.startedAt,
endedAt: session.endedAt ?? null,
messageCount: sessionMessages.length,
messages: sessionMessages
.sort((a, b) => a.createdAt - b.createdAt)
.map((msg) => ({
id: msg._id,
body: msg.body,
authorName: msg.authorSnapshot?.name ?? "Usuario",
authorId: String(msg.authorId),
createdAt: msg.createdAt,
attachments: (msg.attachments ?? []).map((att) => ({
storageId: att.storageId,
name: att.name,
type: att.type ?? null,
})),
})),
}
})
)
return {
sessions: sessionResults,
totalMessages: allMessages.length,
}
},
})