use crate::config::{CommitFormat, Language}; use crate::config::manager::ConfigManager; use crate::git::{CommitInfo, GitRepo}; use crate::llm::{GeneratedCommit, LlmClient}; use anyhow::{Context, Result}; /// Content generator using LLM pub struct ContentGenerator { llm_client: LlmClient, } impl ContentGenerator { /// Create new content generator pub async fn new(manager: &ConfigManager) -> Result { let llm_client = LlmClient::from_config(manager).await?; if !llm_client.is_available().await { anyhow::bail!("LLM provider '{}' is not available", manager.llm_provider()); } Ok(Self { llm_client }) } /// Generate commit message from diff pub async fn generate_commit_message( &self, diff: &str, format: CommitFormat, language: Language, ) -> Result { // Truncate diff if too long let max_diff_len = 4000; let truncated_diff = if diff.len() > max_diff_len { let boundary = diff.floor_char_boundary(max_diff_len); format!("{}\n... (truncated)", &diff[..boundary]) } else { diff.to_string() }; self.llm_client.generate_commit_message(&truncated_diff, format, language).await } /// Generate commit message from repository changes pub async fn generate_commit_from_repo( &self, repo: &GitRepo, format: CommitFormat, language: Language, ) -> Result { let diff = repo.get_staged_diff_sorted() .context("Failed to get staged diff")?; if diff.is_empty() { anyhow::bail!("No staged changes to generate commit from"); } self.generate_commit_message(&diff, format, language).await } /// Generate tag message pub async fn generate_tag_message( &self, version: &str, commits: &[CommitInfo], language: Language, ) -> Result { let commit_messages: Vec = commits .iter() .map(|c| c.subject().to_string()) .collect(); self.llm_client.generate_tag_message(version, &commit_messages, language).await } /// Generate changelog entry pub async fn generate_changelog_entry( &self, version: &str, commits: &[CommitInfo], language: Language, ) -> Result { let typed_commits: Vec<(String, String)> = commits .iter() .map(|c| { let commit_type = c.commit_type().unwrap_or_else(|| "other".to_string()); (commit_type, c.subject().to_string()) }) .collect(); self.llm_client.generate_changelog_entry(version, &typed_commits, language).await } /// Generate changelog from repository pub async fn generate_changelog_from_repo( &self, repo: &GitRepo, version: &str, from_tag: Option<&str>, language: Language, ) -> Result { let commits = if let Some(tag) = from_tag { repo.get_commits_between(tag, "HEAD")? } else { repo.get_commits(50)? }; self.generate_changelog_entry(version, &commits, language).await } /// Interactive commit generation with user feedback pub async fn generate_commit_interactive( &self, repo: &GitRepo, format: CommitFormat, language: Language, ) -> Result { use dialoguer::Select; let diff = repo.get_staged_diff_sorted()?; if diff.is_empty() { anyhow::bail!("No staged changes"); } // Show diff summary let files = repo.get_staged_files()?; println!("\nStaged files ({}):", files.len()); for file in &files { println!(" • {}", file); } // Generate initial commit println!("\nGenerating commit message..."); let mut generated = self.generate_commit_message(&diff, format, language).await?; loop { println!("\n{}", "─".repeat(60)); println!("Generated commit message:"); println!("{}", "─".repeat(60)); println!("{}", generated.to_conventional()); println!("{}", "─".repeat(60)); let options = vec![ "✓ Accept and commit", "🔄 Regenerate", "✏️ Edit", "❌ Cancel", ]; let selection = Select::new() .with_prompt("What would you like to do?") .items(&options) .default(0) .interact()?; match selection { 0 => return Ok(generated), 1 => { println!("Regenerating..."); generated = self.generate_commit_message(&diff, format, language).await?; } 2 => { let edited = crate::utils::editor::edit_content(&generated.to_conventional())?; generated = self.parse_edited_commit(&edited, format)?; } 3 => anyhow::bail!("Cancelled by user"), _ => {} } } } fn parse_edited_commit(&self, edited: &str, _format: CommitFormat) -> Result { let parsed = crate::git::commit::parse_commit_message(edited); Ok(GeneratedCommit { commit_type: parsed.commit_type.unwrap_or_else(|| "chore".to_string()), scope: parsed.scope, description: parsed.description.unwrap_or_else(|| "update".to_string()), body: parsed.body, footer: parsed.footer, breaking: parsed.breaking, }) } } /// Fallback generators when LLM is not available pub mod fallback { use crate::git::commit::create_date_commit_message; /// Generate simple commit message without LLM pub fn generate_simple_commit(files: &[String]) -> String { if files.len() == 1 { format!("chore: update {}", files[0]) } else if files.len() <= 3 { format!("chore: update {}", files.join(", ")) } else { format!("chore: update {} files", files.len()) } } /// Generate date-based commit pub fn generate_date_commit() -> String { create_date_commit_message(None) } /// Generate commit based on file types pub fn generate_by_file_types(files: &[String]) -> String { let has_code = files.iter().any(|f| { f.ends_with(".rs") || f.ends_with(".py") || f.ends_with(".js") || f.ends_with(".ts") }); let has_docs = files.iter().any(|f| f.ends_with(".md") || f.contains("README")); let has_tests = files.iter().any(|f| f.contains("test") || f.contains("spec")); if has_tests { "test: update tests".to_string() } else if has_docs { "docs: update documentation".to_string() } else if has_code { "refactor: update code".to_string() } else { "chore: update files".to_string() } } }