agents empty state: kill synthetic fallback + real CTA
After reaping every real agent/team/company/org, dashboard-data.ts
was still pushing a fabricated "My Workspace → Direct → Ungrouped
(0 claws)" org so the sidebar always had *something*. It was
confusing after a full cascade-reap ("didn't we just delete
everything?") and the placeholder nodes couldn't be selected/deleted
either.
- dashboard-data.ts: drop the empty-workspace fallback (myClawsOrg
is no longer referenced; removed). Real emptiness now returns
{ orgs: [] }.
- Dashboard.tsx: when orgs.length === 0, replace the sidebar tree
with a friendly "NOTHING TO SHOW / Your workforce is empty / Hit
the + …" panel. Canvas gets a purposeful EmptyRosterStage with
a big + button that opens the same MasterPlannerModal the rail
already uses — one wizard, two entry points.
- Removed the old EmptyStage helper (no longer used).
This commit is contained in:
@@ -665,6 +665,17 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
<InfraNav view={infraSel ?? "local"} onSelect={setInfraSel} onConnectHost={() => setInfraConnectOpen(true)} />
|
<InfraNav view={infraSel ?? "local"} onSelect={setInfraSel} onConnectHost={() => setInfraConnectOpen(true)} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Empty roster — no real orgs/companies/teams/agents. Rather than
|
||||||
|
render an empty tree (or fake ones), point the user at the same
|
||||||
|
"+" deploy wizard the rail exposes. */}
|
||||||
|
{orgs.length === 0 ? (
|
||||||
|
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "24px 20px", gap: 10, textAlign: "center" }}>
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 10.5, letterSpacing: ".14em", color: "#5a5a62" }}>NOTHING TO SHOW</span>
|
||||||
|
<span style={{ fontSize: 13, color: "#cfcfd5", lineHeight: 1.55 }}>Your workforce is empty.</span>
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 11, color: "#8a8a92", lineHeight: 1.6, maxWidth: 200 }}>Hit the <span style={{ color: "#ff8a7a" }}>+</span> on the rail (or in the canvas) to add a new agent, team, company, or organization.</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
|
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
|
||||||
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => {
|
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => {
|
||||||
// Synthetic UI-only containers ("my-workspace", "ws-teams",
|
// Synthetic UI-only containers ("my-workspace", "ws-teams",
|
||||||
@@ -682,6 +693,8 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -885,7 +898,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyStage label="No agents yet — hit + to deploy your first agent." />
|
<EmptyRosterStage onAdd={() => setDeployOpen(true)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -953,10 +966,44 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyStage({ label }: { label: string }) {
|
/** Canvas empty state for a workspace with no orgs/companies/teams/agents.
|
||||||
|
* The + button opens the same MasterPlannerModal the rail's + does. */
|
||||||
|
function EmptyRosterStage({ onAdd }: { onAdd: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "radial-gradient(120% 90% at 50% 42%, #100e13 0%, #08080a 70%)" }}>
|
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 22, background: "radial-gradient(120% 90% at 50% 42%, #100e13 0%, #08080a 70%)" }}>
|
||||||
<span style={{ fontFamily: mono, fontSize: 12, color: "#5a5a62" }}>{label}</span>
|
<div style={{ fontFamily: mono, fontSize: 11, letterSpacing: ".18em", color: "#5a5a62" }}>YOUR WORKFORCE IS EMPTY</div>
|
||||||
|
<div style={{ fontSize: 22, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.015em", maxWidth: 480, textAlign: "center", lineHeight: 1.25 }}>
|
||||||
|
Add a new agent, team, company, or organization
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: mono, fontSize: 12, color: "#8a8a92", maxWidth: 460, textAlign: "center", lineHeight: 1.6 }}>
|
||||||
|
The <span style={{ color: "#ff8a7a" }}>+</span> below opens the deploy wizard — pitch it a team spec and it scaffolds a whole workforce in one pass.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAdd}
|
||||||
|
aria-label="Deploy a new agent"
|
||||||
|
title="Deploy — opens the same wizard as the rail's +"
|
||||||
|
style={{
|
||||||
|
width: 74,
|
||||||
|
height: 74,
|
||||||
|
borderRadius: 20,
|
||||||
|
border: "1px dashed rgba(255,111,97,.5)",
|
||||||
|
background: "rgba(255,111,97,.09)",
|
||||||
|
color: "#ff6f61",
|
||||||
|
fontSize: 34,
|
||||||
|
fontWeight: 200,
|
||||||
|
lineHeight: 1,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
transition: "transform .18s ease, background .18s ease, border-color .18s ease",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(255,111,97,.16)"; e.currentTarget.style.borderColor = "rgba(255,111,97,.8)"; e.currentTarget.style.transform = "scale(1.04)"; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(255,111,97,.09)"; e.currentTarget.style.borderColor = "rgba(255,111,97,.5)"; e.currentTarget.style.transform = "scale(1)"; }}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,22 +89,6 @@ async function buildOrg(id: string, name: string, clawById: Map<string, Agent>,
|
|||||||
return { id, name, topology: node?.kind || "federated", companies };
|
return { id, name, topology: node?.kind || "federated", companies };
|
||||||
}
|
}
|
||||||
|
|
||||||
function myClawsOrg(standalone: DemoAgent[]): DemoOrg {
|
|
||||||
// Holding area for claws not yet in a real team — NOT a dummy team: it's
|
|
||||||
// explicitly "Ungrouped" and empties out as claws are added to real teams
|
|
||||||
// (via "Add to teams"). Once every claw is in a real team, this disappears.
|
|
||||||
const running = standalone.some((c) => c.status === "running");
|
|
||||||
const team: DemoTeam = {
|
|
||||||
id: "ungrouped-team", name: "Ungrouped", topology: "flat",
|
|
||||||
status: running ? "running" : standalone.some((c) => c.status === "online") ? "online" : "idle",
|
|
||||||
dot: "#3a3a40", agents: standalone, groupApps: [],
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
id: "my-workspace", name: "My Workspace", topology: "flat",
|
|
||||||
companies: [{ id: "ungrouped-co", name: "Direct", topology: "flat", meta: `${standalone.length} ungrouped claw${standalone.length === 1 ? "" : "s"}`, teams: [team] }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load the live workspace as the dashboard's nested view-model, plus the raw
|
/** Load the live workspace as the dashboard's nested view-model, plus the raw
|
||||||
* claws (real `Agent`s) so the claw page can feed the computer + chat by id.
|
* claws (real `Agent`s) so the claw page can feed the computer + chat by id.
|
||||||
* Always returns at least the "My Claws" group so created claws are visible. */
|
* Always returns at least the "My Claws" group so created claws are visible. */
|
||||||
@@ -162,8 +146,10 @@ export async function loadWorkspace(): Promise<{ orgs: DemoOrg[]; claws: Agent[]
|
|||||||
}
|
}
|
||||||
if (synthCompanies.length > 0) {
|
if (synthCompanies.length > 0) {
|
||||||
populated.push({ id: "my-workspace", name: "My Workspace", topology: "flat", companies: synthCompanies });
|
populated.push({ id: "my-workspace", name: "My Workspace", topology: "flat", companies: synthCompanies });
|
||||||
} else if (populated.length === 0) {
|
|
||||||
populated.push(myClawsOrg([]));
|
|
||||||
}
|
}
|
||||||
|
// Fully-empty workspaces used to fall back to a fake "My Workspace / Direct
|
||||||
|
// / 0 ungrouped" org — visually confusing after a full reap. Return an
|
||||||
|
// empty roots list instead so the shell can render a proper empty state
|
||||||
|
// that points at the "+" deploy wizard.
|
||||||
return { orgs: populated, claws };
|
return { orgs: populated, claws };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user