| 1 | //! Tauri command: `chat_send_message` |
| 2 | //! |
| 3 | //! Sends a user message to the on-device LLM and returns `Ok(())` |
| 4 | //! immediately. The actual reply (or error) is delivered to the frontend |
| 5 | //! via the `chat_reply` Tauri event emitted from a spawned async task. |
| 6 | |
| 7 | #[cfg(any( |
| 8 | target_os = "macos", |
| 9 | target_os = "ios", |
| 10 | target_os = "android", |
| 11 | target_os = "windows" |
| 12 | ))] |
| 13 | use { |
| 14 | super::{resolved_model_config, sampling_config, CHAT_SYSTEM_PROMPT}, |
| 15 | ed_agent_tauri::EdState, |
| 16 | log::{error, info}, |
| 17 | std::sync::atomic::{AtomicU64, Ordering}, |
| 18 | tauri::{AppHandle, State}, |
| 19 | }; |
| 20 | |
| 21 | #[cfg(not(any( |
| 22 | target_os = "macos", |
| 23 | target_os = "ios", |
| 24 | target_os = "android", |
| 25 | target_os = "windows" |
| 26 | )))] |
| 27 | use log::debug; |
| 28 | |
| 29 | /// Distinguishes turns in `chat_text_delta` and `chat_reply` payloads. |
| 30 | #[cfg(any( |
| 31 | target_os = "macos", |
| 32 | target_os = "ios", |
| 33 | target_os = "android", |
| 34 | target_os = "windows" |
| 35 | ))] |
| 36 | static NEXT_TURN: AtomicU64 = AtomicU64::new(1); |
| 37 | |
| 38 | /// Send a user message to Siti and receive an assistant reply. |
| 39 | /// |
| 40 | /// **Fire-and-return-immediately design**: this command returns `Ok(())` |
| 41 | /// as soon as the inference task is spawned. The actual reply (or error) |
| 42 | /// is delivered to the frontend via the `chat_reply` Tauri event |
| 43 | /// (`EVENT_CHAT_REPLY`). |
| 44 | /// |
| 45 | /// ## Why events instead of a blocking return? |
| 46 | /// |
| 47 | /// On Android, the Tauri IPC bridge stores the JS-side `invoke()` resolve |
| 48 | /// callback in a V8 `Map`. Android's WebView GC can collect that entry |
| 49 | /// while the command is still running, especially on budget SoCs where |
| 50 | /// GGUF inference takes 60-300 seconds. Emitting an event from a spawned |
| 51 | /// task sidesteps the GC entirely. |
| 52 | /// |
| 53 | /// While the model generates, the reply also arrives piece by piece as |
| 54 | /// `chat_text_delta` events; `chat_reply` then carries the whole text. |
| 55 | #[cfg(any( |
| 56 | target_os = "macos", |
| 57 | target_os = "ios", |
| 58 | target_os = "android", |
| 59 | target_os = "windows" |
| 60 | ))] |
| 61 | #[tauri::command] |
| 62 | pub async fn chat_send_message( |
| 63 | app: AppHandle, |
| 64 | state: State<'_, EdState>, |
| 65 | message: String, |
| 66 | ) -> Result<(), String> { |
| 67 | // ── 1. Lazy-load the model if not already cached ────────────────────── |
| 68 | // Ed reports `Loading`, then `Ready` or `Error`. |
| 69 | if !state.ed().is_loaded().await { |
| 70 | info!("No chat model loaded; lazy-loading default model."); |
| 71 | |
| 72 | resolved_model_config() |
| 73 | .load( |
| 74 | state.ed(), |
| 75 | Some(CHAT_SYSTEM_PROMPT.to_string()), |
| 76 | Some(sampling_config()), |
| 77 | ) |
| 78 | .await |
| 79 | .map_err(|e| { |
| 80 | let msg = format!("Failed to lazy-load chat model: {}", e); |
| 81 | error!("{}", msg); |
| 82 | msg |
| 83 | })?; |
| 84 | } |
| 85 | |
| 86 | // ── 2. Run the turn on a spawned task; this command returns at once ─── |
| 87 | // `submit` emits `chat_text_delta` while generating and `chat_reply` at |
| 88 | // the end, with `{id, session, reply, duration, error}`. |
| 89 | info!( |
| 90 | "Chat inference DISPATCH history_turns={} message_chars={}", |
| 91 | state.ed().history().await.len(), |
| 92 | message.chars().count() |
| 93 | ); |
| 94 | let id = NEXT_TURN.fetch_add(1, Ordering::Relaxed).to_string(); |
| 95 | state.submit(app, id, message); |
| 96 | |
| 97 | Ok(()) |
| 98 | } |
| 99 | |
| 100 | #[cfg(not(any( |
| 101 | target_os = "macos", |
| 102 | target_os = "ios", |
| 103 | target_os = "android", |
| 104 | target_os = "windows" |
| 105 | )))] |
| 106 | #[tauri::command] |
| 107 | pub async fn chat_send_message(_message: String) -> Result<(), String> { |
| 108 | debug!("Siti chat is not supported on this platform."); |
| 109 | Err("Siti chat is not supported on this platform.".to_string()) |
| 110 | } |