release/windows-microsoft-store
rs 133 lines 4.31 KB
Raw
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 use super::*;
8 use tauri::AppHandle;
9
10 #[cfg(any(
11 target_os = "macos",
12 target_os = "ios",
13 target_os = "android",
14 target_os = "windows"
15 ))]
16 use {log::info, tauri::Emitter};
17
18 #[cfg(not(any(
19 target_os = "macos",
20 target_os = "ios",
21 target_os = "android",
22 target_os = "windows"
23 )))]
24 use log::debug;
25
26 /// Send a user message to Siti and receive an assistant reply.
27 ///
28 /// **Fire-and-return-immediately design**: this command returns `Ok(())`
29 /// as soon as the inference task is spawned. The actual reply (or error)
30 /// is delivered to the frontend via the `chat_reply` Tauri event
31 /// (`EVENT_CHAT_REPLY`).
32 ///
33 /// ## Why events instead of a blocking return?
34 ///
35 /// On Android, the Tauri IPC bridge stores the JS-side `invoke()` resolve
36 /// callback in a V8 `Map`. Android's WebView GC can collect that entry
37 /// while the command is still running, especially on budget SoCs where
38 /// GGUF inference takes 60-300 seconds. Emitting an event from a spawned
39 /// task sidesteps the GC entirely.
40 #[cfg(any(
41 target_os = "macos",
42 target_os = "ios",
43 target_os = "android",
44 target_os = "windows"
45 ))]
46 #[tauri::command]
47 pub async fn chat_send_message(app: AppHandle, message: String) -> Result<(), String> {
48 // ── 1. Lazy-load the model if not already cached ──────────────────────
49 if !ENGINE.is_loaded().await {
50 info!("No chat model loaded; lazy-loading default model.");
51
52 let config = resolved_model_config();
53 let display_name = config.display_name();
54
55 emit_chat_status(&app, ChatStatus::Loading, Some(&display_name), None);
56
57 config
58 .load(
59 Some(CHAT_SYSTEM_PROMPT.to_string()),
60 Some(sampling_config()),
61 )
62 .await
63 .map_err(|e| {
64 let msg = format!("Failed to lazy-load chat model: {}", e);
65 error!("{}", msg);
66 emit_chat_status(&app, ChatStatus::Error, Some(&display_name), Some(&msg));
67 msg
68 })?;
69
70 emit_chat_status(&app, ChatStatus::Ready, Some(&display_name), None);
71 }
72
73 // ── 2. Spawn inference task: this command returns Ok(()) immediately ──
74 let history_len = ENGINE.history().await.len();
75 info!(
76 "Chat inference DISPATCH history_turns={} message=\"{}\"",
77 history_len, message
78 );
79
80 tokio::task::spawn(async move {
81 info!("Chat inference START");
82
83 let result = ENGINE.send_message(&message).await;
84
85 let payload = match result {
86 Ok(inference_result) => {
87 info!(
88 "Chat inference END ({}) reply: \"{}\"",
89 inference_result.duration_display,
90 if inference_result.text.len() > 100 {
91 format!("{}...", &inference_result.text[..100])
92 } else {
93 inference_result.text.clone()
94 }
95 );
96
97 ChatReplyPayload {
98 reply: Some(inference_result.text),
99 duration: Some(inference_result.duration_display),
100 error: None,
101 }
102 }
103 Err(e) => {
104 let msg = format!("Chat inference FAILED: {}", e);
105 error!("{}", msg);
106 emit_chat_status(&app, ChatStatus::Error, None, Some(&msg));
107 ChatReplyPayload {
108 reply: None,
109 duration: None,
110 error: Some(msg),
111 }
112 }
113 };
114
115 if let Err(e) = app.emit(EVENT_CHAT_REPLY, &payload) {
116 error!("Failed to emit chat_reply event: {:?}", e);
117 }
118 });
119
120 Ok(())
121 }
122
123 #[cfg(not(any(
124 target_os = "macos",
125 target_os = "ios",
126 target_os = "android",
127 target_os = "windows"
128 )))]
129 #[tauri::command]
130 pub async fn chat_send_message(_app: AppHandle, _message: String) -> Result<(), String> {
131 debug!("Siti chat is not supported on this platform.");
132 Err("Siti chat is not supported on this platform.".to_string())
133 }