use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use crate::Storage;
#[derive(Debug, Clone)]
pub struct Bank {
pub name: String,
pub display_name: String,
pub description: String,
pub author: String,
pub version: String,
pub tags: Vec<String>,
pub prompts: Vec<Prompt>,
pub bank_type: BankType,
pub is_expanded: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BankType {
Local, LocalBank, Registry, GitHub, }
#[derive(Debug, Clone)]
pub struct Prompt {
pub name: String,
pub description: String,
pub content: String,
pub bank_name: Option<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub tags: Vec<String>,
pub is_favorite: bool,
pub usage_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankMetadata {
pub name: String,
pub description: String,
pub author: String,
pub version: String,
pub tags: Vec<String>,
pub prompts: Vec<PromptMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptMetadata {
pub name: String,
pub description: String,
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum TreeItem {
Bank { bank: Bank, depth: usize },
Prompt { prompt: Prompt, depth: usize },
}
impl TreeItem {
pub fn depth(&self) -> usize {
match self {
TreeItem::Bank { depth, .. } => *depth,
TreeItem::Prompt { depth, .. } => *depth,
}
}
pub fn name(&self) -> &str {
match self {
TreeItem::Bank { bank, .. } => &bank.name,
TreeItem::Prompt { prompt, .. } => &prompt.name,
}
}
pub fn description(&self) -> &str {
match self {
TreeItem::Bank { bank, .. } => &bank.description,
TreeItem::Prompt { prompt, .. } => &prompt.description,
}
}
pub fn is_bank(&self) -> bool {
matches!(self, TreeItem::Bank { .. })
}
pub fn is_prompt(&self) -> bool {
matches!(self, TreeItem::Prompt { .. })
}
}
impl Bank {
pub fn load_all_banks(storage: &Storage) -> Result<(Vec<Bank>, Vec<Prompt>)> {
let mut banks = Vec::new();
let local_prompts = Self::load_local_prompts(storage)?;
let banks_dir = storage.base_dir().join("banks");
if banks_dir.exists() {
if let Ok(entries) = fs::read_dir(&banks_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(bank_name) = path.file_name().and_then(|n| n.to_str()) {
if let Ok(bank) = Self::load_bank(storage, bank_name) {
banks.push(bank);
}
}
}
}
}
}
banks.sort_by(|a, b| a.name.cmp(&b.name));
Ok((banks, local_prompts))
}
pub fn load_bank(storage: &Storage, bank_name: &str) -> Result<Bank> {
let bank_dir = storage.base_dir().join("banks").join(bank_name);
let bank_yaml_path = bank_dir.join("bank.yaml");
let metadata = if bank_yaml_path.exists() {
let yaml_content = fs::read_to_string(&bank_yaml_path)?;
serde_yaml::from_str::<BankMetadata>(&yaml_content).unwrap_or_else(|_| {
BankMetadata {
name: bank_name.to_string(),
description: format!("Bank: {}", bank_name),
author: "Unknown".to_string(),
version: "1.0.0".to_string(),
tags: vec![],
prompts: vec![],
}
})
} else {
BankMetadata {
name: bank_name.to_string(),
description: format!("Bank: {}", bank_name),
author: "Local".to_string(),
version: "1.0.0".to_string(),
tags: vec!["local".to_string()],
prompts: vec![],
}
};
let prompts = Self::load_bank_prompts(storage, bank_name)?;
let bank_type = if bank_name.starts_with('@') {
BankType::Registry
} else if bank_name.contains('/') {
BankType::GitHub
} else {
BankType::LocalBank
};
Ok(Bank {
name: bank_name.to_string(),
display_name: metadata.name,
description: metadata.description,
author: metadata.author,
version: metadata.version,
tags: metadata.tags,
prompts,
bank_type,
is_expanded: false,
})
}
fn load_bank_prompts(storage: &Storage, bank_name: &str) -> Result<Vec<Prompt>> {
let mut prompts = Vec::new();
let bank_dir = storage.base_dir().join("banks").join(bank_name);
fn load_prompts_recursive(
storage: &Storage,
bank_name: &str,
dir_path: &std::path::Path,
relative_path: &str,
prompts: &mut Vec<Prompt>,
) -> Result<()> {
if let Ok(entries) = fs::read_dir(dir_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(subdir_name) = path.file_name().and_then(|s| s.to_str()) {
let new_relative_path = if relative_path.is_empty() {
subdir_name.to_string()
} else {
format!("{}/{}", relative_path, subdir_name)
};
let _ = load_prompts_recursive(
storage,
bank_name,
&path,
&new_relative_path,
prompts,
);
}
} else if path.extension().and_then(|s| s.to_str()) == Some("md") {
if let Some(prompt_name) = path.file_stem().and_then(|s| s.to_str()) {
if prompt_name.eq_ignore_ascii_case("readme") {
continue;
}
let display_name = if relative_path.is_empty() {
prompt_name.to_string()
} else {
format!("{}/{}", relative_path, prompt_name)
};
let full_prompt_name = if relative_path.is_empty() {
format!("{}/{}", bank_name, prompt_name)
} else {
format!("{}/{}", bank_name, display_name)
};
if let Ok((metadata, content)) = storage.read_prompt(&full_prompt_name)
{
prompts.push(Prompt {
name: display_name,
description: metadata.description,
content,
bank_name: Some(bank_name.to_string()),
created_at: metadata.created_at,
updated_at: metadata.updated_at,
tags: metadata.tags.unwrap_or_default(),
is_favorite: false, usage_count: 0, });
}
}
}
}
}
Ok(())
}
load_prompts_recursive(storage, bank_name, &bank_dir, "", &mut prompts)?;
prompts.sort_by(|a, b| a.name.cmp(&b.name));
Ok(prompts)
}
fn load_local_prompts(storage: &Storage) -> Result<Vec<Prompt>> {
let mut prompts = Vec::new();
let prompts_dir = storage.prompts_dir();
if prompts_dir.exists() {
if let Ok(entries) = fs::read_dir(&prompts_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("md") {
if let Some(prompt_name) = path.file_stem().and_then(|s| s.to_str()) {
if let Ok((metadata, content)) = storage.read_prompt(prompt_name) {
prompts.push(Prompt {
name: prompt_name.to_string(),
description: metadata.description,
content,
bank_name: None,
created_at: metadata.created_at,
updated_at: metadata.updated_at,
tags: metadata.tags.unwrap_or_default(),
is_favorite: false,
usage_count: 0,
});
}
}
}
}
}
}
prompts.sort_by(|a, b| a.name.cmp(&b.name));
Ok(prompts)
}
pub fn icon(&self) -> &'static str {
match self.bank_type {
BankType::Local => "📁",
BankType::LocalBank => "📁",
BankType::Registry => "🌐",
BankType::GitHub => "🐙",
}
}
pub fn display_name_with_count(&self) -> String {
format!("{} ({})", self.display_name, self.prompts.len())
}
pub fn toggle_expanded(&mut self) {
self.is_expanded = !self.is_expanded;
}
}
impl Prompt {
pub fn full_name(&self) -> String {
if let Some(bank) = &self.bank_name {
format!("{}/{}", bank, self.name)
} else {
self.name.clone()
}
}
pub fn display_name(&self) -> String {
self.name.clone()
}
}