research canvas: wider rail + stage explainer + 409 fix
Three connected fixes from a single session's feedback:
- Structure rail widened from 60→76 px, tabs 42→58 px wide with a
bit of left padding so labels ("Research", "Visualizations") no
longer bump against the active-tab indicator strip.
- Research canvas: each stage now shows a small "STAGE · <status>"
card explaining what state the topic is in and a "Next → …" hint
describing what the primary button will do. No more guessing
which of standby/processing/reviewing/publishing means what.
- Request-publish 409 fix:
- Backend TopicDetail now includes has_pending_publish_request
(SELECTs pending_for_topic when the topic loads). Frontend
TopicDetail interface + ResearchCanvas honor the flag: when
an approval is already pending the "Request publish" button
is replaced with an amber "Awaiting reviewer approval" pill,
so double-clicks can't 409 in the first place.
- runAction() also catches 409 as a signal-of-success (the
user's intent — "queue for approval" — is satisfied by the
first attempt), refetches the topic, and lets the new
awaiting-approval card render instead of surfacing a scary
error to the user.
This commit is contained in:
@@ -27,7 +27,27 @@ const STATUS_COLOR: Record<TopicStatus, string> = {
|
||||
published: "#5fd08a",
|
||||
};
|
||||
|
||||
function nextAction(status: TopicStatus): {
|
||||
// Human-readable stage description + what the next button does. Shown
|
||||
// under the primary action so the user knows what each step means
|
||||
// without having to remember the pipeline.
|
||||
const STAGE_COPY: Record<TopicStatus, { stage: string; nextHint: string }> = {
|
||||
standby: {
|
||||
stage: "Ready to run. Assigned agents will investigate and draft the outcome.",
|
||||
nextHint: "Kick off the research — assigned agents start working the prompt.",
|
||||
},
|
||||
processing: {
|
||||
stage: "Research in progress. Waiting for the assigned agents to finish drafting.",
|
||||
nextHint: "Move the topic to review once the draft looks complete.",
|
||||
},
|
||||
reviewing: {
|
||||
stage: "Under review — read the draft above and decide whether to publish.",
|
||||
nextHint: "Ask a reviewer to approve publishing. Stays in review until they decide.",
|
||||
},
|
||||
publishing: { stage: "Approved. Publishing pipeline is running.", nextHint: "" },
|
||||
published: { stage: "Published. This topic is done.", nextHint: "" },
|
||||
};
|
||||
|
||||
function nextAction(status: TopicStatus, hasPendingPublish: boolean): {
|
||||
label: string;
|
||||
run: (id: string) => Promise<unknown>;
|
||||
} | null {
|
||||
@@ -37,6 +57,9 @@ function nextAction(status: TopicStatus): {
|
||||
case "processing":
|
||||
return { label: "Submit for review", run: submitReview };
|
||||
case "reviewing":
|
||||
// Backend already has a pending approval — don't offer the button
|
||||
// (a second click 409s). Frontend surfaces an "Awaiting" note below.
|
||||
if (hasPendingPublish) return null;
|
||||
return { label: "Request publish", run: requestPublish };
|
||||
default:
|
||||
return null;
|
||||
@@ -99,17 +122,27 @@ export function ResearchCanvas({
|
||||
}
|
||||
|
||||
const dot = STATUS_COLOR[topic.status];
|
||||
const action = nextAction(topic.status);
|
||||
const action = nextAction(topic.status, topic.has_pending_publish_request);
|
||||
const stageCopy = STAGE_COPY[topic.status];
|
||||
const agentById = new Map(agents.map((a) => [a.id, a]));
|
||||
|
||||
async function runAction() {
|
||||
if (!action) return;
|
||||
setActing(true);
|
||||
setError(null);
|
||||
try {
|
||||
await action.run(topic!.id);
|
||||
onChanged();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "action failed");
|
||||
const msg = e instanceof Error ? e.message : "action failed";
|
||||
// A 409 on request-publish means an approval is already pending.
|
||||
// Treat it as success (the user's intent is satisfied) so the UI
|
||||
// flips into "awaiting approval" instead of surfacing a scary code.
|
||||
if (/\b409\b/.test(msg)) {
|
||||
onChanged();
|
||||
} else {
|
||||
setError(msg);
|
||||
}
|
||||
} finally {
|
||||
setActing(false);
|
||||
}
|
||||
@@ -226,32 +259,75 @@ export function ResearchCanvas({
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Action */}
|
||||
{action && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={runAction}
|
||||
disabled={acting}
|
||||
{/* Stage explainer + primary action */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 14px",
|
||||
borderRadius: 10,
|
||||
border: "1px solid rgba(255,255,255,.06)",
|
||||
background: "#101014",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: dot }}>
|
||||
STAGE · {topic.status.toUpperCase()}
|
||||
</div>
|
||||
<div style={{ fontSize: 12.5, color: "#cfcfd5", lineHeight: 1.55 }}>
|
||||
{stageCopy.stage}
|
||||
</div>
|
||||
{action && stageCopy.nextHint ? (
|
||||
<div style={{ fontFamily: mono, fontSize: 11, color: "#8a8a92", marginTop: 2 }}>
|
||||
Next → {stageCopy.nextHint}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{action ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={runAction}
|
||||
disabled={acting}
|
||||
style={{
|
||||
padding: "12px 22px",
|
||||
borderRadius: 10,
|
||||
border: 0,
|
||||
background: "linear-gradient(135deg,#ff8a7a,#ff5f57)",
|
||||
color: "#2a0d0a",
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
cursor: acting ? "default" : "pointer",
|
||||
opacity: acting ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{acting ? "Working…" : action.label}
|
||||
</button>
|
||||
{error && (
|
||||
<span style={{ fontFamily: mono, fontSize: 12, color: "#ff8a7a" }}>{error}</span>
|
||||
)}
|
||||
</div>
|
||||
) : topic.status === "reviewing" && topic.has_pending_publish_request ? (
|
||||
<div
|
||||
style={{
|
||||
padding: "12px 22px",
|
||||
padding: "10px 14px",
|
||||
borderRadius: 10,
|
||||
border: 0,
|
||||
background: "linear-gradient(135deg,#ff8a7a,#ff5f57)",
|
||||
color: "#2a0d0a",
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
cursor: acting ? "default" : "pointer",
|
||||
opacity: acting ? 0.6 : 1,
|
||||
border: "1px solid rgba(255,180,74,.35)",
|
||||
background: "rgba(255,180,74,.06)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
fontSize: 12.5,
|
||||
color: "#ffb44a",
|
||||
}}
|
||||
>
|
||||
{acting ? "Working…" : action.label}
|
||||
</button>
|
||||
{error && (
|
||||
<span style={{ fontFamily: mono, fontSize: 12, color: "#ff8a7a" }}>{error}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#ffb44a" }} />
|
||||
Awaiting reviewer approval. A workspace approver needs to sign off before publish.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user