import Foundation
/// Renders a session (`speakers.json` + recap) into human-readable artifacts:
/// `transcript.md` (portable/editable) and `recap.html` (self-contained, dark
/// theme, printable, shareable). Port of recap-relay's meetingToMarkdown/Html,
/// driven by our already-named transcript.
enum RecapRenderer {
static func write(file: SpeakersFile, result: RecapResult, title: String, to folder: URL) throws {
let entries = RecapAnalyzer.entries(from: file)
try markdown(file: file, result: result, title: title, entries: entries)
.data(using: .utf8)?.write(to: folder.appendingPathComponent("transcript.md"))
try html(file: file, result: result, title: title, entries: entries)
.data(using: .utf8)?.write(to: folder.appendingPathComponent("recap.html"))
}
// MARK: - Markdown
static func markdown(file: SpeakersFile, result: RecapResult, title: String,
entries: [RecapAnalyzer.Entry]) -> String {
var out = "# \(title)\n\n"
let speakers = RecapAnalyzer.orderedSpeakerNames(entries)
out += "*\(file.app) · \(RecapAnalyzer.mmss(file.durationSec))"
if !speakers.isEmpty { out += " · \(speakers.count) speaker\(speakers.count == 1 ? "" : "s"): \(speakers.joined(separator: ", "))" }
out += "*\n\n"
if let x = result.extras {
if !x.tldr.isEmpty {
out += "## Summary\n\n\(x.tldr)\n"
if !x.primarySpeakers.isEmpty { out += "\n*Primary speakers: \(x.primarySpeakers.joined(separator: ", "))*\n" }
out += "\n"
}
for section in x.sections where !section.isEmpty {
out += "## \(section.title)\n\n"
switch section.kind {
case .paragraph:
out += "\(section.paragraph)\n\n"
case .bullets:
for b in section.bullets { out += "- \(b)\n" }
out += "\n"
case .items:
for item in section.items {
var line = "- \(item.text)"
if let who = item.who { line += " — **\(who)**" }
if let note = item.note { line += " (\(note))" }
if let when = item.when { line += " *(\(RecapAnalyzer.mmss(Double(when))))*" }
out += line + "\n"
}
out += "\n"
}
}
}
if !result.sections.isEmpty {
out += "## Topics\n\n"
for (i, sec) in result.sections.enumerated() {
let range = timeRange(sec, entries: entries)
out += "### \(i + 1). \(sec.title)\(range)\n\n"
if !sec.summary.isEmpty { out += "\(sec.summary)\n\n" }
out += "Transcript
\n\n"
out += transcriptLines(sec, entries: entries)
out += "\n
\(esc(x.tldr))
" + (x.primarySpeakers.isEmpty ? "" : "Primary: \(x.primarySpeakers.map(esc).joined(separator: ", "))
")) } for section in x.sections where !section.isEmpty { switch section.kind { case .paragraph: cards += card(section.title, "\(esc(section.paragraph))
") case .bullets: cards += card(section.title, "