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. Nothtml2canvas, 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.
Backend companion filed: chat_backend#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.
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`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Assistant messages have no actions at all.
ConversationDetailCardrenders 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
ConversationDetailCard.tsxhas no action row. The only message-adjacent control anywhere is conversation deletion in the sidebar (AsyncDashboard2.tsx:467-477).AppErrorBoundary.tsx:86-102, for crash info.FeedbackPage2is unrelated. It posts app-wide feedback toPOST /feedbacks/(FeedbackPage2.tsx:250-258). It is not per-message and should not be reused as-is.CustomCodeBlock/CustomPreBlockexist but are unwired —ConversationDetailCardnever setsoptions.overridesonmarkdown-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-jsxfor 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.writeTextwith adocument.execCommand('copy')fallback for the Capacitor WebView and non-secure contexts.CustomCodeBlockintoConversationDetailCard'smarkdown-to-jsxoverrides 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:PromptFeedbackmodel:prompt(FK to the assistantPrompt),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_feedbackupsert,DELETEto clear a vote.PromptMetricso ratings can be sliced by model — this is what makes it useful for measuringchat_backend#62. Note thatPromptMetric.model_nameis currently hard-coded to"llama3.2"; #62 fixes that, and without the fix the slice is meaningless.Frontend behaviour:
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.GET conversation_details) so votes persist across reloads.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.
pdfmakehtml2canvas, which produces unsearchable rasterised text.docxpapaparsexlsx(SheetJS)Tabular rules (CSV/XLSX). A conversation is not naturally tabular, so:
timestamp,role,message.Common requirements:
hesychia-taylor-swift-question-2026-08-02.pdf.chat_backend#62 lands, exports must include citations as footnotes (PDF/DOCX) or a trailing column (CSV/XLSX).import(). SheetJS, pdfmake, and docx are large; none may land in the main bundle.4. Action row placement and behaviour
Acceptance criteria
Copy
CustomCodeBlockis wired intoConversationDetailCard's markdown overrides and gives each code block its own copy button.Feedback
chat_backendand linked here.PromptMetricfor per-model slicing.Export
Action row
Testing
Out of scope
Backend companion filed: chat_backend#67 —
PromptFeedbackmodel +POST/DELETE /api/prompt_feedback+ hydrate ratings onconversation_details.Frontend implementation for #97/#98 in progress on branch
feature/message-actions-97-citations-98.