feat: store chat_id to allow multiple chats, use enums in tauri
communication
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS messages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
text TEXT NOT NULL,
|
|
||||||
is_user BOOL NOT NULL
|
|
||||||
);
|
|
||||||
12
crates/daemon/migrations/20260222_1_init.sql
Normal file
12
crates/daemon/migrations/20260222_1_init.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chat_id INTEGER NOT NULL,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
is_user BOOL NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(id, chat_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_message_timestamp ON messages(timestamp);
|
||||||
|
CREATE INDEX idx_message_chat_id ON messages(chat_id);
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use sqlx::sqlite::SqliteConnectOptions;
|
use sqlx::sqlite::SqliteConnectOptions;
|
||||||
use sqlx::Row;
|
use sqlx::{Row, SqlitePool};
|
||||||
use sqlx::SqlitePool;
|
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tonic::async_trait;
|
use tonic::async_trait;
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
pub struct ChatMessageData {
|
pub struct ChatMessageData {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
pub chat_id: i64,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub is_user: bool,
|
pub is_user: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ChatRepository {
|
pub trait ChatRepository {
|
||||||
async fn save_message(&self, text: &str, is_user: &bool) -> Result<ChatMessageData>;
|
async fn save_message(
|
||||||
async fn get_latest_messages(&self) -> Result<Vec<ChatMessageData>>;
|
&self,
|
||||||
|
text: &str,
|
||||||
|
is_user: &bool,
|
||||||
|
chat_id: &i32,
|
||||||
|
) -> Result<ChatMessageData>;
|
||||||
|
async fn get_latest_messages(&self, chat_id: &i32, count: &i32)
|
||||||
|
-> Result<Vec<ChatMessageData>>;
|
||||||
|
async fn get_chat_ids(&self) -> Result<Box<[i32]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SqliteChatRepository {
|
pub struct SqliteChatRepository {
|
||||||
@@ -51,31 +58,46 @@ impl SqliteChatRepository {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ChatRepository for SqliteChatRepository {
|
impl ChatRepository for SqliteChatRepository {
|
||||||
async fn save_message(&self, text: &str, is_user: &bool) -> Result<ChatMessageData> {
|
async fn save_message(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
is_user: &bool,
|
||||||
|
chat_id: &i32,
|
||||||
|
) -> Result<ChatMessageData> {
|
||||||
let result = sqlx::query_as::<_, ChatMessageData>(
|
let result = sqlx::query_as::<_, ChatMessageData>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO messages (text, is_user)
|
INSERT INTO messages (text, is_user, chat_id)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?, ?)
|
||||||
RETURNING id, text, is_user
|
RETURNING id, chat_id, text, is_user
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(text)
|
.bind(text)
|
||||||
.bind(is_user)
|
.bind(is_user)
|
||||||
|
.bind(chat_id)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| println!("sql error: {}", e))?;
|
.inspect_err(|e| println!("sql error: {}", e))?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_latest_messages(&self) -> Result<Vec<ChatMessageData>> {
|
async fn get_latest_messages(
|
||||||
|
&self,
|
||||||
|
chat_id: &i32,
|
||||||
|
count: &i32,
|
||||||
|
) -> Result<Vec<ChatMessageData>> {
|
||||||
|
// From all chat ids get the latest id.
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
format!(
|
||||||
SELECT * FROM (
|
r#"
|
||||||
SELECT id, text, is_user
|
SELECT * FROM (
|
||||||
FROM messages
|
SELECT id, chat_id, text, is_user
|
||||||
ORDER BY id DESC
|
FROM messages
|
||||||
LIMIT 10
|
WHERE chat_id = {chat_id}
|
||||||
) AS subquery ORDER BY id ASC"#,
|
ORDER BY id DESC
|
||||||
|
LIMIT {count}
|
||||||
|
) AS subquery ORDER BY id ASC;"#
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
@@ -85,11 +107,27 @@ impl ChatRepository for SqliteChatRepository {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| ChatMessageData {
|
.map(|row| ChatMessageData {
|
||||||
id: row.get(0),
|
id: row.get(0),
|
||||||
text: row.get(1),
|
chat_id: row.get(1),
|
||||||
is_user: row.get(2),
|
text: row.get(2),
|
||||||
|
is_user: row.get(3),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(messages)
|
Ok(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_chat_ids(&self) -> Result<Box<[i32]>> {
|
||||||
|
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<i32> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
let i: i32 = row.get(0);
|
||||||
|
i
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(ids.into_boxed_slice())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ impl DaemonServer {
|
|||||||
impl AiDaemon for DaemonServer {
|
impl AiDaemon for DaemonServer {
|
||||||
async fn chat(&self, request: Request<CRequest>) -> Result<Response<CResponse>, Status> {
|
async fn chat(&self, request: Request<CRequest>) -> Result<Response<CResponse>, Status> {
|
||||||
let r = request.into_inner();
|
let r = request.into_inner();
|
||||||
let mut messages = gather_history(self.repo.clone())
|
let chat_id = get_chat_id(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)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;
|
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;
|
||||||
messages.push(ChatMessage::user(r.text()));
|
messages.push(ChatMessage::user(r.text()));
|
||||||
@@ -42,7 +45,7 @@ impl AiDaemon for DaemonServer {
|
|||||||
let user_message = message_to_dto(
|
let user_message = message_to_dto(
|
||||||
&self
|
&self
|
||||||
.repo
|
.repo
|
||||||
.save_message(r.text(), &true)
|
.save_message(r.text(), &true, &0)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::new(Code::Internal, e.to_string()))?,
|
.map_err(|e| Status::new(Code::Internal, e.to_string()))?,
|
||||||
);
|
);
|
||||||
@@ -52,12 +55,12 @@ impl AiDaemon for DaemonServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
println!("User: {}", r.text());
|
println!("User: {}", r.text());
|
||||||
println!("AI: {}", response_text.clone());
|
println!("AI: {}", response_text);
|
||||||
|
|
||||||
let ai_message = message_to_dto(
|
let ai_message = message_to_dto(
|
||||||
&self
|
&self
|
||||||
.repo
|
.repo
|
||||||
.save_message(response_text, &false)
|
.save_message(response_text, &false, &0)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::new(Code::Internal, e.to_string()))?,
|
.map_err(|e| Status::new(Code::Internal, e.to_string()))?,
|
||||||
);
|
);
|
||||||
@@ -70,11 +73,14 @@ impl AiDaemon for DaemonServer {
|
|||||||
|
|
||||||
async fn chat_history(
|
async fn chat_history(
|
||||||
&self,
|
&self,
|
||||||
_: Request<ChatHistoryRequest>,
|
request: Request<ChatHistoryRequest>,
|
||||||
) -> Result<Response<ChatHistoryResponse>, Status> {
|
) -> Result<Response<ChatHistoryResponse>, Status> {
|
||||||
|
let chat_id = get_chat_id(self.repo.clone(), request.into_inner().chat_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;
|
||||||
let messages = self
|
let messages = self
|
||||||
.repo
|
.repo
|
||||||
.get_latest_messages()
|
.get_latest_messages(&chat_id, &20)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;
|
.map_err(|e| Status::new(Code::Internal, e.to_string()))?;
|
||||||
|
|
||||||
@@ -106,8 +112,11 @@ pub fn message_to_dto(msg: &ChatMessageData) -> CMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn gather_history(repo: Arc<dyn ChatRepository + Send + Sync>) -> Result<Vec<ChatMessage>> {
|
async fn gather_history(
|
||||||
let messages = repo.get_latest_messages().await?;
|
repo: Arc<dyn ChatRepository + Send + Sync>,
|
||||||
|
chat_id: &i32,
|
||||||
|
) -> Result<Vec<ChatMessage>> {
|
||||||
|
let messages = repo.get_latest_messages(chat_id, &10).await?;
|
||||||
Ok(messages
|
Ok(messages
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| match m.is_user {
|
.map(|m| match m.is_user {
|
||||||
@@ -116,3 +125,13 @@ async fn gather_history(repo: Arc<dyn ChatRepository + Send + Sync>) -> Result<V
|
|||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_chat_id(
|
||||||
|
repo: Arc<dyn ChatRepository + Send + Sync>,
|
||||||
|
chat_id: Option<i64>,
|
||||||
|
) -> Result<i32> {
|
||||||
|
Ok(match chat_id {
|
||||||
|
Some(i) => i as i32,
|
||||||
|
None => repo.get_chat_ids().await?.get(0).copied().unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ mod daemongrpc;
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use genai::chat::{ChatMessage, ChatRequest};
|
|
||||||
use genai::Client;
|
use genai::Client;
|
||||||
use shared::ai::ai_daemon_server::AiDaemonServer;
|
use shared::ai::ai_daemon_server::AiDaemonServer;
|
||||||
use tonic::transport::Server;
|
use tonic::transport::Server;
|
||||||
@@ -11,20 +10,6 @@ use tonic::transport::Server;
|
|||||||
use chatpersistence::SqliteChatRepository;
|
use chatpersistence::SqliteChatRepository;
|
||||||
use daemongrpc::DaemonServer;
|
use daemongrpc::DaemonServer;
|
||||||
|
|
||||||
async fn prompt_ollama(
|
|
||||||
client: &Client,
|
|
||||||
model: &str,
|
|
||||||
prompt: &str,
|
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
|
||||||
let chat_req = ChatRequest::new(vec![ChatMessage::user(prompt)]);
|
|
||||||
let chat_res = client.exec_chat(model, chat_req, None).await?;
|
|
||||||
let output = chat_res
|
|
||||||
.first_text()
|
|
||||||
.unwrap_or("No response content!")
|
|
||||||
.to_string();
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let chat_repo = SqliteChatRepository::new().await?;
|
let chat_repo = SqliteChatRepository::new().await?;
|
||||||
|
|||||||
@@ -13,6 +13,26 @@ pub mod chatmessage {
|
|||||||
pub chat_id: Option<i64>,
|
pub chat_id: Option<i64>,
|
||||||
pub history: Vec<Message>,
|
pub history: Vec<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum TauriCommand {
|
||||||
|
Chat,
|
||||||
|
ChatHistory,
|
||||||
|
DaemonState,
|
||||||
|
ToggleDarkMode,
|
||||||
|
TogglePopup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TauriCommand {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TauriCommand::TogglePopup => "toggle_popup",
|
||||||
|
TauriCommand::Chat => "chat",
|
||||||
|
TauriCommand::ChatHistory => "chat_history",
|
||||||
|
TauriCommand::DaemonState => "daemon_state",
|
||||||
|
TauriCommand::ToggleDarkMode => "toggle_dark_mode",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod daemon {
|
pub mod daemon {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ pub async fn chat_history(
|
|||||||
) -> Result<MessageHistory, String> {
|
) -> Result<MessageHistory, String> {
|
||||||
let mut client = state.grpc_client.lock().await;
|
let mut client = state.grpc_client.lock().await;
|
||||||
let result = client
|
let result = client
|
||||||
.chat_history(ChatHistoryRequest { chat_id: None })
|
.chat_history(ChatHistoryRequest { chat_id: chat_id })
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use feshared::daemon::DaemonState;
|
use feshared::{chatmessage::TauriCommand, daemon::DaemonState};
|
||||||
use leptos::{prelude::*, reactive::spawn_local};
|
use leptos::{prelude::*, reactive::spawn_local};
|
||||||
use leptos_router::{
|
use leptos_router::{
|
||||||
components::{Route, Router, Routes},
|
components::{Route, Router, Routes},
|
||||||
@@ -34,7 +34,7 @@ fn Dashboard() -> impl IntoView {
|
|||||||
let on_click = move |_ev: leptos::ev::MouseEvent| {
|
let on_click = move |_ev: leptos::ev::MouseEvent| {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let empty_args = serde_wasm_bindgen::to_value(&serde_json::json!({})).unwrap();
|
let empty_args = serde_wasm_bindgen::to_value(&serde_json::json!({})).unwrap();
|
||||||
invoke("toggle_popup", empty_args).await;
|
invoke(TauriCommand::TogglePopup.as_str(), empty_args).await;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
view! {
|
view! {
|
||||||
@@ -60,7 +60,7 @@ pub fn DaemonStatusIndicator() -> impl IntoView {
|
|||||||
);
|
);
|
||||||
let status = LocalResource::new(move || async move {
|
let status = LocalResource::new(move || async move {
|
||||||
poll_count.get();
|
poll_count.get();
|
||||||
let s: DaemonState = invoke_typed("daemon_state", JsValue::NULL).await;
|
let s: DaemonState = invoke_typed(TauriCommand::DaemonState, JsValue::NULL).await;
|
||||||
s
|
s
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use feshared::chatmessage::TauriCommand;
|
||||||
use serde::{de::DeserializeOwned, Deserialize};
|
use serde::{de::DeserializeOwned, Deserialize};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -9,11 +10,11 @@ extern "C" {
|
|||||||
pub async fn listen(event: &str, handler: &Closure<dyn FnMut(JsValue)>) -> JsValue;
|
pub async fn listen(event: &str, handler: &Closure<dyn FnMut(JsValue)>) -> JsValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn invoke_typed<T>(cmd: &str, args: JsValue) -> T
|
pub async fn invoke_typed<T>(cmd: TauriCommand, args: JsValue) -> T
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
{
|
{
|
||||||
let response = invoke(cmd, args).await;
|
let response = invoke(cmd.as_str(), args).await;
|
||||||
let result: T = serde_wasm_bindgen::from_value(response).unwrap();
|
let result: T = serde_wasm_bindgen::from_value(response).unwrap();
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use feshared::daemon::DaemonState;
|
use feshared::{chatmessage::TauriCommand, daemon::DaemonState};
|
||||||
use leptos::{component, prelude::*, reactive::spawn_local, view, IntoView};
|
use leptos::{component, prelude::*, reactive::spawn_local, view, IntoView};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
@@ -16,7 +16,7 @@ pub fn DaemonProvider(children: ChildrenFn) -> impl IntoView {
|
|||||||
);
|
);
|
||||||
let status_res = LocalResource::new(move || async move {
|
let status_res = LocalResource::new(move || async move {
|
||||||
poll_count.get();
|
poll_count.get();
|
||||||
let s: DaemonState = invoke_typed("daemon_state", JsValue::NULL).await;
|
let s: DaemonState = invoke_typed(TauriCommand::DaemonState, JsValue::NULL).await;
|
||||||
s
|
s
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ pub fn ThemeProvider(children: Children) -> impl IntoView {
|
|||||||
pub fn DarkModeToggle() -> impl IntoView {
|
pub fn DarkModeToggle() -> impl IntoView {
|
||||||
let toggle_dark_mode = |_ev: leptos::ev::MouseEvent| {
|
let toggle_dark_mode = |_ev: leptos::ev::MouseEvent| {
|
||||||
spawn_local(async {
|
spawn_local(async {
|
||||||
let _ = invoke("toggle_dark_mode", JsValue::UNDEFINED).await;
|
let _ = invoke(TauriCommand::ToggleDarkMode.as_str(), JsValue::UNDEFINED).await;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
components::{DaemonProvider, DarkModeToggle, ThemeProvider},
|
components::{DaemonProvider, DarkModeToggle, ThemeProvider},
|
||||||
};
|
};
|
||||||
use feshared::{
|
use feshared::{
|
||||||
chatmessage::{Message, MessageHistory},
|
chatmessage::{Message, MessageHistory, TauriCommand},
|
||||||
daemon::DaemonState,
|
daemon::DaemonState,
|
||||||
};
|
};
|
||||||
use leptos::{ev::keydown, html::Input, prelude::*};
|
use leptos::{ev::keydown, html::Input, prelude::*};
|
||||||
@@ -31,7 +31,7 @@ pub fn Popup() -> impl IntoView {
|
|||||||
|
|
||||||
let init_history = Action::new_local(|(): &()| async move {
|
let init_history = Action::new_local(|(): &()| async move {
|
||||||
let history: MessageHistory = invoke_typed(
|
let history: MessageHistory = invoke_typed(
|
||||||
"chat_history",
|
TauriCommand::ChatHistory,
|
||||||
serde_wasm_bindgen::to_value(&serde_json::json!({"chat_id": 1})).unwrap(),
|
serde_wasm_bindgen::to_value(&serde_json::json!({"chat_id": 1})).unwrap(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -55,7 +55,7 @@ pub fn Popup() -> impl IntoView {
|
|||||||
let prompt = prompt.clone();
|
let prompt = prompt.clone();
|
||||||
async move {
|
async move {
|
||||||
let result: Vec<Message> = invoke_typed(
|
let result: Vec<Message> = invoke_typed(
|
||||||
"chat",
|
TauriCommand::Chat,
|
||||||
serde_wasm_bindgen::to_value(&serde_json::json!({"prompt": prompt})).unwrap(),
|
serde_wasm_bindgen::to_value(&serde_json::json!({"prompt": prompt})).unwrap(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -88,7 +88,7 @@ pub fn Popup() -> impl IntoView {
|
|||||||
let _ = window_event_listener(keydown, move |ev| {
|
let _ = window_event_listener(keydown, move |ev| {
|
||||||
if ev.key() == "Escape" {
|
if ev.key() == "Escape" {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let _ = invoke("toggle_popup", JsValue::UNDEFINED).await;
|
let _ = invoke(TauriCommand::TogglePopup.as_str(), JsValue::UNDEFINED).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user