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` |