feat: add emoji-regex and remove-accents dependencies; enhance chat and setup pages
- Added `emoji-regex` and `remove-accents` packages to improve text processing capabilities. - Implemented automatic Ollama startup in the chat page for better user experience. - Enhanced setup page to include GPU selection based on hardware specifications. - Updated chat input to support real-time validation and improved user feedback. - Refactored message handling in chat to incorporate new processing features.
This commit is contained in:
1 parent
4275295e4f
commit
84ea2dec19
51 files changed
+7857
-573
No files matched your search
+644
-29
@@ -1,6 +1,7 @@
|
||||
use std::process::{Command, Stdio, Child};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::time::Duration;
|
||||
use std::io::{BufRead, BufReader, Write, Read};
|
||||
use std::time::{Duration, Instant};
|
||||
use futures_util::StreamExt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
@@ -18,6 +19,7 @@ mod task_executor;
|
||||
mod scheduler_loop;
|
||||
mod sources_config;
|
||||
mod system_monitor;
|
||||
mod intent_classifier;
|
||||
|
||||
use web_scraper::{
|
||||
ScrapedContent,
|
||||
@@ -34,10 +36,9 @@ use web_scraper::{
|
||||
use headless_chrome::Browser;
|
||||
use scheduler::{SentinelTask, SchedulerService, SchedulerState, TaskAction};
|
||||
use sources_config::{SourcesConfig, load_sources_config, save_sources_config};
|
||||
use system_monitor::{SystemStats, SystemMonitorState};
|
||||
use system_monitor::{SystemStats, SystemMonitorState, GpuInfo};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
// CommandExt é importado localmente onde necessário
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
struct Message {
|
||||
@@ -47,6 +48,27 @@ struct Message {
|
||||
metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct DownloadProgress {
|
||||
status: String, // "pulling", "verifying", "success"
|
||||
percent: Option<u8>, // 0-100
|
||||
downloaded: Option<String>, // "552 MB"
|
||||
total: Option<String>, // "1.2 GB"
|
||||
speed: Option<String>, // "25 MB/s"
|
||||
raw: String, // linha original para fallback
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct PullProgress {
|
||||
status: String,
|
||||
#[serde(default)]
|
||||
digest: String, // Mantido para compatibilidade com API, mas não usado atualmente
|
||||
#[serde(default)]
|
||||
total: u64,
|
||||
#[serde(default)]
|
||||
completed: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
struct ChatSession {
|
||||
id: String,
|
||||
@@ -75,6 +97,7 @@ struct SystemSpecs {
|
||||
total_memory: u64,
|
||||
cpu_count: usize,
|
||||
os_name: String,
|
||||
gpus: Vec<GpuInfo>,
|
||||
}
|
||||
|
||||
// SystemStats movido para system_monitor.rs
|
||||
@@ -440,13 +463,39 @@ fn get_system_specs() -> SystemSpecs {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
// Detectar todas as GPUs
|
||||
let gpus = system_monitor::detect_all_gpus();
|
||||
|
||||
SystemSpecs {
|
||||
total_memory: sys.total_memory(),
|
||||
cpu_count: sys.cpus().len(),
|
||||
os_name: System::name().unwrap_or("Unknown".to_string()),
|
||||
gpus,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retorna o sistema operacional atual: 'windows', 'mac', ou 'linux'
|
||||
#[command]
|
||||
fn get_operating_system() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return "windows".to_string();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
return "mac".to_string();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return "linux".to_string();
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
return "unknown".to_string();
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn start_system_monitor(window: Window) {
|
||||
std::thread::spawn(move || {
|
||||
@@ -542,34 +591,248 @@ fn check_if_model_installed(name: String) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn pull_model(window: Window, name: String) -> Result<(), String> {
|
||||
let mut child = Command::new("ollama")
|
||||
.arg("pull")
|
||||
.arg(&name)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let stdout = child.stdout.take().ok_or("Failed to open stdout")?;
|
||||
let reader = BufReader::new(stdout);
|
||||
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(l) => {
|
||||
window.emit("download-progress", l).unwrap_or(());
|
||||
// Função auxiliar para ler linha até encontrar \r ou \n (mantida para fallback)
|
||||
#[allow(dead_code)]
|
||||
fn read_line_until_delimiter<R: Read>(reader: &mut BufReader<R>, buffer: &mut Vec<u8>) -> Result<usize, std::io::Error> {
|
||||
buffer.clear();
|
||||
let mut byte = [0u8; 1];
|
||||
let mut count = 0;
|
||||
|
||||
loop {
|
||||
match reader.read(&mut byte)? {
|
||||
0 => break, // EOF
|
||||
_ => {
|
||||
if byte[0] == b'\r' {
|
||||
// Se for \r, verificar se o próximo é \n e pular ambos
|
||||
let mut peek = [0u8; 1];
|
||||
if reader.read(&mut peek).unwrap_or(0) > 0 && peek[0] == b'\n' {
|
||||
// É \r\n, já consumimos ambos
|
||||
} else {
|
||||
// É apenas \r, já consumimos
|
||||
}
|
||||
break;
|
||||
} else if byte[0] == b'\n' {
|
||||
break;
|
||||
}
|
||||
buffer.push(byte[0]);
|
||||
count += 1;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let status = child.wait().map_err(|e| e.to_string())?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// Função auxiliar para formatar bytes em formato legível
|
||||
fn format_bytes(bytes: u64) -> Option<String> {
|
||||
if bytes == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = KB * 1024;
|
||||
const GB: u64 = MB * 1024;
|
||||
|
||||
Some(if bytes >= GB {
|
||||
format!("{:.1} GB", bytes as f64 / GB as f64)
|
||||
} else if bytes >= MB {
|
||||
format!("{:.1} MB", bytes as f64 / MB as f64)
|
||||
} else if bytes >= KB {
|
||||
format!("{:.1} KB", bytes as f64 / KB as f64)
|
||||
} else {
|
||||
Err("Failed to pull model".to_string())
|
||||
format!("{} B", bytes)
|
||||
})
|
||||
}
|
||||
|
||||
// Função para parsear linha do Ollama e extrair informações (mantida para fallback)
|
||||
#[allow(dead_code)]
|
||||
fn parse_ollama_progress(line: &str) -> DownloadProgress {
|
||||
let line_lower = line.to_lowercase();
|
||||
let mut status = "downloading".to_string();
|
||||
let mut percent: Option<u8> = None;
|
||||
let mut downloaded: Option<String> = None;
|
||||
let mut total: Option<String> = None;
|
||||
let mut speed: Option<String> = None;
|
||||
|
||||
// Determinar status
|
||||
if line_lower.contains("pulling manifest") || line_lower.contains("pulling") {
|
||||
status = "pulling".to_string();
|
||||
} else if line_lower.contains("verifying") {
|
||||
status = "verifying".to_string();
|
||||
} else if line_lower.contains("writing manifest") {
|
||||
status = "writing".to_string();
|
||||
} else if line_lower.contains("success") || line_lower.contains("complete") || line_lower.contains("pulled") {
|
||||
status = "success".to_string();
|
||||
}
|
||||
|
||||
// Extrair porcentagem: "45%" ou "45 %"
|
||||
if let Some(caps) = regex::Regex::new(r"(\d+)\s*%").unwrap().captures(line) {
|
||||
if let Ok(p) = caps[1].parse::<u8>() {
|
||||
percent = Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Extrair tamanho baixado/total: "552 MB/1.2 GB" ou "552MB / 1.2GB"
|
||||
if let Some(caps) = regex::Regex::new(r"(\d+(?:\.\d+)?)\s*([KMGT]?B)\s*/\s*(\d+(?:\.\d+)?)\s*([KMGT]?B)").unwrap().captures(line) {
|
||||
downloaded = Some(format!("{} {}", &caps[1], &caps[2]));
|
||||
total = Some(format!("{} {}", &caps[3], &caps[4]));
|
||||
}
|
||||
|
||||
// Extrair velocidade: "25 MB/s" ou "25MB/s"
|
||||
if let Some(caps) = regex::Regex::new(r"(\d+(?:\.\d+)?)\s*([KMGT]?B/s)").unwrap().captures(line) {
|
||||
speed = Some(format!("{} {}", &caps[1], &caps[2]));
|
||||
}
|
||||
|
||||
DownloadProgress {
|
||||
status,
|
||||
percent,
|
||||
downloaded,
|
||||
total,
|
||||
speed,
|
||||
raw: line.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn pull_model(window: Window, name: String) -> Result<(), String> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Fazer requisição POST para API do Ollama com streaming
|
||||
let response = client
|
||||
.post("http://localhost:11434/api/pull")
|
||||
.json(&serde_json::json!({ "name": name, "stream": true }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to Ollama API: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Ollama API returned error: {}", response.status()));
|
||||
}
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut buffer = String::new();
|
||||
let mut last_completed: u64 = 0;
|
||||
let mut last_time = Instant::now();
|
||||
|
||||
// Processar stream NDJSON (Newline Delimited JSON)
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| format!("Stream error: {}", e))?;
|
||||
let chunk_str = String::from_utf8_lossy(&chunk);
|
||||
buffer.push_str(&chunk_str);
|
||||
|
||||
// Processar linhas completas (separadas por \n)
|
||||
while let Some(pos) = buffer.find('\n') {
|
||||
let line = buffer[..pos].trim().to_string();
|
||||
buffer = buffer[pos + 1..].to_string();
|
||||
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tentar deserializar como PullProgress
|
||||
match serde_json::from_str::<PullProgress>(&line) {
|
||||
Ok(json_progress) => {
|
||||
// Calcular porcentagem se tiver total/completed
|
||||
let percent = if json_progress.total > 0 {
|
||||
Some(((json_progress.completed as f64 / json_progress.total as f64) * 100.0) as u8)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Calcular velocidade
|
||||
let now = Instant::now();
|
||||
let delta_time = now.duration_since(last_time).as_secs_f64();
|
||||
let speed = if delta_time > 0.0 && json_progress.completed > last_completed {
|
||||
let delta_bytes = json_progress.completed - last_completed;
|
||||
let bytes_per_sec = delta_bytes as f64 / delta_time;
|
||||
Some(format_speed(bytes_per_sec))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
last_completed = json_progress.completed;
|
||||
last_time = now;
|
||||
|
||||
// Criar DownloadProgress estruturado
|
||||
let progress = DownloadProgress {
|
||||
status: json_progress.status.clone(),
|
||||
percent,
|
||||
downloaded: format_bytes(json_progress.completed),
|
||||
total: format_bytes(json_progress.total),
|
||||
speed,
|
||||
raw: line.clone(),
|
||||
};
|
||||
|
||||
// Emitir evento para frontend
|
||||
if let Ok(json) = serde_json::to_string(&progress) {
|
||||
window.emit("download-progress", json).unwrap_or(());
|
||||
}
|
||||
|
||||
// Se status for "success", finalizar
|
||||
if json_progress.status == "success" {
|
||||
let success_progress = DownloadProgress {
|
||||
status: "success".to_string(),
|
||||
percent: Some(100),
|
||||
downloaded: format_bytes(json_progress.completed),
|
||||
total: format_bytes(json_progress.total),
|
||||
speed: None,
|
||||
raw: "success".to_string(),
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&success_progress) {
|
||||
window.emit("download-progress", json).unwrap_or(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Se não conseguir parsear como JSON, tratar como linha raw (fallback)
|
||||
let progress = DownloadProgress {
|
||||
status: "downloading".to_string(),
|
||||
percent: None,
|
||||
downloaded: None,
|
||||
total: None,
|
||||
speed: None,
|
||||
raw: line,
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&progress) {
|
||||
window.emit("download-progress", json).unwrap_or(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Se chegou aqui, o stream terminou sem "success" explícito
|
||||
// Emitir sucesso final
|
||||
let success_progress = DownloadProgress {
|
||||
status: "success".to_string(),
|
||||
percent: Some(100),
|
||||
downloaded: format_bytes(last_completed),
|
||||
total: None,
|
||||
speed: None,
|
||||
raw: "success".to_string(),
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&success_progress) {
|
||||
window.emit("download-progress", json).unwrap_or(());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Função auxiliar para formatar velocidade
|
||||
fn format_speed(bytes_per_sec: f64) -> String {
|
||||
const KB: f64 = 1024.0;
|
||||
const MB: f64 = KB * 1024.0;
|
||||
const GB: f64 = MB * 1024.0;
|
||||
|
||||
if bytes_per_sec >= GB {
|
||||
format!("{:.1} GB/s", bytes_per_sec / GB)
|
||||
} else if bytes_per_sec >= MB {
|
||||
format!("{:.1} MB/s", bytes_per_sec / MB)
|
||||
} else if bytes_per_sec >= KB {
|
||||
format!("{:.1} KB/s", bytes_per_sec / KB)
|
||||
} else {
|
||||
format!("{:.0} B/s", bytes_per_sec)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,6 +852,84 @@ async fn check_ollama_running() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Verificação completa do Ollama: instalação e execução
|
||||
#[derive(serde::Serialize)]
|
||||
struct OllamaCheckResult {
|
||||
installed: bool,
|
||||
running: bool,
|
||||
status: String, // "not_installed" | "installed_stopped" | "running"
|
||||
}
|
||||
|
||||
/// Inicia o Ollama automaticamente se estiver instalado mas não estiver rodando
|
||||
#[command]
|
||||
async fn auto_start_ollama() -> Result<bool, String> {
|
||||
// Verificar se está instalado
|
||||
let installed = check_ollama_installed();
|
||||
if !installed {
|
||||
log::info!("Ollama não está instalado, pulando inicialização automática");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Verificar se já está rodando
|
||||
let running = check_ollama_running().await;
|
||||
if running {
|
||||
log::info!("Ollama já está rodando");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Tentar iniciar
|
||||
log::info!("Iniciando Ollama automaticamente...");
|
||||
match start_ollama_server() {
|
||||
Ok(_) => {
|
||||
// Aguardar um pouco para o servidor iniciar
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Verificar se iniciou com sucesso
|
||||
let is_running = check_ollama_running().await;
|
||||
if is_running {
|
||||
log::info!("Ollama iniciado com sucesso");
|
||||
Ok(true)
|
||||
} else {
|
||||
log::warn!("Ollama foi iniciado mas ainda não está respondendo");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Falha ao iniciar Ollama automaticamente: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn check_ollama_full() -> Result<OllamaCheckResult, String> {
|
||||
let installed = check_ollama_installed();
|
||||
|
||||
if !installed {
|
||||
return Ok(OllamaCheckResult {
|
||||
installed: false,
|
||||
running: false,
|
||||
status: "not_installed".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let running = check_ollama_running().await;
|
||||
|
||||
if !running {
|
||||
return Ok(OllamaCheckResult {
|
||||
installed: true,
|
||||
running: false,
|
||||
status: "installed_stopped".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(OllamaCheckResult {
|
||||
installed: true,
|
||||
running: true,
|
||||
status: "running".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn start_ollama_server() -> Result<(), String> {
|
||||
let mut cmd = Command::new("ollama");
|
||||
@@ -596,6 +937,7 @@ fn start_ollama_server() -> Result<(), String> {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
@@ -759,6 +1101,7 @@ fn start_mcp_server(
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
@@ -864,6 +1207,7 @@ fn restart_mcp_server(
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
@@ -970,6 +1314,7 @@ fn restart_all_mcp_servers(
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
@@ -1518,6 +1863,228 @@ fn save_sources_config_command(app_handle: AppHandle, config: SourcesConfig) ->
|
||||
save_sources_config(&app_handle, config)
|
||||
}
|
||||
|
||||
// ========== Ollama Installer Download Commands ==========
|
||||
|
||||
/// Verifica se uma URL de download está disponível
|
||||
#[command]
|
||||
async fn check_download_url(url: String) -> Result<bool, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
match client.head(&url).send().await {
|
||||
Ok(response) => Ok(response.status().is_success()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtém o caminho do instalador local se existir
|
||||
#[command]
|
||||
fn get_local_installer_path(filename: String, app_handle: AppHandle) -> Result<Option<String>, String> {
|
||||
// Tentar no diretório do executável (dev e produção)
|
||||
// Em desenvolvimento, os arquivos estão em public/ relativo ao projeto
|
||||
// Em produção, tentamos encontrar o arquivo em vários locais possíveis
|
||||
if let Ok(exe_dir) = app_handle.path().executable_dir() {
|
||||
// Tentar vários caminhos possíveis
|
||||
let possible_paths = vec![
|
||||
// Caminho relativo ao executável (dev) - subir até a raiz do projeto
|
||||
exe_dir.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.join("public").join("installers").join(&filename)),
|
||||
// Caminho direto do executável (dev)
|
||||
exe_dir.parent()
|
||||
.map(|p| p.join("public").join("installers").join(&filename)),
|
||||
// Caminho absoluto do workspace (dev) - tentar encontrar a raiz
|
||||
exe_dir.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.join("public").join("installers").join(&filename)),
|
||||
// Em produção, arquivos podem estar no diretório do executável
|
||||
Some(exe_dir.join("installers").join(&filename)),
|
||||
// Ou no diretório pai do executável
|
||||
exe_dir.parent()
|
||||
.map(|p| p.join("installers").join(&filename)),
|
||||
];
|
||||
|
||||
for path_opt in possible_paths {
|
||||
if let Some(path) = path_opt {
|
||||
if path.exists() {
|
||||
return Ok(Some(path.to_string_lossy().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Faz download do instalador da URL oficial ou usa fallback local
|
||||
#[command]
|
||||
async fn download_installer(
|
||||
url: String,
|
||||
filename: String,
|
||||
window: Window,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<String, String> {
|
||||
use std::io::Write;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
// Primeiro, tentar usar instalador local como fallback
|
||||
if let Some(local_path) = get_local_installer_path(filename.clone(), app_handle.clone())? {
|
||||
let local_path_buf = PathBuf::from(&local_path);
|
||||
if local_path_buf.exists() {
|
||||
// Copiar para app_data_dir/installers
|
||||
let app_data_dir = app_handle.path().app_data_dir()
|
||||
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
|
||||
let installers_dir = app_data_dir.join("installers");
|
||||
|
||||
if !installers_dir.exists() {
|
||||
fs::create_dir_all(&installers_dir)
|
||||
.map_err(|e| format!("Failed to create installers directory: {}", e))?;
|
||||
}
|
||||
|
||||
let dest_path = installers_dir.join(&filename);
|
||||
fs::copy(&local_path_buf, &dest_path)
|
||||
.map_err(|e| format!("Failed to copy local installer: {}", e))?;
|
||||
|
||||
window.emit("installer-download-progress", serde_json::json!({
|
||||
"progress": 100,
|
||||
"status": "Concluído (versão local)"
|
||||
})).ok();
|
||||
|
||||
return Ok(dest_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Fazer download da URL
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(300)) // 5 minutos de timeout
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download installer: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Download failed with status: {}", response.status()));
|
||||
}
|
||||
|
||||
// Obter tamanho total do arquivo
|
||||
let total_size = response.content_length().unwrap_or(0);
|
||||
|
||||
// Criar diretório de instaladores
|
||||
let app_data_dir = app_handle.path().app_data_dir()
|
||||
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
|
||||
let installers_dir = app_data_dir.join("installers");
|
||||
|
||||
if !installers_dir.exists() {
|
||||
fs::create_dir_all(&installers_dir)
|
||||
.map_err(|e| format!("Failed to create installers directory: {}", e))?;
|
||||
}
|
||||
|
||||
let dest_path = installers_dir.join(&filename);
|
||||
let mut file = fs::File::create(&dest_path)
|
||||
.map_err(|e| format!("Failed to create file: {}", e))?;
|
||||
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk = item.map_err(|e| format!("Failed to read chunk: {}", e))?;
|
||||
file.write_all(&chunk)
|
||||
.map_err(|e| format!("Failed to write chunk: {}", e))?;
|
||||
|
||||
downloaded += chunk.len() as u64;
|
||||
|
||||
// Emitir progresso
|
||||
let progress = if total_size > 0 {
|
||||
(downloaded * 100) / total_size
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
window.emit("installer-download-progress", serde_json::json!({
|
||||
"progress": progress,
|
||||
"downloaded": downloaded,
|
||||
"total": total_size,
|
||||
"status": format!("Baixando... {}%", progress)
|
||||
})).ok();
|
||||
}
|
||||
|
||||
window.emit("installer-download-progress", serde_json::json!({
|
||||
"progress": 100,
|
||||
"status": "Download concluído"
|
||||
})).ok();
|
||||
|
||||
log::info!("Instalador baixado para: {:?}", dest_path);
|
||||
Ok(dest_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Executa o instalador baixado
|
||||
#[command]
|
||||
fn run_installer(file_path: String) -> Result<(), String> {
|
||||
let path = PathBuf::from(&file_path);
|
||||
|
||||
if !path.exists() {
|
||||
return Err(format!("Instalador não encontrado: {}", file_path));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// No Windows, executar o .exe diretamente
|
||||
Command::new(&path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to run installer: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// No Linux, dar permissão de execução e executar
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&path)
|
||||
.map_err(|e| format!("Failed to get file metadata: {}", e))?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&path, perms)
|
||||
.map_err(|e| format!("Failed to set executable permissions: {}", e))?;
|
||||
|
||||
Command::new(&path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to run installer: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// No macOS, executar o .zip (precisa ser extraído primeiro)
|
||||
// Por enquanto, apenas abrir o arquivo
|
||||
Command::new("open")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open installer: {}", e))?;
|
||||
}
|
||||
|
||||
log::info!("Instalador executado: {:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifica se o instalador já foi baixado
|
||||
#[command]
|
||||
fn get_downloaded_installer_path(filename: String, app_handle: AppHandle) -> Result<Option<String>, String> {
|
||||
let app_data_dir = app_handle.path().app_data_dir()
|
||||
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
|
||||
let installer_path = app_data_dir.join("installers").join(&filename);
|
||||
|
||||
if installer_path.exists() {
|
||||
Ok(Some(installer_path.to_string_lossy().to_string()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Export & Backup Commands ==========
|
||||
|
||||
/// Exporta todos os dados do app (chats, tasks, sources, settings) para um arquivo ZIP
|
||||
@@ -1671,6 +2238,19 @@ fn get_recent_logs(app_handle: AppHandle, lines: usize) -> Result<Vec<String>, S
|
||||
}
|
||||
}
|
||||
|
||||
/// Recebe logs do frontend e os imprime no terminal
|
||||
#[command]
|
||||
fn log_to_terminal(level: String, message: String) -> Result<(), String> {
|
||||
match level.as_str() {
|
||||
"info" => log::info!("{}", message),
|
||||
"warn" => log::warn!("{}", message),
|
||||
"error" => log::error!("{}", message),
|
||||
"debug" => log::debug!("{}", message),
|
||||
_ => log::info!("{}", message),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========== System Monitor Commands ==========
|
||||
|
||||
/// Obtém estatísticas do sistema em tempo real
|
||||
@@ -1755,6 +2335,20 @@ async fn toggle_task(
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn classify_intent(query: String) -> String {
|
||||
use intent_classifier::{IntentClassifier, QueryIntent};
|
||||
let intent = IntentClassifier::classify(&query);
|
||||
match intent {
|
||||
QueryIntent::Factual => "factual".to_string(),
|
||||
QueryIntent::Conversational => "conversational".to_string(),
|
||||
QueryIntent::Technical => "technical".to_string(),
|
||||
QueryIntent::Opinion => "opinion".to_string(),
|
||||
QueryIntent::Calculation => "calculation".to_string(),
|
||||
QueryIntent::Unknown => "unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -1798,6 +2392,17 @@ pub fn run() {
|
||||
let app_handle = app.handle().clone();
|
||||
let scheduler_clone = scheduler_state.clone();
|
||||
|
||||
// Inicializar Ollama automaticamente se estiver instalado
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// Aguardar um pouco para o app inicializar completamente
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
|
||||
// Tentar iniciar Ollama automaticamente
|
||||
if let Err(e) = auto_start_ollama().await {
|
||||
log::warn!("Falha ao iniciar Ollama automaticamente: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// BrowserState não é mais necessário - o scheduler criará o browser quando necessário
|
||||
// Usar o runtime async do Tauri ao invés de tokio::spawn
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -1826,6 +2431,7 @@ pub fn run() {
|
||||
check_ollama_installed,
|
||||
check_ollama_running,
|
||||
get_system_specs,
|
||||
get_operating_system,
|
||||
check_if_model_installed,
|
||||
pull_model,
|
||||
start_ollama_server,
|
||||
@@ -1862,12 +2468,21 @@ pub fn run() {
|
||||
load_sources_config_command,
|
||||
save_sources_config_command,
|
||||
get_recent_logs,
|
||||
log_to_terminal,
|
||||
get_system_stats,
|
||||
create_task,
|
||||
list_tasks,
|
||||
update_task,
|
||||
delete_task,
|
||||
toggle_task
|
||||
toggle_task,
|
||||
check_download_url,
|
||||
get_local_installer_path,
|
||||
download_installer,
|
||||
run_installer,
|
||||
get_downloaded_installer_path,
|
||||
check_ollama_full,
|
||||
auto_start_ollama,
|
||||
classify_intent
|
||||
])
|
||||
.manage(Arc::new(Mutex::new(HashMap::<String, McpProcessHandle>::new())) as McpProcessMap)
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
Reference in new issue
Block a user