Add chat functionality between technician and client

- Add ChatMessage to protobuf definitions
- Server relays chat messages between agent and viewer
- Agent chat module shows messages via MessageBox
- Dashboard chat modal with WebSocket connection
- Simplified protobuf encoder/decoder in JavaScript

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 16:31:16 -07:00
parent 0dcbae69a0
commit aa03a87c7c
6 changed files with 694 additions and 9 deletions

172
agent/src/chat/mod.rs Normal file
View File

@@ -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<ChatCommand>,
message_rx: Arc<Mutex<Receiver<ChatMessage>>>,
_handle: thread::JoinHandle<()>,
}
impl ChatController {
/// Create a new chat controller (spawns chat window thread)
#[cfg(windows)]
pub fn new() -> Option<Self> {
let (command_tx, command_rx) = mpsc::channel::<ChatCommand>();
let (message_tx, message_rx) = mpsc::channel::<ChatMessage>();
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<Self> {
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<ChatMessage> {
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<ChatCommand>, message_tx: Sender<ChatMessage>) {
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<u16> = OsStr::new(title)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let message_wide: Vec<u16> = 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<ChatCommand>, _message_tx: Sender<ChatMessage>) {
// No-op on non-Windows
}