use std::collections::HashMap;
use std::fmt::Write;
use crate::JobConfig;
use super::types::FileChunkResult;
pub struct MarkdownBuilder {
content: String,
}
impl MarkdownBuilder {
pub fn new() -> Self {
Self {
content: String::with_capacity(16384),
}
}
pub fn heading(&mut self, level: u8, text: &str) -> &mut Self {
let prefix = "#".repeat(level as usize);
writeln!(self.content, "{} {}\n", prefix, text).unwrap();
self
}
pub fn paragraph(&mut self, text: &str) -> &mut Self {
writeln!(self.content, "{}\n", text).unwrap();
self
}
#[allow(dead_code)]
pub fn code_block(&mut self, text: &str) -> &mut Self {
writeln!(self.content, "```\n{}\n```\n", text).unwrap();
self
}
pub fn metadata(&mut self, key: &str, value: &str) -> &mut Self {
writeln!(self.content, "**{}:** {}\n", key, value).unwrap();
self
}
pub fn horizontal_rule(&mut self) -> &mut Self {
writeln!(self.content, "---\n").unwrap();
self
}
pub fn emoji_heading(&mut self, level: u8, emoji: &str, text: &str) -> &mut Self {
let prefix = "#".repeat(level as usize);
writeln!(self.content, "{} {} {}\n", prefix, emoji, text).unwrap();
self
}
pub fn build(self) -> String {
self.content
}
}
impl Default for MarkdownBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct MarkdownFormatter;
impl MarkdownFormatter {
pub fn format_as_markdown(config: &JobConfig, files_summary: &str,
file_groups: &HashMap<String, Vec<&FileChunkResult>>,
final_summary: &str) -> String {
let mut builder = MarkdownBuilder::new();
builder
.heading(1, "File Analysis Results")
.metadata("Job ID", &config.job_id)
.metadata("Model", &config.model_id)
.paragraph("");
let clean_summary = MarkdownFormatter::clean_processing_summary(files_summary);
builder
.heading(2, "Processing Summary")
.paragraph(&clean_summary)
.horizontal_rule();
MarkdownFormatter::add_file_analyses(&mut builder, file_groups);
if !final_summary.trim().is_empty() {
builder
.emoji_heading(1, "📊", "Final Analysis Summary")
.paragraph(final_summary);
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
builder
.horizontal_rule()
.paragraph(&format!("*Analysis completed: Unix timestamp {}*", timestamp));
builder.build()
}
pub fn add_file_analyses(builder: &mut MarkdownBuilder,
file_groups: &HashMap<String, Vec<&FileChunkResult>>) {
let mut sorted_files: Vec<_> = file_groups.iter().collect();
sorted_files.sort_by_key(|(path, _)| path.as_str());
for (file_path, file_results) in sorted_files {
if file_results.is_empty() {
continue;
}
let file_type = &file_results[0].file_type;
builder
.emoji_heading(2, "📁", &format!("File: `{}`", file_path))
.metadata("Type", file_type)
.metadata("Chunks", &file_results.len().to_string())
.paragraph("");
for result in file_results {
if file_results.len() > 1 {
builder.heading(3, &format!("Chunk {} (Tokens: {})",
result.chunk_id + 1,
result.tokens_used.as_ref().map(|t| t.total_tokens).unwrap_or(0)));
}
let cleaned_output = Self::clean_ai_output(&result.output);
builder.paragraph(&cleaned_output);
}
builder.horizontal_rule();
}
}
pub fn clean_ai_output(output: &str) -> String {
output
.replace("### ", "### ")
.replace("#### ", "#### ")
.replace("=== ", "### ")
.replace("---", "\n---\n")
.trim()
.to_string()
}
pub fn clean_processing_summary(files_summary: &str) -> String {
files_summary
.replace("Processing Summary:", "")
.trim()
.to_string()
}
}