"""Conversation export helpers kept separate from Flask request handling.""" import os import re from typing import Iterable, Mapping def _unicode_font(base_dir: str) -> str: candidates = [ os.path.join(base_dir, "fonts", "NotoSans-Regular.ttf"), "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", r"C:\Windows\Fonts\arial.ttf", ] return next((candidate for candidate in candidates if os.path.exists(candidate)), "") def _core_font_text(value) -> str: text = str(value or "") replacements = { "→": " -> ", "⇒": " => ", "—": " - ", "–": " - ", "‘": "'", "’": "'", "“": '"', "”": '"', "…": "...", "•": "-", "✓": "[done]", "✗": "[failed]", } for source, replacement in replacements.items(): text = text.replace(source, replacement) return text.encode("latin-1", "replace").decode("latin-1") def _readable_export_text(value) -> str: text = str(value or "") text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE) text = text.replace("**", "").replace("__", "") text = re.sub(r"^- \[ \]\s+", "[ ] ", text, flags=re.MULTILINE) text = re.sub(r"^- \[[xX]\]\s+", "[done] ", text, flags=re.MULTILINE) return text def create_conversation_pdf(messages: Iterable[Mapping[str, str]], output_path: str, base_dir: str) -> str: """Create a paginated export without silently truncating normal conversation turns.""" from fpdf import FPDF class ConversationPDF(FPDF): def header(self): if self.page_no() <= 1: return self.set_font("Helvetica", "", 8) self.set_text_color(90, 100, 115) self.cell(0, 6, "Invicta AI Conversation Export", align="R") self.ln(10) self.set_text_color(0, 0, 0) def footer(self): self.set_y(-12) self.set_font("Helvetica", "", 8) self.set_text_color(110, 120, 132) self.cell(0, 6, f"Page {self.page_no()}/{{nb}}", align="C") pdf = ConversationPDF() pdf.alias_nb_pages() pdf.set_title("Invicta AI Conversation Export") pdf.set_author("InvictaTill AI") pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() font_path = _unicode_font(base_dir) if font_path: pdf.add_font("InvictaUnicode", "", font_path) pdf.set_font("InvictaUnicode", "", 16) else: pdf.set_font("Helvetica", "B", 16) pdf.cell(0, 10, "Invicta AI Conversation Export", new_x="LMARGIN", new_y="NEXT", align="C") pdf.ln(5) for message in messages: role = str(message.get("role", "user")).upper() content = _readable_export_text(str(message.get("content", ""))[:20_000]) if not font_path: role = _core_font_text(role) content = _core_font_text(content) pdf.set_font("InvictaUnicode" if font_path else "Helvetica", "" if font_path else "B", 11) pdf.multi_cell(0, 6, f"[{role}]", new_x="LMARGIN", new_y="NEXT") pdf.set_font("InvictaUnicode" if font_path else "Helvetica", "", 10) pdf.multi_cell(0, 5.5, content, new_x="LMARGIN", new_y="NEXT") pdf.ln(2) pdf.output(output_path) return output_path