packages/ui/src/components/Scorecard.tsx (props L27-39) |
Single-metric display: value, label, prefix?, suffix?, format? ('compact'|'decimal'|'none'), decimals?, trend?, variant? ('bare'|'card'). Used for the 3 hero metrics. |
packages/ui/src/components/LineAreaChart.tsx (props L57-82, data item L25-29, series def L31-36) |
Multi-series line/area chart: data: LineAreaChartDataItem[] ({x, y, series?}), series?: LineAreaChartSeriesDef[] ({key, label, color?}), showArea?, showDots?. Used for Chart 1 (G$ Volume) and Chart 3 (unique wallets). |
packages/ui/src/components/BarChart.tsx (props L35-52, data item L23-26) |
data: BarChartDataItem[] ({category, value}), layout? ('vertical'|'horizontal'), valueFormatter?. Used for Chart 2 (AI Credits/day). |
packages/ui/src/components/DataTable.tsx (props L46-61, column def L29-39) |
Generic data: TRow[] + columns: DataTableColumnDef<TRow>[] ({key, label, type?, align?, formatter?, sortable?}), built-in sort, striped/compact/stickyHeader. No built-in pagination — confirmed no page/pageSize props exist. Used for the daily summary table; pagination must be app-level (see below). |
packages/ui/src/utils/formatMetricValue.ts (L77-90) / resolveThemeColor.ts (L18-37) |
Shared formatting (K/M/B/T compact numbers) and Tamagui-token→raw-color resolution (needed because SVG fill/stroke props don't understand $tokens). Reused as-is, not reimplemented in the app. |
packages/ui/src/index.ts (L98-128) |
Public export surface — confirms all 5 components + types + the two utilities above are already exported from @goodwidget/ui on this branch. |
apps/ai-credits-web/{index.html,main.tsx,App.tsx,vite.config.ts,tsconfig.json,vercel.json,package.json,reactNativeSvgWeb.tsx} |
Primary scaffold template. App.tsx wraps the page in <TamaguiProvider config={defaultConfig} defaultTheme="dark"> (defaultConfig imported from @goodwidget/ui) — copy this wrapper, omit the DefaultAppKitProvider/wallet wiring (App.tsx L88-114, L271-274) entirely. reactNativeSvgWeb.tsx shim is required verbatim (unmodified) for the SVG-based charts to render on web. |
apps/superfluid-campaign-web/App.tsx |
Confirms the scaffold pattern is consistent across a second app and shows the simpler case (no landing page, single content column) — closer in shape to what this analytics app needs than ai-credits-web's multi-section landing page. |
.github/workflows/deploy-ai-credits-web.yml, deploy-superfluid-campaign-web.yml |
Deploy pattern to mirror: workflow_dispatch + push-to-main path filters + pull_request_target with an authorize-preview gate job, then preview/production jobs, each using its own VERCEL_*_PROJECT_ID secret and concurrency group deploy-<app>-${{ github.ref }}. Job names are the generic preview/production in both existing files — the new workflow fixes this for itself (see Execution plan step 6); the existing two files are not touched in this PR (see scope boundary above). |
apps/ai-credits-web/tests/, playwright.config.ts |
App-level Playwright convention: tests live under apps/<app>/tests/, each app has its own playwright.config.ts (dev port, webServer.command: pnpm --filter @goodwidget/<app> dev --host 127.0.0.1, outputDir: ../../test-results/<app>). This (not the tests/widgets/<name>/ convention used for widget packages) is the pattern to follow, since this is a standalone app, not a widget package. |
GoodDollar/data-team @ projects/antseed-analytics/dashboard/app.js (475 lines, full file reviewed) |
The reference implementation to recreate. Exact response shape confirmed: { days, daily: [{date, gdOneTimeDepositsWei, gdStreamedWei, gdTotalFlowRateWeiPerSecond, aiCreditsUsedWei, uniqueGdBuyers, uniqueCreditUsers, updatedAt, missing}], global: {gdOneTimeDepositsWei, gdStreamedWei, aiCreditsUsedWei, gdTotalFlowRateWeiPerSecond, updatedAt}, lastRun: {currentDate, updatedAt} }. Conversion helpers (weiToGd L77-84, weiToUsd L86-91, flowRateToDaily L93-100), API layer (fetchAnalytics/postRefresh L115-125), state machine (loadData/switchSource/updateToggleState L128-206), and table pagination (renderTable/renderPagination/goToPage L398-465) are the exact logic to port to React state/hooks. One thing intentionally not carried over: the live-unavailable copy in switchSource (L174) hardcodes a stale link to antseed-integration PR #19 — this is specific to the old dashboard's history and must be replaced with generic "endpoint not yet deployed" copy in the new app. |
[DRAFT][PLAN] AI Credits analytics data widget
Type: Task
Parent issue: #176
Plan for #176.
Key constraints (hard requirements, not open to reinterpretation)
feat/analytics-components(PR feat(ui): add analytics chart components (Scorecard, Pie/Donut, Bar, Line/Area, DataTable) #148 — open,head: feat/analytics-components→base: main, unmerged), notmain.packages/ui's chart components only exist on this branch today.feat/analytics-components(i.e. against PR feat(ui): add analytics chart components (Scorecard, Pie/Donut, Bar, Line/Area, DataTable) #148), notmain.ai-credits-web/superfluid-campaign-webwhich both require wallet connect.ai-credits-web,superfluid-campaign-web) job/environment naming is out of scope here — tracked as a separate follow-up, to be raised with Lewis directly. The new workflow follows the "show which app/preview it deploys" convention from the start.Reference files mapped (across both repos mentioned)
packages/ui/src/components/Scorecard.tsx(props L27-39)value,label,prefix?,suffix?,format?('compact'|'decimal'|'none'),decimals?,trend?,variant?('bare'|'card'). Used for the 3 hero metrics.packages/ui/src/components/LineAreaChart.tsx(props L57-82, data item L25-29, series def L31-36)data: LineAreaChartDataItem[]({x, y, series?}),series?: LineAreaChartSeriesDef[]({key, label, color?}),showArea?,showDots?. Used for Chart 1 (G$ Volume) and Chart 3 (unique wallets).packages/ui/src/components/BarChart.tsx(props L35-52, data item L23-26)data: BarChartDataItem[]({category, value}),layout?('vertical'|'horizontal'),valueFormatter?. Used for Chart 2 (AI Credits/day).packages/ui/src/components/DataTable.tsx(props L46-61, column def L29-39)data: TRow[]+columns: DataTableColumnDef<TRow>[]({key, label, type?, align?, formatter?, sortable?}), built-in sort,striped/compact/stickyHeader. No built-in pagination — confirmed nopage/pageSizeprops exist. Used for the daily summary table; pagination must be app-level (see below).packages/ui/src/utils/formatMetricValue.ts(L77-90) /resolveThemeColor.ts(L18-37)$tokens). Reused as-is, not reimplemented in the app.packages/ui/src/index.ts(L98-128)@goodwidget/uion this branch.apps/ai-credits-web/{index.html,main.tsx,App.tsx,vite.config.ts,tsconfig.json,vercel.json,package.json,reactNativeSvgWeb.tsx}App.tsxwraps the page in<TamaguiProvider config={defaultConfig} defaultTheme="dark">(defaultConfigimported from@goodwidget/ui) — copy this wrapper, omit theDefaultAppKitProvider/wallet wiring (App.tsx L88-114, L271-274) entirely.reactNativeSvgWeb.tsxshim is required verbatim (unmodified) for the SVG-based charts to render on web.apps/superfluid-campaign-web/App.tsxai-credits-web's multi-section landing page..github/workflows/deploy-ai-credits-web.yml,deploy-superfluid-campaign-web.ymlworkflow_dispatch+ push-to-main path filters +pull_request_targetwith anauthorize-previewgate job, thenpreview/productionjobs, each using its ownVERCEL_*_PROJECT_IDsecret and concurrency groupdeploy-<app>-${{ github.ref }}. Job names are the genericpreview/productionin both existing files — the new workflow fixes this for itself (see Execution plan step 6); the existing two files are not touched in this PR (see scope boundary above).apps/ai-credits-web/tests/,playwright.config.tsapps/<app>/tests/, each app has its ownplaywright.config.ts(dev port,webServer.command: pnpm --filter @goodwidget/<app> dev --host 127.0.0.1,outputDir: ../../test-results/<app>). This (not thetests/widgets/<name>/convention used for widget packages) is the pattern to follow, since this is a standalone app, not a widget package.GoodDollar/data-team@projects/antseed-analytics/dashboard/app.js(475 lines, full file reviewed){ days, daily: [{date, gdOneTimeDepositsWei, gdStreamedWei, gdTotalFlowRateWeiPerSecond, aiCreditsUsedWei, uniqueGdBuyers, uniqueCreditUsers, updatedAt, missing}], global: {gdOneTimeDepositsWei, gdStreamedWei, aiCreditsUsedWei, gdTotalFlowRateWeiPerSecond, updatedAt}, lastRun: {currentDate, updatedAt} }. Conversion helpers (weiToGdL77-84,weiToUsdL86-91,flowRateToDailyL93-100), API layer (fetchAnalytics/postRefreshL115-125), state machine (loadData/switchSource/updateToggleStateL128-206), and table pagination (renderTable/renderPagination/goToPageL398-465) are the exact logic to port to React state/hooks. One thing intentionally not carried over: the live-unavailable copy inswitchSource(L174) hardcodes a stale link toantseed-integrationPR #19 — this is specific to the old dashboard's history and must be replaced with generic "endpoint not yet deployed" copy in the new app.Existing
@goodwidgetpackages to import@goodwidget/ui—TamaguiProvider'sdefaultConfig,Scorecard,LineAreaChart,BarChart,DataTable(+ their prop/data types),formatMetricValue,resolveThemeColor, plus basic primitives already used by other apps (Button,ButtonText,Card,Heading,Text,XStack,YStack) for layout, the live/demo toggle, refresh button, and pagination controls.@goodwidget/core— only if any shared formatting/date utilities already exist there that overlap withweiToGd/weiToUsd/flowRateToDaily; otherwise these three conversion functions are analytics-specific and live in the app's own internal module, not@goodwidget/core.@goodwidget/embedneeded (no wallet connection), unlike both existing apps.New components assessed
No new components needed in
packages/ui. All 5 chart components (Scorecard,LineAreaChart,BarChart,DataTable;PieDonutChartis not used per the issue's proposed solution) plus their supporting formatting utilities already exist and cover every visualization this issue requires — confirmed by reading each component's full prop surface. This issue is pure composition, not new generic UI.New app-local pieces (all inside
apps/ai-credits-analytics-web/src/, notpackages/ui) — assessed against "does another widget/app plausibly need this generic primitive" and judged no in each case, since all of them are either analytics-specific business logic or thin one-off compositions:lib/analyticsApi.ts—fetchAnalytics()/postRefresh()against the worker endpoints, typed to the response shape above.lib/analyticsConversions.ts—weiToGd/weiToUsd/flowRateToDaily, ported verbatim fromapp.jswith the same BigInt-precision-preserving approach (divide beforeNumber()conversion).lib/generateDemoData.ts— the demo-data generator, ported fromgenerateMockData(app.jsL25-74), generated once and cached (not regenerated on every fallback).hooks/useAnalyticsData.ts— ownsliveData/demoData/isDemo/liveAvailablestate, the initial-load → try-live → fall-back-to-demo sequence,switchSource, and the 5-minute auto-refresh interval (app.jsL128-206, L468-472) — reimplemented as a React hook withuseEffect/useStateinstead of DOM manipulation.components/DataSourceToggle.tsx— small two-button Live/Demo toggle composed from@goodwidget/ui'sButton/XStack; a one-off composition, not a new generic control (no evidence any other GoodWidget app needs a live/demo toggle).components/PaginatedDataTable.tsx— thin wrapper aroundDataTablethat ownscurrentPagestate, slicesdailyinto pages ofTABLE_PAGE_SIZE = 10, and renders first/prev/next/last controls (mirroringrenderPagination/goToPage,app.jsL447-465) — needed becausepackages/ui'sDataTablehas no built-in pagination. Flagged in the human-reviewer checklist below as a candidate for promotion topackages/uiif another widget needs paginated tables later, but not promoted now (no second consumer yet).Required states, flows, and behaviors
GET {WORKER_URL}/v1/analytics?days=365; on success, show live data with the "Live" toggle active.switchSource's L156-187 behavior but with the stale PR-[Plan] Build packages/savings-widget: SDK integration, UI mapping, and execution checklist #19 reference removed.POST {WORKER_URL}/v1/analytics/refresh, then after a short delay (matching the reference's 2s) re-fetches vialoadData; button shows a loading state for the whole duration, matchingtriggerRefresh(app.jsL225-244).init()(app.jsL468-472).weiToGd), 6-decimal USD/credits (weiToUsd), flow-rate wei/sec × 86400 then reduced the same way as G$ (flowRateToDaily) — no precision loss (BigInt division beforeNumber()conversion, never a directNumber(BigInt)cast on the full-precision value).weiToGd(deposits) + weiToGd(streamed)), AI Credits Used USD (weiToUsd(global.aiCreditsUsedWei)), G$ Flow Rate/day (flowRateToDaily(global.gdTotalFlowRateWeiPerSecond)).LineAreaChart, 2 overlaid area series — "One-time Deposits" and "Streamed" — matching the reference's two independently-filled (not mathematically stacked) area series.BarChart, single series, one bar per day.LineAreaChart, 2 line series ("G$ Buyers", "Credit Users"), no area fill — matches the reference'sfill: falsefor both.renderTable/renderPagination.daily.length === 0, after filtering outmissing: truedays) shown distinctly from the loading state, in both the charts area and the table.Execution plan
feat/analytics-components, confirmed at557ad15, clean, trackingorigin/feat/analytics-components), create a new feature branch off it (e.g.feat/ai-credits-analytics-web) — implementation happens there, not directly onfeat/analytics-components.apps/ai-credits-analytics-webby copying theapps/ai-credits-webstructure (index.html,main.tsx,vite.config.ts,tsconfig.json,vercel.json,reactNativeSvgWeb.tsxshim verbatim,package.json), stripping all@reown/appkit/wallet-related dependencies and code, and writing a newApp.tsxthat wraps the page in<TamaguiProvider config={defaultConfig} defaultTheme="dark">with no wallet provider. Assign a free dev port (3001/3002 taken by the two existing apps → use 3003).src/lib/analyticsApi.ts,src/lib/analyticsConversions.ts,src/lib/generateDemoData.ts,src/hooks/useAnalyticsData.ts) — port the referenceapp.jslogic per the "New app-local pieces" section above, as an internal module inside the app.Scorecards), charts section (LineAreaChart×2,BarChart×1) driven by the hook's current dataset,DataSourceToggle, refresh button,PaginatedDataTablewrappingDataTable.apps/ai-credits-analytics-web/tests/+playwright.config.tsmirroringai-credits-web's, covering: live data loaded, demo fallback, manual toggle to unavailable-live state, refresh loading state, empty state, table pagination (page 1 / middle page / last page)..github/workflows/deploy-ai-credits-analytics-web.ymlmirroring the existing two files, but with job names that include the app identifier from the start (e.g.preview-ai-credits-analytics-web/production-ai-credits-analytics-web, or anenvironment.nameofpreview / ai-credits-analytics-web) rather than the barepreview/productionused by the existing two workflows. This PR only adds this one new file — it does not modifydeploy-ai-credits-web.ymlordeploy-superfluid-campaign-web.yml(tracked separately, see scope boundary above). Do not push or merge this workflow file until Laurence has explicitly confirmed specifics — everything else in this plan can proceed independently of that sign-off.feat/analytics-components(PR feat(ui): add analytics chart components (Scorecard, Pie/Donut, Bar, Line/Area, DataTable) #148's branch), notmain.Acceptance criteria
apps/ai-credits-analytics-webexists, builds, and deploys as a standalone Vite+React app (once the held-back deploy workflow is separately approved).packages/uicomponents, with no new chart components added.POST /v1/analytics/refreshthen re-fetches, with a loading state throughout.Number(BigInt)precision loss).Human-reviewer checklist
antseed-integrationPR [Plan] Build packages/savings-widget: SDK integration, UI mapping, and execution checklist #19 link) — this wasn't stated in the issue but is clearly stale/one-off content from the reference implementation.PaginatedDataTable's pagination logic app-local rather than proposing it as a newpackages/uiprimitive now — no second consumer exists yet, but flag if there's a known near-term need elsewhere that would justify building it there instead.ai-credits-web/superfluid-campaign-web) gets tracked somewhere (new issue) so it isn't lost once this PR merges.