@promptbook/cli 0.103.0-52 → 0.103.0-53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/config.ts +3 -3
  3. package/apps/agents-server/next.config.ts +1 -1
  4. package/apps/agents-server/public/sw.js +16 -0
  5. package/apps/agents-server/src/app/AddAgentButton.tsx +24 -4
  6. package/apps/agents-server/src/app/actions.ts +15 -13
  7. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
  8. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  9. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  10. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  11. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  12. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  13. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  14. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  15. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  16. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  19. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  27. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +11 -11
  28. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  32. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +41 -22
  37. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  38. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  39. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  40. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  41. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  42. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  43. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  44. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  45. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  46. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  47. package/apps/agents-server/src/app/docs/[docId]/page.tsx +62 -0
  48. package/apps/agents-server/src/app/docs/page.tsx +33 -0
  49. package/apps/agents-server/src/app/layout.tsx +29 -3
  50. package/apps/agents-server/src/app/manifest.ts +109 -0
  51. package/apps/agents-server/src/app/page.tsx +8 -45
  52. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  53. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  54. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  55. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  56. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  57. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  58. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  59. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  60. package/apps/agents-server/src/components/Header/Header.tsx +445 -79
  61. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  62. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  63. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  64. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  65. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  66. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  67. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +28 -3
  68. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  69. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  70. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  71. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  72. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  73. package/apps/agents-server/src/middleware.ts +146 -93
  74. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  75. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  76. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  77. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  78. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  79. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  80. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  81. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  82. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  83. package/apps/agents-server/vercel.json +7 -0
  84. package/esm/index.es.js +153 -2
  85. package/esm/index.es.js.map +1 -1
  86. package/esm/typings/servers.d.ts +8 -1
  87. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  88. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  89. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  90. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  91. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  92. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +4 -0
  93. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  94. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  95. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  96. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +28 -0
  97. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +28 -0
  98. package/esm/typings/src/commitments/index.d.ts +20 -1
  99. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  100. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  101. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  102. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  103. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  104. package/esm/typings/src/version.d.ts +1 -1
  105. package/package.json +1 -1
  106. package/umd/index.umd.js +153 -2
  107. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,541 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { Card } from '../../../components/Homepage/Card';
5
+ import {
6
+ $clearAgentChatFeedback,
7
+ $deleteChatFeedbackRow,
8
+ $fetchChatFeedback,
9
+ type ChatFeedbackRow,
10
+ type ChatFeedbackSortField,
11
+ type ChatFeedbackSortOrder,
12
+ } from '../../../utils/chatFeedbackAdmin';
13
+
14
+ type ChatFeedbackClientProps = {
15
+ /**
16
+ * Optional initial agent filter, taken from the URL query.
17
+ */
18
+ initialAgentName?: string;
19
+ };
20
+
21
+ type AdminAgentInfo = {
22
+ agentName: string;
23
+ fullname?: string | null;
24
+ };
25
+
26
+ type AgentsApiResponse = {
27
+ agents: Array<{
28
+ agentName: string;
29
+ meta: { fullname?: string | null };
30
+ }>;
31
+ };
32
+
33
+ function formatDate(dateString: string | null | undefined): string {
34
+ if (!dateString) return '-';
35
+ const date = new Date(dateString);
36
+ if (Number.isNaN(date.getTime())) return dateString;
37
+ return date.toLocaleString();
38
+ }
39
+
40
+ function getTextPreview(value: unknown, maxLength = 160): string {
41
+ if (value == null) return '-';
42
+
43
+ const text =
44
+ typeof value === 'string'
45
+ ? value
46
+ : Array.isArray(value)
47
+ ? value.map((part) => String(part)).join(' ')
48
+ : typeof value === 'object'
49
+ ? JSON.stringify(value)
50
+ : String(value);
51
+
52
+ return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
53
+ }
54
+
55
+ export function ChatFeedbackClient({ initialAgentName }: ChatFeedbackClientProps) {
56
+ const [items, setItems] = useState<ChatFeedbackRow[]>([]);
57
+ const [total, setTotal] = useState(0);
58
+ const [page, setPage] = useState(1);
59
+ const [pageSize, setPageSize] = useState(20);
60
+ const [agentName, setAgentName] = useState(initialAgentName ?? '');
61
+ const [searchInput, setSearchInput] = useState('');
62
+ const [search, setSearch] = useState('');
63
+ const [sortBy, setSortBy] = useState<ChatFeedbackSortField>('createdAt');
64
+ const [sortOrder, setSortOrder] = useState<ChatFeedbackSortOrder>('desc');
65
+ const [loading, setLoading] = useState(true);
66
+ const [error, setError] = useState<string | null>(null);
67
+
68
+ const [agents, setAgents] = useState<AdminAgentInfo[]>([]);
69
+ const [agentsLoading, setAgentsLoading] = useState(false);
70
+
71
+ // Load agents for filter dropdown
72
+ useEffect(() => {
73
+ let isCancelled = false;
74
+
75
+ async function loadAgents() {
76
+ try {
77
+ setAgentsLoading(true);
78
+ const response = await fetch('/api/agents');
79
+ if (!response.ok) {
80
+ // If agents listing fails, we can still show chat feedback
81
+ return;
82
+ }
83
+ const data = (await response.json()) as AgentsApiResponse;
84
+
85
+ if (isCancelled) return;
86
+
87
+ const mappedAgents: AdminAgentInfo[] =
88
+ data.agents?.map((agent) => ({
89
+ agentName: agent.agentName,
90
+ fullname: agent.meta?.fullname ?? null,
91
+ })) ?? [];
92
+
93
+ // Sort by display name
94
+ mappedAgents.sort((a, b) => {
95
+ const nameA = (a.fullname || a.agentName).toLowerCase();
96
+ const nameB = (b.fullname || b.agentName).toLowerCase();
97
+ return nameA.localeCompare(nameB);
98
+ });
99
+
100
+ setAgents(mappedAgents);
101
+ } finally {
102
+ if (!isCancelled) {
103
+ setAgentsLoading(false);
104
+ }
105
+ }
106
+ }
107
+
108
+ loadAgents();
109
+
110
+ return () => {
111
+ isCancelled = true;
112
+ };
113
+ }, []);
114
+
115
+ // Initial search input: we do not auto-search, but if initialAgentName is present we set it
116
+ useEffect(() => {
117
+ if (initialAgentName) {
118
+ setAgentName(initialAgentName);
119
+ }
120
+ }, [initialAgentName]);
121
+
122
+ // Load chat feedback whenever filters / pagination / sorting change
123
+ useEffect(() => {
124
+ let isCancelled = false;
125
+
126
+ async function loadData() {
127
+ try {
128
+ setLoading(true);
129
+ setError(null);
130
+
131
+ const response = await $fetchChatFeedback({
132
+ page,
133
+ pageSize,
134
+ agentName: agentName || undefined,
135
+ search: search || undefined,
136
+ sortBy,
137
+ sortOrder,
138
+ });
139
+
140
+ if (isCancelled) return;
141
+
142
+ setItems(response.items);
143
+ setTotal(response.total);
144
+ } catch (err) {
145
+ if (isCancelled) return;
146
+ setError(err instanceof Error ? err.message : 'Failed to load chat feedback');
147
+ } finally {
148
+ if (!isCancelled) {
149
+ setLoading(false);
150
+ }
151
+ }
152
+ }
153
+
154
+ loadData();
155
+
156
+ return () => {
157
+ isCancelled = true;
158
+ };
159
+ }, [page, pageSize, agentName, search, sortBy, sortOrder]);
160
+
161
+ const totalPages = useMemo(() => {
162
+ if (total <= 0 || pageSize <= 0) return 1;
163
+ return Math.max(1, Math.ceil(total / pageSize));
164
+ }, [total, pageSize]);
165
+
166
+ const handleSearchSubmit = (event: React.FormEvent) => {
167
+ event.preventDefault();
168
+ setPage(1);
169
+ setSearch(searchInput.trim());
170
+ };
171
+
172
+ const handleAgentChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
173
+ const value = event.target.value;
174
+ setAgentName(value);
175
+ setPage(1);
176
+ };
177
+
178
+ const handlePageSizeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
179
+ const next = parseInt(event.target.value, 10);
180
+ if (!Number.isNaN(next) && next > 0) {
181
+ setPageSize(next);
182
+ setPage(1);
183
+ }
184
+ };
185
+
186
+ const handleSortChange = (field: ChatFeedbackSortField) => {
187
+ if (sortBy === field) {
188
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
189
+ } else {
190
+ setSortBy(field);
191
+ setSortOrder(field === 'createdAt' ? 'desc' : 'asc');
192
+ }
193
+ };
194
+
195
+ const handleDeleteRow = async (row: ChatFeedbackRow) => {
196
+ if (!row.id) return;
197
+
198
+ const confirmed = window.confirm('Are you sure you want to delete this feedback entry?');
199
+ if (!confirmed) return;
200
+
201
+ try {
202
+ await $deleteChatFeedbackRow(row.id);
203
+ // Reload current page
204
+ const response = await $fetchChatFeedback({
205
+ page,
206
+ pageSize,
207
+ agentName: agentName || undefined,
208
+ search: search || undefined,
209
+ sortBy,
210
+ sortOrder,
211
+ });
212
+ setItems(response.items);
213
+ setTotal(response.total);
214
+ } catch (err) {
215
+ setError(err instanceof Error ? err.message : 'Failed to delete feedback entry');
216
+ }
217
+ };
218
+
219
+ const handleClearAgentFeedback = async () => {
220
+ if (!agentName) return;
221
+
222
+ const confirmed = window.confirm(
223
+ `Are you sure you want to permanently delete all feedback for agent "${agentName}"?`,
224
+ );
225
+ if (!confirmed) return;
226
+
227
+ try {
228
+ setLoading(true);
229
+ setError(null);
230
+ await $clearAgentChatFeedback(agentName);
231
+
232
+ // After clearing, reload first page
233
+ setPage(1);
234
+ const response = await $fetchChatFeedback({
235
+ page: 1,
236
+ pageSize,
237
+ agentName: agentName || undefined,
238
+ search: search || undefined,
239
+ sortBy,
240
+ sortOrder,
241
+ });
242
+ setItems(response.items);
243
+ setTotal(response.total);
244
+ } catch (err) {
245
+ setError(err instanceof Error ? err.message : 'Failed to clear feedback');
246
+ } finally {
247
+ setLoading(false);
248
+ }
249
+ };
250
+
251
+ const isSortedBy = (field: ChatFeedbackSortField) => sortBy === field;
252
+
253
+ return (
254
+ <div className="container mx-auto px-4 py-8 space-y-6">
255
+ <div className="mt-20 mb-4 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
256
+ <div>
257
+ <h1 className="text-3xl text-gray-900 font-light">Chat feedback</h1>
258
+ <p className="mt-1 text-sm text-gray-500">
259
+ Review and triage user feedback collected from your agents.
260
+ </p>
261
+ </div>
262
+ <div className="text-sm text-gray-500 md:text-right">
263
+ <div className="text-xl font-semibold text-gray-900">
264
+ {total.toLocaleString()}
265
+ </div>
266
+ <div className="text-xs uppercase tracking-wide text-gray-400">
267
+ Total feedback entries
268
+ </div>
269
+ </div>
270
+ </div>
271
+ <Card>
272
+ <div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
273
+ <form onSubmit={handleSearchSubmit} className="flex flex-col gap-2 md:flex-row md:items-end">
274
+ <div className="flex flex-col gap-1">
275
+ <label htmlFor="search" className="text-sm font-medium text-gray-700">
276
+ Search
277
+ </label>
278
+ <input
279
+ id="search"
280
+ type="text"
281
+ value={searchInput}
282
+ onChange={(event) => setSearchInput(event.target.value)}
283
+ placeholder="Search by agent, URL, IP, rating or note"
284
+ className="w-full md:w-72 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
285
+ />
286
+ </div>
287
+ <button
288
+ type="submit"
289
+ className="mt-2 inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 md:mt-0 md:ml-3"
290
+ >
291
+ Apply
292
+ </button>
293
+ </form>
294
+
295
+ <div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-4">
296
+ <div className="flex flex-col gap-1">
297
+ <label htmlFor="agentFilter" className="text-sm font-medium text-gray-700">
298
+ Agent filter
299
+ </label>
300
+ <select
301
+ id="agentFilter"
302
+ value={agentName}
303
+ onChange={handleAgentChange}
304
+ className="w-full md:w-64 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
305
+ >
306
+ <option value="">All agents</option>
307
+ {agents.map((agent) => (
308
+ <option key={agent.agentName} value={agent.agentName}>
309
+ {agent.fullname || agent.agentName}
310
+ </option>
311
+ ))}
312
+ </select>
313
+ {agentsLoading && (
314
+ <span className="text-xs text-gray-400">Loading agents…</span>
315
+ )}
316
+ </div>
317
+
318
+ <div className="flex flex-col gap-1">
319
+ <label htmlFor="pageSize" className="text-sm font-medium text-gray-700">
320
+ Page size
321
+ </label>
322
+ <select
323
+ id="pageSize"
324
+ value={pageSize}
325
+ onChange={handlePageSizeChange}
326
+ className="w-28 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
327
+ >
328
+ <option value={10}>10</option>
329
+ <option value={20}>20</option>
330
+ <option value={50}>50</option>
331
+ <option value={100}>100</option>
332
+ </select>
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ {agentName && (
338
+ <div className="mt-4 flex items-center justify-between gap-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3">
339
+ <p className="text-sm text-amber-800">
340
+ Showing feedback for agent{' '}
341
+ <span className="font-semibold break-all">{agentName}</span>.
342
+ </p>
343
+ <button
344
+ type="button"
345
+ onClick={handleClearAgentFeedback}
346
+ className="inline-flex items-center justify-center rounded-md border border-red-300 bg-white px-3 py-1.5 text-xs font-medium text-red-700 hover:bg-red-50"
347
+ >
348
+ Clear feedback for this agent
349
+ </button>
350
+ </div>
351
+ )}
352
+ </Card>
353
+
354
+ <Card>
355
+ <div className="flex items-center justify-between mb-4">
356
+ <h2 className="text-lg font-medium text-gray-900">
357
+ Feedback entries ({total})
358
+ </h2>
359
+ </div>
360
+ {error && (
361
+ <div className="mb-4 rounded-md bg-red-50 px-4 py-3 text-sm text-red-800">
362
+ {error}
363
+ </div>
364
+ )}
365
+
366
+ {loading && items.length === 0 ? (
367
+ <div className="py-8 text-center text-gray-500">Loading feedback…</div>
368
+ ) : items.length === 0 ? (
369
+ <div className="py-8 text-center text-gray-500">No feedback found.</div>
370
+ ) : (
371
+ <div className="overflow-x-auto">
372
+ <table className="min-w-full divide-y divide-gray-200 text-sm">
373
+ <thead className="bg-gray-50">
374
+ <tr>
375
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
376
+ <button
377
+ type="button"
378
+ onClick={() => handleSortChange('createdAt')}
379
+ className="inline-flex items-center gap-1"
380
+ >
381
+ Time
382
+ {isSortedBy('createdAt') && (
383
+ <span>{sortOrder === 'asc' ? '▲' : '▼'}</span>
384
+ )}
385
+ </button>
386
+ </th>
387
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
388
+ <button
389
+ type="button"
390
+ onClick={() => handleSortChange('agentName')}
391
+ className="inline-flex items-center gap-1"
392
+ >
393
+ Agent
394
+ {isSortedBy('agentName') && (
395
+ <span>{sortOrder === 'asc' ? '▲' : '▼'}</span>
396
+ )}
397
+ </button>
398
+ </th>
399
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
400
+ Rating
401
+ </th>
402
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
403
+ Text rating
404
+ </th>
405
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
406
+ User note
407
+ </th>
408
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
409
+ Expected answer
410
+ </th>
411
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
412
+ URL
413
+ </th>
414
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
415
+ IP
416
+ </th>
417
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
418
+ Language
419
+ </th>
420
+ <th className="px-4 py-3 text-left font-medium text-gray-500">
421
+ Platform
422
+ </th>
423
+ <th className="px-4 py-3 text-right font-medium text-gray-500">
424
+ Actions
425
+ </th>
426
+ </tr>
427
+ </thead>
428
+ <tbody className="divide-y divide-gray-200 bg-white">
429
+ {items.map((row) => (
430
+ <tr key={row.id}>
431
+ <td className="whitespace-nowrap px-4 py-3 text-gray-700">
432
+ {formatDate(row.createdAt)}
433
+ </td>
434
+ <td className="whitespace-nowrap px-4 py-3 text-gray-700">
435
+ {row.agentName}
436
+ </td>
437
+ <td className="whitespace-nowrap px-4 py-3 text-gray-700">
438
+ {row.rating || '-'}
439
+ </td>
440
+ <td className="max-w-xs px-4 py-3 text-gray-700">
441
+ <div className="max-h-24 overflow-hidden overflow-ellipsis text-xs leading-snug">
442
+ {row.textRating ? getTextPreview(row.textRating) : '-'}
443
+ </div>
444
+ </td>
445
+ <td className="max-w-xs px-4 py-3 text-gray-700">
446
+ <div className="max-h-24 overflow-hidden overflow-ellipsis text-xs leading-snug">
447
+ {row.userNote ? getTextPreview(row.userNote) : '-'}
448
+ </div>
449
+ </td>
450
+ <td className="max-w-xs px-4 py-3 text-gray-700">
451
+ <div className="max-h-24 overflow-hidden overflow-ellipsis text-xs leading-snug">
452
+ {row.expectedAnswer ? getTextPreview(row.expectedAnswer) : '-'}
453
+ </div>
454
+ </td>
455
+ <td className="max-w-xs px-4 py-3 text-gray-500">
456
+ <div className="truncate text-xs">
457
+ {row.url || '-'}
458
+ </div>
459
+ </td>
460
+ <td className="whitespace-nowrap px-4 py-3 text-gray-500">
461
+ {row.ip || '-'}
462
+ </td>
463
+ <td className="whitespace-nowrap px-4 py-3 text-gray-500">
464
+ {row.language || '-'}
465
+ </td>
466
+ <td className="max-w-xs px-4 py-3 text-gray-500">
467
+ <div className="truncate text-xs">
468
+ {row.platform || '-'}
469
+ </div>
470
+ </td>
471
+ <td className="whitespace-nowrap px-4 py-3 text-right text-xs font-medium">
472
+ <button
473
+ type="button"
474
+ onClick={() => handleDeleteRow(row)}
475
+ className="text-red-600 hover:text-red-800"
476
+ >
477
+ Delete
478
+ </button>
479
+ </td>
480
+ </tr>
481
+ ))}
482
+ </tbody>
483
+ </table>
484
+ </div>
485
+ )}
486
+
487
+ <div className="mt-4 flex flex-col items-center justify-between gap-3 border-t border-gray-100 pt-4 text-xs text-gray-600 md:flex-row">
488
+ <div>
489
+ {total > 0 ? (
490
+ <>
491
+ Showing{' '}
492
+ <span className="font-semibold">
493
+ {Math.min((page - 1) * pageSize + 1, total)}
494
+ </span>{' '}
495
+ –{' '}
496
+ <span className="font-semibold">
497
+ {Math.min(page * pageSize, total)}
498
+ </span>{' '}
499
+ of{' '}
500
+ <span className="font-semibold">
501
+ {total}
502
+ </span>{' '}
503
+ feedback entries
504
+ </>
505
+ ) : (
506
+ 'No feedback'
507
+ )}
508
+ </div>
509
+ <div className="flex items-center gap-2">
510
+ <button
511
+ type="button"
512
+ onClick={() => setPage((prev) => Math.max(1, prev - 1))}
513
+ disabled={page <= 1}
514
+ className="inline-flex items-center justify-center rounded-md border border-gray-300 px-2 py-1 text-xs font-medium text-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
515
+ >
516
+ Previous
517
+ </button>
518
+ <span>
519
+ Page{' '}
520
+ <span className="font-semibold">
521
+ {page}
522
+ </span>{' '}
523
+ of{' '}
524
+ <span className="font-semibold">
525
+ {totalPages}
526
+ </span>
527
+ </span>
528
+ <button
529
+ type="button"
530
+ onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
531
+ disabled={page >= totalPages}
532
+ className="inline-flex items-center justify-center rounded-md border border-gray-300 px-2 py-1 text-xs font-medium text-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
533
+ >
534
+ Next
535
+ </button>
536
+ </div>
537
+ </div>
538
+ </Card>
539
+ </div>
540
+ );
541
+ }
@@ -0,0 +1,22 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
3
+ import { ChatFeedbackClient } from './ChatFeedbackClient';
4
+
5
+ type AdminChatFeedbackPageProps = {
6
+ searchParams?: Promise<{
7
+ agentName?: string;
8
+ }>;
9
+ };
10
+
11
+ export default async function AdminChatFeedbackPage({ searchParams }: AdminChatFeedbackPageProps) {
12
+ const isAdmin = await isUserAdmin();
13
+
14
+ if (!isAdmin) {
15
+ return <ForbiddenPage />;
16
+ }
17
+
18
+ const resolvedSearchParams = searchParams ? await searchParams : undefined;
19
+ const initialAgentName = resolvedSearchParams?.agentName || undefined;
20
+
21
+ return <ChatFeedbackClient initialAgentName={initialAgentName} />;
22
+ }