use super::{PresetMatch, TemplateDiscovery};
use crate::template::PresetInfo;
use std::collections::BTreeMap;
use strsim::normalized_damerau_levenshtein;
pub struct KeywordDiscovery {
presets: BTreeMap<String, PresetInfo>,
}
impl KeywordDiscovery {
pub fn new(presets: BTreeMap<String, PresetInfo>) -> Self {
Self { presets }
}
fn score_preset(&self, preset: &PresetInfo, query_lower: &str) -> f64 {
let mut best: f64 = 0.0;
let name_score = normalized_damerau_levenshtein(&preset.name.to_lowercase(), query_lower);
best = best.max(name_score * 1.5);
let title_score = normalized_damerau_levenshtein(&preset.title.to_lowercase(), query_lower);
best = best.max(title_score * 1.2);
if preset.description.to_lowercase().contains(query_lower) {
best = best.max(0.8);
}
for tag in &preset.tags {
let tag_score = normalized_damerau_levenshtein(&tag.to_lowercase(), query_lower);
best = best.max(tag_score);
}
for use_case in &preset.use_cases {
if use_case.to_lowercase().contains(query_lower) {
best = best.max(0.9);
}
}
best
}
}
impl TemplateDiscovery for KeywordDiscovery {
fn find_presets(&self, query: &str) -> Vec<PresetMatch> {
let query_lower = query.to_lowercase();
let mut matches: Vec<PresetMatch> = self
.presets
.values()
.map(|p| PresetMatch {
name: p.name.clone(),
score: self.score_preset(p, &query_lower),
})
.filter(|m| m.score > 0.3)
.collect();
matches.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
matches.truncate(10);
matches
}
fn get_preset(&self, name: &str) -> Option<&PresetInfo> {
self.presets.get(name)
}
fn list_all(&self) -> Vec<&PresetInfo> {
self.presets.values().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn preset(name: &str, tags: &[&str]) -> PresetInfo {
PresetInfo {
name: name.to_string(),
category: "test".to_string(),
title: name.to_string(),
description: format!("Test preset {}", name),
tags: tags.iter().map(|s| s.to_string()).collect(),
use_cases: vec![],
example: Value::Null,
schema: Value::Null,
template_source: String::new(),
}
}
#[test]
fn test_find_by_name() {
let mut presets = BTreeMap::new();
presets.insert("menu-card".to_string(), preset("menu-card", &[]));
presets.insert("leave-request".to_string(), preset("leave-request", &[]));
let discovery = KeywordDiscovery::new(presets);
let matches = discovery.find_presets("menu");
assert!(!matches.is_empty());
assert_eq!(matches[0].name, "menu-card");
}
#[test]
fn test_find_by_tag() {
let mut presets = BTreeMap::new();
presets.insert(
"leave-request".to_string(),
preset("leave-request", &["cuti", "izin"]),
);
let discovery = KeywordDiscovery::new(presets);
let matches = discovery.find_presets("cuti");
assert!(!matches.is_empty());
assert_eq!(matches[0].name, "leave-request");
}
#[test]
fn test_get_preset() {
let mut presets = BTreeMap::new();
presets.insert("menu-card".to_string(), preset("menu-card", &[]));
let discovery = KeywordDiscovery::new(presets);
assert!(discovery.get_preset("menu-card").is_some());
assert!(discovery.get_preset("nonexistent").is_none());
}
}