feat(chat): save a single answer as PDF via the print dialog

Hides app chrome via a 'printing-answer' class on body, leaves only the
.print-target visible. The class is removed in a finally so a cancelled
print dialog can't leave the app invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-08-30 13:09:06 +02:00
parent 7d066f37dc
commit e528589e9c
2 changed files with 50 additions and 1 deletions

View file

@ -32,6 +32,21 @@ export function ChatView() {
const bottomRef = React.useRef<HTMLDivElement>(null);
const inputRef = React.useRef<HTMLTextAreaElement>(null);
/** Print one answer. The class is removed in a finally so a cancelled
print dialog cannot leave the app invisible. */
const printAnswer = React.useCallback((index: number) => {
const el = document.getElementById(`answer-${index}`);
if (!el) return;
el.classList.add("print-target");
document.body.classList.add("printing-answer");
try {
window.print();
} finally {
document.body.classList.remove("printing-answer");
el.classList.remove("print-target");
}
}, []);
// Pending photos attached to the next user turn. Declared before any
// handler that reads it; useCallback closes over this state at the
// time the handler is created, so the state must be in scope by then.
@ -337,7 +352,7 @@ export function ChatView() {
}
return (
<div key={i} className="border-l-2 border-[#F2B165]/40 pl-4">
<div key={i} id={`answer-${i}`} className="border-l-2 border-[#F2B165]/40 pl-4">
{t.text ? (
<AnswerMarkdown>{t.text}</AnswerMarkdown>
) : (
@ -364,6 +379,12 @@ export function ChatView() {
{toolLabel(name)}
</span>
))}
<button
onClick={() => printAnswer(i)}
className="print-hide ml-auto rounded-full border border-border px-2 py-0.5 text-[11px] text-muted-foreground transition-colors hover:border-[#F2B165]/50 hover:text-foreground"
>
Save as PDF
</button>
</div>
)}
</div>

View file

@ -216,3 +216,31 @@
background-size: 32px 32px;
}
}
/* Printing an answer: hide the app chrome and let the answer fill the page. Triggered by adding .printing-answer to <body>. */
@media print {
body.printing-answer * {
visibility: hidden;
}
body.printing-answer .print-target,
body.printing-answer .print-target * {
visibility: visible;
}
body.printing-answer .print-target {
position: absolute;
inset: 0;
padding: 0;
border: 0;
max-width: none;
}
body.printing-answer .print-hide {
display: none !important;
}
/* Keep table rows intact across page breaks. */
body.printing-answer table {
page-break-inside: auto;
}
body.printing-answer tr {
page-break-inside: avoid;
}
}