# AI Agent page redesign — execution plan (saved 2026-06-11, NOT yet executed)

Operator verdict on the current `/agent` page: **cluttered, not organized, not
intuitive** — everything (status, config, knowledge stats, retrieval tester,
doc list, inline editor, preview) is one long stacked scroll inside nested
cards. This plan replaces it with a tabbed, decluttered page. It is
self-contained: a fresh session can execute it without prior context.

**Honest model note (operator asked):** execute this on **Fable 5** — it
out-benchmarks Opus 4.8 on coding/frontend polish, and dev-session cost is
trivial (the cost argument only ruled Fable out as the *bot's runtime brain*).

## Already done (committed with this plan — start from green)

- `apps/web/src/lib/agent-ui.ts` — pure helpers: `parseAgentReply` (splits a
  raw agent reply into body + tappable options using the REAL shared
  `extractInteractive`/`stripInteractiveMarkers`, so the Playground renders
  exactly what WhatsApp would), `filterDocs`, `groupByCategory`.
- `apps/web/src/lib/agent-ui.test.ts` — 9 tests, all passing
  (`cd apps/web && npx vitest run src/lib/agent-ui.test.ts --pool=forks --poolOptions.forks.maxForks=1`).
- Backend already serves config/preview/knowledge, and preview is dry-run
  server-side (commit `97d8569`) so the Playground can never create real
  leads/emails. The ONE backend addition this plan requires is the
  `agentBehaviour` setting + prompt/assignee wiring (Tab 1.5 below).

## Constraints & conventions (read before coding)

- Rewrite **`apps/web/src/routes/Agent.tsx`** (888 lines today — read it first;
  all existing functionality must survive: config save/discard, canary
  add/remove, global toggle, doc CRUD + enable-toggle + reindex, retrieval
  tester, preview).
- House style: inline `React.CSSProperties` consts at file bottom, `so-card` /
  `so-btn so-btn-{primary|secondary|ghost}` classes, `PageHeader`,
  `QueryState`, `useToast`, react-query (`useQuery`/`useMutation` +
  `invalidateQueries`). CSS vars: `--navy --gold-2 --gold-soft --gold-line
  --line --bg --muted --text --red --green --muted-ink --navy-soft`.
- **Tab pattern**: copy the horizontal underline tabs from
  `apps/web/src/routes/Templates.tsx` (`tabBar`/`tabBtn`/`tabBtnActive`/
  `tabCount`/`tabCountActive` style consts around line 699; markup with
  `role="tab"`/`aria-selected` around line 316).
- Behaviour charter (`plans/agent-phase-3-plan.md`) applies to any copy text:
  production tone, no "testing" phrasing in user-facing labels.

## Target information architecture

```
PageHeader  (existing eyebrow/title/subtitle)
StatusStrip (always visible, one row)
[ Status & Config ] [ Behaviour ] [ Knowledge (N) ] [ Playground ]   ← underline tabs
<active tab content>
```

Tab state: local `useState<"config"|"knowledge"|"playground">("config")`,
persisted to `sessionStorage` (`agent-tab`) so a refresh keeps the tab.
Knowledge tab count pill = total documents (from the list query, cached).

### StatusStrip (extract from today's ConfigSection header)

One slim row (not a card): status pill (reuse `deriveStatus` exactly —
Off / Canary—N / Live—everyone / Not configured) · `Model: <model>` ·
connection state (red warning + link text "add an anthropic connection under
Settings" when unconfigured). Data from the existing `agent-config` query.

### Tab 1 — Status & Config

Single card, two zones separated by an `<hr>`:
1. **Rollout**: the "Enable for everyone" switch. ADD a confirm guard — when
   flipping from off→on and saving, `window.confirm("Enable the agent for ALL
   inbound WhatsApp conversations? The legacy flows stop receiving messages
   immediately.")` before the PATCH (this is the production cutover switch and
   today it saves silently).
2. **Canary numbers**: existing chip list + add-row, unchanged behavior
   (digits-only normalize, dedupe toast). Helper text logic unchanged.
Footer: Save changes / Discard (dirty-tracking exactly as today: local state
re-seeded from server data via `useEffect`).

### Tab 1.5 — Behaviour (NEW — operator-editable flows)

The operator must be able to change the three conversational flows from the UI
(today they are frozen in source: the system prompt + a hardcoded assignee).
This tab needs a SMALL backend addition first — build it in the same session:

**Backend (do before the tab):**
- New setting key `agentBehaviour` (SettingsService, same pattern as `agent`):
  `{ clientFlow: string; jobSeekerFlow: string; internalFlow: string;
  leadAssigneeUserId: string; leadAssigneeName: string }`. Empty strings =
  defaults; normalize with a `normalizeAgentBehaviour` in
  `packages/shared/src/agent/settings.ts` (+ tests).
- `buildSystemPrompt` (packages/shared/src/agent/system-prompt.ts): accept an
  optional `behaviour` object. Non-empty `clientFlow` text is APPENDED to the
  external prompt as an "OPERATOR INSTRUCTIONS (follow these — they override
  defaults where they conflict)" block; `jobSeekerFlow` replaces/extends the
  JOB SEEKERS paragraph the same way; `internalFlow` appends to the internal
  prompt. The deterministic core (never ask for phone, lead-capture mechanics,
  safety boundaries, no-price rule) stays NON-editable — operator text is
  additive steering, not a raw prompt editor. (+ tests)
- `AgentService.handleTurn` reads the setting (cheap — SettingsService) and
  passes it to `buildSystemPrompt`; `AgentToolsService.crmLeadTool` uses
  `leadAssigneeUserId` from the setting, falling back to the current hardcoded
  `ASHA_USER_ID` when blank. (+ test: custom assignee honored)
- Extend `GET/PATCH /api/agent/config` (agent-admin.controller) with the
  `behaviour` object (zod-validated, each text capped at 2000 chars) and
  mirror it in `apps/web/src/lib/agent-api.ts` types.

**The tab itself** — three editable sections + assignment, one Save/Discard
footer (same dirty-tracking pattern as Config):
- **Client / lead flow** — textarea, placeholder shows a worked example
  ("e.g. Always ask which emirate first; mention our Golden Visa service when
  budget exceeds AED 2M…"). Caption: "Extra instructions for customer
  conversations. The bot's core rules (capture the lead early, never ask for
  the phone number, no price quoting) always apply."
- **Job-seeker flow** — textarea, caption about CV intake steering.
- **Staff assistant flow** — textarea, caption: applies to recognised sales
  consultants only.
- **Lead assignment** — two inputs: LeadRat user id + display name. Caption:
  "Every bot-created lead is assigned to this CRM user. Default: Asha
  Hareendran." (Surfacing the hardcode.)
- Each textarea gets a "Reset to default" ghost button (clears to empty =
  built-in behaviour). After saving, suggest in a toast: "Saved — try it in
  the Playground." The Playground automatically uses the new behaviour since
  the server rebuilds the prompt per turn.

### Tab 2 — Knowledge (the decluttering core)

Toolbar row (flex, wraps):
- **Filter input** (left, grows): placeholder "Filter by title, category, or
  tag…", client-side via `filterDocs`. Live, no button.
- **Stats as one muted text line**, not boxes: `9 documents · 9 enabled ·
  10 chunks` (drop the three big stat tiles — main clutter source).
- Buttons right: `Test retrieval` (ghost, toggles the tester panel,
  collapsed by default), `Reindex` (ghost, keep title tooltip), `+ Add
  document` (secondary).

**Doc list, grouped by category** via `groupByCategory`:
- Group header row: chevron (▸/▾ collapse toggle, default expanded, collapsed
  set in `useState<Set<string>>`), category badge (existing `catBadge` style),
  doc count. When a filter query is active, force-expand all groups.
- Doc row (tighter than today): title (ellipsis) + inline muted tags
  (`#golden-visa`) · meta line `3 chunks · updated 11 Jun` · enabled switch ·
  Edit (ghost) · Delete (ghost red, keep `window.confirm`). Disabled docs at
  55% opacity (as today).
- Empty states: no docs at all → existing QueryState empty message; filter
  matches nothing → inline "No documents match '<q>'".

**Editor becomes a modal overlay** (today it's an inline panel that shoves the
list down): fixed inset-0 backdrop `rgba(16,24,40,.45)`, centered panel
(`maxWidth 720, width "min(720px, calc(100vw - 32px))", maxHeight "85vh",
overflow auto`, `so-card`-like surface). Same fields (title, category with the
`KB_CATEGORIES` datalist, tags, content textarea rows=12, enabled switch),
same save/cancel mutations. Esc closes (window keydown effect); backdrop click
does NOT close (protect long content edits). Focus the title input on open.

**Tester panel** (when toggled open): same search input + ranked hits as today
(`catBadge` + doc title + score + snippet), but inside a collapsible bordered
panel under the toolbar. Keep "No matches — the agent would offer a specialist"
empty state.

### Tab 3 — Playground (replaces the single-shot "Try it" card)

A chat-style tester that looks like WhatsApp and renders interactive replies:
- Local `turns: Array<{ role: "user"|"agent"; text: string; options?: string[];
  listLabel?: string|null; meta?: string; error?: boolean }>` — client-side
  only.
- Composer at bottom (input + Send, Enter sends, disabled while pending or
  when `!config.configured` with the existing red "no LLM connection" notice).
- Each send: push user bubble → `previewAgent(text)` → `parseAgentReply(reply)`
  → push agent bubble with `body`, `options`, `listLabel`, and a mono meta line
  `model · 123 in / 45 out · 900 cached`.
- **Options render as tappable chips** under the agent bubble (gold-line
  bordered, rounded; for a list, show the `listLabel` as a small caption
  above). **Clicking a chip sends that title as the next message** — exactly
  how a WhatsApp tap arrives to the bot.
- Error → red-tinted agent bubble with the message (plus toast, as today).
- Empty thread → 4 suggestion chips that send on click:
  "I'm interested in a Golden Visa", "I want to buy an apartment in Dubai
  Marina", "أريد الاستثمار في دبي" (RTL test), "I'm looking for a job at
  Silver Oak".
- Header utilities: `Clear chat` (ghost, resets turns) + one persistent muted
  caption: "Each message starts a fresh conversation — memory and history live
  on WhatsApp. Tools run in preview mode: nothing is sent and no CRM lead is
  created."
- Bubbles: user right-aligned (`--gold-soft` bg, `--gold-line` border), agent
  left (`#fff`/card bg, `--line` border), `maxWidth 78%`, `whiteSpace
  "pre-wrap"`, radius 12 with the WhatsApp corner notch optional (skip if
  fiddly). Auto-scroll: `ref` on the thread div, `useEffect` on `turns.length`
  → `scrollTop = scrollHeight`. Thread height: `min(58vh, 520px)`, scroll-y,
  `--bg` background.

## Execution checklist

1. **Backend first (Tab 1.5 wiring)**: `normalizeAgentBehaviour` in shared
   settings (+tests) → `buildSystemPrompt` behaviour blocks (+tests) →
   AgentService/AgentToolsService wiring (+tests) → agent-admin controller
   `behaviour` in GET/PATCH (+test). Build shared, build api, run both
   packages' tests (forks pool, maxForks=2). Deploy the api
   (`deploy.py` from `Documents\dev\specialized_chatbot`, env via `_env.ps1`;
   if the server tree has operator WIP, stash-cycle around the build as done
   on 2026-06-11).
2. Read `apps/web/src/routes/Agent.tsx` fully, then rewrite per the spec.
   Reuse existing style consts where they fit; delete dead ones (stat tiles).
3. Keep `KB_CATEGORIES`, `deriveStatus`, all mutations/queries and their
   query-keys (`agent-config`, `agent-knowledge`, `agent-knowledge-doc`)
   unchanged — only the presentation moves (plus the new `behaviour` fields).
4. Web tests (PID cap — NEVER bare `pnpm test`):
   `cd apps/web && npx vitest run --pool=forks --poolOptions.forks.maxForks=2`
   — agent-ui suite green + no regressions in the existing ~322.
5. `corepack pnpm --filter @whatapp/web build` (from repo root) — clean.
6. Commit per logical change (backend wiring, then UI) + push.
7. Publish: build is NOT live until rsynced. Either the operator runs
   `scripts/publish-web.sh` (sudo), or over SSH as root (established flow):
   build as sopserver1 → `bash scripts/publish-web.sh` → verify HTTP 200 on
   channels.silveroakglobal.com and hard-refresh `/agent`.
8. Manual verify on the live page: tab switching, config save round-trip,
   behaviour edit → Playground reflects it (e.g. "always greet with the
   customer's name" visibly changes the reply), assignee override saves,
   doc filter/grouping/editor modal, retrieval tester, playground chips
   (tap an option chip → it sends), Arabic suggestion renders RTL.

## Out of scope (deliberate)

- No backend changes EXCEPT the `agentBehaviour` setting + prompt/tool wiring
  described in Tab 1.5; no multi-turn preview persistence (playground stays
  stateless by design — server preview is single-turn).
- KB content trimming + prompt tightening = phase-3 W0 (separate plan).
- Model selector stays read-only (model changes happen in Settings →
  Connections).
