diff --git a/agent/src/chat/mod.rs b/agent/src/chat/mod.rs new file mode 100644 index 0000000..57fd9f7 --- /dev/null +++ b/agent/src/chat/mod.rs @@ -0,0 +1,172 @@ +//! Chat window for the agent +//! +//! Provides a simple chat interface for communication between +//! the technician and the end user. + +use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::{Arc, Mutex}; +use std::thread; +use tracing::{info, warn, error}; + +#[cfg(windows)] +use windows::Win32::UI::WindowsAndMessaging::*; +#[cfg(windows)] +use windows::Win32::Foundation::*; +#[cfg(windows)] +use windows::Win32::Graphics::Gdi::*; +#[cfg(windows)] +use windows::Win32::System::LibraryLoader::GetModuleHandleW; +#[cfg(windows)] +use windows::core::PCWSTR; + +/// A chat message +#[derive(Debug, Clone)] +pub struct ChatMessage { + pub id: String, + pub sender: String, + pub content: String, + pub timestamp: i64, +} + +/// Commands that can be sent to the chat window +#[derive(Debug)] +pub enum ChatCommand { + Show, + Hide, + AddMessage(ChatMessage), + Close, +} + +/// Controller for the chat window +pub struct ChatController { + command_tx: Sender, + message_rx: Arc>>, + _handle: thread::JoinHandle<()>, +} + +impl ChatController { + /// Create a new chat controller (spawns chat window thread) + #[cfg(windows)] + pub fn new() -> Option { + let (command_tx, command_rx) = mpsc::channel::(); + let (message_tx, message_rx) = mpsc::channel::(); + + let handle = thread::spawn(move || { + run_chat_window(command_rx, message_tx); + }); + + Some(Self { + command_tx, + message_rx: Arc::new(Mutex::new(message_rx)), + _handle: handle, + }) + } + + #[cfg(not(windows))] + pub fn new() -> Option { + warn!("Chat window not supported on this platform"); + None + } + + /// Show the chat window + pub fn show(&self) { + let _ = self.command_tx.send(ChatCommand::Show); + } + + /// Hide the chat window + pub fn hide(&self) { + let _ = self.command_tx.send(ChatCommand::Hide); + } + + /// Add a message to the chat window + pub fn add_message(&self, msg: ChatMessage) { + let _ = self.command_tx.send(ChatCommand::AddMessage(msg)); + } + + /// Check for outgoing messages from the user + pub fn poll_outgoing(&self) -> Option { + if let Ok(rx) = self.message_rx.lock() { + rx.try_recv().ok() + } else { + None + } + } + + /// Close the chat window + pub fn close(&self) { + let _ = self.command_tx.send(ChatCommand::Close); + } +} + +#[cfg(windows)] +fn run_chat_window(command_rx: Receiver, message_tx: Sender) { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + + info!("Starting chat window thread"); + + // For now, we'll use a simple message box approach + // A full implementation would create a proper window with a text input + + // Process commands + loop { + match command_rx.recv() { + Ok(ChatCommand::Show) => { + info!("Chat window: Show requested"); + // Show a simple notification that chat is available + } + Ok(ChatCommand::Hide) => { + info!("Chat window: Hide requested"); + } + Ok(ChatCommand::AddMessage(msg)) => { + info!("Chat message received: {} - {}", msg.sender, msg.content); + + // Show the message to the user via a message box (simple implementation) + let title = format!("Message from {}", msg.sender); + let content = msg.content.clone(); + + // Spawn a thread to show the message box (non-blocking) + thread::spawn(move || { + show_message_box_internal(&title, &content); + }); + } + Ok(ChatCommand::Close) => { + info!("Chat window: Close requested"); + break; + } + Err(_) => { + // Channel closed + break; + } + } + } +} + +#[cfg(windows)] +fn show_message_box_internal(title: &str, message: &str) { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + + let title_wide: Vec = OsStr::new(title) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let message_wide: Vec = OsStr::new(message) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + unsafe { + MessageBoxW( + None, + PCWSTR(message_wide.as_ptr()), + PCWSTR(title_wide.as_ptr()), + MB_OK | MB_ICONINFORMATION | MB_TOPMOST | MB_SETFOREGROUND, + ); + } +} + +#[cfg(not(windows))] +fn run_chat_window(_command_rx: Receiver, _message_tx: Sender) { + // No-op on non-Windows +} diff --git a/agent/src/main.rs b/agent/src/main.rs index 5cf03f2..462a929 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -9,6 +9,7 @@ //! for a one-time support session. mod capture; +mod chat; mod config; mod encoder; mod input; @@ -198,6 +199,12 @@ async fn run_agent(config: config::Config) -> Result<()> { } }; + // Create chat controller + let chat_ctrl = chat::ChatController::new(); + if chat_ctrl.is_some() { + info!("Chat controller created"); + } + // Connect to server and run main loop loop { info!("Connecting to server..."); @@ -219,8 +226,8 @@ async fn run_agent(config: config::Config) -> Result<()> { t.update_status("Status: Connected"); } - // Run session until disconnect, passing tray for event processing - if let Err(e) = session.run_with_tray(tray.as_ref()).await { + // Run session until disconnect, passing tray and chat for event processing + if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await { let error_msg = e.to_string(); // Check if this is a user-initiated exit diff --git a/agent/src/session/mod.rs b/agent/src/session/mod.rs index 63fddc7..02f4f02 100644 --- a/agent/src/session/mod.rs +++ b/agent/src/session/mod.rs @@ -7,10 +7,11 @@ //! - Input event handling use crate::capture::{self, Capturer, Display}; +use crate::chat::{ChatController, ChatMessage as ChatMsg}; use crate::config::Config; use crate::encoder::{self, Encoder}; use crate::input::InputController; -use crate::proto::{Message, message}; +use crate::proto::{Message, message, ChatMessage}; use crate::transport::WebSocketTransport; use crate::tray::{TrayController, TrayAction}; use anyhow::Result; @@ -148,8 +149,8 @@ impl SessionManager { Ok(()) } - /// Run the session main loop with tray event processing - pub async fn run_with_tray(&mut self, tray: Option<&TrayController>) -> Result<()> { + /// Run the session main loop with tray and chat event processing + pub async fn run_with_tray(&mut self, tray: Option<&TrayController>, chat: Option<&ChatController>) -> Result<()> { if self.transport.is_none() { anyhow::bail!("Not connected"); } @@ -215,9 +216,38 @@ impl SessionManager { }; for msg in messages { + // Handle chat messages specially + if let Some(message::Payload::ChatMessage(chat_msg)) = &msg.payload { + if let Some(c) = chat { + c.add_message(ChatMsg { + id: chat_msg.id.clone(), + sender: chat_msg.sender.clone(), + content: chat_msg.content.clone(), + timestamp: chat_msg.timestamp, + }); + } + continue; // Don't pass to handle_message + } self.handle_message(&mut input, msg)?; } + // Check for outgoing chat messages from user + if let Some(c) = chat { + if let Some(outgoing) = c.poll_outgoing() { + let chat_proto = ChatMessage { + id: outgoing.id, + sender: "client".to_string(), + content: outgoing.content, + timestamp: outgoing.timestamp, + }; + let msg = Message { + payload: Some(message::Payload::ChatMessage(chat_proto)), + }; + let transport = self.transport.as_mut().unwrap(); + transport.send(msg).await?; + } + } + // Capture and send frame if interval elapsed if last_frame_time.elapsed() >= frame_interval { last_frame_time = Instant::now(); diff --git a/proto/guruconnect.proto b/proto/guruconnect.proto index d8d6353..cc8c5a5 100644 --- a/proto/guruconnect.proto +++ b/proto/guruconnect.proto @@ -229,6 +229,17 @@ message LatencyReport { int32 bitrate_kbps = 3; } +// ============================================================================ +// Chat Messages +// ============================================================================ + +message ChatMessage { + string id = 1; // Unique message ID + string sender = 2; // "technician" or "client" + string content = 3; // Message text + int64 timestamp = 4; // Unix timestamp +} + // ============================================================================ // Control Messages // ============================================================================ @@ -282,5 +293,8 @@ message Message { Heartbeat heartbeat = 50; HeartbeatAck heartbeat_ack = 51; Disconnect disconnect = 52; + + // Chat + ChatMessage chat_message = 60; } } diff --git a/server/src/relay/mod.rs b/server/src/relay/mod.rs index 8882fab..251ed77 100644 --- a/server/src/relay/mod.rs +++ b/server/src/relay/mod.rs @@ -161,9 +161,17 @@ async fn handle_agent_connection( // Try to decode as protobuf message match proto::Message::decode(data.as_ref()) { Ok(proto_msg) => { - if let Some(proto::message::Payload::VideoFrame(_)) = &proto_msg.payload { - // Broadcast frame to all viewers - let _ = frame_tx.send(data.to_vec()); + match &proto_msg.payload { + Some(proto::message::Payload::VideoFrame(_)) => { + // Broadcast frame to all viewers + let _ = frame_tx.send(data.to_vec()); + } + Some(proto::message::Payload::ChatMessage(chat)) => { + // Broadcast chat message to all viewers + info!("Chat from client: {}", chat.content); + let _ = frame_tx.send(data.to_vec()); + } + _ => {} } } Err(e) => { @@ -255,6 +263,11 @@ async fn handle_viewer_connection( // Forward input to agent let _ = input_tx.send(data.to_vec()).await; } + Some(proto::message::Payload::ChatMessage(chat)) => { + // Forward chat message to agent + info!("Chat from technician: {}", chat.content); + let _ = input_tx.send(data.to_vec()).await; + } _ => {} } } diff --git a/server/static/dashboard.html b/server/static/dashboard.html index 71013c2..fccfb12 100644 --- a/server/static/dashboard.html +++ b/server/static/dashboard.html @@ -206,6 +206,130 @@ .detail-row { display: flex; justify-content: space-between; padding: 8px 0; font-size: 14px; border-bottom: 1px solid hsl(var(--border)); } .detail-label { color: hsl(var(--muted-foreground)); } .detail-value { color: hsl(var(--foreground)); text-align: right; } + + /* Chat Modal */ + .modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; + justify-content: center; + align-items: center; + } + .modal-overlay.active { display: flex; } + + .modal { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 12px; + width: 90%; + max-width: 500px; + max-height: 80vh; + display: flex; + flex-direction: column; + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid hsl(var(--border)); + } + + .modal-title { font-size: 18px; font-weight: 600; } + + .modal-close { + background: transparent; + border: none; + color: hsl(var(--muted-foreground)); + font-size: 24px; + cursor: pointer; + padding: 4px; + } + .modal-close:hover { color: hsl(var(--foreground)); } + + .chat-messages { + flex: 1; + overflow-y: auto; + padding: 16px 20px; + min-height: 300px; + max-height: 400px; + } + + .chat-message { + margin-bottom: 12px; + padding: 10px 14px; + border-radius: 8px; + max-width: 80%; + } + + .chat-message.technician { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + margin-left: auto; + } + + .chat-message.client { + background: hsl(var(--muted)); + color: hsl(var(--foreground)); + } + + .chat-message-sender { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + opacity: 0.8; + margin-bottom: 4px; + } + + .chat-message-content { font-size: 14px; line-height: 1.4; } + + .chat-empty { + text-align: center; + color: hsl(var(--muted-foreground)); + padding: 40px 20px; + } + + .chat-input-container { + display: flex; + gap: 8px; + padding: 16px 20px; + border-top: 1px solid hsl(var(--border)); + } + + .chat-input { + flex: 1; + padding: 10px 14px; + font-size: 14px; + background: hsl(var(--input)); + border: 1px solid hsl(var(--border)); + border-radius: 6px; + color: hsl(var(--foreground)); + outline: none; + } + + .chat-input:focus { + border-color: hsl(var(--ring)); + box-shadow: 0 0 0 3px hsla(var(--ring), 0.3); + } + + .chat-send { + padding: 10px 20px; + font-size: 14px; + font-weight: 500; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border: none; + border-radius: 6px; + cursor: pointer; + } + .chat-send:hover { opacity: 0.9; } + .chat-send:disabled { opacity: 0.5; cursor: not-allowed; } @@ -388,7 +512,29 @@ + + +