From 0a0f722bd8b035f4b354a1cbf56793c746e71d7b Mon Sep 17 00:00:00 2001 From: rever-tecnologia Date: Thu, 18 Dec 2025 23:53:24 -0300 Subject: [PATCH] Melhora UX do chat no desktop --- apps/desktop/src-tauri/src/chat.rs | 23 ++++++++++++++ apps/desktop/src-tauri/src/lib.rs | 19 +++++++++--- apps/desktop/src/chat/ChatWidget.tsx | 34 +++++++++++++++++++-- docs/diagnostico-chat-desktop-2025-12-19.md | 13 ++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src-tauri/src/chat.rs b/apps/desktop/src-tauri/src/chat.rs index db8d8fb..7c924de 100644 --- a/apps/desktop/src-tauri/src/chat.rs +++ b/apps/desktop/src-tauri/src/chat.rs @@ -1060,6 +1060,21 @@ async fn process_chat_update( // Serializa operacoes de janela para evitar race/deadlock no Windows (winit/WebView2). static WINDOW_OP_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); +fn hide_other_chat_windows(app: &tauri::AppHandle, active_label: &str) { + for (label, window) in app.webview_windows() { + if !label.starts_with("chat-") { + continue; + } + if label == active_label { + continue; + } + let _ = window.hide(); + } + if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) { + let _ = hub.hide(); + } +} + fn resolve_chat_window_position( app: &tauri::AppHandle, window: Option<&tauri::WebviewWindow>, @@ -1115,6 +1130,10 @@ fn open_chat_window_with_state(app: &tauri::AppHandle, ticket_id: &str, ticket_r start_minimized ); + if !start_minimized { + hide_other_chat_windows(app, &label); + } + // Verificar se ja existe if let Some(window) = app.get_webview_window(&label) { let _ = window.set_ignore_cursor_events(false); @@ -1221,6 +1240,10 @@ fn set_chat_minimized_unlocked(app: &tauri::AppHandle, ticket_id: &str, minimize let label = format!("chat-{}", ticket_id); let window = app.get_webview_window(&label).ok_or("Janela não encontrada")?; + if minimized { + hide_other_chat_windows(app, &label); + } + // Tamanhos - chip minimizado com margem extra para badge (absolute -top-1 -right-1) let (width, height) = if minimized { (240.0, 52.0) // Tamanho com folga para "Ticket #XXX" e badge diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index c09927c..5e3939e 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -410,9 +410,15 @@ async fn upload_chat_file( } #[tauri::command] -fn open_chat_window(app: tauri::AppHandle, ticket_id: String, ticket_ref: u64) -> Result<(), String> { +async fn open_chat_window(app: tauri::AppHandle, ticket_id: String, ticket_ref: u64) -> Result<(), String> { log_info!("[CMD] open_chat_window called: ticket_id={}, ticket_ref={}", ticket_id, ticket_ref); - let result = chat::open_chat_window(&app, &ticket_id, ticket_ref); + let app_handle = app.clone(); + let ticket_id_for_task = ticket_id.clone(); + let result = tauri::async_runtime::spawn_blocking(move || { + chat::open_chat_window(&app_handle, &ticket_id_for_task, ticket_ref) + }) + .await + .map_err(|err| format!("Falha ao abrir chat (join): {err}"))?; log_info!("[CMD] open_chat_window result: {:?}", result); result } @@ -433,8 +439,13 @@ fn set_chat_minimized(app: tauri::AppHandle, ticket_id: String, minimized: bool) } #[tauri::command] -fn open_hub_window(app: tauri::AppHandle) -> Result<(), String> { - chat::open_hub_window(&app) +async fn open_hub_window(app: tauri::AppHandle) -> Result<(), String> { + let app_handle = app.clone(); + tauri::async_runtime::spawn_blocking(move || { + chat::open_hub_window(&app_handle) + }) + .await + .map_err(|err| format!("Falha ao abrir hub (join): {err}"))? } #[tauri::command] diff --git a/apps/desktop/src/chat/ChatWidget.tsx b/apps/desktop/src/chat/ChatWidget.tsx index df93ed5..c60ae67 100644 --- a/apps/desktop/src/chat/ChatWidget.tsx +++ b/apps/desktop/src/chat/ChatWidget.tsx @@ -277,6 +277,8 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { | { type: "message"; messageId: string; behavior: ScrollBehavior; markRead: boolean } | null >(null) + const autoReadInFlightRef = useRef(false) + const lastAutoReadCountRef = useRef(null) const unreadAgentMessageIds = useMemo(() => getUnreadAgentMessageIds(messages, unreadCount), [messages, unreadCount]) const firstUnreadAgentMessageId = unreadAgentMessageIds[0] ?? null @@ -374,9 +376,9 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { }, [apiBaseUrl, machineToken, ticketId]) const markUnreadMessagesRead = useCallback(async () => { - if (unreadCount <= 0) return + if (unreadCount <= 0) return false const ids = getUnreadAgentMessageIds(messages, unreadCount) - if (ids.length === 0) return + if (ids.length === 0) return false const chunks = chunkArray(ids, MARK_READ_BATCH_SIZE) for (const chunk of chunks) { @@ -385,8 +387,26 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { messageIds: chunk as Id<"ticketChatMessages">[], }) } + return true }, [messages, ticketId, unreadCount, markMessagesRead]) + const maybeAutoMarkRead = useCallback(async () => { + if (autoReadInFlightRef.current) return + if (!hasSession || unreadCount <= 0) return + if (isMinimizedRef.current || !isAtBottomRef.current) return + if (lastAutoReadCountRef.current === unreadCount) return + + autoReadInFlightRef.current = true + try { + const didMark = await markUnreadMessagesRead() + if (didMark) { + lastAutoReadCountRef.current = unreadCount + } + } finally { + autoReadInFlightRef.current = false + } + }, [hasSession, unreadCount, markUnreadMessagesRead]) + // Auto-scroll quando novas mensagens chegam (se ja estava no bottom) const prevMessagesLengthRef = useRef(messages.length) useEffect(() => { @@ -430,6 +450,14 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { } }, [isMinimized, messages, markUnreadMessagesRead, scrollToBottom, scrollToMessage]) + useEffect(() => { + if (unreadCount === 0) { + lastAutoReadCountRef.current = null + return + } + maybeAutoMarkRead().catch((err) => console.error("Falha ao auto-marcar mensagens:", err)) + }, [isMinimized, isAtBottom, unreadCount, maybeAutoMarkRead]) + // Sincronizar estado minimizado com tamanho da janela useEffect(() => { const mountTime = Date.now() @@ -538,7 +566,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { setIsMinimized(false) try { - await invoke("set_chat_minimized", { ticketId, minimized: false }) + await invoke("open_chat_window", { ticketId, ticketRef: ticketRef ?? 0 }) } catch (err) { console.error("Erro ao expandir janela:", err) } diff --git a/docs/diagnostico-chat-desktop-2025-12-19.md b/docs/diagnostico-chat-desktop-2025-12-19.md index f5583de..cca63d4 100644 --- a/docs/diagnostico-chat-desktop-2025-12-19.md +++ b/docs/diagnostico-chat-desktop-2025-12-19.md @@ -15,16 +15,29 @@ Relato de instabilidade no chat do desktop (Raven): mensagens enviadas pela web - Logs locais do desktop: - `raven-agent.log` sem entradas `[CHAT DEBUG]`. - `app.log` sem `chat:started`. + - Com duas sessoes ativas, o log parou em: + - `[CMD] open_chat_window called...` + - `[WINDOW] ... build() inicio` + - sem `build() OK` / `open_chat_window result`, indicando travamento na criacao da janela quando chamada via comando. ## Causa raiz O desktop nao estava iniciando o runtime de chat. Em `apps/desktop/src/main.tsx`, o `invoke("start_chat_polling", ...)` enviava `base_url` e `convex_url` em snake_case. No Tauri v2, o mapeamento esperado e camelCase (`baseUrl`, `convexUrl`). Com isso, o comando falha na desserializacao dos args e o chat nao inicia (sem polling/WebSocket), resultando em nenhuma mensagem chegando ao app. +Em cenarios com multiplas sessoes, a abertura do segundo chat via hub usa o comando `open_chat_window` (JS). Esse comando era sincrono e rodava no thread principal; ao criar uma nova janela (`WebviewWindowBuilder::build`), a execucao travava e a janela nao concluia o build, congelando o chat no desktop. + ## Correcoes aplicadas - Ajustado `invoke("start_chat_polling")` para usar `baseUrl` e `convexUrl` (camelCase). +- Tornado `open_chat_window` e `open_hub_window` assíncronos, executando em `spawn_blocking` para evitar bloqueio do thread principal ao criar novas janelas de chat. +- Quando o chat esta aberto e no fim da conversa, o desktop marca automaticamente mensagens como lidas (evita badge preso). +- Ao abrir um chat (foco), outras janelas de chat sao ocultadas e o hub e escondido para evitar sobreposicao. +- Ao minimizar um chat, outras janelas de chat abertas sao ocultadas automaticamente. ## Arquivos alterados - `apps/desktop/src/main.tsx` +- `apps/desktop/src-tauri/src/lib.rs` +- `apps/desktop/src-tauri/src/chat.rs` +- `apps/desktop/src/chat/ChatWidget.tsx` ## Testes recomendados - `bun run lint`