From 7428d40e9f5c3319892f8ce4274b3f5c85fcdb21 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:10:22 +0100 Subject: [PATCH] fix(results-panel): correct feedback charts, insights, and add refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - biggestBlocker: tally() not textSamples() (it is a single-select field, not free text) — removes duplicates and enables a proper bar chart - buildFeedbackInsights: pattern-match option strings instead of hardcoded short keys; actual stored values are 'Yes, already planning to'/'Maybe', not 'yes'/'maybe'; same fix for deadlineStatus (full sentence options, not 'comfortable'/'tight') - FeedbackChart: dynamic YAxis width computed from longest label (max 160px) + tickFormatter truncation so long option strings are readable - Layout: 6-chart grid (adds surfacesPrimary + biggestBlocker); replaces the old 'BIGGEST BLOCKERS' text list; adds 'WHAT COULDN'T BITREFILL HANDLE' for couldntHandle free-text responses - Refresh button: increments refreshTick dep so leaderboard/stats/feedback can be reloaded after score re-saves without a full page refresh - Mock data: updated to use real Bitrefill option strings so dev:harness exercises the same code paths as production --- .../admin/ProgramResultsSummarySection.tsx | 94 +++++++++++-------- client/src/lib/api.ts | 29 ++++-- .../program-submission.repository.js | 2 +- 3 files changed, 76 insertions(+), 49 deletions(-) diff --git a/client/src/components/admin/ProgramResultsSummarySection.tsx b/client/src/components/admin/ProgramResultsSummarySection.tsx index 59a0125..2be410a 100644 --- a/client/src/components/admin/ProgramResultsSummarySection.tsx +++ b/client/src/components/admin/ProgramResultsSummarySection.tsx @@ -42,38 +42,35 @@ function top3By( function buildFeedbackInsights(agg: ApiResultsFeedbackAggregate): string { const parts: string[] = []; - // Deadline comfort - const deadlineCounts = agg.deadlineStatus; - const comfy = deadlineCounts["comfortable"] ?? 0; - const tight = deadlineCounts["tight"] ?? 0; - const tooTight = deadlineCounts["too tight"] ?? 0; - const deadlineTotal = comfy + tight + tooTight; - if (deadlineTotal > 0) { - const comfyPct = Math.round((comfy / deadlineTotal) * 100); - if (comfyPct >= 60) { - parts.push(`${comfyPct}% of teams found the deadline comfortable.`); - } else if (tooTight > comfy) { - parts.push("Teams generally felt the timeline was tight; more hacking time would help."); - } else { - parts.push("Teams had mixed feelings about the deadline."); - } + // Would keep building: match "yes…" or "maybe" prefix, case-insensitive + const keepEntries = Object.entries(agg.wouldKeepBuilding); + const keepTotal = keepEntries.reduce((s, [, v]) => s + v, 0); + if (keepTotal > 0) { + const continuers = keepEntries + .filter(([k]) => /^yes/i.test(k) || /^maybe/i.test(k)) + .reduce((s, [, v]) => s + v, 0); + parts.push(`${Math.round((continuers / keepTotal) * 100)}% plan to keep building.`); } - // Would keep building - const keepCounts = agg.wouldKeepBuilding; - const yes = keepCounts["yes"] ?? 0; - const maybe = keepCounts["maybe"] ?? 0; - if (agg.total > 0) { - const continuePct = Math.round(((yes + maybe) / agg.total) * 100); - parts.push(`${continuePct}% plan to continue building their project.`); + // Deadline: find top outcome by count and categorise by pattern + const deadlineEntries = Object.entries(agg.deadlineStatus).sort((a, b) => b[1] - a[1]); + if (deadlineEntries.length > 0) { + const [top, topCount] = [deadlineEntries[0][0], deadlineEntries[0][1]]; + const deadlineTotal = deadlineEntries.reduce((s, [, v]) => s + v, 0); + const topPct = Math.round((topCount / deadlineTotal) * 100); + if (/bought and paid/i.test(top)) { + parts.push(`${topPct}% completed a full autonomous purchase.`); + } else if (/couldn.t get a core flow/i.test(top)) { + parts.push(`${topPct}% couldn't get a core flow going.`); + } } - // Top blocker - const blockerSamples = agg.biggestBlockerSamples ?? []; - if (blockerSamples.length > 0) { - const sample = blockerSamples[0]; - if (sample && sample.length < 120) { - parts.push(`The most cited blocker: "${sample}"`); + // Top blocker from tally + const blockerEntries = Object.entries(agg.biggestBlocker ?? {}).sort((a, b) => b[1] - a[1]); + if (blockerEntries.length > 0 && blockerEntries[0][1] > 1) { + const [label, count] = blockerEntries[0]; + if (label.length < 80) { + parts.push(`Top blocker: "${label}" (${count} teams).`); } } @@ -162,9 +159,15 @@ function FeedbackChart({ const data = entries.map(([name, value]) => ({ name, value })); const maxVal = Math.max(...data.map((d) => d.value)); + // min-w-[1px] + debounce prevents Recharts creating a 0-dim canvas pattern + // when the admin panel first opens (layout still in flight). + // Y-axis width is computed from the longest label so long option strings + // don't overflow; tickFormatter truncates anything that still doesn't fit. + const longestLabel = Math.max(...entries.map(([k]) => k.length)); + const yAxisWidth = Math.min(160, Math.max(80, Math.ceil(longestLabel * 5.4))); + const maxChars = Math.floor(yAxisWidth / 5.4); + return ( - // min-w-[1px] + debounce prevents Recharts creating a 0-dim canvas pattern - // when the admin panel first opens (layout still in flight).
{title.toUpperCase()}
@@ -184,7 +187,8 @@ function FeedbackChart({ v.length > maxChars ? v.slice(0, maxChars - 1) + "…" : v} tick={{ fontSize: 9, fontFamily: "monospace", fill: "var(--color-label-mid)" }} tickLine={false} axisLine={false} @@ -218,6 +222,7 @@ export function ProgramResultsSummarySection({ const [stats, setStats] = useState(null); const [feedback, setFeedback] = useState(null); const [loading, setLoading] = useState(true); + const [refreshTick, setRefreshTick] = useState(0); useEffect(() => { let active = true; @@ -249,7 +254,7 @@ export function ProgramResultsSummarySection({ return () => { active = false; }; // getAuth is a stable memoised ref — intentionally excluded to run once on mount. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [program.slug]); + }, [program.slug, refreshTick]); const hours = eventHours(program); const rows = leaderboard?.rows ?? []; @@ -259,7 +264,16 @@ export function ProgramResultsSummarySection({ return (
-
·RESULTS SUMMARY
+
+
·RESULTS SUMMARY
+ +
{/* Stats strip */}
@@ -357,16 +371,18 @@ export function ProgramResultsSummarySection({ ·PARTICIPANT FEEDBACK ({feedback.total} response{feedback.total !== 1 ? "s" : ""})
- + - - + + + +
- {(feedback.biggestBlockerSamples ?? []).length > 0 && ( + {(feedback.couldntHandleSamples ?? []).length > 0 && (
-
·BIGGEST BLOCKERS (SAMPLE)
+
·WHAT BITREFILL COULDN'T HANDLE (OPEN TEXT)
    - {(feedback.biggestBlockerSamples ?? []).slice(0, 5).map((s, i) => ( + {(feedback.couldntHandleSamples ?? []).slice(0, 10).map((s, i) => (
  • - {s}
  • ))}
diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 51703c5..2f06e55 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -471,7 +471,7 @@ export type ApiResultsFeedbackAggregate = { wouldKeepBuilding: Record; surfaces: Record; surfacesPrimary: Record; - biggestBlockerSamples: string[]; + biggestBlocker: Record; couldntHandleSamples: string[]; total: number; }; @@ -2246,14 +2246,25 @@ export const api = { return { status: "success", data: { - deadlineStatus: { comfortable: 5, tight: 3, "too tight": 1 }, - agentEnv: { yes: 6, no: 2, partial: 1 }, - wouldKeepBuilding: { yes: 7, maybe: 1, no: 1 }, - surfaces: { wallet: 5, dapp: 4, "smart contract": 3 }, - surfacesPrimary: { wallet: 4, dapp: 3 }, - biggestBlockerSamples: ["Polkadot docs hard to navigate", "Limited time"], - couldntHandleSamples: ["Complex multi-chain state management"], - total: 9, + deadlineStatus: { + "Agent bought and paid on its own, running in about an hour": 3, + "Agent bought and paid on its own, but it took most of the day": 4, + "A purchase worked, but I handled (or mocked) the payment": 2, + "Search/pricing worked, never completed a buy": 3, + "Couldn't get a core flow going": 5, + }, + wouldKeepBuilding: { "Yes, already planning to": 7, "Maybe": 1, "No": 1 }, + biggestBlocker: { + "Finding the right product in the catalog": 5, + "Access / keys / OAuth": 3, + "Nothing major, it was smooth": 2, + "Webhooks / order status": 2, + }, + agentEnv: { "Claude Code": 6, "Cursor": 2, "Custom agent framework": 1 }, + surfaces: { "Hosted MCP": 8, "REST API": 5, "Agent Skills (bitrefill/agents)": 4 }, + surfacesPrimary: { "Hosted MCP": 7, "REST API": 3 }, + couldntHandleSamples: ["Complex multi-step invoice flows", "Webhook retry logic"], + total: 17, }, }; } diff --git a/server/api/repositories/program-submission.repository.js b/server/api/repositories/program-submission.repository.js index e0d5eba..013487f 100644 --- a/server/api/repositories/program-submission.repository.js +++ b/server/api/repositories/program-submission.repository.js @@ -250,7 +250,7 @@ class ProgramSubmissionRepository { wouldKeepBuilding: tally('wouldKeepBuilding'), surfaces: tally('surfaces'), surfacesPrimary: tally('surfacesPrimary'), - biggestBlockerSamples: textSamples('biggestBlocker'), + biggestBlocker: tally('biggestBlocker'), couldntHandleSamples: textSamples('couldntHandle'), total: rows.length, };