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,
};