Skip to main content

trailcache_core/
summaries.rs

1//! AI-generated requirement summaries for merit badges.
2//!
3//! This module loads pre-generated 40-character summaries for merit badge
4//! requirements, providing concise descriptions that fit in the TUI display.
5
6use std::collections::HashMap;
7use std::sync::OnceLock;
8
9use serde::Deserialize;
10use tracing::debug;
11
12/// Global summaries cache, loaded once at startup
13static SUMMARIES: OnceLock<SummaryData> = OnceLock::new();
14
15#[derive(Debug, Deserialize, Default)]
16struct SummaryFile {
17    #[serde(default)]
18    version: String,
19    #[serde(default)]
20    generated: String,
21    #[serde(default)]
22    summaries: HashMap<String, String>,
23}
24
25#[derive(Debug, Default)]
26struct SummaryData {
27    summaries: HashMap<String, String>,
28}
29
30/// Initialize the summaries from the embedded JSON file.
31/// Call this once at app startup.
32pub fn init() {
33    let _ = SUMMARIES.get_or_init(|| {
34        load_summaries().unwrap_or_default()
35    });
36}
37
38fn load_summaries() -> Option<SummaryData> {
39    // Try to load from disk at runtime
40    let paths = [
41        "data/requirement_summaries.json",
42        "./data/requirement_summaries.json",
43        "../data/requirement_summaries.json",
44    ];
45
46    for path in paths {
47        if let Ok(data) = std::fs::read_to_string(path) {
48            if let Ok(file) = serde_json::from_str::<SummaryFile>(&data) {
49                debug!(
50                    path = %path,
51                    version = %file.version,
52                    generated = %file.generated,
53                    count = file.summaries.len(),
54                    "Loaded summaries from disk"
55                );
56                return Some(SummaryData {
57                    summaries: file.summaries,
58                });
59            }
60        }
61    }
62
63    debug!("No summaries file found, using original requirement text");
64    None
65}
66
67/// Get a summary for a requirement text.
68/// Returns the AI-generated summary if available, otherwise returns None.
69pub fn get_summary(original_text: &str) -> Option<&'static str> {
70    SUMMARIES
71        .get()
72        .and_then(|data| data.summaries.get(original_text))
73        .map(|s| s.as_str())
74}