Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 55 additions & 39 deletions client/src/components/admin/ProgramResultsSummarySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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).`);
}
}

Expand Down Expand Up @@ -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).
<div className="min-w-0" style={{ minWidth: 1 }}>
<div className="label-hw-dim mb-1">{title.toUpperCase()}</div>
<ResponsiveContainer width="100%" height={data.length * 28 + 10} debounce={50}>
Expand All @@ -184,7 +187,8 @@ function FeedbackChart({
<YAxis
type="category"
dataKey="name"
width={90}
width={yAxisWidth}
tickFormatter={(v: string) => v.length > maxChars ? v.slice(0, maxChars - 1) + "…" : v}
tick={{ fontSize: 9, fontFamily: "monospace", fill: "var(--color-label-mid)" }}
tickLine={false}
axisLine={false}
Expand Down Expand Up @@ -218,6 +222,7 @@ export function ProgramResultsSummarySection({
const [stats, setStats] = useState<ApiProgramStats | null>(null);
const [feedback, setFeedback] = useState<ApiResultsFeedbackAggregate | null>(null);
const [loading, setLoading] = useState(true);
const [refreshTick, setRefreshTick] = useState(0);

useEffect(() => {
let active = true;
Expand Down Expand Up @@ -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 ?? [];
Expand All @@ -259,7 +264,16 @@ export function ProgramResultsSummarySection({
return (
<div className="mb-3 space-y-3">
<div className="panel px-3 py-2.5">
<div className="label-hw text-display mb-3">·RESULTS SUMMARY</div>
<div className="flex items-center justify-between mb-3">
<div className="label-hw text-display">·RESULTS SUMMARY</div>
<button
onClick={() => { setLoading(true); setRefreshTick((n) => n + 1); }}
disabled={loading}
className="label-hw-dim hover:text-display text-[10px] tracking-widest disabled:opacity-40"
>
{loading ? "LOADING…" : "↺ REFRESH"}
</button>
</div>

{/* Stats strip */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-4">
Expand Down Expand Up @@ -357,16 +371,18 @@ export function ProgramResultsSummarySection({
·PARTICIPANT FEEDBACK ({feedback.total} response{feedback.total !== 1 ? "s" : ""})
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-3">
<FeedbackChart title="Deadline comfort" counts={feedback.deadlineStatus} />
<FeedbackChart title="Where did you land?" counts={feedback.deadlineStatus} />
<FeedbackChart title="Would keep building" counts={feedback.wouldKeepBuilding} />
<FeedbackChart title="Agent environment used" counts={feedback.agentEnv} />
<FeedbackChart title="Surfaces built for" counts={feedback.surfaces} />
<FeedbackChart title="Agent environment" counts={feedback.agentEnv} />
<FeedbackChart title="Surfaces used" counts={feedback.surfaces} />
<FeedbackChart title="Primary surface" counts={feedback.surfacesPrimary} />
<FeedbackChart title="Biggest blocker" counts={feedback.biggestBlocker} />
</div>
{(feedback.biggestBlockerSamples ?? []).length > 0 && (
{(feedback.couldntHandleSamples ?? []).length > 0 && (
<div className="mb-2">
<div className="label-hw-dim mb-1">·BIGGEST BLOCKERS (SAMPLE)</div>
<div className="label-hw-dim mb-1">·WHAT BITREFILL COULDN'T HANDLE (OPEN TEXT)</div>
<ul className="space-y-1">
{(feedback.biggestBlockerSamples ?? []).slice(0, 5).map((s, i) => (
{(feedback.couldntHandleSamples ?? []).slice(0, 10).map((s, i) => (
<li key={i} className="label-hw-dim text-[11px]">- {s}</li>
))}
</ul>
Expand Down
29 changes: 20 additions & 9 deletions client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ export type ApiResultsFeedbackAggregate = {
wouldKeepBuilding: Record<string, number>;
surfaces: Record<string, number>;
surfacesPrimary: Record<string, number>;
biggestBlockerSamples: string[];
biggestBlocker: Record<string, number>;
couldntHandleSamples: string[];
total: number;
};
Expand Down Expand Up @@ -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,
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion server/api/repositories/program-submission.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down