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

# 2026 07 02 companion data analyst charts design

# Companion `data-analyst` skill — render the full Impact-Dashboard chart set

**Date:** 2026-07-02
**Branch:** `worktree-cheeky-greeting-origami` (off `main`)
**Status:** approved-by-default (Aziz away; recommended options chosen — overridable)

## Goal

Give the OpenCX **companion** (the in-product AI agent) the ability to render the
same charts that live on the **Impact Dashboard** (`/reports/impact-dashboard`),
inline in chat, with the same companion feature set it already has for the Sankey:
**fetch → render inline spec → deep-link to the dashboard → period-over-period
comparison**. Collapse the existing `conversation-flow-analyst` skill and the new
chart capabilities into **one** skill named **`data-analyst`**.

### Non-goals (explicit)

* **No per-segment ticket drill-down** for the new charts. The Sankey keeps its own
  `/sankey/tickets` drill-down; pie/bar/metric charts do not get one.
* **No maximize/zoom, no AI-mode filter.** Those are Impact-Dashboard *page*
  features (and not even merged to `main`); companion renders charts inline as
  specs and never uses them.
* Do **not** touch `automation-strategist` / `handoff-analyst` skills.

## Impact-Dashboard chart inventory (target set)

| # | Chart                      | Kind        | Data today                                 |
| - | -------------------------- | ----------- | ------------------------------------------ |
| 1 | Total tickets solved by AI | Metric card | `get_impact_report` totals ✅               |
| 2 | Automation percentage      | Metric card | `get_impact_report` totals ✅               |
| 3 | Conversation flow          | Sankey      | `get_conversation_sankey` ✅ (skill exists) |
| 4 | Resolutions by type        | Donut/Pie   | needs data ❌                               |
| 5 | Resolutions by day         | Stacked bar | needs data ❌                               |
| 6 | Sentiments by type         | Donut/Pie   | needs data ❌                               |
| 7 | Sentiments by day          | Stacked bar | needs data ❌                               |

Charts 4–7 are derivable from `ReportsService.getResolutionsReportForTimeZone`
(`backend/src/reports/reports.service.ts:57`), which already returns per-day
`resolutions` and `handoffSentiments` arrays (`resolutionsReportResSchema`,
`backend/src/reports/dto/resolutions-report-res.dto.ts`). That service is behind
the dashboard-only `POST /reports/resolutions` (`dashboardAuth`) — **not** exposed
to the public API that companion's MCP tools call.

## Architecture / how companion renders charts (context)

* Companion skills = `SKILL.md` (YAML frontmatter + markdown) under
  `backend/src/companion/skills/.claude/skills/<name>/`, baked into the Daytona
  sandbox by `daytona/build-snapshot.ts` and listed in `companion.prompt.ts`.
* Charts are rendered via the **json-render spec protocol**: the agent emits inline
  JSON like `{ "type": "Sankey", "props": { … } }`; the dashboard renders it via a
  component **catalog** (`dashboard/apps/dashboard/app/_lib/json-render/catalog.ts`)
  → **registry** (`registry.tsx`) → the real React chart component.
* The catalog is the single source of truth; it generates the backend system-prompt
  string `backend/src/companion/ui-prompt.generated.ts` via
  `pnpm -C dashboard/apps/dashboard gen:companion-prompt`
  (`dashboard/apps/dashboard/scripts/generate-companion-ui-prompt.ts`). CI gates on
  `git diff --exit-code` for that generated file.
* Companion's MCP tools live in `dashboard/packages/mcp-server/src/tools/` and call
  the **public API** via the generated OpenAPI client.
* Existing spec components: `Sankey`, `Metric`, `Table`, `Card`, `Badge`, `Heading`,
  `Text`, `Stack`, `List`, `CoworkerArtifactCard`, `PhoneAgentCard`. **Missing:
  Pie/Donut and Stacked Bar.**

## Work items

### 1. Backend — extend the public impact-report (ADDITIVE only)

File: `backend/src/public-api/controllers/impact-report.public.controller.ts`
(`GET /impact-report`, currently returns `ImpactReportResponseSchema`).

Add these **new optional/additional fields** to the response (do not change or
remove existing fields — public API is contract-stable):

* `resolutions_daily`: `[{ day, resolved, assumed_resolved, handed_off, in_progress }]`
* `sentiments_daily`: `[{ day, happy, neutral, angry, in_progress }]`
* `sentiment_totals`: `{ happy, neutral, angry, in_progress }`

(Resolution totals for the Pie chart #4 are already present as
`total_resolved / total_assumed_resolved / total_handed_off / total_in_progress`.)

Source the daily arrays from `ReportsService.getResolutionsReportForTimeZone`
(UTC / `timeZoneOffset: '+00:00'`, matching the public `/sankey` route). Sum the
per-day sentiment array to produce `sentiment_totals`. Prefer adding the logic to
the service method the controller already calls (`ReportsService.getImpactReport`)
or composing both service calls in the controller — whichever is the smaller,
cleaner seam. Use snake\_case for new public fields to match the existing response.

DTO rules: response schema keeps `.meta({ id })`; no `any`/`as`/`!`; Zod `.parse`
at boundaries. Regenerate the OpenAPI spec + SDK (`pnpm gensdk` from repo root) and
the mcp-server client types so the new fields are typed end-to-end.

### 2. mcp-server — surface the richer data

File: `dashboard/packages/mcp-server/src/tools/impact-report.ts`.
The tool already passes `data` through, so the new fields flow automatically once
the client types regenerate. **Update the tool description** to state it now also
returns daily resolutions, daily sentiments, and sentiment totals, so the model
knows it can build the pie/bar charts from one call.

### 3. Dashboard json-render — add `Pie` + `Bar` spec components

* Add `Pie` (donut) and `Bar` (stacked) definitions to `catalog.ts`, mapped in the
  catalog component map, each with a custom rule (mirroring the `Sankey` rule) that
  tells the model to pass the tool output straight through.
* Add their props schemas to
  `dashboard/apps/dashboard/app/_lib/json-render/json-render-props.ts`
  (follow `sankeyPropsSchema`). Pie props ≈ `{ data: [{ name, value }], … }`;
  Bar props ≈ `{ data: [{ day, …series }], keys, … }`.
* Add registry entries in `registry.tsx` that render the **existing** reusable
  components `ReportPieChart` / `ReportStackedBarChart`
  (`.../impact-dashboard/_components/common/`). Reuse them; only add a thin adapter
  if their props don't map directly. **Do not create new chart components** if the
  existing ones suffice (ponytail; honor "no new shared helpers").
* Run `pnpm -C dashboard/apps/dashboard gen:companion-prompt` and commit the updated
  `backend/src/companion/ui-prompt.generated.ts`.

### 4. Companion skill — create `data-analyst`, delete `conversation-flow-analyst`

* New `backend/src/companion/skills/.claude/skills/data-analyst/SKILL.md` that:
  * **Absorbs everything** in `conversation-flow-analyst/SKILL.md` verbatim in
    behavior (Sankey: `get_sankey_dimensions` → `get_conversation_sankey` →
    `get_sankey_tickets`; deep-link; period compare; drill-down).
  * Adds sections for automation/resolutions and sentiments: call
    `get_impact_report`, then render `Metric` cards (charts 1,2), `Pie` (charts 4,6),
    and `Bar` (charts 5,7). Passing tool output through to props.
  * Deep-links to `{dashboardBaseUrl}/reports/impact-dashboard`.
  * Documents period-over-period (call the tool twice).
* **Delete** `backend/src/companion/skills/.claude/skills/conversation-flow-analyst/`.
* Update `backend/src/companion/companion.prompt.ts` Skills section: replace the
  `conversation-flow-analyst` line with `data-analyst` (name + trigger blurb). Leave
  other skill lines untouched.
* `grep -rn "conversation-flow-analyst"` across the repo and update any cross-skill
  references / handoffs to point at `data-analyst`. Also fix the stale "maximize
  button" claim if edited text touches it.

### 5. Tests (real end-to-end, exhaustive — the closing step)

* **Backend (primary surface):** a real e2e spec for the extended `GET /impact-report`.
  Fresh org per test (`TestUtils.*`), seed chat sessions across multiple days with
  every resolution status (resolved / assumed-resolved / handed-off / in-progress)
  and every sentiment (happy / neutral / angry / in-progress via handoff). Assert:
  existing totals unchanged (regression), `resolutions_daily` per-day counts,
  `sentiments_daily` per-day counts, `sentiment_totals` = sum of daily, date-range
  filtering (rows outside the window excluded), empty range → zeroed/empty arrays,
  and **multi-tenant isolation** (another org's sessions never counted). Reference
  seeding: `backend/src/reports/__tests__/get-resolutions-report-for-timezone.spec.ts`.
  Follow public-api auth/scope (`impact-report:read`).
* **json-render:** rely on `pnpm type-check` + the CI prompt-drift gate for the
  catalog/registry wiring; add a focused test only if a pure function is introduced.
* `SKILL.md` is markdown — no test; verify it's discoverable (present in the skills
  dir, referenced in `companion.prompt.ts`).

## Verification / pre-commit

* `cd backend && pnpm tsgo` + `pnpm lint <touched files>`
* `cd dashboard && pnpm type-check` + `pnpm lint <touched files>`
* `pnpm gensdk` (repo root) after the DTO change; commit generated diffs.
* Run the new backend spec (targeted filter) and show green output.

## Open decisions (defaulted; Aziz can override)

1. Plumbing: **extend `get_impact_report`** (one endpoint/tool) — chosen over new
   per-dataset routes.
2. Feature parity: **all-but-drill-down** for the new charts.
3. Skill scope: **merge flow-analyst + all ID charts into `data-analyst`**, delete
   `conversation-flow-analyst`, leave the other analytics skills alone.
