Newer
Older
TelosDB / src-tauri / src / lib.rs
@楽曲作りまくりおじさん 楽曲作りまくりおじさん 3 days ago 11 KB feat: add re-index button and resolve all clippy lints
// 使用中モデル名をグローバルで保持
#[allow(dead_code)]
static MODEL_NAME: &str = "LSA (Local Semantic Analysis)";
// モデル名を返すAPI
#[tauri::command]
#[allow(dead_code)]
fn get_model_name() -> String {
    MODEL_NAME.to_string()
}
// Read last N lines from the log file for UI consumption
#[tauri::command]
fn read_logs(limit: Option<usize>) -> Result<String, String> {
    let log_dir = if cfg!(debug_assertions) {
        std::env::current_dir()
            .map_err(|e| e.to_string())?
            .join("logs")
    } else {
        dirs::data_dir()
            .unwrap_or_else(|| std::env::current_dir().unwrap())
            .join("com.telosdb.app")
            .join("logs")
    };

    let log_file = log_dir.join("telos.log");
    if !log_file.exists() {
        return Ok(String::new());
    }

    let s = std::fs::read_to_string(&log_file).map_err(|e| e.to_string())?;
    let lines: Vec<&str> = s.lines().collect();
    let n = limit.unwrap_or(200);
    let start = if lines.len() > n { lines.len() - n } else { 0 };
    Ok(lines[start..].join("\n"))
}
pub mod db;
pub mod utils;
pub mod mcp;

use std::sync::{Arc, Mutex};
use tauri::Manager;
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{TrayIconBuilder, TrayIconEvent};
use tauri_plugin_shell::process::CommandChild;

#[allow(dead_code)]
struct AppState {
    db_pool: sqlx::SqlitePool,
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let llama_child: Arc<Mutex<Option<CommandChild>>> = Arc::new(Mutex::new(None));
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .plugin({
            let log_dir = if cfg!(debug_assertions) {
                std::env::current_dir().unwrap().join("logs")
            } else {
                dirs::data_dir()
                    .unwrap_or_else(|| std::env::current_dir().unwrap())
                    .join("com.telosdb.app")
                    .join("logs")
            };
            std::fs::create_dir_all(&log_dir).ok();

            // Prune old telos logs, keep latest 7 files
            if let Ok(entries) = std::fs::read_dir(&log_dir) {
                let mut logs: Vec<_> = entries
                    .filter_map(|e| e.ok())
                    .filter_map(|de| {
                        let p = de.path();
                        if let Some(name) = p.file_name().and_then(|n| n.to_str()) {
                            if name.starts_with("telos") && name.ends_with(".log") {
                                return Some(p);
                            }
                        }
                        None
                    })
                    .collect();

                logs.sort_by_key(|p| std::fs::metadata(p).and_then(|m| m.modified()).ok());
                while logs.len() > 7 {
                    let old = logs.remove(0);
                    let _ = std::fs::remove_file(old);
                }
            }

            tauri_plugin_log::Builder::default()
                .level(log::LevelFilter::Info)
                .filter(|metadata| {
                    if metadata.target().starts_with("reqwest")
                        || metadata.target().starts_with("hyper")
                        || metadata.target().starts_with("h2")
                        || metadata.target().starts_with("tracing")
                        || metadata.target().starts_with("html5ever")
                        || metadata.target().starts_with("selectors")
                    {
                        metadata.level() <= log::Level::Warn
                    } else {
                        true
                    }
                })
                .targets([
                    tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
                    tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir {
                        file_name: Some("telos.log".to_string()),
                    }),
                    tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
                ])
                .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
                .max_file_size(10 * 1024 * 1024)
                .build()
        })
            .invoke_handler(tauri::generate_handler![get_model_name, read_logs])
        .setup({
            let llama_child = llama_child.clone();
            move |app| {
                // Resolve paths
                let app_data_dir = app
                    .path()
                    .app_data_dir()
                    .expect("failed to get app data dir");
                let db_path = app_data_dir.join("telos.db");

                // llama-serverの起動をTauriのsidecar APIで行う
                let resource_dir = app.path().resource_dir().unwrap_or_default();
                let bin_dir = resource_dir.join("bin");
                let model_path = bin_dir.join("gemma-3-270m-it-Q4_K_M.gguf");

                // vec0.dll はビルド時に実行ルート(resource_dir)にコピーされる
                let mut vec0_path = resource_dir.join("vec0.dll");

                // 開発時 (target/debug) かつ bin にある場合のフォールバック
                if !vec0_path.exists() && bin_dir.join("vec0.dll").exists() {
                    vec0_path = bin_dir.join("vec0.dll");
                }

                // Tray Menu
                let quit_i = MenuItem::with_id(app, "quit", "終了", true, None::<&str>)?;
                let show_i = MenuItem::with_id(app, "show", "表示", true, None::<&str>)?;
                let menu = Menu::with_items(app, &[&show_i, &quit_i])?;

                let llama_child_tray = llama_child.clone();
                let _tray = TrayIconBuilder::new()
                    .icon(app.default_window_icon().unwrap().clone())
                    .menu(&menu)
                    .show_menu_on_left_click(false)
                    .on_menu_event(move |app, event| {
                        match event.id.as_ref() {
                            "quit" => {
                                if let Some(child) = llama_child_tray.lock().unwrap().take() {
                                    let _ = child.kill();
                                }
                                app.exit(0);
                            }
                            "show" => {
                                if let Some(window) = app.get_webview_window("main") {
                                    window.show().unwrap();
                                    window.set_focus().unwrap();
                                }
                            }
                            _ => {}
                        }
                    })
                    .on_tray_icon_event(|tray, event| {
                        if let TrayIconEvent::Click {
                            button: tauri::tray::MouseButton::Left,
                            ..
                        } = event
                        {
                            let app = tray.app_handle();
                            if let Some(window) = app.get_webview_window("main") {
                                if window.is_visible().unwrap_or(false) {
                                    window.hide().unwrap();
                                } else {
                                    window.show().unwrap();
                                    window.set_focus().unwrap();
                                }
                            }
                        }
                    })
                    .build(app)?;

                log::info!("Initializing TelosDB at {:?}", db_path);
                log::info!("Bin directory: {:?}", bin_dir);
                log::info!("Model path (Gemma-3): {:?}", model_path);
                log::info!("vec0.dll path: {:?}", vec0_path);

                if !vec0_path.exists() {
                    log::error!(
                        "vec0.dll NOT FOUND at {:?}. Vector search and DB init will fail.",
                        vec0_path
                    );
                }

                // llama-server自動起動(Tauri sidecar API使用)は無効化
                /*
                if model_path.exists() {
                    let (mut rx, child) = app
                        .shell()
                        .sidecar("llama-server")
                        .expect("failed to create sidecar")
                        .args([
                            "--model",
                            model_path.to_str().unwrap(),
                            "--port",
                            "8080",
                            "--embedding",
                            "--pooling",
                            "mean",
                        ])
                        .spawn()
                        .expect("failed to spawn sidecar");

                    log::info!("llama-server sidecar started (Qwen3-4B)");
                    *llama_child.lock().unwrap() = Some(child);
                    std::thread::spawn(move || {
                        while let Some(event) = rx.blocking_recv() {
                            match event {
                                CommandEvent::Stdout(line) => {
                                    log::info!("llama-server: {}", String::from_utf8_lossy(&line))
                                }
                                CommandEvent::Stderr(line) => {
                                    let s = String::from_utf8_lossy(&line);
                                    if !s.contains("post_embedding") { // 埋め込みリクエスト時のノイズを軽減
                                        log::error!("llama-server: {}", s)
                                    }
                                }
                                _ => {}
                            }
                        }
                    });
                }
                */

                // DB初期化とマネージドステートの設定
                let pool = tauri::async_runtime::block_on(async {
                    let dimension = 50; // LSA のランクに合わせて 50次元に設定
                    match db::initialize_database(&db_path, &vec0_path, dimension).await {
                        Ok(pool) => {
                            log::info!("Database initialized (LSA-mode).");
                            pool
                        }
                        Err(e) => {
                            log::error!("Database initialization failed: {}", e);
                            panic!("DB Init Error: {}", e);
                        }
                    }
                });

                app.manage(AppState {
                    db_pool: pool.clone(),
                });

                // MCP Server 起動 (llama_status は stopped 固定)
                use tokio::sync::RwLock;
                let llama_status = Arc::new(RwLock::new("stopped".to_string()));
                tauri::async_runtime::spawn({
                    let pool = pool.clone();
                    async move {
                        mcp::run_server(3001, pool, llama_status, MODEL_NAME.to_string()).await;
                    }
                });

                Ok(())
            }
        })
        .on_window_event(|window, event| {
            if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                // Prevent window from closing, just hide it
                api.prevent_close();
                window.hide().unwrap();
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}