Add message actions: thumbs up/down feedback, copy, and export (PDF / Word / CSV / XLSX / TXT) #97

Closed
opened 2026-08-02 06:48:45 -07:00 by westfarn · 1 comment
Owner

Problem

Assistant messages have no actions at all. ConversationDetailCard renders a markdown bubble and nothing else — no copy, no rating, no export. Users cannot get an answer out of the app without selecting text by hand, and we have no signal on which answers are good.

That second point matters beyond UX: the accuracy work in chat_backend#62 needs real user judgements to measure against. Thumbs up/down is the data source for that.

Current state

  • No per-message actions. ConversationDetailCard.tsx has no action row. The only message-adjacent control anywhere is conversation deletion in the sidebar (AsyncDashboard2.tsx:467-477).
  • No copy anywhere in chat. The only clipboard use in the app is "Copy details" in AppErrorBoundary.tsx:86-102, for crash info.
  • No export. Nothing downloads a conversation or a message.
  • FeedbackPage2 is unrelated. It posts app-wide feedback to POST /feedbacks/ (FeedbackPage2.tsx:250-258). It is not per-message and should not be reused as-is.
  • CustomCodeBlock / CustomPreBlock exist but are unwiredConversationDetailCard never sets options.overrides on markdown-to-jsx, so code blocks have no copy button either.

Stack context: MUI v5 + styled-components, React Context (no Redux/React Query), axios via src/axiosApi.js, markdown-to-jsx for rendering.

Scope

Four actions on every assistant message. User messages get copy only.


1. Copy

Copy the message as raw markdown (what the model produced), not the rendered DOM text — pasting into another tool should preserve structure.

  • navigator.clipboard.writeText with a document.execCommand('copy') fallback for the Capacitor WebView and non-secure contexts.
  • Icon button flips to a check for ~2s on success; show an error toast on failure.
  • Also wire CustomCodeBlock into ConversationDetailCard's markdown-to-jsx overrides so individual code blocks get their own copy button. The component already exists and is unused.

2. Thumbs up / thumbs down

Needs a backend endpoint — file a companion issue on chat_backend. Required shape:

  • PromptFeedback model: prompt (FK to the assistant Prompt), user, rating (up / down), reason (optional short code), comment (optional free text), created_at, updated_at. Unique on (prompt, user) so a re-vote updates rather than duplicates.
  • POST /api/prompt_feedback upsert, DELETE to clear a vote.
  • Feedback must be joinable to PromptMetric so ratings can be sliced by model — this is what makes it useful for measuring chat_backend#62. Note that PromptMetric.model_name is currently hard-coded to "llama3.2"; #62 fixes that, and without the fix the slice is meaningless.

Frontend behaviour:

  • Two icon buttons, filled when active. Clicking the active one clears the vote.
  • Optimistic update, revert on request failure.
  • On thumbs down, open a lightweight popover with optional reason chips (Incorrect, Out of date, Didn't follow instructions, Unsafe, Other) and a free-text field. Submitting the rating must not be blocked on filling this in — the vote posts immediately, the reason is a follow-up patch.
  • Ratings must be loaded with conversation history (GET conversation_details) so votes persist across reloads.
  • Emit analytics events per ANALYTICS.md.

3. Export

A menu on the message action row plus a conversation-level export in the chat header. Formats: PDF, Word (.docx), CSV, XLSX, TXT.

Both scopes: this message and entire conversation.

Format Library Output
TXT none Plain text; markdown stripped. Conversation export prefixes each turn with role and timestamp.
PDF pdfmake Rendered from the markdown AST — real headings, lists, tables, code blocks. Not html2canvas, which produces unsearchable rasterised text.
DOCX docx Native Word paragraphs, headings, lists, and tables from the same AST.
CSV papaparse See tabular rules below.
XLSX xlsx (SheetJS) See tabular rules below.

Tabular rules (CSV/XLSX). A conversation is not naturally tabular, so:

  • If the exported message contains one or more markdown tables, export those tables — one sheet per table for XLSX, the first table for CSV.
  • Otherwise export turns as rows: timestamp, role, message.
  • For a conversation export, always use the turns-as-rows form.

Common requirements:

  • Filename: sanitised conversation title plus ISO date, e.g. hesychia-taylor-swift-question-2026-08-02.pdf.
  • PDF and DOCX must include conversation title and export timestamp in a header.
  • Once chat_backend#62 lands, exports must include citations as footnotes (PDF/DOCX) or a trailing column (CSV/XLSX).
  • Lazy-load every export library via dynamic import(). SheetJS, pdfmake, and docx are large; none may land in the main bundle.
  • Generate client-side. No new backend endpoint for export.
  • Verify downloads work in the Capacitor Android WebView — a plain anchor-download may need the Filesystem/Share plugin there. Call this out during implementation if it does.

4. Action row placement and behaviour

  • Row sits below the bubble, left-aligned for assistant messages.
  • Desktop: reveal on hover, always visible on the last message and on keyboard focus.
  • Mobile/touch: always visible — hover-only is unreachable.
  • Hidden while a message is still streaming; appears on completion.
  • Icon-only with tooltips; MUI icons to match the existing header and sidebar.
  • Theme-aware, consistent with the agent bubble styling.

Acceptance criteria

Copy

  • Every assistant and user message has a copy button that copies raw markdown.
  • Confirmation state shows for ~2s; failure shows an error toast.
  • Works in the Capacitor Android WebView, including the non-secure-context fallback path.
  • CustomCodeBlock is wired into ConversationDetailCard's markdown overrides and gives each code block its own copy button.

Feedback

  • Thumbs up and thumbs down on every assistant message; active state visually distinct.
  • Clicking the active rating clears the vote.
  • Updates are optimistic and revert on failure.
  • Thumbs down opens a reason popover; the rating posts immediately and is not blocked on the reason.
  • Ratings persist across reload, loaded with conversation history.
  • Re-voting updates rather than duplicating (enforced by the unique constraint).
  • Backend companion issue is filed on chat_backend and linked here.
  • Feedback is joinable to PromptMetric for per-model slicing.
  • Analytics events fire for both ratings and for reason submission.

Export

  • All five formats export from a single message and from a whole conversation.
  • PDF contains selectable, searchable text with real headings, lists, and tables — not a rasterised image.
  • DOCX opens cleanly in Word and Google Docs with structure preserved.
  • CSV/XLSX follow the tabular rules: markdown tables when present, turns-as-rows otherwise; XLSX uses one sheet per table.
  • Filenames are sanitised and include the conversation title and ISO date.
  • PDF and DOCX carry a header with title and export timestamp.
  • Every export library is dynamically imported; verify main bundle size is unchanged before and after.
  • Downloads work in the Capacitor Android WebView.
  • Unicode, emoji, and code blocks survive all five formats.
  • A conversation with 100+ messages exports without freezing the UI.

Action row

  • Reveals on hover on desktop; always visible on the last message, on focus, and on touch devices.
  • Hidden during streaming, appears on completion.
  • Keyboard accessible with visible focus rings; every button has an accessible label.
  • Correct in light and dark themes; no horizontal overflow at 320px width.
  • No layout shift when the row appears.

Testing

  • Unit tests: copy fallback path, optimistic feedback with rollback on failure, per-format export generation, tabular-rule branch selection.
  • Existing chat tests still pass.

Out of scope

  • Regenerate and edit-message actions — worth doing, but separate.
  • Server-side export rendering.
  • An admin dashboard over collected feedback — follow-up once data exists.
## Problem Assistant messages have no actions at all. `ConversationDetailCard` renders a markdown bubble and nothing else — no copy, no rating, no export. Users cannot get an answer out of the app without selecting text by hand, and we have no signal on which answers are good. That second point matters beyond UX: the accuracy work in `chat_backend`#62 needs real user judgements to measure against. Thumbs up/down is the data source for that. ## Current state - **No per-message actions.** `ConversationDetailCard.tsx` has no action row. The only message-adjacent control anywhere is conversation deletion in the sidebar (`AsyncDashboard2.tsx:467-477`). - **No copy anywhere in chat.** The only clipboard use in the app is "Copy details" in `AppErrorBoundary.tsx:86-102`, for crash info. - **No export.** Nothing downloads a conversation or a message. - **`FeedbackPage2` is unrelated.** It posts app-wide feedback to `POST /feedbacks/` (`FeedbackPage2.tsx:250-258`). It is not per-message and should not be reused as-is. - **`CustomCodeBlock` / `CustomPreBlock` exist but are unwired** — `ConversationDetailCard` never sets `options.overrides` on `markdown-to-jsx`, so code blocks have no copy button either. Stack context: MUI v5 + styled-components, React Context (no Redux/React Query), axios via `src/axiosApi.js`, `markdown-to-jsx` for rendering. ## Scope Four actions on every **assistant** message. User messages get copy only. --- ## 1. Copy Copy the message as **raw markdown** (what the model produced), not the rendered DOM text — pasting into another tool should preserve structure. - `navigator.clipboard.writeText` with a `document.execCommand('copy')` fallback for the Capacitor WebView and non-secure contexts. - Icon button flips to a check for ~2s on success; show an error toast on failure. - Also wire `CustomCodeBlock` into `ConversationDetailCard`'s `markdown-to-jsx` overrides so individual code blocks get their own copy button. The component already exists and is unused. ## 2. Thumbs up / thumbs down **Needs a backend endpoint — file a companion issue on `chat_backend`.** Required shape: - `PromptFeedback` model: `prompt` (FK to the assistant `Prompt`), `user`, `rating` (`up` / `down`), `reason` (optional short code), `comment` (optional free text), `created_at`, `updated_at`. Unique on `(prompt, user)` so a re-vote updates rather than duplicates. - `POST /api/prompt_feedback` upsert, `DELETE` to clear a vote. - Feedback must be joinable to `PromptMetric` so ratings can be sliced by model — this is what makes it useful for measuring `chat_backend`#62. Note that `PromptMetric.model_name` is currently hard-coded to `"llama3.2"`; #62 fixes that, and without the fix the slice is meaningless. Frontend behaviour: - Two icon buttons, filled when active. Clicking the active one clears the vote. - Optimistic update, revert on request failure. - On thumbs **down**, open a lightweight popover with optional reason chips (`Incorrect`, `Out of date`, `Didn't follow instructions`, `Unsafe`, `Other`) and a free-text field. Submitting the rating must not be blocked on filling this in — the vote posts immediately, the reason is a follow-up patch. - Ratings must be loaded with conversation history (`GET conversation_details`) so votes persist across reloads. - Emit analytics events per `ANALYTICS.md`. ## 3. Export A menu on the message action row plus a conversation-level export in the chat header. Formats: **PDF, Word (.docx), CSV, XLSX, TXT**. Both scopes: **this message** and **entire conversation**. | Format | Library | Output | |---|---|---| | TXT | none | Plain text; markdown stripped. Conversation export prefixes each turn with role and timestamp. | | PDF | `pdfmake` | Rendered from the markdown AST — real headings, lists, tables, code blocks. **Not** `html2canvas`, which produces unsearchable rasterised text. | | DOCX | `docx` | Native Word paragraphs, headings, lists, and tables from the same AST. | | CSV | `papaparse` | See tabular rules below. | | XLSX | `xlsx` (SheetJS) | See tabular rules below. | **Tabular rules (CSV/XLSX).** A conversation is not naturally tabular, so: - If the exported message contains one or more markdown tables, export those tables — one sheet per table for XLSX, the first table for CSV. - Otherwise export turns as rows: `timestamp`, `role`, `message`. - For a conversation export, always use the turns-as-rows form. **Common requirements:** - Filename: sanitised conversation title plus ISO date, e.g. `hesychia-taylor-swift-question-2026-08-02.pdf`. - PDF and DOCX must include conversation title and export timestamp in a header. - Once `chat_backend`#62 lands, exports must include citations as footnotes (PDF/DOCX) or a trailing column (CSV/XLSX). - **Lazy-load every export library via dynamic `import()`.** SheetJS, pdfmake, and docx are large; none may land in the main bundle. - Generate client-side. No new backend endpoint for export. - Verify downloads work in the Capacitor Android WebView — a plain anchor-download may need the Filesystem/Share plugin there. Call this out during implementation if it does. ## 4. Action row placement and behaviour - Row sits below the bubble, left-aligned for assistant messages. - Desktop: reveal on hover, always visible on the last message and on keyboard focus. - Mobile/touch: always visible — hover-only is unreachable. - Hidden while a message is still streaming; appears on completion. - Icon-only with tooltips; MUI icons to match the existing header and sidebar. - Theme-aware, consistent with the agent bubble styling. --- ## Acceptance criteria ### Copy - [ ] Every assistant and user message has a copy button that copies **raw markdown**. - [ ] Confirmation state shows for ~2s; failure shows an error toast. - [ ] Works in the Capacitor Android WebView, including the non-secure-context fallback path. - [ ] `CustomCodeBlock` is wired into `ConversationDetailCard`'s markdown overrides and gives each code block its own copy button. ### Feedback - [ ] Thumbs up and thumbs down on every assistant message; active state visually distinct. - [ ] Clicking the active rating clears the vote. - [ ] Updates are optimistic and revert on failure. - [ ] Thumbs down opens a reason popover; **the rating posts immediately** and is not blocked on the reason. - [ ] Ratings persist across reload, loaded with conversation history. - [ ] Re-voting updates rather than duplicating (enforced by the unique constraint). - [ ] Backend companion issue is filed on `chat_backend` and linked here. - [ ] Feedback is joinable to `PromptMetric` for per-model slicing. - [ ] Analytics events fire for both ratings and for reason submission. ### Export - [ ] All five formats export from a single message and from a whole conversation. - [ ] PDF contains selectable, searchable text with real headings, lists, and tables — not a rasterised image. - [ ] DOCX opens cleanly in Word and Google Docs with structure preserved. - [ ] CSV/XLSX follow the tabular rules: markdown tables when present, turns-as-rows otherwise; XLSX uses one sheet per table. - [ ] Filenames are sanitised and include the conversation title and ISO date. - [ ] PDF and DOCX carry a header with title and export timestamp. - [ ] Every export library is dynamically imported; **verify main bundle size is unchanged** before and after. - [ ] Downloads work in the Capacitor Android WebView. - [ ] Unicode, emoji, and code blocks survive all five formats. - [ ] A conversation with 100+ messages exports without freezing the UI. ### Action row - [ ] Reveals on hover on desktop; always visible on the last message, on focus, and on touch devices. - [ ] Hidden during streaming, appears on completion. - [ ] Keyboard accessible with visible focus rings; every button has an accessible label. - [ ] Correct in light and dark themes; no horizontal overflow at 320px width. - [ ] No layout shift when the row appears. ### Testing - [ ] Unit tests: copy fallback path, optimistic feedback with rollback on failure, per-format export generation, tabular-rule branch selection. - [ ] Existing chat tests still pass. ## Out of scope - Regenerate and edit-message actions — worth doing, but separate. - Server-side export rendering. - An admin dashboard over collected feedback — follow-up once data exists.
Author
Owner

Backend companion filed: chat_backend#67PromptFeedback model + POST/DELETE /api/prompt_feedback + hydrate ratings on conversation_details.

Frontend implementation for #97/#98 in progress on branch feature/message-actions-97-citations-98.

Backend companion filed: [chat_backend#67](https://git.aimloperations.com/ai_ml_operations/chat_backend/issues/67) — `PromptFeedback` model + `POST/DELETE /api/prompt_feedback` + hydrate ratings on `conversation_details`. Frontend implementation for #97/#98 in progress on branch `feature/message-actions-97-citations-98`.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ai_ml_operations/chat_web_app#97