agent-authored-dashboards-architecture.md9.6 KBView on GitHub
# Agent-Authored Dashboards — Architecture & Current State

This documents how the agent-authored dashboard system works as built, and — importantly —
**what is a real, reusable engine vs. what is hardcoded mock content** for the "top rep playbook"
demo.

The mental model: there are **two layers**. A *generic, reusable dashboard engine* (real) and a
*hardcoded mock document* (`top-rep-playbook`) that exercises it. Almost everything "smart" is the
engine; almost everything "sales-data" is hardcoded.

## 1. The big picture

```text
Agent or seed script
   │  writes markdown containing ```dashboard {JSON spec} fences
   ▼
Cedar Doc  (documents table: content = markdown, content_yjs = editor state)
   │  opened in /brain
   ▼
TipTap MarkdownEditor  ──DashboardFenceNode──►  <DashboardViewer spec={…}>
                                                    │ resolves dataSources
                                       ┌────────────┴────────────┐
                                  mock source                query source
                                  (inline rows)          tRPC dashboard.execute
                                       └────────────┬────────────┘
                                          recursive block renderer
                                  (tabs · tables · charts · funnels · cards · docLinks)
```

## 2. What is HARDCODED

All demo content lives in one file: `apps/mail/modules/dashboards/mock/top-rep-playbook.ts`.
Everything in it is static TypeScript literals — it does **not** query the database at render time.

- **Reps** (Willem/Sarah/Marcus/Lily): funnel counts, win rates, quota attainment, call-intelligence
  stats, follow-up content, per-stage playbook cards — invented mock numbers.
- **Follow-up section**: response/follow-up times, won/lost breakdown, pattern-breaker example
  emails — hardcoded.
- **Competitors** (Claude, Sybill, Gong, Clari, HubSpot Sequences, Status quo), **pain points**,
  **ICP segments** — hardcoded.
- **Objections**: the *list and win rates* were taken from the org's **real** objection wiki
  (`organisation/wiki-legacy/objections/*`) — but they are **baked in as literals**. A script read
  them once and the values were copied in; the document does **not** live-read the wiki. So "Build
  vs Buy = 25%" is a real number, frozen at authoring time.

The generator functions (`stageFence`, `repSubdoc`, `followupFence`, `orgConversionFence`,
`perRepFence`, `objectionSubdoc`, …) assemble these literals into ```dashboard JSON fences + markdown,
producing **25 documents** (1 main + 4 rep subdocs + 20 objection subdocs). The output is emitted to
`apps/mail/docs/playbook-docs.json` and seeded as static content.

The **seeded document itself is also effectively static**: once written to `/brain`, updating it means
editing the generator and re-running the seed script.

## 3. What is NOT hardcoded (the real engine)

A general agent-authored-dashboard system. Any agent can emit any ```dashboard spec and it renders —
nothing about the engine is sales- or rep-specific.

### Spec format — `apps/mail/modules/dashboards/types/dashboard.ts`
A zod-validated tree of `dataSources` (each `query`- or `mock`-backed) + a recursive `layout` of typed
blocks: `tabs`, `table`, `chart`, `funnel`, `stat`, `comparison`, `playbook`, `repeater`,
`entityCard`, `profile`, `docLink`, `text`, and containers (`row`/`column`/`grid`). The manual TS
interfaces are the source of truth for the renderer; the zod schema validates at runtime. This is the
"document structure."

### Renderer — `DashboardViewer.tsx` + `blocks.tsx`
- Resolves each data source. **Mock sources** return their inline rows with zero network calls (this
  is why the demo needs no backend). **Query sources** call `trpc.dashboard.execute` with a 5-minute
  cache.
- A recursive `LayoutNode` dispatcher walks the tree. Block data resolves by precedence: explicit
  `source` → named top-level source; `rowField` → array on the current repeater row; otherwise the
  repeater's current row.
- `lib/data.ts` holds the pure helpers: `formatValue` (percent/duration/currency/currency-compact),
  `applyComputations`, `interpolate` (the `{{rep.name}}` templating), and `teamAggregate` (column-wise
  mean for rep-vs-team comparisons).
- `components/context.tsx` provides `SourcesContext` (resolved sources) and `ScopeContext` (the current
  repeater row + team aggregate).

### Live backend path — `apps/server/src/trpc/routes/dashboard.ts`
A real `execute` procedure: takes a SELECT query, enforces SELECT-only, binds `$1 = userId` for
scoping, executes via `conn.unsafe(query, params)`, returns rows + metadata. **The demo never calls
this** (all sources are mock), but it is the path a data-backed dashboard uses.

### Document links resolve live
`DocLink` in `blocks.tsx` calls `documents.getDoc({ documentType, path })` at render time to resolve a
path → id and navigate via the store (`selectDocumentId` + `setActiveFolderContext`). So rep-card and
objection-analysis links are genuinely dynamic, not baked ids.

### Agent authoring path — `apps/server/src/mastra/tools/dashboard/createDashboardTool.ts`
A registered Mastra tool that lets an agent write a dashboard spec into any conversation/doc. This is
the production authoring path (vs. the seed script used for the demo).

## 4. The rendering plumbing (why the fence actually displays)

Getting ```dashboard to render as a live React component inside a *collaborative* editor required four
pieces that round-trip a `dashboardBlock` ProseMirror node:

1. **Client fence node** — `modules/conversations/components/tiptap-extensions/DashboardFenceNode.tsx`:
   a TipTap block node with a markdown tokenizer that recognizes the ```dashboard fence and renders
   `<DashboardViewer>`. Wired into `components/markdown-editor.tsx` and
   `components/read-only-markdown-view.tsx`.
2. **Server parser** — `apps/server/src/services/document-saving/markdown-parser.ts`: converts
   ```dashboard → a `dashboardBlock` node (so it survives into `content_yjs`, which is what the editor
   hydrates from — not the raw markdown).
3. **Server serializer** — `apps/server/src/services/document-saving/serialize.ts`: the reverse,
   `dashboardBlock` → ```dashboard, so the markdown `content` mirror stays correct on every save.
4. **Two-step write** — `writeDocument` sets `content` (markdown), then `writeFileAsYjs` builds
   `content_yjs` (collaborative state). `apps/server/src/scripts/seed-dashboard-doc.ts` does both for
   all 25 docs.

> Note: a one-line addition to `modules/documents/yjs/ensureNodeIdsPlugin.ts` lists `dashboardBlock`
> as an id-bearing node so it gets a stable `nodeId` in the collaborative document. (This file is
> shared with a concurrent "playbook" feature and is committed alongside that work, not the dashboard
> commit.)

## 5. The surfaces

- **`/brain`** (real): the 25 seeded Cedar Docs under `user/files/top-rep-playbook` and subpaths for
  `<email>`.
- **`/dashboard-demo`** (preview): `app/(full-width)/dashboard-demo/page.tsx` renders the main doc's
  markdown via `ReadOnlyMarkdownView` — same rendering, no DB needed.

## 6. Honest summary

| Concern | Status |
|---|---|
| Dashboard spec + block vocabulary | **Real, generic** |
| Renderer (tabs/tables/charts/funnels/cards/links) | **Real, generic** |
| Markdown ↔ Y.js round-trip plumbing | **Real, production-path** |
| `dashboard.execute` SQL backend | **Real, but unused by the demo** |
| Agent authoring tool (`createDashboardTool`) | **Real, registered** |
| DocLink navigation | **Real, resolves live** |
| All rep/objection/competitor/pain/ICP **numbers** | **Hardcoded** (objection win-rates mirror the real wiki, frozen) |
| The seeded document | **Static** — regenerate + reseed to change |

The engine is real and reusable — point a spec at `query` sources and it is a live dashboard. The
"top rep" content is a hardcoded fixture demonstrating the engine, with objection win-rates being the
one slice grounded in actual data (copied in, not queried).

## 7. To make it "real"

- Replace the demo's `mock` sources with `query` sources hitting `crm_events` / `crm_conversations`
  (response/follow-up timing, win rates, stage progression), scoped to the user via `$1`.
- Have the agent author specs via `createDashboardTool` instead of the seed script.
- Optionally resolve objection/competitor/pain data live from the org wiki + objection stats rather
  than freezing them in the generator.

## Key files

| Area | Path |
|---|---|
| Spec types + zod | `apps/mail/modules/dashboards/types/dashboard.ts` |
| Pure helpers | `apps/mail/modules/dashboards/lib/data.ts` |
| Contexts | `apps/mail/modules/dashboards/components/context.tsx` |
| Viewer (source resolution) | `apps/mail/modules/dashboards/components/DashboardViewer.tsx` |
| Block renderers + dispatcher | `apps/mail/modules/dashboards/components/blocks.tsx` |
| Mock content (hardcoded) | `apps/mail/modules/dashboards/mock/top-rep-playbook.ts` |
| Fence node (TipTap) | `apps/mail/modules/conversations/components/tiptap-extensions/DashboardFenceNode.tsx` |
| Editor wiring | `apps/mail/components/markdown-editor.tsx`, `read-only-markdown-view.tsx` |
| tRPC execute route | `apps/server/src/trpc/routes/dashboard.ts` |
| Agent tool | `apps/server/src/mastra/tools/dashboard/createDashboardTool.ts` |
| Server parse/serialize round-trip | `apps/server/src/services/document-saving/markdown-parser.ts`, `serialize.ts` |
| Seed script | `apps/server/src/scripts/seed-dashboard-doc.ts` |
| Demo route | `apps/mail/app/(full-width)/dashboard-demo/page.tsx` |