Skip to content

Commit 8217275

Browse files
raymondfengclaude
andcommitted
fix(console): loading + error state for Contribute-to-explore (slow prepare)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 275a24f commit 8217275

2 files changed

Lines changed: 69 additions & 3 deletions

File tree

packages/console/src/panels/Insights/InsightsReportCard.test.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, afterEach } from "vitest";
2-
import { render, screen, cleanup } from "@testing-library/react";
2+
import { render, screen, cleanup, waitFor, act, fireEvent } from "@testing-library/react";
33
import { InsightsReportCard } from "./index.js";
44
import type { InsightsReportView } from "./insightsStream.js";
55

@@ -50,4 +50,53 @@ describe("InsightsReportCard", () => {
5050
render(<InsightsReportCard report={report} onContribute={() => {}} />);
5151
expect(screen.getByText("Contribute to explore →")).toBeTruthy();
5252
});
53+
54+
const contributeReport: InsightsReportView = {
55+
totals: { sessions: 1, mostly: 1, partially: 0, not: 0 },
56+
outcomes_summary: "1 session(s): 1 mostly achieved.",
57+
narrative: "You ship.",
58+
by_model: [],
59+
friction: [],
60+
publish_candidates: [{ sessionId: "c", goal: "deploy feature", why: "Succeeded" }],
61+
};
62+
63+
it("disables the Contribute button and shows Preparing… while the prepare call is in flight", async () => {
64+
let resolveContribute!: () => void;
65+
const onContribute = () => new Promise<void>((r) => { resolveContribute = r; });
66+
67+
render(<InsightsReportCard report={contributeReport} onContribute={onContribute} />);
68+
const btn = screen.getByText("Contribute to explore →") as HTMLButtonElement;
69+
expect(btn.disabled).toBe(false);
70+
71+
fireEvent.click(btn);
72+
73+
await waitFor(() => {
74+
const preparing = screen.getByText("Preparing…") as HTMLButtonElement;
75+
expect(preparing).toBeTruthy();
76+
expect(preparing.disabled).toBe(true);
77+
});
78+
79+
act(() => { resolveContribute(); });
80+
81+
await waitFor(() => {
82+
const restored = screen.getByText("Contribute to explore →") as HTMLButtonElement;
83+
expect(restored.disabled).toBe(false);
84+
});
85+
});
86+
87+
it("shows an error and does not navigate when prepare rejects", async () => {
88+
const originalHash = window.location.hash;
89+
const onContribute = () => Promise.reject(new Error("server blew up"));
90+
91+
render(<InsightsReportCard report={contributeReport} onContribute={onContribute} />);
92+
const btn = screen.getByText("Contribute to explore →");
93+
fireEvent.click(btn);
94+
95+
await waitFor(() => {
96+
expect(screen.getByText(/server blew up/)).toBeTruthy();
97+
});
98+
99+
expect(window.location.hash).toBe(originalHash);
100+
expect((screen.getByText("Contribute to explore →") as HTMLButtonElement).disabled).toBe(false);
101+
});
53102
});

packages/console/src/panels/Insights/index.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,23 @@ export function Insights({ apiBase }: { apiBase: string }) {
125125
);
126126
}
127127

128-
export function InsightsReportCard({ report, scanned, onBuild, onContribute }: { report: InsightsReportView; scanned?: number | null; onBuild?: () => void; onContribute?: () => void }) {
128+
export function InsightsReportCard({ report, scanned, onBuild, onContribute }: { report: InsightsReportView; scanned?: number | null; onBuild?: () => void; onContribute?: () => void | Promise<void> }) {
129+
const [contributing, setContributing] = useState(false);
130+
const [contributeError, setContributeError] = useState<string | null>(null);
131+
132+
const handleContribute = async () => {
133+
if (!onContribute) return;
134+
setContributing(true);
135+
setContributeError(null);
136+
try {
137+
await onContribute();
138+
} catch (e) {
139+
setContributeError(e instanceof Error ? e.message : "Prepare failed.");
140+
} finally {
141+
setContributing(false);
142+
}
143+
};
144+
129145
// Be honest about the cap: the report judges the most-recent sessions, which
130146
// can be fewer than were scanned (50-session batch bound, or unmissioned ones).
131147
const judged = report.totals.sessions;
@@ -161,8 +177,9 @@ export function InsightsReportCard({ report, scanned, onBuild, onContribute }: {
161177
<div className="analyze-candidate-head">
162178
<h4 style={{ margin: 0 }}>Worth publishing</h4>
163179
{onBuild && <button type="button" className="ledger-build" style={{ marginLeft: "auto" }} onClick={onBuild}>Build a Gem from this project →</button>}
164-
{onContribute && <button type="button" className="ledger-build" onClick={onContribute}>Contribute to explore →</button>}
180+
{onContribute && <button type="button" className="ledger-build" disabled={contributing} onClick={handleContribute}>{contributing ? "Preparing…" : "Contribute to explore →"}</button>}
165181
</div>
182+
{contributeError && <p className="ledger-error">{contributeError}</p>}
166183
<ul className="analyze-include">
167184
{publishCandidates.map((c) => (
168185
<li key={c.sessionId}>

0 commit comments

Comments
 (0)