@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
@@ -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,63 @@
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
+ <span className="mr-3">{primary.icon}</span>
34
+ {primary.type}
35
+ {aliases.length > 0 && (
36
+ <span className="text-gray-400 font-normal ml-4 text-2xl">
37
+ / {aliases.join(' / ')}
38
+ </span>
39
+ )}
40
+ </h1>
41
+ <span className="px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
42
+ Commitment
43
+ </span>
44
+ </div>
45
+ {primary.description && (
46
+ <p className="text-xl text-gray-600 leading-relaxed max-w-3xl">
47
+ {primary.description}
48
+ </p>
49
+ )}
50
+ </div>
51
+
52
+ <div className="p-8">
53
+ <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">
54
+ <ReactMarkdown>
55
+ {primary.documentation}
56
+ </ReactMarkdown>
57
+ </article>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,34 @@
1
+ import Link from 'next/link';
2
+ import { Card } from '../../components/Homepage/Card';
3
+ import { Section } from '../../components/Homepage/Section';
4
+ import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
5
+
6
+ export default function DocsPage() {
7
+ const groupedCommitments = getVisibleCommitmentDefinitions();
8
+
9
+ return (
10
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
11
+ <div className="container mx-auto px-4 py-16">
12
+ <Section title="Documentation">
13
+ {groupedCommitments.map(({ primary, aliases }) => (
14
+ <Link key={primary.type} href={`/docs/${primary.type}`} className="block h-full group">
15
+ <Card className="h-full group-hover:border-blue-500 transition-colors">
16
+ <h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
17
+ <span className="mr-2">{primary.icon}</span>
18
+ {primary.type}
19
+ {aliases.length > 0 && (
20
+ <span className="text-gray-400 font-normal text-lg">
21
+ {' / '}
22
+ {aliases.join(' / ')}
23
+ </span>
24
+ )}
25
+ </h3>
26
+ {primary.description && <p className="text-gray-600 line-clamp-3">{primary.description}</p>}
27
+ </Card>
28
+ </Link>
29
+ ))}
30
+ </Section>
31
+ </div>
32
+ </div>
33
+ );
34
+ }
@@ -3,7 +3,10 @@ import { LayoutWrapper } from '@/src/components/LayoutWrapper/LayoutWrapper';
3
3
  import type { Metadata } from 'next';
4
4
  import { Barlow_Condensed } from 'next/font/google';
5
5
  import { getMetadata } from '../database/getMetadata';
6
+ import { $provideAgentCollectionForServer } from '../tools/$provideAgentCollectionForServer';
7
+ import { $provideServer } from '../tools/$provideServer';
6
8
  import { isUserAdmin } from '../utils/isUserAdmin';
9
+ import { getCurrentUser } from '../utils/getCurrentUser';
7
10
  import './globals.css';
8
11
 
9
12
  const barlowCondensed = Barlow_Condensed({
@@ -13,9 +16,9 @@ const barlowCondensed = Barlow_Condensed({
13
16
  });
14
17
 
15
18
  export async function generateMetadata(): Promise<Metadata> {
19
+ const { publicUrl } = await $provideServer();
16
20
  const serverName = (await getMetadata('SERVER_NAME')) || 'Promptbook Agents Server';
17
21
  const serverDescription = (await getMetadata('SERVER_DESCRIPTION')) || 'Agents server powered by Promptbook';
18
- const serverUrl = (await getMetadata('SERVER_URL')) || 'https://ptbk.io';
19
22
 
20
23
  return {
21
24
  title: serverName,
@@ -44,7 +47,7 @@ export async function generateMetadata(): Promise<Metadata> {
44
47
  description: serverDescription,
45
48
  // TODO: images: ['https://www.ptbk.io/design'],
46
49
  },
47
- metadataBase: new URL(serverUrl),
50
+ metadataBase: publicUrl,
48
51
  };
49
52
  }
50
53
 
@@ -54,9 +57,22 @@ export default async function RootLayout({
54
57
  children: React.ReactNode;
55
58
  }>) {
56
59
  const isAdmin = await isUserAdmin();
60
+ const currentUser = await getCurrentUser();
57
61
  const serverName = (await getMetadata('SERVER_NAME')) || 'Promptbook Agents Server';
58
62
  const serverLogoUrl = (await getMetadata('SERVER_LOGO_URL')) || null;
59
63
  const serverFaviconUrl = (await getMetadata('SERVER_FAVICON_URL')) || faviconLogoImage.src;
64
+ const isFooterShown = ((await getMetadata('IS_FOOTER_SHOWN')) || 'true') === 'true';
65
+
66
+ let footerLinks = [];
67
+ try {
68
+ const footerLinksString = (await getMetadata('FOOTER_LINKS')) || '[]';
69
+ footerLinks = JSON.parse(footerLinksString);
70
+ } catch (error) {
71
+ console.error('Failed to parse FOOTER_LINKS', error);
72
+ }
73
+
74
+ const collection = await $provideAgentCollectionForServer();
75
+ const agents = await collection.listAgents();
60
76
 
61
77
  return (
62
78
  <html lang="en">
@@ -83,9 +99,19 @@ export default async function RootLayout({
83
99
  <link rel="icon" href={serverFaviconUrl} type="image/x-icon" />
84
100
  </head>
85
101
  <body className={`${barlowCondensed.variable} antialiased bg-white text-gray-900`}>
86
- <LayoutWrapper isAdmin={isAdmin} serverName={serverName} serverLogoUrl={serverLogoUrl}>
102
+ <LayoutWrapper
103
+ isAdmin={isAdmin}
104
+ currentUser={currentUser}
105
+ serverName={serverName}
106
+ serverLogoUrl={serverLogoUrl}
107
+ agents={JSON.parse(JSON.stringify(agents))}
108
+ isFooterShown={isFooterShown}
109
+ footerLinks={footerLinks}
110
+ >
87
111
  {children}
88
112
  </LayoutWrapper>
113
+ {/* Global portal root for modals/popups */}
114
+ <div id="portal-root" />
89
115
  </body>
90
116
  </html>
91
117
  );
@@ -0,0 +1,109 @@
1
+ import { PROMPTBOOK_COLOR } from '@promptbook-local/core';
2
+ import { MetadataRoute } from 'next';
3
+ import { headers } from 'next/headers';
4
+ import { Color } from '../../../../src/utils/color/Color';
5
+ import { getMetadata } from '../database/getMetadata';
6
+ import { $provideServer } from '../tools/$provideServer';
7
+ import { getAgentProfile } from './agents/[agentName]/_utils';
8
+
9
+ export default async function manifest(): Promise<MetadataRoute.Manifest> {
10
+ const { publicUrl } = await $provideServer();
11
+ const serverName = (await getMetadata('SERVER_NAME')) || 'Promptbook Agents Server';
12
+ const serverDescription = (await getMetadata('SERVER_DESCRIPTION')) || 'Agents server powered by Promptbook';
13
+
14
+ const referer = (await headers()).get('referer') || '';
15
+ const isAgentManifest = referer.includes('/agents/');
16
+
17
+ if (!isAgentManifest) {
18
+ return {
19
+ name: serverName,
20
+ short_name: serverName,
21
+ description: serverDescription,
22
+ start_url: publicUrl.href,
23
+ display_override: ['fullscreen', 'minimal-ui'],
24
+ display: 'standalone',
25
+ background_color: PROMPTBOOK_COLOR.toHex(),
26
+ theme_color: PROMPTBOOK_COLOR.toHex(),
27
+ icons: [
28
+ // TODO: Create icons for the server homepage
29
+ ],
30
+ scope: publicUrl.href,
31
+ } satisfies MetadataRoute.Manifest;
32
+ }
33
+
34
+ const agentName = decodeURIComponent(referer.split('/agents/')[1]?.split('/')[0]);
35
+
36
+ try {
37
+ const agentProfile = await getAgentProfile(agentName);
38
+
39
+ const name = agentProfile.meta.fullname || agentProfile.agentName;
40
+ const short_name = agentProfile.agentName;
41
+ const description = agentProfile.meta.description || agentProfile.personaDescription || `Agent ${name}`;
42
+
43
+ // Extract brand color from meta
44
+ const brandColor = Color.from(agentProfile.meta.color || PROMPTBOOK_COLOR);
45
+ const theme_color = brandColor.toHex();
46
+ const background_color = '#ffffff';
47
+
48
+ const agentUrl = `${publicUrl.href}agents/${encodeURIComponent(agentName)}`;
49
+
50
+ const icons: MetadataRoute.Manifest['icons'] = [
51
+ {
52
+ src: `${agentUrl}/images/icon-256.png`,
53
+ sizes: '256x256',
54
+ type: 'image/png',
55
+ purpose: 'any',
56
+ },
57
+ {
58
+ src: `${agentUrl}/images/icon-256.png`,
59
+ sizes: '256x256',
60
+ type: 'image/png',
61
+ purpose: 'maskable',
62
+ },
63
+ ];
64
+
65
+ const screenshots: MetadataRoute.Manifest['screenshots'] = [
66
+ {
67
+ src: `${agentUrl}/images/screenshot-fullhd.png`,
68
+ sizes: '1920x1080',
69
+ type: 'image/png',
70
+ form_factor: 'wide',
71
+ label: 'Full HD Screenshot',
72
+ },
73
+ {
74
+ src: `${agentUrl}/images/screenshot-phone.png`,
75
+ sizes: '1080x1920',
76
+ type: 'image/png',
77
+ form_factor: 'narrow',
78
+ label: 'Phone Screenshot',
79
+ },
80
+ ];
81
+
82
+ return {
83
+ id: agentUrl,
84
+ name,
85
+ short_name,
86
+ description,
87
+ start_url: `${agentUrl}/chat`,
88
+ scope: agentUrl,
89
+ display_override: ['fullscreen', 'minimal-ui'],
90
+ display: 'standalone',
91
+ background_color,
92
+ theme_color,
93
+ icons,
94
+ screenshots,
95
+ } satisfies MetadataRoute.Manifest;
96
+ } catch (error) {
97
+ console.warn(`Failed to generate manifest for agent ${agentName}`, error);
98
+ return {
99
+ name: agentName,
100
+ short_name: agentName,
101
+ start_url: `${publicUrl.href}agents/${encodeURIComponent(agentName)}/chat`,
102
+ display_override: ['fullscreen', 'minimal-ui'],
103
+ display: 'standalone',
104
+ background_color: '#ffffff',
105
+ theme_color: PROMPTBOOK_COLOR.toHex(),
106
+ icons: [],
107
+ } satisfies MetadataRoute.Manifest;
108
+ }
109
+ }
@@ -5,22 +5,18 @@ import moment from 'moment';
5
5
  import { headers } from 'next/headers';
6
6
  import { AboutPromptbookInformation } from '../../../../src/utils/misc/xAboutPromptbookInformation';
7
7
  import { $sideEffect } from '../../../../src/utils/organization/$sideEffect';
8
- import { AuthControls } from '../components/Auth/AuthControls';
9
- import { AgentCard } from '../components/Homepage/AgentCard';
10
- import { ModelCard } from '../components/Homepage/ModelCard';
8
+ import { AgentsList } from '../components/Homepage/AgentsList';
9
+ import { ExternalAgentsSectionClient } from '../components/Homepage/ExternalAgentsSectionClient';
10
+ import { ModelsSection } from '../components/Homepage/ModelsSection';
11
11
  import { Section } from '../components/Homepage/Section';
12
12
  import { TechInfoCard } from '../components/Homepage/TechInfoCard';
13
13
  import { UsersList } from '../components/UsersList/UsersList';
14
14
  import VercelDeploymentCard from '../components/VercelDeploymentCard/VercelDeploymentCard';
15
- import { getMetadata } from '../database/getMetadata';
16
15
  import { getLongRunningTask } from '../deamons/longRunningTask';
17
16
  import { $provideAgentCollectionForServer } from '../tools/$provideAgentCollectionForServer';
18
17
  import { $provideExecutionToolsForServer } from '../tools/$provideExecutionToolsForServer';
19
18
  import { $provideServer } from '../tools/$provideServer';
20
- import { getCurrentUser } from '../utils/getCurrentUser';
21
- import { getFederatedAgents } from '../utils/getFederatedAgents';
22
19
  import { isUserAdmin } from '../utils/isUserAdmin';
23
- import { AddAgentButton } from './AddAgentButton';
24
20
 
25
21
  // Add calendar formats that include seconds
26
22
  const calendarWithSeconds = {
@@ -36,19 +32,10 @@ export default async function HomePage() {
36
32
  $sideEffect(/* Note: [🐶] This will ensure dynamic rendering of page and avoid Next.js pre-render */ headers());
37
33
 
38
34
  const isAdmin = await isUserAdmin(); /* <- TODO: [👹] Here should be user permissions */
39
- const currentUser = await getCurrentUser();
40
35
 
41
36
  const collection = await $provideAgentCollectionForServer();
42
37
  const agents = await collection.listAgents();
43
38
 
44
- const federatedServersString = (await getMetadata('FEDERATED_SERVERS')) || '';
45
- const federatedServers = federatedServersString
46
- .split(',')
47
- .map((s) => s.trim())
48
- .filter((s) => s !== '');
49
-
50
- const externalAgents = await getFederatedAgents(federatedServers);
51
-
52
39
  const longRunningTask = getLongRunningTask();
53
40
 
54
41
  const executionTools = await $provideExecutionToolsForServer();
@@ -59,38 +46,14 @@ export default async function HomePage() {
59
46
  return (
60
47
  <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
61
48
  <div className="container mx-auto px-4 py-16">
62
- <div className="flex justify-end mb-4">
63
- <AuthControls initialUser={currentUser} />
64
- </div>
65
-
66
- <Section title={`Agents (${agents.length})`}>
67
- {agents.map((agent) => (
68
- <AgentCard key={agent.agentName} agent={agent} href={`/${agent.agentName}`} />
69
- ))}
70
- {isAdmin && <AddAgentButton />}
71
- </Section>
72
-
73
- {externalAgents.length > 0 && (
74
- <Section title={`External Agents (${externalAgents.length})`}>
75
- {externalAgents.map((agent) => (
76
- <AgentCard key={agent.url} agent={agent} href={agent.url} />
77
- ))}
78
- </Section>
79
- )}
49
+ <AgentsList agents={[...agents]} isAdmin={isAdmin} />
50
+
51
+ <ExternalAgentsSectionClient />
80
52
 
81
- {isAdmin && <UsersList />}
53
+ {isAdmin && <UsersList allowCreate={false} />}
82
54
 
83
55
  {isAdmin && (
84
- <Section title={`Models (${models.length})`}>
85
- {models.map(({ modelName, modelTitle, modelDescription }) => (
86
- <ModelCard
87
- key={modelName}
88
- modelName={modelName}
89
- modelTitle={modelTitle || modelName}
90
- modelDescription={modelDescription}
91
- />
92
- ))}
93
- </Section>
56
+ <ModelsSection models={models} maxVisible={11} showViewAllLink />
94
57
  )}
95
58
 
96
59
  {isAdmin && (
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { RefreshCcwIcon } from 'lucide-react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useState } from 'react';
6
+ import { restoreDeletedAgent } from './actions';
7
+
8
+ type RestoreAgentButtonProps = {
9
+ agentName: string;
10
+ };
11
+
12
+ export function RestoreAgentButton({ agentName }: RestoreAgentButtonProps) {
13
+ const router = useRouter();
14
+ const [isRestoring, setIsRestoring] = useState(false);
15
+
16
+ const handleRestore = async () => {
17
+ try {
18
+ setIsRestoring(true);
19
+ await restoreDeletedAgent(agentName);
20
+ router.refresh();
21
+ } catch (error) {
22
+ console.error('Failed to restore agent:', error);
23
+ alert('Failed to restore agent');
24
+ } finally {
25
+ setIsRestoring(false);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <button
31
+ onClick={handleRestore}
32
+ disabled={isRestoring}
33
+ className="flex items-center gap-2 px-3 py-2 bg-white border border-gray-300 rounded hover:bg-gray-50 text-gray-700 disabled:opacity-50"
34
+ title="Restore agent"
35
+ >
36
+ <RefreshCcwIcon className={`w-4 h-4 ${isRestoring ? 'animate-spin' : ''}`} />
37
+ {isRestoring ? 'Restoring...' : 'Restore'}
38
+ </button>
39
+ );
40
+ }
@@ -0,0 +1,27 @@
1
+ 'use server';
2
+
3
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
4
+ import { revalidatePath } from 'next/cache';
5
+
6
+ export async function restoreDeletedAgent(agentName: string) {
7
+ const collection = await $provideAgentCollectionForServer();
8
+
9
+ // Find the latest history item for this agent
10
+ const history = await collection.listAgentHistory(agentName);
11
+
12
+ if (history.length === 0) {
13
+ throw new Error(`No history found for agent ${agentName}`);
14
+ }
15
+
16
+ const latestVersion = history[0];
17
+
18
+ if (!latestVersion) {
19
+ throw new Error(`No history found for agent ${agentName}`);
20
+ }
21
+
22
+ await collection.restoreAgent(latestVersion.id);
23
+
24
+ revalidatePath('/recycle-bin');
25
+ revalidatePath(`/agents/${agentName}`);
26
+ revalidatePath('/');
27
+ }