@promptbook/cli 0.104.0-1 → 0.104.0-3

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 (128) hide show
  1. package/apps/agents-server/next.config.ts +2 -2
  2. package/apps/agents-server/package.json +7 -3
  3. package/apps/agents-server/public/fonts/OpenMoji-color-cbdt.woff2 +0 -0
  4. package/apps/agents-server/public/swagger.json +115 -0
  5. package/apps/agents-server/scripts/generate-reserved-paths/generate-reserved-paths.ts +54 -0
  6. package/apps/agents-server/scripts/generate-reserved-paths/tsconfig.json +19 -0
  7. package/apps/agents-server/src/app/AddAgentButton.tsx +3 -3
  8. package/apps/agents-server/src/app/actions.ts +17 -5
  9. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +221 -274
  10. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +94 -137
  11. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +23 -19
  12. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +15 -1
  13. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +51 -9
  14. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +47 -4
  15. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +2 -0
  16. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +18 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +8 -8
  18. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +17 -26
  19. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +20 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +6 -11
  21. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +1 -1
  22. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +5 -2
  23. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +20 -16
  24. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +15 -2
  25. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +15 -2
  26. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +12 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/code/api/route.ts +68 -0
  28. package/apps/agents-server/src/app/agents/[agentName]/code/page.tsx +214 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +5 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +2 -2
  31. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +1 -1
  32. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +2 -2
  33. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +12 -6
  34. package/apps/agents-server/src/app/agents/[agentName]/system-message/page.tsx +87 -0
  35. package/apps/agents-server/src/app/api/admin-email/route.ts +12 -0
  36. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +10 -12
  37. package/apps/agents-server/src/app/api/agents/[agentName]/restore/route.ts +19 -0
  38. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +41 -0
  39. package/apps/agents-server/src/app/api/agents/route.ts +28 -3
  40. package/apps/agents-server/src/app/api/api-tokens/route.ts +6 -7
  41. package/apps/agents-server/src/app/api/docs/book.md/route.ts +61 -0
  42. package/apps/agents-server/src/app/api/federated-agents/route.ts +12 -0
  43. package/apps/agents-server/src/app/api/images/[filename]/route.ts +107 -0
  44. package/apps/agents-server/src/app/api/metadata/route.ts +5 -6
  45. package/apps/agents-server/src/app/api/upload/route.ts +128 -45
  46. package/apps/agents-server/src/app/docs/[docId]/page.tsx +2 -3
  47. package/apps/agents-server/src/app/docs/page.tsx +12 -12
  48. package/apps/agents-server/src/app/globals.css +140 -33
  49. package/apps/agents-server/src/app/layout.tsx +27 -22
  50. package/apps/agents-server/src/app/page.tsx +50 -4
  51. package/apps/agents-server/src/app/recycle-bin/actions.ts +20 -14
  52. package/apps/agents-server/src/app/recycle-bin/page.tsx +25 -41
  53. package/apps/agents-server/src/app/sitemap.xml/route.ts +6 -3
  54. package/apps/agents-server/src/app/swagger/page.tsx +14 -0
  55. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +9 -98
  56. package/apps/agents-server/src/components/AgentProfile/QrCodeModal.tsx +0 -1
  57. package/apps/agents-server/src/components/AgentProfile/useAgentBackground.ts +97 -0
  58. package/apps/agents-server/src/components/Auth/AuthControls.tsx +5 -4
  59. package/apps/agents-server/src/components/DeletedAgentBanner.tsx +26 -0
  60. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +38 -0
  61. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +11 -9
  62. package/apps/agents-server/src/components/Footer/Footer.tsx +5 -5
  63. package/apps/agents-server/src/components/ForgottenPasswordDialog/ForgottenPasswordDialog.tsx +61 -0
  64. package/apps/agents-server/src/components/Header/Header.tsx +106 -40
  65. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +104 -20
  66. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +72 -12
  67. package/apps/agents-server/src/components/Homepage/DeletedAgentsList.tsx +50 -0
  68. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +3 -2
  69. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +50 -1
  70. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +7 -2
  71. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +16 -7
  72. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +4 -4
  73. package/apps/agents-server/src/components/RegisterUserDialog/RegisterUserDialog.tsx +61 -0
  74. package/apps/agents-server/src/components/_utils/headlessParam.tsx +7 -3
  75. package/apps/agents-server/src/database/metadataDefaults.ts +19 -1
  76. package/apps/agents-server/src/database/migrations/2025-12-0240-agent-public-id.sql +3 -0
  77. package/apps/agents-server/src/database/migrations/2025-12-0360-agent-deleted-at.sql +1 -0
  78. package/apps/agents-server/src/database/migrations/2025-12-0370-image-table.sql +19 -0
  79. package/apps/agents-server/src/database/migrations/2025-12-0380-agent-visibility.sql +1 -0
  80. package/apps/agents-server/src/database/migrations/2025-12-0390-upload-tracking.sql +20 -0
  81. package/apps/agents-server/src/database/migrations/2025-12-0401-file-upload-status.sql +13 -0
  82. package/apps/agents-server/src/database/migrations/2025-12-0640-openai-assistant-cache.sql +12 -0
  83. package/apps/agents-server/src/database/schema.ts +109 -0
  84. package/apps/agents-server/src/generated/reservedPaths.ts +32 -0
  85. package/apps/agents-server/src/middleware.ts +19 -23
  86. package/apps/agents-server/src/tools/$provideCdnForServer.ts +6 -1
  87. package/apps/agents-server/src/utils/auth.ts +117 -17
  88. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +57 -0
  89. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +4 -0
  90. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +18 -0
  91. package/apps/agents-server/src/utils/getUserIdFromRequest.ts +35 -0
  92. package/apps/agents-server/src/utils/handleChatCompletion.ts +65 -5
  93. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +21 -0
  94. package/apps/agents-server/src/utils/validateApiKey.ts +7 -11
  95. package/esm/index.es.js +194 -34
  96. package/esm/index.es.js.map +1 -1
  97. package/esm/typings/src/_packages/types.index.d.ts +8 -2
  98. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +6 -1
  99. package/esm/typings/src/book-components/Chat/Chat/ChatMessageItem.d.ts +5 -1
  100. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  101. package/esm/typings/src/book-components/Chat/CodeBlock/CodeBlock.d.ts +13 -0
  102. package/esm/typings/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  103. package/esm/typings/src/book-components/Chat/types/ChatMessage.d.ts +7 -11
  104. package/esm/typings/src/book-components/_common/Dropdown/Dropdown.d.ts +2 -2
  105. package/esm/typings/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +56 -0
  106. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +13 -7
  107. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +6 -0
  108. package/esm/typings/src/commitments/DICTIONARY/DICTIONARY.d.ts +46 -0
  109. package/esm/typings/src/commitments/index.d.ts +2 -1
  110. package/esm/typings/src/llm-providers/ollama/OllamaExecutionTools.d.ts +1 -1
  111. package/esm/typings/src/llm-providers/openai/createOpenAiCompatibleExecutionTools.d.ts +1 -1
  112. package/esm/typings/src/types/Message.d.ts +49 -0
  113. package/esm/typings/src/types/typeAliases.d.ts +12 -0
  114. package/esm/typings/src/utils/environment/$detectRuntimeEnvironment.d.ts +4 -4
  115. package/esm/typings/src/utils/environment/$isRunningInBrowser.d.ts +1 -1
  116. package/esm/typings/src/utils/environment/$isRunningInJest.d.ts +1 -1
  117. package/esm/typings/src/utils/environment/$isRunningInNode.d.ts +1 -1
  118. package/esm/typings/src/utils/environment/$isRunningInWebWorker.d.ts +1 -1
  119. package/esm/typings/src/utils/markdown/extractAllBlocksFromMarkdown.d.ts +2 -2
  120. package/esm/typings/src/utils/markdown/extractOneBlockFromMarkdown.d.ts +2 -2
  121. package/esm/typings/src/utils/random/$randomBase58.d.ts +12 -0
  122. package/esm/typings/src/version.d.ts +1 -1
  123. package/package.json +1 -1
  124. package/umd/index.umd.js +200 -40
  125. package/umd/index.umd.js.map +1 -1
  126. package/apps/agents-server/package-lock.json +0 -27
  127. package/apps/agents-server/public/fonts/download-font.js +0 -22
  128. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +0 -18
@@ -4,14 +4,15 @@ import promptbookLogoBlueTransparent from '@/public/logo-blue-white-256.png';
4
4
  import { $createAgentAction, logoutAction } from '@/src/app/actions';
5
5
  import { ArrowRight, ChevronDown, Lock, LogIn, LogOut, User } from 'lucide-react';
6
6
  import Image from 'next/image';
7
- import { HeadlessLink, useIsHeadless, pushWithHeadless } from '../_utils/headlessParam';
8
7
  import { useRouter } from 'next/navigation';
9
8
  import { ReactNode, useState } from 'react';
10
9
  import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
11
10
  import { HamburgerMenu } from '../../../../../src/book-components/_common/HamburgerMenu/HamburgerMenu';
11
+ import { useMenuHoisting } from '../../../../../src/book-components/_common/MenuHoisting/MenuHoistingContext';
12
12
  import { just } from '../../../../../src/utils/organization/just';
13
13
  import type { UserInfo } from '../../utils/getCurrentUser';
14
14
  import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
15
+ import { HeadlessLink, pushWithHeadless, useIsHeadless } from '../_utils/headlessParam';
15
16
  import { ChangePasswordDialog } from '../ChangePasswordDialog/ChangePasswordDialog';
16
17
  import { LoginDialog } from '../LoginDialog/LoginDialog';
17
18
  import { useUsersAdmin } from '../UsersList/useUsersAdmin';
@@ -92,6 +93,7 @@ export function Header(props: HeaderProps) {
92
93
  const [isCreatingAgent, setIsCreatingAgent] = useState(false);
93
94
  const router = useRouter();
94
95
  const isHeadless = useIsHeadless();
96
+ const menuHoisting = useMenuHoisting();
95
97
 
96
98
  const { users: adminUsers } = useUsersAdmin();
97
99
 
@@ -117,16 +119,22 @@ export function Header(props: HeaderProps) {
117
119
 
118
120
  // Federated servers dropdown items (respect logo, only current is not clickable)
119
121
  const [isFederatedOpen, setIsFederatedOpen] = useState(false);
120
- const [isMobileFederatedOpen, setIsMobileFederatedOpen] = useState(false);
122
+ // const [isMobileFederatedOpen, setIsMobileFederatedOpen] = useState(false);
121
123
 
122
- const federatedDropdownItems: SubMenuItem[] = federatedServers.map(server => {
124
+ const federatedDropdownItems: SubMenuItem[] = federatedServers.map((server) => {
123
125
  const isCurrent = server.url === (typeof window !== 'undefined' ? window.location.origin : '');
124
126
  return isCurrent
125
127
  ? {
126
128
  label: (
127
129
  <span className="flex items-center gap-2">
128
130
  {/* eslint-disable-next-line @next/next/no-img-element */}
129
- <img src={server.logoUrl || serverLogoUrl || promptbookLogoBlueTransparent.src} alt={server.title} width={20} height={20} className="w-5 h-5 object-contain rounded-full" />
131
+ <img
132
+ src={server.logoUrl || serverLogoUrl || promptbookLogoBlueTransparent.src}
133
+ alt={server.title}
134
+ width={20}
135
+ height={20}
136
+ className="w-5 h-5 object-contain rounded-full"
137
+ />
130
138
  <span className="font-semibold">{server.title.replace(/^Federated: /, '')}</span>
131
139
  <span className="ml-1 text-xs text-blue-600">(current)</span>
132
140
  </span>
@@ -138,7 +146,13 @@ export function Header(props: HeaderProps) {
138
146
  label: (
139
147
  <span className="flex items-center gap-2">
140
148
  {/* eslint-disable-next-line @next/next/no-img-element */}
141
- <img src={server.logoUrl || promptbookLogoBlueTransparent.src} alt={server.title} width={20} height={20} className="w-5 h-5 object-contain rounded-full" />
149
+ <img
150
+ src={server.logoUrl || promptbookLogoBlueTransparent.src}
151
+ alt={server.title}
152
+ width={20}
153
+ height={20}
154
+ className="w-5 h-5 object-contain rounded-full"
155
+ />
142
156
  <span>{server.title.replace(/^Federated: /, '')}</span>
143
157
  </span>
144
158
  ),
@@ -167,6 +181,12 @@ export function Header(props: HeaderProps) {
167
181
  isBold: true,
168
182
  isBordered: true,
169
183
  } as SubMenuItem,
184
+ {
185
+ label: 'API Reference',
186
+ href: '/swagger',
187
+ isBold: true,
188
+ isBordered: true,
189
+ } as SubMenuItem,
170
190
  ...getVisibleCommitmentDefinitions().map(
171
191
  ({ primary, aliases }) =>
172
192
  ({
@@ -261,6 +281,10 @@ export function Header(props: HeaderProps) {
261
281
  isMobileOpen: isMobileSystemOpen,
262
282
  setIsMobileOpen: setIsMobileSystemOpen,
263
283
  items: [
284
+ {
285
+ label: 'OpenAPI Documentation',
286
+ href: '/swagger',
287
+ },
264
288
  {
265
289
  label: 'API Tokens',
266
290
  href: '/admin/api-tokens',
@@ -320,44 +344,46 @@ export function Header(props: HeaderProps) {
320
344
 
321
345
  {/* Desktop Navigation */}
322
346
  <nav className="hidden lg:flex items-center gap-8">
323
- {/* Federated servers dropdown */}
324
- <div className="relative">
325
- <button
326
- className="flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
327
- onClick={() => setIsFederatedOpen(!isFederatedOpen)}
328
- onBlur={() => setTimeout(() => setIsFederatedOpen(false), 200)}
329
- >
330
- <ChevronDown className="w-4 h-4" />
331
- <span>Switch server</span>
332
- </button>
333
- {isFederatedOpen && (
334
- <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">
335
- {federatedDropdownItems.map((subItem, subIndex) => {
336
- const className = `block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 ${
337
- subItem.isBold ? 'font-medium' : ''
338
- } ${subItem.isBordered ? 'border-b border-gray-100' : ''}`;
339
-
340
- if (subItem.href) {
347
+ {/* Federated servers dropdown - only show if there are federated servers */}
348
+ {federatedServers.length > 0 && (
349
+ <div className="relative">
350
+ <button
351
+ className="flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
352
+ onClick={() => setIsFederatedOpen(!isFederatedOpen)}
353
+ onBlur={() => setTimeout(() => setIsFederatedOpen(false), 200)}
354
+ >
355
+ <ChevronDown className="w-4 h-4" />
356
+ <span>Switch server</span>
357
+ </button>
358
+ {isFederatedOpen && (
359
+ <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">
360
+ {federatedDropdownItems.map((subItem, subIndex) => {
361
+ const className = `block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 ${
362
+ subItem.isBold ? 'font-medium' : ''
363
+ } ${subItem.isBordered ? 'border-b border-gray-100' : ''}`;
364
+
365
+ if (subItem.href) {
366
+ return (
367
+ <HeadlessLink
368
+ key={subIndex}
369
+ href={subItem.href}
370
+ className={className}
371
+ onClick={() => setIsFederatedOpen(false)}
372
+ >
373
+ {subItem.label}
374
+ </HeadlessLink>
375
+ );
376
+ }
341
377
  return (
342
- <HeadlessLink
343
- key={subIndex}
344
- href={subItem.href}
345
- className={className}
346
- onClick={() => setIsFederatedOpen(false)}
347
- >
378
+ <span key={subIndex} className={className}>
348
379
  {subItem.label}
349
- </HeadlessLink>
380
+ </span>
350
381
  );
351
- }
352
- return (
353
- <span key={subIndex} className={className}>
354
- {subItem.label}
355
- </span>
356
- );
357
- })}
358
- </div>
359
- )}
360
- </div>
382
+ })}
383
+ </div>
384
+ )}
385
+ </div>
386
+ )}
361
387
  {menuItems.map((item, index) => {
362
388
  if (item.type === 'link') {
363
389
  return (
@@ -433,6 +459,24 @@ export function Header(props: HeaderProps) {
433
459
  )}
434
460
  </nav>
435
461
 
462
+ {/* Hoisted Menu Items */}
463
+ {menuHoisting && menuHoisting.menu.length > 0 && (
464
+ <div className="hidden lg:flex items-center gap-2 border-r border-gray-200 pr-4 mr-2">
465
+ {menuHoisting.menu.map((item, index) => (
466
+ <button
467
+ key={index}
468
+ onClick={item.onClick}
469
+ className={`p-2 rounded-md hover:bg-gray-100 transition-colors text-gray-600 hover:text-gray-900 ${
470
+ item.isActive ? 'bg-gray-100 text-gray-900' : ''
471
+ }`}
472
+ title={item.name}
473
+ >
474
+ {item.icon}
475
+ </button>
476
+ ))}
477
+ </div>
478
+ )}
479
+
436
480
  {/* CTA Button & Mobile Menu Toggle */}
437
481
  <div className="flex items-center gap-4">
438
482
  {just(false /* TODO: [🧠] Figure out what to do with call to action */) && (
@@ -531,6 +575,28 @@ export function Header(props: HeaderProps) {
531
575
  }}
532
576
  >
533
577
  <nav className="container mx-auto flex flex-col gap-4 px-6">
578
+ {/* Hoisted Menu Items for Mobile */}
579
+ {menuHoisting && menuHoisting.menu.length > 0 && (
580
+ <div className="py-2 border-b border-gray-100 mb-2 flex gap-2 overflow-x-auto">
581
+ {menuHoisting.menu.map((item, index) => (
582
+ <button
583
+ key={index}
584
+ onClick={() => {
585
+ item.onClick();
586
+ setIsMenuOpen(false);
587
+ }}
588
+ className={`p-2 rounded-md hover:bg-gray-100 transition-colors text-gray-600 hover:text-gray-900 ${
589
+ item.isActive ? 'bg-gray-100 text-gray-900' : ''
590
+ }`}
591
+ title={item.name}
592
+ >
593
+ {item.icon}
594
+ <span className="sr-only">{item.name}</span>
595
+ </button>
596
+ ))}
597
+ </div>
598
+ )}
599
+
534
600
  {/* Login Status for Mobile */}
535
601
  <div className="py-2 border-b border-gray-100 mb-2">
536
602
  {!currentUser && !isAdmin && (
@@ -1,43 +1,127 @@
1
+ 'use client';
2
+
3
+ import { really_any } from '@promptbook-local/types';
4
+ import { EyeIcon, EyeOffIcon, RotateCcwIcon } from 'lucide-react';
1
5
  import Link from 'next/link';
2
- import React from 'react';
3
6
  import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
4
- import { AvatarProfile } from '../../../../../src/book-components/AvatarProfile/AvatarProfile/AvatarProfile';
5
- import { Card } from './Card';
7
+ import { useAgentBackground } from '../AgentProfile/useAgentBackground';
6
8
 
7
9
  type AgentCardProps = {
8
10
  agent: AgentBasicInformation;
9
11
  href: string;
10
12
  isAdmin?: boolean;
11
- onDelete?: (agentName: string) => void;
12
- onClone?: (agentName: string) => void;
13
+ onDelete?: (agentIdentifier: string) => void;
14
+ onClone?: (agentIdentifier: string) => void;
15
+ onToggleVisibility?: (agentIdentifier: string) => void;
16
+ onRestore?: (agentIdentifier: string) => void;
17
+ visibility?: 'PUBLIC' | 'PRIVATE';
13
18
  };
14
19
 
15
20
  const ACTION_BUTTON_CLASSES =
16
21
  'text-white px-3 py-1 rounded shadow text-xs font-medium transition-colors uppercase tracking-wider opacity-80 hover:opacity-100';
17
22
 
18
- export function AgentCard({ agent, href, isAdmin, onDelete, onClone }: AgentCardProps) {
23
+ export function AgentCard({
24
+ agent,
25
+ href,
26
+ isAdmin,
27
+ onDelete,
28
+ onClone,
29
+ onToggleVisibility,
30
+ onRestore,
31
+ visibility,
32
+ }: AgentCardProps) {
33
+ const { meta, agentName } = agent;
34
+ const fullname = (meta.fullname as string) || agentName || 'Agent';
35
+ const imageUrl = (meta.image as string) || null;
36
+ const personaDescription = agent.personaDescription || '';
37
+
38
+ const { brandColorLightHex, brandColorDarkHex, backgroundImage } = useAgentBackground(meta.color);
39
+
19
40
  return (
20
41
  <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
- }
42
+ <Link href={href} className="block h-full transition-transform hover:scale-[1.02] duration-300">
43
+ <div
44
+ className="h-full rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 flex flex-col border border-white/20"
45
+ style={{
46
+ background: `url("${backgroundImage}")`,
47
+ backgroundSize: 'cover',
48
+ backgroundPosition: 'center',
49
+ }}
30
50
  >
31
- <AvatarProfile agent={agent} />
32
- </Card>
51
+ <div className="p-6 flex flex-col items-center flex-grow backdrop-blur-[2px]">
52
+ {/* Image container */}
53
+ <div
54
+ className="w-32 h-32 mb-4 shadow-lg overflow-hidden flex-shrink-0 bg-black/20"
55
+ style={{
56
+ boxShadow: `0 10px 20px -5px rgba(0, 0, 0, 0.2), 0 0 0 1px ${brandColorLightHex}40`,
57
+
58
+ // Note: Make it squircle
59
+ borderRadius: '50%',
60
+ ['cornerShape' as really_any /* <- Note: `cornerShape` is non standard CSS property */]:
61
+ 'squircle ',
62
+ }}
63
+ >
64
+ {imageUrl ? (
65
+ // eslint-disable-next-line @next/next/no-img-element
66
+ <img src={imageUrl} alt={fullname} className="w-full h-full object-cover" />
67
+ ) : (
68
+ <div
69
+ className="w-full h-full flex items-center justify-center text-4xl font-bold text-white/80"
70
+ style={{ backgroundColor: brandColorDarkHex }}
71
+ >
72
+ {fullname.charAt(0).toUpperCase()}
73
+ </div>
74
+ )}
75
+ </div>
76
+
77
+ <h3
78
+ className="text-lg font-bold text-gray-900 text-center leading-tight mb-2"
79
+ style={{ textShadow: '0 1px 2px rgba(255,255,255,0.8)' }}
80
+ >
81
+ {fullname}
82
+ </h3>
83
+
84
+ <p className="text-sm text-gray-800 text-center line-clamp-3 leading-relaxed font-medium mix-blend-hard-light">
85
+ {personaDescription}
86
+ </p>
87
+ </div>
88
+ </div>
33
89
  </Link>
34
- {isAdmin && (
90
+ {isAdmin && onRestore && (
91
+ <div className="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
92
+ <button
93
+ className={`bg-green-500 hover:bg-green-600 ${ACTION_BUTTON_CLASSES}`}
94
+ onClick={(e) => {
95
+ e.preventDefault();
96
+ onRestore(agent.permanentId || agent.agentName);
97
+ }}
98
+ title="Restore agent"
99
+ >
100
+ <RotateCcwIcon className="w-3 h-3" />
101
+ </button>
102
+ </div>
103
+ )}
104
+ {isAdmin && !onRestore && (
35
105
  <div className="absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
106
+ <button
107
+ className={`${
108
+ visibility === 'PUBLIC'
109
+ ? 'bg-green-500 hover:bg-green-600'
110
+ : 'bg-gray-500 hover:bg-gray-600'
111
+ } ${ACTION_BUTTON_CLASSES}`}
112
+ onClick={(e) => {
113
+ e.preventDefault();
114
+ onToggleVisibility?.(agent.permanentId || agent.agentName);
115
+ }}
116
+ title={`Make ${visibility === 'PUBLIC' ? 'private' : 'public'}`}
117
+ >
118
+ {visibility === 'PUBLIC' ? <EyeIcon className="w-3 h-3" /> : <EyeOffIcon className="w-3 h-3" />}
119
+ </button>
36
120
  <button
37
121
  className={`bg-blue-500 hover:bg-blue-600 ${ACTION_BUTTON_CLASSES}`}
38
122
  onClick={(e) => {
39
123
  e.preventDefault();
40
- onClone?.(agent.agentName);
124
+ onClone?.(agent.permanentId || agent.agentName);
41
125
  }}
42
126
  title="Clone agent"
43
127
  >
@@ -47,7 +131,7 @@ export function AgentCard({ agent, href, isAdmin, onDelete, onClone }: AgentCard
47
131
  className={`bg-red-500 hover:bg-red-600 ${ACTION_BUTTON_CLASSES}`}
48
132
  onClick={(e) => {
49
133
  e.preventDefault();
50
- onDelete?.(agent.agentName);
134
+ onDelete?.(agent.permanentId || agent.agentName);
51
135
  }}
52
136
  title="Delete agent"
53
137
  >
@@ -2,6 +2,7 @@
2
2
  'use client';
3
3
 
4
4
  import React, { useState } from 'react';
5
+ import { useRouter } from 'next/navigation';
5
6
  import { TrashIcon } from 'lucide-react';
6
7
  import Link from 'next/link';
7
8
  import { AddAgentButton } from '../../app/AddAgentButton';
@@ -10,37 +11,96 @@ import { Section } from './Section';
10
11
 
11
12
  import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
12
13
 
14
+ type AgentWithVisibility = AgentBasicInformation & {
15
+ visibility?: 'PUBLIC' | 'PRIVATE';
16
+ };
17
+
13
18
  type AgentsListProps = {
14
- agents: AgentBasicInformation[];
19
+ agents: AgentWithVisibility[];
15
20
  isAdmin: boolean;
16
21
  };
17
22
 
18
23
  export function AgentsList({ agents: initialAgents, isAdmin }: AgentsListProps) {
24
+ const router = useRouter();
19
25
  const [agents, setAgents] = useState(Array.from(initialAgents));
20
26
 
21
- const handleDelete = async (agentName: string) => {
22
- if (!window.confirm(`Delete agent "${agentName}"? It will be moved to Recycle Bin.`)) return;
23
- await fetch(`/api/agents/${encodeURIComponent(agentName)}`, { method: 'DELETE' });
24
- setAgents(agents.filter((a) => a.agentName !== agentName));
27
+ const handleDelete = async (agentIdentifier: string) => {
28
+ const agent = agents.find(a => a.permanentId === agentIdentifier || a.agentName === agentIdentifier);
29
+ if (!agent) return;
30
+ if (!window.confirm(`Delete agent "${agent.agentName}"? It will be moved to Recycle Bin.`)) return;
31
+
32
+ try {
33
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentIdentifier)}`, { method: 'DELETE' });
34
+ if (response.ok) {
35
+ // Update local state immediately
36
+ setAgents(agents.filter((a) => a.permanentId !== agent.permanentId && a.agentName !== agent.agentName));
37
+ // Note: router.refresh() is not needed here as the local state update is sufficient
38
+ // and prevents the brief empty list issue during refresh
39
+ } else {
40
+ alert('Failed to delete agent');
41
+ }
42
+ } catch (error) {
43
+ alert('Failed to delete agent');
44
+ }
25
45
  };
26
46
 
27
- const handleClone = async (agentName: string) => {
28
- if (!window.confirm(`Clone agent "${agentName}"?`)) return;
29
- const response = await fetch(`/api/agents/${encodeURIComponent(agentName)}/clone`, { method: 'POST' });
30
- const newAgent = await response.json();
31
- setAgents([...agents, newAgent]);
47
+ const handleClone = async (agentIdentifier: string) => {
48
+ const agent = agents.find(a => a.permanentId === agentIdentifier || a.agentName === agentIdentifier);
49
+ if (!agent) return;
50
+ if (!window.confirm(`Clone agent "${agent.agentName}"?`)) return;
51
+
52
+ try {
53
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentIdentifier)}/clone`, { method: 'POST' });
54
+ if (response.ok) {
55
+ const newAgent = await response.json();
56
+ setAgents([...agents, newAgent]);
57
+ router.refresh(); // Refresh server data to ensure consistency
58
+ } else {
59
+ alert('Failed to clone agent');
60
+ }
61
+ } catch (error) {
62
+ alert('Failed to clone agent');
63
+ }
64
+ };
65
+
66
+ const handleToggleVisibility = async (agentIdentifier: string) => {
67
+ const agent = agents.find(a => a.permanentId === agentIdentifier || a.agentName === agentIdentifier);
68
+ if (!agent) return;
69
+
70
+ const newVisibility = agent.visibility === 'PUBLIC' ? 'PRIVATE' : 'PUBLIC';
71
+ if (!window.confirm(`Make agent "${agent.agentName}" ${newVisibility.toLowerCase()}?`)) return;
72
+
73
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentIdentifier)}`, {
74
+ method: 'PATCH',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({ visibility: newVisibility }),
77
+ });
78
+
79
+ if (response.ok) {
80
+ // Update the local state
81
+ setAgents(agents.map(a =>
82
+ a.permanentId === agent.permanentId || a.agentName === agent.agentName
83
+ ? { ...a, visibility: newVisibility }
84
+ : a
85
+ ));
86
+ router.refresh(); // Refresh server data to ensure consistency
87
+ } else {
88
+ alert('Failed to update agent visibility');
89
+ }
32
90
  };
33
91
 
34
92
  return (
35
93
  <Section title={`Agents (${agents.length})`}>
36
94
  {agents.map((agent) => (
37
95
  <AgentCard
38
- key={agent.agentName}
96
+ key={agent.permanentId || agent.agentName}
39
97
  agent={agent}
40
- href={`/${agent.agentName}`}
98
+ href={`/agents/${encodeURIComponent(agent.permanentId || agent.agentName)}`}
41
99
  isAdmin={isAdmin}
42
100
  onDelete={handleDelete}
43
101
  onClone={handleClone}
102
+ onToggleVisibility={handleToggleVisibility}
103
+ visibility={agent.visibility}
44
104
  />
45
105
  ))}
46
106
  {isAdmin && <AddAgentButton />}
@@ -0,0 +1,50 @@
1
+ // Client Component for rendering deleted agents
2
+ 'use client';
3
+
4
+ import React, { useState } from 'react';
5
+ import { AgentCard } from './AgentCard';
6
+
7
+ import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
8
+
9
+ type DeletedAgentsListProps = {
10
+ agents: readonly AgentBasicInformation[];
11
+ isAdmin: boolean;
12
+ };
13
+
14
+ export function DeletedAgentsList({ agents: initialAgents, isAdmin }: DeletedAgentsListProps) {
15
+ const [agents, setAgents] = useState(Array.from(initialAgents));
16
+
17
+ const handleRestore = async (agentIdentifier: string) => {
18
+ const agent = agents.find(a => a.permanentId === agentIdentifier || a.agentName === agentIdentifier);
19
+ if (!agent) return;
20
+ if (!window.confirm(`Restore agent "${agent.agentName}"?`)) return;
21
+
22
+ try {
23
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentIdentifier)}/restore`, { method: 'POST' });
24
+ if (response.ok) {
25
+ // Update local state immediately
26
+ setAgents(agents.filter((a) => a.permanentId !== agent.permanentId && a.agentName !== agent.agentName));
27
+ // Note: router.refresh() is not needed here as the local state update is sufficient
28
+ // and prevents the brief empty list issue during refresh
29
+ } else {
30
+ alert('Failed to restore agent');
31
+ }
32
+ } catch (error) {
33
+ alert('Failed to restore agent');
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
39
+ {agents.map((agent) => (
40
+ <AgentCard
41
+ key={agent.permanentId || agent.agentName}
42
+ agent={agent}
43
+ href={`/agents/${encodeURIComponent(agent.permanentId || agent.agentName)}`}
44
+ isAdmin={isAdmin}
45
+ onRestore={handleRestore}
46
+ />
47
+ ))}
48
+ </div>
49
+ );
50
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { usePathname, useSearchParams } from 'next/navigation';
4
4
  import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
5
+ import { MenuHoistingProvider } from '../../../../../src/book-components/_common/MenuHoisting/MenuHoistingContext';
5
6
  import type { UserInfo } from '../../utils/getCurrentUser';
6
7
  import { Footer, type FooterLink } from '../Footer/Footer';
7
8
  import { Header } from '../Header/Header';
@@ -41,7 +42,7 @@ export function LayoutWrapper({
41
42
  }
42
43
 
43
44
  return (
44
- <>
45
+ <MenuHoistingProvider>
45
46
  <Header
46
47
  isAdmin={isAdmin}
47
48
  currentUser={currentUser}
@@ -52,6 +53,6 @@ export function LayoutWrapper({
52
53
  />
53
54
  <main className={`pt-[60px]`}>{children}</main>
54
55
  {isFooterShown && !isFooterHiddenOnPage && <Footer extraLinks={footerLinks} />}
55
- </>
56
+ </MenuHoistingProvider>
56
57
  );
57
58
  }
@@ -1,8 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { loginAction } from '@/src/app/actions';
4
+ import { ForgottenPasswordDialog } from '../ForgottenPasswordDialog/ForgottenPasswordDialog';
5
+ import { RegisterUserDialog } from '../RegisterUserDialog/RegisterUserDialog';
4
6
  import { Loader2, Lock, User } from 'lucide-react';
5
- import { useState } from 'react';
7
+ import { useEffect, useState } from 'react';
6
8
 
7
9
  type LoginFormProps = {
8
10
  onSuccess?: () => void;
@@ -13,6 +15,24 @@ export function LoginForm(props: LoginFormProps) {
13
15
  const { onSuccess, className } = props;
14
16
  const [isLoading, setIsLoading] = useState(false);
15
17
  const [error, setError] = useState<string | null>(null);
18
+ const [adminEmail, setAdminEmail] = useState<string>('support@ptbk.io');
19
+ const [isForgottenPasswordOpen, setIsForgottenPasswordOpen] = useState(false);
20
+ const [isRegisterUserOpen, setIsRegisterUserOpen] = useState(false);
21
+
22
+ useEffect(() => {
23
+ // Fetch admin email on component mount
24
+ fetch('/api/admin-email')
25
+ .then(response => response.json())
26
+ .then(data => {
27
+ if (data.adminEmail) {
28
+ setAdminEmail(data.adminEmail);
29
+ }
30
+ })
31
+ .catch(error => {
32
+ console.error('Failed to fetch admin email:', error);
33
+ // Keep default value
34
+ });
35
+ }, []);
16
36
 
17
37
  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
18
38
  event.preventDefault();
@@ -104,6 +124,35 @@ export function LoginForm(props: LoginFormProps) {
104
124
  'Log in'
105
125
  )}
106
126
  </button>
127
+
128
+ <div className="flex justify-between text-sm">
129
+ <button
130
+ type="button"
131
+ onClick={() => setIsForgottenPasswordOpen(true)}
132
+ className="text-promptbook-blue hover:text-promptbook-blue-dark underline focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 rounded-sm"
133
+ >
134
+ Forgotten password?
135
+ </button>
136
+ <button
137
+ type="button"
138
+ onClick={() => setIsRegisterUserOpen(true)}
139
+ className="text-promptbook-blue hover:text-promptbook-blue-dark underline focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 rounded-sm"
140
+ >
141
+ Register new user
142
+ </button>
143
+ </div>
144
+
145
+ <ForgottenPasswordDialog
146
+ isOpen={isForgottenPasswordOpen}
147
+ onClose={() => setIsForgottenPasswordOpen(false)}
148
+ adminEmail={adminEmail}
149
+ />
150
+
151
+ <RegisterUserDialog
152
+ isOpen={isRegisterUserOpen}
153
+ onClose={() => setIsRegisterUserOpen(false)}
154
+ adminEmail={adminEmail}
155
+ />
107
156
  </form>
108
157
  );
109
158
  }