feat: melhorias no sistema de chat ao vivo
- Chat do agente abre expandido automaticamente ao iniciar nova sessao - Toasts fecham apos tempo fixo independente do foco da janela - Janela de chat do desktop com transparencia (sem fundo branco) - Chat reabre quando usuario abre o Raven (duplo clique no tray) - Chat nao reabre sozinho com novas mensagens (apenas notificacao) - Mensagem de toast simplificada: "Chat ao vivo iniciado" - Reduz intervalo de polling SSE de 2s para 1s (mais responsivo) 🤖 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
3700ac9dad
commit
24dee5d5eb
9 changed files with 74 additions and 31 deletions
|
|
@ -663,7 +663,7 @@ async fn run_polling_loop(
|
||||||
crate::log_info!("Iniciando polling HTTP (fallback)");
|
crate::log_info!("Iniciando polling HTTP (fallback)");
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let poll_interval = Duration::from_secs(2);
|
let poll_interval = Duration::from_secs(1); // 1s para ser mais responsivo
|
||||||
let mut last_checked_at: Option<i64> = None;
|
let mut last_checked_at: Option<i64> = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -747,10 +747,15 @@ async fn process_chat_update(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Abrir janela de chat imediatamente quando nova sessao e detectada
|
||||||
|
if let Err(e) = open_chat_window(app, &session.ticket_id) {
|
||||||
|
crate::log_warn!("Falha ao abrir janela de chat para nova sessao: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
// Notificacao nativa
|
// Notificacao nativa
|
||||||
let notification_title = format!("Chat iniciado - Chamado #{}", session.ticket_ref);
|
let notification_title = format!("Chat iniciado - Chamado #{}", session.ticket_ref);
|
||||||
let notification_body = format!(
|
let notification_body = format!(
|
||||||
"{} iniciou um chat de suporte.\nClique no icone do Raven para abrir.",
|
"{} iniciou um chat de suporte.",
|
||||||
session.agent_name
|
session.agent_name
|
||||||
);
|
);
|
||||||
let _ = app
|
let _ = app
|
||||||
|
|
@ -797,7 +802,7 @@ async fn process_chat_update(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Notificar novas mensagens
|
// Notificar novas mensagens (sem abrir janela automaticamente - so na nova sessao)
|
||||||
if new_messages && total_unread > 0 {
|
if new_messages && total_unread > 0 {
|
||||||
let new_count = total_unread - prev_unread;
|
let new_count = total_unread - prev_unread;
|
||||||
|
|
||||||
|
|
@ -812,12 +817,7 @@ async fn process_chat_update(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Abrir janela de chat
|
// Notificacao nativa (sem abrir janela - usuario pode clicar para abrir)
|
||||||
if let Some(session) = current_sessions.first() {
|
|
||||||
let _ = open_chat_window(app, &session.ticket_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notificacao nativa
|
|
||||||
let notification_title = "Nova mensagem de suporte";
|
let notification_title = "Nova mensagem de suporte";
|
||||||
let notification_body = if new_count == 1 {
|
let notification_body = if new_count == 1 {
|
||||||
"Voce recebeu 1 nova mensagem no chat".to_string()
|
"Voce recebeu 1 nova mensagem no chat".to_string()
|
||||||
|
|
@ -875,9 +875,10 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str) -> Result<
|
||||||
)
|
)
|
||||||
.title("Chat de Suporte")
|
.title("Chat de Suporte")
|
||||||
.inner_size(380.0, 520.0)
|
.inner_size(380.0, 520.0)
|
||||||
.min_inner_size(300.0, 400.0)
|
.min_inner_size(168.0, 36.0) // Tamanho minimo para modo minimizado
|
||||||
.position(x, y)
|
.position(x, y)
|
||||||
.decorations(false) // Sem decoracoes nativas - usa header customizado
|
.decorations(false) // Sem decoracoes nativas - usa header customizado
|
||||||
|
.transparent(true) // Permite fundo transparente
|
||||||
.always_on_top(true)
|
.always_on_top(true)
|
||||||
.skip_taskbar(true)
|
.skip_taskbar(true)
|
||||||
.focused(true)
|
.focused(true)
|
||||||
|
|
@ -914,9 +915,9 @@ pub fn set_chat_minimized(app: &tauri::AppHandle, ticket_id: &str, minimized: bo
|
||||||
let label = format!("chat-{}", ticket_id);
|
let label = format!("chat-{}", ticket_id);
|
||||||
let window = app.get_webview_window(&label).ok_or("Janela nao encontrada")?;
|
let window = app.get_webview_window(&label).ok_or("Janela nao encontrada")?;
|
||||||
|
|
||||||
// Tamanhos
|
// Tamanhos - chip minimizado precisa ser exato para transparencia funcionar
|
||||||
let (width, height) = if minimized {
|
let (width, height) = if minimized {
|
||||||
(200.0, 56.0) // Tamanho do chip
|
(168.0, 36.0) // Tamanho exato do chip rounded-full
|
||||||
} else {
|
} else {
|
||||||
(380.0, 520.0) // Tamanho expandido
|
(380.0, 520.0) // Tamanho expandido
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,13 @@ fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> {
|
||||||
let _ = win.show();
|
let _ = win.show();
|
||||||
let _ = win.set_focus();
|
let _ = win.set_focus();
|
||||||
}
|
}
|
||||||
|
// Reabrir chat se houver sessao ativa
|
||||||
|
if let Some(chat_runtime) = tray.app_handle().try_state::<ChatRuntime>() {
|
||||||
|
let sessions = chat_runtime.get_sessions();
|
||||||
|
if let Some(session) = sessions.first() {
|
||||||
|
let _ = chat::open_chat_window(tray.app_handle(), &session.ticket_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"chat" => {
|
"chat" => {
|
||||||
// Abrir janela de chat se houver sessao ativa
|
// Abrir janela de chat se houver sessao ativa
|
||||||
|
|
@ -601,6 +608,13 @@ fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> {
|
||||||
let _ = win.show();
|
let _ = win.show();
|
||||||
let _ = win.set_focus();
|
let _ = win.set_focus();
|
||||||
}
|
}
|
||||||
|
// Reabrir chat se houver sessao ativa
|
||||||
|
if let Some(chat_runtime) = tray.app_handle().try_state::<ChatRuntime>() {
|
||||||
|
let sessions = chat_runtime.get_sessions();
|
||||||
|
if let Some(session) = sessions.first() {
|
||||||
|
let _ = chat::open_chat_window(tray.app_handle(), &session.ticket_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@ export function ChatWidget({ ticketId }: ChatWidgetProps) {
|
||||||
// Quando não há sessão, mostrar versão minimizada com indicador de offline
|
// Quando não há sessão, mostrar versão minimizada com indicador de offline
|
||||||
if (!hasSession) {
|
if (!hasSession) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col items-center justify-end bg-transparent p-4">
|
<div className="flex h-full w-full items-end justify-end bg-transparent">
|
||||||
<div className="flex items-center gap-2 rounded-full bg-slate-200 px-4 py-2 text-slate-600 shadow-lg">
|
<div className="flex items-center gap-2 rounded-full bg-slate-200 px-4 py-2 text-slate-600 shadow-lg">
|
||||||
<MessageCircle className="size-4" />
|
<MessageCircle className="size-4" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
|
|
@ -375,7 +375,7 @@ export function ChatWidget({ ticketId }: ChatWidgetProps) {
|
||||||
// Versão minimizada (chip compacto igual web)
|
// Versão minimizada (chip compacto igual web)
|
||||||
if (isMinimized) {
|
if (isMinimized) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-transparent">
|
<div className="flex h-full w-full items-end justify-end bg-transparent">
|
||||||
<button
|
<button
|
||||||
onClick={handleExpand}
|
onClick={handleExpand}
|
||||||
className="flex items-center gap-2 rounded-full bg-black px-4 py-2 text-white shadow-lg hover:bg-black/90"
|
className="flex items-center gap-2 rounded-full bg-black px-4 py-2 text-white shadow-lg hover:bg-black/90"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,13 @@ html, body, #root {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-slate-50 text-slate-900;
|
@apply text-slate-900;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fundo padrão para janelas que não são chat minimizado */
|
||||||
|
.app-bg {
|
||||||
|
@apply bg-slate-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-status {
|
.badge-status {
|
||||||
|
|
|
||||||
|
|
@ -1443,7 +1443,7 @@ const resolvedAppUrl = useMemo(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen grid place-items-center p-6">
|
<div className="min-h-screen grid place-items-center bg-slate-50 p-6">
|
||||||
{token && !isMachineActive ? (
|
{token && !isMachineActive ? (
|
||||||
<DeactivationScreen companyName={companyName} />
|
<DeactivationScreen companyName={companyName} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export async function GET(request: Request) {
|
||||||
sendEvent("heartbeat", { ts: Date.now() })
|
sendEvent("heartbeat", { ts: Date.now() })
|
||||||
}, 30_000)
|
}, 30_000)
|
||||||
|
|
||||||
// Poll interno a cada 2s e push via SSE
|
// Poll interno a cada 1s e push via SSE (responsivo para chat)
|
||||||
const pollInterval = setInterval(async () => {
|
const pollInterval = setInterval(async () => {
|
||||||
if (isAborted) {
|
if (isAborted) {
|
||||||
clearInterval(pollInterval)
|
clearInterval(pollInterval)
|
||||||
|
|
@ -118,7 +118,7 @@ export async function GET(request: Request) {
|
||||||
clearInterval(heartbeatInterval)
|
clearInterval(heartbeatInterval)
|
||||||
controller.close()
|
controller.close()
|
||||||
}
|
}
|
||||||
}, 2_000)
|
}, 1_000)
|
||||||
|
|
||||||
// Enviar evento inicial de conexao
|
// Enviar evento inicial de conexao
|
||||||
sendEvent("connected", { ts: Date.now() })
|
sendEvent("connected", { ts: Date.now() })
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ export function ChatWidget() {
|
||||||
const inputRef = useRef<HTMLTextAreaElement | null>(null)
|
const inputRef = useRef<HTMLTextAreaElement | null>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const dropAreaRef = useRef<HTMLDivElement | null>(null)
|
const dropAreaRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const prevSessionCountRef = useRef<number>(0)
|
||||||
|
|
||||||
// Buscar sessões de chat ativas do agente
|
// Buscar sessões de chat ativas do agente
|
||||||
const activeSessions = useQuery(
|
const activeSessions = useQuery(
|
||||||
|
|
@ -285,6 +286,26 @@ export function ChatWidget() {
|
||||||
}
|
}
|
||||||
}, [activeTicketId, activeSessions])
|
}, [activeTicketId, activeSessions])
|
||||||
|
|
||||||
|
// Auto-abrir widget quando uma nova sessão é iniciada
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeSessions) return
|
||||||
|
const currentCount = activeSessions.length
|
||||||
|
const prevCount = prevSessionCountRef.current
|
||||||
|
|
||||||
|
// Se aumentou o número de sessões, é uma nova sessão - abrir o widget expandido
|
||||||
|
if (currentCount > prevCount && prevCount >= 0) {
|
||||||
|
setIsOpen(true)
|
||||||
|
setIsMinimized(false)
|
||||||
|
// Selecionar a sessão mais recente (última da lista ou primeira se única)
|
||||||
|
const newestSession = activeSessions[activeSessions.length - 1] ?? activeSessions[0]
|
||||||
|
if (newestSession) {
|
||||||
|
setActiveTicketId(newestSession.ticketId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSessionCountRef.current = currentCount
|
||||||
|
}, [activeSessions])
|
||||||
|
|
||||||
// Scroll para última mensagem
|
// Scroll para última mensagem
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current && isOpen && !isMinimized) {
|
if (messagesEndRef.current && isOpen && !isMinimized) {
|
||||||
|
|
|
||||||
|
|
@ -133,9 +133,9 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) {
|
||||||
actorId: viewerId as Id<"users">,
|
actorId: viewerId as Id<"users">,
|
||||||
})
|
})
|
||||||
if (result.isNew) {
|
if (result.isNew) {
|
||||||
toast.success("Chat ao vivo iniciado! O cliente será notificado.", { id: "live-chat" })
|
toast.success("Chat ao vivo iniciado", { id: "live-chat" })
|
||||||
} else {
|
} else {
|
||||||
toast.info("Já existe uma sessão de chat ativa.", { id: "live-chat" })
|
toast.info("Já existe uma sessão de chat ativa", { id: "live-chat" })
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : "Não foi possível iniciar o chat"
|
const message = error instanceof Error ? error.message : "Não foi possível iniciar o chat"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
const baseClass =
|
const baseClass =
|
||||||
"inline-flex w-auto min-w-0 items-center justify-center gap-2 self-center rounded-xl border border-black bg-black px-3 py-2 text-sm font-medium text-white shadow-lg"
|
"inline-flex w-auto min-w-0 items-center justify-center gap-2 self-center rounded-xl border border-black bg-black px-3 py-2 text-sm font-medium text-white shadow-lg"
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
|
|
@ -23,6 +23,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
|
pauseWhenPageIsHidden={false}
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
className: baseClass,
|
className: baseClass,
|
||||||
style: baseStyle,
|
style: baseStyle,
|
||||||
|
|
@ -44,6 +45,6 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Toaster }
|
export { Toaster }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue