@promptbook/cli 0.103.0-52 → 0.103.0-54

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 (142) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/config.ts +3 -3
  3. package/apps/agents-server/next.config.ts +1 -1
  4. package/apps/agents-server/public/sw.js +16 -0
  5. package/apps/agents-server/src/app/AddAgentButton.tsx +24 -4
  6. package/apps/agents-server/src/app/actions.ts +15 -13
  7. package/apps/agents-server/src/app/admin/api-tokens/ApiTokensClient.tsx +186 -0
  8. package/apps/agents-server/src/app/admin/api-tokens/page.tsx +13 -0
  9. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
  10. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  11. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  12. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  13. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  14. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  15. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  16. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  17. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +10 -2
  19. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +176 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  28. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  31. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +11 -11
  32. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  38. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  39. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  40. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +64 -24
  41. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  42. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  43. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  44. package/apps/agents-server/src/app/api/api-tokens/route.ts +76 -0
  45. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  46. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  47. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  48. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  49. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  50. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  51. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  52. package/apps/agents-server/src/app/docs/[docId]/page.tsx +63 -0
  53. package/apps/agents-server/src/app/docs/page.tsx +34 -0
  54. package/apps/agents-server/src/app/layout.tsx +29 -3
  55. package/apps/agents-server/src/app/manifest.ts +109 -0
  56. package/apps/agents-server/src/app/page.tsx +8 -45
  57. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  58. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  59. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  60. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  61. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  62. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  63. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  64. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  65. package/apps/agents-server/src/components/Header/Header.tsx +450 -79
  66. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  67. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  68. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  69. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  70. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  71. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  72. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +29 -3
  73. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  74. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  75. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  76. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  77. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  78. package/apps/agents-server/src/database/migrations/2025-12-0010-llm-cache.sql +12 -0
  79. package/apps/agents-server/src/database/migrations/2025-12-0060-api-tokens.sql +13 -0
  80. package/apps/agents-server/src/database/schema.ts +51 -0
  81. package/apps/agents-server/src/middleware.ts +193 -92
  82. package/apps/agents-server/src/tools/$provideCdnForServer.ts +3 -7
  83. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +10 -1
  84. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  85. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  86. package/apps/agents-server/src/utils/cache/SupabaseCacheStorage.ts +55 -0
  87. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +63 -0
  88. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  89. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  90. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  91. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  92. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  93. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  94. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  95. package/apps/agents-server/vercel.json +7 -0
  96. package/esm/index.es.js +279 -2
  97. package/esm/index.es.js.map +1 -1
  98. package/esm/typings/servers.d.ts +8 -1
  99. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  100. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  101. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  102. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  103. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  104. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +4 -0
  105. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  106. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  107. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  108. package/esm/typings/src/commitments/ACTION/ACTION.d.ts +4 -0
  109. package/esm/typings/src/commitments/DELETE/DELETE.d.ts +4 -0
  110. package/esm/typings/src/commitments/FORMAT/FORMAT.d.ts +4 -0
  111. package/esm/typings/src/commitments/GOAL/GOAL.d.ts +4 -0
  112. package/esm/typings/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
  113. package/esm/typings/src/commitments/MEMORY/MEMORY.d.ts +4 -0
  114. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +32 -0
  115. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +4 -0
  116. package/esm/typings/src/commitments/MESSAGE/MESSAGE.d.ts +4 -0
  117. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +32 -0
  118. package/esm/typings/src/commitments/META/META.d.ts +4 -0
  119. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +4 -0
  120. package/esm/typings/src/commitments/META_IMAGE/META_IMAGE.d.ts +4 -0
  121. package/esm/typings/src/commitments/META_LINK/META_LINK.d.ts +4 -0
  122. package/esm/typings/src/commitments/MODEL/MODEL.d.ts +4 -0
  123. package/esm/typings/src/commitments/NOTE/NOTE.d.ts +4 -0
  124. package/esm/typings/src/commitments/PERSONA/PERSONA.d.ts +4 -0
  125. package/esm/typings/src/commitments/RULE/RULE.d.ts +4 -0
  126. package/esm/typings/src/commitments/SAMPLE/SAMPLE.d.ts +4 -0
  127. package/esm/typings/src/commitments/SCENARIO/SCENARIO.d.ts +4 -0
  128. package/esm/typings/src/commitments/STYLE/STYLE.d.ts +4 -0
  129. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +5 -0
  130. package/esm/typings/src/commitments/_base/CommitmentDefinition.d.ts +5 -0
  131. package/esm/typings/src/commitments/_base/NotYetImplementedCommitmentDefinition.d.ts +4 -0
  132. package/esm/typings/src/commitments/index.d.ts +20 -1
  133. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  134. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  135. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  136. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  137. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  138. package/esm/typings/src/version.d.ts +1 -1
  139. package/package.json +2 -2
  140. package/umd/index.umd.js +279 -2
  141. package/umd/index.umd.js.map +1 -1
  142. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +0 -119
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
4
- import { metadataDefaults } from '../../../database/metadataDefaults';
3
+ import { FileText, Hash, Image, Shield, ToggleLeft, Type, Upload } from 'lucide-react';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { metadataDefaults, MetadataType } from '../../../database/metadataDefaults';
5
6
 
6
7
  type MetadataEntry = {
7
8
  id: number;
@@ -11,17 +12,53 @@ type MetadataEntry = {
11
12
  createdAt: string;
12
13
  updatedAt: string;
13
14
  isDefault?: boolean;
15
+ type?: MetadataType;
14
16
  };
15
17
 
18
+ function mergeMetadataWithDefaults(data: MetadataEntry[]): MetadataEntry[] {
19
+ const byKey = new Map<string, MetadataEntry>();
20
+
21
+ // First prefer existing (non-default) metadata coming from the database
22
+ for (const entry of data) {
23
+ const existing = byKey.get(entry.key);
24
+ if (!existing || existing.isDefault) {
25
+ // Find type from defaults
26
+ const def = metadataDefaults.find((d) => d.key === entry.key);
27
+ byKey.set(entry.key, { ...entry, type: def?.type });
28
+ }
29
+ }
30
+
31
+ // Then add defaults only for keys that are missing
32
+ for (const def of metadataDefaults) {
33
+ if (!byKey.has(def.key)) {
34
+ byKey.set(def.key, {
35
+ id: -1,
36
+ key: def.key,
37
+ value: def.value,
38
+ note: def.note,
39
+ createdAt: new Date().toISOString(),
40
+ updatedAt: new Date().toISOString(),
41
+ isDefault: true,
42
+ type: def.type,
43
+ });
44
+ }
45
+ }
46
+
47
+ return Array.from(byKey.values()).sort((a, b) => a.key.localeCompare(b.key));
48
+ }
49
+
16
50
  export function MetadataClient() {
17
51
  const [metadata, setMetadata] = useState<MetadataEntry[]>([]);
18
52
  const [loading, setLoading] = useState(true);
19
53
  const [error, setError] = useState<string | null>(null);
20
54
  const [editingId, setEditingId] = useState<number | null>(null);
55
+ const [isUploading, setIsUploading] = useState(false);
56
+ const fileInputRef = useRef<HTMLInputElement>(null);
21
57
  const [formState, setFormState] = useState<{
22
58
  key: string;
23
59
  value: string;
24
60
  note: string;
61
+ type?: MetadataType;
25
62
  }>({ key: '', value: '', note: '' });
26
63
 
27
64
  const fetchMetadata = async () => {
@@ -33,23 +70,7 @@ export function MetadataClient() {
33
70
  }
34
71
  const data: MetadataEntry[] = await response.json();
35
72
 
36
- // Merge defaults
37
- const mergedData = [...data];
38
- for (const def of metadataDefaults) {
39
- if (!mergedData.find((m) => m.key === def.key)) {
40
- mergedData.push({
41
- id: -1,
42
- key: def.key,
43
- value: def.value,
44
- note: def.note,
45
- createdAt: new Date().toISOString(),
46
- updatedAt: new Date().toISOString(),
47
- isDefault: true,
48
- });
49
- }
50
- }
51
- // Sort by key
52
- mergedData.sort((a, b) => a.key.localeCompare(b.key));
73
+ const mergedData = mergeMetadataWithDefaults(data);
53
74
 
54
75
  setMetadata(mergedData);
55
76
  } catch (err) {
@@ -96,6 +117,7 @@ export function MetadataClient() {
96
117
  key: entry.key,
97
118
  value: entry.value,
98
119
  note: entry.note || '',
120
+ type: entry.type,
99
121
  });
100
122
  };
101
123
 
@@ -123,6 +145,77 @@ export function MetadataClient() {
123
145
  setFormState({ key: '', value: '', note: '' });
124
146
  };
125
147
 
148
+ const getTypeIcon = (type?: MetadataType) => {
149
+ switch (type) {
150
+ case 'TEXT_SINGLE_LINE':
151
+ return <Type className="w-4 h-4" />;
152
+ case 'TEXT':
153
+ return <FileText className="w-4 h-4" />;
154
+ case 'NUMBER':
155
+ return <Hash className="w-4 h-4" />;
156
+ case 'BOOLEAN':
157
+ return <ToggleLeft className="w-4 h-4" />;
158
+ case 'IMAGE_URL':
159
+ return <Image className="w-4 h-4" />;
160
+ case 'IP_RANGE':
161
+ return <Shield className="w-4 h-4" />;
162
+ default:
163
+ return <Type className="w-4 h-4" />;
164
+ }
165
+ };
166
+
167
+ const validateIpOrCidr = (ip: string) => {
168
+ const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
169
+ const cidrV4Regex =
170
+ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(?:3[0-2]|[12]?[0-9])$/;
171
+ // Simple IPv6 check (allows :: abbreviation)
172
+ const ipv6Regex =
173
+ /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
174
+ const cidrV6Regex = /^([0-9a-fA-F:.]{2,})\/(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9])$/;
175
+
176
+ if (ip.includes('/')) {
177
+ return cidrV4Regex.test(ip) || cidrV6Regex.test(ip);
178
+ }
179
+ return ipv4Regex.test(ip) || ipv6Regex.test(ip);
180
+ };
181
+
182
+ const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
183
+ const file = e.target.files?.[0];
184
+ if (!file) return;
185
+
186
+ try {
187
+ setIsUploading(true);
188
+ const formData = new FormData();
189
+ formData.append('file', file);
190
+
191
+ const response = await fetch('/api/upload', {
192
+ method: 'POST',
193
+ body: formData,
194
+ });
195
+
196
+ if (!response.ok) {
197
+ throw new Error(`Failed to upload file: ${response.statusText}`);
198
+ }
199
+
200
+ const { fileUrl: longFileUrl } = await response.json();
201
+
202
+ const LONG_URL = `${process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!}/${process.env
203
+ .NEXT_PUBLIC_CDN_PATH_PREFIX!}/user/files/`;
204
+ const SHORT_URL = `https://ptbk.io/k/`;
205
+ // <- TODO: [🌍] Unite this logic in one place
206
+
207
+ const shortFileUrl = longFileUrl.split(LONG_URL).join(SHORT_URL);
208
+ setFormState((prev) => ({ ...prev, value: shortFileUrl }));
209
+ } catch (err) {
210
+ setError(err instanceof Error ? err.message : 'Failed to upload image');
211
+ } finally {
212
+ setIsUploading(false);
213
+ if (fileInputRef.current) {
214
+ fileInputRef.current.value = '';
215
+ }
216
+ }
217
+ };
218
+
126
219
  if (loading && metadata.length === 0) {
127
220
  return <div className="p-8 text-center">Loading metadata...</div>;
128
221
  }
@@ -158,14 +251,127 @@ export function MetadataClient() {
158
251
  <label htmlFor="value" className="block text-sm font-medium text-gray-700 mb-1">
159
252
  Value
160
253
  </label>
161
- <textarea
162
- id="value"
163
- value={formState.value}
164
- onChange={(e) => setFormState({ ...formState, value: e.target.value })}
165
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[100px]"
166
- required
167
- placeholder="Metadata value..."
168
- />
254
+ {formState.type === 'TEXT_SINGLE_LINE' ? (
255
+ <input
256
+ type="text"
257
+ id="value"
258
+ value={formState.value}
259
+ onChange={(e) => setFormState({ ...formState, value: e.target.value })}
260
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
261
+ required
262
+ placeholder="Metadata value..."
263
+ />
264
+ ) : formState.type === 'IMAGE_URL' ? (
265
+ <div className="space-y-2">
266
+ <div className="flex space-x-2">
267
+ <input
268
+ type="text"
269
+ id="value"
270
+ value={formState.value}
271
+ onChange={(e) => setFormState({ ...formState, value: e.target.value })}
272
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
273
+ placeholder="Image URL..."
274
+ />
275
+ <input
276
+ type="file"
277
+ accept="image/*"
278
+ className="hidden"
279
+ ref={fileInputRef}
280
+ onChange={handleFileUpload}
281
+ />
282
+ <button
283
+ type="button"
284
+ onClick={() => fileInputRef.current?.click()}
285
+ disabled={isUploading}
286
+ className="bg-gray-100 text-gray-700 px-3 py-2 rounded-md border border-gray-300 hover:bg-gray-200 flex items-center space-x-2 min-w-max"
287
+ >
288
+ <Upload className="w-4 h-4" />
289
+ <span>{isUploading ? 'Uploading...' : 'Upload Image'}</span>
290
+ </button>
291
+ </div>
292
+ {formState.value && (
293
+ <div className="mt-2 p-2 border border-gray-200 rounded-md bg-gray-50 inline-block">
294
+ {/* eslint-disable-next-line @next/next/no-img-element */}
295
+ <img
296
+ src={formState.value}
297
+ alt="Preview"
298
+ className="max-w-full h-auto max-h-[200px] object-contain"
299
+ onError={(e) => {
300
+ (e.target as HTMLImageElement).style.display = 'none';
301
+ }}
302
+ />
303
+ </div>
304
+ )}
305
+ </div>
306
+ ) : formState.type === 'BOOLEAN' ? (
307
+ <select
308
+ id="value"
309
+ value={formState.value}
310
+ onChange={(e) => setFormState({ ...formState, value: e.target.value })}
311
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
312
+ >
313
+ <option value="true">True</option>
314
+ <option value="false">False</option>
315
+ </select>
316
+ ) : formState.type === 'NUMBER' ? (
317
+ <input
318
+ type="number"
319
+ id="value"
320
+ value={formState.value}
321
+ onChange={(e) => setFormState({ ...formState, value: e.target.value })}
322
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
323
+ required
324
+ placeholder="Metadata value..."
325
+ />
326
+ ) : formState.type === 'IP_RANGE' ? (
327
+ <div className="space-y-2">
328
+ <textarea
329
+ id="value"
330
+ value={formState.value.split(',').join('\n')}
331
+ onChange={(e) => {
332
+ const newValue = e.target.value
333
+ .split('\n')
334
+ .map((line) => line.trim())
335
+ .filter((line) => line !== '')
336
+ .join(',');
337
+ setFormState({ ...formState, value: newValue });
338
+ }}
339
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[100px] font-mono"
340
+ placeholder="e.g. 192.168.1.1&#10;10.0.0.0/24"
341
+ />
342
+ <div className="flex flex-wrap gap-2">
343
+ {formState.value
344
+ .split(',')
345
+ .filter((ip) => ip.trim() !== '')
346
+ .map((ip, i) => {
347
+ const isValid = validateIpOrCidr(ip.trim());
348
+ return (
349
+ <span
350
+ key={i}
351
+ className={`px-2 py-1 rounded text-xs font-mono border ${
352
+ isValid
353
+ ? 'bg-green-100 text-green-800 border-green-200'
354
+ : 'bg-red-100 text-red-800 border-red-200'
355
+ }`}
356
+ >
357
+ {ip}
358
+ {!isValid && ' (Invalid)'}
359
+ </span>
360
+ );
361
+ })}
362
+ </div>
363
+ <p className="text-xs text-gray-500">Enter each IP or CIDR range on a new line.</p>
364
+ </div>
365
+ ) : (
366
+ <textarea
367
+ id="value"
368
+ value={formState.value}
369
+ onChange={(e) => setFormState({ ...formState, value: e.target.value })}
370
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[100px]"
371
+ required
372
+ placeholder="Metadata value..."
373
+ />
374
+ )}
169
375
  </div>
170
376
  <div>
171
377
  <label htmlFor="note" className="block text-sm font-medium text-gray-700 mb-1">
@@ -204,6 +410,9 @@ export function MetadataClient() {
204
410
  <table className="min-w-full divide-y divide-gray-200">
205
411
  <thead className="bg-gray-50">
206
412
  <tr>
413
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
414
+ Type
415
+ </th>
207
416
  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
208
417
  Key
209
418
  </th>
@@ -228,6 +437,11 @@ export function MetadataClient() {
228
437
  ) : (
229
438
  metadata.map((entry) => (
230
439
  <tr key={entry.id}>
440
+ <td className="px-6 py-4 whitespace-nowrap text-gray-500">
441
+ <div className="flex items-center" title={entry.type || 'Unknown'}>
442
+ {getTypeIcon(entry.type)}
443
+ </div>
444
+ </td>
231
445
  <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
232
446
  {entry.key}
233
447
  </td>
@@ -0,0 +1,22 @@
1
+ import { getSingleLlmExecutionTools } from '@promptbook-local/core';
2
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
3
+ import { ModelsSection } from '../../../components/Homepage/ModelsSection';
4
+ import { $provideExecutionToolsForServer } from '../../../tools/$provideExecutionToolsForServer';
5
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
6
+
7
+ export default async function AdminModelsPage() {
8
+ const isAdmin = await isUserAdmin();
9
+
10
+ if (!isAdmin) {
11
+ return <ForbiddenPage />;
12
+ }
13
+
14
+ const executionTools = await $provideExecutionToolsForServer();
15
+ const models = await getSingleLlmExecutionTools(executionTools.llm).listModels();
16
+
17
+ return (
18
+ <div className="container mx-auto px-4 py-8">
19
+ <ModelsSection models={models} />
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useMemo } from 'react';
6
+ import { Card } from '../../../../components/Homepage/Card';
7
+ import { Section } from '../../../../components/Homepage/Section';
8
+ import { useUsersAdmin } from '../../../../components/UsersList/useUsersAdmin';
9
+
10
+ type UserDetailClientProps = {
11
+ /**
12
+ * User identifier from the URL.
13
+ *
14
+ * In practice this is the username (see Header links), but we also
15
+ * gracefully handle the case where a numeric ID is used.
16
+ */
17
+ userId: string;
18
+ };
19
+
20
+ export function UserDetailClient({ userId }: UserDetailClientProps) {
21
+ const router = useRouter();
22
+ const { users, loading, error, deleteUser, toggleAdmin } = useUsersAdmin();
23
+
24
+ const user = useMemo(
25
+ () => users.find((u) => u.username === userId || String(u.id) === userId) ?? null,
26
+ [users, userId],
27
+ );
28
+
29
+ const handleDelete = async () => {
30
+ if (!user) return;
31
+
32
+ await deleteUser(user.username);
33
+ router.push('/admin/users');
34
+ };
35
+
36
+ const handleToggleAdmin = async () => {
37
+ if (!user) return;
38
+
39
+ await toggleAdmin(user.username, user.isAdmin);
40
+ };
41
+
42
+ if (loading && !user) {
43
+ return <div className="container mx-auto px-4 py-8">Loading user...</div>;
44
+ }
45
+
46
+ if (error && !user) {
47
+ return (
48
+ <div className="container mx-auto px-4 py-8">
49
+ <div className="bg-red-100 text-red-700 p-3 rounded">{error}</div>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ if (!user) {
55
+ return (
56
+ <div className="container mx-auto px-4 py-8">
57
+ <p className="text-gray-600">User not found.</p>
58
+ <Link href="/admin/users" className="mt-4 inline-block text-blue-600 hover:text-blue-800 text-sm">
59
+ &larr; Back to users
60
+ </Link>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div className="container mx-auto px-4 py-8 space-y-6">
67
+ <Link href="/admin/users" className="text-sm text-blue-600 hover:text-blue-800">
68
+ &larr; Back to users
69
+ </Link>
70
+
71
+ <Section title={`User profile: ${user.username}`}>
72
+ <Card>
73
+ <div className="flex justify-between items-start">
74
+ <div>
75
+ <h2 className="text-2xl font-semibold text-gray-900">{user.username}</h2>
76
+ {user.isAdmin && (
77
+ <span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded mt-1">
78
+ Admin
79
+ </span>
80
+ )}
81
+ <p className="text-gray-500 text-sm mt-2">ID: {user.id}</p>
82
+ <p className="text-gray-500 text-sm mt-1">
83
+ Created:{' '}
84
+ {user.createdAt
85
+ ? new Date(user.createdAt).toLocaleString()
86
+ : 'Unknown'}
87
+ </p>
88
+ <p className="text-gray-500 text-sm mt-1">
89
+ Last updated:{' '}
90
+ {user.updatedAt
91
+ ? new Date(user.updatedAt).toLocaleString()
92
+ : 'Unknown'}
93
+ </p>
94
+ </div>
95
+ <div className="space-x-2">
96
+ <button
97
+ onClick={handleToggleAdmin}
98
+ className="text-sm text-blue-600 hover:text-blue-800"
99
+ >
100
+ {user.isAdmin ? 'Remove admin' : 'Make admin'}
101
+ </button>
102
+ <button
103
+ onClick={handleDelete}
104
+ className="text-sm text-red-600 hover:text-red-800"
105
+ >
106
+ Delete user
107
+ </button>
108
+ </div>
109
+ </div>
110
+ </Card>
111
+
112
+ <Card>
113
+ <h3 className="text-lg font-semibold text-gray-900 mb-2">Created agents</h3>
114
+ <p className="text-gray-600 text-sm">
115
+ Listing agents created by users is not wired to the data model yet.
116
+ {/* TODO: [🧠] Once agents are linked to users, show their agents here. */}
117
+ </p>
118
+ </Card>
119
+
120
+ <Card>
121
+ <h3 className="text-lg font-semibold text-gray-900 mb-2">Activity</h3>
122
+ <p className="text-gray-600 text-sm">
123
+ Detailed activity tracking is not implemented yet. For now, you can use the
124
+ created/updated timestamps above as a basic signal of recent changes.
125
+ {/* TODO: [🧠] Implement user activity timeline once events are stored. */}
126
+ </p>
127
+ </Card>
128
+ </Section>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,21 @@
1
+ 'use server';
2
+
3
+ import { ForbiddenPage } from '../../../../components/ForbiddenPage/ForbiddenPage';
4
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
5
+ import { UserDetailClient } from './UserDetailClient';
6
+
7
+ export default async function UserDetailPage({
8
+ params,
9
+ }: {
10
+ params: Promise<{ userId: string }>;
11
+ }) {
12
+ const isAdmin = await isUserAdmin();
13
+
14
+ if (!isAdmin) {
15
+ return <ForbiddenPage />;
16
+ }
17
+
18
+ const { userId } = await params;
19
+
20
+ return <UserDetailClient userId={decodeURIComponent(userId)} />;
21
+ }
@@ -0,0 +1,18 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { UsersList } from '../../../components/UsersList/UsersList';
3
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
4
+
5
+ export default async function AdminUsersPage() {
6
+ const isAdmin = await isUserAdmin();
7
+
8
+ if (!isAdmin) {
9
+ return <ForbiddenPage />;
10
+ }
11
+
12
+ // Full users management (list + create) is only available on this page
13
+ return (
14
+ <div className="container mx-auto px-4 py-8">
15
+ <UsersList allowCreate />
16
+ </div>
17
+ );
18
+ }
@@ -8,12 +8,13 @@ import { string_agent_url } from '../../../../../../src/types/typeAliases';
8
8
 
9
9
  type AgentChatWrapperProps = {
10
10
  agentUrl: string_agent_url;
11
+ defaultMessage?: string;
11
12
  };
12
13
 
13
14
  // TODO: [🐱‍🚀] Rename to AgentChatSomethingWrapper
14
15
 
15
16
  export function AgentChatWrapper(props: AgentChatWrapperProps) {
16
- const { agentUrl } = props;
17
+ const { agentUrl, defaultMessage } = props;
17
18
 
18
19
  const agentPromise = useMemo(
19
20
  () =>
@@ -60,7 +61,14 @@ export function AgentChatWrapper(props: AgentChatWrapperProps) {
60
61
  return <>{/* <- TODO: [🐱‍🚀] <PromptbookLoading /> */}</>;
61
62
  }
62
63
 
63
- return <AgentChat className={`w-full h-full`} agent={agent} onFeedback={handleFeedback} />;
64
+ return (
65
+ <AgentChat
66
+ className={`w-full h-full`}
67
+ agent={agent}
68
+ onFeedback={handleFeedback}
69
+ defaultMessage={defaultMessage}
70
+ />
71
+ );
64
72
  }
65
73
 
66
74
  /**
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { $clearAgentChatFeedback } from '../../../utils/chatFeedbackAdmin';
5
+
6
+ type ClearAgentChatFeedbackButtonProps = {
7
+ /**
8
+ * Agent name for which the chat feedback should be cleared.
9
+ */
10
+ agentName: string;
11
+
12
+ /**
13
+ * Optional callback invoked after successful clearing.
14
+ */
15
+ onCleared?: () => void;
16
+ };
17
+
18
+ /**
19
+ * Admin-only button to clear chat feedback for a specific agent.
20
+ *
21
+ * This is intentionally small and self-contained so it can be reused
22
+ * from different admin-oriented surfaces without duplicating logic.
23
+ */
24
+ export function ClearAgentChatFeedbackButton({ agentName, onCleared }: ClearAgentChatFeedbackButtonProps) {
25
+ const [loading, setLoading] = useState(false);
26
+ const [error, setError] = useState<string | null>(null);
27
+
28
+ const handleClick = async () => {
29
+ if (!agentName) return;
30
+
31
+ const confirmed = window.confirm(
32
+ `Are you sure you want to permanently delete all feedback for agent "${agentName}"?`,
33
+ );
34
+ if (!confirmed) return;
35
+
36
+ try {
37
+ setLoading(true);
38
+ setError(null);
39
+ await $clearAgentChatFeedback(agentName);
40
+ if (onCleared) {
41
+ onCleared();
42
+ }
43
+ } catch (err) {
44
+ setError(err instanceof Error ? err.message : 'Failed to clear chat feedback');
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <div className="flex flex-col gap-2">
52
+ <button
53
+ type="button"
54
+ onClick={handleClick}
55
+ disabled={loading}
56
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-red-300 bg-white px-3 py-1.5 text-xs font-semibold text-red-700 shadow-sm hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-60"
57
+ >
58
+ {loading ? 'Clearing feedback…' : 'Clear chat feedback'}
59
+ </button>
60
+ {error && <div className="text-xs text-red-600">{error}</div>}
61
+ </div>
62
+ );
63
+ }