@promptbook/cli 0.103.0-51 → 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.
Files changed (114) 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/chat-feedback/ChatFeedbackClient.tsx +541 -0
  8. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  9. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  10. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  11. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  12. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  13. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  14. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  15. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  16. package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +3 -3
  17. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  19. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  28. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +12 -12
  29. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  32. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +46 -24
  38. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  39. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  40. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  41. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  42. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  43. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  44. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  45. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  46. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  47. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  48. package/apps/agents-server/src/app/docs/[docId]/page.tsx +62 -0
  49. package/apps/agents-server/src/app/docs/page.tsx +33 -0
  50. package/apps/agents-server/src/app/layout.tsx +29 -3
  51. package/apps/agents-server/src/app/manifest.ts +109 -0
  52. package/apps/agents-server/src/app/page.tsx +8 -45
  53. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  54. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  55. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  56. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  57. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  58. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  59. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  60. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  61. package/apps/agents-server/src/components/Header/Header.tsx +445 -79
  62. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  63. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  64. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  65. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  66. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  67. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  68. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +28 -3
  69. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  70. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  71. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  72. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  73. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  74. package/apps/agents-server/src/middleware.ts +146 -93
  75. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  76. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  77. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  78. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  79. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  80. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  81. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  82. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  83. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  84. package/apps/agents-server/vercel.json +7 -0
  85. package/esm/index.es.js +344 -11
  86. package/esm/index.es.js.map +1 -1
  87. package/esm/typings/servers.d.ts +8 -1
  88. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  89. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  90. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  91. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  92. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +1 -0
  93. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  94. package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +2 -2
  95. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +10 -0
  96. package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +6 -0
  97. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  98. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  99. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  100. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +28 -0
  101. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +28 -0
  102. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +38 -0
  103. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +2 -1
  104. package/esm/typings/src/commitments/index.d.ts +22 -1
  105. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  106. package/esm/typings/src/llm-providers/agent/Agent.d.ts +1 -0
  107. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  108. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  109. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  110. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  111. package/esm/typings/src/version.d.ts +1 -1
  112. package/package.json +1 -1
  113. package/umd/index.umd.js +344 -11
  114. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,47 @@
1
+ // POST /api/agents/[agentName]/clone
2
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
3
+ import { AgentBasicInformation } from '../../../../../../../../src/book-2.0/agent-source/AgentBasicInformation';
4
+ import { string_book } from '../../../../../../../../src/book-2.0/agent-source/string_book';
5
+ import { TODO_any } from '@promptbook-local/types';
6
+ import { NextResponse } from 'next/server';
7
+
8
+ export async function POST(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
9
+ const { agentName } = await params;
10
+ const collection = await $provideAgentCollectionForServer();
11
+
12
+ try {
13
+ const source = await collection.getAgentSource(agentName);
14
+
15
+ // Generate new name
16
+ // TODO: [🧠] Better naming strategy, maybe check for collisions
17
+ let newAgentName = `${agentName} (Copy)`;
18
+ let counter = 1;
19
+
20
+ // eslint-disable-next-line no-constant-condition
21
+ while (true) {
22
+ try {
23
+ await collection.getAgentSource(newAgentName);
24
+ // If success, it means it exists, so we try next one
25
+ counter++;
26
+ newAgentName = `${agentName} (Copy ${counter})`;
27
+ } catch (error) {
28
+ // If error, it likely means it does not exist (NotFoundError), so we can use it
29
+ // TODO: [🧠] Check if it is really NotFoundError
30
+ break;
31
+ }
32
+ }
33
+
34
+ const lines = source.split('\n');
35
+ lines[0] = newAgentName;
36
+ const newSource = lines.join('\n') as string_book;
37
+
38
+ const newAgent = await collection.createAgent(newSource);
39
+
40
+ return NextResponse.json(newAgent);
41
+ } catch (error) {
42
+ return NextResponse.json(
43
+ { success: false, error: (error as TODO_any)?.message || 'Failed to clone agent' },
44
+ { status: 500 },
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,19 @@
1
+ // DELETE /api/agents/[agentName]
2
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
3
+ import { TODO_any } from '@promptbook-local/types';
4
+ import { NextResponse } from 'next/server';
5
+
6
+ export async function DELETE(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
7
+ const { agentName } = await params;
8
+ const collection = await $provideAgentCollectionForServer();
9
+
10
+ try {
11
+ await collection.deleteAgent(agentName);
12
+ return NextResponse.json({ success: true });
13
+ } catch (error) {
14
+ return NextResponse.json(
15
+ { success: false, error: (error as TODO_any)?.message || 'Failed to delete agent' },
16
+ { status: 500 },
17
+ );
18
+ }
19
+ }
@@ -1,6 +1,7 @@
1
+ import { $provideServer } from '@/src/tools/$provideServer';
1
2
  import { NextResponse } from 'next/server';
2
- import { getMetadata } from '../../../database/getMetadata';
3
3
  import { $provideAgentCollectionForServer } from '../../../tools/$provideAgentCollectionForServer';
4
+ import { getFederatedServersFromMetadata } from '../../../utils/getFederatedServersFromMetadata';
4
5
 
5
6
  export const dynamic = 'force-dynamic';
6
7
 
@@ -8,27 +9,35 @@ export async function GET() {
8
9
  try {
9
10
  const collection = await $provideAgentCollectionForServer();
10
11
  const agents = await collection.listAgents();
11
- const serverUrl = (await getMetadata('SERVER_URL')) || '';
12
- const federatedServersString = (await getMetadata('FEDERATED_SERVERS')) || '';
13
- const federatedServers = federatedServersString
14
- .split(',')
15
- .map((s) => s.trim())
16
- .filter((s) => s !== '');
12
+ const federatedServers = await getFederatedServersFromMetadata();
13
+ const { publicUrl } = await $provideServer();
17
14
 
18
15
  const agentsWithUrl = agents.map((agent) => ({
19
16
  ...agent,
20
- url: `${serverUrl}/${agent.agentName}`,
17
+ url: `${publicUrl.href}agents/${encodeURIComponent(agent.agentName)}`,
21
18
  }));
22
19
 
23
- return NextResponse.json({
20
+ const response = NextResponse.json({
24
21
  agents: agentsWithUrl,
25
22
  federatedServers,
26
23
  });
24
+
25
+ // Add CORS headers
26
+ response.headers.set('Access-Control-Allow-Origin', '*');
27
+ response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
28
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
29
+
30
+ return response;
27
31
  } catch (error) {
28
32
  console.error('Error fetching agents:', error);
29
- return NextResponse.json(
30
- { error: 'Failed to fetch agents' },
31
- { status: 500 },
32
- );
33
+ return NextResponse.json({ error: 'Failed to fetch agents' }, { status: 500 });
33
34
  }
34
35
  }
36
+
37
+ export async function OPTIONS() {
38
+ const response = new NextResponse(null, { status: 200 });
39
+ response.headers.set('Access-Control-Allow-Origin', '*');
40
+ response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
41
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
42
+ return response;
43
+ }
@@ -1,7 +1,4 @@
1
- import { $getTableName } from '@/src/database/$getTableName';
2
- import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
3
- import { AgentsServerDatabase } from '../../../../database/schema';
4
- import { verifyPassword } from '../../../../utils/auth';
1
+ import { authenticateUser } from '../../../../utils/authenticateUser';
5
2
  import { setSession } from '../../../../utils/session';
6
3
  import { NextResponse } from 'next/server';
7
4
 
@@ -14,50 +11,15 @@ export async function POST(request: Request) {
14
11
  return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
15
12
  }
16
13
 
17
- // 1. Check if it's the environment admin
18
- if (process.env.ADMIN_PASSWORD && password === process.env.ADMIN_PASSWORD && username === 'admin') {
19
- // Or maybe allow any username if password matches admin password?
20
- // The task says "process.env.ADMIN_PASSWORD is like one of the admin users"
21
- // Assuming username 'admin' for environment password login.
22
- await setSession({ username: 'admin', isAdmin: true });
23
- return NextResponse.json({ success: true });
24
- }
25
-
26
- // 2. Check DB users
27
- const supabase = $provideSupabaseForServer();
28
- const { data: user, error } = await supabase
29
- .from(await $getTableName('User'))
30
- .select('*')
31
- .eq('username', username)
32
- .single();
33
-
34
- if (error || !user) {
35
- // Check if password matches ADMIN_PASSWORD even if user doesn't exist?
36
- // "The table User should work together with the process.env.ADMIN_PASSWORD"
37
- // If the user enters a password that matches process.env.ADMIN_PASSWORD, should they get admin access regardless of username?
38
- // "process.env.ADMIN_PASSWORD is like one of the admin users" implies it's a specific credential.
39
- // Let's stick to: if username is 'admin' and password is ADMIN_PASSWORD, it works.
40
- // Or if the password matches ADMIN_PASSWORD, maybe we grant admin access?
41
- // "Non-admin users can only log in... cannot see list of users"
42
-
43
- // Re-reading: "process.env.ADMIN_PASSWORD is like one of the admin users in the User table"
44
- // This suggests it's treated as a user.
45
-
46
- // If I login with a valid user from DB, I check password hash.
47
- // If I login with 'admin' and ADMIN_PASSWORD, I get admin.
14
+ const user = await authenticateUser(username, password);
48
15
 
16
+ if (user) {
17
+ await setSession(user);
18
+ return NextResponse.json({ success: true });
19
+ } else {
49
20
  return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
50
21
  }
51
22
 
52
- const isValid = await verifyPassword(password, (user as AgentsServerDatabase['public']['Tables']['User']['Row']).passwordHash);
53
-
54
- if (!isValid) {
55
- return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
56
- }
57
-
58
- await setSession({ username: (user as AgentsServerDatabase['public']['Tables']['User']['Row']).username, isAdmin: (user as AgentsServerDatabase['public']['Tables']['User']['Row']).isAdmin });
59
- return NextResponse.json({ success: true });
60
-
61
23
  } catch (error) {
62
24
  console.error('Login error:', error);
63
25
  return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
@@ -0,0 +1,38 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../../database/$getTableName';
3
+ import { $provideSupabase } from '../../../../database/$provideSupabase';
4
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
5
+
6
+ /**
7
+ * Delete a single chat feedback entry by ID.
8
+ */
9
+ export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
10
+ if (!(await isUserAdmin())) {
11
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12
+ }
13
+
14
+
15
+ const rawId = (await context.params).id;
16
+ const id = Number.parseInt(rawId, 10);
17
+
18
+ if (!Number.isFinite(id) || id <= 0) {
19
+ return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
20
+ }
21
+
22
+ try {
23
+ const supabase = $provideSupabase();
24
+ const table = await $getTableName('ChatFeedback');
25
+
26
+ const { error } = await supabase.from(table).delete().eq('id', id);
27
+
28
+ if (error) {
29
+ console.error('Delete chat feedback row error:', error);
30
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
31
+ }
32
+
33
+ return NextResponse.json({ success: true });
34
+ } catch (error) {
35
+ console.error('Delete chat feedback row error:', error);
36
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
37
+ }
38
+ }
@@ -0,0 +1,157 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../database/$getTableName';
3
+ import { $provideSupabase } from '../../../database/$provideSupabase';
4
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
5
+
6
+ const DEFAULT_PAGE_SIZE = 20;
7
+ const MAX_PAGE_SIZE = 100;
8
+
9
+ type SortField = 'createdAt' | 'agentName' | 'id';
10
+ type SortOrder = 'asc' | 'desc';
11
+
12
+ function parsePositiveInt(value: string | null, fallback: number): number {
13
+ if (!value) return fallback;
14
+ const parsed = parseInt(value, 10);
15
+ if (Number.isNaN(parsed) || parsed <= 0) return fallback;
16
+ return parsed;
17
+ }
18
+
19
+ function parseSortField(value: string | null): SortField {
20
+ if (value === 'agentName' || value === 'id') return value;
21
+ return 'createdAt';
22
+ }
23
+
24
+ function parseSortOrder(value: string | null): SortOrder {
25
+ return value === 'asc' ? 'asc' : 'desc';
26
+ }
27
+
28
+ /**
29
+ * List chat feedback with filters, search and pagination.
30
+ *
31
+ * Query params:
32
+ * - page: number (1-based)
33
+ * - pageSize: number (items per page)
34
+ * - agentName: filter by agent name
35
+ * - search: free-text search across agentName, url, ip, textRating, userNote and expectedAnswer
36
+ * - sortBy: createdAt | agentName | id (default: createdAt)
37
+ * - sortOrder: asc | desc (default: desc)
38
+ */
39
+ export async function GET(request: NextRequest) {
40
+ if (!(await isUserAdmin())) {
41
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
42
+ }
43
+
44
+ try {
45
+ const searchParams = request.nextUrl.searchParams;
46
+
47
+ const page = parsePositiveInt(searchParams.get('page'), 1);
48
+ const pageSize = Math.min(
49
+ MAX_PAGE_SIZE,
50
+ parsePositiveInt(searchParams.get('pageSize'), DEFAULT_PAGE_SIZE),
51
+ );
52
+ const agentName = searchParams.get('agentName');
53
+ const search = searchParams.get('search')?.trim() || '';
54
+ const sortBy = parseSortField(searchParams.get('sortBy'));
55
+ const sortOrder = parseSortOrder(searchParams.get('sortOrder'));
56
+
57
+ const supabase = $provideSupabase();
58
+ const table = await $getTableName('ChatFeedback');
59
+
60
+ let query = supabase
61
+ .from(table)
62
+ .select('*', { count: 'exact' });
63
+
64
+ if (agentName) {
65
+ query = query.eq('agentName', agentName);
66
+ }
67
+
68
+ if (search) {
69
+ // Note: We intentionally limit search to simple text columns
70
+ // to keep the query portable and efficient.
71
+ //
72
+ // This searches across:
73
+ // - agentName
74
+ // - url
75
+ // - ip
76
+ // - textRating
77
+ // - userNote
78
+ // - expectedAnswer
79
+ const escaped = search.replace(/%/g, '\\%').replace(/_/g, '\\_');
80
+ query = query.or(
81
+ [
82
+ `agentName.ilike.%${escaped}%`,
83
+ `url.ilike.%${escaped}%`,
84
+ `ip.ilike.%${escaped}%`,
85
+ `textRating.ilike.%${escaped}%`,
86
+ `userNote.ilike.%${escaped}%`,
87
+ `expectedAnswer.ilike.%${escaped}%`,
88
+ ].join(','),
89
+ );
90
+ }
91
+
92
+ query = query.order(sortBy, { ascending: sortOrder === 'asc' });
93
+
94
+ const from = (page - 1) * pageSize;
95
+ const to = from + pageSize - 1;
96
+
97
+ query = query.range(from, to);
98
+
99
+ const { data, error, count } = await query;
100
+
101
+ if (error) {
102
+ console.error('List chat feedback error:', error);
103
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
104
+ }
105
+
106
+ return NextResponse.json({
107
+ items: data ?? [],
108
+ total: count ?? 0,
109
+ page,
110
+ pageSize,
111
+ sortBy,
112
+ sortOrder,
113
+ });
114
+ } catch (error) {
115
+ console.error('List chat feedback error:', error);
116
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Delete chat feedback for a specific agent.
122
+ *
123
+ * Query params:
124
+ * - agentName: name of the agent whose feedback should be removed
125
+ */
126
+ export async function DELETE(request: NextRequest) {
127
+ if (!(await isUserAdmin())) {
128
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
129
+ }
130
+
131
+ const searchParams = request.nextUrl.searchParams;
132
+ const agentName = searchParams.get('agentName');
133
+
134
+ if (!agentName) {
135
+ return NextResponse.json({ error: 'agentName is required' }, { status: 400 });
136
+ }
137
+
138
+ try {
139
+ const supabase = $provideSupabase();
140
+ const table = await $getTableName('ChatFeedback');
141
+
142
+ const { error } = await supabase
143
+ .from(table)
144
+ .delete()
145
+ .eq('agentName', agentName);
146
+
147
+ if (error) {
148
+ console.error('Clear chat feedback error:', error);
149
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
150
+ }
151
+
152
+ return NextResponse.json({ success: true });
153
+ } catch (error) {
154
+ console.error('Clear chat feedback error:', error);
155
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
156
+ }
157
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../../database/$getTableName';
3
+ import { $provideSupabase } from '../../../../database/$provideSupabase';
4
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
5
+
6
+ /**
7
+ * Delete a single chat history entry by ID.
8
+ */
9
+ export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
10
+ if (!(await isUserAdmin())) {
11
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12
+ }
13
+
14
+ const rawId = (await context.params).id;
15
+ const id = Number.parseInt(rawId, 10);
16
+
17
+ if (!Number.isFinite(id) || id <= 0) {
18
+ return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
19
+ }
20
+
21
+ try {
22
+ const supabase = $provideSupabase();
23
+ const table = await $getTableName('ChatHistory');
24
+
25
+ const { error } = await supabase.from(table).delete().eq('id', id);
26
+
27
+ if (error) {
28
+ console.error('Delete chat history row error:', error);
29
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
30
+ }
31
+
32
+ return NextResponse.json({ success: true });
33
+ } catch (error) {
34
+ console.error('Delete chat history row error:', error);
35
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
36
+ }
37
+ }
@@ -0,0 +1,147 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../database/$getTableName';
3
+ import { $provideSupabase } from '../../../database/$provideSupabase';
4
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
5
+
6
+ const DEFAULT_PAGE_SIZE = 20;
7
+ const MAX_PAGE_SIZE = 100;
8
+
9
+ type SortField = 'createdAt' | 'agentName' | 'id';
10
+ type SortOrder = 'asc' | 'desc';
11
+
12
+ function parsePositiveInt(value: string | null, fallback: number): number {
13
+ if (!value) return fallback;
14
+ const parsed = parseInt(value, 10);
15
+ if (Number.isNaN(parsed) || parsed <= 0) return fallback;
16
+ return parsed;
17
+ }
18
+
19
+ function parseSortField(value: string | null): SortField {
20
+ if (value === 'agentName' || value === 'id') return value;
21
+ return 'createdAt';
22
+ }
23
+
24
+ function parseSortOrder(value: string | null): SortOrder {
25
+ return value === 'asc' ? 'asc' : 'desc';
26
+ }
27
+
28
+ /**
29
+ * List chat history with filters, search and pagination.
30
+ *
31
+ * Query params:
32
+ * - page: number (1-based)
33
+ * - pageSize: number (items per page)
34
+ * - agentName: filter by agent name
35
+ * - search: free-text search across agentName, url and ip
36
+ * - sortBy: createdAt | agentName | id (default: createdAt)
37
+ * - sortOrder: asc | desc (default: desc)
38
+ */
39
+ export async function GET(request: NextRequest) {
40
+ if (!(await isUserAdmin())) {
41
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
42
+ }
43
+
44
+ try {
45
+ const searchParams = request.nextUrl.searchParams;
46
+
47
+ const page = parsePositiveInt(searchParams.get('page'), 1);
48
+ const pageSize = Math.min(
49
+ MAX_PAGE_SIZE,
50
+ parsePositiveInt(searchParams.get('pageSize'), DEFAULT_PAGE_SIZE),
51
+ );
52
+ const agentName = searchParams.get('agentName');
53
+ const search = searchParams.get('search')?.trim() || '';
54
+ const sortBy = parseSortField(searchParams.get('sortBy'));
55
+ const sortOrder = parseSortOrder(searchParams.get('sortOrder'));
56
+
57
+ const supabase = $provideSupabase();
58
+ const table = await $getTableName('ChatHistory');
59
+
60
+ let query = supabase
61
+ .from(table)
62
+ .select('*', { count: 'exact' });
63
+
64
+ if (agentName) {
65
+ query = query.eq('agentName', agentName);
66
+ }
67
+
68
+ if (search) {
69
+ // Note: We intentionally limit search to simple text columns
70
+ // to keep the query portable and efficient.
71
+ //
72
+ // This searches across:
73
+ // - agentName
74
+ // - url
75
+ // - ip
76
+ const escaped = search.replace(/%/g, '\\%').replace(/_/g, '\\_');
77
+ query = query.or(
78
+ `agentName.ilike.%${escaped}%,url.ilike.%${escaped}%,ip.ilike.%${escaped}%`,
79
+ );
80
+ }
81
+
82
+ query = query.order(sortBy, { ascending: sortOrder === 'asc' });
83
+
84
+ const from = (page - 1) * pageSize;
85
+ const to = from + pageSize - 1;
86
+
87
+ query = query.range(from, to);
88
+
89
+ const { data, error, count } = await query;
90
+
91
+ if (error) {
92
+ console.error('List chat history error:', error);
93
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
94
+ }
95
+
96
+ return NextResponse.json({
97
+ items: data ?? [],
98
+ total: count ?? 0,
99
+ page,
100
+ pageSize,
101
+ sortBy,
102
+ sortOrder,
103
+ });
104
+ } catch (error) {
105
+ console.error('List chat history error:', error);
106
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Delete chat history for a specific agent.
112
+ *
113
+ * Query params:
114
+ * - agentName: name of the agent whose history should be removed
115
+ */
116
+ export async function DELETE(request: NextRequest) {
117
+ if (!(await isUserAdmin())) {
118
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
119
+ }
120
+
121
+ const searchParams = request.nextUrl.searchParams;
122
+ const agentName = searchParams.get('agentName');
123
+
124
+ if (!agentName) {
125
+ return NextResponse.json({ error: 'agentName is required' }, { status: 400 });
126
+ }
127
+
128
+ try {
129
+ const supabase = $provideSupabase();
130
+ const table = await $getTableName('ChatHistory');
131
+
132
+ const { error } = await supabase
133
+ .from(table)
134
+ .delete()
135
+ .eq('agentName', agentName);
136
+
137
+ if (error) {
138
+ console.error('Clear chat history error:', error);
139
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
140
+ }
141
+
142
+ return NextResponse.json({ success: true });
143
+ } catch (error) {
144
+ console.error('Clear chat history error:', error);
145
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
146
+ }
147
+ }
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getFederatedServersFromMetadata } from '../../../utils/getFederatedServersFromMetadata';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export async function GET() {
7
+ try {
8
+ const federatedServers = await getFederatedServersFromMetadata();
9
+
10
+ return NextResponse.json({
11
+ federatedServers,
12
+ });
13
+ } catch (error) {
14
+ console.error('Error fetching federated servers:', error);
15
+ return NextResponse.json({ error: 'Failed to fetch federated servers' }, { status: 500 });
16
+ }
17
+ }
@@ -11,6 +11,7 @@ import { keepUnused } from '../../../../../../src/utils/organization/keepUnused'
11
11
  import { $provideCdnForServer } from '../../../../src/tools/$provideCdnForServer';
12
12
  import { getUserFileCdnKey } from '../../../../src/utils/cdn/utils/getUserFileCdnKey';
13
13
  import { validateMimeType } from '../../../../src/utils/validators/validateMimeType';
14
+ import { getMetadata } from '../../../database/getMetadata';
14
15
 
15
16
  export async function POST(request: NextRequest) {
16
17
  try {
@@ -18,9 +19,16 @@ export async function POST(request: NextRequest) {
18
19
  // await forTime(5000);
19
20
 
20
21
  const nodeRequest = await nextRequestToNodeRequest(request);
22
+ let maxFileSizeMb = Number((await getMetadata('MAX_FILE_UPLOAD_SIZE_MB')) || '50'); // <- TODO: [🌲] To /config.ts
23
+
24
+ if (Number.isNaN(maxFileSizeMb)) {
25
+ maxFileSizeMb = 50; // <- TODO: [🌲] To /config.ts
26
+ }
27
+
28
+ const maxFileSize = maxFileSizeMb * 1024 * 1024;
21
29
 
22
30
  const files = await new Promise<formidable.Files>((resolve, reject) => {
23
- const form = formidable({});
31
+ const form = formidable({ maxFileSize });
24
32
  form.parse(nodeRequest as TODO_any, (error, fields, files) => {
25
33
  keepUnused(fields);
26
34
 
@@ -0,0 +1,62 @@
1
+ import { notFound } from 'next/navigation';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import { BookCommitment } from '../../../../../../src/commitments/_base/BookCommitment';
4
+ import { getVisibleCommitmentDefinitions } from '../../../utils/getVisibleCommitmentDefinitions';
5
+
6
+ type DocPageProps = {
7
+ params: Promise<{
8
+ docId: string;
9
+ }>;
10
+ };
11
+
12
+ export default async function DocPage(props: DocPageProps) {
13
+ const { docId } = await props.params;
14
+
15
+ // Decode the docId in case it contains encoded characters (though types usually don't)
16
+ const commitmentType = decodeURIComponent(docId) as BookCommitment;
17
+ const groupedCommitments = getVisibleCommitmentDefinitions();
18
+ const group = groupedCommitments.find((g) => g.primary.type === commitmentType || g.aliases.includes(commitmentType));
19
+
20
+ if (!group) {
21
+ notFound();
22
+ }
23
+
24
+ const { primary, aliases } = group;
25
+
26
+ return (
27
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
28
+ <div className="container mx-auto px-4 py-16">
29
+ <div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
30
+ <div className="p-8 border-b border-gray-100 bg-gray-50/50">
31
+ <div className="flex items-center gap-4 mb-4">
32
+ <h1 className="text-4xl font-bold text-gray-900 tracking-tight">
33
+ {primary.type}
34
+ {aliases.length > 0 && (
35
+ <span className="text-gray-400 font-normal ml-4 text-2xl">
36
+ / {aliases.join(' / ')}
37
+ </span>
38
+ )}
39
+ </h1>
40
+ <span className="px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
41
+ Commitment
42
+ </span>
43
+ </div>
44
+ {primary.description && (
45
+ <p className="text-xl text-gray-600 leading-relaxed max-w-3xl">
46
+ {primary.description}
47
+ </p>
48
+ )}
49
+ </div>
50
+
51
+ <div className="p-8">
52
+ <article className="prose prose-lg prose-slate max-w-none prose-headings:font-bold prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-p:text-gray-600 prose-pre:bg-gray-50 prose-pre:border prose-pre:border-gray-200 prose-pre:text-gray-800">
53
+ <ReactMarkdown>
54
+ {primary.documentation}
55
+ </ReactMarkdown>
56
+ </article>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ );
62
+ }