use chrono::{DateTime, FixedOffset}; use serde_json::Value; pub fn parse_timestamp_to_ms(value: &Value) -> Option { let raw = value.as_str()?; DateTime::parse_from_rfc3339(raw) .ok() .map(|dt: DateTime| dt.timestamp_millis()) } pub fn extract_text(content: &Value) -> String { match content { Value::String(text) => text.to_string(), Value::Array(items) => items .iter() .filter_map(extract_text_from_item) .filter(|text| !text.trim().is_empty()) .collect::>() .join("\n"), Value::Object(map) => map .get("text") .and_then(|v| v.as_str()) .unwrap_or_default() .to_string(), _ => String::new(), } } fn extract_text_from_item(item: &Value) -> Option { if let Some(text) = item.get("text").and_then(|v| v.as_str()) { return Some(text.to_string()); } if let Some(text) = item.get("input_text").and_then(|v| v.as_str()) { return Some(text.to_string()); } if let Some(text) = item.get("output_text").and_then(|v| v.as_str()) { return Some(text.to_string()); } if let Some(content) = item.get("content") { let text = extract_text(content); if !text.is_empty() { return Some(text); } } None } pub fn truncate_summary(text: &str, max_chars: usize) -> String { let trimmed = text.trim(); if trimmed.is_empty() { return String::new(); } if trimmed.chars().count() <= max_chars { return trimmed.to_string(); } let mut result = trimmed.chars().take(max_chars).collect::(); result.push_str("..."); result } pub fn path_basename(value: &str) -> Option { let trimmed = value.trim(); if trimmed.is_empty() { return None; } let normalized = trimmed.trim_end_matches(['/', '\\']); let last = normalized .split(['/', '\\']) .next_back() .filter(|segment| !segment.is_empty())?; Some(last.to_string()) }