Implementa sistema de chat em tempo real entre agente e cliente
- Adiciona tabela liveChatSessions no schema Convex - Cria convex/liveChat.ts com mutations e queries para chat - Adiciona API routes para maquinas (sessions, messages, poll) - Cria modulo chat.rs no Tauri com ChatRuntime e polling - Adiciona comandos de chat no lib.rs (start/stop polling, open/close window) - Cria componentes React do chat widget (ChatWidget, types) - Adiciona botao "Iniciar Chat" no dashboard (ticket-chat-panel) - Implementa menu de chat no system tray - Polling de 2 segundos para maior responsividade - Janela de chat flutuante, frameless, always-on-top 🤖 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
0c8d53c0b6
commit
ba91c1e0f5
15 changed files with 2004 additions and 15 deletions
482
apps/desktop/src-tauri/src/chat.rs
Normal file
482
apps/desktop/src-tauri/src/chat.rs
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
//! Modulo de Chat em Tempo Real
|
||||
//!
|
||||
//! Este modulo implementa o sistema de chat entre agentes (dashboard web)
|
||||
//! e clientes (Raven desktop). Inclui polling de mensagens, gerenciamento
|
||||
//! de janelas de chat e emissao de eventos.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::async_runtime::JoinHandle;
|
||||
use tauri::{Emitter, Manager, WebviewWindowBuilder, WebviewUrl};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatSession {
|
||||
pub session_id: String,
|
||||
pub ticket_id: String,
|
||||
pub ticket_ref: u64,
|
||||
pub ticket_subject: String,
|
||||
pub agent_name: String,
|
||||
pub agent_email: Option<String>,
|
||||
pub agent_avatar_url: Option<String>,
|
||||
pub unread_count: u32,
|
||||
pub last_activity_at: i64,
|
||||
pub started_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatMessage {
|
||||
pub id: String,
|
||||
pub body: String,
|
||||
pub author_name: String,
|
||||
pub author_avatar_url: Option<String>,
|
||||
pub is_from_machine: bool,
|
||||
pub created_at: i64,
|
||||
pub attachments: Vec<ChatAttachment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatAttachment {
|
||||
pub storage_id: String,
|
||||
pub name: String,
|
||||
pub size: Option<u64>,
|
||||
#[serde(rename = "type")]
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatPollResponse {
|
||||
pub has_active_sessions: bool,
|
||||
pub sessions: Vec<ChatSessionSummary>,
|
||||
pub total_unread: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatSessionSummary {
|
||||
pub ticket_id: String,
|
||||
pub unread_count: u32,
|
||||
pub last_activity_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatMessagesResponse {
|
||||
pub messages: Vec<ChatMessage>,
|
||||
pub has_session: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendMessageResponse {
|
||||
pub message_id: String,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SessionStartedEvent {
|
||||
pub session: ChatSession,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTTP CLIENT
|
||||
// ============================================================================
|
||||
|
||||
static CHAT_CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||
Client::builder()
|
||||
.user_agent("raven-chat/1.0")
|
||||
.timeout(Duration::from_secs(15))
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.expect("failed to build chat http client")
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// API FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
pub async fn poll_chat_updates(
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
last_checked_at: Option<i64>,
|
||||
) -> Result<ChatPollResponse, String> {
|
||||
let url = format!("{}/api/machines/chat/poll", base_url);
|
||||
|
||||
let mut payload = serde_json::json!({
|
||||
"machineToken": token,
|
||||
});
|
||||
|
||||
if let Some(ts) = last_checked_at {
|
||||
payload["lastCheckedAt"] = serde_json::json!(ts);
|
||||
}
|
||||
|
||||
let response = CHAT_CLIENT
|
||||
.post(&url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Falha na requisicao de poll: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Poll falhou: status={}, body={}", status, body));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Falha ao parsear resposta de poll: {e}"))
|
||||
}
|
||||
|
||||
pub async fn fetch_sessions(base_url: &str, token: &str) -> Result<Vec<ChatSession>, String> {
|
||||
let url = format!("{}/api/machines/chat/sessions", base_url);
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"machineToken": token,
|
||||
});
|
||||
|
||||
let response = CHAT_CLIENT
|
||||
.post(&url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Falha na requisicao de sessions: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Sessions falhou: status={}, body={}", status, body));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SessionsResponse {
|
||||
sessions: Vec<ChatSession>,
|
||||
}
|
||||
|
||||
let data: SessionsResponse = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Falha ao parsear resposta de sessions: {e}"))?;
|
||||
|
||||
Ok(data.sessions)
|
||||
}
|
||||
|
||||
pub async fn fetch_messages(
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
ticket_id: &str,
|
||||
since: Option<i64>,
|
||||
) -> Result<ChatMessagesResponse, String> {
|
||||
let url = format!("{}/api/machines/chat/messages", base_url);
|
||||
|
||||
let mut payload = serde_json::json!({
|
||||
"machineToken": token,
|
||||
"ticketId": ticket_id,
|
||||
"action": "list",
|
||||
});
|
||||
|
||||
if let Some(ts) = since {
|
||||
payload["since"] = serde_json::json!(ts);
|
||||
}
|
||||
|
||||
let response = CHAT_CLIENT
|
||||
.post(&url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Falha na requisicao de messages: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Messages falhou: status={}, body={}", status, body));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Falha ao parsear resposta de messages: {e}"))
|
||||
}
|
||||
|
||||
pub async fn send_message(
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
ticket_id: &str,
|
||||
body: &str,
|
||||
) -> Result<SendMessageResponse, String> {
|
||||
let url = format!("{}/api/machines/chat/messages", base_url);
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"machineToken": token,
|
||||
"ticketId": ticket_id,
|
||||
"action": "send",
|
||||
"body": body,
|
||||
});
|
||||
|
||||
let response = CHAT_CLIENT
|
||||
.post(&url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Falha na requisicao de send: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Send falhou: status={}, body={}", status, body));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Falha ao parsear resposta de send: {e}"))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CHAT RUNTIME
|
||||
// ============================================================================
|
||||
|
||||
struct ChatPollerHandle {
|
||||
stop_signal: Arc<Notify>,
|
||||
join_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl ChatPollerHandle {
|
||||
fn stop(self) {
|
||||
self.stop_signal.notify_waiters();
|
||||
self.join_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ChatRuntime {
|
||||
inner: Arc<Mutex<Option<ChatPollerHandle>>>,
|
||||
last_sessions: Arc<Mutex<Vec<ChatSession>>>,
|
||||
}
|
||||
|
||||
impl ChatRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(None)),
|
||||
last_sessions: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_polling(
|
||||
&self,
|
||||
base_url: String,
|
||||
token: String,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let sanitized_base = base_url.trim().trim_end_matches('/').to_string();
|
||||
if sanitized_base.is_empty() {
|
||||
return Err("URL base invalida".to_string());
|
||||
}
|
||||
|
||||
// Para polling existente
|
||||
{
|
||||
let mut guard = self.inner.lock();
|
||||
if let Some(handle) = guard.take() {
|
||||
handle.stop();
|
||||
}
|
||||
}
|
||||
|
||||
let stop_signal = Arc::new(Notify::new());
|
||||
let stop_clone = stop_signal.clone();
|
||||
let base_clone = sanitized_base.clone();
|
||||
let token_clone = token.clone();
|
||||
let last_sessions = self.last_sessions.clone();
|
||||
|
||||
let join_handle = tauri::async_runtime::spawn(async move {
|
||||
crate::log_info!("Chat polling iniciado");
|
||||
|
||||
let mut last_checked_at: Option<i64> = None;
|
||||
let poll_interval = Duration::from_secs(2); // Intervalo reduzido para maior responsividade
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = stop_clone.notified() => {
|
||||
crate::log_info!("Chat polling encerrado");
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(poll_interval) => {
|
||||
match poll_chat_updates(&base_clone, &token_clone, last_checked_at).await {
|
||||
Ok(result) => {
|
||||
last_checked_at = Some(chrono::Utc::now().timestamp_millis());
|
||||
|
||||
if result.has_active_sessions {
|
||||
// Verificar novas sessoes
|
||||
let prev_sessions: Vec<String> = {
|
||||
last_sessions.lock().iter().map(|s| s.session_id.clone()).collect()
|
||||
};
|
||||
|
||||
// Buscar detalhes das sessoes
|
||||
if let Ok(sessions) = fetch_sessions(&base_clone, &token_clone).await {
|
||||
for session in &sessions {
|
||||
if !prev_sessions.contains(&session.session_id) {
|
||||
// Nova sessao! Emitir evento
|
||||
crate::log_info!(
|
||||
"Nova sessao de chat: ticket={}",
|
||||
session.ticket_id
|
||||
);
|
||||
let _ = app.emit(
|
||||
"raven://chat/session-started",
|
||||
SessionStartedEvent {
|
||||
session: session.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Abrir janela de chat automaticamente
|
||||
if let Err(e) = open_chat_window_internal(
|
||||
&app,
|
||||
&session.ticket_id,
|
||||
) {
|
||||
crate::log_error!(
|
||||
"Falha ao abrir janela de chat: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar cache
|
||||
*last_sessions.lock() = sessions;
|
||||
}
|
||||
|
||||
// Verificar mensagens nao lidas e emitir evento
|
||||
if result.total_unread > 0 {
|
||||
crate::log_info!(
|
||||
"Chat: {} mensagens nao lidas",
|
||||
result.total_unread
|
||||
);
|
||||
let _ = app.emit(
|
||||
"raven://chat/unread-update",
|
||||
serde_json::json!({
|
||||
"totalUnread": result.total_unread,
|
||||
"sessions": result.sessions
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Sem sessoes ativas
|
||||
*last_sessions.lock() = Vec::new();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
crate::log_warn!("Falha no polling de chat: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut guard = self.inner.lock();
|
||||
*guard = Some(ChatPollerHandle {
|
||||
stop_signal,
|
||||
join_handle,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
let mut guard = self.inner.lock();
|
||||
if let Some(handle) = guard.take() {
|
||||
handle.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_sessions(&self) -> Vec<ChatSession> {
|
||||
self.last_sessions.lock().clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WINDOW MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> {
|
||||
let label = format!("chat-{}", ticket_id);
|
||||
|
||||
// Verificar se ja existe
|
||||
if let Some(window) = app.get_webview_window(&label) {
|
||||
window.show().map_err(|e| e.to_string())?;
|
||||
window.set_focus().map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Obter tamanho da tela para posicionar no canto inferior direito
|
||||
let monitors = app.available_monitors().map_err(|e| e.to_string())?;
|
||||
let primary = monitors.into_iter().next();
|
||||
|
||||
let (x, y) = if let Some(monitor) = primary {
|
||||
let size = monitor.size();
|
||||
let scale = monitor.scale_factor();
|
||||
let width = 380.0;
|
||||
let height = 520.0;
|
||||
let margin = 20.0;
|
||||
let taskbar_height = 50.0;
|
||||
(
|
||||
(size.width as f64 / scale) - width - margin,
|
||||
(size.height as f64 / scale) - height - margin - taskbar_height,
|
||||
)
|
||||
} else {
|
||||
(100.0, 100.0)
|
||||
};
|
||||
|
||||
let url_path = format!("/chat?ticketId={}", ticket_id);
|
||||
|
||||
WebviewWindowBuilder::new(
|
||||
app,
|
||||
&label,
|
||||
WebviewUrl::App(url_path.into()),
|
||||
)
|
||||
.title("Chat de Suporte")
|
||||
.inner_size(380.0, 520.0)
|
||||
.min_inner_size(300.0, 400.0)
|
||||
.position(x, y)
|
||||
.decorations(false) // Frameless
|
||||
.always_on_top(true)
|
||||
.skip_taskbar(true)
|
||||
.focused(true)
|
||||
.visible(true)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
crate::log_info!("Janela de chat aberta: {}", label);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn open_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> {
|
||||
open_chat_window_internal(app, ticket_id)
|
||||
}
|
||||
|
||||
pub fn close_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> {
|
||||
let label = format!("chat-{}", ticket_id);
|
||||
if let Some(window) = app.get_webview_window(&label) {
|
||||
window.close().map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn minimize_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> {
|
||||
let label = format!("chat-{}", ticket_id);
|
||||
if let Some(window) = app.get_webview_window(&label) {
|
||||
window.hide().map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
mod agent;
|
||||
mod chat;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod rustdesk;
|
||||
mod usb_control;
|
||||
|
||||
use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile};
|
||||
use chat::{ChatRuntime, ChatSession, ChatMessagesResponse, SendMessageResponse};
|
||||
use chrono::Local;
|
||||
use usb_control::{UsbPolicy, UsbPolicyResult};
|
||||
use tauri::{Emitter, Manager, WindowEvent};
|
||||
|
|
@ -222,10 +224,76 @@ fn refresh_usb_policy() -> Result<(), String> {
|
|||
usb_control::refresh_group_policy().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMANDOS DE CHAT
|
||||
// ============================================================================
|
||||
|
||||
#[tauri::command]
|
||||
fn start_chat_polling(
|
||||
state: tauri::State<ChatRuntime>,
|
||||
app: tauri::AppHandle,
|
||||
base_url: String,
|
||||
token: String,
|
||||
) -> Result<(), String> {
|
||||
state.start_polling(base_url, token, app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_chat_polling(state: tauri::State<ChatRuntime>) -> Result<(), String> {
|
||||
state.stop();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_chat_sessions(state: tauri::State<ChatRuntime>) -> Vec<ChatSession> {
|
||||
state.get_sessions()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn fetch_chat_sessions(base_url: String, token: String) -> Result<Vec<ChatSession>, String> {
|
||||
chat::fetch_sessions(&base_url, &token).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn fetch_chat_messages(
|
||||
base_url: String,
|
||||
token: String,
|
||||
ticket_id: String,
|
||||
since: Option<i64>,
|
||||
) -> Result<ChatMessagesResponse, String> {
|
||||
chat::fetch_messages(&base_url, &token, &ticket_id, since).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn send_chat_message(
|
||||
base_url: String,
|
||||
token: String,
|
||||
ticket_id: String,
|
||||
body: String,
|
||||
) -> Result<SendMessageResponse, String> {
|
||||
chat::send_message(&base_url, &token, &ticket_id, &body).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_chat_window(app: tauri::AppHandle, ticket_id: String) -> Result<(), String> {
|
||||
chat::open_chat_window(&app, &ticket_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn close_chat_window(app: tauri::AppHandle, ticket_id: String) -> Result<(), String> {
|
||||
chat::close_chat_window(&app, &ticket_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn minimize_chat_window(app: tauri::AppHandle, ticket_id: String) -> Result<(), String> {
|
||||
chat::minimize_chat_window(&app, &ticket_id)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.manage(AgentRuntime::new())
|
||||
.manage(ChatRuntime::new())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(StorePluginBuilder::default().build())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
|
|
@ -249,13 +317,14 @@ pub fn run() {
|
|||
setup_raven_autostart();
|
||||
setup_tray(&app.handle())?;
|
||||
|
||||
// Tenta iniciar o agente em background se houver credenciais salvas
|
||||
// Tenta iniciar o agente e chat em background se houver credenciais salvas
|
||||
let app_handle = app.handle().clone();
|
||||
let runtime = app.state::<AgentRuntime>().inner().clone();
|
||||
let agent_runtime = app.state::<AgentRuntime>().inner().clone();
|
||||
let chat_runtime = app.state::<ChatRuntime>().inner().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// Aguarda um pouco para o app estabilizar
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
if let Err(e) = try_start_background_agent(&app_handle, runtime).await {
|
||||
if let Err(e) = try_start_background_agent(&app_handle, agent_runtime, chat_runtime).await {
|
||||
log_warn!("Agente nao iniciado em background: {e}");
|
||||
}
|
||||
});
|
||||
|
|
@ -272,7 +341,17 @@ pub fn run() {
|
|||
ensure_rustdesk_and_emit,
|
||||
apply_usb_policy,
|
||||
get_usb_policy,
|
||||
refresh_usb_policy
|
||||
refresh_usb_policy,
|
||||
// Chat commands
|
||||
start_chat_polling,
|
||||
stop_chat_polling,
|
||||
get_chat_sessions,
|
||||
fetch_chat_sessions,
|
||||
fetch_chat_messages,
|
||||
send_chat_message,
|
||||
open_chat_window,
|
||||
close_chat_window,
|
||||
minimize_chat_window
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
@ -333,9 +412,10 @@ fn setup_raven_autostart() {
|
|||
#[cfg(target_os = "windows")]
|
||||
fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> {
|
||||
let show_item = MenuItemBuilder::with_id("show", "Mostrar").build(app)?;
|
||||
let chat_item = MenuItemBuilder::with_id("chat", "Abrir Chat").build(app)?;
|
||||
let quit_item = MenuItemBuilder::with_id("quit", "Sair").build(app)?;
|
||||
let menu = MenuBuilder::new(app)
|
||||
.items(&[&show_item, &quit_item])
|
||||
.items(&[&show_item, &chat_item, &quit_item])
|
||||
.build()?;
|
||||
|
||||
let mut builder = TrayIconBuilder::new()
|
||||
|
|
@ -348,6 +428,17 @@ fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> {
|
|||
let _ = win.set_focus();
|
||||
}
|
||||
}
|
||||
"chat" => {
|
||||
// Abrir janela de 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() {
|
||||
if let Err(e) = chat::open_chat_window(tray.app_handle(), &session.ticket_id) {
|
||||
log_error!("Falha ao abrir janela de chat: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
tray.app_handle().exit(0);
|
||||
}
|
||||
|
|
@ -376,7 +467,8 @@ fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> {
|
|||
#[cfg(target_os = "windows")]
|
||||
async fn try_start_background_agent(
|
||||
app: &tauri::AppHandle,
|
||||
runtime: AgentRuntime,
|
||||
agent_runtime: AgentRuntime,
|
||||
chat_runtime: ChatRuntime,
|
||||
) -> Result<(), String> {
|
||||
log_info!("Verificando credenciais salvas para iniciar agente...");
|
||||
|
||||
|
|
@ -422,7 +514,7 @@ async fn try_start_background_agent(
|
|||
interval
|
||||
);
|
||||
|
||||
runtime
|
||||
agent_runtime
|
||||
.start_heartbeat(
|
||||
api_base_url.to_string(),
|
||||
token.to_string(),
|
||||
|
|
@ -432,5 +524,17 @@ async fn try_start_background_agent(
|
|||
.map_err(|e| format!("Falha ao iniciar heartbeat: {e}"))?;
|
||||
|
||||
log_info!("Agente iniciado com sucesso em background");
|
||||
|
||||
// Iniciar chat polling
|
||||
if let Err(e) = chat_runtime.start_polling(
|
||||
api_base_url.to_string(),
|
||||
token.to_string(),
|
||||
app.clone(),
|
||||
) {
|
||||
log_warn!("Falha ao iniciar chat polling: {e}");
|
||||
} else {
|
||||
log_info!("Chat polling iniciado com sucesso");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue