Melhora UX do chat no desktop
All checks were successful
All checks were successful
This commit is contained in:
parent
9142446f06
commit
0a0f722bd8
4 changed files with 82 additions and 7 deletions
|
|
@ -1060,6 +1060,21 @@ async fn process_chat_update(
|
||||||
// Serializa operacoes de janela para evitar race/deadlock no Windows (winit/WebView2).
|
// Serializa operacoes de janela para evitar race/deadlock no Windows (winit/WebView2).
|
||||||
static WINDOW_OP_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
static WINDOW_OP_LOCK: Lazy<Mutex<()>> = 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(
|
fn resolve_chat_window_position(
|
||||||
app: &tauri::AppHandle,
|
app: &tauri::AppHandle,
|
||||||
window: Option<&tauri::WebviewWindow>,
|
window: Option<&tauri::WebviewWindow>,
|
||||||
|
|
@ -1115,6 +1130,10 @@ fn open_chat_window_with_state(app: &tauri::AppHandle, ticket_id: &str, ticket_r
|
||||||
start_minimized
|
start_minimized
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if !start_minimized {
|
||||||
|
hide_other_chat_windows(app, &label);
|
||||||
|
}
|
||||||
|
|
||||||
// Verificar se ja existe
|
// Verificar se ja existe
|
||||||
if let Some(window) = app.get_webview_window(&label) {
|
if let Some(window) = app.get_webview_window(&label) {
|
||||||
let _ = window.set_ignore_cursor_events(false);
|
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 label = format!("chat-{}", ticket_id);
|
||||||
let window = app.get_webview_window(&label).ok_or("Janela não encontrada")?;
|
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)
|
// Tamanhos - chip minimizado com margem extra para badge (absolute -top-1 -right-1)
|
||||||
let (width, height) = if minimized {
|
let (width, height) = if minimized {
|
||||||
(240.0, 52.0) // Tamanho com folga para "Ticket #XXX" e badge
|
(240.0, 52.0) // Tamanho com folga para "Ticket #XXX" e badge
|
||||||
|
|
|
||||||
|
|
@ -410,9 +410,15 @@ async fn upload_chat_file(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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);
|
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);
|
log_info!("[CMD] open_chat_window result: {:?}", result);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
@ -433,8 +439,13 @@ fn set_chat_minimized(app: tauri::AppHandle, ticket_id: String, minimized: bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_hub_window(app: tauri::AppHandle) -> Result<(), String> {
|
async fn open_hub_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
chat::open_hub_window(&app)
|
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]
|
#[tauri::command]
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,8 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
| { type: "message"; messageId: string; behavior: ScrollBehavior; markRead: boolean }
|
| { type: "message"; messageId: string; behavior: ScrollBehavior; markRead: boolean }
|
||||||
| null
|
| null
|
||||||
>(null)
|
>(null)
|
||||||
|
const autoReadInFlightRef = useRef(false)
|
||||||
|
const lastAutoReadCountRef = useRef<number | null>(null)
|
||||||
|
|
||||||
const unreadAgentMessageIds = useMemo(() => getUnreadAgentMessageIds(messages, unreadCount), [messages, unreadCount])
|
const unreadAgentMessageIds = useMemo(() => getUnreadAgentMessageIds(messages, unreadCount), [messages, unreadCount])
|
||||||
const firstUnreadAgentMessageId = unreadAgentMessageIds[0] ?? null
|
const firstUnreadAgentMessageId = unreadAgentMessageIds[0] ?? null
|
||||||
|
|
@ -374,9 +376,9 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
}, [apiBaseUrl, machineToken, ticketId])
|
}, [apiBaseUrl, machineToken, ticketId])
|
||||||
|
|
||||||
const markUnreadMessagesRead = useCallback(async () => {
|
const markUnreadMessagesRead = useCallback(async () => {
|
||||||
if (unreadCount <= 0) return
|
if (unreadCount <= 0) return false
|
||||||
const ids = getUnreadAgentMessageIds(messages, unreadCount)
|
const ids = getUnreadAgentMessageIds(messages, unreadCount)
|
||||||
if (ids.length === 0) return
|
if (ids.length === 0) return false
|
||||||
|
|
||||||
const chunks = chunkArray(ids, MARK_READ_BATCH_SIZE)
|
const chunks = chunkArray(ids, MARK_READ_BATCH_SIZE)
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
|
|
@ -385,8 +387,26 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
messageIds: chunk as Id<"ticketChatMessages">[],
|
messageIds: chunk as Id<"ticketChatMessages">[],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}, [messages, ticketId, unreadCount, markMessagesRead])
|
}, [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)
|
// Auto-scroll quando novas mensagens chegam (se ja estava no bottom)
|
||||||
const prevMessagesLengthRef = useRef(messages.length)
|
const prevMessagesLengthRef = useRef(messages.length)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -430,6 +450,14 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
}
|
}
|
||||||
}, [isMinimized, messages, markUnreadMessagesRead, scrollToBottom, scrollToMessage])
|
}, [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
|
// Sincronizar estado minimizado com tamanho da janela
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mountTime = Date.now()
|
const mountTime = Date.now()
|
||||||
|
|
@ -538,7 +566,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
|
|
||||||
setIsMinimized(false)
|
setIsMinimized(false)
|
||||||
try {
|
try {
|
||||||
await invoke("set_chat_minimized", { ticketId, minimized: false })
|
await invoke("open_chat_window", { ticketId, ticketRef: ticketRef ?? 0 })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erro ao expandir janela:", err)
|
console.error("Erro ao expandir janela:", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,29 @@ Relato de instabilidade no chat do desktop (Raven): mensagens enviadas pela web
|
||||||
- Logs locais do desktop:
|
- Logs locais do desktop:
|
||||||
- `raven-agent.log` sem entradas `[CHAT DEBUG]`.
|
- `raven-agent.log` sem entradas `[CHAT DEBUG]`.
|
||||||
- `app.log` sem `chat:started`.
|
- `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
|
## Causa raiz
|
||||||
O desktop nao estava iniciando o runtime de chat.
|
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 `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
|
## Correcoes aplicadas
|
||||||
- Ajustado `invoke("start_chat_polling")` para usar `baseUrl` e `convexUrl` (camelCase).
|
- 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
|
## Arquivos alterados
|
||||||
- `apps/desktop/src/main.tsx`
|
- `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
|
## Testes recomendados
|
||||||
- `bun run lint`
|
- `bun run lint`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue