> ## Documentation Index
> Fetch the complete documentation index at: https://haico.gr/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# User feedback

> How participants rate the co-construction: one modal with three sections (this reply, the whole trajectory, a technical report) and a single Submit button, anchored per turn so it survives branching, plus the admin dashboard that aggregates it.

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`](https://github.com/petrosrapto/HAICO/blob/main/frontend/src/lib/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).
