@promptbook/cli 0.103.0-51 → 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 (114) 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]/AgentQrCode.tsx +3 -3
  17. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  19. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  28. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +12 -12
  29. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  32. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +46 -24
  38. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  39. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  40. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  41. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  42. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  43. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  44. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  45. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  46. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  47. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  48. package/apps/agents-server/src/app/docs/[docId]/page.tsx +62 -0
  49. package/apps/agents-server/src/app/docs/page.tsx +33 -0
  50. package/apps/agents-server/src/app/layout.tsx +29 -3
  51. package/apps/agents-server/src/app/manifest.ts +109 -0
  52. package/apps/agents-server/src/app/page.tsx +8 -45
  53. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  54. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  55. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  56. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  57. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  58. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  59. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  60. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  61. package/apps/agents-server/src/components/Header/Header.tsx +445 -79
  62. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  63. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  64. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  65. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  66. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  67. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  68. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +28 -3
  69. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  70. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  71. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  72. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  73. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  74. package/apps/agents-server/src/middleware.ts +146 -93
  75. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  76. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  77. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  78. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  79. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  80. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  81. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  82. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  83. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  84. package/apps/agents-server/vercel.json +7 -0
  85. package/esm/index.es.js +344 -11
  86. package/esm/index.es.js.map +1 -1
  87. package/esm/typings/servers.d.ts +8 -1
  88. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  89. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  90. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  91. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  92. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +1 -0
  93. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  94. package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +2 -2
  95. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +10 -0
  96. package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +6 -0
  97. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  98. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  99. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  100. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +28 -0
  101. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +28 -0
  102. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +38 -0
  103. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +2 -1
  104. package/esm/typings/src/commitments/index.d.ts +22 -1
  105. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  106. package/esm/typings/src/llm-providers/agent/Agent.d.ts +1 -0
  107. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  108. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  109. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  110. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  111. package/esm/typings/src/version.d.ts +1 -1
  112. package/package.json +1 -1
  113. package/umd/index.umd.js +344 -11
  114. package/umd/index.umd.js.map +1 -1
@@ -1,13 +1,19 @@
1
1
  'use client';
2
2
 
3
3
  import promptbookLogoBlueTransparent from '@/public/logo-blue-white-256.png';
4
- import { logoutAction } from '@/src/app/actions';
5
- import { ArrowRight, LogIn, LogOut, Menu, X } from 'lucide-react';
4
+ import { $createAgentAction, logoutAction } from '@/src/app/actions';
5
+ import { ArrowRight, ChevronDown, LogIn, LogOut } from 'lucide-react';
6
6
  import Image from 'next/image';
7
7
  import Link from 'next/link';
8
- import { useState } from 'react';
8
+ import { useRouter } from 'next/navigation';
9
+ import { ReactNode, useState } from 'react';
10
+ import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
11
+ import { HamburgerMenu } from '../../../../../src/book-components/_common/HamburgerMenu/HamburgerMenu';
9
12
  import { just } from '../../../../../src/utils/organization/just';
13
+ import type { UserInfo } from '../../utils/getCurrentUser';
14
+ import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
10
15
  import { LoginDialog } from '../LoginDialog/LoginDialog';
16
+ import { useUsersAdmin } from '../UsersList/useUsersAdmin';
11
17
 
12
18
  type HeaderProps = {
13
19
  /**
@@ -15,6 +21,11 @@ type HeaderProps = {
15
21
  */
16
22
  isAdmin?: boolean;
17
23
 
24
+ /**
25
+ * Current user info (if logged in)
26
+ */
27
+ currentUser?: UserInfo | null;
28
+
18
29
  /**
19
30
  * The name of the server
20
31
  */
@@ -24,74 +35,304 @@ type HeaderProps = {
24
35
  * The URL of the logo displayed in the heading bar
25
36
  */
26
37
  serverLogoUrl: string | null;
38
+
39
+ /**
40
+ * List of agents
41
+ */
42
+ agents: Array<AgentBasicInformation>;
27
43
  };
28
44
 
29
45
  /* TODO: [🐱‍🚀] Make this Agents server native */
30
46
 
47
+ type SubMenuItem = {
48
+ label: ReactNode;
49
+ href?: string;
50
+ onClick?: () => void | Promise<void>;
51
+ isBold?: boolean;
52
+ isBordered?: boolean;
53
+ };
54
+
55
+ type MenuItem =
56
+ | {
57
+ type: 'link';
58
+ label: ReactNode;
59
+ href: string;
60
+ }
61
+ | {
62
+ type: 'dropdown';
63
+ label: ReactNode;
64
+ isOpen: boolean;
65
+ setIsOpen: (isOpen: boolean) => void;
66
+ isMobileOpen: boolean;
67
+ setIsMobileOpen: (isOpen: boolean) => void;
68
+ items: Array<SubMenuItem>;
69
+ };
70
+
31
71
  export function Header(props: HeaderProps) {
32
- const { isAdmin = false, serverName, serverLogoUrl } = props;
72
+ const { isAdmin = false, currentUser = null, serverName, serverLogoUrl, agents } = props;
33
73
 
34
74
  const [isMenuOpen, setIsMenuOpen] = useState(false);
35
75
  const [isLoginOpen, setIsLoginOpen] = useState(false);
76
+ const [isAgentsOpen, setIsAgentsOpen] = useState(false);
77
+ const [isDocsOpen, setIsDocsOpen] = useState(false);
78
+ const [isUsersOpen, setIsUsersOpen] = useState(false);
79
+ const [isMobileAgentsOpen, setIsMobileAgentsOpen] = useState(false);
80
+ const [isMobileDocsOpen, setIsMobileDocsOpen] = useState(false);
81
+ const [isMobileUsersOpen, setIsMobileUsersOpen] = useState(false);
82
+ const [isCreatingAgent, setIsCreatingAgent] = useState(false);
83
+ const router = useRouter();
84
+
85
+ const { users: adminUsers } = useUsersAdmin();
36
86
 
37
87
  const handleLogout = async () => {
38
88
  await logoutAction();
39
89
  };
40
90
 
91
+ const handleCreateAgent = async () => {
92
+ setIsCreatingAgent(true);
93
+ const agentName = await $createAgentAction();
94
+
95
+ if (agentName) {
96
+ router.push(`/agents/${agentName}`);
97
+ setIsAgentsOpen(false);
98
+ setIsMenuOpen(false);
99
+ } else {
100
+ router.refresh();
101
+ setIsCreatingAgent(false);
102
+ setIsAgentsOpen(false);
103
+ setIsMenuOpen(false);
104
+ }
105
+ };
106
+
107
+ // Menu items configuration (DRY principle)
108
+ const menuItems: MenuItem[] = [
109
+ {
110
+ type: 'link' as const,
111
+ label: 'Home',
112
+ href: '/',
113
+ },
114
+ {
115
+ type: 'dropdown' as const,
116
+ label: 'Documentation',
117
+ isOpen: isDocsOpen,
118
+ setIsOpen: setIsDocsOpen,
119
+ isMobileOpen: isMobileDocsOpen,
120
+ setIsMobileOpen: setIsMobileDocsOpen,
121
+ items: [
122
+ {
123
+ label: 'Overview',
124
+ href: '/docs',
125
+ isBold: true,
126
+ isBordered: true,
127
+ } as SubMenuItem,
128
+ ...getVisibleCommitmentDefinitions().map(
129
+ ({ primary, aliases }) =>
130
+ ({
131
+ label: (
132
+ <>
133
+ {primary.type}
134
+ {aliases.length > 0 && <span className="text-gray-400 font-normal"> / {aliases.join(' / ')}</span>}
135
+ </>
136
+ ),
137
+ href: `/docs/${primary.type}`,
138
+ } as SubMenuItem),
139
+ ),
140
+ ],
141
+ },
142
+ ...(isAdmin
143
+ ? [
144
+ {
145
+ type: 'dropdown' as const,
146
+ label: 'Agents',
147
+ isOpen: isAgentsOpen,
148
+ setIsOpen: setIsAgentsOpen,
149
+ isMobileOpen: isMobileAgentsOpen,
150
+ setIsMobileOpen: setIsMobileAgentsOpen,
151
+ items: [
152
+ ...agents.map(
153
+ (agent) =>
154
+ ({
155
+ label: agent.meta.fullname || agent.agentName,
156
+ href: `/${agent.agentName}`,
157
+ } as SubMenuItem),
158
+ ),
159
+ {
160
+ label: 'View all agents',
161
+ href: '/',
162
+ isBold: true,
163
+ isBordered: true,
164
+ } as SubMenuItem,
165
+ {
166
+ label: isCreatingAgent ? (
167
+ <div className="flex items-center">
168
+ <span className="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
169
+ Creating agent...
170
+ </div>
171
+ ) : (
172
+ 'Create new agent'
173
+ ),
174
+ onClick: isCreatingAgent ? undefined : handleCreateAgent,
175
+ isBold: true,
176
+ } as SubMenuItem,
177
+ ],
178
+ },
179
+ {
180
+ type: 'link' as const,
181
+ label: 'Models',
182
+ href: '/admin/models',
183
+ },
184
+ {
185
+ type: 'dropdown' as const,
186
+ label: 'Users',
187
+ isOpen: isUsersOpen,
188
+ setIsOpen: setIsUsersOpen,
189
+ isMobileOpen: isMobileUsersOpen,
190
+ setIsMobileOpen: setIsMobileUsersOpen,
191
+ items: [
192
+ ...adminUsers.map(
193
+ (user) =>
194
+ ({
195
+ label: user.username,
196
+ href: `/admin/users/${encodeURIComponent(user.username)}`,
197
+ } as SubMenuItem),
198
+ ),
199
+ {
200
+ label: 'View all users',
201
+ href: '/admin/users',
202
+ isBold: true,
203
+ isBordered: true,
204
+ } as SubMenuItem,
205
+ {
206
+ label: 'Create new user',
207
+ href: '/admin/users#create-user',
208
+ isBold: true,
209
+ } as SubMenuItem,
210
+ ],
211
+ },
212
+ {
213
+ type: 'link' as const,
214
+ label: 'Metadata',
215
+ href: '/admin/metadata',
216
+ },
217
+ {
218
+ type: 'link' as const,
219
+ label: 'Chat history',
220
+ href: '/admin/chat-history',
221
+ },
222
+ {
223
+ type: 'link' as const,
224
+ label: 'Chat feedback',
225
+ href: '/admin/chat-feedback',
226
+ },
227
+ {
228
+ type: 'link' as const,
229
+ label: 'About',
230
+ href: 'https://ptbk.io/',
231
+ },
232
+ ]
233
+ : []),
234
+ ];
235
+
41
236
  return (
42
- <header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200 h-[60px]">
237
+ <header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200 h-16">
43
238
  <LoginDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
44
- <div className="container mx-auto px-4">
45
- <div className="flex items-center justify-between h-16">
46
- {/* Logo <- TODO: This should be <h1>*/}
239
+ <div className="container mx-auto px-4 h-full">
240
+ <div className="flex items-center justify-between h-full">
241
+ {/* Logo */}
47
242
  <Link href="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity">
48
- <Image
49
- src={serverLogoUrl || promptbookLogoBlueTransparent}
50
- alt={serverName}
51
- width={32}
52
- height={32}
53
- className="w-8 h-8 object-contain"
54
- />
55
- <span className="text-xl text-gray-900">{serverName}</span>
243
+ {serverLogoUrl ? (
244
+ // Note: `next/image` does not load external images well without extra config
245
+ // eslint-disable-next-line @next/next/no-img-element
246
+ <img
247
+ src={serverLogoUrl}
248
+ alt={serverName}
249
+ width={32}
250
+ height={32}
251
+ className="w-8 h-8 object-contain"
252
+ />
253
+ ) : (
254
+ <Image
255
+ src={promptbookLogoBlueTransparent}
256
+ alt={serverName}
257
+ width={32}
258
+ height={32}
259
+ className="w-8 h-8 object-contain"
260
+ />
261
+ )}
262
+ <h1 className="text-xl font-bold tracking-tight text-gray-900">{serverName}</h1>
56
263
  </Link>
57
264
 
58
265
  {/* Desktop Navigation */}
59
- {/* Desktop Navigation */}
60
- <nav className="hidden md:flex items-center gap-8">
61
- {isAdmin && (
62
- <>
63
- <Link
64
- href="/"
65
- className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
66
- >
67
- Agents
68
- </Link>
69
- <Link
70
- href="/"
71
- className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
72
- >
73
- Models
74
- </Link>
75
- <Link
76
- href="/admin/metadata"
77
- className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
78
- >
79
- Metadata
80
- </Link>
81
- <Link
82
- href="https://ptbk.io/"
83
- className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
84
- >
85
- About
86
- </Link>
87
- </>
88
- )}
266
+ <nav className="hidden lg:flex items-center gap-8">
267
+ {menuItems.map((item, index) => {
268
+ if (item.type === 'link') {
269
+ return (
270
+ <Link
271
+ key={index}
272
+ href={item.href}
273
+ className="text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
274
+ >
275
+ {item.label}
276
+ </Link>
277
+ );
278
+ }
279
+
280
+ if (item.type === 'dropdown') {
281
+ return (
282
+ <div key={index} className="relative">
283
+ <button
284
+ className="flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
285
+ onClick={() => item.setIsOpen(!item.isOpen)}
286
+ onBlur={() => setTimeout(() => item.setIsOpen(false), 200)}
287
+ >
288
+ {item.label}
289
+ <ChevronDown className="w-4 h-4" />
290
+ </button>
291
+
292
+ {item.isOpen && (
293
+ <div className="absolute top-full left-0 mt-2 w-56 bg-white rounded-md shadow-lg border border-gray-100 py-1 z-50 animate-in fade-in zoom-in-95 duration-200 max-h-[80vh] overflow-y-auto">
294
+ {item.items.map((subItem, subIndex) => {
295
+ const className = `block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 ${
296
+ subItem.isBold ? 'font-medium' : ''
297
+ } ${subItem.isBordered ? 'border-b border-gray-100' : ''}`;
298
+
299
+ if (subItem.onClick) {
300
+ return (
301
+ <button
302
+ key={subIndex}
303
+ onClick={subItem.onClick}
304
+ className={`${className} w-full text-left`}
305
+ >
306
+ {subItem.label}
307
+ </button>
308
+ );
309
+ }
310
+
311
+ return (
312
+ <Link
313
+ key={subIndex}
314
+ href={subItem.href!}
315
+ className={className}
316
+ onClick={() => item.setIsOpen(false)}
317
+ >
318
+ {subItem.label}
319
+ </Link>
320
+ );
321
+ })}
322
+ </div>
323
+ )}
324
+ </div>
325
+ );
326
+ }
327
+
328
+ return null;
329
+ })}
89
330
 
90
331
  {just(false /* TODO: [🧠] Figure out what to do with theese links */) && (
91
332
  <Link
92
333
  href="https://ptbk.io/"
93
334
  target="_blank"
94
- className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
335
+ className="text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
95
336
  >
96
337
  Create your server
97
338
  </Link>
@@ -109,54 +350,179 @@ export function Header(props: HeaderProps) {
109
350
  </Link>
110
351
  )}
111
352
 
112
- {!isAdmin ? (
353
+ {!currentUser && !isAdmin && (
113
354
  <button
114
355
  onClick={() => {
115
356
  setIsLoginOpen(true);
116
357
  setIsMenuOpen(false);
117
358
  }}
118
- className="w-full inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
359
+ className="hidden lg:inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors"
119
360
  >
120
361
  Log in
121
362
  <LogIn className="ml-2 w-4 h-4" />
122
363
  </button>
123
- ) : (
124
- <button
125
- onClick={() => {
126
- handleLogout();
127
- setIsMenuOpen(false);
128
- }}
129
- className="w-full inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
130
- >
131
- Log out
132
- <LogOut className="ml-2 w-4 h-4" />
133
- </button>
134
364
  )}
135
365
 
136
- {/* Mobile Menu Toggle */}
137
- {just(false /* TODO: [🧠] Figure out whether we want a menu */) && (
138
- <button
139
- className="md:hidden p-2 text-gray-600 hover:text-gray-900"
140
- onClick={() => setIsMenuOpen(!isMenuOpen)}
141
- >
142
- {isMenuOpen ? <X size={24} /> : <Menu size={24} />}
143
- </button>
366
+ {(currentUser || isAdmin) && (
367
+ <div className="hidden lg:flex items-center gap-3">
368
+ <span className="inline text-sm text-gray-600">
369
+ Logged in as <strong>{currentUser?.username || 'Admin'}</strong>
370
+ {(currentUser?.isAdmin || isAdmin) && (
371
+ <span className="ml-2 bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
372
+ Admin
373
+ </span>
374
+ )}
375
+ </span>
376
+ <button
377
+ onClick={() => {
378
+ handleLogout();
379
+ setIsMenuOpen(false);
380
+ }}
381
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors"
382
+ >
383
+ Log out
384
+ <LogOut className="ml-2 w-4 h-4" />
385
+ </button>
386
+ </div>
144
387
  )}
388
+
389
+ {/* Mobile Menu Toggle */}
390
+ <HamburgerMenu
391
+ isOpen={isMenuOpen}
392
+ onClick={() => setIsMenuOpen(!isMenuOpen)}
393
+ className="lg:hidden p-2 text-gray-600 hover:text-gray-900"
394
+ />
145
395
  </div>
146
396
  </div>
147
397
 
148
398
  {/* Mobile Navigation */}
149
- {just(false /* TODO: [🧠] Figure out whether we want a menu */) && isMenuOpen && (
150
- <div className="md:hidden py-4 border-t border-gray-100 animate-in slide-in-from-top-2">
151
- <nav className="flex flex-col gap-4">
152
- <Link
153
- href="https://ptbk.io/#try-it-yourself"
154
- target="_blank"
155
- className="text-gray-600 hover:text-gray-900 transition-colors py-2"
156
- onClick={() => setIsMenuOpen(false)}
157
- >
158
- Try it yourself
159
- </Link>
399
+ {isMenuOpen && (
400
+ <div
401
+ className="lg:hidden absolute top-16 left-0 right-0 z-50 bg-white/80 shadow-xl py-4 border-t border-gray-100 animate-in slide-in-from-top-2 h-[calc(100vh-4rem)] overflow-y-auto"
402
+ style={{
403
+ backdropFilter: 'blur(20px)',
404
+ WebkitBackdropFilter: 'blur(20px)',
405
+ }}
406
+ >
407
+ <nav className="container mx-auto flex flex-col gap-4 px-6">
408
+ {/* Login Status for Mobile */}
409
+ <div className="py-2 border-b border-gray-100 mb-2">
410
+ {!currentUser && !isAdmin && (
411
+ <button
412
+ onClick={() => {
413
+ setIsLoginOpen(true);
414
+ setIsMenuOpen(false);
415
+ }}
416
+ className="flex items-center gap-2 text-base font-medium text-gray-600 hover:text-gray-900 w-full"
417
+ >
418
+ <LogIn className="w-4 h-4" />
419
+ Log in
420
+ </button>
421
+ )}
422
+
423
+ {(currentUser || isAdmin) && (
424
+ <div className="flex flex-col gap-3">
425
+ <div className="text-sm text-gray-600">
426
+ Logged in as <strong>{currentUser?.username || 'Admin'}</strong>
427
+ {(currentUser?.isAdmin || isAdmin) && (
428
+ <span className="ml-2 bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
429
+ Admin
430
+ </span>
431
+ )}
432
+ </div>
433
+ <button
434
+ onClick={() => {
435
+ handleLogout();
436
+ setIsMenuOpen(false);
437
+ }}
438
+ className="flex items-center gap-2 text-base font-medium text-red-600 hover:text-red-700 w-full"
439
+ >
440
+ <LogOut className="w-4 h-4" />
441
+ Log out
442
+ </button>
443
+ </div>
444
+ )}
445
+ </div>
446
+
447
+ {menuItems.map((item, index) => {
448
+ if (item.type === 'link') {
449
+ return (
450
+ <Link
451
+ key={index}
452
+ href={item.href}
453
+ className="block text-base font-medium text-gray-600 hover:text-gray-900 py-2"
454
+ onClick={() => setIsMenuOpen(false)}
455
+ >
456
+ {item.label}
457
+ </Link>
458
+ );
459
+ }
460
+
461
+ if (item.type === 'dropdown') {
462
+ return (
463
+ <div key={index} className="flex flex-col">
464
+ <button
465
+ className="w-full flex items-center justify-between text-base font-medium text-gray-600 hover:text-gray-900 py-2"
466
+ onClick={() => item.setIsMobileOpen(!item.isMobileOpen)}
467
+ >
468
+ {item.label}
469
+ <ChevronDown
470
+ className={`w-4 h-4 transition-transform duration-200 ${
471
+ item.isMobileOpen ? 'rotate-180' : ''
472
+ }`}
473
+ />
474
+ </button>
475
+ {item.isMobileOpen && (
476
+ <div className="pl-4 flex flex-col gap-2 border-l-2 border-gray-100 ml-1 mt-1">
477
+ {item.items.map((subItem, subIndex) => {
478
+ const className = `block text-sm ${
479
+ subItem.isBold
480
+ ? 'font-medium text-gray-900 hover:text-gray-700'
481
+ : 'text-gray-600 hover:text-gray-900'
482
+ } py-2`;
483
+
484
+ if (subItem.onClick) {
485
+ return (
486
+ <button
487
+ key={subIndex}
488
+ className={`${className} w-full text-left`}
489
+ onClick={subItem.onClick}
490
+ >
491
+ {subItem.label}
492
+ </button>
493
+ );
494
+ }
495
+
496
+ return (
497
+ <Link
498
+ key={subIndex}
499
+ href={subItem.href!}
500
+ className={className}
501
+ onClick={() => setIsMenuOpen(false)}
502
+ >
503
+ {subItem.label}
504
+ </Link>
505
+ );
506
+ })}
507
+ </div>
508
+ )}
509
+ </div>
510
+ );
511
+ }
512
+
513
+ return null;
514
+ })}
515
+
516
+ {just(false /* TODO: [🧠] Figure out what to do with these links */) && (
517
+ <Link
518
+ href="https://ptbk.io/"
519
+ target="_blank"
520
+ className="text-base font-medium text-gray-600 hover:text-gray-900 py-2"
521
+ onClick={() => setIsMenuOpen(false)}
522
+ >
523
+ Create your server
524
+ </Link>
525
+ )}
160
526
  </nav>
161
527
  </div>
162
528
  )}
@@ -7,22 +7,54 @@ import { Card } from './Card';
7
7
  type AgentCardProps = {
8
8
  agent: AgentBasicInformation;
9
9
  href: string;
10
+ isAdmin?: boolean;
11
+ onDelete?: (agentName: string) => void;
12
+ onClone?: (agentName: string) => void;
10
13
  };
11
14
 
12
- export function AgentCard({ agent, href }: AgentCardProps) {
15
+ const ACTION_BUTTON_CLASSES =
16
+ 'text-white px-3 py-1 rounded shadow text-xs font-medium transition-colors uppercase tracking-wider opacity-80 hover:opacity-100';
17
+
18
+ export function AgentCard({ agent, href, isAdmin, onDelete, onClone }: AgentCardProps) {
13
19
  return (
14
- <Link href={href}>
15
- <Card
16
- style={
17
- !agent.meta.color
18
- ? {}
19
- : {
20
- backgroundColor: `${agent.meta.color}22`,
21
- }
22
- }
23
- >
24
- <AvatarProfile agent={agent} />
25
- </Card>
26
- </Link>
20
+ <div className="relative h-full group">
21
+ <Link href={href} className="block h-full">
22
+ <Card
23
+ style={
24
+ !agent.meta.color
25
+ ? {}
26
+ : {
27
+ backgroundColor: `${agent.meta.color}22`,
28
+ }
29
+ }
30
+ >
31
+ <AvatarProfile agent={agent} />
32
+ </Card>
33
+ </Link>
34
+ {isAdmin && (
35
+ <div className="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
36
+ <button
37
+ className={`bg-blue-500 hover:bg-blue-600 ${ACTION_BUTTON_CLASSES}`}
38
+ onClick={(e) => {
39
+ e.preventDefault();
40
+ onClone?.(agent.agentName);
41
+ }}
42
+ title="Clone agent"
43
+ >
44
+ Clone
45
+ </button>
46
+ <button
47
+ className={`bg-red-500 hover:bg-red-600 ${ACTION_BUTTON_CLASSES}`}
48
+ onClick={(e) => {
49
+ e.preventDefault();
50
+ onDelete?.(agent.agentName);
51
+ }}
52
+ title="Delete agent"
53
+ >
54
+ Delete
55
+ </button>
56
+ </div>
57
+ )}
58
+ </div>
27
59
  );
28
60
  }