Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/apps/desktop/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@
"dialog:allow-message",
"opener:default",
"opener:allow-open-url",
"opener:allow-open-path",
{
"identifier": "opener:allow-open-path",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$HOME/**" }
]
},
"opener:allow-reveal-item-in-dir",
"fs:default",
"fs:allow-read-file",
Expand Down
67 changes: 67 additions & 0 deletions src/apps/desktop/src/api/insights_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use bitfun_core::agentic::insights::{InsightsReport, InsightsReportMeta, InsightsService};
use log::{error, info};
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerateInsightsRequest {
pub days: Option<u32>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadInsightsReportRequest {
pub path: String,
}

#[tauri::command]
pub async fn generate_insights(
request: GenerateInsightsRequest,
) -> Result<InsightsReport, String> {
let days = request.days.unwrap_or(30);
info!("Generating insights for the last {} days", days);

InsightsService::generate(days)
.await
.map_err(|e| {
error!("Failed to generate insights: {}", e);
format!("Failed to generate insights: {}", e)
})
}

#[tauri::command]
pub async fn get_latest_insights() -> Result<Vec<InsightsReportMeta>, String> {
InsightsService::load_latest_reports().await.map_err(|e| {
error!("Failed to load latest insights: {}", e);
format!("Failed to load latest insights: {}", e)
})
}

#[tauri::command]
pub async fn load_insights_report(
request: LoadInsightsReportRequest,
) -> Result<InsightsReport, String> {
InsightsService::load_report(&request.path).await.map_err(|e| {
error!("Failed to load insights report: {}", e);
format!("Failed to load insights report: {}", e)
})
}

#[tauri::command]
pub async fn has_insights_data(
request: GenerateInsightsRequest,
) -> Result<bool, String> {
let days = request.days.unwrap_or(30);
InsightsService::has_data(days).await.map_err(|e| {
error!("Failed to check insights data: {}", e);
format!("Failed to check insights data: {}", e)
})
}

#[tauri::command]
pub async fn cancel_insights_generation() -> Result<(), String> {
InsightsService::cancel().await.map_err(|e| {
error!("Failed to cancel insights generation: {}", e);
e
})
}
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ pub mod system_api;
pub mod terminal_api;
pub mod token_usage_api;
pub mod tool_api;
pub mod insights_api;

pub use app_state::{AppState, AppStatistics, HealthStatus};
6 changes: 6 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,12 @@ pub async fn run() {
// Browser API
api::browser_api::browser_webview_eval,
api::browser_api::browser_get_url,
// Insights API
api::insights_api::generate_insights,
api::insights_api::get_latest_insights,
api::insights_api::load_insights_report,
api::insights_api::has_insights_data,
api::insights_api::cancel_insights_generation,
])
.run(tauri::generate_context!());
if let Err(e) = run_result {
Expand Down
26 changes: 2 additions & 24 deletions src/crates/core/src/agentic/image_analysis/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ impl ImageAnalyzer {
}

fn parse_analysis_response(response: &str, image_id: &str) -> ImageAnalysisResult {
let json_str = Self::extract_json_from_markdown(response).unwrap_or(response);
let extracted = crate::util::extract_json_from_ai_response(response);
let json_str = extracted.as_deref().unwrap_or(response);

if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_str) {
return ImageAnalysisResult {
Expand Down Expand Up @@ -253,27 +254,4 @@ impl ImageAnalyzer {
}
}

fn extract_json_from_markdown(text: &str) -> Option<&str> {
if let Some(start_idx) = text.find("<|begin_of_box|>") {
let content_start = start_idx + "<|begin_of_box|>".len();
if let Some(end_idx) = text[content_start..].find("<|end_of_box|>") {
let json_content = &text[content_start..content_start + end_idx].trim();
debug!("Extracted Zhipu AI box format JSON");
return Some(json_content);
}
}

let start_markers = ["```json\n", "```\n"];

for marker in &start_markers {
if let Some(start_idx) = text.find(marker) {
let content_start = start_idx + marker.len();
if let Some(end_idx) = text[content_start..].find("```") {
return Some(&text[content_start..content_start + end_idx].trim());
}
}
}

None
}
}
49 changes: 49 additions & 0 deletions src/crates/core/src/agentic/insights/cancellation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use log::{debug, info, warn};
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;

type Slot = Arc<Mutex<Option<CancellationToken>>>;

static SLOT: std::sync::OnceLock<Slot> = std::sync::OnceLock::new();

fn get_slot() -> Slot {
SLOT.get_or_init(|| Arc::new(Mutex::new(None))).clone()
}

/// Registers a new insights generation task, cancelling any previous one.
pub async fn register() -> CancellationToken {
let token = CancellationToken::new();
let arc = get_slot();
let mut slot = arc.lock().await;
if let Some(old) = slot.take() {
old.cancel();
debug!("Cancelled previous insights generation");
}
*slot = Some(token.clone());
token
}

/// Cancels the current insights generation task.
pub async fn cancel() -> Result<(), String> {
let arc = get_slot();
let mut slot = arc.lock().await;
match slot.take() {
Some(token) => {
token.cancel();
info!("Insights generation cancelled by user");
Ok(())
}
None => {
warn!("No insights generation in progress to cancel");
Err("No insights generation in progress".into())
}
}
}

/// Unregisters the current task (call on completion).
pub async fn unregister() {
let arc = get_slot();
let mut slot = arc.lock().await;
*slot = None;
}
Loading
Loading