Skip to content

feat(canvas): render dashboard/web-analytics on the React tier, Quill-styled#2809

Open
adamleithp wants to merge 7 commits into
mainfrom
bluebird/html-apps-nits
Open

feat(canvas): render dashboard/web-analytics on the React tier, Quill-styled#2809
adamleithp wants to merge 7 commits into
mainfrom
bluebird/html-apps-nits

Conversation

@adamleithp

@adamleithp adamleithp commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Makes the HTML/React canvas tier (the freeform sandboxed iframe) the home for PostHog data dashboards, fully Quill-styled, with the correct data path — and fixes the rough edges found while testing the live app.

Architecture

  • dashboard + web-analytics templates now render React in the sandbox, not json-render. Routed by REACT_TIER_TEMPLATE_IDS; kind:"freeform" is set at create time, so existing json-render canvases are untouched (no migration).
  • New freeformSystemPromptFor(templateId) registry — those two templates get opinionated React prompts, while plain freeform/blank stay a generic "anything goes" sandbox. templateId is threaded generate → store → service.

Data path — typed query nodes (the right way)

  • ph.query accepts a typed query node ({ kind: "TrendsQuery", … }) as well as inline HogQL. Typed nodes run through the same /query/ runner the PostHog UI uses, so the numbers match the product (sessionization, unique users, breakdowns, math, bounce rate) and the node's dateRange handles the window — no hand-written time SQL. HogQL stays as the escape hatch. The agent is steered (via MCP insights) to prefer typed nodes.
  • Result-shape handling: a TrendsQuery returns series objects, not rows-of-cells — CanvasDataService passes typed-node results through untouched and only row-coerces HogQL. The contract + prompt document both shapes. (Without this every typed-node value rendered 0.)
  • ph.run(insightShortId) stays stubbed — it's the view/published tier's model (allowlisted saved insights), to be wired at publish, not in edit.

Sandbox styling (Quill renders correctly in the iframe)

  • Load Quill's compiled CSS + design tokens into the iframe <head>; add the Tailwind Play CDN with the full Quill token map (primary/card/muted/fill-*/chrome/warning/info) + the v4 not-disabled: variant, and disable Preflight (its unlayered form reset overrode Quill's @layer components styles). This matters most for pure-utility components like DateTimePicker, which have no BEM fallback.
  • Add lucide-react to the import whitelist.

Prompt rules (data templates only)

  • Build entirely from Quill components (banned native <select>/<button>); default Button variant="outline"; Base UI conventions; never restyle Quill components.
  • In-canvas Quill DateTimePicker, always compact (its useMediaQuery auto-detect measures the full-width iframe and picks the wide layout, overflowing the popover). The dead toolbar picker is hidden for freeform.
  • SkeletonText loading/refresh placeholders per data point.

UI features

  • Context tab — Quill Tabs (Chat | Context) with a CodeMirror markdown editor (ContextEditor.tsx). Context persists per-canvas and per-version, is prepended to every agent turn, and editing it creates a version.
  • Refresh button reloads the canvas iframe (re-runs ph.query) and drops the polling/settings gear dropdown.
  • Loader + no flicker — freeze the canvas and suppress the runtime-error banner mid-turn, with a swooping .quill-section-loading bar.

Backend

  • Bump @posthog/quill + @posthog/quill-charts to 0.3.0-beta.18.
  • Reaped-session recovery — the freeform agent recreates the session and retries once on Session not found.

Docs

  • New .claude/skills/canvas-templates skill: the two tiers, the data path + result-shape gotcha, the compact-picker and Tailwind/Quill styling gotchas, for the next dev.

Testing

  • Typecheck (22/22), Biome lint, and @posthog/core (1592) + @posthog/ui (754) tests pass.
  • Live-tested over CDP: web-analytics canvas builds on the React tier; agent uses typed TrendsQuery nodes with compareFilter (per the new prompt); Quill date picker renders correctly (compact). Found + fixed the all-zeros result-shape bug and the picker overflow during this pass.
  • A consolidated live re-verification (typed-node data rendering real numbers + compact picker on a fresh build) is pending a stack restart, since the data-path and prompt changes run in the main process.

Known follow-ups

  • Custom date ranges work for the session but don't persist across an iframe reload/refresh (the freeform window lives only in iframe useState).
  • View/published canvas tier still needs self-hosted styles (no CDN there) and ph.run allowlisting — Phase 2.

🤖 Generated with Claude Code

adamleithp and others added 2 commits June 18, 2026 21:55
…-styled

Make the HTML/React canvas tier the home for PostHog data dashboards.

- Point dashboard + web-analytics templates at the freeform (React-in-iframe)
  renderer via REACT_TIER_TEMPLATE_IDS; add freeformSystemPromptFor() so they
  get opinionated React prompts while plain freeform stays a generic sandbox.
  Legacy json-render canvases are untouched (kind set at create, no migration).
- Style the sandbox: load Quill's compiled CSS + tokens into the iframe, add the
  Tailwind Play CDN with a token-mapped config, and disable Preflight (its
  unlayered form reset was overriding Quill's @layer component styles). Add
  lucide-react to the import whitelist.
- Prompt rules (data templates only): Quill for everything, default outline
  buttons, Base UI conventions, never restyle Quill components, in-canvas Quill
  DateTimePicker, half-open toDateTime() windows, SkeletonText loading states.
- Context tab: Chat | Context Quill tabs with a CodeMirror markdown editor;
  context persists per-canvas/per-version and is prepended to every agent turn;
  editing creates a version.
- Refresh button reloads the canvas iframe (re-runs ph.query) and drops the
  polling/settings dropdown; hide the now-inert toolbar date picker for freeform.
- Freeze the canvas + suppress the runtime-error banner mid-turn, and add the
  swooping .quill-section-loading bar so a turn shows progress without flicker.
- Bump @posthog/quill + quill-charts to 0.3.0-beta.18.
- Recover from reaped freeform agent sessions by recreating + retrying once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

React Doctor found 5 issues in 4 files · 1 error & 4 warnings.

Errors

4 warnings

src/features/canvas/components/ChannelsList.tsx

src/features/canvas/components/NewCanvasMenu.tsx

src/features/canvas/freeform/ContextEditor.tsx

Reviewed by React Doctor for commit 2d28b60.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (2)

  1. packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx, line 559-561 (link)

    P2 Duplicated thread-ID format

    threadIdFor reconstructs dashboard:${dashboardId} locally, but WebsiteDashboard.tsx already computes the same string (const threadId = \dashboard:${dashboardId}`). The canvasRefreshStorebridge between the toolbar button and the canvas view depends on both sides producing identical values. If the format ever drifts (e.g. a future rename), the Refresh button willbumpa key thatuseCanvasRefreshNonce` never reads and the feature silently breaks without any error.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx
    Line: 559-561
    
    Comment:
    **Duplicated thread-ID format**
    
    `threadIdFor` reconstructs `dashboard:${dashboardId}` locally, but `WebsiteDashboard.tsx` already computes the same string (`const threadId = \`dashboard:${dashboardId}\``). The `canvasRefreshStore` bridge between the toolbar button and the canvas view depends on both sides producing identical values. If the format ever drifts (e.g. a future rename), the Refresh button will `bump` a key that `useCanvasRefreshNonce` never reads and the feature silently breaks without any error.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx, line 622-626 (link)

    P2 Hardcoded 600 ms spinner doesn't reflect actual iframe load time

    The button re-enables after 600 ms regardless of whether the iframe has finished reloading. On a slow connection (CDN fetch of Tailwind or the four Quill stylesheets), the app may still be mounting when the spinner stops. A user clicking Refresh again mid-load would bump the nonce a second time, triggering another full reload and discarding the first one in flight.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx
    Line: 622-626
    
    Comment:
    **Hardcoded 600 ms spinner doesn't reflect actual iframe load time**
    
    The button re-enables after 600 ms regardless of whether the iframe has finished reloading. On a slow connection (CDN fetch of Tailwind or the four Quill stylesheets), the app may still be mounting when the spinner stops. A user clicking Refresh again mid-load would bump the nonce a second time, triggering another full reload and discarding the first one in flight.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx:559-561
**Duplicated thread-ID format**

`threadIdFor` reconstructs `dashboard:${dashboardId}` locally, but `WebsiteDashboard.tsx` already computes the same string (`const threadId = \`dashboard:${dashboardId}\``). The `canvasRefreshStore` bridge between the toolbar button and the canvas view depends on both sides producing identical values. If the format ever drifts (e.g. a future rename), the Refresh button will `bump` a key that `useCanvasRefreshNonce` never reads and the feature silently breaks without any error.

### Issue 2 of 3
packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx:622-626
**Hardcoded 600 ms spinner doesn't reflect actual iframe load time**

The button re-enables after 600 ms regardless of whether the iframe has finished reloading. On a slow connection (CDN fetch of Tailwind or the four Quill stylesheets), the app may still be mounting when the spinner stops. A user clicking Refresh again mid-load would bump the nonce a second time, triggering another full reload and discarding the first one in flight.

### Issue 3 of 3
packages/ui/src/features/canvas/stores/canvasRefreshStore.ts:1-22
**Nonces accumulate without cleanup**

Every `bump(threadId)` call grows the `nonces` map by one entry permanently. There is no removal when a dashboard is closed or its thread is reset. For a long-lived session with many canvases opened and refreshed, this is a persistent leak on the global store.

Reviews (1): Last reviewed commit: "feat(canvas): render dashboard/web-analy..." | Re-trigger Greptile

Comment on lines +1 to +22
import { create } from "zustand";

// View-state bridge between the toolbar Refresh button and a freeform canvas:
// the button and the iframe live in separate subtrees, connected only by the
// canvas thread id. Bumping a thread's nonce reloads its sandbox iframe, which
// re-mounts the React app and re-runs its `ph.query` calls (fresh data).
interface CanvasRefreshStore {
nonces: Record<string, number>;
bump: (threadId: string) => void;
}

export const useCanvasRefreshStore = create<CanvasRefreshStore>()((set) => ({
nonces: {},
bump: (threadId) =>
set((s) => ({
nonces: { ...s.nonces, [threadId]: (s.nonces[threadId] ?? 0) + 1 },
})),
}));

export function useCanvasRefreshNonce(threadId: string): number {
return useCanvasRefreshStore((s) => s.nonces[threadId] ?? 0);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Nonces accumulate without cleanup

Every bump(threadId) call grows the nonces map by one entry permanently. There is no removal when a dashboard is closed or its thread is reset. For a long-lived session with many canvases opened and refreshed, this is a persistent leak on the global store.

Rule Used: When using global state, consider the trade-offs i... (source)

Learned From
PostHog/posthog#32692

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/stores/canvasRefreshStore.ts
Line: 1-22

Comment:
**Nonces accumulate without cleanup**

Every `bump(threadId)` call grows the `nonces` map by one entry permanently. There is no removal when a dashboard is closed or its thread is reset. For a long-lived session with many canvases opened and refreshed, this is a persistent leak on the global store.

**Rule Used:** When using global state, consider the trade-offs i... ([source](https://app.greptile.com/posthog-org-19734/-/custom-context?memory=619e9241-3e55-409f-ab92-1c8f3d14fffa))

**Learned From**
[PostHog/posthog#32692](https://github.com/PostHog/posthog/pull/32692)

How can I resolve this? If you propose a fix, please make it concise.

adamleithp and others added 2 commits June 21, 2026 00:14
…ixes

Follows up the React-tier canvas work with the correct data path and fixes
found while testing the live app.

- ph.query now accepts a TYPED query node ({ kind: "TrendsQuery", … }) as well
  as an inline HogQL string. Typed nodes run through the same /query/ runner the
  PostHog UI uses, so numbers match the product (sessionization, unique users,
  breakdowns, math) and the node's dateRange handles the window. HogQL stays as
  the escape hatch. Steer the agent (via MCP insights) to prefer typed nodes.
- Fix the result-shape mismatch that rendered every typed-node value as 0:
  TrendsQuery returns SERIES OBJECTS, not rows-of-cells. CanvasDataService now
  passes typed-node results through untouched and only row-coerces HogQL; the
  result contract + prompt document both shapes.
- Date picker: force `compact` on DateTimePicker (its useMediaQuery auto-detect
  measures the full-width iframe and picks the wide layout, overflowing the
  popover), and complete the iframe Tailwind config with Quill's full token map
  (fill-*/chrome/warning/info) + the v4 `not-disabled:` variant so the calendar
  (pure-utility, no BEM fallback) renders correctly.
- Add a `canvas-templates` skill documenting the two tiers, the data path, the
  result-shape gotcha, and the picker/styling gotchas for the next dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx
#	packages/ui/src/features/canvas/components/WebsiteLayout.tsx
#	packages/ui/src/features/canvas/freeform/FreeformCanvasView.tsx
#	packages/ui/src/features/canvas/freeform/FreeformChat.tsx
@charlesvien charlesvien added Stamphog This will request an autostamp by stamphog on small changes and removed Stamphog This will request an autostamp by stamphog on small changes labels Jun 21, 2026
Turn each channel's "+" new-task button in the sidebar nav into a
dropdown with two options. "New task" keeps the existing navigation;
"New canvas" reuses the canvas template picker and assigns the canvas to
the channel via the existing useCreateAndOpenDashboard plumbing.

Extract a controlled NewCanvasDialog (and shared trackAndCreateCanvas)
from NewCanvasMenu so the dashboards grid and the sidebar dropdown share
one picker.

Generated-By: PostHog Code
Task-Id: 8c2574d8-f89d-4656-adca-1d5a577e65f6
Move freeform (React) canvas generation off the in-process streaming
side-panel and onto a dedicated agent task, mirroring CONTEXT.md:
the canvas screen shows a "Generating… View task" state and the agent
publishes the result via the new desktop-file-system-canvas-partial-update
MCP tool.

- Add buildFreeformGenerationPrompt + useGenerateFreeformCanvas (createTask
  → file into channel → record task in meta.generationTaskId).
- Track the generation task in the canvas record's meta; add
  dashboards.setGenerationTask mutation (meta-merge, no clobber).
- Rewire FreeformCanvasView: drop the chat panel, add the generating state
  + View task link + FreeformGenerateBar composer; poll the record and adopt
  the published version via store syncFromRecord. Keep undo/redo/revert/autosave.
- Remove the streaming path: freeform-gen service/router, FreeformChat,
  freeformSubscription, freeformStreamParser, and all DI/router wiring.

Generated-By: PostHog Code
Task-Id: f23b35ef-22d8-4e82-9798-acb9224a783c
@raquelmsmith raquelmsmith marked this pull request as ready for review June 21, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants