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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Versions follow [SemVer](https://semver.org/) (`0.1.0-alpha.x` while the public
- **Cursor Cloud Agents** AI provider — use dashboard `crsr_…` keys via the Cloud Agents API (no-repo agents for digests/drafts)
- Guided **/setup** onboarding: team → assign-first roles → optional AI → integrations with credential guides → evidence backfill with progress
- **1:1 sessions** on the person dossier — manual Q&A agenda plus optional AI-suggested questions grounded in evidence
- Home **suggested next actions** queue: prioritized primary NextStep plus “Up next” list that refreshes when you return to the tab

### Changed

Expand Down
98 changes: 98 additions & 0 deletions apps/ui/src/components/HomeSuggestionQueue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Link } from "react-router-dom";
import type { ReactNode } from "react";
import type { HomeSuggestion } from "../lib/homeSuggestions";
import { NextStep } from "./PageChrome";

function SuggestionLink({
href,
className,
children,
}: {
href: string;
className?: string;
children: ReactNode;
}) {
if (href.startsWith("#")) {
return (
<a className={className} href={href}>
{children}
</a>
);
}
return (
<Link className={className} to={href}>
{children}
</Link>
);
}

/** Primary NextStep + remaining “up next” queue that shrinks as work completes. */
export function HomeSuggestionQueue({
suggestions,
refreshing,
onRefresh,
}: {
suggestions: HomeSuggestion[];
refreshing?: boolean;
onRefresh?: () => void;
}) {
if (suggestions.length === 0) return null;
const [primary, ...rest] = suggestions;

return (
<div className="stack home-suggestions" style={{ gap: "0.75rem" }}>
<NextStep
tone={primary.tone}
label={primary.title}
detail={primary.detail}
action={
<>
<SuggestionLink className="btn" href={primary.href}>
{primary.cta}
</SuggestionLink>
{primary.secondaryHref && primary.secondaryCta ? (
<SuggestionLink className="btn secondary" href={primary.secondaryHref}>
{primary.secondaryCta}
</SuggestionLink>
) : null}
{onRefresh ? (
<button type="button" className="btn ghost" disabled={refreshing} onClick={onRefresh}>
{refreshing ? "Updating…" : "Refresh"}
</button>
) : null}
</>
}
/>

{rest.length > 0 ? (
<div className="panel home-up-next" aria-label="Suggested next actions">
<div className="row" style={{ justifyContent: "space-between", gap: 8, alignItems: "baseline" }}>
<h2 style={{ margin: 0, fontSize: "1rem" }}>Up next</h2>
<span className="muted" style={{ fontSize: "0.82rem" }}>
{suggestions.length} suggestion{suggestions.length === 1 ? "" : "s"} · updates as you clear work
</span>
</div>
<ol className="home-up-next-list">
{rest.map((s, idx) => (
<li key={s.id} className={`home-up-next-item home-up-next-item-${s.tone}`}>
<div className="home-up-next-copy">
<div className="home-up-next-title">
<span className="muted">{idx + 2}.</span> {s.title}
</div>
<p className="muted" style={{ margin: "0.2rem 0 0" }}>
{s.detail}
</p>
</div>
<div className="home-up-next-actions">
<SuggestionLink className="btn secondary" href={s.href}>
{s.cta}
</SuggestionLink>
</div>
</li>
))}
</ol>
</div>
) : null}
</div>
);
}
114 changes: 114 additions & 0 deletions apps/ui/src/lib/homeSuggestions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { buildHomeSuggestions } from "./homeSuggestions.js";

describe("buildHomeSuggestions", () => {
it("returns empty when no cycle is selected", () => {
assert.deepEqual(
buildHomeSuggestions({
cycleId: null,
thinEvidenceCount: 2,
items: [],
nudgeCount: 0,
nudgeDays: null,
attentionCount: 0,
}),
[],
);
});

it("prioritizes role assignment before self bundles and writing", () => {
const suggestions = buildHomeSuggestions({
cycleId: "cyc_1",
thinEvidenceCount: 1,
nudgeCount: 0,
nudgeDays: null,
attentionCount: 0,
items: [
{
person: { id: "p1", name: "Alex", roleTitle: null },
hasSelf: false,
managerStatus: "missing",
},
{
person: { id: "p2", name: "Blake", roleTitle: "IC3" },
hasSelf: true,
managerStatus: "draft",
selfStatus: "returned",
},
],
});
assert.equal(suggestions[0]?.id, "assign-roles");
assert.ok(suggestions.some((s) => s.id === "self-bundles"));
assert.ok(suggestions.some((s) => s.id === "write-p2"));
assert.ok(suggestions.some((s) => s.id === "backfill"));
});

it("drops cleared items as pipeline advances", () => {
const before = buildHomeSuggestions({
cycleId: "cyc_1",
thinEvidenceCount: 0,
nudgeCount: 0,
nudgeDays: null,
attentionCount: 0,
items: [
{
person: { id: "p1", name: "Alex", roleTitle: "IC3" },
hasSelf: false,
managerStatus: "missing",
selfStatus: "pending",
},
],
});
assert.equal(before[0]?.id, "self-bundles");

const after = buildHomeSuggestions({
cycleId: "cyc_1",
thinEvidenceCount: 0,
nudgeCount: 0,
nudgeDays: null,
attentionCount: 0,
items: [
{
person: { id: "p1", name: "Alex", roleTitle: "IC3" },
hasSelf: true,
managerStatus: "missing",
selfStatus: "returned",
},
],
});
assert.equal(after[0]?.id, "write-p1");
assert.equal(after.some((s) => s.id === "self-bundles"), false);
});

it("shows cycle-complete when everyone is finalized", () => {
const suggestions = buildHomeSuggestions({
cycleId: "cyc_1",
thinEvidenceCount: 0,
nudgeCount: 0,
nudgeDays: null,
attentionCount: 0,
items: [
{
person: { id: "p1", name: "Alex", roleTitle: "IC3" },
hasSelf: true,
managerStatus: "finalized",
selfStatus: "returned",
},
],
});
assert.equal(suggestions[0]?.id, "cycle-complete");
});

it("falls back to year-round capture when the queue is idle", () => {
const suggestions = buildHomeSuggestions({
cycleId: "cyc_1",
thinEvidenceCount: 0,
nudgeCount: 0,
nudgeDays: null,
attentionCount: 0,
items: [],
});
assert.equal(suggestions[0]?.id, "capture-evidence");
});
});
Loading
Loading