Answer feedback
Every finished answer, typed or spoken, has a thumbs up and a thumbs down. The votes tell the people improving the prompts which answers were not good enough and why, broken down by capability and by prompt set.
How it behaves
- A thumbs up is recorded at once.
- A thumbs down is recorded at once too, and opens a card under the answer with reason chips and an optional note. Sending needs at least one reason or a note.
- Skip closes the card; the thumbs down stands.
- Pressing the other thumb switches the vote. Pressing the same one again withdraws it.
The reasons are one shared list (FEEDBACK_REASONS), used by the card, the API and the report:
| Id | Shown as |
|---|---|
incorrect | Wrong or made up |
missed | Missed something on the claim |
off_target | Didn’t answer my question |
unclear | Too long or unclear |
unsafe | Inappropriate or unsafe |
other | Other |
Why the reasons never reach the model
They are asked for in a card, not in the chat box, on purpose. Anything typed in the chat box becomes part of the conversation. A complaint about an answer would colour every later answer and could be read as an instruction. The card posts to /api/feedback, and the chat history is untouched. A Vitest test and a Playwright spec both check this.
What is recorded
The Worker mints a turnId for every text answer and forgets it. Because the Worker keeps no transcript, the browser sends the context with the vote: the claim id, the capability, the channel (text or voice), the surface (app or embed), the locale and brand, the model, the prompt fingerprint and the overridden prompt ids, and the content itself (the question, the answer and the lookups). Voice answers have no server turn, so the browser mints their turn id.
The contract is FeedbackRequest in packages/shared/src/feedback.ts. A null rating withdraws a vote; reasons and the note are dropped on anything but a thumbs down.
Content and the embed
| Surface | Stored with the vote |
|---|---|
| The demo app | Everything, including the question, answer and lookups |
| An embedded host | The vote, reasons, note and context, but not the question, answer or lookups, unless the element has feedback-content |
In a host system, that text quotes a real claim. The host always gets the full vote, content included, in the claimpilot-feedback DOM event.
Where it goes
One row per rated answer in a feedback table, keyed by turn id, in the same store as the claims (api/src/feedback/). With DATABASE_PROVIDER=none each vote is written to the Worker log as one JSON line instead. See Data stores.
Reading it back
The Prompt Lab's Answer feedback panel calls GET /api/feedback and shows:
- By capability: helpful and unhelpful counts for each capability.
- By prompt set: the same per capability and fingerprint. The same Prompt Lab versions in any browser give the same fingerprint, so the effect of an edit can be measured without the server ever storing a prompt.
- Recent: the latest votes (thumbs down by default), with reasons, note, question and the answer behind a disclosure.
Replay in Try It loads the rated question, its claim and its capability into the Prompt Lab runner. Open claim goes to the claim (demo votes only).
Code map
| Part | Where |
|---|---|
| Contract and reason list | packages/shared/src/feedback.ts |
| Turn id and fingerprint | api/src/chat/service.ts, api/src/prompts/fingerprint.ts |
| Route, port, adapters | api/src/feedback/ |
| Migration | db/migrations/{d1,neon}/0002_feedback.sql |
| Thumbs and card | frontend/src/chat/Feedback.tsx |
| What a click means and what is sent | useChat().feedback in frontend/src/chat/useChat.ts |
| Embed event and the content switch | frontend/src/embed/element.tsx, EmbeddedAssistant.tsx |
| Report | frontend/src/promptlab/FeedbackPanel.tsx |