@promptbook/cli 0.103.0-54 → 0.103.0-56

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 (80) hide show
  1. package/apps/agents-server/config.ts +0 -2
  2. package/apps/agents-server/package-lock.json +1163 -0
  3. package/apps/agents-server/package.json +6 -0
  4. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +79 -6
  5. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +171 -69
  6. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +3 -1
  7. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +216 -0
  8. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +78 -0
  9. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileView.tsx +233 -0
  10. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +4 -4
  11. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +2 -2
  12. package/apps/agents-server/src/app/agents/[agentName]/QrCodeModal.tsx +90 -0
  13. package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +80 -0
  14. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +3 -1
  15. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +11 -1
  16. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +203 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +3 -1
  18. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -1
  19. package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +3 -169
  20. package/apps/agents-server/src/app/agents/[agentName]/api/openai/models/route.ts +93 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/chat/completions/route.ts +10 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/api/openai/v1/models/route.ts +93 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/openrouter/chat/completions/route.ts +10 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +4 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +9 -2
  26. package/apps/agents-server/src/app/agents/[agentName]/integration/SdkCodeTabs.tsx +31 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +271 -30
  28. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +182 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +108 -165
  30. package/apps/agents-server/src/app/agents/[agentName]/website-integration/page.tsx +61 -0
  31. package/apps/agents-server/src/app/api/auth/change-password/route.ts +75 -0
  32. package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +55 -0
  33. package/apps/agents-server/src/app/api/chat-history/export/route.ts +55 -0
  34. package/apps/agents-server/src/app/api/openai/v1/chat/completions/route.ts +6 -0
  35. package/apps/agents-server/src/app/api/openai/v1/models/route.ts +65 -0
  36. package/apps/agents-server/src/app/docs/[docId]/page.tsx +12 -32
  37. package/apps/agents-server/src/app/docs/page.tsx +42 -17
  38. package/apps/agents-server/src/app/globals.css +129 -0
  39. package/apps/agents-server/src/app/layout.tsx +8 -2
  40. package/apps/agents-server/src/app/manifest.ts +1 -1
  41. package/apps/agents-server/src/components/ChangePasswordDialog/ChangePasswordDialog.tsx +41 -0
  42. package/apps/agents-server/src/components/ChangePasswordForm/ChangePasswordForm.tsx +159 -0
  43. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +87 -0
  44. package/apps/agents-server/src/components/Header/Header.tsx +94 -38
  45. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +20 -0
  46. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +18 -0
  47. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +18 -0
  48. package/apps/agents-server/src/database/migrations/2025-12-0070-chat-history-source.sql +2 -0
  49. package/apps/agents-server/src/database/schema.ts +6 -0
  50. package/apps/agents-server/src/middleware.ts +1 -1
  51. package/apps/agents-server/src/utils/convertToCsv.ts +31 -0
  52. package/apps/agents-server/src/utils/handleChatCompletion.ts +355 -0
  53. package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +100 -0
  54. package/apps/agents-server/src/utils/validateApiKey.ts +128 -0
  55. package/apps/agents-server/tailwind.config.ts +1 -1
  56. package/esm/index.es.js +1188 -175
  57. package/esm/index.es.js.map +1 -1
  58. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +4 -0
  59. package/esm/typings/src/book-components/Chat/LlmChat/LlmChatProps.d.ts +5 -0
  60. package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +35 -0
  61. package/esm/typings/src/commitments/COMPONENT/COMPONENT.d.ts +28 -0
  62. package/esm/typings/src/commitments/FROM/FROM.d.ts +34 -0
  63. package/esm/typings/src/commitments/LANGUAGE/LANGUAGE.d.ts +35 -0
  64. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +6 -0
  65. package/esm/typings/src/commitments/META_FONT/META_FONT.d.ts +42 -0
  66. package/esm/typings/src/commitments/OPEN/OPEN.d.ts +35 -0
  67. package/esm/typings/src/commitments/USE/USE.d.ts +53 -0
  68. package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.d.ts +38 -0
  69. package/esm/typings/src/commitments/USE_BROWSER/USE_BROWSER.test.d.ts +1 -0
  70. package/esm/typings/src/commitments/USE_MCP/USE_MCP.d.ts +37 -0
  71. package/esm/typings/src/commitments/USE_SEARCH_ENGINE/USE_SEARCH_ENGINE.d.ts +38 -0
  72. package/esm/typings/src/commitments/index.d.ts +12 -1
  73. package/esm/typings/src/playground/playground.d.ts +3 -0
  74. package/esm/typings/src/utils/color/Color.d.ts +8 -0
  75. package/esm/typings/src/utils/color/css-colors.d.ts +1 -0
  76. package/esm/typings/src/version.d.ts +1 -1
  77. package/package.json +2 -2
  78. package/umd/index.umd.js +1180 -167
  79. package/umd/index.umd.js.map +1 -1
  80. package/esm/typings/src/playground/playground1.d.ts +0 -2
@@ -0,0 +1,159 @@
1
+ 'use client';
2
+
3
+ import { Loader2, Lock } from 'lucide-react';
4
+ import { useState } from 'react';
5
+
6
+ type ChangePasswordFormProps = {
7
+ onSuccess?: () => void;
8
+ className?: string;
9
+ };
10
+
11
+ export function ChangePasswordForm(props: ChangePasswordFormProps) {
12
+ const { onSuccess, className } = props;
13
+ const [isLoading, setIsLoading] = useState(false);
14
+ const [error, setError] = useState<string | null>(null);
15
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
16
+
17
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
18
+ event.preventDefault();
19
+ setIsLoading(true);
20
+ setError(null);
21
+ setSuccessMessage(null);
22
+
23
+ const formData = new FormData(event.currentTarget);
24
+ const currentPassword = formData.get('currentPassword') as string;
25
+ const newPassword = formData.get('newPassword') as string;
26
+ const confirmNewPassword = formData.get('confirmNewPassword') as string;
27
+
28
+ if (newPassword !== confirmNewPassword) {
29
+ setError('New passwords do not match');
30
+ setIsLoading(false);
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const response = await fetch('/api/auth/change-password', {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ },
40
+ body: JSON.stringify({ currentPassword, newPassword }),
41
+ });
42
+
43
+ const result = await response.json();
44
+
45
+ if (response.ok) {
46
+ setSuccessMessage('Password changed successfully');
47
+ // Reset form
48
+ event.currentTarget.reset();
49
+ if (onSuccess) {
50
+ setTimeout(onSuccess, 1500);
51
+ }
52
+ } else {
53
+ setError(result.error || 'An error occurred');
54
+ }
55
+ } catch (error) {
56
+ setError('An unexpected error occurred');
57
+ console.error(error);
58
+ } finally {
59
+ setIsLoading(false);
60
+ }
61
+ };
62
+
63
+ return (
64
+ <form onSubmit={handleSubmit} className={`space-y-4 ${className || ''}`}>
65
+ <div className="space-y-2">
66
+ <label
67
+ htmlFor="currentPassword"
68
+ className="text-sm font-medium text-gray-700 block"
69
+ >
70
+ Current Password
71
+ </label>
72
+ <div className="relative">
73
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
74
+ <Lock className="w-4 h-4" />
75
+ </div>
76
+ <input
77
+ id="currentPassword"
78
+ name="currentPassword"
79
+ type="password"
80
+ required
81
+ className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
82
+ placeholder="Enter current password"
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <div className="space-y-2">
88
+ <label
89
+ htmlFor="newPassword"
90
+ className="text-sm font-medium text-gray-700 block"
91
+ >
92
+ New Password
93
+ </label>
94
+ <div className="relative">
95
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
96
+ <Lock className="w-4 h-4" />
97
+ </div>
98
+ <input
99
+ id="newPassword"
100
+ name="newPassword"
101
+ type="password"
102
+ required
103
+ className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
104
+ placeholder="Enter new password"
105
+ />
106
+ </div>
107
+ </div>
108
+
109
+ <div className="space-y-2">
110
+ <label
111
+ htmlFor="confirmNewPassword"
112
+ className="text-sm font-medium text-gray-700 block"
113
+ >
114
+ Confirm New Password
115
+ </label>
116
+ <div className="relative">
117
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
118
+ <Lock className="w-4 h-4" />
119
+ </div>
120
+ <input
121
+ id="confirmNewPassword"
122
+ name="confirmNewPassword"
123
+ type="password"
124
+ required
125
+ className="block w-full pl-10 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:border-transparent disabled:opacity-50"
126
+ placeholder="Confirm new password"
127
+ />
128
+ </div>
129
+ </div>
130
+
131
+ {error && (
132
+ <div className="p-3 text-sm text-red-500 bg-red-50 border border-red-200 rounded-md">
133
+ {error}
134
+ </div>
135
+ )}
136
+
137
+ {successMessage && (
138
+ <div className="p-3 text-sm text-green-500 bg-green-50 border border-green-200 rounded-md">
139
+ {successMessage}
140
+ </div>
141
+ )}
142
+
143
+ <button
144
+ type="submit"
145
+ disabled={isLoading}
146
+ className="w-full inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-promptbook-blue-dark text-white hover:bg-promptbook-blue-dark/90 focus:outline-none focus:ring-2 focus:ring-promptbook-blue focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none transition-colors"
147
+ >
148
+ {isLoading ? (
149
+ <>
150
+ <Loader2 className="mr-2 w-4 h-4 animate-spin" />
151
+ Changing Password...
152
+ </>
153
+ ) : (
154
+ 'Change Password'
155
+ )}
156
+ </button>
157
+ </form>
158
+ );
159
+ }
@@ -0,0 +1,87 @@
1
+ import ReactMarkdown from 'react-markdown';
2
+ import remarkGfm from 'remark-gfm';
3
+ import { string_book } from '../../../../../src/book-2.0/agent-source/string_book';
4
+ import { BookEditor } from '../../../../../src/book-components/BookEditor/BookEditor';
5
+ import { OpenMojiIcon } from '../OpenMojiIcon/OpenMojiIcon';
6
+
7
+ type DocumentationContentProps = {
8
+ primary: {
9
+ type: string;
10
+ icon: string;
11
+ description?: string;
12
+ documentation: string;
13
+ };
14
+ aliases?: string[];
15
+ isPrintOnly?: boolean;
16
+ };
17
+
18
+ export function DocumentationContent({ primary, aliases = [], isPrintOnly = false }: DocumentationContentProps) {
19
+ return (
20
+ <div className={`bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden ${isPrintOnly ? 'shadow-none border-none' : ''}`}>
21
+ <div className={`p-8 border-b border-gray-100 bg-gray-50/50 ${isPrintOnly ? 'border-none bg-white p-0 mb-4' : ''}`}>
22
+ <div className="flex items-center gap-4 mb-4">
23
+ <h1 className="text-4xl font-bold text-gray-900 tracking-tight">
24
+ <OpenMojiIcon icon={primary.icon} className="mr-3" />
25
+ {primary.type}
26
+ {aliases.length > 0 && (
27
+ <span className="text-gray-400 font-normal ml-4 text-2xl">
28
+ / {aliases.join(' / ')}
29
+ </span>
30
+ )}
31
+ </h1>
32
+ {!isPrintOnly && (
33
+ <span className="px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
34
+ Commitment
35
+ </span>
36
+ )}
37
+ </div>
38
+ {primary.description && (
39
+ <p className="text-xl text-gray-600 leading-relaxed max-w-3xl">
40
+ {primary.description}
41
+ </p>
42
+ )}
43
+ </div>
44
+
45
+ <div className={`p-8 ${isPrintOnly ? 'p-0' : ''}`}>
46
+ <article className="prose prose-lg prose-slate max-w-none prose-headings:font-bold prose-headings:tracking-tight prose-headings:text-gray-900 prose-h1:text-4xl prose-h1:mb-8 prose-h2:text-2xl prose-h2:mt-12 prose-h2:mb-6 prose-h2:pb-2 prose-h2:border-b prose-h2:border-gray-200 prose-h3:text-xl prose-h3:mt-8 prose-h3:mb-4 prose-h3:text-gray-800 prose-p:text-gray-600 prose-p:leading-relaxed prose-p:mb-6 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:text-blue-700 hover:prose-a:underline prose-a:transition-colors prose-strong:font-bold prose-strong:text-gray-900 prose-code:text-blue-600 prose-code:bg-blue-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:content-none prose-code:after:content-none prose-code:font-medium prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:shadow-lg prose-pre:rounded-xl prose-pre:p-6 prose-ul:list-disc prose-ul:pl-6 prose-li:marker:text-gray-400 prose-li:mb-2 prose-ol:list-decimal prose-ol:pl-6 prose-li:mb-2 prose-blockquote:border-l-4 prose-blockquote:border-blue-500 prose-blockquote:bg-blue-50/50 prose-blockquote:py-2 prose-blockquote:px-4 prose-blockquote:rounded-r-lg prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-blockquote:my-8 prose-img:rounded-xl prose-img:shadow-md prose-img:my-8 prose-hr:border-gray-200 prose-hr:my-10 prose-table:w-full prose-th:text-left prose-th:py-2 prose-th:px-3 prose-th:bg-gray-100 prose-th:font-semibold prose-th:text-gray-900 prose-td:py-2 prose-td:px-3 prose-td:border-b prose-td:border-gray-200 prose-tr:hover:bg-gray-50">
47
+ <ReactMarkdown
48
+ remarkPlugins={[remarkGfm]}
49
+ components={{
50
+ code(props) {
51
+ const { children, className, node, ...rest } = props;
52
+ const match = /language-(\w+)/.exec(className || '');
53
+ if (match && match[1] === 'book') {
54
+ const value = String(children).replace(/\n$/, '');
55
+ // Estimate height: lines * 30px + padding
56
+ const lineCount = value.split('\n').length;
57
+ const height = lineCount * 30 + 40; // 30px per line + 40px buffer
58
+
59
+ return (
60
+ <div className="not-prose my-6 rounded-lg overflow-hidden border border-gray-200 shadow-sm print:border-gray-300">
61
+ <BookEditor
62
+ value={value as string_book}
63
+ isReadonly={true}
64
+ isVerbose={false}
65
+ height={`${height}px`}
66
+ isDownloadButtonShown={false}
67
+ isAboutButtonShown={false}
68
+ isFullscreenButtonShown={true}
69
+ />
70
+ </div>
71
+ );
72
+ }
73
+ return (
74
+ <code className={className} {...rest}>
75
+ {children}
76
+ </code>
77
+ );
78
+ },
79
+ }}
80
+ >
81
+ {primary.documentation}
82
+ </ReactMarkdown>
83
+ </article>
84
+ </div>
85
+ </div>
86
+ );
87
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import promptbookLogoBlueTransparent from '@/public/logo-blue-white-256.png';
4
4
  import { $createAgentAction, logoutAction } from '@/src/app/actions';
5
- import { ArrowRight, ChevronDown, LogIn, LogOut } from 'lucide-react';
5
+ import { ArrowRight, ChevronDown, Lock, LogIn, LogOut, User } from 'lucide-react';
6
6
  import Image from 'next/image';
7
7
  import Link from 'next/link';
8
8
  import { useRouter } from 'next/navigation';
@@ -12,6 +12,7 @@ import { HamburgerMenu } from '../../../../../src/book-components/_common/Hambur
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 { ChangePasswordDialog } from '../ChangePasswordDialog/ChangePasswordDialog';
15
16
  import { LoginDialog } from '../LoginDialog/LoginDialog';
16
17
  import { useUsersAdmin } from '../UsersList/useUsersAdmin';
17
18
 
@@ -73,12 +74,16 @@ export function Header(props: HeaderProps) {
73
74
 
74
75
  const [isMenuOpen, setIsMenuOpen] = useState(false);
75
76
  const [isLoginOpen, setIsLoginOpen] = useState(false);
77
+ const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
76
78
  const [isAgentsOpen, setIsAgentsOpen] = useState(false);
77
79
  const [isDocsOpen, setIsDocsOpen] = useState(false);
78
80
  const [isUsersOpen, setIsUsersOpen] = useState(false);
81
+ const [isSystemOpen, setIsSystemOpen] = useState(false);
82
+ const [isProfileOpen, setIsProfileOpen] = useState(false);
79
83
  const [isMobileAgentsOpen, setIsMobileAgentsOpen] = useState(false);
80
84
  const [isMobileDocsOpen, setIsMobileDocsOpen] = useState(false);
81
85
  const [isMobileUsersOpen, setIsMobileUsersOpen] = useState(false);
86
+ const [isMobileSystemOpen, setIsMobileSystemOpen] = useState(false);
82
87
  const [isCreatingAgent, setIsCreatingAgent] = useState(false);
83
88
  const router = useRouter();
84
89
 
@@ -131,7 +136,9 @@ export function Header(props: HeaderProps) {
131
136
  label: (
132
137
  <>
133
138
  {primary.type}
134
- {aliases.length > 0 && <span className="text-gray-400 font-normal"> / {aliases.join(' / ')}</span>}
139
+ {aliases.length > 0 && (
140
+ <span className="text-gray-400 font-normal"> / {aliases.join(' / ')}</span>
141
+ )}
135
142
  </>
136
143
  ),
137
144
  href: `/docs/${primary.type}`,
@@ -210,24 +217,30 @@ export function Header(props: HeaderProps) {
210
217
  ],
211
218
  },
212
219
  {
213
- type: 'link' as const,
214
- label: 'API Tokens',
215
- href: '/admin/api-tokens',
216
- },
217
- {
218
- type: 'link' as const,
219
- label: 'Metadata',
220
- href: '/admin/metadata',
221
- },
222
- {
223
- type: 'link' as const,
224
- label: 'Chat history',
225
- href: '/admin/chat-history',
226
- },
227
- {
228
- type: 'link' as const,
229
- label: 'Chat feedback',
230
- href: '/admin/chat-feedback',
220
+ type: 'dropdown' as const,
221
+ label: 'System',
222
+ isOpen: isSystemOpen,
223
+ setIsOpen: setIsSystemOpen,
224
+ isMobileOpen: isMobileSystemOpen,
225
+ setIsMobileOpen: setIsMobileSystemOpen,
226
+ items: [
227
+ {
228
+ label: 'API Tokens',
229
+ href: '/admin/api-tokens',
230
+ },
231
+ {
232
+ label: 'Metadata',
233
+ href: '/admin/metadata',
234
+ },
235
+ {
236
+ label: 'Chat history',
237
+ href: '/admin/chat-history',
238
+ },
239
+ {
240
+ label: 'Chat feedback',
241
+ href: '/admin/chat-feedback',
242
+ },
243
+ ],
231
244
  },
232
245
  {
233
246
  type: 'link' as const,
@@ -241,6 +254,7 @@ export function Header(props: HeaderProps) {
241
254
  return (
242
255
  <header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200 h-16">
243
256
  <LoginDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
257
+ <ChangePasswordDialog isOpen={isChangePasswordOpen} onClose={() => setIsChangePasswordOpen(false)} />
244
258
  <div className="container mx-auto px-4 h-full">
245
259
  <div className="flex items-center justify-between h-full">
246
260
  {/* Logo */}
@@ -370,24 +384,56 @@ export function Header(props: HeaderProps) {
370
384
 
371
385
  {(currentUser || isAdmin) && (
372
386
  <div className="hidden lg:flex items-center gap-3">
373
- <span className="inline text-sm text-gray-600">
374
- Logged in as <strong>{currentUser?.username || 'Admin'}</strong>
375
- {(currentUser?.isAdmin || isAdmin) && (
376
- <span className="ml-2 bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
377
- Admin
378
- </span>
387
+ <div className="relative">
388
+ <button
389
+ onClick={() => setIsProfileOpen(!isProfileOpen)}
390
+ onBlur={() => setTimeout(() => setIsProfileOpen(false), 200)}
391
+ className="flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors px-3 py-2 rounded-md hover:bg-gray-50"
392
+ >
393
+ <div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
394
+ <User className="w-4 h-4" />
395
+ </div>
396
+ <div className="flex flex-col items-start">
397
+ <span className="leading-none">{currentUser?.username || 'Admin'}</span>
398
+ {(currentUser?.isAdmin || isAdmin) && (
399
+ <span className="text-xs text-blue-600">Admin</span>
400
+ )}
401
+ </div>
402
+ <ChevronDown className="w-4 h-4 ml-1 opacity-50" />
403
+ </button>
404
+
405
+ {isProfileOpen && (
406
+ <div className="absolute top-full right-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">
407
+ <div className="px-4 py-3 border-b border-gray-100">
408
+ <p className="text-sm font-medium text-gray-900">
409
+ {currentUser?.username || 'Admin'}
410
+ </p>
411
+ {(currentUser?.isAdmin || isAdmin) && (
412
+ <p className="text-xs text-blue-600 mt-1">Administrator</p>
413
+ )}
414
+ </div>
415
+
416
+ <button
417
+ onClick={() => setIsChangePasswordOpen(true)}
418
+ className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 flex items-center gap-2"
419
+ >
420
+ <Lock className="w-4 h-4" />
421
+ Change Password
422
+ </button>
423
+
424
+ <button
425
+ onClick={() => {
426
+ handleLogout();
427
+ setIsMenuOpen(false);
428
+ }}
429
+ className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 hover:text-red-700 flex items-center gap-2"
430
+ >
431
+ <LogOut className="w-4 h-4" />
432
+ Log out
433
+ </button>
434
+ </div>
379
435
  )}
380
- </span>
381
- <button
382
- onClick={() => {
383
- handleLogout();
384
- setIsMenuOpen(false);
385
- }}
386
- 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"
387
- >
388
- Log out
389
- <LogOut className="ml-2 w-4 h-4" />
390
- </button>
436
+ </div>
391
437
  </div>
392
438
  )}
393
439
 
@@ -403,7 +449,7 @@ export function Header(props: HeaderProps) {
403
449
  {/* Mobile Navigation */}
404
450
  {isMenuOpen && (
405
451
  <div
406
- 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"
452
+ className="lg:hidden absolute top-16 left-0 right-0 z-50 bg-white shadow-xl py-4 border-t border-gray-100 animate-in slide-in-from-top-2 h-[calc(100vh-4rem)] overflow-y-auto"
407
453
  style={{
408
454
  backdropFilter: 'blur(20px)',
409
455
  WebkitBackdropFilter: 'blur(20px)',
@@ -435,6 +481,16 @@ export function Header(props: HeaderProps) {
435
481
  </span>
436
482
  )}
437
483
  </div>
484
+ <button
485
+ onClick={() => {
486
+ setIsChangePasswordOpen(true);
487
+ setIsMenuOpen(false);
488
+ }}
489
+ className="flex items-center gap-2 text-base font-medium text-gray-600 hover:text-gray-900 w-full"
490
+ >
491
+ <Lock className="w-4 h-4" />
492
+ Change Password
493
+ </button>
438
494
  <button
439
495
  onClick={() => {
440
496
  handleLogout();
@@ -0,0 +1,20 @@
1
+ import { DetailedHTMLProps, HTMLAttributes } from 'react';
2
+
3
+ type OpenMojiIconProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
4
+ icon: string;
5
+ };
6
+
7
+ /**
8
+ * Renders an emoji using the OpenMoji black and white font
9
+ */
10
+ export function OpenMojiIcon({ icon, className, style, ...rest }: OpenMojiIconProps) {
11
+ return (
12
+ <span
13
+ className={className}
14
+ style={{ ...style, fontFamily: '"OpenMojiBlack", sans-serif' }}
15
+ {...rest}
16
+ >
17
+ {icon}
18
+ </span>
19
+ );
20
+ }
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+
3
+ import { OpenMojiIcon } from '../OpenMojiIcon/OpenMojiIcon';
4
+
5
+ export function PrintButton() {
6
+ return (
7
+ <button
8
+ onClick={() => window.print()}
9
+ className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-4 shadow-lg transition-all hover:scale-105 print:hidden z-50 flex items-center justify-center gap-2 group"
10
+ title="Print documentation"
11
+ >
12
+ <OpenMojiIcon icon="🖨️" className="text-2xl" />
13
+ <span className="max-w-0 overflow-hidden group-hover:max-w-xs transition-all duration-300 ease-in-out whitespace-nowrap">
14
+ Print
15
+ </span>
16
+ </button>
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+ export function PrintHeader({ title }: { title?: string }) {
2
+ return (
3
+ <div className="hidden print:block mb-8 border-b-2 border-blue-600 pb-4">
4
+ <div className="flex justify-between items-end">
5
+ <div>
6
+ <h1 className="text-3xl font-bold text-gray-900 font-poppins">Agents Server</h1>
7
+ <div className="text-sm text-gray-500 mt-1 flex items-center gap-1">
8
+ Powered by <span className="font-semibold text-blue-600">Promptbook</span>
9
+ </div>
10
+ </div>
11
+ {title && <h2 className="text-xl font-semibold text-gray-700">{title}</h2>}
12
+ </div>
13
+ <div className="text-xs text-gray-400 mt-2 text-right">
14
+ {new Date().toLocaleDateString()}
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "ChatHistory" ADD COLUMN "source" TEXT CHECK ("source" IN ('AGENT_PAGE_CHAT', 'OPENAI_API_COMPATIBILITY'));
2
+ ALTER TABLE "ChatHistory" ADD COLUMN "apiKey" TEXT;
@@ -128,6 +128,8 @@ export type AgentsServerDatabase = {
128
128
  userAgent: string | null;
129
129
  language: string | null;
130
130
  platform: string | null;
131
+ source: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
132
+ apiKey: string | null;
131
133
  };
132
134
  Insert: {
133
135
  id?: number;
@@ -143,6 +145,8 @@ export type AgentsServerDatabase = {
143
145
  userAgent?: string | null;
144
146
  language?: string | null;
145
147
  platform?: string | null;
148
+ source?: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
149
+ apiKey?: string | null;
146
150
  };
147
151
  Update: {
148
152
  id?: number;
@@ -158,6 +162,8 @@ export type AgentsServerDatabase = {
158
162
  userAgent?: string | null;
159
163
  language?: string | null;
160
164
  platform?: string | null;
165
+ source?: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
166
+ apiKey?: string | null;
161
167
  };
162
168
  Relationships: [];
163
169
  };
@@ -134,7 +134,7 @@ export async function middleware(req: NextRequest) {
134
134
  headers: {
135
135
  'Access-Control-Allow-Origin': '*',
136
136
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
137
- 'Access-Control-Allow-Headers': 'Content-Type',
137
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
138
138
  },
139
139
  });
140
140
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Converts an array of objects to a CSV string
3
+ */
4
+ export function convertToCsv(data: Array<Record<string, unknown>>): string {
5
+ if (data.length === 0) {
6
+ return '';
7
+ }
8
+
9
+ const headers = Object.keys(data[0]);
10
+ const csvRows = [headers.join(',')];
11
+
12
+ for (const row of data) {
13
+ const values = headers.map((header) => {
14
+ let value = row[header];
15
+
16
+ if (value === null || value === undefined) {
17
+ value = '';
18
+ } else if (typeof value === 'object') {
19
+ value = JSON.stringify(value);
20
+ } else {
21
+ value = String(value);
22
+ }
23
+
24
+ const escaped = (value as string).replace(/"/g, '""');
25
+ return `"${escaped}"`;
26
+ });
27
+ csvRows.push(values.join(','));
28
+ }
29
+
30
+ return csvRows.join('\n');
31
+ }