Files
momentry_portal/src-tauri/src/api/search.rs

174 lines
6.3 KiB
Rust

use crate::config::get_config;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchRequest {
pub query: String,
pub limit: Option<usize>,
pub mode: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchResult {
pub query: String,
pub count: usize,
pub hits: Vec<SearchHit>,
}
#[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<String>,
#[serde(default)]
pub file_path: Option<String>,
#[serde(default)]
pub has_visual_stats: Option<bool>,
#[serde(default)]
pub parent_id: Option<String>,
}
#[tauri::command]
pub async fn search_videos(
query: String,
limit: Option<usize>,
mode: Option<String>,
) -> Result<SearchResult, String> {
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<SearchHit> = 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<String>) -> Result<SearchResult, String> {
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<SearchHit> = 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,
})
}