diff --git a/README.md b/README.md index 96b461a..81ab508 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,61 @@ -# 🦀 TelosDB (Tauri 2 Edition) +# TelosDB -> **SQLite と Rust で構築された、ローカル向けベクトル検索サーバー & DBブラウザ** +TelosDBは、SQLiteのベクトル検索機能(`sqlite-vec`)とモデル・コンテキスト・プロトコル(MCP)を統合した、モダンなデータベースブラウザ兼常駐型サーバーです。 +高性能なRustランタイム(Tauri 2)と、ローカルLLM(llama.cpp)を活用し、セマンティック検索とデータ管理をシームレスに提供します。 -[![Tauri 2](https://img.shields.io/badge/Tauri-2.0-blue?style=for-the-badge&logo=tauri)](https://tauri.app/) -[![Rust](https://img.shields.io/badge/Rust-LATEST-orange?style=for-the-badge&logo=rust)](https://www.rust-lang.org/) -[![SQLite](https://img.shields.io/badge/SQLite-vec0-lightgrey?style=for-the-badge&logo=sqlite)](https://github.com/asg017/sqlite-vec) +## 主な機能 -## 📝 プロジェクト概要 +- **モダンなUI**: グラスモルフィズムを採用したダークテーマUI(Sidebar & Tabbed Main Content)。 +- **データエクスプローラー**: データベーステーブルのブラウジング、部分一致検索、ページネーション対応。 +- **ベクトル検索**: `llama.cpp` サイドカー(嵌入モデル)と連携した、自然言語による意味的な検索。 +- **MCP サーバー機能**: 他のAIエージェントからTelosDBのデータへアクセス可能にするSSEプロトコル対応。 +- **軽量・高速**: Tauri 2(Rust)による低メモリフットプリントとネイティブ性能。 -TelosDB は、ローカル環境で動作するベクトル検索サーバーおよびデータベースブラウザです。 -Gemma 3 モデルを統合し、プライバシーを重視した効率的なデータ検索と管理を提供します。 - -## 🏗️ システム構造 +## システム構造 ```mermaid graph TD - Client[Frontend: Tauri/Svelte] -->|SSE/JSON-RPC| Server[Backend: Rust/Axum] - Server -->|load_extension| DB[(SQLite + sqlite-vec)] - Server -->|Sidecar| AI[llama-server] - AI -->|GGUF| Model[Gemma 3 300M] + UI[Frontend: HTML/CSS/JS] <--> Backend[Backend: Tauri/Rust] + Backend <--> DB[(SQLite + sqlite-vec)] + Backend <--> Llama[Sidecar: llama-server] + Backend <--> MCP[MCP Server: SSE/HTTP] + Llama <--> Model[Embedding Model: GGUF] ``` -- **Backend**: Rust (Axum, SeaORM, sqlx) -- **Database**: SQLite (ベクトル検索拡張 `sqlite-vec` を動的にロード) -- **AI Inference**: `llama-server` を Tauri サイドカーとして管理 -- **Frontend**: Svelte (Tauri v2) +- **Frontend**: Vanilla JavaScript + CSS + HTML +- **Backend**: Rust (Tauri 2) +- **Database**: SeaORM (Entity Mapping) +- **Vector Engine**: `sqlite-vec` (Windows x64) +- **LLM Engine**: `llama.cpp` (sidecar binary) -## 🛠️ セットアップ +## 開発と実行 -1. **Rust と Tauri のインストール**: - [Tauri 2 Prerequisites](https://v2.tauri.app/start/prerequisites/) に従って環境を構築してください。 +### セットアップ -2. **依存関係のインストール**: +```bash +npm install +npm run setup +``` - ```bash - npm install - ``` +### 開発モード -3. **開発モードでの実行**: +```bash +npm run dev +``` - ```bash - npm run tauri dev - ``` +### ビルド -## 📜 変更履歴 +```bash +npm run build +``` -詳細は [journals](./journals) フォルダを参照してください。 +## 最近の更新 (リファクタリング) -- `20260213-0010`: SQLite Vector ロード権限問題の修正とサイドカー管理の正常化 -- `20260213-0009`: `sqlite-vec` (Alex Garcia版) への回帰と `vec0.dll` の復旧 -- `20260213-0008`: マーケットプレース同期不全の根本解決 -- `20260213-0007`: Java 25 設定エラーの解消 +コード品質向上のため、以下のリファクタリングを実施しました: + +- **lib.rsのモジュール化**: 巨大な初期化処理を分割し、パス解決やデータ変換ロジックを共通化。ネストの深さを削減。 +- **リソースの外部ファイル化**: フロントエンドのCSSとJavaScriptを独立したファイルに分離し、保守性を向上。 +- **ロジックの堅牢化**: UIのタブ切り替えやパス解決におけるエラーハンドリングを強化。 + +--- +Developed by Antigravity (Advanced Agentic Coding) diff --git a/src/backend/src/lib.rs b/src/backend/src/lib.rs index 68ad737..075b102 100644 --- a/src/backend/src/lib.rs +++ b/src/backend/src/lib.rs @@ -23,33 +23,31 @@ pub app_handle: tauri::AppHandle, } -#[tauri::command] -fn get_mcp_info(app_handle: tauri::AppHandle) -> Result { - let mut candidates = vec![PathBuf::from("mcp.json")]; +fn find_resource_path(app_handle: &tauri::AppHandle, folder: &str, file: &str) -> Option { + let mut candidates = vec![PathBuf::from(file)]; if let Ok(res_dir) = app_handle.path().resource_dir() { - candidates.push(res_dir.join("build_assets").join("mcp.json")); + candidates.push(res_dir.join(folder).join(file)); } if let Ok(exe_path) = env::current_exe() { if let Some(exe_dir) = exe_path.parent() { let mut p = exe_dir.to_path_buf(); for _ in 0..5 { - candidates.push(p.join("build_assets").join("mcp.json")); + candidates.push(p.join(folder).join(file)); if !p.pop() { break; } } } } - let mut found_path = None; - for candidate in &candidates { - log::info!("Checking mcp.json candidate: {:?}", candidate); - if candidate.exists() { - found_path = Some(candidate.clone()); - break; - } - } - let mcp_path = found_path.ok_or_else(|| format!("mcp.json not found. Checked: {:?}", candidates))?; + candidates.into_iter().find(|p| p.exists()) +} + +#[tauri::command] +fn get_mcp_info(app_handle: tauri::AppHandle) -> Result { + let mcp_path = find_resource_path(&app_handle, "build_assets", "mcp.json") + .ok_or_else(|| "mcp.json not found".to_string())?; + let content = std::fs::read_to_string(&mcp_path).map_err(|e| e.to_string())?; let mcp_data: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?; Ok(mcp_data) @@ -69,6 +67,112 @@ Ok(state.llama.check_health().await) } +#[tauri::command] +async fn get_table_list(state: tauri::State<'_, Arc>) -> Result, String> { + use sea_orm::{ConnectionTrait, Statement}; + let backend = state.db.get_database_backend(); + let res = state.db.query_all(Statement::from_string(backend, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")).await.map_err(|e| e.to_string())?; + + let tables = res.into_iter().map(|row| row.try_get::("", "name").unwrap_or_default()).collect(); + Ok(tables) +} + +#[tauri::command] +async fn get_table_data( + state: tauri::State<'_, Arc>, + table_name: String, + limit: u64, + offset: u64, +) -> Result { + use sea_orm::{ConnectionTrait, Statement}; + let backend = state.db.get_database_backend(); + + let count_sql = format!("SELECT COUNT(*) as total FROM \"{}\"", table_name.replace("\"", "\"\"")); + let count_res = state.db.query_one(Statement::from_string(backend, &count_sql)).await.map_err(|e| e.to_string())?; + let total: i64 = count_res.map(|r| r.try_get::("", "total").unwrap_or(0)).unwrap_or(0); + + let data_sql = format!("SELECT * FROM \"{}\" LIMIT {} OFFSET {}", table_name.replace("\"", "\"\""), limit, offset); + let data_res = state.db.query_all(Statement::from_string(backend, &data_sql)).await.map_err(|e| e.to_string())?; + + let mut items = Vec::new(); + for row in data_res { + items.push(database_row_to_json(row).await); + } + + Ok(serde_json::json!({ + "data": items, + "total": total + })) +} + +#[tauri::command] +async fn get_table_schema( + state: tauri::State<'_, Arc>, + table_name: String, +) -> Result { + use sea_orm::{ConnectionTrait, Statement}; + let backend = state.db.get_database_backend(); + let sql = format!("PRAGMA table_info(\"{}\")", table_name.replace("\"", "\"\"")); + let res = state.db.query_all(Statement::from_string(backend, &sql)).await.map_err(|e| e.to_string())?; + + let mut schema = Vec::new(); + for row in res { + schema.push(database_row_to_json(row).await); + } + Ok(serde_json::Value::Array(schema)) +} + +async fn database_row_to_json(row: sea_orm::QueryResult) -> serde_json::Value { + let mut map = serde_json::Map::new(); + for col in row.column_names() { + let col_name = col.as_str(); + if let Ok(val) = row.try_get::("", col_name) { + map.insert(col.to_string(), serde_json::Value::String(val)); + } else if let Ok(val) = row.try_get::("", col_name) { + map.insert(col.to_string(), serde_json::json!(val)); + } else if let Ok(val) = row.try_get::("", col_name) { + map.insert(col.to_string(), serde_json::json!(val)); + } else if let Ok(val) = row.try_get::("", col_name) { + map.insert(col.to_string(), serde_json::json!(val)); + } else { + map.insert(col.to_string(), serde_json::Value::Null); + } + } + serde_json::Value::Object(map) +} + +#[tauri::command] +async fn vector_search_text( + state: tauri::State<'_, Arc>, + text: String, + limit: i32, +) -> Result { + let embedding = state.llama.get_embedding(&text).await.map_err(|e: anyhow::Error| e.to_string())?; + + use sea_orm::{ConnectionTrait, Statement}; + let backend = state.db.get_database_backend(); + + let vec_json = serde_json::to_string(&embedding).unwrap(); + let sql = format!( + "SELECT i.*, v.distance \ + FROM items i \ + JOIN vec_items v ON i.id = v.id \ + WHERE v.embedding MATCH '{}' \ + AND k = {} \ + ORDER BY v.distance ASC", + vec_json, limit + ); + + let res = state.db.query_all(Statement::from_string(backend, &sql)).await.map_err(|e| e.to_string())?; + + let mut results = Vec::new(); + for row in res { + results.push(database_row_to_json(row).await); + } + + Ok(serde_json::Value::Array(results)) +} + fn get_config(app_handle: &tauri::AppHandle) -> serde_json::Value { let mut config_paths = vec![]; if let Ok(app_data) = app_handle.path().app_config_dir() { config_paths.push(app_data.join("config.json")); } @@ -86,12 +190,9 @@ config_paths.push(res_dir.join("build_assets").join("config.json")); } - for path in config_paths { - log::info!("Checking config.json candidate: {:?}", path); - if path.exists() { - if let Ok(content) = std::fs::read_to_string(path) { - if let Ok(parsed) = serde_json::from_str::(&content) { return parsed; } - } + if let Some(path) = find_resource_path(app_handle, "build_assets", "config.json") { + if let Ok(content) = std::fs::read_to_string(path) { + if let Ok(parsed) = serde_json::from_str::(&content) { return parsed; } } } serde_json::json!({ @@ -275,6 +376,69 @@ } #[cfg_attr(mobile, tauri::mobile_entry_point)] +fn setup_logging(app: &mut tauri::App) { + let mut log_builder = tauri_plugin_log::Builder::default() + .targets([ + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }), + ]) + .max_file_size(10 * 1024 * 1024) + .level(log::LevelFilter::Info); + + if cfg!(debug_assertions) { + log_builder = log_builder.target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder { path: std::path::PathBuf::from("logs"), file_name: None })); + } + let _ = app.handle().plugin(log_builder.build()); +} + +async fn initialize_app(app_handle: tauri::AppHandle) { + dotenv().ok(); + let config = get_config(&app_handle); + spawn_llama_server(&app_handle, &config); + + let db_path = resolve_db_path(&app_handle, &config); + let ext_path = resolve_extension_path(&app_handle); + log::info!("DB Path: {}, Ext Path: {}", db_path, ext_path); + + let conn = db::init_db(&db_path, &ext_path).await.expect("Failed to init db"); + + let state = Arc::new(AppState { + db: conn, + llama: Arc::new(LlamaClient::new( + env::var("LLAMA_CPP_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()), + env::var("LLAMA_CPP_EMBEDDING_MODEL").unwrap_or_else(|_| "nomic-embed-text".to_string()), + env::var("LLAMA_CPP_MODEL").unwrap_or_else(|_| "mistral".to_string()), + )), + mcp_tx: tokio::sync::broadcast::channel(100).0, + connection_count: Arc::new(AtomicUsize::new(0)), + app_handle: app_handle.clone(), + }); + app_handle.manage(state.clone()); + let port = env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string()).parse::().unwrap_or(3000); + tokio::spawn(async move { mcp::start_mcp_server(state, port).await; }); +} + +fn setup_tray(app: &tauri::App) -> Result<(), Box> { + let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let show_i = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show_i, &quit_i])?; + let _tray = TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "quit" => app.exit(0), + "show" => if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } + _ => {} + }) + .on_tray_icon_event(|tray, event| if let TrayIconEvent::DoubleClick { .. } = event { + let app = tray.app_handle(); + if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } + }) + .build(app)?; + Ok(()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) @@ -285,76 +449,27 @@ } }) .setup(|app| { - // 1. Initialize Logging FIRST for crash diagnostics - let mut log_builder = tauri_plugin_log::Builder::default() - .targets([ - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }), - ]) - .max_file_size(10 * 1024 * 1024) - .level(log::LevelFilter::Info); - - if cfg!(debug_assertions) { - log_builder = log_builder.target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder { path: std::path::PathBuf::from("logs"), file_name: None })); - } - let _ = app.handle().plugin(log_builder.build()); + setup_logging(app); log::info!("Application starting (Tauri 2)..."); - // 2. Load env - dotenv().ok(); let app_handle = app.handle().clone(); - - // 3. Start Sidecar - let config = get_config(&app_handle); - spawn_llama_server(&app_handle, &config); - - // 4. Database and MCP initialization tauri::async_runtime::block_on(async move { - let db_path = resolve_db_path(&app_handle, &config); - let ext_path = resolve_extension_path(&app_handle); - log::info!("DB Path: {}, Ext Path: {}", db_path, ext_path); - - let _vec_dim = env::var("VEC_DIM").unwrap_or_else(|_| "768".to_string()).parse::().unwrap_or(768); - let conn = db::init_db(&db_path, &ext_path).await.expect("Failed to init db"); - - let state = Arc::new(AppState { - db: conn, - llama: Arc::new(LlamaClient::new( - env::var("LLAMA_CPP_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()), - env::var("LLAMA_CPP_EMBEDDING_MODEL").unwrap_or_else(|_| "nomic-embed-text".to_string()), - env::var("LLAMA_CPP_MODEL").unwrap_or_else(|_| "mistral".to_string()), - )), - mcp_tx: tokio::sync::broadcast::channel(100).0, - connection_count: Arc::new(AtomicUsize::new(0)), - app_handle: app_handle.clone(), - }); - app_handle.manage(state.clone()); - let port = env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string()).parse::().unwrap_or(3000); - tokio::spawn(async move { mcp::start_mcp_server(state, port).await; }); + initialize_app(app_handle).await; }); - // 5. Tray setup - let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; - let show_i = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?; - let menu = Menu::with_items(app, &[&show_i, &quit_i])?; - let _tray = TrayIconBuilder::new() - .icon(app.default_window_icon().unwrap().clone()) - .menu(&menu) - .on_menu_event(|app, event| match event.id.as_ref() { - "quit" => app.exit(0), - "show" => if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } - _ => {} - }) - .on_tray_icon_event(|tray, event| if let TrayIconEvent::DoubleClick { .. } = event { - let app = tray.app_handle(); - if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } - }) - .build(app)?; - + setup_tray(app).expect("Failed to setup tray"); log::info!("Tauri setup completed"); Ok(()) }) - .invoke_handler(tauri::generate_handler![get_mcp_info, get_db_stats, get_sidecar_status]) + .invoke_handler(tauri::generate_handler![ + get_mcp_info, + get_db_stats, + get_sidecar_status, + get_table_list, + get_table_data, + get_table_schema, + vector_search_text + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/frontend/index.css b/src/frontend/index.css new file mode 100644 index 0000000..42bb18f --- /dev/null +++ b/src/frontend/index.css @@ -0,0 +1,425 @@ +:root { + --bg-color: #0f172a; + --sidebar-bg: #1e293b; + --card-bg: #1e293b; + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --accent-color: #38bdf8; + --accent-hover: #0ea5e9; + --border-color: #334155; + --success: #22c55e; + --warning: #f59e0b; + --error: #ef4444; + --font-family: 'Inter', system-ui, -apple-system, sans-serif; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background-color: var(--bg-color); + color: var(--text-primary); + height: 100vh; + display: flex; + overflow: hidden; +} + +/* Sidebar */ +.sidebar { + width: 260px; + background-color: var(--sidebar-bg); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + padding: 24px 0; + flex-shrink: 0; +} + +.sidebar-header { + padding: 0 24px 24px 24px; + border-bottom: 1px solid var(--border-color); + margin-bottom: 24px; +} + +.sidebar-header h1 { + font-size: 1.5rem; + font-weight: 800; + color: var(--accent-color); + letter-spacing: -0.02em; +} + +.sidebar-section { + margin-bottom: 32px; + padding: 0 12px; +} + +.sidebar-section-title { + padding: 0 12px 12px 12px; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-secondary); + font-weight: 700; +} + +.status-list { + list-style: none; +} + +.status-item { + padding: 8px 12px; + font-size: 0.8rem; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 6px; +} + +.table-list { + list-style: none; +} + +.table-item { + padding: 10px 12px; + margin: 2px 0; + border-radius: 8px; + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 10px; +} + +.table-item:before { + content: "▤"; + font-size: 0.9rem; + opacity: 0.5; +} + +.table-item:hover { + background-color: rgba(255, 255, 255, 0.05); + color: var(--text-primary); +} + +.table-item.active { + background-color: rgba(56, 189, 248, 0.15); + color: var(--accent-color); + font-weight: 600; +} + +/* Main Content */ +.main-content { + flex-grow: 1; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; +} + +.main-header { + padding: 20px 32px; + background-color: rgba(15, 23, 42, 0.7); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + z-index: 10; +} + +.main-header h2 { + font-size: 1.25rem; + font-weight: 700; +} + +.tabs { + display: flex; + gap: 28px; + padding: 0 32px; + background-color: rgba(15, 23, 42, 0.4); + border-bottom: 1px solid var(--border-color); +} + +.tab { + padding: 14px 2px; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; + position: relative; +} + +.tab:hover { + color: var(--text-primary); +} + +.tab.active { + color: var(--accent-color); + border-bottom-color: var(--accent-color); +} + +/* Views */ +.view-container { + flex-grow: 1; + overflow: auto; + padding: 32px; + scroll-behavior: smooth; +} + +.view { + display: none; + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.view.active { + display: block; +} + +/* Tables */ +.table-wrapper { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +th { + text-align: left; + padding: 14px 18px; + background-color: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--border-color); + color: var(--text-secondary); + font-weight: 700; + text-transform: uppercase; + font-size: 0.7rem; + letter-spacing: 0.05em; +} + +td { + padding: 14px 18px; + border-bottom: 1px solid var(--border-color); + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +tr:last-child td { + border-bottom: none; +} + +tr:hover td { + background-color: rgba(255, 255, 255, 0.02); +} + +/* Form Elements */ +.search-container { + margin-bottom: 24px; + display: flex; + gap: 12px; +} + +.input-group { + flex-grow: 1; + position: relative; +} + +input[type="text"], +textarea { + width: 100%; + padding: 12px 16px; + background-color: rgba(30, 41, 59, 0.5); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-family: inherit; + font-size: 0.9rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +input:focus, +textarea:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2); +} + +.btn { + padding: 10px 20px; + background-color: var(--accent-color); + border: none; + border-radius: 8px; + color: #000; + font-weight: 700; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.btn:hover:not(:disabled) { + background-color: var(--accent-hover); + transform: translateY(-1px); +} + +.btn:active:not(:disabled) { + transform: translateY(0); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-outline { + background-color: transparent; + border: 1px solid var(--border-color); + color: var(--text-primary); +} + +.btn-outline:hover { + background-color: rgba(255, 255, 255, 0.05); + border-color: var(--text-secondary); +} + +/* Result Cards */ +.vector-result { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; + transition: box-shadow 0.2s; +} + +.vector-result:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border-color: var(--accent-color); +} + +.vector-result-header { + display: flex; + justify-content: space-between; + margin-bottom: 12px; +} + +.score-badge { + padding: 4px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 700; + background: rgba(34, 197, 94, 0.2); + color: var(--success); +} + +.content-preview { + font-size: 0.95rem; + line-height: 1.6; + color: var(--text-primary); +} + +/* General Utilities */ +.tag { + padding: 3px 8px; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; +} + +.tag-green { + background: rgba(34, 197, 94, 0.2); + color: var(--success); +} + +.tag-orange { + background: rgba(245, 158, 11, 0.2); + color: var(--warning); +} + +.tag-blue { + background: rgba(56, 189, 248, 0.2); + color: var(--accent-color); +} + +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 24px; + color: var(--text-secondary); + font-size: 0.8rem; +} + +/* Glassmorphism Loader */ +.loader-overlay { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + border-radius: 12px; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(56, 189, 248, 0.1); + border-left-color: var(--accent-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-secondary); + text-align: center; +} + +.empty-state-icon { + font-size: 4rem; + margin-bottom: 20px; + opacity: 0.1; +} diff --git a/src/frontend/index.html b/src/frontend/index.html index 2d2c487..9957f48 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -4,218 +4,148 @@ - SQLite Vector MCP Server - + TelosDB - Database Browser + -

SQLite Vector MCP Server

-

- Status: Loading... | - Sidecar: Checking... | - Connected Clients: 0 | - Stored Items: 0 -

+