(thread_id, turn_index) (the same key the workspace snapshots, artifacts and the trajectory-graph nodes use), so feedback lines up 1:1 with graph nodes and is never lost or mis-attributed when a conversation branches.
The exact instrument (every question, its options, and when each appears) lives in the frontend taxonomy feedback-taxonomy.ts, the single source of truth; this page is the overview.
Entry points
- Each AI reply has three hover-revealed icons (next to copy/retry): a trajectory icon (the whole co-construction), a single like/dislike glyph (this reply), and a report icon. Each opens the modal at its section. The glyph lights up (its like half green, dislike half red, the same regardless of which option you picked) once any feedback exists for that turn, and the row then stays visible so it reads as submitted. So rating, say, the objective also lights the reply’s glyph.
- The Objective / Plan / Artifact / Preferences panels each carry the same like/dislike glyph. When a component changed this turn and hasn’t been rated, its icon shows a pulsing yellow “ping” with a “just updated” cloud to its right, persisting until you open it, rate it, or the next turn arrives; the modal also flags it “Just updated”.
- The check-in after the latest reply routes into the modal; it records nothing itself, never blocks follow-ups, and appears only once the reply has finished streaming.
The three sections (conditional)
Questions appear only when they apply: a component’s questions show when it exists, and a delta question (and its “Just updated” badge) shows only when that component actually changed this turn (computed by diffing the live workspace against the pre-turn snapshot).On this specific reply
- An overall judgement (Worked well / Mixed / Didn’t work), then reasons (which depend on the judgement, e.g. “Respected my constraints”) and a free-text note.
- The four components as they stand at this step (objective, plan, artifact, preferences), each with an endorsement question, change-gated delta/attribution questions, and (for the objective, on rejection) a free-text capture of the emerging goal in the user’s own words.
On the whole trajectory
Task completion (4 states), objective endorsement given the whole history, goal evolution and goal ownership (after a few turns), an overall assessment with a reason and note, and a branch-reflection question when the conversation has more than one path.Report a technical issue
A short list of genuinely technical categories (broken response, network bug, tool failure, rendering, unsafe content, other) plus a description. Content judgements like “off-topic” or “incorrect reasoning” live in the reply section, not here.The data model
One unifiedfeedback table, discriminated by scope + aspect:
scope:message|objective|plan|artifact|preferences|conversationaspect:reaction/report(message); the component questions (target_endorsement,target_change,plan_endorsement,plan_delta,artifact_satisfaction,artifact_fit,artifact_delta,preferences_extracted,preferences_followed,preferences_delta); or the trajectory aspects (task_completion,objective_endorsement_history,goal_evolution,goal_ownership,trajectory_assessment,branch_value)sentiment: the 3-point valencepositive|neutral|negative; null for reportscategory: a report category, a reason tag, or a multi-state choice value (the choice maps to a sentiment so it still aggregates)artifact_id: which artifact anartifact-scope row rates (0= no specific artifact: every non-artifact row, and a rating of the live document)origin:userfor a real opinion,branch_copyfor feedback carried onto a fork
(thread_id, turn_index, user_id, scope, aspect, artifact_id) makes every write an idempotent upsert: a user holds at most one rating per aspect per turn, while rating many different aspects of the same turn in parallel. Because artifact_id is part of the key, several artifacts produced in the same turn can each be rated independently: the modal renders one artifact card per artifact (falling back to a single card for the live document when a turn produced none). The modal’s controls each own one field of one row and merge their update, so picking a reason never clobbers the sentiment.
One source of truth for the vocabulary
The backend validates only the structural shape of a row: a known scope, a well-formed aspect slug, and the report/sentiment split. It deliberately does not enumerate the dimension aspects, report categories or reason tags. All of that presentation vocabulary lives only infrontend/src/lib/feedback-taxonomy.ts, so the UI can add or rename a reason without any backend change or migration. There is no list to keep in sync.
Branching & persistence
Feedback is always read and written by the viewed thread’s(thread_id, turn_index). When a conversation is forked, the parent’s prefix feedback is copied onto the new thread (origin='branch_copy') alongside the workspace and artifacts. Admin statistics count only origin='user', so those copies never inflate the figures; editing a copied row on the fork makes it a genuine, independent opinion on that path.
Admin dashboard
/admin/feedback (admin only) shows headline counts (likes, dislikes, reports, tasks marked complete), the sentiment split per dimension, a 30-day trend, and the report-category breakdown. Beyond those it adds four analytics:
- Completion funnel — engaged (left any feedback) → self-assessed completion → marked complete, plus the distribution of the latest completion state per thread.
- Per-dimension evolution over time — small-multiples showing each co-construction dimension’s daily positive/neutral/negative responses across the trailing window.
- Negative signals — a triage panel of valenced-negative rows (dislikes, low ratings) across every scope, so dissatisfaction surfaces without hunting. Technical reports keep their own table.
- Filters & search on the recent-feedback feed — narrow by scope, question (aspect), and response (sentiment), and search comment text; results paginate.
PHOENIX_API_KEY, since Phoenix runs with auth on); issue creation needs a GITHUB_TOKEN (set it to Coco’s PAT so issues are filed as Coco), while the non-secret repo and default labels live in config.*.yaml under github:.
API
User-facing (/api/feedback, ownership-checked): PUT upserts one row, DELETE toggles one off, GET /{thread_id} returns all of the caller’s feedback for a thread. Admin (/api/admin/feedback, admin only): …/stats (headline counts, dimension rates, completion funnel/breakdown, per-dimension trend, daily trend), …/reports, the recent feed (with scope / aspect / sentiment / category / q / negative filters), and …/node (all of a user’s feedback for one turn, for the read-only viewer).