create-nextblock 0.2.78 → 0.8.1

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 (413) hide show
  1. package/bin/create-nextblock.js +793 -472
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,1507 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
+ import { usePathname, useRouter } from "next/navigation";
5
+ import {
6
+ Brain,
7
+ CheckCircle2,
8
+ History,
9
+ Loader2,
10
+ MessageSquarePlus,
11
+ Send,
12
+ Trash2,
13
+ Wrench,
14
+ XCircle,
15
+ } from "lucide-react";
16
+
17
+ import { Button } from "@nextblock-cms/ui";
18
+ import { Textarea } from "@nextblock-cms/ui";
19
+ import { cn } from "@nextblock-cms/utils";
20
+ import {
21
+ useCortexAiPageContext,
22
+ type CortexAiPageContext,
23
+ } from "./CortexAiPageContext";
24
+
25
+ type ChatRole = "assistant" | "user";
26
+
27
+ type ChatMessage = {
28
+ content: string;
29
+ id: string;
30
+ role: ChatRole;
31
+ };
32
+
33
+ type ChatThread = {
34
+ createdAt: string;
35
+ id: string;
36
+ messages: ChatMessage[];
37
+ title: string;
38
+ updatedAt: string;
39
+ };
40
+
41
+ type ToolActivity = {
42
+ id: string;
43
+ input?: unknown;
44
+ name: string;
45
+ output?: unknown;
46
+ status: "error" | "running" | "success";
47
+ };
48
+
49
+ type ConfirmedToolCall = {
50
+ confirmationPhrase: string;
51
+ input: unknown;
52
+ toolName: string;
53
+ };
54
+
55
+ type CortexAgentStreamEvent =
56
+ | {
57
+ credentialSource: string;
58
+ modelId: string;
59
+ type: "meta";
60
+ }
61
+ | {
62
+ text: string;
63
+ type: "text-delta";
64
+ }
65
+ | {
66
+ input?: unknown;
67
+ toolCallId?: string;
68
+ toolName: string;
69
+ type: "tool-call";
70
+ }
71
+ | {
72
+ output?: unknown;
73
+ toolCallId?: string;
74
+ toolName: string;
75
+ type: "tool-result";
76
+ }
77
+ | {
78
+ message: string;
79
+ toolCallId?: string;
80
+ toolName?: string;
81
+ type: "tool-error";
82
+ }
83
+ | {
84
+ message: string;
85
+ type: "error";
86
+ }
87
+ | {
88
+ type: "finish";
89
+ };
90
+
91
+ const LEGACY_STORAGE_KEY = "nextblock-cortex-global-agent-chat";
92
+ const THREADS_STORAGE_KEY = "nextblock-cortex-global-agent-chat-threads";
93
+ const MAX_STORED_MESSAGES = 40;
94
+ const MAX_STORED_THREADS = 20;
95
+ const REQUEST_TIMEOUT_MS = 90000;
96
+ const CORTEX_AI_SETTINGS_CHANGED_EVENT = "nextblock:cortex-ai-settings-changed";
97
+ const MUTATING_TOOL_NAMES = new Set([
98
+ "create_cms_page",
99
+ "create_cms_post",
100
+ "create_cms_product",
101
+ "create_custom_block",
102
+ "delete_cms_item",
103
+ "delete_custom_block",
104
+ "execute_database_action_plan",
105
+ "execute_database_mutation",
106
+ "execute_cms_action_plan",
107
+ "insert_content_block",
108
+ "update_cms_item_field",
109
+ "update_current_cms_fields",
110
+ "update_content_block",
111
+ "update_custom_block",
112
+ "update_footer",
113
+ "update_navigation_bar",
114
+ "update_section_column_block",
115
+ ]);
116
+
117
+ // Window event fired after a Cortex mutation so client-rendered lists that fetch
118
+ // their own data (e.g. the custom blocks library) can refresh without a full reload.
119
+ export const CORTEX_DATA_CHANGED_EVENT = "nextblock:cortex-data-changed";
120
+
121
+ const TOOL_COPY: Record<string, { done: string; running: string }> = {
122
+ search_documentation: {
123
+ done: "Documentation searched",
124
+ running: "Searching documentation...",
125
+ },
126
+ create_cms_page: {
127
+ done: "Page created",
128
+ running: "Preparing page...",
129
+ },
130
+ create_cms_post: {
131
+ done: "Post created",
132
+ running: "Preparing post...",
133
+ },
134
+ create_cms_product: {
135
+ done: "Product created",
136
+ running: "Preparing product...",
137
+ },
138
+ create_custom_block: {
139
+ done: "Custom block created",
140
+ running: "Designing custom block...",
141
+ },
142
+ update_custom_block: {
143
+ done: "Custom block updated",
144
+ running: "Updating custom block...",
145
+ },
146
+ delete_custom_block: {
147
+ done: "Custom block deleted",
148
+ running: "Preparing delete...",
149
+ },
150
+ list_custom_blocks: {
151
+ done: "Custom blocks listed",
152
+ running: "Listing custom blocks...",
153
+ },
154
+ execute_cms_action_plan: {
155
+ done: "CMS plan completed",
156
+ running: "Preparing CMS plan...",
157
+ },
158
+ describe_database_schema: {
159
+ done: "Database schema inspected",
160
+ running: "Inspecting database schema...",
161
+ },
162
+ execute_database_action_plan: {
163
+ done: "Database plan completed",
164
+ running: "Preparing database plan...",
165
+ },
166
+ execute_database_mutation: {
167
+ done: "Database updated",
168
+ running: "Preparing database update...",
169
+ },
170
+ read_database_records: {
171
+ done: "Database records read",
172
+ running: "Reading database records...",
173
+ },
174
+ insert_content_block: {
175
+ done: "Content block inserted",
176
+ running: "Inserting content block...",
177
+ },
178
+ delete_cms_item: {
179
+ done: "CMS item deleted",
180
+ running: "Preparing delete...",
181
+ },
182
+ prepare_delete_cms_item: {
183
+ done: "Delete reviewed",
184
+ running: "Reviewing delete...",
185
+ },
186
+ update_footer: {
187
+ done: "Footer updated",
188
+ running: "Updating footer...",
189
+ },
190
+ update_navigation_bar: {
191
+ done: "Navigation bar updated",
192
+ running: "Updating navigation bar...",
193
+ },
194
+ read_current_cms_item: {
195
+ done: "Current item read",
196
+ running: "Reading current item...",
197
+ },
198
+ update_current_cms_fields: {
199
+ done: "Current item updated",
200
+ running: "Updating current item...",
201
+ },
202
+ update_cms_item_field: {
203
+ done: "CMS field updated",
204
+ running: "Preparing field update...",
205
+ },
206
+ update_content_block: {
207
+ done: "Content block updated",
208
+ running: "Updating content block...",
209
+ },
210
+ update_section_column_block: {
211
+ done: "Section block updated",
212
+ running: "Updating section block...",
213
+ },
214
+ };
215
+
216
+ function buildFallbackPageContext(pathname: string | null): CortexAiPageContext | null {
217
+ if (!pathname) {
218
+ return null;
219
+ }
220
+
221
+ const pageMatch = pathname.match(/^\/cms\/pages\/(\d+)\/edit$/);
222
+ if (pageMatch?.[1]) {
223
+ return { contentType: "page", entityId: Number(pageMatch[1]) };
224
+ }
225
+
226
+ const postMatch = pathname.match(/^\/cms\/posts\/(\d+)\/edit$/);
227
+ if (postMatch?.[1]) {
228
+ return { contentType: "post", entityId: Number(postMatch[1]) };
229
+ }
230
+
231
+ const productMatch = pathname.match(/^\/cms\/products\/([^/]+)\/edit$/);
232
+ if (productMatch?.[1]) {
233
+ return { contentType: "product", entityId: productMatch[1] };
234
+ }
235
+
236
+ return null;
237
+ }
238
+
239
+ function createId() {
240
+ return globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`;
241
+ }
242
+
243
+ function isChatMessage(message: unknown): message is ChatMessage {
244
+ if (!message || typeof message !== "object") {
245
+ return false;
246
+ }
247
+
248
+ const candidate = message as Partial<ChatMessage>;
249
+
250
+ return (
251
+ typeof candidate.id === "string" &&
252
+ (candidate.role === "assistant" || candidate.role === "user") &&
253
+ typeof candidate.content === "string"
254
+ );
255
+ }
256
+
257
+ function sanitizeMessages(messages: unknown) {
258
+ return Array.isArray(messages)
259
+ ? messages.filter(isChatMessage).slice(-MAX_STORED_MESSAGES)
260
+ : [];
261
+ }
262
+
263
+ function getThreadTitle(messages: ChatMessage[]) {
264
+ const firstUserMessage = messages.find((message) => message.role === "user")?.content.trim();
265
+
266
+ if (!firstUserMessage) {
267
+ return "New chat";
268
+ }
269
+
270
+ return firstUserMessage.length > 44 ? `${firstUserMessage.slice(0, 41)}...` : firstUserMessage;
271
+ }
272
+
273
+ function createChatThread(messages: ChatMessage[] = []): ChatThread {
274
+ const now = new Date().toISOString();
275
+
276
+ return {
277
+ createdAt: now,
278
+ id: createId(),
279
+ messages,
280
+ title: getThreadTitle(messages),
281
+ updatedAt: now,
282
+ };
283
+ }
284
+
285
+ function isChatThread(thread: unknown): thread is ChatThread {
286
+ if (!thread || typeof thread !== "object") {
287
+ return false;
288
+ }
289
+
290
+ const candidate = thread as Partial<ChatThread>;
291
+
292
+ return (
293
+ typeof candidate.id === "string" &&
294
+ Array.isArray(candidate.messages)
295
+ );
296
+ }
297
+
298
+ function readStoredThreads() {
299
+ if (typeof window === "undefined") {
300
+ return [];
301
+ }
302
+
303
+ try {
304
+ const stored = window.localStorage.getItem(THREADS_STORAGE_KEY);
305
+ const parsed = stored ? JSON.parse(stored) : [];
306
+ const threads = Array.isArray(parsed)
307
+ ? parsed.filter(isChatThread).map((thread) => {
308
+ const messages = sanitizeMessages(thread.messages);
309
+ const now = new Date().toISOString();
310
+
311
+ return {
312
+ createdAt: typeof thread.createdAt === "string" ? thread.createdAt : now,
313
+ id: thread.id,
314
+ messages,
315
+ title: typeof thread.title === "string" ? thread.title : getThreadTitle(messages),
316
+ updatedAt: typeof thread.updatedAt === "string" ? thread.updatedAt : now,
317
+ };
318
+ })
319
+ : [];
320
+
321
+ if (threads.length > 0) {
322
+ return threads
323
+ .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
324
+ .slice(0, MAX_STORED_THREADS);
325
+ }
326
+
327
+ const legacyMessages = sanitizeMessages(
328
+ JSON.parse(window.sessionStorage.getItem(LEGACY_STORAGE_KEY) || "[]")
329
+ );
330
+
331
+ if (legacyMessages.length > 0) {
332
+ return [createChatThread(legacyMessages)];
333
+ }
334
+ } catch {
335
+ return [];
336
+ }
337
+
338
+ return [];
339
+ }
340
+
341
+ function formatThreadTime(value: string) {
342
+ const timestamp = Date.parse(value);
343
+
344
+ if (!Number.isFinite(timestamp)) {
345
+ return "";
346
+ }
347
+
348
+ return new Intl.DateTimeFormat(undefined, {
349
+ day: "numeric",
350
+ hour: "numeric",
351
+ minute: "2-digit",
352
+ month: "short",
353
+ }).format(new Date(timestamp));
354
+ }
355
+
356
+ function getToolCopy(name: string) {
357
+ return (
358
+ TOOL_COPY[name] || {
359
+ done: "Tool finished",
360
+ running: "Running tool...",
361
+ }
362
+ );
363
+ }
364
+
365
+ function isMutatingToolName(name: string | undefined) {
366
+ return Boolean(name && MUTATING_TOOL_NAMES.has(name));
367
+ }
368
+
369
+ function isRecord(value: unknown): value is Record<string, unknown> {
370
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
371
+ }
372
+
373
+ function toolOutputRequiresConfirmation(output: unknown) {
374
+ return isRecord(output) && output.requiresConfirmation === true;
375
+ }
376
+
377
+ function toolOutputExecutedMutation(output: unknown) {
378
+ return isRecord(output) && output.mutationExecuted === true;
379
+ }
380
+
381
+ function toolOutputIsNotice(output: unknown) {
382
+ return isRecord(output) && (output.success === false || output.unsupported === true);
383
+ }
384
+
385
+ function getToolOutputNavigationPath(output: unknown) {
386
+ if (!toolOutputExecutedMutation(output) || !isRecord(output)) {
387
+ return null;
388
+ }
389
+
390
+ const path = output.editPath || output.redirectPath;
391
+
392
+ return typeof path === "string" && path.startsWith("/") ? path : null;
393
+ }
394
+
395
+ function readNumberField(value: unknown, key: string) {
396
+ if (!isRecord(value)) {
397
+ return null;
398
+ }
399
+
400
+ const parsed = Number(value[key]);
401
+
402
+ return Number.isFinite(parsed) ? parsed : null;
403
+ }
404
+
405
+ function readStringField(value: unknown, key: string) {
406
+ if (!isRecord(value)) {
407
+ return null;
408
+ }
409
+
410
+ const fieldValue = value[key];
411
+
412
+ return typeof fieldValue === "string" ? fieldValue : null;
413
+ }
414
+
415
+ function pluralize(count: number, singular: string, plural = `${singular}s`) {
416
+ return `${count} ${count === 1 ? singular : plural}`;
417
+ }
418
+
419
+ function getConfirmationSummary(activity: ToolActivity) {
420
+ if (!isRecord(activity.output) || !isRecord(activity.output.preview)) {
421
+ return "Complete the requested change.";
422
+ }
423
+
424
+ const preview = activity.output.preview;
425
+ const summary = readStringField(preview, "summary");
426
+
427
+ if (summary) {
428
+ const actionSummaries = Array.isArray(preview.actionSummaries)
429
+ ? preview.actionSummaries.filter((item): item is string => typeof item === "string")
430
+ : [];
431
+
432
+ return actionSummaries.length > 0
433
+ ? `${summary}\n\n${actionSummaries
434
+ .map((item, index) => `${index + 1}. ${item}`)
435
+ .join("\n")}`
436
+ : summary;
437
+ }
438
+
439
+ const title = readStringField(preview, "title");
440
+ const slug = readStringField(preview, "slug");
441
+ const status = readStringField(preview, "status");
442
+ const contentType = readStringField(preview, "contentType");
443
+ const field = readStringField(preview, "field");
444
+ const mode = readStringField(preview, "mode");
445
+ const languageCode = readStringField(preview, "languageCode");
446
+ const blockCount = readNumberField(preview, "blockCount");
447
+ const itemCount = readNumberField(preview, "itemCount");
448
+ const affectedCount = readNumberField(preview, "affectedCount");
449
+
450
+ if (activity.name === "create_cms_page" || activity.name === "create_cms_post") {
451
+ return `Create ${status || "draft"} ${activity.name === "create_cms_page" ? "page" : "post"} "${title || slug || "Untitled"}"${slug ? ` at slug "${slug}"` : ""}${blockCount !== null ? ` with ${pluralize(blockCount, "content block")}` : ""}.`;
452
+ }
453
+
454
+ if (activity.name === "create_cms_product") {
455
+ return `Create ${status || "draft"} product "${title || slug || "Untitled"}"${slug ? ` at slug "${slug}"` : ""}.`;
456
+ }
457
+
458
+ if (activity.name === "update_cms_item_field") {
459
+ return `Update ${field || "one field"} on the ${contentType || "CMS item"} "${title || slug || "selected item"}".`;
460
+ }
461
+
462
+ if (activity.name === "update_navigation_bar") {
463
+ return `${mode === "append" ? "Add" : mode === "update" ? "Update" : "Replace"} ${itemCount !== null ? pluralize(itemCount, "navigation item") : "navigation items"} in the ${languageCode || "selected"} header navigation.`;
464
+ }
465
+
466
+ if (activity.name === "update_footer") {
467
+ const linkCount = readNumberField(preview, "linkCount");
468
+ return `Update the ${languageCode || "selected"} footer${linkCount !== null ? ` with ${pluralize(linkCount, "link")}` : ""}.`;
469
+ }
470
+
471
+ if (activity.name === "update_content_block") {
472
+ return `Update the selected ${readStringField(preview, "blockType") || "content"} block.`;
473
+ }
474
+
475
+ if (activity.name === "insert_content_block") {
476
+ return `Insert ${readStringField(preview, "blockType") || "content"} block on the ${contentType || "CMS item"} "${title || slug || "selected item"}".`;
477
+ }
478
+
479
+ if (activity.name === "update_section_column_block") {
480
+ return `Update the selected nested ${readStringField(preview, "nestedBlockType") || "section"} block.`;
481
+ }
482
+
483
+ if (activity.name === "delete_cms_item" || activity.name === "prepare_delete_cms_item") {
484
+ return `Delete ${affectedCount !== null ? pluralize(affectedCount, contentType || "CMS item") : `the selected ${contentType || "CMS item"}`}${title || slug ? ` for "${title || slug}"` : ""}.`;
485
+ }
486
+
487
+ return "Complete the requested change.";
488
+ }
489
+
490
+ function getConfirmedToolCall(activity: ToolActivity): ConfirmedToolCall | null {
491
+ if (!toolOutputRequiresConfirmation(activity.output) || !isRecord(activity.output)) {
492
+ return null;
493
+ }
494
+
495
+ const confirmationPhrase = activity.output.confirmationPhrase;
496
+
497
+ if (typeof confirmationPhrase !== "string" || !activity.input) {
498
+ return null;
499
+ }
500
+
501
+ return {
502
+ confirmationPhrase,
503
+ input: activity.input,
504
+ toolName: activity.name === "prepare_delete_cms_item" ? "delete_cms_item" : activity.name,
505
+ };
506
+ }
507
+
508
+ function getConfirmationKey(activity: ToolActivity) {
509
+ if (!toolOutputRequiresConfirmation(activity.output) || !isRecord(activity.output)) {
510
+ return null;
511
+ }
512
+
513
+ const phrase = activity.output.confirmationPhrase;
514
+
515
+ if (typeof phrase === "string" && phrase.trim()) {
516
+ return phrase;
517
+ }
518
+
519
+ return `${activity.name}:${getConfirmationSummary(activity)}`;
520
+ }
521
+
522
+ function getToolActivityDetail(activity: ToolActivity) {
523
+ if (activity.status === "error" && isRecord(activity.output)) {
524
+ const error = activity.output.error;
525
+
526
+ return typeof error === "string" ? error : null;
527
+ }
528
+
529
+ if (!isRecord(activity.output)) {
530
+ return null;
531
+ }
532
+
533
+ if (activity.output.requiresConfirmation === true) {
534
+ return getConfirmationSummary(activity);
535
+ }
536
+
537
+ if (activity.output.unsupported === true || activity.output.success === false) {
538
+ const message = activity.output.message;
539
+
540
+ return typeof message === "string" ? message : null;
541
+ }
542
+
543
+ return null;
544
+ }
545
+
546
+ function getToolActivityId(event: { toolCallId?: string; toolName?: string }) {
547
+ return event.toolCallId || `${event.toolName || "tool"}-${Date.now()}`;
548
+ }
549
+
550
+ function findMatchingToolActivityIndex(
551
+ activities: ToolActivity[],
552
+ event: { toolCallId?: string; toolName?: string }
553
+ ) {
554
+ if (event.toolCallId) {
555
+ const exactIndex = activities.findIndex((activity) => activity.id === event.toolCallId);
556
+
557
+ if (exactIndex >= 0) {
558
+ return exactIndex;
559
+ }
560
+ }
561
+
562
+ for (let index = activities.length - 1; index >= 0; index--) {
563
+ const activity = activities[index];
564
+
565
+ if (activity.name === event.toolName && activity.status === "running") {
566
+ return index;
567
+ }
568
+ }
569
+
570
+ return -1;
571
+ }
572
+
573
+ function parseStreamFrame(frame: string): CortexAgentStreamEvent | null {
574
+ const data = frame
575
+ .split("\n")
576
+ .filter((line) => line.startsWith("data:"))
577
+ .map((line) => line.slice(5).trimStart())
578
+ .join("\n");
579
+
580
+ if (!data) {
581
+ return null;
582
+ }
583
+
584
+ return JSON.parse(data) as CortexAgentStreamEvent;
585
+ }
586
+
587
+ function FormattedText({ text }: { text: string }) {
588
+ // Handle bold (**text**) and inline code (`text`)
589
+ const parts = text.split(/(\*\*.*?\*\*|`.*?`)/g);
590
+
591
+ return (
592
+ <>
593
+ {parts.map((part, i) => {
594
+ if (part.startsWith("**") && part.endsWith("**")) {
595
+ return <strong key={i}>{part.slice(2, -2)}</strong>;
596
+ }
597
+ if (part.startsWith("`") && part.endsWith("`")) {
598
+ return (
599
+ <code
600
+ key={i}
601
+ className="rounded bg-slate-200 px-1 py-0.5 font-mono text-[11px] dark:bg-slate-800"
602
+ >
603
+ {part.slice(1, -1)}
604
+ </code>
605
+ );
606
+ }
607
+ return part;
608
+ })}
609
+ </>
610
+ );
611
+ }
612
+
613
+ function MarkdownContent({ content }: { content: string }) {
614
+ if (!content) return null;
615
+
616
+ const lines = content.split("\n");
617
+ const elements: React.ReactNode[] = [];
618
+ let currentList: React.ReactNode[] = [];
619
+
620
+ const flushList = () => {
621
+ if (currentList.length > 0) {
622
+ elements.push(
623
+ <ul key={`list-${elements.length}`} className="mb-3 mt-1 list-disc space-y-1 pl-5">
624
+ {currentList}
625
+ </ul>
626
+ );
627
+ currentList = [];
628
+ }
629
+ };
630
+
631
+ lines.forEach((line, index) => {
632
+ const trimmed = line.trim();
633
+
634
+ // Check for bullet points (* or -)
635
+ if (trimmed.startsWith("* ") || trimmed.startsWith("- ")) {
636
+ const text = line.slice(line.indexOf(trimmed[0]) + 2);
637
+ currentList.push(
638
+ <li key={`li-${index}`}>
639
+ <FormattedText text={text} />
640
+ </li>
641
+ );
642
+ } else if (trimmed === "" && currentList.length > 0) {
643
+ // Empty line breaks the list
644
+ flushList();
645
+ } else if (trimmed !== "") {
646
+ // Regular line
647
+ flushList();
648
+ elements.push(
649
+ <p key={`p-${index}`} className="mb-2 last:mb-0">
650
+ <FormattedText text={line} />
651
+ </p>
652
+ );
653
+ } else {
654
+ // Empty line (not in list)
655
+ flushList();
656
+ if (index < lines.length - 1) {
657
+ elements.push(<div key={`br-${index}`} className="h-2" />);
658
+ }
659
+ }
660
+ });
661
+
662
+ flushList();
663
+
664
+ return <div className="markdown-content">{elements}</div>;
665
+ }
666
+
667
+ function MessageBubble({ message }: { message: ChatMessage }) {
668
+ const isUser = message.role === "user";
669
+
670
+ return (
671
+ <div className={cn("flex", isUser ? "justify-end" : "justify-start")}>
672
+ <div
673
+ className={cn(
674
+ "max-w-[86%] break-words rounded-lg px-3 py-2 text-sm leading-6",
675
+ isUser
676
+ ? "bg-primary text-primary-foreground whitespace-pre-wrap"
677
+ : "border border-slate-200 bg-slate-50 text-slate-800 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
678
+ )}
679
+ >
680
+ {message.content ? (
681
+ isUser ? (
682
+ message.content
683
+ ) : (
684
+ <MarkdownContent content={message.content} />
685
+ )
686
+ ) : (
687
+ <span className="inline-flex items-center gap-2 text-slate-500 dark:text-slate-400">
688
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
689
+ </span>
690
+ )}
691
+ </div>
692
+ </div>
693
+ );
694
+ }
695
+
696
+ function ToolActivityRow({
697
+ activity,
698
+ disabled,
699
+ onCancel,
700
+ onConfirm,
701
+ }: {
702
+ activity: ToolActivity;
703
+ disabled?: boolean;
704
+ onCancel: (activity: ToolActivity) => void;
705
+ onConfirm: (toolCall: ConfirmedToolCall) => void;
706
+ }) {
707
+ const copy = getToolCopy(activity.name);
708
+ const requiresConfirmation = toolOutputRequiresConfirmation(activity.output);
709
+ const isNotice = toolOutputIsNotice(activity.output);
710
+ const detail = getToolActivityDetail(activity);
711
+ const confirmedToolCall = getConfirmedToolCall(activity);
712
+ const label =
713
+ activity.status === "running"
714
+ ? copy.running
715
+ : activity.status === "success"
716
+ ? requiresConfirmation
717
+ ? "Confirmation needed"
718
+ : isNotice
719
+ ? "Tool notice"
720
+ : copy.done
721
+ : "Tool failed";
722
+ const iconState =
723
+ activity.status === "running"
724
+ ? "running"
725
+ : activity.status === "success" && !isNotice
726
+ ? "success"
727
+ : "error";
728
+ const row = (
729
+ <div className="flex items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-2 text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
730
+ {iconState === "running" ? (
731
+ <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
732
+ ) : iconState === "success" ? (
733
+ <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />
734
+ ) : (
735
+ <XCircle className="h-3.5 w-3.5 text-destructive" />
736
+ )}
737
+ <span className="min-w-0 flex-1 truncate">{label}</span>
738
+ {confirmedToolCall ? (
739
+ <div className="flex shrink-0 items-center gap-1">
740
+ <Button
741
+ className="h-7 rounded-md px-2.5 text-xs"
742
+ disabled={disabled}
743
+ onClick={() => onConfirm(confirmedToolCall)}
744
+ type="button"
745
+ >
746
+ Confirm
747
+ </Button>
748
+ <Button
749
+ className="h-7 rounded-md px-2.5 text-xs"
750
+ disabled={disabled}
751
+ onClick={() => onCancel(activity)}
752
+ type="button"
753
+ variant="outline"
754
+ >
755
+ Cancel
756
+ </Button>
757
+ </div>
758
+ ) : (
759
+ <Wrench className="h-3.5 w-3.5 shrink-0 text-slate-400" />
760
+ )}
761
+ </div>
762
+ );
763
+
764
+ if (confirmedToolCall) {
765
+ return (
766
+ <div className="rounded-md">
767
+ {row}
768
+ {detail && (
769
+ <div className="mt-1 whitespace-pre-wrap rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-600 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-300">
770
+ {detail}
771
+ </div>
772
+ )}
773
+ </div>
774
+ );
775
+ }
776
+
777
+ if (!detail) {
778
+ return row;
779
+ }
780
+
781
+ return (
782
+ <details className="group rounded-md">
783
+ <summary className="list-none cursor-pointer [&::-webkit-details-marker]:hidden">
784
+ {row}
785
+ </summary>
786
+ <pre className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-600 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-300">
787
+ {detail}
788
+ </pre>
789
+ </details>
790
+ );
791
+ }
792
+
793
+ export function CortexGlobalAgentChat() {
794
+ const pathname = usePathname();
795
+ const router = useRouter();
796
+ const cortexAiPageContext = useCortexAiPageContext();
797
+ const [isMounted, setIsMounted] = useState(false);
798
+ const [threads, setThreads] = useState<ChatThread[]>([]);
799
+ const [activeThreadId, setActiveThreadId] = useState<string | null>(null);
800
+ const [input, setInput] = useState("");
801
+ const [open, setOpen] = useState(false);
802
+ const [showHistory, setShowHistory] = useState(false);
803
+ const [isStreaming, setIsStreaming] = useState(false);
804
+ const [streamError, setStreamError] = useState<string | null>(null);
805
+ const [toolActivities, setToolActivities] = useState<ToolActivity[]>([]);
806
+ const [cancelledConfirmationKeys, setCancelledConfirmationKeys] = useState<Set<string>>(
807
+ () => new Set()
808
+ );
809
+ const [, setMetadata] = useState<{ credentialSource: string; modelId: string } | null>(
810
+ null
811
+ );
812
+ const abortControllerRef = useRef<AbortController | null>(null);
813
+ const pendingRefreshPathRef = useRef<string | null>(null);
814
+ const scrollAreaRef = useRef<HTMLDivElement | null>(null);
815
+
816
+ useEffect(() => {
817
+ const storedThreads = readStoredThreads();
818
+ const initialThreads = storedThreads.length > 0 ? storedThreads : [createChatThread()];
819
+
820
+ setThreads(initialThreads);
821
+ setActiveThreadId(initialThreads[0]?.id ?? null);
822
+ setIsMounted(true);
823
+ }, []);
824
+
825
+ useEffect(() => {
826
+ if (!isMounted || threads.length === 0) {
827
+ return;
828
+ }
829
+
830
+ try {
831
+ window.localStorage.setItem(
832
+ THREADS_STORAGE_KEY,
833
+ JSON.stringify(threads.slice(0, MAX_STORED_THREADS))
834
+ );
835
+ } catch {
836
+ // Ignore storage failures in private browsing or locked-down environments.
837
+ }
838
+ }, [isMounted, threads]);
839
+
840
+ useEffect(() => {
841
+ scrollAreaRef.current?.scrollTo({
842
+ behavior: "smooth",
843
+ top: scrollAreaRef.current.scrollHeight,
844
+ });
845
+ }, [activeThreadId, threads, toolActivities]);
846
+
847
+ useEffect(() => {
848
+ return () => abortControllerRef.current?.abort();
849
+ }, []);
850
+
851
+ useEffect(() => {
852
+ const resetProviderMetadata = () => setMetadata(null);
853
+
854
+ window.addEventListener(CORTEX_AI_SETTINGS_CHANGED_EVENT, resetProviderMetadata);
855
+
856
+ return () => {
857
+ window.removeEventListener(CORTEX_AI_SETTINGS_CHANGED_EVENT, resetProviderMetadata);
858
+ };
859
+ }, []);
860
+
861
+ useEffect(() => {
862
+ if (!pendingRefreshPathRef.current || pendingRefreshPathRef.current !== pathname) {
863
+ return;
864
+ }
865
+
866
+ pendingRefreshPathRef.current = null;
867
+ router.refresh();
868
+ }, [pathname, router]);
869
+
870
+ const activeThread = useMemo(
871
+ () => threads.find((thread) => thread.id === activeThreadId) ?? null,
872
+ [activeThreadId, threads]
873
+ );
874
+ const messages = activeThread?.messages ?? [];
875
+ const canSubmit = useMemo(() => input.trim().length > 0 && !isStreaming, [input, isStreaming]);
876
+ const fallbackPageContext = useMemo(() => buildFallbackPageContext(pathname), [pathname]);
877
+ const pageContext = cortexAiPageContext?.pageContext ?? fallbackPageContext;
878
+ const hasSuccessfulMutationActivity = useMemo(
879
+ () =>
880
+ toolActivities.some(
881
+ (activity) =>
882
+ activity.status === "success" &&
883
+ isMutatingToolName(activity.name) &&
884
+ toolOutputExecutedMutation(activity.output)
885
+ ),
886
+ [toolActivities]
887
+ );
888
+ const visibleToolActivities = useMemo(
889
+ () =>
890
+ toolActivities.filter((activity, index) => {
891
+ if (hasSuccessfulMutationActivity && activity.status === "error") {
892
+ return false;
893
+ }
894
+
895
+ if (activity.status !== "error") {
896
+ const confirmationKey = getConfirmationKey(activity);
897
+
898
+ if (confirmationKey) {
899
+ if (cancelledConfirmationKeys.has(confirmationKey)) {
900
+ return false;
901
+ }
902
+
903
+ return !toolActivities
904
+ .slice(index + 1)
905
+ .some((nextActivity) => getConfirmationKey(nextActivity) === confirmationKey);
906
+ }
907
+
908
+ return true;
909
+ }
910
+
911
+ return !toolActivities
912
+ .slice(index + 1)
913
+ .some((nextActivity) => nextActivity.status === "success" && !toolOutputIsNotice(nextActivity.output));
914
+ }),
915
+ [cancelledConfirmationKeys, hasSuccessfulMutationActivity, toolActivities]
916
+ );
917
+
918
+ const updateThreadMessages = (
919
+ threadId: string,
920
+ updater: (currentMessages: ChatMessage[]) => ChatMessage[]
921
+ ) => {
922
+ setThreads((currentThreads) => {
923
+ let updatedThread: ChatThread | null = null;
924
+ const remainingThreads: ChatThread[] = [];
925
+
926
+ for (const thread of currentThreads) {
927
+ if (thread.id === threadId) {
928
+ const nextMessages = updater(thread.messages).slice(-MAX_STORED_MESSAGES);
929
+ updatedThread = {
930
+ ...thread,
931
+ messages: nextMessages,
932
+ title: getThreadTitle(nextMessages),
933
+ updatedAt: new Date().toISOString(),
934
+ };
935
+ } else {
936
+ remainingThreads.push(thread);
937
+ }
938
+ }
939
+
940
+ if (!updatedThread) {
941
+ return currentThreads;
942
+ }
943
+
944
+ return [updatedThread, ...remainingThreads].slice(0, MAX_STORED_THREADS);
945
+ });
946
+ };
947
+
948
+ const createNewThread = () => {
949
+ if (isStreaming) {
950
+ return;
951
+ }
952
+
953
+ const thread = createChatThread();
954
+
955
+ setThreads((currentThreads) => [thread, ...currentThreads].slice(0, MAX_STORED_THREADS));
956
+ setActiveThreadId(thread.id);
957
+ setInput("");
958
+ setStreamError(null);
959
+ setToolActivities([]);
960
+ setCancelledConfirmationKeys(new Set());
961
+ setShowHistory(false);
962
+ };
963
+
964
+ const selectThread = (threadId: string) => {
965
+ if (isStreaming) {
966
+ return;
967
+ }
968
+
969
+ setActiveThreadId(threadId);
970
+ setStreamError(null);
971
+ setToolActivities([]);
972
+ setCancelledConfirmationKeys(new Set());
973
+ setShowHistory(false);
974
+ };
975
+
976
+ const deleteThread = (threadId: string) => {
977
+ if (isStreaming) {
978
+ return;
979
+ }
980
+
981
+ const nextThreads = threads.filter((thread) => thread.id !== threadId);
982
+ const normalizedThreads = nextThreads.length > 0 ? nextThreads : [createChatThread()];
983
+
984
+ setThreads(normalizedThreads);
985
+
986
+ if (activeThreadId === threadId || !normalizedThreads.some((thread) => thread.id === activeThreadId)) {
987
+ setActiveThreadId(normalizedThreads[0]?.id ?? null);
988
+ setStreamError(null);
989
+ setToolActivities([]);
990
+ }
991
+ };
992
+
993
+ const applyStreamEvent = (
994
+ event: CortexAgentStreamEvent,
995
+ assistantId: string,
996
+ threadId: string
997
+ ) => {
998
+ if (event.type === "meta") {
999
+ setMetadata({
1000
+ credentialSource: event.credentialSource,
1001
+ modelId: event.modelId,
1002
+ });
1003
+ return;
1004
+ }
1005
+
1006
+ if (event.type === "text-delta") {
1007
+ updateThreadMessages(threadId, (currentMessages) =>
1008
+ currentMessages.map((message) =>
1009
+ message.id === assistantId
1010
+ ? { ...message, content: `${message.content}${event.text}` }
1011
+ : message
1012
+ )
1013
+ );
1014
+ return;
1015
+ }
1016
+
1017
+ if (event.type === "tool-call") {
1018
+ const id = getToolActivityId(event);
1019
+ setToolActivities((current) => [
1020
+ ...current,
1021
+ {
1022
+ id,
1023
+ input: event.input,
1024
+ name: event.toolName,
1025
+ status: "running",
1026
+ },
1027
+ ]);
1028
+ return;
1029
+ }
1030
+
1031
+ if (event.type === "tool-result") {
1032
+ if (isRecord(event.output) && event.output.success !== false) {
1033
+ setStreamError(null);
1034
+ }
1035
+
1036
+ setToolActivities((current) => {
1037
+ const activityIndex = findMatchingToolActivityIndex(current, event);
1038
+
1039
+ if (activityIndex < 0) {
1040
+ return [
1041
+ ...current,
1042
+ {
1043
+ id: getToolActivityId(event),
1044
+ name: event.toolName,
1045
+ output: event.output,
1046
+ status: "success",
1047
+ },
1048
+ ];
1049
+ }
1050
+
1051
+ return current.map((activity, index) =>
1052
+ index === activityIndex
1053
+ ? {
1054
+ ...activity,
1055
+ output: event.output,
1056
+ status: "success",
1057
+ }
1058
+ : activity
1059
+ );
1060
+ });
1061
+ return;
1062
+ }
1063
+
1064
+ if (event.type === "tool-error") {
1065
+ setToolActivities((current) => {
1066
+ const activityIndex = findMatchingToolActivityIndex(current, event);
1067
+
1068
+ if (activityIndex < 0) {
1069
+ return [
1070
+ ...current,
1071
+ {
1072
+ id: getToolActivityId(event),
1073
+ name: event.toolName || "tool",
1074
+ output: { error: event.message },
1075
+ status: "error",
1076
+ },
1077
+ ];
1078
+ }
1079
+
1080
+ return current.map((activity, index) =>
1081
+ index === activityIndex
1082
+ ? {
1083
+ ...activity,
1084
+ output: { error: event.message },
1085
+ status: "error",
1086
+ }
1087
+ : activity
1088
+ );
1089
+ });
1090
+ setStreamError(event.message);
1091
+ return;
1092
+ }
1093
+
1094
+ if (event.type === "error") {
1095
+ setStreamError(event.message);
1096
+ updateThreadMessages(threadId, (currentMessages) =>
1097
+ currentMessages.map((message) =>
1098
+ message.id === assistantId && !message.content
1099
+ ? { ...message, content: event.message }
1100
+ : message
1101
+ )
1102
+ );
1103
+ }
1104
+ };
1105
+
1106
+ const sendMessage = async (options?: {
1107
+ confirmedToolCall?: ConfirmedToolCall;
1108
+ displayContent?: string;
1109
+ prompt?: string;
1110
+ }) => {
1111
+ const prompt = (options?.prompt ?? input).trim();
1112
+ const displayContent = (options?.displayContent ?? prompt).trim();
1113
+
1114
+ if (!prompt || isStreaming) {
1115
+ return;
1116
+ }
1117
+
1118
+ const userMessage: ChatMessage = {
1119
+ content: displayContent || prompt,
1120
+ id: createId(),
1121
+ role: "user",
1122
+ };
1123
+ const assistantMessage: ChatMessage = {
1124
+ content: "",
1125
+ id: createId(),
1126
+ role: "assistant",
1127
+ };
1128
+ let threadId = activeThread?.id ?? activeThreadId;
1129
+ const currentMessages = activeThread?.messages ?? [];
1130
+ const requestMessages = [...currentMessages, userMessage].slice(-20).map(({ content, role }) => ({
1131
+ content,
1132
+ role,
1133
+ }));
1134
+ const abortController = new AbortController();
1135
+ let timedOut = false;
1136
+ const timeoutId = window.setTimeout(() => {
1137
+ timedOut = true;
1138
+ abortController.abort();
1139
+ }, REQUEST_TIMEOUT_MS);
1140
+
1141
+ if (!threadId) {
1142
+ const thread = createChatThread([userMessage, assistantMessage]);
1143
+
1144
+ threadId = thread.id;
1145
+ setThreads((currentThreads) => [thread, ...currentThreads].slice(0, MAX_STORED_THREADS));
1146
+ setActiveThreadId(thread.id);
1147
+ } else {
1148
+ updateThreadMessages(threadId, (threadMessages) => [
1149
+ ...threadMessages,
1150
+ userMessage,
1151
+ assistantMessage,
1152
+ ]);
1153
+ }
1154
+
1155
+ abortControllerRef.current = abortController;
1156
+ setInput("");
1157
+ setStreamError(null);
1158
+ setToolActivities([]);
1159
+ setCancelledConfirmationKeys(new Set());
1160
+ setIsStreaming(true);
1161
+ setShowHistory(false);
1162
+ let shouldRefreshAfterMutation = false;
1163
+ let navigationPath: string | null = null;
1164
+
1165
+ try {
1166
+ const headers: Record<string, string> = {
1167
+ "Content-Type": "application/json",
1168
+ };
1169
+
1170
+ if (process.env.NEXT_PUBLIC_IS_SANDBOX === "true") {
1171
+ const sandboxKey = window.localStorage.getItem("cortex_ai_sandbox_openrouter_api_key");
1172
+ const sandboxModel = window.localStorage.getItem("cortex_ai_sandbox_openrouter_model_selection");
1173
+ if (sandboxKey) {
1174
+ headers["x-sandbox-openrouter-key"] = sandboxKey;
1175
+ }
1176
+ if (sandboxModel) {
1177
+ headers["x-sandbox-openrouter-model"] = sandboxModel;
1178
+ }
1179
+ }
1180
+
1181
+ const response = await fetch("/api/ai/global-agent", {
1182
+ body: JSON.stringify({
1183
+ ...(options?.confirmedToolCall ? { confirmedToolCall: options.confirmedToolCall } : {}),
1184
+ messages: requestMessages,
1185
+ ...(pageContext ? { pageContext } : {}),
1186
+ }),
1187
+ headers,
1188
+ method: "POST",
1189
+ signal: abortController.signal,
1190
+ });
1191
+
1192
+ if (!response.ok || !response.body) {
1193
+ const errorPayload = await response.json().catch(() => null);
1194
+ throw new Error(errorPayload?.error || "Cortex AI request failed.");
1195
+ }
1196
+
1197
+ const reader = response.body.getReader();
1198
+ const decoder = new TextDecoder();
1199
+ let buffer = "";
1200
+ let sawFinish = false;
1201
+
1202
+ while (!sawFinish) {
1203
+ const { done, value } = await reader.read();
1204
+
1205
+ if (done) {
1206
+ break;
1207
+ }
1208
+
1209
+ buffer += decoder.decode(value, { stream: true });
1210
+ const frames = buffer.split("\n\n");
1211
+ buffer = frames.pop() || "";
1212
+
1213
+ for (const frame of frames) {
1214
+ const event = parseStreamFrame(frame);
1215
+
1216
+ if (event) {
1217
+ if (
1218
+ event.type === "tool-result" &&
1219
+ isMutatingToolName(event.toolName) &&
1220
+ toolOutputExecutedMutation(event.output)
1221
+ ) {
1222
+ navigationPath = getToolOutputNavigationPath(event.output) || navigationPath;
1223
+ shouldRefreshAfterMutation = true;
1224
+ }
1225
+
1226
+ applyStreamEvent(event, assistantMessage.id, threadId);
1227
+
1228
+ if (event.type === "finish") {
1229
+ sawFinish = true;
1230
+ await reader.cancel().catch(() => undefined);
1231
+ break;
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+
1237
+ if (!sawFinish && buffer.trim()) {
1238
+ const event = parseStreamFrame(buffer);
1239
+
1240
+ if (event) {
1241
+ if (
1242
+ event.type === "tool-result" &&
1243
+ isMutatingToolName(event.toolName) &&
1244
+ toolOutputExecutedMutation(event.output)
1245
+ ) {
1246
+ navigationPath = getToolOutputNavigationPath(event.output) || navigationPath;
1247
+ shouldRefreshAfterMutation = true;
1248
+ }
1249
+
1250
+ applyStreamEvent(event, assistantMessage.id, threadId);
1251
+ }
1252
+ }
1253
+ } catch (error) {
1254
+ if (error instanceof DOMException && error.name === "AbortError") {
1255
+ if (!timedOut) {
1256
+ return;
1257
+ }
1258
+ }
1259
+
1260
+ const message = timedOut
1261
+ ? "Cortex AI took too long to respond. Please try again."
1262
+ : error instanceof Error
1263
+ ? error.message
1264
+ : "Cortex AI request failed.";
1265
+ setStreamError(message);
1266
+ setToolActivities((current) =>
1267
+ current.map((activity) =>
1268
+ activity.status === "running"
1269
+ ? {
1270
+ ...activity,
1271
+ output: { error: message },
1272
+ status: "error",
1273
+ }
1274
+ : activity
1275
+ )
1276
+ );
1277
+ updateThreadMessages(threadId, (threadMessages) =>
1278
+ threadMessages.map((chatMessage) =>
1279
+ chatMessage.id === assistantMessage.id
1280
+ ? { ...chatMessage, content: chatMessage.content || message }
1281
+ : chatMessage
1282
+ )
1283
+ );
1284
+ } finally {
1285
+ window.clearTimeout(timeoutId);
1286
+ if (shouldRefreshAfterMutation && typeof window !== "undefined") {
1287
+ // Let client-rendered lists (e.g. the custom blocks library) refetch even
1288
+ // though router.refresh() does not re-run their mount-time data fetch.
1289
+ window.dispatchEvent(new Event(CORTEX_DATA_CHANGED_EVENT));
1290
+ }
1291
+ if (navigationPath) {
1292
+ if (shouldRefreshAfterMutation) {
1293
+ pendingRefreshPathRef.current = navigationPath;
1294
+ }
1295
+
1296
+ router.push(navigationPath);
1297
+
1298
+ if (shouldRefreshAfterMutation && pathname === navigationPath) {
1299
+ pendingRefreshPathRef.current = null;
1300
+ router.refresh();
1301
+ }
1302
+ } else if (shouldRefreshAfterMutation) {
1303
+ router.refresh();
1304
+ }
1305
+ setIsStreaming(false);
1306
+ abortControllerRef.current = null;
1307
+ }
1308
+ };
1309
+
1310
+ const stopStreaming = () => {
1311
+ abortControllerRef.current?.abort();
1312
+ setIsStreaming(false);
1313
+ };
1314
+
1315
+ const cancelToolCall = (activity: ToolActivity) => {
1316
+ const confirmationKey = getConfirmationKey(activity);
1317
+
1318
+ if (confirmationKey) {
1319
+ setCancelledConfirmationKeys((current) => {
1320
+ const next = new Set(current);
1321
+ next.add(confirmationKey);
1322
+ return next;
1323
+ });
1324
+ }
1325
+ };
1326
+
1327
+ const confirmToolCall = (toolCall: ConfirmedToolCall) => {
1328
+ void sendMessage({
1329
+ confirmedToolCall: toolCall,
1330
+ displayContent: "Confirmed",
1331
+ prompt: toolCall.confirmationPhrase,
1332
+ });
1333
+ };
1334
+
1335
+ if (!isMounted) {
1336
+ return null;
1337
+ }
1338
+
1339
+ return (
1340
+ <>
1341
+ <Button
1342
+ aria-label="NextBlock Cortex AI"
1343
+ className="fixed bottom-20 right-4 z-50 h-12 w-12 rounded-full shadow-lg md:bottom-5"
1344
+ onClick={() => setOpen((current) => !current)}
1345
+ size="icon"
1346
+ title="NextBlock Cortex AI"
1347
+ >
1348
+ <Brain className="h-6 w-6" />
1349
+ </Button>
1350
+
1351
+ {open && (
1352
+ <div
1353
+ className="fixed inset-0 z-40 bg-black/20 md:pointer-events-none md:bg-transparent"
1354
+ onClick={() => setOpen(false)}
1355
+ />
1356
+ )}
1357
+
1358
+ <aside
1359
+ className={cn(
1360
+ "fixed bottom-0 right-0 top-16 z-50 flex w-[calc(100vw-1rem)] translate-x-full flex-col overflow-hidden border-l border-slate-200 bg-background shadow-2xl transition-transform duration-300 dark:border-slate-800 sm:max-w-md md:top-0",
1361
+ open && "translate-x-0"
1362
+ )}
1363
+ >
1364
+ <div className="border-b border-slate-200 px-4 py-4 dark:border-slate-800">
1365
+ <div className="flex items-center gap-3">
1366
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
1367
+ <Brain className="h-5 w-5" />
1368
+ </div>
1369
+ <div className="min-w-0 flex-1">
1370
+ <h2 className="truncate text-base font-semibold text-foreground">
1371
+ NextBlock Cortex AI
1372
+ </h2>
1373
+ </div>
1374
+ <Button
1375
+ disabled={isStreaming}
1376
+ onClick={() => setShowHistory((current) => !current)}
1377
+ size="icon"
1378
+ title="Chat history"
1379
+ variant={showHistory ? "outline" : "ghost"}
1380
+ >
1381
+ <History className="h-4 w-4" />
1382
+ </Button>
1383
+ <Button
1384
+ disabled={isStreaming}
1385
+ onClick={createNewThread}
1386
+ size="icon"
1387
+ title="New chat"
1388
+ variant="ghost"
1389
+ >
1390
+ <MessageSquarePlus className="h-4 w-4" />
1391
+ </Button>
1392
+ <Button
1393
+ onClick={() => setOpen(false)}
1394
+ size="icon"
1395
+ title="Close"
1396
+ variant="ghost"
1397
+ >
1398
+ <XCircle className="h-4 w-4" />
1399
+ </Button>
1400
+ </div>
1401
+ </div>
1402
+
1403
+ {showHistory && (
1404
+ <div className="max-h-64 overflow-y-auto border-b border-slate-200 bg-slate-50 px-3 py-3 dark:border-slate-800 dark:bg-slate-950">
1405
+ <div className="space-y-1">
1406
+ {threads.map((thread) => {
1407
+ const isActive = thread.id === activeThreadId;
1408
+
1409
+ return (
1410
+ <div
1411
+ className={cn(
1412
+ "flex items-center gap-2 rounded-md px-2 py-1.5",
1413
+ isActive && "bg-white shadow-sm dark:bg-slate-900"
1414
+ )}
1415
+ key={thread.id}
1416
+ >
1417
+ <button
1418
+ className="min-w-0 flex-1 text-left"
1419
+ disabled={isStreaming}
1420
+ onClick={() => selectThread(thread.id)}
1421
+ type="button"
1422
+ >
1423
+ <span className="block truncate text-sm font-medium text-slate-800 dark:text-slate-100">
1424
+ {thread.title}
1425
+ </span>
1426
+ <span className="block truncate text-xs text-slate-500 dark:text-slate-400">
1427
+ {formatThreadTime(thread.updatedAt)}
1428
+ </span>
1429
+ </button>
1430
+ <Button
1431
+ disabled={isStreaming}
1432
+ onClick={() => deleteThread(thread.id)}
1433
+ size="icon"
1434
+ title="Delete chat"
1435
+ variant="ghost"
1436
+ >
1437
+ <Trash2 className="h-4 w-4" />
1438
+ </Button>
1439
+ </div>
1440
+ );
1441
+ })}
1442
+ </div>
1443
+ </div>
1444
+ )}
1445
+
1446
+ <div ref={scrollAreaRef} className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-4">
1447
+ {messages.length === 0 ? (
1448
+ <div className="flex h-full min-h-[320px] items-center justify-center text-center text-sm text-slate-500 dark:text-slate-400">
1449
+ {activeThread?.title || "New chat"}
1450
+ </div>
1451
+ ) : (
1452
+ messages.map((message) => <MessageBubble key={message.id} message={message} />)
1453
+ )}
1454
+
1455
+ {visibleToolActivities.length > 0 && (
1456
+ <div className="space-y-2">
1457
+ {visibleToolActivities.map((activity) => (
1458
+ <ToolActivityRow
1459
+ key={activity.id}
1460
+ activity={activity}
1461
+ disabled={isStreaming}
1462
+ onCancel={cancelToolCall}
1463
+ onConfirm={confirmToolCall}
1464
+ />
1465
+ ))}
1466
+ </div>
1467
+ )}
1468
+ </div>
1469
+
1470
+ {streamError && (
1471
+ <div className="border-t border-destructive/20 bg-destructive/10 px-4 py-2 text-sm text-destructive">
1472
+ {streamError}
1473
+ </div>
1474
+ )}
1475
+
1476
+ <div className="border-t border-slate-200 p-4 dark:border-slate-800">
1477
+ <div className="flex items-end gap-2">
1478
+ <Textarea
1479
+ className="max-h-36 min-h-12 resize-none rounded-lg text-sm"
1480
+ disabled={isStreaming}
1481
+ onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
1482
+ setInput(event.target.value)
1483
+ }
1484
+ onKeyDown={(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
1485
+ if (event.key === "Enter" && !event.shiftKey) {
1486
+ event.preventDefault();
1487
+ void sendMessage();
1488
+ }
1489
+ }}
1490
+ placeholder="Message Cortex AI..."
1491
+ value={input}
1492
+ />
1493
+ {isStreaming ? (
1494
+ <Button onClick={stopStreaming} size="icon" variant="outline" title="Stop">
1495
+ <XCircle className="h-4 w-4" />
1496
+ </Button>
1497
+ ) : (
1498
+ <Button disabled={!canSubmit} onClick={() => void sendMessage()} size="icon">
1499
+ <Send className="h-4 w-4" />
1500
+ </Button>
1501
+ )}
1502
+ </div>
1503
+ </div>
1504
+ </aside>
1505
+ </>
1506
+ );
1507
+ }