diff --git a/crates/daemon/src/chatpersistence.rs b/crates/daemon/src/chatpersistence.rs index 88ece73..961a952 100644 --- a/crates/daemon/src/chatpersistence.rs +++ b/crates/daemon/src/chatpersistence.rs @@ -19,11 +19,11 @@ pub trait ChatRepository { &self, text: &str, is_user: &bool, - chat_id: &i32, + chat_id: &i64, ) -> Result; - async fn get_latest_messages(&self, chat_id: &i32, count: &i32) + async fn get_latest_messages(&self, chat_id: &i64, count: &i64) -> Result>; - async fn get_chat_ids(&self) -> Result>; + async fn get_chat_ids(&self) -> Result>; } pub struct SqliteChatRepository { @@ -62,7 +62,7 @@ impl ChatRepository for SqliteChatRepository { &self, text: &str, is_user: &bool, - chat_id: &i32, + chat_id: &i64, ) -> Result { let result = sqlx::query_as::<_, ChatMessageData>( r#" @@ -82,8 +82,8 @@ impl ChatRepository for SqliteChatRepository { async fn get_latest_messages( &self, - chat_id: &i32, - count: &i32, + chat_id: &i64, + count: &i64, ) -> Result> { // From all chat ids get the latest id. let rows = sqlx::query( @@ -116,15 +116,15 @@ impl ChatRepository for SqliteChatRepository { Ok(messages) } - async fn get_chat_ids(&self) -> Result> { + async fn get_chat_ids(&self) -> Result> { let rows = sqlx::query("SELECT DISTINCT(chat_id) FROM messages ORDER BY chat_id DESC") .fetch_all(&self.pool) .await .inspect_err(|e| println!("sql error: {}", e))?; - let ids: Vec = rows + let ids: Vec = rows .into_iter() .map(|row| { - let i: i32 = row.get(0); + let i: i64 = row.get(0); i }) .collect(); diff --git a/crates/daemon/src/daemongrpc.rs b/crates/daemon/src/daemongrpc.rs index f8f358b..48aae63 100644 --- a/crates/daemon/src/daemongrpc.rs +++ b/crates/daemon/src/daemongrpc.rs @@ -2,7 +2,7 @@ use crate::chatpersistence::{ChatMessageData, ChatRepository}; use anyhow::Result; use genai::chat::{ChatMessage, ChatRequest}; use genai::Client; -use shared::ai::ai_daemon_server::AiDaemon; +use shared::ai::ai_service_server::AiService; use shared::ai::{ ChatHistoryRequest, ChatHistoryResponse, ChatMessage as CMessage, ChatRequest as CRequest, ChatResponse as CResponse, DaemonStatusRequest, DaemonStatusResponse, @@ -25,10 +25,10 @@ impl DaemonServer { } #[tonic::async_trait] -impl AiDaemon for DaemonServer { +impl AiService for DaemonServer { async fn chat(&self, request: Request) -> Result, Status> { let r = request.into_inner(); - let chat_id = get_chat_id(self.repo.clone(), r.chat_id) + let chat_id = id_or_new(self.repo.clone(), r.chat_id) .await .map_err(|e| Status::new(Code::Internal, e.to_string()))?; let mut messages = gather_history(self.repo.clone(), &chat_id) @@ -45,7 +45,7 @@ impl AiDaemon for DaemonServer { let user_message = message_to_dto( &self .repo - .save_message(r.text(), &true, &0) + .save_message(r.text(), &true, &chat_id) .await .map_err(|e| Status::new(Code::Internal, e.to_string()))?, ); @@ -60,12 +60,12 @@ impl AiDaemon for DaemonServer { let ai_message = message_to_dto( &self .repo - .save_message(response_text, &false, &0) + .save_message(response_text, &false, &chat_id) .await .map_err(|e| Status::new(Code::Internal, e.to_string()))?, ); let response = CResponse { - chat_id: 1, + chat_id: ai_message.chat_id, messages: vec![user_message, ai_message], }; return Ok(Response::new(response)); @@ -75,7 +75,7 @@ impl AiDaemon for DaemonServer { &self, request: Request, ) -> Result, Status> { - let chat_id = get_chat_id(self.repo.clone(), request.into_inner().chat_id) + let chat_id = get_latest_chat_id(self.repo.clone(), request.into_inner().chat_id) .await .map_err(|e| Status::new(Code::Internal, e.to_string()))?; let messages = self @@ -85,7 +85,7 @@ impl AiDaemon for DaemonServer { .map_err(|e| Status::new(Code::Internal, e.to_string()))?; let response = ChatHistoryResponse { - chat_id: 1, + chat_id: chat_id, history: messages.iter().map(|m| message_to_dto(m)).collect(), }; Ok(Response::new(response)) @@ -107,6 +107,7 @@ impl AiDaemon for DaemonServer { pub fn message_to_dto(msg: &ChatMessageData) -> CMessage { CMessage { id: msg.id, + chat_id: msg.chat_id, text: msg.text.clone(), is_user: msg.is_user, } @@ -114,7 +115,7 @@ pub fn message_to_dto(msg: &ChatMessageData) -> CMessage { async fn gather_history( repo: Arc, - chat_id: &i32, + chat_id: &i64, ) -> Result> { let messages = repo.get_latest_messages(chat_id, &10).await?; Ok(messages @@ -126,12 +127,22 @@ async fn gather_history( .collect()) } -async fn get_chat_id( +async fn get_latest_chat_id( repo: Arc, chat_id: Option, -) -> Result { +) -> Result { Ok(match chat_id { - Some(i) => i as i32, + Some(i) => i, None => repo.get_chat_ids().await?.get(0).copied().unwrap_or(0), }) } + +async fn id_or_new( + repo: Arc, + chat_id: Option, +) -> Result { + Ok(match chat_id { + Some(i) => i, + None => repo.get_chat_ids().await?.get(0).copied().unwrap_or(0) + 1, + }) +} diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 39fe4c9..acad7cf 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -4,7 +4,7 @@ mod daemongrpc; use std::sync::Arc; use genai::Client; -use shared::ai::ai_daemon_server::AiDaemonServer; +use shared::ai::ai_service_server::AiServiceServer; use tonic::transport::Server; use chatpersistence::SqliteChatRepository; @@ -24,7 +24,7 @@ async fn main() -> Result<(), Box> { .build_v1()?; println!("Started daemon at {}", addr_s); Server::builder() - .add_service(AiDaemonServer::new(daemon)) + .add_service(AiServiceServer::new(daemon)) .add_service(reflection_service) .serve(addr) .await?; diff --git a/crates/feshared/src/lib.rs b/crates/feshared/src/lib.rs index 060ac01..a86c306 100644 --- a/crates/feshared/src/lib.rs +++ b/crates/feshared/src/lib.rs @@ -16,6 +16,7 @@ pub mod chatmessage { pub enum TauriCommand { Chat, + SetChatId, ChatHistory, DaemonState, ToggleDarkMode, @@ -27,6 +28,7 @@ pub mod chatmessage { match self { TauriCommand::TogglePopup => "toggle_popup", TauriCommand::Chat => "chat", + TauriCommand::SetChatId => "set_chat_id", TauriCommand::ChatHistory => "chat_history", TauriCommand::DaemonState => "daemon_state", TauriCommand::ToggleDarkMode => "toggle_dark_mode", diff --git a/crates/shared/proto/api.proto b/crates/shared/proto/api.proto index b4cf51c..1e56a86 100644 --- a/crates/shared/proto/api.proto +++ b/crates/shared/proto/api.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package ai_daemon; -service AiDaemon { +service AiService { rpc Chat(ChatRequest) returns (ChatResponse); rpc ChatHistory(ChatHistoryRequest) returns (ChatHistoryResponse); rpc DaemonStatus(DaemonStatusRequest) returns (DaemonStatusResponse); @@ -9,6 +9,7 @@ service AiDaemon { message ChatMessage { int64 id = 1; + int64 chat_id = 2; string text = 10; bool is_user = 20; } diff --git a/frontend/src-tauri/src/commands.rs b/frontend/src-tauri/src/commands.rs index 14f8712..beb9e65 100644 --- a/frontend/src-tauri/src/commands.rs +++ b/frontend/src-tauri/src/commands.rs @@ -29,19 +29,19 @@ pub fn toggle_popup(app_handle: tauri::AppHandle) { } #[tauri::command] -pub async fn chat( - state: State<'_, AppState>, - prompt: String, - chat_id: Option, -) -> Result, String> { +pub async fn chat(state: State<'_, AppState>, prompt: String) -> Result, String> { let mut client = state.grpc_client.lock().await; + let cid = state.current_chat_id.lock().await.clone(); let request = tonic::Request::new(ChatRequest { - chat_id: chat_id, + chat_id: cid, text: Some(prompt), }); match client.chat(request).await { Ok(response) => { let r = response.into_inner(); + let mut cid = state.current_chat_id.lock().await; + *cid = Some(r.chat_id); + println!("CID={}", r.chat_id); r.messages.iter().for_each(|m| { if m.is_user { println!(">>> {}", m.text) @@ -66,19 +66,30 @@ pub async fn chat( } #[tauri::command] -pub async fn chat_history( +pub async fn set_chat_id( state: State<'_, AppState>, chat_id: Option, -) -> Result { +) -> Result, String> { + let mut cid = state.current_chat_id.lock().await; + *cid = chat_id; + Ok(chat_id) +} + +#[tauri::command] +pub async fn chat_history(state: State<'_, AppState>) -> Result { let mut client = state.grpc_client.lock().await; + let chat_id = state.current_chat_id.lock().await.clone(); let result = client .chat_history(ChatHistoryRequest { chat_id: chat_id }) .await; match result { Ok(response) => { let r = response.into_inner(); + let mut cid = state.current_chat_id.lock().await; + *cid = Some(r.chat_id); + println!("CID={}", r.chat_id); Ok(MessageHistory { - chat_id: None, + chat_id: Some(r.chat_id), history: r .history .iter() diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index 0341f0a..a79b8b3 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -6,34 +6,35 @@ mod commands; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState}; use tokio::sync::Mutex; -use commands::{chat, chat_history, daemon_state, toggle_dark_mode, toggle_popup}; -use shared::ai::ai_daemon_client::AiDaemonClient; +use commands::{chat, chat_history, daemon_state, set_chat_id, toggle_dark_mode, toggle_popup}; +use shared::ai::ai_service_client::AiServiceClient; pub struct AppConfig { dark_mode: bool, } pub struct AppState { - grpc_client: Mutex>, + grpc_client: Mutex>, config: Mutex, - current_chat_id: Mutex, + current_chat_id: Mutex>, } #[tokio::main] async fn main() { let channel = tonic::transport::Channel::from_static("http://[::1]:50051").connect_lazy(); - let client = AiDaemonClient::new(channel); + let client = AiServiceClient::new(channel); tauri::Builder::default() .manage(AppState { grpc_client: Mutex::new(client), config: Mutex::new(AppConfig { dark_mode: true }), - current_chat_id: Mutex::new(-1), + current_chat_id: Mutex::new(None), }) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) .invoke_handler(tauri::generate_handler![ toggle_popup, chat_history, + set_chat_id, chat, daemon_state, toggle_dark_mode, diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 2384b08..5615551 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -9,7 +9,7 @@ use leptos_router::{ use wasm_bindgen::JsValue; use crate::popup::PopupView; -use crate::{bridge::invoke, components::DarkModeToggle}; +use crate::{bridge::invoke_js, components::DarkModeToggle}; use crate::{ bridge::invoke_typed, components::{DaemonProvider, ThemeProvider}, @@ -34,7 +34,7 @@ fn Dashboard() -> impl IntoView { let on_click = move |_ev: leptos::ev::MouseEvent| { spawn_local(async move { let empty_args = serde_wasm_bindgen::to_value(&serde_json::json!({})).unwrap(); - invoke(TauriCommand::TogglePopup.as_str(), empty_args).await; + invoke_js(TauriCommand::TogglePopup, empty_args).await; }); }; view! { diff --git a/frontend/src/bridge.rs b/frontend/src/bridge.rs index 5f901d2..ff3520c 100644 --- a/frontend/src/bridge.rs +++ b/frontend/src/bridge.rs @@ -4,12 +4,16 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] - pub async fn invoke(cmd: &str, args: JsValue) -> JsValue; + async fn invoke(cmd: &str, args: JsValue) -> JsValue; #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"])] pub async fn listen(event: &str, handler: &Closure) -> JsValue; } +pub async fn invoke_js(cmd: TauriCommand, args: JsValue) -> JsValue { + invoke(cmd.as_str(), args).await +} + pub async fn invoke_typed(cmd: TauriCommand, args: JsValue) -> T where T: DeserializeOwned, diff --git a/frontend/src/components.rs b/frontend/src/components.rs index e452938..f73e3d2 100644 --- a/frontend/src/components.rs +++ b/frontend/src/components.rs @@ -5,7 +5,7 @@ use leptos::{component, prelude::*, reactive::spawn_local, view, IntoView}; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsValue; -use crate::bridge::{event_handler, invoke, invoke_typed, listen}; +use crate::bridge::{event_handler, invoke_js, invoke_typed, listen}; #[component] pub fn DaemonProvider(children: ChildrenFn) -> impl IntoView { @@ -88,7 +88,7 @@ pub fn ThemeProvider(children: Children) -> impl IntoView { pub fn DarkModeToggle() -> impl IntoView { let toggle_dark_mode = |_ev: leptos::ev::MouseEvent| { spawn_local(async { - let _ = invoke(TauriCommand::ToggleDarkMode.as_str(), JsValue::UNDEFINED).await; + let _ = invoke_js(TauriCommand::ToggleDarkMode, JsValue::UNDEFINED).await; }); }; view! { @@ -100,8 +100,6 @@ pub fn DarkModeToggle() -> impl IntoView { } } -const DIALOG_BUTTON: &str = "primary-button p-3 m-3"; - #[component] pub fn ConfirmDialog( is_open: ReadSignal, diff --git a/frontend/src/popup.rs b/frontend/src/popup.rs index 4929986..75bf6bc 100644 --- a/frontend/src/popup.rs +++ b/frontend/src/popup.rs @@ -1,5 +1,5 @@ use crate::{ - bridge::{invoke, invoke_typed}, + bridge::{invoke_js, invoke_typed}, components::{ConfirmDialog, DaemonProvider, DarkModeToggle, ThemeProvider}, }; use feshared::{ @@ -30,11 +30,8 @@ pub fn Popup() -> impl IntoView { use_context::>().expect("No daemon connection context!"); let init_history = Action::new_local(|(): &()| async move { - let history: MessageHistory = invoke_typed( - TauriCommand::ChatHistory, - serde_wasm_bindgen::to_value(&serde_json::json!({"chat_id": 1})).unwrap(), - ) - .await; + let history: MessageHistory = + invoke_typed(TauriCommand::ChatHistory, JsValue::UNDEFINED).await; history }); Effect::new(move |prev_status: Option| { @@ -50,6 +47,18 @@ pub fn Popup() -> impl IntoView { } }); + let set_chat_id_action = Action::new_local(|chat_id: &Option| { + let cid = chat_id.clone(); + async move { + let result: Option = invoke_typed( + TauriCommand::SetChatId, + serde_wasm_bindgen::to_value(&serde_json::json!({"chat_id": cid})).unwrap(), + ) + .await; + result + } + }); + // Action that calls the chat action on the daemon let prompt_action = Action::new_local(|prompt: &String| { let prompt = prompt.clone(); @@ -88,17 +97,24 @@ pub fn Popup() -> impl IntoView { let _ = window_event_listener(keydown, move |ev| { if ev.key() == "Escape" { spawn_local(async move { - let _ = invoke(TauriCommand::TogglePopup.as_str(), JsValue::UNDEFINED).await; + let _ = invoke_js(TauriCommand::TogglePopup, JsValue::UNDEFINED).await; }); } }); let (show_new_chat_confirm, set_new_chat_confirm) = signal(false); + let new_chat = move |_: ()| { + set_messages.set(Vec::::new()); + // TODO add callback to the action, and clear chat and close dialog in the callback! + set_chat_id_action.dispatch(None); + set_new_chat_confirm.set(false); + }; + view! { diff --git a/frontend/styles.css b/frontend/styles.css index 298bde3..62a82c5 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -783,10 +783,6 @@ .field-sizing-fixed { field-sizing: fixed; } - .size-3 { - width: calc(var(--spacing) * 3); - height: calc(var(--spacing) * 3); - } .size-3\.5 { width: calc(var(--spacing) * 3.5); height: calc(var(--spacing) * 3.5); @@ -1293,9 +1289,6 @@ .justify-items-stretch { justify-items: stretch; } - .gap-1 { - gap: calc(var(--spacing) * 1); - } .gap-1\.5 { gap: calc(var(--spacing) * 1.5); } @@ -1575,9 +1568,6 @@ .bg-\[\#fbf0df\] { background-color: #fbf0df; } - .bg-blue-300 { - background-color: var(--color-blue-300); - } .bg-green-600 { background-color: var(--color-green-600); } @@ -2834,14 +2824,6 @@ line-height: var(--tw-leading, var(--text-sm--line-height)); } } - .dark\:bg-black\/10 { - &:where(.dark, .dark *) { - background-color: color-mix(in srgb, #000 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 10%, transparent); - } - } - } .dark\:bg-black\/30 { &:where(.dark, .dark *) { background-color: color-mix(in srgb, #000 30%, transparent); @@ -2850,32 +2832,11 @@ } } } - .dark\:bg-gray-800 { - &:where(.dark, .dark *) { - background-color: var(--color-gray-800); - } - } .dark\:bg-slate-800 { &:where(.dark, .dark *) { background-color: var(--color-slate-800); } } - .dark\:bg-white\/10 { - &:where(.dark, .dark *) { - background-color: color-mix(in srgb, #fff 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 10%, transparent); - } - } - } - .dark\:bg-white\/50 { - &:where(.dark, .dark *) { - background-color: color-mix(in srgb, #fff 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 50%, transparent); - } - } - } .dark\:bg-zinc-900 { &:where(.dark, .dark *) { background-color: var(--color-zinc-900);