@promptbook/cli 0.104.0-1 → 0.104.0-11

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 (229) hide show
  1. package/apps/agents-server/config.ts +1 -3
  2. package/apps/agents-server/next.config.ts +2 -2
  3. package/apps/agents-server/package.json +7 -3
  4. package/apps/agents-server/public/fonts/OpenMoji-color-cbdt.woff2 +0 -0
  5. package/apps/agents-server/public/swagger.json +115 -0
  6. package/apps/agents-server/scripts/generate-reserved-paths/generate-reserved-paths.ts +54 -0
  7. package/apps/agents-server/scripts/generate-reserved-paths/tsconfig.json +19 -0
  8. package/apps/agents-server/src/app/AddAgentButton.tsx +47 -21
  9. package/apps/agents-server/src/app/actions.ts +22 -5
  10. package/apps/agents-server/src/app/admin/browser-test/BrowserTestClient.tsx +211 -0
  11. package/apps/agents-server/src/app/admin/browser-test/page.tsx +13 -0
  12. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +221 -274
  13. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +94 -137
  14. package/apps/agents-server/src/app/admin/files/FilesGalleryClient.tsx +263 -0
  15. package/apps/agents-server/src/app/admin/files/actions.ts +61 -0
  16. package/apps/agents-server/src/app/admin/files/page.tsx +13 -0
  17. package/apps/agents-server/src/app/admin/image-generator-test/ImageGeneratorTestClient.tsx +169 -0
  18. package/apps/agents-server/src/app/admin/image-generator-test/page.tsx +13 -0
  19. package/apps/agents-server/src/app/admin/images/ImagesGalleryClient.tsx +256 -0
  20. package/apps/agents-server/src/app/admin/images/actions.ts +60 -0
  21. package/apps/agents-server/src/app/admin/images/page.tsx +13 -0
  22. package/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +294 -0
  23. package/apps/agents-server/src/app/admin/messages/page.tsx +13 -0
  24. package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +104 -0
  25. package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +35 -0
  26. package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +13 -0
  27. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +23 -19
  28. package/apps/agents-server/src/app/admin/search-engine-test/SearchEngineTestClient.tsx +109 -0
  29. package/apps/agents-server/src/app/admin/search-engine-test/actions.ts +17 -0
  30. package/apps/agents-server/src/app/admin/search-engine-test/page.tsx +13 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +15 -1
  32. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +51 -9
  33. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +47 -4
  34. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +53 -11
  35. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +23 -3
  36. package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +8 -8
  37. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +17 -26
  38. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +4 -2
  39. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +20 -0
  40. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +6 -11
  41. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +5 -1
  42. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +5 -2
  43. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +20 -16
  44. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +15 -2
  45. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +15 -2
  46. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +12 -0
  47. package/apps/agents-server/src/app/agents/[agentName]/code/api/route.ts +68 -0
  48. package/apps/agents-server/src/app/agents/[agentName]/code/page.tsx +223 -0
  49. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +5 -0
  50. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +2 -2
  51. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +10 -3
  52. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/getAgentDefaultAvatarPrompt.ts +31 -0
  53. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +194 -0
  54. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +14 -2
  55. package/apps/agents-server/src/app/agents/[agentName]/images/page.tsx +200 -0
  56. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +4 -3
  57. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +4 -3
  58. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +10 -3
  59. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +11 -4
  60. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +11 -2
  61. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +18 -10
  62. package/apps/agents-server/src/app/agents/[agentName]/system-message/page.tsx +100 -0
  63. package/apps/agents-server/src/app/api/admin-email/route.ts +12 -0
  64. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +13 -14
  65. package/apps/agents-server/src/app/api/agents/[agentName]/restore/route.ts +20 -0
  66. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +43 -1
  67. package/apps/agents-server/src/app/api/agents/route.ts +28 -3
  68. package/apps/agents-server/src/app/api/api-tokens/route.ts +6 -7
  69. package/apps/agents-server/src/app/api/browser-test/act/route.ts +141 -0
  70. package/apps/agents-server/src/app/api/browser-test/screenshot/route.ts +30 -0
  71. package/apps/agents-server/src/app/api/browser-test/scroll-facebook/route.ts +62 -0
  72. package/apps/agents-server/src/app/api/docs/book.md/route.ts +61 -0
  73. package/apps/agents-server/src/app/api/emails/incoming/sendgrid/route.ts +48 -0
  74. package/apps/agents-server/src/app/api/federated-agents/route.ts +12 -0
  75. package/apps/agents-server/src/app/api/images/[filename]/route.ts +128 -0
  76. package/apps/agents-server/src/app/api/messages/route.ts +102 -0
  77. package/apps/agents-server/src/app/api/metadata/route.ts +5 -6
  78. package/apps/agents-server/src/app/api/upload/route.ts +128 -45
  79. package/apps/agents-server/src/app/docs/[docId]/page.tsx +2 -3
  80. package/apps/agents-server/src/app/docs/page.tsx +12 -12
  81. package/apps/agents-server/src/app/globals.css +140 -33
  82. package/apps/agents-server/src/app/humans.txt/route.ts +1 -1
  83. package/apps/agents-server/src/app/layout.tsx +27 -22
  84. package/apps/agents-server/src/app/page.tsx +54 -6
  85. package/apps/agents-server/src/app/recycle-bin/actions.ts +20 -14
  86. package/apps/agents-server/src/app/recycle-bin/page.tsx +27 -41
  87. package/apps/agents-server/src/app/robots.txt/route.ts +1 -1
  88. package/apps/agents-server/src/app/security.txt/route.ts +1 -1
  89. package/apps/agents-server/src/app/sitemap.xml/route.ts +9 -7
  90. package/apps/agents-server/src/app/swagger/page.tsx +14 -0
  91. package/apps/agents-server/src/components/AgentProfile/AgentCapabilityChips.tsx +38 -0
  92. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +44 -116
  93. package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +92 -0
  94. package/apps/agents-server/src/components/AgentProfile/QrCodeModal.tsx +0 -1
  95. package/apps/agents-server/src/components/AgentProfile/useAgentBackground.ts +97 -0
  96. package/apps/agents-server/src/components/Auth/AuthControls.tsx +5 -4
  97. package/apps/agents-server/src/components/DeletedAgentBanner.tsx +26 -0
  98. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +38 -0
  99. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +11 -9
  100. package/apps/agents-server/src/components/Footer/Footer.tsx +5 -5
  101. package/apps/agents-server/src/components/ForgottenPasswordDialog/ForgottenPasswordDialog.tsx +61 -0
  102. package/apps/agents-server/src/components/Header/Header.tsx +130 -40
  103. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +150 -23
  104. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +93 -15
  105. package/apps/agents-server/src/components/Homepage/DeletedAgentsList.tsx +66 -0
  106. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +12 -3
  107. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +19 -10
  108. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +3 -2
  109. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +50 -1
  110. package/apps/agents-server/src/components/NewAgentDialog/NewAgentDialog.tsx +88 -0
  111. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +7 -2
  112. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +16 -7
  113. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +4 -4
  114. package/apps/agents-server/src/components/RegisterUserDialog/RegisterUserDialog.tsx +61 -0
  115. package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +2 -0
  116. package/apps/agents-server/src/components/_utils/generateMetaTxt.ts +12 -10
  117. package/apps/agents-server/src/components/_utils/headlessParam.tsx +7 -3
  118. package/apps/agents-server/src/database/$getTableName.ts +1 -0
  119. package/apps/agents-server/src/database/$provideSupabaseForBrowser.ts +3 -3
  120. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -1
  121. package/apps/agents-server/src/database/$provideSupabaseForWorker.ts +3 -3
  122. package/apps/agents-server/src/database/metadataDefaults.ts +19 -1
  123. package/apps/agents-server/src/database/migrate.ts +34 -1
  124. package/apps/agents-server/src/database/migrations/2025-11-0001-initial-schema.sql +1 -3
  125. package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
  126. package/apps/agents-server/src/database/migrations/2025-12-0240-agent-public-id.sql +3 -0
  127. package/apps/agents-server/src/database/migrations/2025-12-0360-agent-deleted-at.sql +1 -0
  128. package/apps/agents-server/src/database/migrations/2025-12-0370-image-table.sql +19 -0
  129. package/apps/agents-server/src/database/migrations/2025-12-0380-agent-visibility.sql +1 -0
  130. package/apps/agents-server/src/database/migrations/2025-12-0390-upload-tracking.sql +20 -0
  131. package/apps/agents-server/src/database/migrations/2025-12-0401-file-upload-status.sql +13 -0
  132. package/apps/agents-server/src/database/migrations/2025-12-0402-message-table.sql +42 -0
  133. package/apps/agents-server/src/database/migrations/2025-12-0403-generation-lock-table.sql +15 -0
  134. package/apps/agents-server/src/database/migrations/2025-12-0640-openai-assistant-cache.sql +12 -0
  135. package/apps/agents-server/src/database/migrations/2025-12-0820-agent-history-permanent-id.sql +29 -0
  136. package/apps/agents-server/src/database/migrations/2025-12-0830-image-purpose.sql +5 -0
  137. package/apps/agents-server/src/database/migrations/2025-12-0890-file-agent-id.sql +5 -0
  138. package/apps/agents-server/src/database/schema.ts +244 -4
  139. package/apps/agents-server/src/generated/reservedPaths.ts +32 -0
  140. package/apps/agents-server/src/message-providers/email/_common/Email.ts +73 -0
  141. package/apps/agents-server/src/message-providers/email/_common/utils/TODO.txt +1 -0
  142. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo +108 -0
  143. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.ts +62 -0
  144. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.test.ts.todo +117 -0
  145. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.ts +19 -0
  146. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.test.ts.todo +119 -0
  147. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.ts +19 -0
  148. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.test.ts.todo +74 -0
  149. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts +14 -0
  150. package/apps/agents-server/src/message-providers/email/sendgrid/SendgridMessageProvider.ts +44 -0
  151. package/apps/agents-server/src/message-providers/email/sendgrid/parseInboundSendgridEmail.ts +49 -0
  152. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +51 -0
  153. package/apps/agents-server/src/message-providers/index.ts +13 -0
  154. package/apps/agents-server/src/message-providers/interfaces/MessageProvider.ts +11 -0
  155. package/apps/agents-server/src/middleware.ts +19 -23
  156. package/apps/agents-server/src/tools/$provideBrowserForServer.ts +32 -0
  157. package/apps/agents-server/src/tools/$provideCdnForServer.ts +7 -2
  158. package/apps/agents-server/src/utils/auth.ts +117 -17
  159. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +57 -0
  160. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +4 -0
  161. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +18 -0
  162. package/apps/agents-server/src/utils/content/extractBodyContentFromHtml.ts +19 -0
  163. package/apps/agents-server/src/utils/getUserIdFromRequest.ts +35 -0
  164. package/apps/agents-server/src/utils/handleChatCompletion.ts +65 -5
  165. package/apps/agents-server/src/utils/messages/sendMessage.ts +91 -0
  166. package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
  167. package/apps/agents-server/src/utils/normalization/filenameToPrompt.test.ts +36 -0
  168. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +25 -0
  169. package/apps/agents-server/src/utils/validateApiKey.ts +7 -11
  170. package/esm/index.es.js +1534 -1330
  171. package/esm/index.es.js.map +1 -1
  172. package/esm/typings/servers.d.ts +8 -0
  173. package/esm/typings/src/_packages/core.index.d.ts +2 -0
  174. package/esm/typings/src/_packages/types.index.d.ts +16 -2
  175. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +29 -1
  176. package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirements.d.ts +6 -6
  177. package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.closed.test.d.ts +1 -0
  178. package/esm/typings/src/book-2.0/utils/generatePlaceholderAgentProfileImageUrl.d.ts +3 -3
  179. package/esm/typings/src/book-components/Chat/Chat/ChatMessageItem.d.ts +5 -1
  180. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  181. package/esm/typings/src/book-components/Chat/CodeBlock/CodeBlock.d.ts +13 -0
  182. package/esm/typings/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  183. package/esm/typings/src/book-components/Chat/types/ChatMessage.d.ts +9 -13
  184. package/esm/typings/src/book-components/_common/Dropdown/Dropdown.d.ts +3 -3
  185. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +1 -1
  186. package/esm/typings/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +56 -0
  187. package/esm/typings/src/book-components/icons/AboutIcon.d.ts +1 -1
  188. package/esm/typings/src/book-components/icons/AttachmentIcon.d.ts +1 -1
  189. package/esm/typings/src/book-components/icons/CameraIcon.d.ts +1 -1
  190. package/esm/typings/src/book-components/icons/DownloadIcon.d.ts +1 -1
  191. package/esm/typings/src/book-components/icons/MenuIcon.d.ts +1 -1
  192. package/esm/typings/src/book-components/icons/SaveIcon.d.ts +1 -1
  193. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -12
  194. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +27 -15
  195. package/esm/typings/src/commitments/DICTIONARY/DICTIONARY.d.ts +46 -0
  196. package/esm/typings/src/commitments/index.d.ts +2 -1
  197. package/esm/typings/src/llm-providers/_common/utils/count-total-usage/countUsage.d.ts +1 -1
  198. package/esm/typings/src/llm-providers/_multiple/MultipleLlmExecutionTools.d.ts +6 -2
  199. package/esm/typings/src/llm-providers/agent/Agent.d.ts +6 -1
  200. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +1 -1
  201. package/esm/typings/src/llm-providers/ollama/OllamaExecutionTools.d.ts +1 -1
  202. package/esm/typings/src/llm-providers/openai/createOpenAiCompatibleExecutionTools.d.ts +1 -1
  203. package/esm/typings/src/llm-providers/remote/RemoteLlmExecutionTools.d.ts +1 -0
  204. package/esm/typings/src/remote-server/ui/ServerApp.d.ts +1 -1
  205. package/esm/typings/src/search-engines/SearchEngine.d.ts +9 -0
  206. package/esm/typings/src/search-engines/SearchResult.d.ts +18 -0
  207. package/esm/typings/src/search-engines/bing/BingSearchEngine.d.ts +15 -0
  208. package/esm/typings/src/search-engines/dummy/DummySearchEngine.d.ts +15 -0
  209. package/esm/typings/src/types/Message.d.ts +49 -0
  210. package/esm/typings/src/types/ModelRequirements.d.ts +38 -14
  211. package/esm/typings/src/types/typeAliases.d.ts +23 -1
  212. package/esm/typings/src/utils/color/utils/colorToDataUrl.d.ts +2 -1
  213. package/esm/typings/src/utils/environment/$detectRuntimeEnvironment.d.ts +4 -4
  214. package/esm/typings/src/utils/environment/$isRunningInBrowser.d.ts +1 -1
  215. package/esm/typings/src/utils/environment/$isRunningInJest.d.ts +1 -1
  216. package/esm/typings/src/utils/environment/$isRunningInNode.d.ts +1 -1
  217. package/esm/typings/src/utils/environment/$isRunningInWebWorker.d.ts +1 -1
  218. package/esm/typings/src/utils/markdown/extractAllBlocksFromMarkdown.d.ts +2 -2
  219. package/esm/typings/src/utils/markdown/extractOneBlockFromMarkdown.d.ts +2 -2
  220. package/esm/typings/src/utils/random/$randomAgentPersona.d.ts +3 -2
  221. package/esm/typings/src/utils/random/$randomBase58.d.ts +12 -0
  222. package/esm/typings/src/version.d.ts +1 -1
  223. package/package.json +1 -1
  224. package/umd/index.umd.js +1542 -1338
  225. package/umd/index.umd.js.map +1 -1
  226. package/apps/agents-server/package-lock.json +0 -27
  227. package/apps/agents-server/public/fonts/download-font.js +0 -22
  228. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +0 -18
  229. package/esm/typings/src/book-2.0/utils/generateGravatarUrl.d.ts +0 -10
@@ -0,0 +1,294 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { Card } from '../../../components/Homepage/Card';
5
+ import {
6
+ $fetchMessages,
7
+ type MessageRow,
8
+ type MessageSendAttemptRow,
9
+ } from '../../../utils/messagesAdmin';
10
+
11
+ function formatDate(dateString: string | null | undefined): string {
12
+ if (!dateString) return '-';
13
+ const date = new Date(dateString);
14
+ if (Number.isNaN(date.getTime())) return dateString;
15
+ return date.toLocaleString();
16
+ }
17
+
18
+ function getMessagePreview(content: string, maxLength = 120): string {
19
+ if (!content) return '-';
20
+ return content.length > maxLength ? `${content.slice(0, maxLength)}…` : content;
21
+ }
22
+
23
+ function getStatusBadge(attempts: MessageSendAttemptRow[] | undefined) {
24
+ if (!attempts || attempts.length === 0) {
25
+ return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">Pending</span>;
26
+ }
27
+ const success = attempts.find(a => a.isSuccessful);
28
+ if (success) {
29
+ return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">Sent ({success.providerName})</span>;
30
+ }
31
+ return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">Failed ({attempts.length})</span>;
32
+ }
33
+
34
+ export function MessagesClient() {
35
+ const [items, setItems] = useState<MessageRow[]>([]);
36
+ const [total, setTotal] = useState(0);
37
+ const [page, setPage] = useState(1);
38
+ const [pageSize, setPageSize] = useState(20);
39
+ const [searchInput, setSearchInput] = useState('');
40
+ const [search, setSearch] = useState('');
41
+ const [channel, setChannel] = useState('');
42
+ const [direction, setDirection] = useState('');
43
+ const [loading, setLoading] = useState(true);
44
+ const [error, setError] = useState<string | null>(null);
45
+
46
+ // Load messages whenever filters / pagination change
47
+ useEffect(() => {
48
+ let isCancelled = false;
49
+
50
+ async function loadData() {
51
+ try {
52
+ setLoading(true);
53
+ setError(null);
54
+
55
+ const response = await $fetchMessages({
56
+ page,
57
+ pageSize,
58
+ search: search || undefined,
59
+ channel: channel || undefined,
60
+ direction: direction || undefined,
61
+ });
62
+
63
+ if (isCancelled) return;
64
+
65
+ setItems(response.items);
66
+ setTotal(response.total);
67
+ } catch (err) {
68
+ if (isCancelled) return;
69
+ setError(err instanceof Error ? err.message : 'Failed to load messages');
70
+ } finally {
71
+ if (!isCancelled) {
72
+ setLoading(false);
73
+ }
74
+ }
75
+ }
76
+
77
+ loadData();
78
+
79
+ return () => {
80
+ isCancelled = true;
81
+ };
82
+ }, [page, pageSize, search, channel, direction]);
83
+
84
+ const totalPages = useMemo(() => {
85
+ if (total <= 0 || pageSize <= 0) return 1;
86
+ return Math.max(1, Math.ceil(total / pageSize));
87
+ }, [total, pageSize]);
88
+
89
+ const handleSearchSubmit = (event: React.FormEvent) => {
90
+ event.preventDefault();
91
+ setPage(1);
92
+ setSearch(searchInput.trim());
93
+ };
94
+
95
+ const handlePageSizeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
96
+ const next = parseInt(event.target.value, 10);
97
+ if (!Number.isNaN(next) && next > 0) {
98
+ setPageSize(next);
99
+ setPage(1);
100
+ }
101
+ };
102
+
103
+ const pagination = (
104
+ <div className="mt-4 flex flex-col items-center justify-between gap-3 border-t border-gray-100 pt-4 text-xs text-gray-600 md:flex-row">
105
+ <div>
106
+ {total > 0 ? (
107
+ <>
108
+ Showing <span className="font-semibold">{Math.min((page - 1) * pageSize + 1, total)}</span> –{' '}
109
+ <span className="font-semibold">{Math.min(page * pageSize, total)}</span> of{' '}
110
+ <span className="font-semibold">{total}</span> messages
111
+ </>
112
+ ) : (
113
+ 'No messages'
114
+ )}
115
+ </div>
116
+ <div className="flex items-center gap-2">
117
+ <button
118
+ type="button"
119
+ onClick={() => setPage((prev) => Math.max(1, prev - 1))}
120
+ disabled={page <= 1}
121
+ className="inline-flex items-center justify-center rounded-md border border-gray-300 px-2 py-1 text-xs font-medium text-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
122
+ >
123
+ Previous
124
+ </button>
125
+ <span>
126
+ Page <span className="font-semibold">{page}</span> of{' '}
127
+ <span className="font-semibold">{totalPages}</span>
128
+ </span>
129
+ <button
130
+ type="button"
131
+ onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
132
+ disabled={page >= totalPages}
133
+ className="inline-flex items-center justify-center rounded-md border border-gray-300 px-2 py-1 text-xs font-medium text-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
134
+ >
135
+ Next
136
+ </button>
137
+ </div>
138
+ </div>
139
+ );
140
+
141
+ return (
142
+ <div className="container mx-auto px-4 py-8 space-y-6">
143
+ <div className="mt-20 mb-4 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
144
+ <div>
145
+ <h1 className="text-3xl text-gray-900 font-light">Messages</h1>
146
+ <p className="mt-1 text-sm text-gray-500">
147
+ Inspect all inbound and outbound messages and their statuses.
148
+ </p>
149
+ </div>
150
+ <div className="flex items-end gap-4 text-sm text-gray-500 md:text-right">
151
+ <div>
152
+ <div className="text-xl font-semibold text-gray-900">{total.toLocaleString()}</div>
153
+ <div className="text-xs uppercase tracking-wide text-gray-400">Total messages</div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ <Card>
158
+ <div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
159
+ <form onSubmit={handleSearchSubmit} className="flex flex-col gap-2 md:flex-row md:items-end">
160
+ <div className="flex flex-col gap-1">
161
+ <label htmlFor="search" className="text-sm font-medium text-gray-700">
162
+ Search
163
+ </label>
164
+ <input
165
+ id="search"
166
+ type="text"
167
+ value={searchInput}
168
+ onChange={(event) => setSearchInput(event.target.value)}
169
+ placeholder="Search in content..."
170
+ className="w-full md:w-72 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
171
+ />
172
+ </div>
173
+ <button
174
+ type="submit"
175
+ className="mt-2 inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 md:mt-0 md:ml-3"
176
+ >
177
+ Apply
178
+ </button>
179
+ </form>
180
+
181
+ <div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-4">
182
+ <div className="flex flex-col gap-1">
183
+ <label htmlFor="channelFilter" className="text-sm font-medium text-gray-700">
184
+ Channel
185
+ </label>
186
+ <select
187
+ id="channelFilter"
188
+ value={channel}
189
+ onChange={(e) => { setChannel(e.target.value); setPage(1); }}
190
+ className="w-full md:w-40 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
191
+ >
192
+ <option value="">All</option>
193
+ <option value="EMAIL">Email</option>
194
+ <option value="PROMPTBOOK_CHAT">Chat</option>
195
+ </select>
196
+ </div>
197
+
198
+ <div className="flex flex-col gap-1">
199
+ <label htmlFor="directionFilter" className="text-sm font-medium text-gray-700">
200
+ Direction
201
+ </label>
202
+ <select
203
+ id="directionFilter"
204
+ value={direction}
205
+ onChange={(e) => { setDirection(e.target.value); setPage(1); }}
206
+ className="w-full md:w-40 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
207
+ >
208
+ <option value="">All</option>
209
+ <option value="INBOUND">Inbound</option>
210
+ <option value="OUTBOUND">Outbound</option>
211
+ </select>
212
+ </div>
213
+
214
+ <div className="flex flex-col gap-1">
215
+ <label htmlFor="pageSize" className="text-sm font-medium text-gray-700">
216
+ Page size
217
+ </label>
218
+ <select
219
+ id="pageSize"
220
+ value={pageSize}
221
+ onChange={handlePageSizeChange}
222
+ className="w-28 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
223
+ >
224
+ <option value={10}>10</option>
225
+ <option value={20}>20</option>
226
+ <option value={50}>50</option>
227
+ <option value={100}>100</option>
228
+ </select>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </Card>
233
+
234
+ <Card>
235
+ <div className="flex items-center justify-between mb-4">
236
+ <h2 className="text-lg font-medium text-gray-900">Message list</h2>
237
+ </div>
238
+ {error && <div className="mb-4 rounded-md bg-red-50 px-4 py-3 text-sm text-red-800">{error}</div>}
239
+
240
+ {loading && items.length === 0 ? (
241
+ <div className="py-8 text-center text-gray-500">Loading messages…</div>
242
+ ) : items.length === 0 ? (
243
+ <div className="py-8 text-center text-gray-500">No messages found.</div>
244
+ ) : (
245
+ <div className="overflow-x-auto">
246
+ <table className="min-w-full divide-y divide-gray-200 text-sm">
247
+ <thead className="bg-gray-50">
248
+ <tr>
249
+ <th className="px-4 py-3 text-left font-medium text-gray-500">Time</th>
250
+ <th className="px-4 py-3 text-left font-medium text-gray-500">Channel</th>
251
+ <th className="px-4 py-3 text-left font-medium text-gray-500">Direction</th>
252
+ <th className="px-4 py-3 text-left font-medium text-gray-500">Sender/Recipients</th>
253
+ <th className="px-4 py-3 text-left font-medium text-gray-500">Content</th>
254
+ <th className="px-4 py-3 text-left font-medium text-gray-500">Status</th>
255
+ </tr>
256
+ </thead>
257
+ <tbody className="divide-y divide-gray-200 bg-white">
258
+ {items.map((row) => (
259
+ <tr key={row.id}>
260
+ <td className="whitespace-nowrap px-4 py-3 text-gray-700">
261
+ {formatDate(row.createdAt)}
262
+ </td>
263
+ <td className="whitespace-nowrap px-4 py-3 text-gray-700">
264
+ {row.channel}
265
+ </td>
266
+ <td className="whitespace-nowrap px-4 py-3 text-gray-700">
267
+ {row.direction}
268
+ </td>
269
+ <td className="max-w-xs px-4 py-3 text-gray-700">
270
+ <div className="truncate" title={JSON.stringify({ sender: row.sender, recipients: row.recipients }, null, 2)}>
271
+ {/* Simple preview of sender/recipients */}
272
+ S: {JSON.stringify(row.sender)}<br/>
273
+ R: {JSON.stringify(row.recipients)}
274
+ </div>
275
+ </td>
276
+ <td className="max-w-md px-4 py-3 text-gray-700">
277
+ <div className="max-h-24 overflow-hidden overflow-ellipsis text-xs leading-snug">
278
+ {getMessagePreview(row.content)}
279
+ </div>
280
+ </td>
281
+ <td className="whitespace-nowrap px-4 py-3 text-gray-700">
282
+ {getStatusBadge(row.sendAttempts)}
283
+ </td>
284
+ </tr>
285
+ ))}
286
+ </tbody>
287
+ </table>
288
+ </div>
289
+ )}
290
+ {pagination}
291
+ </Card>
292
+ </div>
293
+ );
294
+ }
@@ -0,0 +1,13 @@
1
+ import { ForbiddenPage } from '../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
3
+ import { MessagesClient } from './MessagesClient';
4
+
5
+ export default async function AdminMessagesPage() {
6
+ const isAdmin = await isUserAdmin();
7
+
8
+ if (!isAdmin) {
9
+ return <ForbiddenPage />;
10
+ }
11
+
12
+ return <MessagesClient />;
13
+ }
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { assertsError } from '../../../../../../../src/errors/assertsError';
5
+ import { sendEmailAction } from './actions';
6
+
7
+ export function SendEmailClient() {
8
+ const [status, setStatus] = useState<'IDLE' | 'LOADING' | 'SUCCESS' | 'ERROR'>('IDLE');
9
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
10
+
11
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
12
+ event.preventDefault();
13
+ setStatus('LOADING');
14
+ setErrorMessage(null);
15
+
16
+ const formData = new FormData(event.currentTarget);
17
+
18
+ try {
19
+ await sendEmailAction(formData);
20
+ setStatus('SUCCESS');
21
+ } catch (error) {
22
+ assertsError(error);
23
+ console.error(error);
24
+ setStatus('ERROR');
25
+ setErrorMessage(error.message);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <div className="container mx-auto p-4">
31
+ <h1 className="text-2xl font-bold mb-4">Send Email (Test)</h1>
32
+
33
+ {status === 'SUCCESS' && (
34
+ <div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
35
+ Email sent successfully!
36
+ </div>
37
+ )}
38
+
39
+ {status === 'ERROR' && (
40
+ <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
41
+ Error: {errorMessage}
42
+ </div>
43
+ )}
44
+
45
+ <form onSubmit={handleSubmit} className="space-y-4 max-w-lg">
46
+ <div>
47
+ <label className="block text-sm font-medium text-gray-700">From</label>
48
+ <input
49
+ name="from"
50
+ type="text"
51
+ defaultValue="Test Promptbook <test@ptbk.io>"
52
+ required
53
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
54
+ />
55
+ </div>
56
+
57
+ <div>
58
+ <label className="block text-sm font-medium text-gray-700">To</label>
59
+ <input
60
+ name="to"
61
+ type="text"
62
+ placeholder="recipient@example.com"
63
+ defaultValue="Pavol Hejný <pavol@ptbk.io>"
64
+ required
65
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
66
+ />
67
+ <p className="text-xs text-gray-500 mt-1">Separate multiple addresses with commas</p>
68
+ </div>
69
+
70
+ <div>
71
+ <label className="block text-sm font-medium text-gray-700">Subject</label>
72
+ <input
73
+ name="subject"
74
+ type="text"
75
+ placeholder="Test Email"
76
+ defaultValue="Test Email"
77
+ required
78
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
79
+ />
80
+ </div>
81
+
82
+ <div>
83
+ <label className="block text-sm font-medium text-gray-700">Body</label>
84
+ <textarea
85
+ name="body"
86
+ rows={6}
87
+ placeholder="Hello, this is a test email."
88
+ defaultValue="Hello, this is a test email."
89
+ required
90
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
91
+ />
92
+ </div>
93
+
94
+ <button
95
+ type="submit"
96
+ disabled={status === 'LOADING'}
97
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
98
+ >
99
+ {status === 'LOADING' ? 'Sending...' : 'Send Email'}
100
+ </button>
101
+ </form>
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,35 @@
1
+ 'use server';
2
+
3
+ import type { string_email, string_emails, string_html } from '@promptbook-local/types';
4
+ import { parseEmailAddress } from '../../../../message-providers/email/_common/utils/parseEmailAddress';
5
+ import { parseEmailAddresses } from '../../../../message-providers/email/_common/utils/parseEmailAddresses';
6
+ import { stringifyEmailAddress } from '../../../../message-providers/email/_common/utils/stringifyEmailAddress';
7
+ import { sendMessage } from '../../../../utils/messages/sendMessage';
8
+
9
+ export async function sendEmailAction(formData: FormData) {
10
+ const from = formData.get('from') as string;
11
+ const to = formData.get('to') as string;
12
+ const subject = formData.get('subject') as string;
13
+ const body = formData.get('body') as string;
14
+
15
+ if (!from || !to || !subject || !body) {
16
+ throw new Error('All fields are required');
17
+ }
18
+
19
+ const sender = stringifyEmailAddress(parseEmailAddress(from as string_email));
20
+ const recipients = parseEmailAddresses(to as string_emails).map(stringifyEmailAddress);
21
+
22
+ await sendMessage({
23
+ channel: 'EMAIL',
24
+ direction: 'OUTBOUND',
25
+ sender,
26
+ recipients,
27
+ subject,
28
+ content: body as string_html,
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ threadId: crypto.randomUUID() as any,
31
+ cc: [],
32
+ attachments: [],
33
+ metadata: {},
34
+ });
35
+ }
@@ -0,0 +1,13 @@
1
+ import { ForbiddenPage } from '../../../../components/ForbiddenPage/ForbiddenPage';
2
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
3
+ import { SendEmailClient } from './SendEmailClient';
4
+
5
+ export default async function AdminSendEmailPage() {
6
+ const isAdmin = await isUserAdmin();
7
+
8
+ if (!isAdmin) {
9
+ return <ForbiddenPage />;
10
+ }
11
+
12
+ return <SendEmailClient />;
13
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { FileText, Hash, Image, Shield, ToggleLeft, Type, Upload } from 'lucide-react';
3
+ import { upload } from '@vercel/blob/client';
4
+ import { FileTextIcon, HashIcon, ImageIcon, ShieldIcon, ToggleLeftIcon, TypeIcon, Upload } from 'lucide-react';
4
5
  import { useEffect, useRef, useState } from 'react';
5
6
  import { metadataDefaults, MetadataType } from '../../../database/metadataDefaults';
6
7
 
@@ -148,19 +149,19 @@ export function MetadataClient() {
148
149
  const getTypeIcon = (type?: MetadataType) => {
149
150
  switch (type) {
150
151
  case 'TEXT_SINGLE_LINE':
151
- return <Type className="w-4 h-4" />;
152
+ return <TypeIcon className="w-4 h-4" />;
152
153
  case 'TEXT':
153
- return <FileText className="w-4 h-4" />;
154
+ return <FileTextIcon className="w-4 h-4" />;
154
155
  case 'NUMBER':
155
- return <Hash className="w-4 h-4" />;
156
+ return <HashIcon className="w-4 h-4" />;
156
157
  case 'BOOLEAN':
157
- return <ToggleLeft className="w-4 h-4" />;
158
+ return <ToggleLeftIcon className="w-4 h-4" />;
158
159
  case 'IMAGE_URL':
159
- return <Image className="w-4 h-4" />;
160
+ return <ImageIcon className="w-4 h-4" />;
160
161
  case 'IP_RANGE':
161
- return <Shield className="w-4 h-4" />;
162
+ return <ShieldIcon className="w-4 h-4" />;
162
163
  default:
163
- return <Type className="w-4 h-4" />;
164
+ return <TypeIcon className="w-4 h-4" />;
164
165
  }
165
166
  };
166
167
 
@@ -185,26 +186,29 @@ export function MetadataClient() {
185
186
 
186
187
  try {
187
188
  setIsUploading(true);
188
- const formData = new FormData();
189
- formData.append('file', file);
190
189
 
191
- const response = await fetch('/api/upload', {
192
- method: 'POST',
193
- body: formData,
190
+ // Build the full path including prefix and user/files directory
191
+ const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
192
+ const uploadPath = pathPrefix ? `${pathPrefix}/user/files/${file.name}` : `user/files/${file.name}`;
193
+
194
+ // Upload directly to Vercel Blob using client upload
195
+ const blob = await upload(uploadPath, file, {
196
+ access: 'public',
197
+ handleUploadUrl: '/api/upload',
198
+ clientPayload: JSON.stringify({
199
+ purpose: formState.key || 'METADATA_IMAGE',
200
+ contentType: file.type,
201
+ }),
194
202
  });
195
203
 
196
- if (!response.ok) {
197
- throw new Error(`Failed to upload file: ${response.statusText}`);
198
- }
199
-
200
- const { fileUrl: longFileUrl } = await response.json();
204
+ const fileUrl = blob.url;
201
205
 
202
206
  const LONG_URL = `${process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!}/${process.env
203
207
  .NEXT_PUBLIC_CDN_PATH_PREFIX!}/user/files/`;
204
208
  const SHORT_URL = `https://ptbk.io/k/`;
205
209
  // <- TODO: [🌍] Unite this logic in one place
206
210
 
207
- const shortFileUrl = longFileUrl.split(LONG_URL).join(SHORT_URL);
211
+ const shortFileUrl = fileUrl.split(LONG_URL).join(SHORT_URL);
208
212
  setFormState((prev) => ({ ...prev, value: shortFileUrl }));
209
213
  } catch (err) {
210
214
  setError(err instanceof Error ? err.message : 'Failed to upload image');
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Card } from '../../../components/Homepage/Card';
5
+ import { SearchResult } from '../../../../../../src/search-engines/SearchResult';
6
+ import { search } from './actions';
7
+
8
+ export function SearchEngineTestClient() {
9
+ const [query, setQuery] = useState('');
10
+ const [provider, setProvider] = useState('dummy');
11
+ const [results, setResults] = useState<SearchResult[] | null>(null);
12
+ const [isLoading, setIsLoading] = useState(false);
13
+ const [error, setError] = useState<string | null>(null);
14
+
15
+ const handleSearch = async () => {
16
+ if (!query) return;
17
+
18
+ setIsLoading(true);
19
+ setError(null);
20
+ setResults(null);
21
+
22
+ try {
23
+ const results = await search(query, provider);
24
+ setResults(results);
25
+ } catch (err) {
26
+ setError(String(err));
27
+ } finally {
28
+ setIsLoading(false);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="container mx-auto px-4 py-8 space-y-6">
34
+ <div className="mt-20 mb-4 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
35
+ <div>
36
+ <h1 className="text-3xl text-gray-900 font-light">Search Engine Test</h1>
37
+ <p className="mt-1 text-sm text-gray-500">
38
+ Test the search engine capabilities by providing a query.
39
+ </p>
40
+ </div>
41
+ </div>
42
+
43
+ <Card>
44
+ <div className="mb-4 space-y-4">
45
+ <div className="space-y-2">
46
+ <label className="block text-sm font-medium text-gray-700">Query</label>
47
+ <input
48
+ type="text"
49
+ value={query}
50
+ onChange={(e) => setQuery(e.target.value)}
51
+ placeholder="e.g., Cat"
52
+ className="w-full p-2 border border-gray-300 rounded"
53
+ disabled={isLoading}
54
+ onKeyDown={(e) => {
55
+ if (e.key === 'Enter') {
56
+ handleSearch();
57
+ }
58
+ }}
59
+ />
60
+ </div>
61
+ <div className="space-y-2">
62
+ <label className="block text-sm font-medium text-gray-700">Provider</label>
63
+ <select
64
+ value={provider}
65
+ onChange={(e) => setProvider(e.target.value)}
66
+ className="w-full p-2 border border-gray-300 rounded"
67
+ disabled={isLoading}
68
+ >
69
+ <option value="dummy">Dummy</option>
70
+ <option value="bing">Bing</option>
71
+ </select>
72
+ </div>
73
+
74
+ <div className="flex justify-end">
75
+ <button
76
+ onClick={handleSearch}
77
+ disabled={isLoading || !query}
78
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
79
+ >
80
+ {isLoading ? 'Searching...' : 'Search'}
81
+ </button>
82
+ </div>
83
+ </div>
84
+
85
+ {error && (
86
+ <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
87
+ <strong className="font-bold">Error: </strong>
88
+ <span className="block sm:inline">{error}</span>
89
+ </div>
90
+ )}
91
+
92
+ {results && (
93
+ <div className="space-y-4">
94
+ <h2 className="text-xl font-semibold">Results</h2>
95
+ {results.map((result, index) => (
96
+ <div key={index} className="border p-4 rounded bg-gray-50">
97
+ <a href={result.url} target="_blank" rel="noopener noreferrer" className="text-blue-600 font-bold hover:underline">
98
+ {result.title}
99
+ </a>
100
+ <div className="text-sm text-green-700">{result.url}</div>
101
+ <p className="text-gray-700">{result.snippet}</p>
102
+ </div>
103
+ ))}
104
+ </div>
105
+ )}
106
+ </Card>
107
+ </div>
108
+ );
109
+ }