1use anyhow::Result;
2use fuzzy_matcher::skim::SkimMatcherV2;
3use fuzzy_matcher::FuzzyMatcher;
4use sha2::{Digest, Sha256};
5
6pub struct Prompts {
7 storage: Box<dyn crate::storage::Storage + Send + Sync>,
8}
9
10impl Prompts {
11 pub fn new(storage: Box<dyn crate::storage::Storage + Send + Sync>) -> Self {
12 Self { storage }
13 }
14
15 pub async fn add_prompt(&self, prompt: &mut crate::storage::Prompt) -> Result<bool> {
16 let prompts = self.storage.load_prompts().await?;
17 if prompts.iter().any(|p| p.hash == prompt.hash) {
18 return Ok(false);
19 }
20 self.storage.save_prompt(prompt).await?;
21 Ok(true)
22 }
23
24 pub async fn list_prompts(&self, tags: Option<Vec<String>>) -> Result<Vec<crate::storage::Prompt>> {
25 let prompts = self.storage.load_prompts().await?;
26 if let Some(tags) = tags {
27 let search_results = search_prompts(&prompts, "", &tags, &[]);
28 Ok(search_results)
29 } else {
30 Ok(prompts)
31 }
32 }
33
34 pub async fn show_prompt(&self, query: &str, tags: Option<Vec<String>>) -> Result<Vec<crate::storage::Prompt>> {
35 let prompts = self.storage.load_prompts().await?;
36 let search_results = search_prompts(&prompts, query, &tags.unwrap_or_default(), &[]);
37 Ok(search_results)
38 }
39
40 pub async fn edit_prompt(
41 &self,
42 hash: &str,
43 new_text: Option<String>,
44 add_tags: Option<Vec<String>>,
45 remove_tags: Option<Vec<String>>,
46 add_categories: Option<Vec<String>>,
47 remove_categories: Option<Vec<String>>,
48 ) -> Result<()> {
49 let mut prompts = self.storage.load_prompts().await?;
50 let prompt_to_edit = prompts.iter_mut().find(|p| p.hash == hash);
51
52 if let Some(prompt) = prompt_to_edit {
53 if let Some(text) = new_text {
54 prompt.content = text;
55 let hash = Sha256::digest(prompt.content.as_bytes());
56 prompt.hash = format!("{:x}", hash);
57 }
58
59 let mut tags = prompt.tags.clone().unwrap_or_default();
60 if let Some(tags_to_add) = add_tags {
61 tags.extend(tags_to_add);
62 tags.sort();
63 tags.dedup();
64 }
65 if let Some(tags_to_remove) = remove_tags {
66 tags.retain(|t| !tags_to_remove.contains(t));
67 }
68 prompt.tags = Some(tags);
69
70 let mut categories = prompt.categories.clone().unwrap_or_default();
71 if let Some(categories_to_add) = add_categories {
72 categories.extend(categories_to_add);
73 categories.sort();
74 categories.dedup();
75 }
76 if let Some(categories_to_remove) = remove_categories {
77 categories.retain(|c| !categories_to_remove.contains(c));
78 }
79 prompt.categories = Some(categories);
80
81 self.storage.delete_prompt(hash).await?;
82 self.storage.save_prompt(prompt).await?;
83 }
84
85 Ok(())
86 }
87
88 pub async fn delete_prompt(&self, hash: &str) -> Result<()> {
89 self.storage.delete_prompt(hash).await
90 }
91}
92
93pub fn search_prompts(prompts: &[crate::storage::Prompt], query: &str, tags: &[String], categories: &[String]) -> Vec<crate::storage::Prompt> {
94 let matcher = SkimMatcherV2::default();
95 prompts.iter().filter(|p| {
96 let content_match = query.is_empty() || matcher.fuzzy_match(&p.content, query).is_some();
97 let tags_match = tags.is_empty() || p.tags.as_ref().map_or(false, |ptags| {
98 tags.iter().all(|tag| ptags.contains(tag))
99 });
100 let categories_match = categories.is_empty() || p.categories.as_ref().map_or(false, |pcats| {
101 categories.iter().all(|cat| pcats.contains(cat))
102 });
103 content_match && tags_match && categories_match
104 }).cloned().collect()
105}