use crate::config::get_config; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct SearchRequest { pub query: String, pub limit: Option, pub mode: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct SearchResult { pub query: String, pub count: usize, pub hits: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct SearchHit { pub id: String, pub vid: String, pub start_frame: i64, pub end_frame: i64, pub fps: f64, pub start: f64, pub end: f64, pub text: String, pub score: f64, #[serde(default)] pub title: Option, #[serde(default)] pub file_path: Option, #[serde(default)] pub has_visual_stats: Option, #[serde(default)] pub parent_id: Option, } #[tauri::command] pub async fn search_videos( query: String, limit: Option, mode: Option, ) -> Result { let config = get_config(); let client = reqwest::Client::new(); let search_mode = mode.unwrap_or_else(|| "vector".to_string()); let request_body = SearchRequest { query: query.clone(), limit: limit.or(Some(10)), mode: Some(search_mode.clone()), }; let url = format!("{}/api/v1/search", config.api_base_url); let response = client .post(&url) .header("Content-Type", "application/json") .header("x-api-key", &config.api_key) .json(&request_body) .send() .await .map_err(|e| format!("Request failed: {}", e))?; if !response.status().is_success() { return Err(format!("API error: {}", response.status())); } // Parse as generic Value to handle mapping manually let json: serde_json::Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; // Map Backend Response to Frontend SearchResult // Backend: { "query": "...", "results": [ ... ], "total": N, ... } // Frontend: { "query": "...", "hits": [ ... ], "count": N } let backend_results = json.get("results").and_then(|r| r.as_array()).cloned().unwrap_or_default(); let total = json.get("total").and_then(|t| t.as_u64()).unwrap_or(0) as usize; let hits: Vec = backend_results.into_iter().filter_map(|item| { Some(SearchHit { id: item.get("chunk_id").and_then(|v| v.as_str()).unwrap_or("").to_string(), vid: item.get("uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(), start_frame: item.get("start_frame").and_then(|v| v.as_i64()).unwrap_or(0), end_frame: item.get("end_frame").and_then(|v| v.as_i64()).unwrap_or(0), fps: item.get("fps").and_then(|v| v.as_f64()).unwrap_or(30.0), start: item.get("start_time").and_then(|v| v.as_f64()).unwrap_or(0.0), end: item.get("end_time").and_then(|v| v.as_f64()).unwrap_or(0.0), text: item.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string(), score: item.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0), title: item.get("file_name").and_then(|v| v.as_str()).map(|s| s.to_string()), file_path: item.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string()), has_visual_stats: item.get("visual_stats").map(|_| true), parent_id: item.get("parent_chunk_id").and_then(|v| v.as_str()).map(|s| s.to_string()), }) }).collect(); Ok(SearchResult { query: json.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string(), count: total, hits, }) } #[tauri::command] pub async fn search_chunks(query: String, uuid: Option) -> Result { let config = get_config(); let client = reqwest::Client::new(); // Backend expects uuid in the body, not query params let url = format!("{}/api/v1/search", config.api_base_url); let mut request_body = serde_json::json!({ "query": query, "limit": 10 }); if let Some(vid) = uuid { request_body["uuid"] = serde_json::json!(vid); } let response = client .post(&url) .header("Content-Type", "application/json") .header("x-api-key", &config.api_key) .json(&request_body) .send() .await .map_err(|e| format!("Request failed: {}", e))?; if !response.status().is_success() { return Err(format!("API error: {}", response.status())); } // Parse raw JSON to handle structure mapping let json: serde_json::Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; // Backend returns "total", frontend expects "count" let count = json.get("total").and_then(|v| v.as_u64()).unwrap_or(0) as usize; // Backend returns "results", frontend expects "hits" let results = json.get("results").and_then(|v| v.as_array()).cloned().unwrap_or_default(); let hits: Vec = results.into_iter().filter_map(|item| { Some(SearchHit { id: item.get("chunk_id").and_then(|v| v.as_str()).unwrap_or("").to_string(), vid: item.get("uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(), start_frame: item.get("start_frame").and_then(|v| v.as_i64()).unwrap_or(0), end_frame: item.get("end_frame").and_then(|v| v.as_i64()).unwrap_or(0), fps: item.get("fps").and_then(|v| v.as_f64()).unwrap_or(30.0), start: item.get("start_time").and_then(|v| v.as_f64()).unwrap_or(0.0), end: item.get("end_time").and_then(|v| v.as_f64()).unwrap_or(0.0), text: item.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string(), score: item.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0), title: item.get("file_name").and_then(|v| v.as_str()).map(|s| s.to_string()), file_path: item.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string()), has_visual_stats: item.get("visual_stats").map(|_| true), parent_id: item.get("parent_chunk_id").and_then(|v| v.as_str()).map(|s| s.to_string()), }) }).collect(); Ok(SearchResult { query: json.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string(), count, hits, }) }