FEAT: Add conversation export button to the GUI chat view#2259
FEAT: Add conversation export button to the GUI chat view#2259varunj-msft wants to merge 1 commit into
Conversation
Add an Export control to the CoPyRIT chat ribbon that downloads the currently displayed conversation as Markdown or JSON. The file is serialized client-side from the in-view messages (WYSIWYG), including the system prompt shown in the banner; no conversation data is sent to the server. - conversationExport.ts: pure Markdown/JSON serializers, filename builder, and Blob-based download helper - ChatWindow.tsx: Export menu button (Markdown / JSON) enabled only for a viewable, settled conversation - Unit tests plus a real-browser Playwright E2E; GUI docs updated
There was a problem hiding this comment.
Pull request overview
Adds a frontend-only “Export conversation” control to the CoPyRIT chat ribbon, enabling users to download the currently displayed conversation as Markdown (readable transcript) or JSON (structured data) entirely client-side.
Changes:
- Added a new
conversationExport.tsutility to serialize conversations (Markdown/JSON), build safe filenames, and trigger Blob-based downloads. - Integrated an Export menu into
ChatWindowwith gating logic so export is only enabled for a stable, viewable conversation. - Added unit tests (serializer + UI) and Playwright E2E coverage for real downloads; updated GUI documentation.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/utils/conversationExport.ts | New export/serialization and download helper utilities for Markdown/JSON exports. |
| frontend/src/utils/conversationExport.test.ts | Unit tests covering serialization, filename building, and download helper behavior. |
| frontend/src/components/Chat/ChatWindow.tsx | Adds export menu button to the ribbon and enables/disables it based on conversation state. |
| frontend/src/components/Chat/ChatWindow.test.tsx | Integration tests for export gating and menu-driven export behavior. |
| frontend/e2e/chat.spec.ts | Playwright tests validating actual downloaded bytes for Markdown and JSON exports. |
| doc/gui/0_gui.md | Documents the new Export button and its behavior/security note. |
| /** | ||
| * Serialize, name, and download the currently viewed conversation in one call. | ||
| * Markdown and JSON share a single timestamp so the filename and the document | ||
| * body agree. | ||
| */ |
| function longestBacktickRun(content: string): number { | ||
| const runs = content.match(/`+/g) | ||
| if (!runs) { | ||
| return 0 | ||
| } | ||
| let longest = 0 | ||
| for (const run of runs) { | ||
| if (run.length > longest) { | ||
| longest = run.length | ||
| } | ||
| } | ||
| return longest | ||
| } |
|
Could you provide some screenshots / video of what the user experience looks like here? :) |
| // not while empty, loading, or mid-send. Read-only / operator-lock / | ||
| // cross-target states do not block export. | ||
| const canExportConversation = | ||
| messages.some((message) => !message.isLoading) && |
There was a problem hiding this comment.
This condition can be satisfied by just a system prompt. I think we probably don't want to just export a system prompt without user/assistant messages right? We might want to have:
const canExportConversation =
messages.some((message) => !message.isLoading && message.role !== 'system') &&
!isSending &&
!isLoadingAttack &&
!isLoadingMessages &&
!awaitingConversationLoad- a test if this is implemented.
| export function conversationToMarkdown( | ||
| messages: Message[], | ||
| conversationId: string | null, | ||
| exportedAt: Date = new Date(), |
There was a problem hiding this comment.
curious, how come conversationToMarkdown takes an 'exportedAt' param but not conversationToJson?
| !awaitingConversationLoad | ||
|
|
||
| const handleExport = (format: ExportFormat) => { | ||
| exportConversation({ messages, conversationId: activeConversationId ?? conversationId, format }) |
There was a problem hiding this comment.
feel free to ignore, but maybe this can just be conversationId: activeConversationId since exportConversation already handles null gracefully and in the weird edge case where there exist messages but not activeConversationId, the messages could be attributed to the wrong conversationId (i.e., the conversation of original conversation in the attack, not the active one)
Description
Adds an Export control to the CoPyRIT chat ribbon that downloads the currently
displayed conversation as Markdown or JSON. Serialization is fully
client-side (from the in-view messages) — no backend changes and no
conversation data leaves the browser.
conversationExport.ts(new): pure Markdown/JSON serializers, timestampedfilename builder, and a Blob-based download helper. Hardened for safety —
dynamically-sized code fences, sanitized filenames, strips non-serializable
Filehandles and in-flight loading placeholders, and revokes the object URL.ChatWindow.tsx: Export menu button (Markdown / JSON) enabled only when aviewable, settled conversation is shown; exports the displayed branch. Uses a
Fluent
Tooltiplabel to match the sibling ribbon buttons.Frontend-only; no Python/API changes.
Tests and Documentation
Tests
(
conversationExport.test.ts).ChatWindowexport menu (gating, format selection,Markdown/JSON output).
actual downloaded Markdown/JSON bytes (
chat.spec.ts).Documentation
doc/gui/0_gui.mdto document the export button and itsbehavior (including the system prompt shown in the banner).
needed.