@promptbook/cli 0.103.0-52 → 0.103.0-53
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.
- package/apps/agents-server/README.md +1 -1
- package/apps/agents-server/config.ts +3 -3
- package/apps/agents-server/next.config.ts +1 -1
- package/apps/agents-server/public/sw.js +16 -0
- package/apps/agents-server/src/app/AddAgentButton.tsx +24 -4
- package/apps/agents-server/src/app/actions.ts +15 -13
- package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
- package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
- package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
- package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
- package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
- package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
- package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
- package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
- package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
- package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
- package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
- package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
- package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
- package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
- package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
- package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +11 -11
- package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
- package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
- package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
- package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
- package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
- package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
- package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
- package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +41 -22
- package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
- package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
- package/apps/agents-server/src/app/api/agents/route.ts +22 -13
- package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
- package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
- package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
- package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
- package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
- package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
- package/apps/agents-server/src/app/api/upload/route.ts +9 -1
- package/apps/agents-server/src/app/docs/[docId]/page.tsx +62 -0
- package/apps/agents-server/src/app/docs/page.tsx +33 -0
- package/apps/agents-server/src/app/layout.tsx +29 -3
- package/apps/agents-server/src/app/manifest.ts +109 -0
- package/apps/agents-server/src/app/page.tsx +8 -45
- package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
- package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
- package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
- package/apps/agents-server/src/app/restricted/page.tsx +33 -0
- package/apps/agents-server/src/app/test/og-image/README.md +1 -0
- package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
- package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
- package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
- package/apps/agents-server/src/components/Header/Header.tsx +445 -79
- package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
- package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
- package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
- package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
- package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
- package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
- package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +28 -3
- package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
- package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
- package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
- package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
- package/apps/agents-server/src/middleware.ts +146 -93
- package/apps/agents-server/src/tools/$provideServer.ts +2 -2
- package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
- package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
- package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
- package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
- package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
- package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
- package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
- package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
- package/apps/agents-server/vercel.json +7 -0
- package/esm/index.es.js +153 -2
- package/esm/index.es.js.map +1 -1
- package/esm/typings/servers.d.ts +8 -1
- package/esm/typings/src/_packages/components.index.d.ts +2 -0
- package/esm/typings/src/_packages/core.index.d.ts +6 -0
- package/esm/typings/src/_packages/types.index.d.ts +2 -0
- package/esm/typings/src/_packages/utils.index.d.ts +2 -0
- package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
- package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +4 -0
- package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
- package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
- package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +28 -0
- package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +28 -0
- package/esm/typings/src/commitments/index.d.ts +20 -1
- package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
- package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
- package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
- package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
- package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +153 -2
- package/umd/index.umd.js.map +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.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
|
+
← 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
|
+
← 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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { $clearAgentChatHistory } from '../../../utils/chatHistoryAdmin';
|
|
5
|
+
|
|
6
|
+
type ClearAgentChatHistoryButtonProps = {
|
|
7
|
+
/**
|
|
8
|
+
* Agent name for which the chat history 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 history 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 ClearAgentChatHistoryButton({ agentName, onCleared }: ClearAgentChatHistoryButtonProps) {
|
|
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 chat history for agent "${agentName}"?`,
|
|
33
|
+
);
|
|
34
|
+
if (!confirmed) return;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
setLoading(true);
|
|
38
|
+
setError(null);
|
|
39
|
+
await $clearAgentChatHistory(agentName);
|
|
40
|
+
if (onCleared) {
|
|
41
|
+
onCleared();
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
setError(err instanceof Error ? err.message : 'Failed to clear chat history');
|
|
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 history…' : 'Clear chat history'}
|
|
59
|
+
</button>
|
|
60
|
+
{error && <div className="text-xs text-red-600">{error}</div>}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|