use anyhow::Result;
use std::fmt::Write as _;
pub fn assemble_report(report_body: &str, citations: &[(String, String, String)]) -> String {
if citations.is_empty() {
return report_body.to_string();
}
let mut out = report_body.trim_end().to_string();
out.push_str("\n\n## Sources\n\n");
for (i, (_, url, title)) in citations.iter().enumerate() {
if title.is_empty() {
let _ = writeln!(out, "{}. {url}", i + 1);
} else {
let _ = writeln!(out, "{}. {title} — {url}", i + 1);
}
}
out
}
impl super::App {
pub(crate) fn export_report(&mut self) -> Result<()> {
let Some(session) = &self.session else {
self.status = "no active session".to_string();
return Ok(());
};
let Some(report) = self
.messages
.iter()
.rev()
.find(|m| m.role == "assistant")
.map(|m| m.content.clone())
else {
self.status = "nothing to export — no assistant reply yet".to_string();
return Ok(());
};
let citations = self.db.search_citations(&self.active_space.id, None)?;
let cited_here: Vec<(String, String, String)> = {
let urls_in_report: std::collections::HashSet<String> =
crate::citations::parse_citations(&report)
.into_iter()
.map(|(_, url)| url)
.collect();
citations
.into_iter()
.filter(|(_, url, _)| urls_in_report.contains(url))
.collect()
};
let assembled = assemble_report(&report, &cited_here);
let dir = self
.space
.files_dir(&self.active_space.name)
.join("reports");
std::fs::create_dir_all(&dir)?;
let slug = super::sessions::slugify(&session.title);
let path = dir.join(format!("{slug}.md"));
std::fs::write(&path, assembled)?;
self.status = format!("exported to {}", path.display());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assemble_report_appends_numbered_bibliography() {
let citations = vec![
(
"report-a.md".to_string(),
"https://a.example".to_string(),
"Title A".to_string(),
),
(
"report-a.md".to_string(),
"https://b.example".to_string(),
"".to_string(),
),
];
let out = assemble_report("# Report\nBody [1] [2].", &citations);
assert!(out.contains("## Sources"));
assert!(out.contains("1. Title A — https://a.example"));
assert!(out.contains("https://b.example"));
assert!(out.starts_with("# Report"));
}
#[test]
fn assemble_report_with_no_citations_has_no_sources_section() {
let out = assemble_report("# Report\nBody.", &[]);
assert!(!out.contains("## Sources"));
}
}