@promptbook/cli 0.103.0-48 → 0.103.0-50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/TODO.txt +6 -5
  3. package/apps/agents-server/config.ts +130 -0
  4. package/apps/agents-server/next.config.ts +1 -1
  5. package/apps/agents-server/public/fonts/OpenMoji-black-glyf.woff2 +0 -0
  6. package/apps/agents-server/public/fonts/download-font.js +22 -0
  7. package/apps/agents-server/src/app/[agentName]/[...rest]/page.tsx +11 -0
  8. package/apps/agents-server/src/app/[agentName]/page.tsx +1 -0
  9. package/apps/agents-server/src/app/actions.ts +37 -2
  10. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +68 -0
  11. package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +55 -0
  12. package/apps/agents-server/src/app/agents/[agentName]/AgentUrlCopy.tsx +4 -5
  13. package/apps/agents-server/src/app/agents/[agentName]/CopyField.tsx +44 -0
  14. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +8 -8
  15. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +121 -25
  16. package/apps/agents-server/src/app/agents/[agentName]/api/feedback/route.ts +54 -0
  17. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +6 -6
  18. package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +3 -3
  19. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +29 -10
  20. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +4 -5
  21. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +9 -2
  22. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +23 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/book+chat/{AgentBookAndChatComponent.tsx → AgentBookAndChatComponent.tsx.todo} +4 -4
  24. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +28 -17
  25. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx.todo +21 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatWrapper.tsx +34 -4
  27. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +4 -1
  28. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +42 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +117 -106
  30. package/apps/agents-server/src/app/agents/page.tsx +1 -1
  31. package/apps/agents-server/src/app/api/agents/route.ts +34 -0
  32. package/apps/agents-server/src/app/api/auth/login/route.ts +65 -0
  33. package/apps/agents-server/src/app/api/auth/logout/route.ts +7 -0
  34. package/apps/agents-server/src/app/api/metadata/route.ts +116 -0
  35. package/apps/agents-server/src/app/api/upload/route.ts +7 -3
  36. package/apps/agents-server/src/app/api/users/[username]/route.ts +75 -0
  37. package/apps/agents-server/src/app/api/users/route.ts +71 -0
  38. package/apps/agents-server/src/app/globals.css +35 -1
  39. package/apps/agents-server/src/app/layout.tsx +43 -23
  40. package/apps/agents-server/src/app/metadata/MetadataClient.tsx +271 -0
  41. package/apps/agents-server/src/app/metadata/page.tsx +13 -0
  42. package/apps/agents-server/src/app/not-found.tsx +5 -0
  43. package/apps/agents-server/src/app/page.tsx +117 -46
  44. package/apps/agents-server/src/components/Auth/AuthControls.tsx +123 -0
  45. package/apps/agents-server/src/components/ErrorPage/ErrorPage.tsx +33 -0
  46. package/apps/agents-server/src/components/ForbiddenPage/ForbiddenPage.tsx +15 -0
  47. package/apps/agents-server/src/components/Header/Header.tsx +146 -0
  48. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +27 -0
  49. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +40 -0
  50. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +109 -0
  51. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +17 -0
  52. package/apps/agents-server/src/components/UsersList/UsersList.tsx +190 -0
  53. package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +60 -0
  54. package/apps/agents-server/src/database/$getTableName.ts +18 -0
  55. package/apps/agents-server/src/database/$provideSupabase.ts +2 -2
  56. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +3 -3
  57. package/apps/agents-server/src/database/getMetadata.ts +31 -0
  58. package/apps/agents-server/src/database/metadataDefaults.ts +37 -0
  59. package/apps/agents-server/src/database/schema.sql +81 -33
  60. package/apps/agents-server/src/database/schema.ts +35 -1
  61. package/apps/agents-server/src/middleware.ts +200 -0
  62. package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +11 -7
  63. package/apps/agents-server/src/tools/$provideCdnForServer.ts +1 -1
  64. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +11 -13
  65. package/apps/agents-server/src/tools/$provideOpenAiAssistantExecutionToolsForServer.ts +7 -7
  66. package/apps/agents-server/src/tools/$provideServer.ts +39 -0
  67. package/apps/agents-server/src/utils/auth.ts +33 -0
  68. package/apps/agents-server/src/utils/cdn/utils/nameToSubfolderPath.ts +1 -1
  69. package/apps/agents-server/src/utils/getCurrentUser.ts +32 -0
  70. package/apps/agents-server/src/utils/getFederatedAgents.ts +66 -0
  71. package/apps/agents-server/src/utils/isIpAllowed.ts +101 -0
  72. package/apps/agents-server/src/utils/isUserAdmin.ts +31 -0
  73. package/apps/agents-server/src/utils/session.ts +50 -0
  74. package/apps/agents-server/tailwind.config.ts +2 -0
  75. package/esm/index.es.js +147 -31
  76. package/esm/index.es.js.map +1 -1
  77. package/esm/typings/servers.d.ts +1 -0
  78. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  79. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  80. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  81. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +12 -2
  82. package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +20 -0
  83. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +14 -8
  84. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabaseOptions.d.ts +10 -0
  85. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +28 -0
  86. package/esm/typings/src/commitments/index.d.ts +2 -1
  87. package/esm/typings/src/config.d.ts +1 -0
  88. package/esm/typings/src/errors/DatabaseError.d.ts +2 -2
  89. package/esm/typings/src/errors/WrappedError.d.ts +2 -2
  90. package/esm/typings/src/execution/ExecutionTask.d.ts +2 -2
  91. package/esm/typings/src/execution/LlmExecutionTools.d.ts +6 -1
  92. package/esm/typings/src/llm-providers/_common/register/$provideLlmToolsForWizardOrCli.d.ts +2 -2
  93. package/esm/typings/src/llm-providers/agent/Agent.d.ts +19 -3
  94. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +13 -1
  95. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +11 -2
  96. package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +6 -1
  97. package/esm/typings/src/remote-server/startAgentServer.d.ts +2 -2
  98. package/esm/typings/src/utils/color/Color.d.ts +7 -0
  99. package/esm/typings/src/utils/color/Color.test.d.ts +1 -0
  100. package/esm/typings/src/utils/environment/$getGlobalScope.d.ts +2 -2
  101. package/esm/typings/src/utils/misc/computeHash.d.ts +11 -0
  102. package/esm/typings/src/utils/misc/computeHash.test.d.ts +1 -0
  103. package/esm/typings/src/utils/organization/$sideEffect.d.ts +2 -2
  104. package/esm/typings/src/utils/organization/$side_effect.d.ts +2 -2
  105. package/esm/typings/src/utils/organization/TODO_USE.d.ts +2 -2
  106. package/esm/typings/src/utils/organization/keepUnused.d.ts +2 -2
  107. package/esm/typings/src/utils/organization/preserve.d.ts +3 -3
  108. package/esm/typings/src/utils/organization/really_any.d.ts +7 -0
  109. package/esm/typings/src/utils/serialization/asSerializable.d.ts +2 -2
  110. package/esm/typings/src/version.d.ts +1 -1
  111. package/package.json +1 -1
  112. package/umd/index.umd.js +147 -31
  113. package/umd/index.umd.js.map +1 -1
  114. package/apps/agents-server/config.ts.todo +0 -38
@@ -1,17 +1,23 @@
1
1
  'use server';
2
2
 
3
- import logoImage from '@/public/logo-blue-white-256.png';
4
3
  import { getSingleLlmExecutionTools } from '@promptbook-local/core';
5
4
  import moment from 'moment';
6
5
  import { headers } from 'next/headers';
7
- import Image from 'next/image';
8
6
  import Link from 'next/link';
9
7
  import { AvatarProfile } from '../../../../src/book-components/AvatarProfile/AvatarProfile/AvatarProfile';
10
8
  import { AboutPromptbookInformation } from '../../../../src/utils/misc/xAboutPromptbookInformation';
11
9
  import { $sideEffect } from '../../../../src/utils/organization/$sideEffect';
10
+ import { AuthControls } from '../components/Auth/AuthControls';
11
+ import { UsersList } from '../components/UsersList/UsersList';
12
+ import VercelDeploymentCard from '../components/VercelDeploymentCard/VercelDeploymentCard';
13
+ import { getMetadata } from '../database/getMetadata';
12
14
  import { getLongRunningTask } from '../deamons/longRunningTask';
13
15
  import { $provideAgentCollectionForServer } from '../tools/$provideAgentCollectionForServer';
14
16
  import { $provideExecutionToolsForServer } from '../tools/$provideExecutionToolsForServer';
17
+ import { $provideServer } from '../tools/$provideServer';
18
+ import { getFederatedAgents } from '../utils/getFederatedAgents';
19
+ import { getCurrentUser } from '../utils/getCurrentUser';
20
+ import { isUserAdmin } from '../utils/isUserAdmin';
15
21
  import { AddAgentButton } from './AddAgentButton';
16
22
 
17
23
  // Add calendar formats that include seconds
@@ -27,27 +33,39 @@ const calendarWithSeconds = {
27
33
  export default async function HomePage() {
28
34
  $sideEffect(/* Note: [🐶] This will ensure dynamic rendering of page and avoid Next.js pre-render */ headers());
29
35
 
36
+ const isAdmin = await isUserAdmin(); /* <- TODO: [👹] Here should be user permissions */
37
+ const currentUser = await getCurrentUser();
38
+
30
39
  const collection = await $provideAgentCollectionForServer();
31
40
  const agents = await collection.listAgents();
32
41
 
42
+ const federatedServersString = (await getMetadata('FEDERATED_SERVERS')) || '';
43
+ const federatedServers = federatedServersString
44
+ .split(',')
45
+ .map((s) => s.trim())
46
+ .filter((s) => s !== '');
47
+
48
+ const externalAgents = await getFederatedAgents(federatedServers);
49
+
33
50
  const longRunningTask = getLongRunningTask();
34
51
 
35
52
  const executionTools = await $provideExecutionToolsForServer();
36
53
  const models = await getSingleLlmExecutionTools(executionTools.llm).listModels();
37
54
 
55
+ const host = (await headers()).get('host');
56
+
38
57
  return (
39
58
  <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
40
59
  <div className="container mx-auto px-4 py-16">
41
- <h1 className="text-4xl font-bold text-gray-900 mb-4">
42
- <Image src={logoImage} alt="Promptbook Logo" height={50} className="inline-block mr-4" />
43
- Promptbook Agents Server
44
- </h1>
60
+ <div className="flex justify-end mb-4">
61
+ <AuthControls initialUser={currentUser} />
62
+ </div>
45
63
 
46
64
  <>
47
- <h2 className="text-3xl text-gray-900 mt-16 mb-4">Agents ({agents.length})</h2>
65
+ <h2 className="text-3xl text-gray-900 mt-4 mb-4">Agents ({agents.length})</h2>
48
66
  <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
49
67
  {agents.map((agent) => (
50
- <Link key={agent.agentName} href={`/agents/${agent.agentName}`}>
68
+ <Link key={agent.agentName} href={`/${agent.agentName}`}>
51
69
  <AvatarProfile
52
70
  {...{ agent }}
53
71
  style={
@@ -61,50 +79,103 @@ export default async function HomePage() {
61
79
  />
62
80
  </Link>
63
81
  ))}
64
- <AddAgentButton />
82
+ {isAdmin && <AddAgentButton />}
65
83
  </div>
66
84
  </>
67
85
 
68
- <>
69
- <h2 className="text-3xl text-gray-900 mt-16 mb-4">Models ({models.length})</h2>
70
- <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
71
- {models.map(({ modelName, modelTitle, modelDescription }) => (
72
- <Link key={modelName} href={`#!!!`}>
73
- <div className="block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400">
74
- <h2 className="text-2xl font-semibold text-gray-900 mb-2">{modelTitle}</h2>
75
- <code>{modelName}</code>
76
- <p className="text-gray-600">{modelDescription}</p>
77
- </div>
86
+ {externalAgents.length > 0 && (
87
+ <>
88
+ <h2 className="text-3xl text-gray-900 mt-16 mb-4">External Agents ({externalAgents.length})</h2>
89
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
90
+ {externalAgents.map((agent) => (
91
+ <Link key={agent.url} href={agent.url}>
92
+ <AvatarProfile
93
+ {...{ agent }}
94
+ style={
95
+ !agent.meta.color
96
+ ? {}
97
+ : {
98
+ backgroundColor: `${agent.meta.color}22`, // <- TODO: Use Color object here
99
+ }
100
+ }
101
+ className="block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400"
102
+ />
103
+ </Link>
104
+ ))}
105
+ </div>
106
+ </>
107
+ )}
108
+
109
+ {isAdmin && <UsersList />}
110
+
111
+ {isAdmin && (
112
+ <>
113
+ <h2 className="text-3xl text-gray-900 mt-16 mb-4">Models ({models.length})</h2>
114
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
115
+ {models.map(({ modelName, modelTitle, modelDescription }) => (
116
+ <Link key={modelName} href={`#[🐱‍🚀]`}>
117
+ <div className="block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400">
118
+ <h2 className="text-2xl font-semibold text-gray-900 mb-2">{modelTitle}</h2>
119
+ <code>{modelName}</code>
120
+ <p className="text-gray-600">{modelDescription}</p>
121
+ </div>
122
+ </Link>
123
+ ))}
124
+ </div>
125
+ </>
126
+ )}
127
+
128
+ {isAdmin && (
129
+ <>
130
+ {/* Note: Shown in <AboutPromptbookInformation />: <h2 className="text-3xl text-gray-900 mt-16 mb-4">About Promptbook</h2> */}
131
+ <AboutPromptbookInformation />
132
+ </>
133
+ )}
134
+
135
+ {isAdmin && (
136
+ <>
137
+ <h2 className="text-3xl text-gray-900 mt-16 mb-4">Technical Information</h2>
138
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
139
+ <Link
140
+ href={'#'}
141
+ className="block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400"
142
+ >
143
+ <h2 className="text-2xl font-semibold text-gray-900 mb-2">
144
+ Long running task {longRunningTask.taskId}
145
+ </h2>
146
+ <p className="text-gray-600">Tick: {longRunningTask.tick}</p>
147
+ <p className="text-gray-600">
148
+ Created At:{' '}
149
+ {moment(longRunningTask.createdAt).calendar(undefined, calendarWithSeconds)}
150
+ </p>
151
+ <p className="text-gray-600">
152
+ Updated At:{' '}
153
+ {moment(longRunningTask.updatedAt).calendar(undefined, calendarWithSeconds)}
154
+ </p>
78
155
  </Link>
79
- ))}
80
- </div>
81
- </>
82
156
 
83
- <>
84
- <h2 className="text-3xl text-gray-900 mt-16 mb-4">About</h2>
85
- <AboutPromptbookInformation />
86
- </>
157
+ <VercelDeploymentCard />
87
158
 
88
- <>
89
- <h2 className="text-3xl text-gray-900 mt-16 mb-4">Technical Information</h2>
90
- <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
91
- <Link
92
- href={'#'}
93
- className="block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400"
94
- >
95
- <h2 className="text-2xl font-semibold text-gray-900 mb-2">
96
- Long running task {longRunningTask.taskId}
97
- </h2>
98
- <p className="text-gray-600">Tick: {longRunningTask.tick}</p>
99
- <p className="text-gray-600">
100
- Created At: {moment(longRunningTask.createdAt).calendar(undefined, calendarWithSeconds)}
101
- </p>
102
- <p className="text-gray-600">
103
- Updated At: {moment(longRunningTask.updatedAt).calendar(undefined, calendarWithSeconds)}
104
- </p>
105
- </Link>
106
- </div>
107
- </>
159
+ <Link
160
+ href={'#'}
161
+ className="block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400"
162
+ >
163
+ <h2 className="text-2xl font-semibold text-gray-900 mb-2">HTTP Information</h2>
164
+
165
+ <p className="text-gray-600">Host: {host}</p>
166
+ </Link>
167
+
168
+ <Link
169
+ href={'#'}
170
+ className="block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400"
171
+ >
172
+ <h2 className="text-2xl font-semibold text-gray-900 mb-2">Server</h2>
173
+
174
+ <pre>{JSON.stringify(await $provideServer(), null, 2)}</pre>
175
+ </Link>
176
+ </div>
177
+ </>
178
+ )}
108
179
  </div>
109
180
  </div>
110
181
  );
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ type AuthControlsProps = {
7
+ initialUser: { username: string; isAdmin: boolean } | null;
8
+ };
9
+
10
+ export function AuthControls({ initialUser }: AuthControlsProps) {
11
+ const router = useRouter();
12
+ const [user, setUser] = useState(initialUser);
13
+ const [isLoginOpen, setIsLoginOpen] = useState(false);
14
+ const [username, setUsername] = useState('');
15
+ const [password, setPassword] = useState('');
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ const handleLogin = async (e: React.FormEvent) => {
19
+ e.preventDefault();
20
+ setError(null);
21
+
22
+ try {
23
+ const response = await fetch('/api/auth/login', {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify({ username, password }),
27
+ });
28
+
29
+ if (!response.ok) {
30
+ const data = await response.json();
31
+ throw new Error(data.error || 'Login failed');
32
+ }
33
+
34
+ // Reload page to reflect state
35
+ window.location.reload();
36
+ } catch (err) {
37
+ setError(err instanceof Error ? err.message : 'An error occurred');
38
+ }
39
+ };
40
+
41
+ const handleLogout = async () => {
42
+ try {
43
+ await fetch('/api/auth/logout', { method: 'POST' });
44
+ window.location.reload();
45
+ } catch (err) {
46
+ console.error('Logout failed', err);
47
+ }
48
+ };
49
+
50
+ if (user) {
51
+ return (
52
+ <div className="flex items-center space-x-4">
53
+ <span className="text-gray-600">
54
+ Logged in as <strong>{user.username}</strong>
55
+ {user.isAdmin && <span className="ml-2 bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">Admin</span>}
56
+ </span>
57
+ <button
58
+ onClick={handleLogout}
59
+ className="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300 transition-colors"
60
+ >
61
+ Logout
62
+ </button>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ return (
68
+ <div>
69
+ {!isLoginOpen ? (
70
+ <button
71
+ onClick={() => setIsLoginOpen(true)}
72
+ className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors"
73
+ >
74
+ Login
75
+ </button>
76
+ ) : (
77
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
78
+ <div className="bg-white p-6 rounded-lg shadow-xl w-full max-w-md">
79
+ <h2 className="text-2xl font-bold mb-4">Login</h2>
80
+ {error && <div className="bg-red-100 text-red-700 p-3 rounded mb-4">{error}</div>}
81
+ <form onSubmit={handleLogin} className="space-y-4">
82
+ <div>
83
+ <label className="block text-gray-700 mb-1">Username</label>
84
+ <input
85
+ type="text"
86
+ value={username}
87
+ onChange={(e) => setUsername(e.target.value)}
88
+ className="w-full p-2 border border-gray-300 rounded"
89
+ required
90
+ />
91
+ </div>
92
+ <div>
93
+ <label className="block text-gray-700 mb-1">Password</label>
94
+ <input
95
+ type="password"
96
+ value={password}
97
+ onChange={(e) => setPassword(e.target.value)}
98
+ className="w-full p-2 border border-gray-300 rounded"
99
+ required
100
+ />
101
+ </div>
102
+ <div className="flex justify-end space-x-2">
103
+ <button
104
+ type="button"
105
+ onClick={() => setIsLoginOpen(false)}
106
+ className="px-4 py-2 text-gray-600 hover:text-gray-800"
107
+ >
108
+ Cancel
109
+ </button>
110
+ <button
111
+ type="submit"
112
+ className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
113
+ >
114
+ Login
115
+ </button>
116
+ </div>
117
+ </form>
118
+ </div>
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ type ErrorPageProps = {
4
+ /**
5
+ * The title of the error page (e.g. "404 Not Found")
6
+ */
7
+ title: string;
8
+
9
+ /**
10
+ * The message to display to the user
11
+ */
12
+ message: string;
13
+
14
+ /**
15
+ * Optional children to display below the message (e.g. a button or form)
16
+ */
17
+ children?: ReactNode;
18
+ };
19
+
20
+ /**
21
+ * A standard layout for error pages (404, 403, 500, etc.)
22
+ */
23
+ export function ErrorPage({ title, message, children }: ErrorPageProps) {
24
+ return (
25
+ <div className="min-h-screen flex items-center justify-center bg-gray-100">
26
+ <div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full">
27
+ <h1 className="text-3xl font-bold text-red-600 mb-4 text-center">{title}</h1>
28
+ <p className="text-gray-700 mb-6 text-center">{message}</p>
29
+ {children}
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import { useRouter } from 'next/navigation';
4
+ import { ErrorPage } from '../ErrorPage/ErrorPage';
5
+ import { LoginForm } from '../LoginForm/LoginForm';
6
+
7
+ export function ForbiddenPage() {
8
+ const router = useRouter();
9
+
10
+ return (
11
+ <ErrorPage title="403 Forbidden" message="You do not have permission to access this page.">
12
+ <LoginForm onSuccess={() => router.refresh()} />
13
+ </ErrorPage>
14
+ );
15
+ }
@@ -0,0 +1,146 @@
1
+ 'use client';
2
+
3
+ import promptbookLogoBlueTransparent from '@/public/logo-blue-white-256.png';
4
+ import { logoutAction } from '@/src/app/actions';
5
+ import { ArrowRight, LogIn, LogOut, Menu, X } from 'lucide-react';
6
+ import Image from 'next/image';
7
+ import Link from 'next/link';
8
+ import { useState } from 'react';
9
+ import { just } from '../../../../../src/utils/organization/just';
10
+ import { LoginDialog } from '../LoginDialog/LoginDialog';
11
+
12
+ type HeaderProps = {
13
+ /**
14
+ * Is the user an admin
15
+ */
16
+ isAdmin?: boolean;
17
+
18
+ /**
19
+ * The name of the server
20
+ */
21
+ serverName: string;
22
+
23
+ /**
24
+ * The URL of the logo displayed in the heading bar
25
+ */
26
+ serverLogoUrl: string | null;
27
+ };
28
+
29
+ /* TODO: [🐱‍🚀] Make this Agents server native */
30
+
31
+ export function Header(props: HeaderProps) {
32
+ const { isAdmin = false, serverName, serverLogoUrl } = props;
33
+
34
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
35
+ const [isLoginOpen, setIsLoginOpen] = useState(false);
36
+
37
+ const handleLogout = async () => {
38
+ await logoutAction();
39
+ };
40
+
41
+ return (
42
+ <header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200 h-[60px]">
43
+ <LoginDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
44
+ <div className="container mx-auto px-4">
45
+ <div className="flex items-center justify-between h-16">
46
+ {/* Logo <- TODO: This should be <h1>*/}
47
+ <Link href="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity">
48
+ <Image
49
+ src={serverLogoUrl || promptbookLogoBlueTransparent}
50
+ alt={serverName}
51
+ width={32}
52
+ height={32}
53
+ className="w-8 h-8 object-contain"
54
+ />
55
+ <span className="text-xl text-gray-900">{serverName}</span>
56
+ </Link>
57
+
58
+ {/* Desktop Navigation */}
59
+ {/* Desktop Navigation */}
60
+ <nav className="hidden md:flex items-center gap-8">
61
+ {isAdmin && (
62
+ <Link
63
+ href="/metadata"
64
+ className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
65
+ >
66
+ Metadata
67
+ </Link>
68
+ )}
69
+
70
+ {just(false /* TODO: [🧠] Figure out what to do with theese links */) && (
71
+ <Link
72
+ href="https://ptbk.io/#try-it-yourself"
73
+ target="_blank"
74
+ className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer"
75
+ >
76
+ Try it yourself
77
+ </Link>
78
+ )}
79
+ </nav>
80
+
81
+ {/* CTA Button & Mobile Menu Toggle */}
82
+ <div className="flex items-center gap-4">
83
+ {just(false /* TODO: [🧠] Figure out what to do with call to action */) && (
84
+ <Link href="https://ptbk.io/?modal=get-started" target="_blank" className="hidden md:block">
85
+ <button className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2 bg-promptbook-blue-dark text-white hover:bg-promptbook-blue-dark/90">
86
+ Get Started
87
+ <ArrowRight className="ml-2 w-4 h-4" />
88
+ </button>
89
+ </Link>
90
+ )}
91
+
92
+ {!isAdmin ? (
93
+ <button
94
+ onClick={() => {
95
+ setIsLoginOpen(true);
96
+ setIsMenuOpen(false);
97
+ }}
98
+ className="w-full inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
99
+ >
100
+ Log in
101
+ <LogIn className="ml-2 w-4 h-4" />
102
+ </button>
103
+ ) : (
104
+ <button
105
+ onClick={() => {
106
+ handleLogout();
107
+ setIsMenuOpen(false);
108
+ }}
109
+ className="w-full inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
110
+ >
111
+ Log out
112
+ <LogOut className="ml-2 w-4 h-4" />
113
+ </button>
114
+ )}
115
+
116
+ {/* Mobile Menu Toggle */}
117
+ {just(false /* TODO: [🧠] Figure out whether we want a menu */) && (
118
+ <button
119
+ className="md:hidden p-2 text-gray-600 hover:text-gray-900"
120
+ onClick={() => setIsMenuOpen(!isMenuOpen)}
121
+ >
122
+ {isMenuOpen ? <X size={24} /> : <Menu size={24} />}
123
+ </button>
124
+ )}
125
+ </div>
126
+ </div>
127
+
128
+ {/* Mobile Navigation */}
129
+ {just(false /* TODO: [🧠] Figure out whether we want a menu */) && isMenuOpen && (
130
+ <div className="md:hidden py-4 border-t border-gray-100 animate-in slide-in-from-top-2">
131
+ <nav className="flex flex-col gap-4">
132
+ <Link
133
+ href="https://ptbk.io/#try-it-yourself"
134
+ target="_blank"
135
+ className="text-gray-600 hover:text-gray-900 transition-colors py-2"
136
+ onClick={() => setIsMenuOpen(false)}
137
+ >
138
+ Try it yourself
139
+ </Link>
140
+ </nav>
141
+ </div>
142
+ )}
143
+ </div>
144
+ </header>
145
+ );
146
+ }
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import { Header } from '../Header/Header';
5
+
6
+ type LayoutWrapperProps = {
7
+ children: React.ReactNode;
8
+ isAdmin: boolean;
9
+ serverName: string;
10
+ serverLogoUrl: string | null;
11
+ };
12
+
13
+ export function LayoutWrapper({ children, isAdmin, serverName, serverLogoUrl }: LayoutWrapperProps) {
14
+ const pathname = usePathname();
15
+ const isHeaderHidden = pathname?.includes('/chat');
16
+
17
+ if (isHeaderHidden) {
18
+ return <main className={`pt-0`}>{children}</main>;
19
+ }
20
+
21
+ return (
22
+ <>
23
+ <Header isAdmin={isAdmin} serverName={serverName} serverLogoUrl={serverLogoUrl} />
24
+ <main className={`pt-[60px]`}>{children}</main>
25
+ </>
26
+ );
27
+ }
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { X } from 'lucide-react';
4
+ import { LoginForm } from '../LoginForm/LoginForm';
5
+
6
+ type LoginDialogProps = {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ };
10
+
11
+ export function LoginDialog(props: LoginDialogProps) {
12
+ const { isOpen, onClose } = props;
13
+
14
+ if (!isOpen) {
15
+ return null;
16
+ }
17
+
18
+ return (
19
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
20
+ <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">
21
+ <button
22
+ onClick={onClose}
23
+ className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 transition-colors"
24
+ >
25
+ <X className="w-5 h-5" />
26
+ <span className="sr-only">Close</span>
27
+ </button>
28
+
29
+ <div className="mb-6">
30
+ <h2 className="text-xl font-semibold text-gray-900">Log in</h2>
31
+ <p className="text-sm text-gray-500 mt-1">
32
+ Enter your credentials to access the admin area
33
+ </p>
34
+ </div>
35
+
36
+ <LoginForm onSuccess={onClose} />
37
+ </div>
38
+ </div>
39
+ );
40
+ }