Skip to content

Commit 952ae8b

Browse files
committed
feat: renderer adapter — 5 GenUI strategies with shared components and middleware
- RendererSelector: pill toggle for 5 modes (tool-based, A2UI, hashbrown, json-render, open-genui) - useRenderMode: state + localStorage + useAgentContext forwarding - 4 renderer adapters: ToolBasedDashboard, A2UIDashboard, HashBrownDashboard, OpenGenUIDashboard - json-render: deferred (Zod 4 isolation needed), documented architecture - New shared components: DealCard, Pipeline, MetricCard - Python render_mode middleware for context-driven output switching - 338 tests (181 React + 52 TS + 105 Python)
1 parent 5aed8ee commit 952ae8b

31 files changed

Lines changed: 2775 additions & 0 deletions

showcase/shared/frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"types": "src/index.ts",
77
"scripts": {},
88
"dependencies": {
9+
"@hashbrownai/core": "0.5.0-beta.4",
10+
"@hashbrownai/react": "0.5.0-beta.4",
911
"recharts": "^2.15.0",
1012
"zod": "^3.24.0"
1113
},
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { describe, it, expect } from "vitest";
2+
import { render, screen } from "@testing-library/react";
3+
import { DealCard } from "../../sales-dashboard/deal-card";
4+
import type { SalesTodo } from "../../../types";
5+
6+
const baseDeal: SalesTodo = {
7+
id: "1",
8+
title: "Follow up with Acme Corp",
9+
stage: "qualified",
10+
value: 50000,
11+
dueDate: "2026-04-20",
12+
assignee: "Alice",
13+
completed: false,
14+
};
15+
16+
describe("DealCard", () => {
17+
it("renders deal title", () => {
18+
render(<DealCard deal={baseDeal} />);
19+
expect(screen.getByText("Follow up with Acme Corp")).toBeTruthy();
20+
});
21+
22+
it("shows stage badge", () => {
23+
render(<DealCard deal={baseDeal} />);
24+
expect(screen.getByText("qualified")).toBeTruthy();
25+
});
26+
27+
// --- All 6 stage color variants ---
28+
29+
it("shows correct color class for prospect stage", () => {
30+
const deal = { ...baseDeal, stage: "prospect" as const };
31+
const { container } = render(<DealCard deal={deal} />);
32+
const badge = container.querySelector(".bg-blue-100");
33+
expect(badge).toBeTruthy();
34+
expect(badge!.textContent).toBe("prospect");
35+
});
36+
37+
it("shows correct color class for qualified stage", () => {
38+
const deal = { ...baseDeal, stage: "qualified" as const };
39+
const { container } = render(<DealCard deal={deal} />);
40+
const badge = container.querySelector(".bg-purple-100");
41+
expect(badge).toBeTruthy();
42+
expect(badge!.textContent).toBe("qualified");
43+
});
44+
45+
it("shows correct color class for proposal stage", () => {
46+
const deal = { ...baseDeal, stage: "proposal" as const };
47+
const { container } = render(<DealCard deal={deal} />);
48+
const badge = container.querySelector(".bg-amber-100");
49+
expect(badge).toBeTruthy();
50+
expect(badge!.textContent).toBe("proposal");
51+
});
52+
53+
it("shows correct color class for negotiation stage", () => {
54+
const deal = { ...baseDeal, stage: "negotiation" as const };
55+
const { container } = render(<DealCard deal={deal} />);
56+
const badge = container.querySelector(".bg-orange-100");
57+
expect(badge).toBeTruthy();
58+
expect(badge!.textContent).toBe("negotiation");
59+
});
60+
61+
it("shows correct color class for closed-won stage", () => {
62+
const deal = { ...baseDeal, stage: "closed-won" as const };
63+
const { container } = render(<DealCard deal={deal} />);
64+
const badge = container.querySelector(".bg-green-100");
65+
expect(badge).toBeTruthy();
66+
expect(badge!.textContent).toBe("closed-won");
67+
});
68+
69+
it("shows correct color class for closed-lost stage", () => {
70+
const deal = { ...baseDeal, stage: "closed-lost" as const };
71+
const { container } = render(<DealCard deal={deal} />);
72+
const badge = container.querySelector(".bg-red-100");
73+
expect(badge).toBeTruthy();
74+
expect(badge!.textContent).toBe("closed-lost");
75+
});
76+
77+
// --- Formatted currency ---
78+
79+
it("shows formatted currency value", () => {
80+
render(<DealCard deal={baseDeal} />);
81+
expect(screen.getByText("$50,000")).toBeTruthy();
82+
});
83+
84+
it("shows large formatted currency value", () => {
85+
const deal = { ...baseDeal, value: 1250000 };
86+
render(<DealCard deal={deal} />);
87+
expect(screen.getByText("$1,250,000")).toBeTruthy();
88+
});
89+
90+
it("shows zero value correctly", () => {
91+
const deal = { ...baseDeal, value: 0 };
92+
render(<DealCard deal={deal} />);
93+
expect(screen.getByText("$0")).toBeTruthy();
94+
});
95+
96+
// --- Assignee and due date ---
97+
98+
it("shows assignee", () => {
99+
render(<DealCard deal={baseDeal} />);
100+
expect(screen.getByText("Alice")).toBeTruthy();
101+
});
102+
103+
it("shows due date", () => {
104+
render(<DealCard deal={baseDeal} />);
105+
expect(screen.getByText("Due 2026-04-20")).toBeTruthy();
106+
});
107+
108+
it("hides assignee when not provided", () => {
109+
const deal = { ...baseDeal, assignee: "" };
110+
render(<DealCard deal={deal} />);
111+
// Empty string is falsy, so assignee span should not render
112+
expect(screen.queryByText("Alice")).toBeNull();
113+
});
114+
115+
it("hides due date when not provided", () => {
116+
const deal = { ...baseDeal, dueDate: "" };
117+
render(<DealCard deal={deal} />);
118+
expect(screen.queryByText(/^Due /)).toBeNull();
119+
});
120+
121+
// --- Completed state ---
122+
123+
it("applies reduced opacity when completed", () => {
124+
const completedDeal = { ...baseDeal, completed: true };
125+
const { container } = render(<DealCard deal={completedDeal} />);
126+
expect((container.firstChild as HTMLElement).className).toContain(
127+
"opacity-60",
128+
);
129+
});
130+
131+
it("does not apply reduced opacity when active", () => {
132+
const { container } = render(<DealCard deal={baseDeal} />);
133+
expect((container.firstChild as HTMLElement).className).not.toContain(
134+
"opacity-60",
135+
);
136+
});
137+
138+
it("shows line-through on title when completed", () => {
139+
const completedDeal = { ...baseDeal, completed: true };
140+
render(<DealCard deal={completedDeal} />);
141+
const title = screen.getByText("Follow up with Acme Corp");
142+
expect(title.className).toContain("line-through");
143+
});
144+
145+
it("does not show line-through on title when active", () => {
146+
render(<DealCard deal={baseDeal} />);
147+
const title = screen.getByText("Follow up with Acme Corp");
148+
expect(title.className).not.toContain("line-through");
149+
});
150+
151+
it("shows green indicator dot when active", () => {
152+
render(<DealCard deal={baseDeal} />);
153+
const indicator = screen.getByTestId("completion-indicator");
154+
expect(indicator.className).toContain("bg-green-500");
155+
});
156+
157+
it("shows muted indicator dot when completed", () => {
158+
const completedDeal = { ...baseDeal, completed: true };
159+
render(<DealCard deal={completedDeal} />);
160+
const indicator = screen.getByTestId("completion-indicator");
161+
expect(indicator.className).not.toContain("bg-green-500");
162+
expect(indicator.className).toContain("bg-[var(--muted-foreground)]");
163+
});
164+
165+
it("uses muted-foreground text color for title when completed", () => {
166+
const completedDeal = { ...baseDeal, completed: true };
167+
render(<DealCard deal={completedDeal} />);
168+
const title = screen.getByText("Follow up with Acme Corp");
169+
expect(title.className).toContain("text-[var(--muted-foreground)]");
170+
});
171+
172+
it("has deal-card test id", () => {
173+
render(<DealCard deal={baseDeal} />);
174+
expect(screen.getByTestId("deal-card")).toBeTruthy();
175+
});
176+
177+
it("has stage-badge test id", () => {
178+
render(<DealCard deal={baseDeal} />);
179+
expect(screen.getByTestId("stage-badge")).toBeTruthy();
180+
});
181+
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { describe, it, expect } from "vitest";
2+
import { render, screen } from "@testing-library/react";
3+
import { MetricCard } from "../../sales-dashboard/metric-card";
4+
5+
describe("MetricCard", () => {
6+
it("renders label", () => {
7+
render(<MetricCard label="Total Pipeline" value="$245,000" />);
8+
expect(screen.getByText("Total Pipeline")).toBeTruthy();
9+
});
10+
11+
it("renders value", () => {
12+
render(<MetricCard label="Total Pipeline" value="$245,000" />);
13+
expect(screen.getByText("$245,000")).toBeTruthy();
14+
});
15+
16+
it("renders without trend indicator by default", () => {
17+
render(<MetricCard label="Total Pipeline" value="$245,000" />);
18+
expect(screen.queryByTestId("trend-indicator")).toBeNull();
19+
});
20+
21+
// --- All 3 trend variants ---
22+
23+
it("shows upward trend indicator with green color", () => {
24+
render(
25+
<MetricCard
26+
label="Revenue"
27+
value="$100,000"
28+
trend={{ direction: "up", percentage: 12 }}
29+
/>,
30+
);
31+
const trend = screen.getByTestId("trend-indicator");
32+
expect(trend).toBeTruthy();
33+
expect(trend.textContent).toContain("12%");
34+
expect(trend.textContent).toContain("\u2191");
35+
expect(trend.className).toContain("text-green-600");
36+
});
37+
38+
it("shows downward trend indicator with red color", () => {
39+
render(
40+
<MetricCard
41+
label="Churn"
42+
value="5%"
43+
trend={{ direction: "down", percentage: 3 }}
44+
/>,
45+
);
46+
const trend = screen.getByTestId("trend-indicator");
47+
expect(trend.textContent).toContain("3%");
48+
expect(trend.textContent).toContain("\u2193");
49+
expect(trend.className).toContain("text-red-600");
50+
});
51+
52+
it("shows neutral trend indicator with muted color", () => {
53+
render(
54+
<MetricCard
55+
label="Deals"
56+
value="42"
57+
trend={{ direction: "neutral", percentage: 0 }}
58+
/>,
59+
);
60+
const trend = screen.getByTestId("trend-indicator");
61+
expect(trend.textContent).toContain("0%");
62+
expect(trend.textContent).toContain("\u2192");
63+
expect(trend.className).toContain("text-[var(--muted-foreground)]");
64+
});
65+
66+
// --- Large numbers ---
67+
68+
it("renders large number values correctly", () => {
69+
render(<MetricCard label="Revenue" value="$1,250,000" />);
70+
expect(screen.getByText("$1,250,000")).toBeTruthy();
71+
});
72+
73+
it("renders very large percentage in trend", () => {
74+
render(
75+
<MetricCard
76+
label="Growth"
77+
value="$500K"
78+
trend={{ direction: "up", percentage: 150 }}
79+
/>,
80+
);
81+
const trend = screen.getByTestId("trend-indicator");
82+
expect(trend.textContent).toContain("150%");
83+
});
84+
85+
it("renders decimal percentage in trend", () => {
86+
render(
87+
<MetricCard
88+
label="Conversion"
89+
value="3.5%"
90+
trend={{ direction: "up", percentage: 0.5 }}
91+
/>,
92+
);
93+
const trend = screen.getByTestId("trend-indicator");
94+
expect(trend.textContent).toContain("0.5%");
95+
});
96+
97+
// --- No trend default ---
98+
99+
it("does not render trend element when trend is undefined", () => {
100+
const { container } = render(<MetricCard label="Simple" value="100" />);
101+
const trendEl = container.querySelector("[data-testid='trend-indicator']");
102+
expect(trendEl).toBeNull();
103+
});
104+
105+
// --- Label styling ---
106+
107+
it("label has uppercase tracking-wider styling", () => {
108+
render(<MetricCard label="Test Label" value="999" />);
109+
const label = screen.getByText("Test Label");
110+
expect(label.className).toContain("uppercase");
111+
expect(label.className).toContain("tracking-wider");
112+
});
113+
114+
// --- Value styling ---
115+
116+
it("value has bold text-2xl styling", () => {
117+
render(<MetricCard label="Test" value="$1M" />);
118+
const value = screen.getByText("$1M");
119+
expect(value.className).toContain("text-2xl");
120+
expect(value.className).toContain("font-bold");
121+
});
122+
123+
it("has metric-card test id", () => {
124+
render(<MetricCard label="Test" value="123" />);
125+
expect(screen.getByTestId("metric-card")).toBeTruthy();
126+
});
127+
128+
// --- Card container ---
129+
130+
it("card has border and rounded styling", () => {
131+
render(<MetricCard label="Box" value="0" />);
132+
const card = screen.getByTestId("metric-card");
133+
expect(card.className).toContain("rounded-lg");
134+
expect(card.className).toContain("border");
135+
});
136+
});

0 commit comments

Comments
 (0)