Skip to content

Add conversation export to the V2 UI - #1395

Merged
Paul Lizer (paullizer) merged 2 commits into
paullizer-react-v2-uifrom
paullizer-conversation-export-v2
Sep 3, 2026
Merged

Add conversation export to the V2 UI#1395
Paul Lizer (paullizer) merged 2 commits into
paullizer-react-v2-uifrom
paullizer-conversation-export-v2

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Adds whole-conversation export to the V2 (React) interface.

Important

This PR targets paullizer-react-v2-ui, not Development. The V2 UI only exists on that branch.

Why

The V2 interface could export a single message as Word, PowerPoint or an email draft, but had no way to export a conversation. Anyone who needed a transcript had to switch back to the classic interface.

What

A stepped export wizard — Select -> Format -> Packaging -> Summary -> Download — mirroring the classic one, so both interfaces can be described by one set of instructions.

  • Formats: JSON, Markdown, PDF
  • Packaging: single file, or a ZIP with one file per conversation (defaults to ZIP for more than one)
  • Optional AI intro summary, using any chat model
  • Bulk export: a new Select mode in the conversation rail with checkboxes, All/Clear and a live count
  • Three entry points: the conversation ... menu, the selection toolbar, and the conversation details dialog

No backend changes. The wizard drives POST /api/conversations/export and POST /api/conversations/export/visual-scan, which already served the classic wizard, so both interfaces produce identical files down to the filename.

Notable implementation detail

The Mermaid runtime moves out of MermaidDiagram.tsx into lib/mermaidRuntime.ts. A conversation export covers messages that were never rendered, so there is no mounted component to read an SVG from. Both callers now share one hardened runtime — the same separation the classic interface already makes with chat-mermaid-runtime.js.

Diagrams are rasterized in the browser rather than server-side, which avoids launching headless Chromium once per export.

Bugs fixed along the way

  1. Rasterized diagrams were squashed to ~100px wide. Mermaid emits width="100%", which parseFloat read as the number 100, so a 1094x541 diagram came out 100x541. This was also silently affecting the existing per-diagram PNG download in the chat.
  2. The wizard was trapped inside the 280px sidebar. The rail sits inside a .glass element, and a non-none backdrop-filter makes an element a containing block for fixed-position descendants, so fixed inset-0 resolved to the sidebar rather than the viewport. Now portalled to document.body. This affected 2 of the 3 entry points.
  3. Only 20 of 60 diagrams were rasterized. The per-message cap was reused; the conversation route's budget is 60, so diagrams 21-60 were fetched and then discarded, forcing server-side rendering.

Findings 2 and 3 came out of an independent code review of this change.

Security

Extracting the runtime relocated the DOMPurify boundary and mermaid hardening (securityLevel: 'strict', htmlLabels: false, suppressErrorRendering: true). Three security tests were retargeted rather than weakened, and tightened:

  • They now also assert that nothing else drives mermaid directly, so the hardening cannot be bypassed by a new caller.
  • A new library-sink scan covers DOM insertion from lib/**/*.ts, which was previously invisible — the sink scan only looked at components/**/*.tsx.

Rebased onto the latest base

Merged origin/paullizer-react-v2-ui after #1394 (shared conversations) and #1396 (admin Appearance parity) landed. Three conflicts, all resolved by combining rather than choosing:

  • ConversationRail.tsx — shared conversations and export both changed the row. Share, Leave and Export now sit in the same menu, and the row keeps isSelected alongside shareable/shared.
  • release_notes.md — both sides added a top section; both kept, in descending version order.
  • config.py0.261.038 was taken by the shared-conversations work, so this renumbers to 0.261.040.

Validation

  • V2 functional suite: 34/34 (includes the newly merged shared-conversations and admin tests)
  • test_v2_conversation_export.py: 15/15, including 33 TypeScript behaviour checks
  • tsc -b clean; npm run build succeeds
  • Docs coverage and site-quality tests pass

Docs and version

  • New docs/explanation/features/V2_CONVERSATION_EXPORT.md
  • REACT_V2_UI.md updated (new section; removed the now-wrong "conversation export ... not wired" limitation; corrected a stale bundle-size figure)
  • Release notes entry added
  • Version 0.261.039 -> 0.261.040

The V2 interface could export a single message as Word, PowerPoint or an
email draft, but had no way to export a conversation. Anyone needing a
transcript had to switch back to the classic interface.

Adds a stepped export wizard (review, format, packaging, intro summary,
download) for JSON, Markdown and PDF, as a single file or a ZIP, reachable
from a conversation's rail menu, the new rail selection mode for bulk
exports, and the conversation details dialog.

No backend change: the wizard drives POST /api/conversations/export and
/api/conversations/export/visual-scan, which already served the classic
wizard, so both interfaces produce identical files.

The Mermaid runtime moves out of MermaidDiagram.tsx into lib/mermaidRuntime.ts.
An export covers conversations that were never rendered, so there is no
mounted component to read an SVG from. Both callers now share one hardened
runtime, mirroring the classic chat-mermaid-runtime.js split; the security
tests follow the boundary to its new home and additionally assert that
nothing else drives mermaid directly.

Also fixes two bugs found on the way:

- svgRaster read width="100%" as the number 100, rasterizing a 1094x541
  diagram into a 100px sliver. This also affected the existing per-diagram
  PNG download.
- The wizard is portalled to document.body. The rail sits inside a .glass
  element, and a backdrop-filter makes an element a containing block for
  fixed-position descendants, which trapped the dialog in the 280px sidebar.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Conflicts resolved:

- config.py: 0.261.038 was taken by the shared-conversations work that
  landed meanwhile, so the export feature renumbers to 0.261.040.
- ConversationRail.tsx: shared conversations and conversation export both
  changed the row. Combined rather than chosen -- the icon import keeps
  Download/X alongside LogOut/Users, and the row keeps isSelected alongside
  shareable/shared, so Share, Leave and Export all sit in the same menu.
- release_notes.md: both sides added a section at the top. Kept both, with
  the export section renumbered so versions stay in descending order.

The selection-mode row also gains the pin indicator the normal row has, so a
pinned conversation is still recognisable while picking conversations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) merged commit 58fc4a6 into paullizer-react-v2-ui Sep 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant