From e63fd76d2fad0de144f3ebe33fc9843970428769 Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 9 Feb 2026 20:57:47 +0200 Subject: [PATCH] initial work on message history and data transfer --- crates/daemon/src/main.rs | 43 ++++++++++++++++++----- crates/feshared/src/lib.rs | 2 +- crates/shared/proto/api.proto | 8 +++-- frontend/src-tauri/src/main.rs | 62 +++++++++++++++++++++++++++++----- frontend/src/popup.rs | 52 ++++++++++++++-------------- frontend/styles.css | 31 +++++++++++++++-- 6 files changed, 149 insertions(+), 49 deletions(-) diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 3381f55..20f5755 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -1,18 +1,30 @@ mod chatpersistence; -use genai::chat::{ChatMessage, ChatRequest, ChatResponse}; +use std::cell::Cell; +use std::sync::atomic::AtomicI64; + +use genai::chat::{ChatMessage, ChatRequest}; use genai::Client; use shared::ai::ai_daemon_server::{AiDaemon, AiDaemonServer}; use shared::ai::{ - ChatHistoryRequest, ChatHistoryResponse, ChatRequest as CRequest, ChatResponse as CResponse, - PromptRequest, PromptResponse, + ChatHistoryRequest, ChatHistoryResponse, ChatMessage as CMessage, ChatRequest as CRequest, + ChatResponse as CResponse, PromptRequest, PromptResponse, }; use tonic::{transport::Server, Request, Response, Status}; use chatpersistence::SqliteChatRepository; -#[derive(Default)] -pub struct DaemonServer {} +pub struct DaemonServer { + message_counter: AtomicI64, +} + +impl Default for DaemonServer { + fn default() -> Self { + Self { + message_counter: AtomicI64::new(0), + } + } +} #[tonic::async_trait] impl AiDaemon for DaemonServer { @@ -33,11 +45,26 @@ impl AiDaemon for DaemonServer { } async fn chat(&self, request: Request) -> Result, Status> { + let r = request.into_inner(); + println!("<<<: {}", r.text()); let response = CResponse { - id: 1, chat_id: 1, - text: "asdf".to_string(), - is_user: false, + messages: vec![ + CMessage { + id: self + .message_counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed), + text: r.text().to_string(), + is_user: true, + }, + CMessage { + id: self + .message_counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed), + text: format!("Pong: {}", r.text()), + is_user: false, + }, + ], }; return Ok(Response::new(response)); } diff --git a/crates/feshared/src/lib.rs b/crates/feshared/src/lib.rs index ab15901..853602f 100644 --- a/crates/feshared/src/lib.rs +++ b/crates/feshared/src/lib.rs @@ -10,7 +10,7 @@ pub mod chatmessage { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct MessageHistory { - pub chat_id: i64, + pub chat_id: Option, pub history: Vec, } } diff --git a/crates/shared/proto/api.proto b/crates/shared/proto/api.proto index f62e766..00a1c03 100644 --- a/crates/shared/proto/api.proto +++ b/crates/shared/proto/api.proto @@ -7,13 +7,17 @@ service AiDaemon { rpc ChatHistory(ChatHistoryRequest) returns (ChatHistoryResponse); } -message ChatResponse { +message ChatMessage { int64 id = 1; - int64 chat_id = 2; string text = 10; bool is_user = 20; } +message ChatResponse { + int64 chat_id = 2; + repeated ChatMessage messages = 10; +} + message ChatRequest { optional int64 chat_id = 1; optional string text = 10; diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index edd9d45..c67ee75 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -2,13 +2,14 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use feshared::chatmessage::{Message, MessageHistory}; -use shared::ai::{ai_daemon_client::AiDaemonClient, PromptRequest}; +use shared::ai::{ai_daemon_client::AiDaemonClient, ChatRequest, PromptRequest}; use tauri::{Emitter, Manager, State}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState}; use tokio::sync::Mutex; struct AppState { grpc_client: Mutex>, + current_chat: Mutex>, } #[tauri::command] @@ -41,6 +42,40 @@ async fn prompt_llm(state: State<'_, AppState>, prompt: String) -> Result, + prompt: String, + chat_id: Option, +) -> Result, String> { + let mut client = state.grpc_client.lock().await; + let request = tonic::Request::new(ChatRequest { + chat_id: chat_id, + text: Some(prompt), + }); + match client.chat(request).await { + Ok(response) => { + let r = response.into_inner(); + r.messages.iter().for_each(|m| { + if m.is_user { + println!(">>> {}", m.text) + } else { + println!("<<< {}", m.text) + } + }); + Ok(r.messages + .iter() + .map(|msg| Message { + id: msg.id, + text: msg.text.clone(), + is_user: msg.is_user, + }) + .collect()) + } + Err(e) => Err(format!("gRPC error: {}", e)), + } +} + #[tauri::command] async fn chat_history( state: State<'_, AppState>, @@ -48,14 +83,21 @@ async fn chat_history( ) -> Result { let history = MessageHistory { chat_id: match chat_id { - Some(id) => id, - None => -1, + Some(_) => chat_id, + None => Some(-1), }, - history: vec![Message { - id: 1, - text: String::from("asd"), - is_user: false, - }], + history: vec![ + Message { + id: 1, + text: String::from("asd"), + is_user: false, + }, + Message { + id: 2, + text: String::from("yeah!!!!"), + is_user: true, + }, + ], }; Ok(history) } @@ -72,12 +114,14 @@ async fn main() { tauri::Builder::default() .manage(AppState { grpc_client: Mutex::new(client), + current_chat: Mutex::new(None), }) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) .invoke_handler(tauri::generate_handler![ toggle_popup, prompt_llm, - chat_history + chat_history, + chat, ]) .setup(|app| { /* Auto-hide popup when focus is lost diff --git a/frontend/src/popup.rs b/frontend/src/popup.rs index a90c8c5..10717b1 100644 --- a/frontend/src/popup.rs +++ b/frontend/src/popup.rs @@ -10,29 +10,42 @@ pub fn Popup() -> impl IntoView { let prompt_input_ref = NodeRef::::new(); let (prompt_text, set_prompt_text) = signal(String::new()); let (messages, set_messages) = signal(Vec::::new()); - // Action that calls the promp daemon + + let init_history = Action::new_local(|(): &()| async move { + let response = invoke( + "chat_history", + serde_wasm_bindgen::to_value(&serde_json::json!({"chat_id": 1})).unwrap(), + ) + .await; + let history: MessageHistory = serde_wasm_bindgen::from_value(response).unwrap(); + history + }); + Effect::new(move |_| { + init_history.dispatch(()); + }); + Effect::new(move |_| { + if let Some(mut dat) = init_history.value().get() { + set_messages.update(|m| m.append(&mut dat.history)); + } + }); + + // Action that calls the chat action on the daemon let prompt_action = Action::new_local(|prompt: &String| { let prompt = prompt.clone(); async move { let response = invoke( - "prompt_llm", + "chat", serde_wasm_bindgen::to_value(&serde_json::json!({"prompt": prompt})).unwrap(), ) .await; - let result: String = serde_wasm_bindgen::from_value(response).unwrap(); + let result: Vec = serde_wasm_bindgen::from_value(response).unwrap(); result } }); // Update the model response div with the prompt result Effect::new(move |_| { - if let Some(result) = prompt_action.value().get() { - set_messages.update(|previous| { - previous.push(Message { - id: previous.len() as i64, - text: result, - is_user: false, - }); - }); + if let Some(mut result) = prompt_action.value().get() { + set_messages.update(|m| m.append(&mut result)); } }); // Clear the propt text-input when the window loses focus (and is hidden) @@ -60,30 +73,17 @@ pub fn Popup() -> impl IntoView { } }); - spawn_local(async move { - let response = invoke("chat_history", JsValue::bigint_from_str("1")).await; - let history: MessageHistory = serde_wasm_bindgen::from_value(response).unwrap(); - set_messages.set(history.history.clone()); - }); - view! {
-

"AI quick action"