Skip to content

Commit 1a5fa3b

Browse files
committed
feat: replace empty state in recent outputs with proof-of-value gallery
Show 6 example output types (Hero Rewrite Draft, SEO Opportunity Map, etc.) with specialist attribution and color-coded type badges when no outputs exist, replacing the dead "No outputs yet" text with actionable content.
1 parent b2721ec commit 1a5fa3b

File tree

4 files changed

+165
-23
lines changed

4 files changed

+165
-23
lines changed

apps/desktop/src/features/dashboard/components/DashboardWorkspace.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ function DashboardContent({
372372
agentId={agentId}
373373
client={client}
374374
onNavigate={handleOutputNavigate}
375+
onSpecialistChat={handleSpecialistChat}
375376
/>
376377

377378
{/* Action cards — secondary in Mode B */}
@@ -436,6 +437,7 @@ function DashboardContent({
436437
agentId={agentId}
437438
client={client}
438439
onNavigate={handleOutputNavigate}
440+
onSpecialistChat={handleSpecialistChat}
439441
/>
440442

441443
{/* Board summary — bottom, only if tasks exist */}

apps/desktop/src/features/dashboard/components/RecentOutputs.tsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
1-
import { PackageIcon } from "lucide-react";
1+
import { ArrowRightIcon, PackageIcon } from "lucide-react";
22
import type { ArtifactRecord } from "@opengoat/contracts";
33
import type { SidecarClient } from "@/lib/sidecar/client";
44
import { useRecentArtifacts } from "@/features/dashboard/hooks/useRecentArtifacts";
55
import { ArtifactCard } from "@/features/dashboard/components/ArtifactCard";
66
import { BundleCard } from "@/features/dashboard/components/BundleCard";
77
import { getSpecialistMeta, SPECIALIST_META } from "@/features/agents/specialist-meta";
8+
import { getSpecialistColors } from "@/features/agents/specialist-meta";
9+
import { getArtifactTypeConfig } from "@/features/dashboard/lib/artifact-type-config";
10+
11+
const EXAMPLE_OUTPUTS: Array<{
12+
name: string;
13+
type: string;
14+
specialistId: string;
15+
}> = [
16+
{ name: "Hero Rewrite Draft", type: "copy_draft", specialistId: "website-conversion" },
17+
{ name: "SEO Opportunity Map", type: "research_brief", specialistId: "seo-aeo" },
18+
{ name: "Competitor Messaging Matrix", type: "matrix", specialistId: "market-intel" },
19+
{ name: "Product Hunt Launch Pack", type: "launch_pack", specialistId: "distribution" },
20+
{ name: "Content Ideas Bundle", type: "content_calendar", specialistId: "content" },
21+
{ name: "Cold Email Sequence", type: "email_sequence", specialistId: "outbound" },
22+
];
823

924
export interface RecentOutputsProps {
1025
agentId: string;
1126
client: SidecarClient;
1227
onPreview?: (artifactId: string) => void;
1328
onNavigate?: (artifact: ArtifactRecord) => void;
29+
onSpecialistChat?: (specialistId: string) => void;
1430
}
1531

1632
/** Resolve a specialist display name from an artifact's createdBy field. */
@@ -27,7 +43,7 @@ function resolveSpecialistName(createdBy: string): string | undefined {
2743
return undefined;
2844
}
2945

30-
export function RecentOutputs({ agentId, client, onPreview, onNavigate }: RecentOutputsProps) {
46+
export function RecentOutputs({ agentId, client, onPreview, onNavigate, onSpecialistChat }: RecentOutputsProps) {
3147
const { standaloneArtifacts, bundleGroups, isLoading, isEmpty } = useRecentArtifacts(agentId, client);
3248

3349
// Avoid layout flash — return null while loading
@@ -70,11 +86,53 @@ export function RecentOutputs({ agentId, client, onPreview, onNavigate }: Recent
7086
) : null}
7187
</div>
7288

73-
{/* Empty state */}
89+
{/* Empty state — proof-of-value gallery */}
7490
{isEmpty ? (
75-
<p className="text-xs text-muted-foreground/50 ml-9">
76-
No outputs yet — run an action above to get started
77-
</p>
91+
<div className="ml-9">
92+
<p className="mb-3 text-[13px] font-medium text-muted-foreground">
93+
Your team can produce:
94+
</p>
95+
<div className="grid grid-cols-2 gap-2">
96+
{EXAMPLE_OUTPUTS.map((example, idx) => {
97+
const typeConfig = getArtifactTypeConfig(example.type);
98+
const specialist = getSpecialistMeta(example.specialistId);
99+
const colors = getSpecialistColors(example.specialistId);
100+
const isStartCard = idx === 0;
101+
102+
return (
103+
<button
104+
key={example.name}
105+
type="button"
106+
className="group/example flex flex-col items-start gap-1.5 rounded-lg border border-dashed border-border/40 p-3 text-left transition-colors hover:border-border/70 hover:bg-muted/30"
107+
onClick={() => onSpecialistChat?.(example.specialistId)}
108+
>
109+
{/* Type badge */}
110+
<span className={`inline-block rounded px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider ${typeConfig.badgeClassName}`}>
111+
{typeConfig.label}
112+
</span>
113+
114+
{/* Output name */}
115+
<span className="text-[13px] font-medium leading-tight text-foreground">
116+
{example.name}
117+
</span>
118+
119+
{/* Specialist attribution */}
120+
<span className={`text-[11px] ${colors.iconText}`}>
121+
{specialist?.name}
122+
</span>
123+
124+
{/* Start CTA — first card only */}
125+
{isStartCard && (
126+
<span className="mt-0.5 inline-flex items-center gap-1 text-[11px] font-medium text-primary opacity-70 transition-opacity group-hover/example:opacity-100">
127+
Start here
128+
<ArrowRightIcon className="size-3 transition-transform group-hover/example:translate-x-0.5" />
129+
</span>
130+
)}
131+
</button>
132+
);
133+
})}
134+
</div>
135+
</div>
78136
) : (
79137
/* Outputs list */
80138
<div className="space-y-3">

test/features/dashboard/RecentOutputs.test.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,25 @@ const bundleCardSrc = readSrc("components/BundleCard.tsx");
1616
const dashboardSrc = readSrc("components/DashboardWorkspace.tsx");
1717

1818
// ---------------------------------------------------------------------------
19-
// RecentOutputs — empty state (compact inline)
19+
// RecentOutputs — empty state (proof-of-value gallery)
2020
// ---------------------------------------------------------------------------
2121
describe("RecentOutputs empty state", () => {
22-
it("shows 'No outputs yet' message when empty", () => {
23-
expect(componentSrc).toContain("No outputs yet");
22+
it("shows proof-of-value gallery heading when empty", () => {
23+
expect(componentSrc).toContain("Your team can produce");
2424
});
2525

26-
it("suggests running an action to get started", () => {
27-
expect(
28-
componentSrc.includes("run an action") ||
29-
componentSrc.includes("get started"),
30-
).toBe(true);
26+
it("shows example output names in empty state gallery", () => {
27+
expect(componentSrc).toContain("Hero Rewrite Draft");
28+
expect(componentSrc).toContain("SEO Opportunity Map");
29+
expect(componentSrc).toContain("Competitor Messaging Matrix");
3130
});
3231

33-
it("uses compact inline text instead of tall dashed-border card", () => {
34-
// Should NOT have the old tall dashed-border empty state container
35-
expect(componentSrc).not.toContain("border-dashed");
36-
// Should NOT have the stacked vertical layout with centered icon
37-
expect(componentSrc).not.toContain("flex-col items-center gap-2");
38-
expect(componentSrc).not.toContain("py-8");
32+
it("uses 2-column grid layout for example outputs", () => {
33+
expect(componentSrc).toContain("grid-cols-2");
3934
});
4035

41-
it("renders empty state as a single muted paragraph", () => {
42-
// The empty state should be a compact <p> with muted styling
43-
expect(componentSrc).toMatch(/isEmpty[\s\S]*?<p\s+className="[^"]*text-muted-foreground/);
36+
it("includes a Start CTA for routing to specialist", () => {
37+
expect(componentSrc).toContain("Start here");
4438
});
4539
});
4640

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readFileSync } from "node:fs";
3+
import { resolve } from "node:path";
4+
5+
const desktopSrc = resolve(__dirname, "../../apps/desktop/src");
6+
const readFile = (relPath: string) =>
7+
readFileSync(resolve(desktopSrc, relPath), "utf-8");
8+
9+
const COMPONENT_PATH = "features/dashboard/components/RecentOutputs.tsx";
10+
const DASHBOARD_PATH = "features/dashboard/components/DashboardWorkspace.tsx";
11+
12+
// ═══════════════════════════════════════════════════════
13+
// RecentOutputs — structure validation
14+
// ═══════════════════════════════════════════════════════
15+
16+
describe("RecentOutputs structure", () => {
17+
it("exports a named function", () => {
18+
const src = readFile(COMPONENT_PATH);
19+
expect(src).toContain("export function RecentOutputs");
20+
});
21+
22+
it("accepts onSpecialistChat prop", () => {
23+
const src = readFile(COMPONENT_PATH);
24+
expect(src).toContain("onSpecialistChat");
25+
});
26+
});
27+
28+
// ═══════════════════════════════════════════════════════
29+
// RecentOutputs — proof-of-value empty state
30+
// ═══════════════════════════════════════════════════════
31+
32+
describe("RecentOutputs proof-of-value empty state", () => {
33+
it("defines EXAMPLE_OUTPUTS constant", () => {
34+
const src = readFile(COMPONENT_PATH);
35+
expect(src).toContain("EXAMPLE_OUTPUTS");
36+
});
37+
38+
it("includes all 6 human-readable example output names", () => {
39+
const src = readFile(COMPONENT_PATH);
40+
expect(src).toContain("Hero Rewrite Draft");
41+
expect(src).toContain("Competitor Messaging Matrix");
42+
expect(src).toContain("SEO Opportunity Map");
43+
expect(src).toContain("Product Hunt Launch Pack");
44+
expect(src).toContain("Content Ideas Bundle");
45+
expect(src).toContain("Cold Email Sequence");
46+
});
47+
48+
it("imports getArtifactTypeConfig for type badge colors", () => {
49+
const src = readFile(COMPONENT_PATH);
50+
expect(src).toContain("getArtifactTypeConfig");
51+
});
52+
53+
it("imports getSpecialistMeta for specialist attribution", () => {
54+
const src = readFile(COMPONENT_PATH);
55+
expect(src).toContain("getSpecialistMeta");
56+
});
57+
58+
it("imports getSpecialistColors for specialist color tokens", () => {
59+
const src = readFile(COMPONENT_PATH);
60+
expect(src).toContain("getSpecialistColors");
61+
});
62+
63+
it("uses grid-cols-2 for 2-column gallery layout", () => {
64+
const src = readFile(COMPONENT_PATH);
65+
expect(src).toContain("grid-cols-2");
66+
});
67+
68+
it("includes a Start CTA in the empty state", () => {
69+
const src = readFile(COMPONENT_PATH);
70+
expect(src).toMatch(/Start/);
71+
});
72+
73+
it("shows 'Your team can produce' heading", () => {
74+
const src = readFile(COMPONENT_PATH);
75+
expect(src).toContain("Your team can produce");
76+
});
77+
});
78+
79+
// ═══════════════════════════════════════════════════════
80+
// DashboardWorkspace — prop threading
81+
// ═══════════════════════════════════════════════════════
82+
83+
describe("DashboardWorkspace passes onSpecialistChat to RecentOutputs", () => {
84+
it("threads handleSpecialistChat as onSpecialistChat prop", () => {
85+
const src = readFile(DASHBOARD_PATH);
86+
expect(src).toContain("onSpecialistChat={handleSpecialistChat}");
87+
});
88+
});

0 commit comments

Comments
 (0)