Skip to main content
HAI-Co² collects structured feedback through a single modal reachable from several places. Every entry point opens the same modal with one of its three sections expanded; you make your selections across the sections and one Submit feedback button at the bottom saves them all at once, followed by a short thank-you. Each feedback row is anchored on (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 modal is draggable (by its header), width-resizable, and submits with one Submit / Update feedback button (followed by a thank-you).

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 unified feedback table, discriminated by scope + aspect:
  • scope: message | objective | plan | artifact | preferences | conversation
  • aspect: 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 valence positive | neutral | negative; null for reports
  • category: 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 an artifact-scope row rates (0 = no specific artifact: every non-artifact row, and a rating of the live document)
  • origin: user for a real opinion, branch_copy for feedback carried onto a fork
The unique key (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 in frontend/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.
There is also a full “every question, by response” table (each aspect’s positive/neutral/negative distribution across scopes) and the filterable reports table. Clicking any row replays that user’s full submission in a read-only feedback modal. Every figure excludes branch copies. Wherever a conversation is listed, a Trace button opens that thread’s Phoenix trace (resolved once from the Phoenix REST API and cached on the conversation row). From the reports table, Create issue files a GitHub issue with full reproduction context: the report, the reporter, the trace link, and the conversation transcript; on failure the button shows the reason (e.g. an expired token) and stays retryable. The filed issue link is stored on the report so it isn’t filed twice. Trace links need a Phoenix web base URL and a Phoenix REST API key (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).