@promptbook/cli 0.104.0-1 → 0.104.0-10

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 (199) 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/messages/MessagesClient.tsx +294 -0
  15. package/apps/agents-server/src/app/admin/messages/page.tsx +13 -0
  16. package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +104 -0
  17. package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +35 -0
  18. package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +13 -0
  19. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +23 -19
  20. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +15 -1
  21. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +51 -9
  22. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +47 -4
  23. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +53 -11
  24. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +23 -3
  25. package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +8 -8
  26. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +17 -26
  27. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +4 -2
  28. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +20 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +6 -11
  30. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +5 -1
  31. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +5 -2
  32. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +20 -16
  33. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +15 -2
  34. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +15 -2
  35. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +12 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/code/api/route.ts +68 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/code/page.tsx +223 -0
  38. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +5 -0
  39. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +2 -2
  40. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +10 -3
  41. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/getAgentDefaultAvatarPrompt.ts +31 -0
  42. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +194 -0
  43. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +14 -2
  44. package/apps/agents-server/src/app/agents/[agentName]/images/page.tsx +200 -0
  45. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +4 -3
  46. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +4 -3
  47. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +10 -3
  48. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +11 -4
  49. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +11 -2
  50. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +18 -10
  51. package/apps/agents-server/src/app/agents/[agentName]/system-message/page.tsx +100 -0
  52. package/apps/agents-server/src/app/api/admin-email/route.ts +12 -0
  53. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +13 -14
  54. package/apps/agents-server/src/app/api/agents/[agentName]/restore/route.ts +20 -0
  55. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +43 -1
  56. package/apps/agents-server/src/app/api/agents/route.ts +28 -3
  57. package/apps/agents-server/src/app/api/api-tokens/route.ts +6 -7
  58. package/apps/agents-server/src/app/api/browser-test/act/route.ts +141 -0
  59. package/apps/agents-server/src/app/api/browser-test/screenshot/route.ts +30 -0
  60. package/apps/agents-server/src/app/api/browser-test/scroll-facebook/route.ts +62 -0
  61. package/apps/agents-server/src/app/api/docs/book.md/route.ts +61 -0
  62. package/apps/agents-server/src/app/api/emails/incoming/sendgrid/route.ts +48 -0
  63. package/apps/agents-server/src/app/api/federated-agents/route.ts +12 -0
  64. package/apps/agents-server/src/app/api/images/[filename]/route.ts +107 -0
  65. package/apps/agents-server/src/app/api/messages/route.ts +102 -0
  66. package/apps/agents-server/src/app/api/metadata/route.ts +5 -6
  67. package/apps/agents-server/src/app/api/upload/route.ts +128 -45
  68. package/apps/agents-server/src/app/docs/[docId]/page.tsx +2 -3
  69. package/apps/agents-server/src/app/docs/page.tsx +12 -12
  70. package/apps/agents-server/src/app/globals.css +140 -33
  71. package/apps/agents-server/src/app/humans.txt/route.ts +1 -1
  72. package/apps/agents-server/src/app/layout.tsx +27 -22
  73. package/apps/agents-server/src/app/page.tsx +54 -6
  74. package/apps/agents-server/src/app/recycle-bin/actions.ts +20 -14
  75. package/apps/agents-server/src/app/recycle-bin/page.tsx +27 -41
  76. package/apps/agents-server/src/app/robots.txt/route.ts +1 -1
  77. package/apps/agents-server/src/app/security.txt/route.ts +1 -1
  78. package/apps/agents-server/src/app/sitemap.xml/route.ts +9 -7
  79. package/apps/agents-server/src/app/swagger/page.tsx +14 -0
  80. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +41 -116
  81. package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +92 -0
  82. package/apps/agents-server/src/components/AgentProfile/QrCodeModal.tsx +0 -1
  83. package/apps/agents-server/src/components/AgentProfile/useAgentBackground.ts +97 -0
  84. package/apps/agents-server/src/components/Auth/AuthControls.tsx +5 -4
  85. package/apps/agents-server/src/components/DeletedAgentBanner.tsx +26 -0
  86. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +38 -0
  87. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +11 -9
  88. package/apps/agents-server/src/components/Footer/Footer.tsx +5 -5
  89. package/apps/agents-server/src/components/ForgottenPasswordDialog/ForgottenPasswordDialog.tsx +61 -0
  90. package/apps/agents-server/src/components/Header/Header.tsx +114 -40
  91. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +145 -23
  92. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +93 -15
  93. package/apps/agents-server/src/components/Homepage/DeletedAgentsList.tsx +66 -0
  94. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +12 -3
  95. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +19 -10
  96. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +3 -2
  97. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +50 -1
  98. package/apps/agents-server/src/components/NewAgentDialog/NewAgentDialog.tsx +88 -0
  99. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +7 -2
  100. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +16 -7
  101. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +4 -4
  102. package/apps/agents-server/src/components/RegisterUserDialog/RegisterUserDialog.tsx +61 -0
  103. package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +2 -0
  104. package/apps/agents-server/src/components/_utils/generateMetaTxt.ts +12 -10
  105. package/apps/agents-server/src/components/_utils/headlessParam.tsx +7 -3
  106. package/apps/agents-server/src/database/$provideSupabaseForBrowser.ts +3 -3
  107. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -1
  108. package/apps/agents-server/src/database/$provideSupabaseForWorker.ts +3 -3
  109. package/apps/agents-server/src/database/metadataDefaults.ts +19 -1
  110. package/apps/agents-server/src/database/migrate.ts +34 -1
  111. package/apps/agents-server/src/database/migrations/2025-11-0001-initial-schema.sql +1 -3
  112. package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
  113. package/apps/agents-server/src/database/migrations/2025-12-0240-agent-public-id.sql +3 -0
  114. package/apps/agents-server/src/database/migrations/2025-12-0360-agent-deleted-at.sql +1 -0
  115. package/apps/agents-server/src/database/migrations/2025-12-0370-image-table.sql +19 -0
  116. package/apps/agents-server/src/database/migrations/2025-12-0380-agent-visibility.sql +1 -0
  117. package/apps/agents-server/src/database/migrations/2025-12-0390-upload-tracking.sql +20 -0
  118. package/apps/agents-server/src/database/migrations/2025-12-0401-file-upload-status.sql +13 -0
  119. package/apps/agents-server/src/database/migrations/2025-12-0402-message-table.sql +42 -0
  120. package/apps/agents-server/src/database/migrations/2025-12-0403-generation-lock-table.sql +15 -0
  121. package/apps/agents-server/src/database/migrations/2025-12-0640-openai-assistant-cache.sql +12 -0
  122. package/apps/agents-server/src/database/migrations/2025-12-0820-agent-history-permanent-id.sql +29 -0
  123. package/apps/agents-server/src/database/schema.ts +231 -4
  124. package/apps/agents-server/src/generated/reservedPaths.ts +32 -0
  125. package/apps/agents-server/src/message-providers/email/_common/Email.ts +73 -0
  126. package/apps/agents-server/src/message-providers/email/_common/utils/TODO.txt +1 -0
  127. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo +108 -0
  128. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.ts +62 -0
  129. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.test.ts.todo +117 -0
  130. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.ts +19 -0
  131. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.test.ts.todo +119 -0
  132. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.ts +19 -0
  133. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.test.ts.todo +74 -0
  134. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts +14 -0
  135. package/apps/agents-server/src/message-providers/email/sendgrid/SendgridMessageProvider.ts +44 -0
  136. package/apps/agents-server/src/message-providers/email/sendgrid/parseInboundSendgridEmail.ts +49 -0
  137. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +51 -0
  138. package/apps/agents-server/src/message-providers/index.ts +13 -0
  139. package/apps/agents-server/src/message-providers/interfaces/MessageProvider.ts +11 -0
  140. package/apps/agents-server/src/middleware.ts +19 -23
  141. package/apps/agents-server/src/tools/$provideBrowserForServer.ts +32 -0
  142. package/apps/agents-server/src/tools/$provideCdnForServer.ts +7 -2
  143. package/apps/agents-server/src/utils/auth.ts +117 -17
  144. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +57 -0
  145. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +4 -0
  146. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +18 -0
  147. package/apps/agents-server/src/utils/content/extractBodyContentFromHtml.ts +19 -0
  148. package/apps/agents-server/src/utils/getUserIdFromRequest.ts +35 -0
  149. package/apps/agents-server/src/utils/handleChatCompletion.ts +65 -5
  150. package/apps/agents-server/src/utils/messages/sendMessage.ts +91 -0
  151. package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
  152. package/apps/agents-server/src/utils/normalization/filenameToPrompt.test.ts +36 -0
  153. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +25 -0
  154. package/apps/agents-server/src/utils/validateApiKey.ts +7 -11
  155. package/esm/index.es.js +2890 -2737
  156. package/esm/index.es.js.map +1 -1
  157. package/esm/typings/servers.d.ts +8 -0
  158. package/esm/typings/src/_packages/core.index.d.ts +2 -0
  159. package/esm/typings/src/_packages/types.index.d.ts +10 -2
  160. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +6 -1
  161. package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirements.d.ts +6 -6
  162. package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.closed.test.d.ts +1 -0
  163. package/esm/typings/src/book-2.0/utils/generatePlaceholderAgentProfileImageUrl.d.ts +3 -3
  164. package/esm/typings/src/book-components/Chat/Chat/ChatMessageItem.d.ts +5 -1
  165. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  166. package/esm/typings/src/book-components/Chat/CodeBlock/CodeBlock.d.ts +13 -0
  167. package/esm/typings/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  168. package/esm/typings/src/book-components/Chat/types/ChatMessage.d.ts +7 -11
  169. package/esm/typings/src/book-components/_common/Dropdown/Dropdown.d.ts +2 -2
  170. package/esm/typings/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +56 -0
  171. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +21 -11
  172. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +80 -14
  173. package/esm/typings/src/commitments/DICTIONARY/DICTIONARY.d.ts +46 -0
  174. package/esm/typings/src/commitments/index.d.ts +2 -1
  175. package/esm/typings/src/llm-providers/_multiple/MultipleLlmExecutionTools.d.ts +6 -2
  176. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +1 -1
  177. package/esm/typings/src/llm-providers/ollama/OllamaExecutionTools.d.ts +1 -1
  178. package/esm/typings/src/llm-providers/openai/createOpenAiCompatibleExecutionTools.d.ts +1 -1
  179. package/esm/typings/src/llm-providers/remote/RemoteLlmExecutionTools.d.ts +1 -0
  180. package/esm/typings/src/types/Message.d.ts +49 -0
  181. package/esm/typings/src/types/ModelRequirements.d.ts +38 -14
  182. package/esm/typings/src/types/typeAliases.d.ts +23 -1
  183. package/esm/typings/src/utils/color/utils/colorToDataUrl.d.ts +2 -1
  184. package/esm/typings/src/utils/environment/$detectRuntimeEnvironment.d.ts +4 -4
  185. package/esm/typings/src/utils/environment/$isRunningInBrowser.d.ts +1 -1
  186. package/esm/typings/src/utils/environment/$isRunningInJest.d.ts +1 -1
  187. package/esm/typings/src/utils/environment/$isRunningInNode.d.ts +1 -1
  188. package/esm/typings/src/utils/environment/$isRunningInWebWorker.d.ts +1 -1
  189. package/esm/typings/src/utils/markdown/extractAllBlocksFromMarkdown.d.ts +2 -2
  190. package/esm/typings/src/utils/markdown/extractOneBlockFromMarkdown.d.ts +2 -2
  191. package/esm/typings/src/utils/random/$randomBase58.d.ts +12 -0
  192. package/esm/typings/src/version.d.ts +1 -1
  193. package/package.json +1 -1
  194. package/umd/index.umd.js +4018 -3865
  195. package/umd/index.umd.js.map +1 -1
  196. package/apps/agents-server/package-lock.json +0 -27
  197. package/apps/agents-server/public/fonts/download-font.js +0 -22
  198. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +0 -18
  199. package/esm/typings/src/book-2.0/utils/generateGravatarUrl.d.ts +0 -10
@@ -0,0 +1,102 @@
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
+ function parsePositiveInt(value: string | null, fallback: number): number {
10
+ if (!value) return fallback;
11
+ const parsed = parseInt(value, 10);
12
+ if (Number.isNaN(parsed) || parsed <= 0) return fallback;
13
+ return parsed;
14
+ }
15
+
16
+ /**
17
+ * List messages with filters, search and pagination.
18
+ */
19
+ export async function GET(request: NextRequest) {
20
+ if (!(await isUserAdmin())) {
21
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
22
+ }
23
+
24
+ try {
25
+ const searchParams = request.nextUrl.searchParams;
26
+
27
+ const page = parsePositiveInt(searchParams.get('page'), 1);
28
+ const pageSize = Math.min(MAX_PAGE_SIZE, parsePositiveInt(searchParams.get('pageSize'), DEFAULT_PAGE_SIZE));
29
+ const search = searchParams.get('search')?.trim() || '';
30
+ const channel = searchParams.get('channel');
31
+ const direction = searchParams.get('direction');
32
+
33
+ const supabase = $provideSupabase();
34
+
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ let query = supabase.from(await $getTableName('Message')).select('*', { count: 'exact' });
37
+
38
+ if (channel) {
39
+ query = query.eq('channel', channel);
40
+ }
41
+
42
+ if (direction) {
43
+ query = query.eq('direction', direction);
44
+ }
45
+
46
+ if (search) {
47
+ // Search in content, subject (if in metadata?), sender/recipient emails
48
+ // Note: sender and recipients are JSONB, so ilike might not work directly on them unless cast to text
49
+ // Content is TEXT.
50
+ const escaped = search.replace(/%/g, '\\%').replace(/_/g, '\\_');
51
+ // Assuming simple search on content for now to avoid complexity with JSONB search in generic supabase client
52
+ query = query.ilike('content', `%${escaped}%`);
53
+ }
54
+
55
+ // Default sort by createdAt desc
56
+ query = query.order('createdAt', { ascending: false });
57
+
58
+ const from = (page - 1) * pageSize;
59
+ const to = from + pageSize - 1;
60
+
61
+ query = query.range(from, to);
62
+
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ const { data: messages, error, count } = (await query) as { data: any[]; error: any; count: number };
65
+
66
+ if (error) {
67
+ console.error('List messages error:', error);
68
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
69
+ }
70
+
71
+ // Fetch attempts for these messages
72
+ if (messages && messages.length > 0) {
73
+ const messageIds = messages.map((m) => m.id);
74
+ const { data: attempts, error: attemptsError } = await supabase
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ .from(await $getTableName('MessageSendAttempt'))
77
+ .select('*')
78
+ .in('messageId', messageIds);
79
+
80
+ if (attemptsError) {
81
+ console.error('Fetch message attempts error:', attemptsError);
82
+ // We don't fail the whole request, just log it.
83
+ } else {
84
+ // Attach attempts to messages
85
+ for (const message of messages) {
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ (message as any).sendAttempts = attempts?.filter((a: any) => a.messageId === message.id) || [];
88
+ }
89
+ }
90
+ }
91
+
92
+ return NextResponse.json({
93
+ items: messages ?? [],
94
+ total: count ?? 0,
95
+ page,
96
+ pageSize,
97
+ });
98
+ } catch (error) {
99
+ console.error('List messages error:', error);
100
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
101
+ }
102
+ }
@@ -1,9 +1,12 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { keepUnused } from '../../../../../../src/utils/organization/keepUnused';
1
3
  import { $getTableName } from '../../../database/$getTableName';
2
4
  import { $provideSupabase } from '../../../database/$provideSupabase';
3
5
  import { isUserAdmin } from '../../../utils/isUserAdmin';
4
- import { NextRequest, NextResponse } from 'next/server';
5
6
 
6
7
  export async function GET(request: NextRequest) {
8
+ keepUnused(request);
9
+
7
10
  if (!(await isUserAdmin())) {
8
11
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
9
12
  }
@@ -36,11 +39,7 @@ export async function POST(request: NextRequest) {
36
39
  const supabase = $provideSupabase();
37
40
  const table = await $getTableName('Metadata');
38
41
 
39
- const { data, error } = await supabase
40
- .from(table)
41
- .insert({ key, value, note })
42
- .select()
43
- .single();
42
+ const { data, error } = await supabase.from(table).insert({ key, value, note }).select().single();
44
43
 
45
44
  if (error) {
46
45
  return NextResponse.json({ error: error.message }, { status: 500 });
@@ -1,70 +1,144 @@
1
- import { nextRequestToNodeRequest } from '@/src/utils/cdn/utils/nextRequestToNodeRequest';
2
- import { TODO_any } from '@promptbook-local/types';
1
+ import { $getTableName } from '@/src/database/$getTableName';
2
+ import { $provideSupabase } from '@/src/database/$provideSupabase';
3
3
  import { serializeError } from '@promptbook-local/utils';
4
- import formidable from 'formidable';
5
- import { readFile } from 'fs/promises';
4
+ import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
6
5
  import { NextRequest, NextResponse } from 'next/server';
7
- import { forTime } from 'waitasecond';
8
6
  import { assertsError } from '../../../../../../src/errors/assertsError';
9
- import { string_url } from '../../../../../../src/types/typeAliases';
10
- import { keepUnused } from '../../../../../../src/utils/organization/keepUnused';
11
- import { $provideCdnForServer } from '../../../../src/tools/$provideCdnForServer';
12
- import { getUserFileCdnKey } from '../../../../src/utils/cdn/utils/getUserFileCdnKey';
13
- import { validateMimeType } from '../../../../src/utils/validators/validateMimeType';
7
+ import { getUserIdFromRequest } from '../../../../src/utils/getUserIdFromRequest';
14
8
  import { getMetadata } from '../../../database/getMetadata';
15
9
 
16
10
  export async function POST(request: NextRequest) {
17
11
  try {
18
- await forTime(1);
19
- // await forTime(5000);
12
+ const body = (await request.json()) as HandleUploadBody;
13
+ const userId = await getUserIdFromRequest(request);
14
+ const supabase = $provideSupabase();
20
15
 
21
- const nodeRequest = await nextRequestToNodeRequest(request);
22
- let maxFileSizeMb = Number((await getMetadata('MAX_FILE_UPLOAD_SIZE_MB')) || '50'); // <- TODO: [🌲] To /config.ts
16
+ // Handle Vercel Blob client upload protocol
17
+ const jsonResponse = await handleUpload({
18
+ body,
19
+ request,
20
+ token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
21
+ onBeforeGenerateToken: async (pathname, clientPayload) => {
22
+ // Authenticate user and validate upload
23
23
 
24
- if (Number.isNaN(maxFileSizeMb)) {
25
- maxFileSizeMb = 50; // <- TODO: [🌲] To /config.ts
26
- }
24
+ // Parse client payload for additional metadata
25
+ const payload = clientPayload ? JSON.parse(clientPayload) : {};
26
+ const { purpose, contentType } = payload;
27
27
 
28
- const maxFileSize = maxFileSizeMb * 1024 * 1024;
28
+ let maxFileSizeMb = Number((await getMetadata('MAX_FILE_UPLOAD_SIZE_MB')) || '50'); // <- TODO: [🌲] To /config.ts
29
+ if (Number.isNaN(maxFileSizeMb)) {
30
+ maxFileSizeMb = 50; // <- TODO: [🌲] To /config.ts
31
+ }
32
+ const maxFileSize = maxFileSizeMb * 1024 * 1024;
33
+
34
+ // Generate the proper path with prefix
35
+ // Note: With client uploads, we use the original filename provided by the client
36
+ // The file will be stored at: {pathPrefix}/user/files/{filename}
37
+ const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
29
38
 
30
- const files = await new Promise<formidable.Files>((resolve, reject) => {
31
- const form = formidable({ maxFileSize });
32
- form.parse(nodeRequest as TODO_any, (error, fields, files) => {
33
- keepUnused(fields);
39
+ // Create a DB record at the start of the upload to track it
40
+ const uploadPurpose = purpose || 'GENERIC_UPLOAD';
41
+ const { data: insertedFile, error: insertError } = await supabase
42
+ .from(await $getTableName('File'))
43
+ .insert({
44
+ userId: userId || null,
45
+ fileName: pathname,
46
+ fileSize: 0, // <- Will be updated when upload completes
47
+ fileType: contentType || 'application/octet-stream',
48
+ storageUrl: null, // <- To be updated on completion
49
+ shortUrl: null, // <- To be updated on completion
50
+ purpose: uploadPurpose,
51
+ status: 'UPLOADING',
52
+ })
53
+ .select('id')
54
+ .single();
34
55
 
35
- if (error) {
36
- return reject(error);
56
+ if (insertError) {
57
+ console.error('🔼 Failed to create file record:', insertError);
37
58
  }
38
- resolve(files);
39
- });
40
- });
41
59
 
42
- const uploadedFiles = files.file;
60
+ console.info('🔼 Upload started, tracking file:', {
61
+ pathname,
62
+ fileId: insertedFile?.id,
63
+ purpose: uploadPurpose,
64
+ });
43
65
 
44
- if (!uploadedFiles || uploadedFiles.length !== 1) {
45
- return NextResponse.json(
46
- { message: 'In form data there is not EXACTLY one "file" field' },
47
- { status: 400 },
48
- );
49
- }
66
+ return {
67
+ allowedContentTypes: contentType ? [contentType] : undefined,
68
+ maximumSizeInBytes: maxFileSize,
69
+ addRandomSuffix: true, // Add random suffix to avoid filename collisions since we can't hash content
70
+ tokenPayload: JSON.stringify({
71
+ userId: userId || null,
72
+ purpose: uploadPurpose,
73
+ fileId: insertedFile?.id || null,
74
+ uploadPath: pathname,
75
+ pathPrefix,
76
+ }),
77
+ };
78
+ },
79
+ onUploadCompleted: async ({ blob, tokenPayload }) => {
80
+ // !!!!
81
+ // ⚠️ IMPORTANT: This callback is a WEBHOOK called by Vercel's servers AFTER the upload completes
82
+ // - It runs in a DIFFERENT request context (not the original user request)
83
+ // - It WON'T work in local development (Vercel can't reach localhost)
84
+ // - All data must come from tokenPayload (userId, fileId, etc.)
85
+ // - Need to create a fresh supabase client here
86
+ console.info('🔼 Upload completed (webhook callback):', { blob, tokenPayload });
50
87
 
51
- const uploadedFile = uploadedFiles[0]!;
52
- const fileBuffer = await readFile(uploadedFile.filepath);
53
- const cdn = $provideCdnForServer();
54
- const key = getUserFileCdnKey(fileBuffer, uploadedFile.originalFilename || uploadedFile.newFilename);
88
+ try {
89
+ const payload = tokenPayload ? JSON.parse(tokenPayload) : {};
90
+ const { fileId, userId: tokenUserId, purpose: tokenPurpose, uploadPath } = payload;
55
91
 
56
- await cdn.setItem(key, {
57
- type: validateMimeType(uploadedFile.mimetype),
58
- data: fileBuffer,
59
- });
92
+ // Create fresh supabase client for this webhook context
93
+ const supabase = $provideSupabase();
94
+
95
+ if (fileId) {
96
+ // Update the existing record by ID
97
+ const { error: updateError } = await supabase
98
+ .from(await $getTableName('File'))
99
+ .update({
100
+ userId: tokenUserId || null,
101
+ fileSize: 0, // <- !!!!
102
+ fileType: blob.contentType,
103
+ storageUrl: blob.url,
104
+ // <- TODO: !!!! Split between storageUrl and shortUrl
105
+ purpose: tokenPurpose || 'GENERIC_UPLOAD',
106
+ status: 'COMPLETED',
107
+ })
108
+ .eq('id', fileId);
60
109
 
61
- const fileUrl = cdn.getItemUrl(key);
110
+ if (updateError) {
111
+ console.error('🔼 Failed to update file record:', updateError);
112
+ } else {
113
+ console.info('🔼 File record updated successfully:', { fileId, shortUrl: blob.url });
114
+ }
115
+ } else if (uploadPath) {
116
+ // Fallback: Update by uploadPath if fileId is not available
117
+ const { error: updateError } = await supabase
118
+ .from(await $getTableName('File'))
119
+ .update({
120
+ fileSize: 0, // <- !!!!
121
+ fileType: blob.contentType,
122
+ storageUrl: blob.url,
123
+ status: 'COMPLETED',
124
+ })
125
+ .eq('id', fileId);
62
126
 
63
- return NextResponse.json({ fileUrl: fileUrl.href as string_url }, { status: 201 });
127
+ if (updateError) {
128
+ console.error('🔼 Failed to update file record by uploadPath:', updateError);
129
+ }
130
+ }
131
+ } catch (error) {
132
+ console.error('🔼 Error in onUploadCompleted:', error);
133
+ }
134
+ },
135
+ });
136
+
137
+ return NextResponse.json(jsonResponse);
64
138
  } catch (error) {
65
139
  assertsError(error);
66
140
 
67
- console.error(error);
141
+ console.error('🔼', error);
68
142
 
69
143
  return new Response(
70
144
  JSON.stringify(
@@ -81,3 +155,12 @@ export async function POST(request: NextRequest) {
81
155
  );
82
156
  }
83
157
  }
158
+
159
+ /**
160
+ * TODO: !!!! Change uploaded URLs from `storageUrl` to `shortUrl`
161
+ * TODO: !!!! Record both `storageUrl` (actual storage location) and `shortUrl` in `File` table
162
+ * TODO: !!!! Record `purpose` in `File` table
163
+ * TODO: !!!! Record `userId` in `File` table
164
+ * TODO: !!!! Record all things into `File` table
165
+ * TODO: !!!! File type (mime type) of `.book` files should be `application/book` <- [🧠] !!!! Best mime type?!
166
+ */
@@ -1,7 +1,7 @@
1
1
  import { notFound } from 'next/navigation';
2
2
  import { BookCommitment } from '../../../../../../src/commitments/_base/BookCommitment';
3
3
  import { getVisibleCommitmentDefinitions } from '../../../utils/getVisibleCommitmentDefinitions';
4
- import { PrintButton } from '../../../components/PrintButton/PrintButton';
4
+ import { DocsToolbar } from '../../../components/DocsToolbar/DocsToolbar';
5
5
  import { PrintHeader } from '../../../components/PrintHeader/PrintHeader';
6
6
  import { DocumentationContent } from '../../../components/DocumentationContent/DocumentationContent';
7
7
 
@@ -27,9 +27,8 @@ export default async function DocPage(props: DocPageProps) {
27
27
 
28
28
  return (
29
29
  <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
30
- <PrintButton />
31
-
32
30
  <div className="container mx-auto px-4 py-16">
31
+ <DocsToolbar />
33
32
  <PrintHeader title={primary.type} />
34
33
 
35
34
  <DocumentationContent
@@ -1,20 +1,20 @@
1
+ import { MarkdownContent } from '@promptbook-local/components';
1
2
  import Link from 'next/link';
3
+ import { DocsToolbar } from '../../components/DocsToolbar/DocsToolbar';
4
+ import { DocumentationContent } from '../../components/DocumentationContent/DocumentationContent';
2
5
  import { Card } from '../../components/Homepage/Card';
3
6
  import { Section } from '../../components/Homepage/Section';
4
7
  import { OpenMojiIcon } from '../../components/OpenMojiIcon/OpenMojiIcon';
5
- import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
6
- import { PrintButton } from '../../components/PrintButton/PrintButton';
7
8
  import { PrintHeader } from '../../components/PrintHeader/PrintHeader';
8
- import { DocumentationContent } from '../../components/DocumentationContent/DocumentationContent';
9
+ import { getVisibleCommitmentDefinitions } from '../../utils/getVisibleCommitmentDefinitions';
9
10
 
10
11
  export default function DocsPage() {
11
12
  const groupedCommitments = getVisibleCommitmentDefinitions();
12
13
 
13
14
  return (
14
15
  <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50">
15
- <PrintButton />
16
-
17
16
  <div className="container mx-auto px-4 py-16">
17
+ <DocsToolbar />
18
18
  <PrintHeader title="Full Documentation" />
19
19
 
20
20
  {/* Screen view: Cards */}
@@ -24,7 +24,7 @@ export default function DocsPage() {
24
24
  <Link key={primary.type} href={`/docs/${primary.type}`} className="block h-full group">
25
25
  <Card className="h-full group-hover:border-blue-500 transition-colors">
26
26
  <h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
27
- <OpenMojiIcon icon={primary.icon} className="mr-2" />
27
+ <OpenMojiIcon icon={primary.icon} variant="color" className="mr-2" />
28
28
  {primary.type}
29
29
  {aliases.length > 0 && (
30
30
  <span className="text-gray-400 font-normal text-lg">
@@ -33,7 +33,11 @@ export default function DocsPage() {
33
33
  </span>
34
34
  )}
35
35
  </h3>
36
- {primary.description && <p className="text-gray-600 line-clamp-3">{primary.description}</p>}
36
+ {primary.description && (
37
+ <p className="text-gray-600 line-clamp-3">
38
+ <MarkdownContent content={primary.description} />
39
+ </p>
40
+ )}
37
41
  </Card>
38
42
  </Link>
39
43
  ))}
@@ -44,11 +48,7 @@ export default function DocsPage() {
44
48
  <div className="hidden print:block space-y-12">
45
49
  {groupedCommitments.map(({ primary, aliases }) => (
46
50
  <div key={primary.type} className="break-inside-avoid page-break-after-always">
47
- <DocumentationContent
48
- primary={primary}
49
- aliases={aliases}
50
- isPrintOnly={true}
51
- />
51
+ <DocumentationContent primary={primary} aliases={aliases} isPrintOnly={true} />
52
52
  <hr className="my-8 border-gray-200" />
53
53
  </div>
54
54
  ))}
@@ -5,35 +5,67 @@
5
5
  /**
6
6
  * OpenMoji black and white CSS
7
7
  *
8
- * https://github.com/hfg-gmuend/openmoji/blob/master/font/OpenMoji-black-glyf/openmoji.css
8
+ * https://github.com/hfg-gmuend/openmoji/blob/master/font/OpenMoji-black-glyf
9
9
  */
10
10
  @font-face {
11
11
  font-family: 'OpenMojiBlack';
12
12
  src: url('/fonts/OpenMoji-black-glyf.woff2') format('woff2');
13
- unicode-range: U+23, U+2A, U+2D, U+30-39, U+A9, U+AE, U+200D, U+203C, U+2049, U+20E3, U+2117, U+2120, U+2122,
14
- U+2139, U+2194-2199, U+21A9, U+21AA, U+229C, U+231A, U+231B, U+2328, U+23CF, U+23E9-23F3, U+23F8-23FE, U+24C2,
15
- U+25A1, U+25AA-25AE, U+25B6, U+25C0, U+25C9, U+25D0, U+25D1, U+25E7-25EA, U+25ED, U+25EE, U+25FB-25FE,
16
- U+2600-2605, U+260E, U+2611, U+2614, U+2615, U+2618, U+261D, U+2620, U+2622, U+2623, U+2626, U+262A, U+262E,
17
- U+262F, U+2638-263A, U+2640, U+2642, U+2648-2653, U+265F, U+2660, U+2663, U+2665, U+2666, U+2668, U+267B,
18
- U+267E, U+267F, U+2691-2697, U+2699, U+269B, U+269C, U+26A0, U+26A1, U+26A7, U+26AA, U+26AB, U+26B0, U+26B1,
19
- U+26BD, U+26BE, U+26C4, U+26C5, U+26C8, U+26CE, U+26CF, U+26D1, U+26D3, U+26D4, U+26E9, U+26EA, U+26F0-26F5,
20
- U+26F7-26FA, U+26FD, U+2702, U+2705, U+2708-270D, U+270F, U+2712, U+2714, U+2716, U+271D, U+2721, U+2728,
21
- U+2733, U+2734, U+2744, U+2747, U+274C, U+274E, U+2753-2755, U+2757, U+2763, U+2764, U+2795-2797, U+27A1,
22
- U+27B0, U+27BF, U+2934, U+2935, U+2B05-2B07, U+2B0C, U+2B0D, U+2B1B, U+2B1C, U+2B1F-2B24, U+2B2E, U+2B2F,
23
- U+2B50, U+2B55, U+2B58, U+2B8F, U+2BBA-2BBC, U+2BC3, U+2BC4, U+2BEA, U+2BEB, U+3030, U+303D, U+3297, U+3299,
24
- U+E000-E009, U+E010, U+E011, U+E040-E06D, U+E080-E0B4, U+E0C0-E0CC, U+E0FF-E10D, U+E140-E14A, U+E150-E157,
25
- U+E181-E189, U+E1C0-E1C4, U+E1C6-E1D9, U+E200-E216, U+E240-E269, U+E280-E283, U+E2C0-E2C4, U+E2C6-E2DA,
26
- U+E300-E303, U+E305-E30F, U+E312-E316, U+E318-E322, U+E324-E329, U+E32B, U+E340-E348, U+E380, U+E381, U+F000,
27
- U+F77A, U+F8FF, U+FE0F, U+1F004, U+1F0CF, U+1F10D-1F10F, U+1F12F, U+1F16D-1F171, U+1F17E, U+1F17F, U+1F18E,
28
- U+1F191-1F19A, U+1F1E6-1F1FF, U+1F201, U+1F202, U+1F21A, U+1F22F, U+1F232-1F23A, U+1F250, U+1F251,
29
- U+1F260-1F265, U+1F300-1F321, U+1F324-1F393, U+1F396, U+1F397, U+1F399-1F39B, U+1F39E-1F3F0, U+1F3F3-1F3F5,
30
- U+1F3F7-1F4FD, U+1F4FF-1F53D, U+1F549-1F54E, U+1F550-1F567, U+1F56F, U+1F570, U+1F573-1F57A, U+1F587,
31
- U+1F58A-1F58D, U+1F590, U+1F595, U+1F596, U+1F5A4, U+1F5A5, U+1F5A8, U+1F5B1, U+1F5B2, U+1F5BC, U+1F5C2-1F5C4,
32
- U+1F5D1-1F5D3, U+1F5DC-1F5DE, U+1F5E1, U+1F5E3, U+1F5E8, U+1F5EF, U+1F5F3, U+1F5FA-1F64F, U+1F680-1F6C5,
33
- U+1F6CB-1F6D2, U+1F6D5-1F6D7, U+1F6DC-1F6E5, U+1F6E9, U+1F6EB, U+1F6EC, U+1F6F0, U+1F6F3-1F6FC, U+1F7E0-1F7EB,
34
- U+1F7F0, U+1F90C-1F93A, U+1F93C-1F945, U+1F947-1F9FF, U+1FA70-1FA7C, U+1FA80-1FA88, U+1FA90-1FABD,
35
- U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8, U+1FBC5-1FBC9, U+E0061-E0067, U+E0069,
36
- U+E006C-E0079, U+E007F;
13
+ unicode-range: U+23, U+2A, U+2D, U+30-39, U+A9, U+AE, U+200D, U+203C, U+2049, U+20E3, U+2117, U+2120, U+2122, U+2139,
14
+ U+2194-2199, U+21A9, U+21AA, U+229C, U+231A, U+231B, U+2328, U+23CF, U+23E9-23F3, U+23F8-23FE, U+24C2, U+25A1,
15
+ U+25AA-25AE, U+25B6, U+25C0, U+25C9, U+25D0, U+25D1, U+25E7-25EA, U+25ED, U+25EE, U+25FB-25FE, U+2600-2605,
16
+ U+260E, U+2611, U+2614, U+2615, U+2618, U+261D, U+2620, U+2622, U+2623, U+2626, U+262A, U+262E, U+262F,
17
+ U+2638-263A, U+2640, U+2642, U+2648-2653, U+265F, U+2660, U+2663, U+2665, U+2666, U+2668, U+267B, U+267E, U+267F,
18
+ U+2691-2697, U+2699, U+269B, U+269C, U+26A0, U+26A1, U+26A7, U+26AA, U+26AB, U+26B0, U+26B1, U+26BD, U+26BE,
19
+ U+26C4, U+26C5, U+26C8, U+26CE, U+26CF, U+26D1, U+26D3, U+26D4, U+26E9, U+26EA, U+26F0-26F5, U+26F7-26FA, U+26FD,
20
+ U+2702, U+2705, U+2708-270D, U+270F, U+2712, U+2714, U+2716, U+271D, U+2721, U+2728, U+2733, U+2734, U+2744,
21
+ U+2747, U+274C, U+274E, U+2753-2755, U+2757, U+2763, U+2764, U+2795-2797, U+27A1, U+27B0, U+27BF, U+2934, U+2935,
22
+ U+2B05-2B07, U+2B0C, U+2B0D, U+2B1B, U+2B1C, U+2B1F-2B24, U+2B2E, U+2B2F, U+2B50, U+2B55, U+2B58, U+2B8F,
23
+ U+2BBA-2BBC, U+2BC3, U+2BC4, U+2BEA, U+2BEB, U+3030, U+303D, U+3297, U+3299, U+E000-E009, U+E010, U+E011,
24
+ U+E040-E06D, U+E080-E0B4, U+E0C0-E0CC, U+E0FF-E10D, U+E140-E14A, U+E150-E157, U+E181-E189, U+E1C0-E1C4,
25
+ U+E1C6-E1D9, U+E200-E216, U+E240-E269, U+E280-E283, U+E2C0-E2C4, U+E2C6-E2DA, U+E300-E303, U+E305-E30F,
26
+ U+E312-E316, U+E318-E322, U+E324-E329, U+E32B, U+E340-E348, U+E380, U+E381, U+F000, U+F77A, U+F8FF, U+FE0F,
27
+ U+1F004, U+1F0CF, U+1F10D-1F10F, U+1F12F, U+1F16D-1F171, U+1F17E, U+1F17F, U+1F18E, U+1F191-1F19A, U+1F1E6-1F1FF,
28
+ U+1F201, U+1F202, U+1F21A, U+1F22F, U+1F232-1F23A, U+1F250, U+1F251, U+1F260-1F265, U+1F300-1F321, U+1F324-1F393,
29
+ U+1F396, U+1F397, U+1F399-1F39B, U+1F39E-1F3F0, U+1F3F3-1F3F5, U+1F3F7-1F4FD, U+1F4FF-1F53D, U+1F549-1F54E,
30
+ U+1F550-1F567, U+1F56F, U+1F570, U+1F573-1F57A, U+1F587, U+1F58A-1F58D, U+1F590, U+1F595, U+1F596, U+1F5A4,
31
+ U+1F5A5, U+1F5A8, U+1F5B1, U+1F5B2, U+1F5BC, U+1F5C2-1F5C4, U+1F5D1-1F5D3, U+1F5DC-1F5DE, U+1F5E1, U+1F5E3,
32
+ U+1F5E8, U+1F5EF, U+1F5F3, U+1F5FA-1F64F, U+1F680-1F6C5, U+1F6CB-1F6D2, U+1F6D5-1F6D7, U+1F6DC-1F6E5, U+1F6E9,
33
+ U+1F6EB, U+1F6EC, U+1F6F0, U+1F6F3-1F6FC, U+1F7E0-1F7EB, U+1F7F0, U+1F90C-1F93A, U+1F93C-1F945, U+1F947-1F9FF,
34
+ U+1FA70-1FA7C, U+1FA80-1FA88, U+1FA90-1FABD, U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8,
35
+ U+1FBC5-1FBC9, U+E0061-E0067, U+E0069, U+E006C-E0079, U+E007F;
36
+ }
37
+
38
+ /**
39
+ * OpenMoji color CSS
40
+ *
41
+ * https://github.com/hfg-gmuend/openmoji/tree/master/font/OpenMoji-color-cbdt
42
+ */
43
+ @font-face {
44
+ font-family: 'OpenMojiColor';
45
+ src: url('/fonts/OpenMoji-color-cbdt.woff2') format('woff2');
46
+ unicode-range: U+23, U+2A, U+2D, U+30-39, U+A9, U+AE, U+200D, U+203C, U+2049, U+20E3, U+2117, U+2120, U+2122, U+2139,
47
+ U+2194-2199, U+21A9, U+21AA, U+229C, U+231A, U+231B, U+2328, U+23CF, U+23E9-23F3, U+23F8-23FE, U+24C2, U+25A1,
48
+ U+25AA-25AE, U+25B6, U+25C0, U+25C9, U+25D0, U+25D1, U+25E7-25EA, U+25ED, U+25EE, U+25FB-25FE, U+2600-2605,
49
+ U+260E, U+2611, U+2614, U+2615, U+2618, U+261D, U+2620, U+2622, U+2623, U+2626, U+262A, U+262E, U+262F,
50
+ U+2638-263A, U+2640, U+2642, U+2648-2653, U+265F, U+2660, U+2663, U+2665, U+2666, U+2668, U+267B, U+267E, U+267F,
51
+ U+2691-2697, U+2699, U+269B, U+269C, U+26A0, U+26A1, U+26A7, U+26AA, U+26AB, U+26B0, U+26B1, U+26BD, U+26BE,
52
+ U+26C4, U+26C5, U+26C8, U+26CE, U+26CF, U+26D1, U+26D3, U+26D4, U+26E9, U+26EA, U+26F0-26F5, U+26F7-26FA, U+26FD,
53
+ U+2702, U+2705, U+2708-270D, U+270F, U+2712, U+2714, U+2716, U+271D, U+2721, U+2728, U+2733, U+2734, U+2744,
54
+ U+2747, U+274C, U+274E, U+2753-2755, U+2757, U+2763, U+2764, U+2795-2797, U+27A1, U+27B0, U+27BF, U+2934, U+2935,
55
+ U+2B05-2B07, U+2B0C, U+2B0D, U+2B1B, U+2B1C, U+2B1F-2B24, U+2B2E, U+2B2F, U+2B50, U+2B55, U+2B58, U+2B8F,
56
+ U+2BBA-2BBC, U+2BC3, U+2BC4, U+2BEA, U+2BEB, U+3030, U+303D, U+3297, U+3299, U+E000-E009, U+E010, U+E011,
57
+ U+E040-E06D, U+E080-E0B4, U+E0C0-E0CC, U+E0FF-E10D, U+E140-E14A, U+E150-E157, U+E181-E189, U+E1C0-E1C4,
58
+ U+E1C6-E1D9, U+E200-E216, U+E240-E269, U+E280-E283, U+E2C0-E2C4, U+E2C6-E2DA, U+E300-E303, U+E305-E30F,
59
+ U+E312-E316, U+E318-E322, U+E324-E329, U+E32B, U+E340-E348, U+E380, U+E381, U+F000, U+F77A, U+F8FF, U+FE0F,
60
+ U+1F004, U+1F0CF, U+1F10D-1F10F, U+1F12F, U+1F16D-1F171, U+1F17E, U+1F17F, U+1F18E, U+1F191-1F19A, U+1F1E6-1F1FF,
61
+ U+1F201, U+1F202, U+1F21A, U+1F22F, U+1F232-1F23A, U+1F250, U+1F251, U+1F260-1F265, U+1F300-1F321, U+1F324-1F393,
62
+ U+1F396, U+1F397, U+1F399-1F39B, U+1F39E-1F3F0, U+1F3F3-1F3F5, U+1F3F7-1F4FD, U+1F4FF-1F53D, U+1F549-1F54E,
63
+ U+1F550-1F567, U+1F56F, U+1F570, U+1F573-1F57A, U+1F587, U+1F58A-1F58D, U+1F590, U+1F595, U+1F596, U+1F5A4,
64
+ U+1F5A5, U+1F5A8, U+1F5B1, U+1F5B2, U+1F5BC, U+1F5C2-1F5C4, U+1F5D1-1F5D3, U+1F5DC-1F5DE, U+1F5E1, U+1F5E3,
65
+ U+1F5E8, U+1F5EF, U+1F5F3, U+1F5FA-1F64F, U+1F680-1F6C5, U+1F6CB-1F6D2, U+1F6D5-1F6D7, U+1F6DC-1F6E5, U+1F6E9,
66
+ U+1F6EB, U+1F6EC, U+1F6F0, U+1F6F3-1F6FC, U+1F7E0-1F7EB, U+1F7F0, U+1F90C-1F93A, U+1F93C-1F945, U+1F947-1F9FF,
67
+ U+1FA70-1FA7C, U+1FA80-1FA88, U+1FA90-1FABD, U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8,
68
+ U+1FBC5-1FBC9, U+E0061-E0067, U+E0069, U+E006C-E0079, U+E007F;
37
69
  }
38
70
 
39
71
  :root {
@@ -201,7 +233,7 @@ textarea:focus-visible {
201
233
  /* Print styles */
202
234
  @media print {
203
235
  @page {
204
- margin: 2cm;
236
+ margin: 1cm;
205
237
  }
206
238
 
207
239
  /* Hide UI elements */
@@ -211,7 +243,7 @@ textarea:focus-visible {
211
243
  aside,
212
244
  button:not(.print:block),
213
245
  .no-print,
214
- [role="complementary"],
246
+ [role='complementary'],
215
247
  #portal-root {
216
248
  display: none !important;
217
249
  }
@@ -227,6 +259,14 @@ textarea:focus-visible {
227
259
  color: black !important;
228
260
  }
229
261
 
262
+ /* Remove container padding for full width printing */
263
+ .container {
264
+ padding-left: 0 !important;
265
+ padding-right: 0 !important;
266
+ max-width: none !important;
267
+ width: 100% !important;
268
+ }
269
+
230
270
  /* Remove shadows and backgrounds for clean print */
231
271
  * {
232
272
  box-shadow: none !important;
@@ -236,10 +276,66 @@ textarea:focus-visible {
236
276
  }
237
277
 
238
278
  /* Ensure text visibility */
239
- h1, h2, h3, h4, h5, h6, p, li, span, div {
279
+ h1,
280
+ h2,
281
+ h3,
282
+ h4,
283
+ h5,
284
+ h6,
285
+ p,
286
+ li,
287
+ span,
288
+ div {
240
289
  color: black !important;
241
290
  }
242
291
 
292
+ /* Optimize typography for print */
293
+ .prose {
294
+ font-size: 11pt !important;
295
+ line-height: 1.4 !important;
296
+ max-width: 100% !important;
297
+ }
298
+
299
+ .prose h1 {
300
+ font-size: 22pt !important;
301
+ margin-bottom: 0.5rem !important;
302
+ margin-top: 0 !important;
303
+ line-height: 1.2 !important;
304
+ }
305
+
306
+ .prose h2 {
307
+ font-size: 16pt !important;
308
+ margin-top: 1.5rem !important;
309
+ margin-bottom: 0.5rem !important;
310
+ padding-bottom: 0.25rem !important;
311
+ line-height: 1.3 !important;
312
+ }
313
+
314
+ .prose h3 {
315
+ font-size: 14pt !important;
316
+ margin-top: 1rem !important;
317
+ margin-bottom: 0.5rem !important;
318
+ line-height: 1.3 !important;
319
+ }
320
+
321
+ .prose p,
322
+ .prose ul,
323
+ .prose ol {
324
+ margin-bottom: 0.5rem !important;
325
+ margin-top: 0 !important;
326
+ }
327
+
328
+ .prose li {
329
+ margin-top: 0.2rem !important;
330
+ margin-bottom: 0.2rem !important;
331
+ }
332
+
333
+ .prose pre {
334
+ padding: 0.5rem !important;
335
+ margin-bottom: 0.75rem !important;
336
+ margin-top: 0.5rem !important;
337
+ }
338
+
243
339
  /* Links */
244
340
  a {
245
341
  text-decoration: underline;
@@ -247,30 +343,41 @@ textarea:focus-visible {
247
343
  }
248
344
 
249
345
  /* Preserve borders */
250
- .border, .border-b, .border-t, .border-l, .border-r {
346
+ .border,
347
+ .border-b,
348
+ .border-t,
349
+ .border-l,
350
+ .border-r {
251
351
  border-color: #ddd !important;
252
352
  }
253
353
 
254
354
  /* Page break control */
255
- h1, h2 {
355
+ h1,
356
+ h2 {
256
357
  break-after: avoid;
257
358
  page-break-after: avoid;
258
359
  }
259
360
 
260
- p, li, pre, code {
361
+ p,
362
+ li,
363
+ pre,
364
+ code {
261
365
  break-inside: avoid;
262
366
  page-break-inside: avoid;
263
367
  }
264
368
 
265
369
  /* Specific component overrides */
266
- .card, .bg-white {
370
+ .card,
371
+ .bg-white {
267
372
  border: none !important;
268
373
  }
269
374
 
270
375
  /* Code blocks */
271
- pre, code {
376
+ pre,
377
+ code {
272
378
  background-color: #f5f5f5 !important;
273
379
  border: 1px solid #ddd !important;
274
380
  white-space: pre-wrap !important;
381
+ font-size: 0.9em !important;
275
382
  }
276
383
  }