Skip to content

Commit 800d52b

Browse files
committed
feat(web): refine hire agents pagination controls
1 parent 003ceaa commit 800d52b

File tree

6 files changed

+121
-22
lines changed

6 files changed

+121
-22
lines changed

typescript/clients/web-ag-ui/apps/web/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ NEXT_PUBLIC_DELEGATIONS_BYPASS=false
1010

1111
# Optional: which agent is selected by default.
1212
# NEXT_PUBLIC_DEFAULT_AGENT_ID=agent-clmm
13+
# Optional: enable extra mock Hire Agents rows for pagination QA.
14+
NEXT_PUBLIC_ENABLE_HIRE_AGENTS_PAGINATION_MOCKS=false
1315

1416
# Optional: polling intervals (ms) for sync refresh.
1517
# NEXT_PUBLIC_AGENT_LIST_SYNC_POLL_MS=15000

typescript/clients/web-ag-ui/apps/web/src/app/hire-agents/page.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { getAllAgents, getFeaturedAgents } from '@/config/agents';
77
import { canonicalizeChainLabel } from '@/utils/iconResolution';
88
import { mergeUniqueStrings, normalizeStringList } from '@/utils/agentCollections';
99

10+
const PAGINATION_QA_MOCK_COUNT = 27;
11+
const PAGINATION_QA_MOCKS_ENABLED =
12+
process.env.NEXT_PUBLIC_ENABLE_HIRE_AGENTS_PAGINATION_MOCKS === 'true';
13+
1014
export default function HireAgentsRoute() {
1115
const router = useRouter();
1216
const { agents: agentStates } = useAgentList();
@@ -63,6 +67,40 @@ export default function HireAgentsRoute() {
6367
};
6468
});
6569

70+
const paginationMockAgents: Agent[] = PAGINATION_QA_MOCKS_ENABLED
71+
? Array.from({ length: PAGINATION_QA_MOCK_COUNT }, (_, index) => {
72+
const ordinal = index + 1;
73+
const paddedOrdinal = ordinal.toString().padStart(2, '0');
74+
75+
return {
76+
id: `agent-mock-${paddedOrdinal}`,
77+
rank: registeredAgents.length + ordinal,
78+
name: `Mock Strategy ${paddedOrdinal}`,
79+
creator: 'Ember QA',
80+
creatorVerified: false,
81+
rating: undefined,
82+
weeklyIncome: 150 + ordinal * 11,
83+
apy: 4 + ((ordinal * 7) % 18),
84+
users: 20 + ordinal * 3,
85+
aum: 12_000 + ordinal * 1_250,
86+
chains: ['Arbitrum'],
87+
protocols: ordinal % 2 === 0 ? ['Camelot'] : ['Pendle'],
88+
tokens: ordinal % 3 === 0 ? ['USDC', 'ARB'] : ['USDC', 'WETH'],
89+
points: ordinal,
90+
pointsTrend: 'up',
91+
trendMultiplier: `${ordinal}x`,
92+
avatar: '🤖',
93+
avatarBg: 'linear-gradient(135deg, #334155 0%, #0f172a 100%)',
94+
status: 'for_hire',
95+
isActive: false,
96+
isFeatured: false,
97+
isLoaded: true,
98+
};
99+
})
100+
: [];
101+
102+
const agentListWithMocks = [...agentList, ...paginationMockAgents];
103+
66104
// Build featured agents list from config, prioritizing real data when available
67105
const featuredAgents: FeaturedAgent[] = featuredAgentConfigs.map((config) => {
68106
const listState = agentStates[config.id];
@@ -121,7 +159,7 @@ export default function HireAgentsRoute() {
121159

122160
return (
123161
<HireAgentsPage
124-
agents={agentList}
162+
agents={agentListWithMocks}
125163
featuredAgents={featuredAgents}
126164
onHireAgent={handleHireAgent}
127165
onViewAgent={handleViewAgent}

typescript/clients/web-ag-ui/apps/web/src/components/AgentRuntimeProvider.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ export function AgentRuntimeProvider({ children }: { children: ReactNode }) {
104104
}
105105

106106
return (
107-
<CopilotKit runtimeUrl="/api/copilotkit" useSingleEndpoint agent={agentId} threadId={threadId} key={threadId}>
107+
<CopilotKit
108+
runtimeUrl="/api/copilotkit"
109+
useSingleEndpoint
110+
agent={agentId}
111+
threadId={threadId}
112+
key={threadId}
113+
>
108114
<AgentProvider agentId={agentId}>
109115
<AgentListRuntimeBridge />
110116
{children}

typescript/clients/web-ag-ui/apps/web/src/components/HireAgentsPage.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,12 @@ export function HireAgentsPage({
152152

153153
const itemsPerPage = 10;
154154
const totalPages = Math.max(1, Math.ceil(filteredAgents.length / itemsPerPage));
155+
const shouldShowPagination = filteredAgents.length > itemsPerPage;
156+
const safeCurrentPage = Math.min(Math.max(currentPage, 1), totalPages);
157+
155158
const paginatedAgents = filteredAgents.slice(
156-
(currentPage - 1) * itemsPerPage,
157-
currentPage * itemsPerPage,
159+
(safeCurrentPage - 1) * itemsPerPage,
160+
safeCurrentPage * itemsPerPage,
158161
);
159162

160163
const iconDataSources = useMemo(() => [...agents, ...featuredAgents], [agents, featuredAgents]);
@@ -378,7 +381,13 @@ export function HireAgentsPage({
378381
/>
379382

380383
{/* Pagination */}
381-
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />
384+
{shouldShowPagination ? (
385+
<Pagination
386+
currentPage={safeCurrentPage}
387+
totalPages={totalPages}
388+
onPageChange={setCurrentPage}
389+
/>
390+
) : null}
382391
</div>
383392
</div>
384393
</div>

typescript/clients/web-ag-ui/apps/web/src/components/ui/Pagination.tsx

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChevronLeft, ChevronRight } from 'lucide-react';
1+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
22

33
interface PaginationProps {
44
currentPage: number;
@@ -7,52 +7,57 @@ interface PaginationProps {
77
}
88

99
export function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
10+
const normalizedTotalPages =
11+
Number.isFinite(totalPages) && totalPages > 0 ? Math.floor(totalPages) : 1;
12+
13+
if (normalizedTotalPages <= 1) {
14+
return null;
15+
}
16+
17+
const clampedCurrentPage = Math.min(Math.max(currentPage, 1), normalizedTotalPages);
18+
const canGoPrevious = clampedCurrentPage > 1;
19+
const canGoNext = clampedCurrentPage < normalizedTotalPages;
20+
1021
return (
1122
<div className="flex items-center justify-end gap-4 mt-5">
1223
<span className="text-[13px] text-gray-400">
13-
Page {currentPage} of {totalPages}
24+
Page {clampedCurrentPage} of {normalizedTotalPages}
1425
</span>
1526
<div className="flex items-center gap-1 rounded-full border border-white/10 bg-white/5 p-1">
16-
{/* First Page */}
1727
<button
1828
onClick={() => onPageChange(1)}
19-
disabled={currentPage === 1}
29+
disabled={!canGoPrevious}
2030
className="p-2 rounded-full hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
2131
aria-label="First page"
2232
>
23-
<ChevronLeft className="w-4 h-4" />
24-
<ChevronLeft className="w-4 h-4 -ml-2" />
33+
<ChevronsLeft className="w-4 h-4" />
2534
</button>
2635

27-
{/* Previous Page */}
2836
<button
29-
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
30-
disabled={currentPage === 1}
37+
onClick={() => onPageChange(Math.max(1, clampedCurrentPage - 1))}
38+
disabled={!canGoPrevious}
3139
className="p-2 rounded-full hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
3240
aria-label="Previous page"
3341
>
3442
<ChevronLeft className="w-4 h-4" />
3543
</button>
3644

37-
{/* Next Page */}
3845
<button
39-
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
40-
disabled={currentPage === totalPages}
46+
onClick={() => onPageChange(Math.min(normalizedTotalPages, clampedCurrentPage + 1))}
47+
disabled={!canGoNext}
4148
className="p-2 rounded-full hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
4249
aria-label="Next page"
4350
>
4451
<ChevronRight className="w-4 h-4" />
4552
</button>
4653

47-
{/* Last Page */}
4854
<button
49-
onClick={() => onPageChange(totalPages)}
50-
disabled={currentPage === totalPages}
55+
onClick={() => onPageChange(normalizedTotalPages)}
56+
disabled={!canGoNext}
5157
className="p-2 rounded-full hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
5258
aria-label="Last page"
5359
>
54-
<ChevronRight className="w-4 h-4" />
55-
<ChevronRight className="w-4 h-4 -ml-2" />
60+
<ChevronsRight className="w-4 h-4" />
5661
</button>
5762
</div>
5863
</div>

typescript/clients/web-ag-ui/apps/web/src/components/ui/Pagination.unit.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { Pagination } from './Pagination';
66

77
function getPaginationButtons(props: React.ComponentProps<typeof Pagination>) {
88
const tree = Pagination(props);
9+
if (tree === null) {
10+
return [];
11+
}
912
const controls = (tree.props.children as React.ReactNode[])[1] as React.ReactElement<{
1013
children: React.ReactNode[];
1114
}>;
@@ -15,6 +18,18 @@ function getPaginationButtons(props: React.ComponentProps<typeof Pagination>) {
1518
}
1619

1720
describe('Pagination', () => {
21+
it('does not render pagination when there is only one page', () => {
22+
const html = renderToStaticMarkup(
23+
React.createElement(Pagination, {
24+
currentPage: 1,
25+
totalPages: 1,
26+
onPageChange: () => {},
27+
}),
28+
);
29+
30+
expect(html).toBe('');
31+
});
32+
1833
it('renders pagination controls and current page text', () => {
1934
const html = renderToStaticMarkup(
2035
React.createElement(Pagination, {
@@ -78,4 +93,28 @@ describe('Pagination', () => {
7893
expect(onPageChange).toHaveBeenNthCalledWith(3, 5);
7994
expect(onPageChange).toHaveBeenNthCalledWith(4, 5);
8095
});
96+
97+
it('does not render when totalPages is invalid', () => {
98+
const html = renderToStaticMarkup(
99+
React.createElement(Pagination, {
100+
currentPage: 1,
101+
totalPages: Number.NaN,
102+
onPageChange: () => {},
103+
}),
104+
);
105+
106+
expect(html).toBe('');
107+
});
108+
109+
it('clamps currentPage to valid boundaries', () => {
110+
const html = renderToStaticMarkup(
111+
React.createElement(Pagination, {
112+
currentPage: 99,
113+
totalPages: 5,
114+
onPageChange: () => {},
115+
}),
116+
);
117+
118+
expect(html).toContain('Page 5 of 5');
119+
});
81120
});

0 commit comments

Comments
 (0)