@promptbook/cli 0.103.0-53 → 0.103.0-55

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 (76) hide show
  1. package/apps/agents-server/config.ts +0 -2
  2. package/apps/agents-server/src/app/admin/api-tokens/ApiTokensClient.tsx +186 -0
  3. package/apps/agents-server/src/app/admin/api-tokens/page.tsx +13 -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 +10 -2
  7. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +203 -0
  8. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +3 -1
  9. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -1
  10. package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +10 -0
  11. package/apps/agents-server/src/app/agents/[agentName]/api/openrouter/chat/completions/route.ts +10 -0
  12. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +218 -0
  13. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +24 -3
  14. package/apps/agents-server/src/app/api/api-tokens/route.ts +76 -0
  15. package/apps/agents-server/src/app/api/auth/change-password/route.ts +75 -0
  16. package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +55 -0
  17. package/apps/agents-server/src/app/api/chat-history/export/route.ts +55 -0
  18. package/apps/agents-server/src/app/docs/[docId]/page.tsx +1 -0
  19. package/apps/agents-server/src/app/docs/page.tsx +1 -0
  20. package/apps/agents-server/src/components/ChangePasswordDialog/ChangePasswordDialog.tsx +41 -0
  21. package/apps/agents-server/src/components/ChangePasswordForm/ChangePasswordForm.tsx +159 -0
  22. package/apps/agents-server/src/components/Header/Header.tsx +94 -33
  23. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +2 -1
  24. package/apps/agents-server/src/database/migrations/2025-12-0010-llm-cache.sql +12 -0
  25. package/apps/agents-server/src/database/migrations/2025-12-0060-api-tokens.sql +13 -0
  26. package/apps/agents-server/src/database/schema.ts +51 -0
  27. package/apps/agents-server/src/middleware.ts +50 -2
  28. package/apps/agents-server/src/tools/$provideCdnForServer.ts +3 -7
  29. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +10 -1
  30. package/apps/agents-server/src/utils/cache/SupabaseCacheStorage.ts +55 -0
  31. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +63 -0
  32. package/apps/agents-server/src/utils/convertToCsv.ts +31 -0
  33. package/apps/agents-server/src/utils/handleChatCompletion.ts +183 -0
  34. package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +93 -0
  35. package/esm/index.es.js +846 -131
  36. package/esm/index.es.js.map +1 -1
  37. package/esm/typings/src/_packages/core.index.d.ts +8 -6
  38. package/esm/typings/src/_packages/types.index.d.ts +1 -1
  39. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +4 -0
  40. package/esm/typings/src/commitments/ACTION/ACTION.d.ts +4 -0
  41. package/esm/typings/src/commitments/CLOSED/CLOSED.d.ts +35 -0
  42. package/esm/typings/src/commitments/COMPONENT/COMPONENT.d.ts +28 -0
  43. package/esm/typings/src/commitments/DELETE/DELETE.d.ts +4 -0
  44. package/esm/typings/src/commitments/FORMAT/FORMAT.d.ts +4 -0
  45. package/esm/typings/src/commitments/FROM/FROM.d.ts +34 -0
  46. package/esm/typings/src/commitments/GOAL/GOAL.d.ts +4 -0
  47. package/esm/typings/src/commitments/IMPORTANT/IMPORTANT.d.ts +26 -0
  48. package/esm/typings/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
  49. package/esm/typings/src/commitments/LANGUAGE/LANGUAGE.d.ts +35 -0
  50. package/esm/typings/src/commitments/MEMORY/MEMORY.d.ts +4 -0
  51. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +4 -0
  52. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +4 -0
  53. package/esm/typings/src/commitments/MESSAGE/MESSAGE.d.ts +4 -0
  54. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +4 -0
  55. package/esm/typings/src/commitments/META/META.d.ts +4 -0
  56. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +4 -0
  57. package/esm/typings/src/commitments/META_IMAGE/META_IMAGE.d.ts +4 -0
  58. package/esm/typings/src/commitments/META_LINK/META_LINK.d.ts +4 -0
  59. package/esm/typings/src/commitments/MODEL/MODEL.d.ts +4 -0
  60. package/esm/typings/src/commitments/NOTE/NOTE.d.ts +4 -0
  61. package/esm/typings/src/commitments/OPEN/OPEN.d.ts +35 -0
  62. package/esm/typings/src/commitments/PERSONA/PERSONA.d.ts +4 -0
  63. package/esm/typings/src/commitments/RULE/RULE.d.ts +4 -0
  64. package/esm/typings/src/commitments/SAMPLE/SAMPLE.d.ts +4 -0
  65. package/esm/typings/src/commitments/SCENARIO/SCENARIO.d.ts +4 -0
  66. package/esm/typings/src/commitments/STYLE/STYLE.d.ts +4 -0
  67. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +5 -0
  68. package/esm/typings/src/commitments/_base/CommitmentDefinition.d.ts +5 -0
  69. package/esm/typings/src/commitments/_base/NotYetImplementedCommitmentDefinition.d.ts +4 -0
  70. package/esm/typings/src/commitments/index.d.ts +1 -82
  71. package/esm/typings/src/commitments/registry.d.ts +68 -0
  72. package/esm/typings/src/version.d.ts +1 -1
  73. package/package.json +3 -3
  74. package/umd/index.umd.js +846 -131
  75. package/umd/index.umd.js.map +1 -1
  76. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +0 -119
@@ -0,0 +1,75 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../../database/$getTableName';
3
+ import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
4
+ import { AgentsServerDatabase } from '../../../../database/schema';
5
+ import { hashPassword, verifyPassword } from '../../../../utils/auth';
6
+ import { getCurrentUser } from '../../../../utils/getCurrentUser';
7
+
8
+ export async function POST(request: Request) {
9
+ try {
10
+ const user = await getCurrentUser();
11
+ if (!user) {
12
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
13
+ }
14
+
15
+ const body = await request.json();
16
+ const { currentPassword, newPassword } = body;
17
+
18
+ if (!currentPassword || !newPassword) {
19
+ return NextResponse.json({ error: 'Missing password fields' }, { status: 400 });
20
+ }
21
+
22
+ // Special check for environment admin
23
+ if (user.username === 'admin' && process.env.ADMIN_PASSWORD) {
24
+ // Environment admin cannot change password through this API
25
+ // They must change the env variable
26
+ return NextResponse.json(
27
+ {
28
+ error: 'You cannot change the admin password. Please update the `ADMIN_PASSWORD` environment variable.',
29
+ },
30
+ { status: 403 },
31
+ );
32
+ }
33
+
34
+ const supabase = $provideSupabaseForServer();
35
+ const { data: userData, error: fetchError } = await supabase
36
+ .from(await $getTableName('User'))
37
+ .select('*')
38
+ .eq('username', user.username)
39
+ .single();
40
+
41
+ if (fetchError || !userData) {
42
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
43
+ }
44
+
45
+ const userRow = userData as AgentsServerDatabase['public']['Tables']['User']['Row'];
46
+
47
+ // Verify current password
48
+ const isValid = await verifyPassword(currentPassword, userRow.passwordHash);
49
+ if (!isValid) {
50
+ return NextResponse.json({ error: 'Invalid current password' }, { status: 401 });
51
+ }
52
+
53
+ // Hash new password
54
+ const newPasswordHash = await hashPassword(newPassword);
55
+
56
+ // Update password
57
+ const { error: updateError } = await supabase
58
+ .from(await $getTableName('User'))
59
+ .update({
60
+ passwordHash: newPasswordHash,
61
+ updatedAt: new Date().toISOString(),
62
+ })
63
+ .eq('id', userRow.id);
64
+
65
+ if (updateError) {
66
+ console.error('Error updating password:', updateError);
67
+ return NextResponse.json({ error: 'Failed to update password' }, { status: 500 });
68
+ }
69
+
70
+ return NextResponse.json({ success: true });
71
+ } catch (error) {
72
+ console.error('Change password error:', error);
73
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
74
+ }
75
+ }
@@ -0,0 +1,55 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../../database/$getTableName';
3
+ import { $provideSupabase } from '../../../../database/$provideSupabase';
4
+ import { convertToCsv } from '../../../../utils/convertToCsv';
5
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
6
+
7
+ /**
8
+ * Export chat feedback as CSV.
9
+ *
10
+ * Query params:
11
+ * - agentName: filter by agent name (optional)
12
+ */
13
+ export async function GET(request: NextRequest) {
14
+ if (!(await isUserAdmin())) {
15
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
16
+ }
17
+
18
+ try {
19
+ const searchParams = request.nextUrl.searchParams;
20
+ const agentName = searchParams.get('agentName');
21
+
22
+ const supabase = $provideSupabase();
23
+ const table = await $getTableName('ChatFeedback');
24
+
25
+ let query = supabase.from(table).select('*');
26
+
27
+ if (agentName) {
28
+ query = query.eq('agentName', agentName);
29
+ }
30
+
31
+ query = query.order('createdAt', { ascending: false });
32
+
33
+ const { data, error } = await query;
34
+
35
+ if (error) {
36
+ console.error('Export chat feedback error:', error);
37
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
38
+ }
39
+
40
+ const csv = convertToCsv((data || []) as Record<string, unknown>[]);
41
+ const filename = agentName
42
+ ? `chat-feedback-${agentName}-${new Date().toISOString()}.csv`
43
+ : `chat-feedback-${new Date().toISOString()}.csv`;
44
+
45
+ return new NextResponse(csv, {
46
+ headers: {
47
+ 'Content-Type': 'text/csv',
48
+ 'Content-Disposition': `attachment; filename="${filename}"`,
49
+ },
50
+ });
51
+ } catch (error) {
52
+ console.error('Export chat feedback error:', error);
53
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
54
+ }
55
+ }
@@ -0,0 +1,55 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../../database/$getTableName';
3
+ import { $provideSupabase } from '../../../../database/$provideSupabase';
4
+ import { convertToCsv } from '../../../../utils/convertToCsv';
5
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
6
+
7
+ /**
8
+ * Export chat history as CSV.
9
+ *
10
+ * Query params:
11
+ * - agentName: filter by agent name (optional)
12
+ */
13
+ export async function GET(request: NextRequest) {
14
+ if (!(await isUserAdmin())) {
15
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
16
+ }
17
+
18
+ try {
19
+ const searchParams = request.nextUrl.searchParams;
20
+ const agentName = searchParams.get('agentName');
21
+
22
+ const supabase = $provideSupabase();
23
+ const table = await $getTableName('ChatHistory');
24
+
25
+ let query = supabase.from(table).select('*');
26
+
27
+ if (agentName) {
28
+ query = query.eq('agentName', agentName);
29
+ }
30
+
31
+ query = query.order('createdAt', { ascending: false });
32
+
33
+ const { data, error } = await query;
34
+
35
+ if (error) {
36
+ console.error('Export chat history error:', error);
37
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
38
+ }
39
+
40
+ const csv = convertToCsv((data || []) as Record<string, unknown>[]);
41
+ const filename = agentName
42
+ ? `chat-history-${agentName}-${new Date().toISOString()}.csv`
43
+ : `chat-history-${new Date().toISOString()}.csv`;
44
+
45
+ return new NextResponse(csv, {
46
+ headers: {
47
+ 'Content-Type': 'text/csv',
48
+ 'Content-Disposition': `attachment; filename="${filename}"`,
49
+ },
50
+ });
51
+ } catch (error) {
52
+ console.error('Export chat history error:', error);
53
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
54
+ }
55
+ }
@@ -30,6 +30,7 @@ export default async function DocPage(props: DocPageProps) {
30
30
  <div className="p-8 border-b border-gray-100 bg-gray-50/50">
31
31
  <div className="flex items-center gap-4 mb-4">
32
32
  <h1 className="text-4xl font-bold text-gray-900 tracking-tight">
33
+ <span className="mr-3">{primary.icon}</span>
33
34
  {primary.type}
34
35
  {aliases.length > 0 && (
35
36
  <span className="text-gray-400 font-normal ml-4 text-2xl">
@@ -14,6 +14,7 @@ export default function DocsPage() {
14
14
  <Link key={primary.type} href={`/docs/${primary.type}`} className="block h-full group">
15
15
  <Card className="h-full group-hover:border-blue-500 transition-colors">
16
16
  <h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
17
+ <span className="mr-2">{primary.icon}</span>
17
18
  {primary.type}
18
19
  {aliases.length > 0 && (
19
20
  <span className="text-gray-400 font-normal text-lg">
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { X } from 'lucide-react';
4
+ import { ChangePasswordForm } from '../ChangePasswordForm/ChangePasswordForm';
5
+ import { Portal } from '../Portal/Portal';
6
+
7
+ type ChangePasswordDialogProps = {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ };
11
+
12
+ export function ChangePasswordDialog(props: ChangePasswordDialogProps) {
13
+ const { isOpen, onClose } = props;
14
+
15
+ if (!isOpen) {
16
+ return null;
17
+ }
18
+
19
+ return (
20
+ <Portal>
21
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
22
+ <div className="relative w-full max-w-md bg-white rounded-lg shadow-lg border border-gray-200 p-6 animate-in zoom-in-95 duration-200">
23
+ <button
24
+ onClick={onClose}
25
+ className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 transition-colors"
26
+ >
27
+ <X className="w-5 h-5" />
28
+ <span className="sr-only">Close</span>
29
+ </button>
30
+
31
+ <div className="mb-6">
32
+ <h2 className="text-xl font-semibold text-gray-900">Change Password</h2>
33
+ <p className="text-sm text-gray-500 mt-1">Update your password to keep your account secure</p>
34
+ </div>
35
+
36
+ <ChangePasswordForm onSuccess={onClose} />
37
+ </div>
38
+ </div>
39
+ </Portal>
40
+ );
41
+ }
@@ -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
+ }
@@ -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,19 +217,30 @@ export function Header(props: HeaderProps) {
210
217
  ],
211
218
  },
212
219
  {
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',
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
+ ],
226
244
  },
227
245
  {
228
246
  type: 'link' as const,
@@ -236,6 +254,7 @@ export function Header(props: HeaderProps) {
236
254
  return (
237
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">
238
256
  <LoginDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
257
+ <ChangePasswordDialog isOpen={isChangePasswordOpen} onClose={() => setIsChangePasswordOpen(false)} />
239
258
  <div className="container mx-auto px-4 h-full">
240
259
  <div className="flex items-center justify-between h-full">
241
260
  {/* Logo */}
@@ -365,24 +384,56 @@ export function Header(props: HeaderProps) {
365
384
 
366
385
  {(currentUser || isAdmin) && (
367
386
  <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>
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>
374
435
  )}
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>
436
+ </div>
386
437
  </div>
387
438
  )}
388
439
 
@@ -398,7 +449,7 @@ export function Header(props: HeaderProps) {
398
449
  {/* Mobile Navigation */}
399
450
  {isMenuOpen && (
400
451
  <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"
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"
402
453
  style={{
403
454
  backdropFilter: 'blur(20px)',
404
455
  WebkitBackdropFilter: 'blur(20px)',
@@ -430,6 +481,16 @@ export function Header(props: HeaderProps) {
430
481
  </span>
431
482
  )}
432
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>
433
494
  <button
434
495
  onClick={() => {
435
496
  handleLogout();
@@ -31,6 +31,7 @@ export function LayoutWrapper({
31
31
  const isAdminChatPage =
32
32
  pathname?.startsWith('/admin/chat-history') || pathname?.startsWith('/admin/chat-feedback');
33
33
  const isHeaderHidden = pathname?.includes('/chat') && !isAdminChatPage;
34
+ const isFooterHiddenOnPage = pathname ? /^\/agents\/[^/]+\/book(\+chat)?$/.test(pathname) : false;
34
35
 
35
36
  if (isHeaderHidden) {
36
37
  return <main className={`pt-0`}>{children}</main>;
@@ -46,7 +47,7 @@ export function LayoutWrapper({
46
47
  agents={agents}
47
48
  />
48
49
  <main className={`pt-[60px]`}>{children}</main>
49
- {isFooterShown && <Footer extraLinks={footerLinks} />}
50
+ {isFooterShown && !isFooterHiddenOnPage && <Footer extraLinks={footerLinks} />}
50
51
  </>
51
52
  );
52
53
  }
@@ -0,0 +1,12 @@
1
+ CREATE TABLE IF NOT EXISTS "prefix_LlmCache" (
2
+ "id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
3
+ "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
4
+ "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
5
+
6
+ "hash" TEXT NOT NULL,
7
+ "value" JSONB NOT NULL
8
+ );
9
+
10
+ CREATE UNIQUE INDEX IF NOT EXISTS "prefix_LlmCache_hash_idx" ON "prefix_LlmCache" ("hash");
11
+
12
+ ALTER TABLE "prefix_LlmCache" ENABLE ROW LEVEL SECURITY;
@@ -0,0 +1,13 @@
1
+ CREATE TABLE IF NOT EXISTS "prefix_ApiTokens" (
2
+ "id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
3
+ "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
4
+ "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
5
+
6
+ "token" TEXT NOT NULL,
7
+ "note" TEXT,
8
+ "isRevoked" BOOLEAN NOT NULL DEFAULT FALSE
9
+ );
10
+
11
+ CREATE UNIQUE INDEX IF NOT EXISTS "prefix_ApiTokens_token_idx" ON "prefix_ApiTokens" ("token");
12
+
13
+ ALTER TABLE "prefix_ApiTokens" ENABLE ROW LEVEL SECURITY;