create-nextblock 0.2.78 → 0.8.0
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.
- package/bin/create-nextblock.js +740 -459
- package/package.json +1 -2
- package/scripts/sync-template.js +18 -1
- package/templates/nextblock-template/.browserslistrc +11 -0
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/README.md +23 -114
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
- package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
- package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
- package/templates/nextblock-template/app/actions/consent.ts +57 -0
- package/templates/nextblock-template/app/actions/formActions.ts +130 -11
- package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
- package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
- package/templates/nextblock-template/app/actions/postActions.ts +146 -48
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
- package/templates/nextblock-template/app/actions.ts +67 -12
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
- package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
- package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
- package/templates/nextblock-template/app/api/draft/route.ts +93 -0
- package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
- package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
- package/templates/nextblock-template/app/api/search/route.ts +43 -0
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
- package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
- package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
- package/templates/nextblock-template/app/cart/page.tsx +7 -0
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
- package/templates/nextblock-template/app/checkout/page.tsx +52 -0
- package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
- package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
- package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
- package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
- package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
- package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
- package/templates/nextblock-template/app/cms/layout.tsx +29 -10
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
- package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
- package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
- package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
- package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
- package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
- package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
- package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
- package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
- package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
- package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
- package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
- package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
- package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
- package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
- package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
- package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
- package/templates/nextblock-template/app/globals.css +9 -0
- package/templates/nextblock-template/app/layout.tsx +372 -120
- package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
- package/templates/nextblock-template/app/lib/seo.ts +279 -0
- package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
- package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
- package/templates/nextblock-template/app/page.tsx +165 -73
- package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
- package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
- package/templates/nextblock-template/app/profile/account-data.ts +37 -0
- package/templates/nextblock-template/app/profile/account-links.ts +22 -0
- package/templates/nextblock-template/app/profile/account-types.ts +11 -0
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
- package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
- package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
- package/templates/nextblock-template/app/profile/page.tsx +51 -0
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
- package/templates/nextblock-template/app/providers.tsx +55 -17
- package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
- package/templates/nextblock-template/app/sitemap.ts +128 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
- package/templates/nextblock-template/components/AppShell.tsx +154 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
- package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
- package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
- package/templates/nextblock-template/components/Header.tsx +49 -41
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
- package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
- package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/types.ts +7 -6
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -26
- package/templates/nextblock-template/components/header-auth.tsx +69 -17
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
- package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
- package/templates/nextblock-template/context/AuthContext.tsx +23 -90
- package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
- package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
- package/templates/nextblock-template/context/language-rest-client.ts +31 -0
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
- package/templates/nextblock-template/docs/README.md +34 -0
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
- package/templates/nextblock-template/index.d.ts +2 -0
- package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
- package/templates/nextblock-template/lib/ai-client.ts +247 -0
- package/templates/nextblock-template/lib/ai-config.ts +81 -0
- package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
- package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
- package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
- package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
- package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
- package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
- package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
- package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
- package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
- package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
- package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
- package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
- package/templates/nextblock-template/lib/blocks/README.md +13 -670
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
- package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
- package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
- package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
- package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
- package/templates/nextblock-template/lib/privacy/types.ts +67 -0
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
- package/templates/nextblock-template/lib/promotions/server.ts +741 -0
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
- package/templates/nextblock-template/lib/search/server.ts +585 -0
- package/templates/nextblock-template/lib/search/types.ts +27 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
- package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
- package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
- package/templates/nextblock-template/lib/zod-config.ts +5 -0
- package/templates/nextblock-template/next.config.js +190 -66
- package/templates/nextblock-template/package.json +34 -30
- package/templates/nextblock-template/proxy.ts +435 -253
- package/templates/nextblock-template/public/images/NBcover.webp +0 -0
- package/templates/nextblock-template/public/images/cap.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
- package/templates/nextblock-template/public/images/extensibility.webp +0 -0
- package/templates/nextblock-template/public/images/goals.webp +0 -0
- package/templates/nextblock-template/public/images/included.webp +0 -0
- package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
- package/templates/nextblock-template/public/images/pants.webp +0 -0
- package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
- package/templates/nextblock-template/tailwind.config.js +1 -0
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
- package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
- package/templates/nextblock-template/tsconfig.json +52 -66
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -0
- package/templates/nextblock-template/app/force-styles.tsx +0 -31
- package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
- package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
- package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
- package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
- package/templates/nextblock-template/docs/files-structure.md +0 -426
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
|
@@ -0,0 +1,4807 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { createCortexDatabaseAgentTools } from './ai-global-agent-db-tools';
|
|
3
|
+
import { createCortexCustomBlockTools } from './ai-global-agent-custom-block-tools';
|
|
4
|
+
import { z } from './zod-config';
|
|
5
|
+
|
|
6
|
+
export const availableCortexAiBlockTypes = [
|
|
7
|
+
'text',
|
|
8
|
+
'heading',
|
|
9
|
+
'image',
|
|
10
|
+
'button',
|
|
11
|
+
'posts_grid',
|
|
12
|
+
'video_embed',
|
|
13
|
+
'section',
|
|
14
|
+
'form',
|
|
15
|
+
'testimonial',
|
|
16
|
+
'product_grid',
|
|
17
|
+
'featured_product',
|
|
18
|
+
'cart',
|
|
19
|
+
'checkout',
|
|
20
|
+
'product_details',
|
|
21
|
+
] as const;
|
|
22
|
+
type BlockType = (typeof availableCortexAiBlockTypes)[number];
|
|
23
|
+
type ColumnBlock = { block_type: BlockType; content: Record<string, unknown>; temp_id?: string };
|
|
24
|
+
type SectionBlockContent = Record<string, any> & {
|
|
25
|
+
column_blocks: Array<Array<ColumnBlock>>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type SupabaseLike = {
|
|
29
|
+
from: (table: string) => any;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type RevalidateFn = (path: string, type?: 'layout' | 'page') => void;
|
|
33
|
+
type MenuKey = 'HEADER' | 'FOOTER';
|
|
34
|
+
type CmsContentType = 'page' | 'post' | 'product';
|
|
35
|
+
|
|
36
|
+
type ToolExecutionContext = {
|
|
37
|
+
actorUserId?: string | null;
|
|
38
|
+
cortexAiApiKey?: string | null;
|
|
39
|
+
cortexAiModelSelection?: unknown;
|
|
40
|
+
latestUserMessage?: string | null;
|
|
41
|
+
pageContext?: CortexAiPageContext | null;
|
|
42
|
+
revalidatePath?: RevalidateFn;
|
|
43
|
+
skipConfirmation?: boolean;
|
|
44
|
+
supabase?: SupabaseLike;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const SEARCH_DOCUMENTATION_TIMEOUT_MS = 10000;
|
|
48
|
+
|
|
49
|
+
const LANGUAGE_NAME_ALIASES: Record<string, string> = {
|
|
50
|
+
arabic: 'ar',
|
|
51
|
+
chinese: 'zh',
|
|
52
|
+
dutch: 'nl',
|
|
53
|
+
english: 'en',
|
|
54
|
+
french: 'fr',
|
|
55
|
+
francaise: 'fr',
|
|
56
|
+
francais: 'fr',
|
|
57
|
+
german: 'de',
|
|
58
|
+
italian: 'it',
|
|
59
|
+
japanese: 'ja',
|
|
60
|
+
korean: 'ko',
|
|
61
|
+
portuguese: 'pt',
|
|
62
|
+
russian: 'ru',
|
|
63
|
+
spanish: 'es',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const urlSchema = z
|
|
67
|
+
.string()
|
|
68
|
+
.trim()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(2048)
|
|
71
|
+
.refine(
|
|
72
|
+
(value) =>
|
|
73
|
+
value.startsWith('/') ||
|
|
74
|
+
value.startsWith('#') ||
|
|
75
|
+
value.startsWith('http://') ||
|
|
76
|
+
value.startsWith('https://') ||
|
|
77
|
+
value.startsWith('mailto:') ||
|
|
78
|
+
value.startsWith('tel:'),
|
|
79
|
+
'URL must be a relative path, hash link, http(s) URL, mailto URL, or tel URL.'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const navigationChildItemSchema = z.strictObject({
|
|
83
|
+
label: z.string().trim().min(1).max(120),
|
|
84
|
+
target: z.enum(['_self', '_blank']).optional(),
|
|
85
|
+
url: urlSchema,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const navigationItemInputSchema = navigationChildItemSchema.extend({
|
|
89
|
+
children: z.array(navigationChildItemSchema).max(20).optional(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const navigationItemMatchSchema = z
|
|
93
|
+
.strictObject({
|
|
94
|
+
label: z.string().trim().min(1).max(120).optional(),
|
|
95
|
+
url: urlSchema.optional(),
|
|
96
|
+
})
|
|
97
|
+
.refine((value) => Boolean(value.label || value.url), {
|
|
98
|
+
message: 'Navigation item match requires label or url.',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const updateNavigationBarInputSchema = z.strictObject({
|
|
102
|
+
items: z.array(navigationItemInputSchema).min(1).max(30),
|
|
103
|
+
languageCode: z
|
|
104
|
+
.string()
|
|
105
|
+
.trim()
|
|
106
|
+
.min(2)
|
|
107
|
+
.max(80)
|
|
108
|
+
.default('en')
|
|
109
|
+
.describe('Locale code or language name, for example "en", "fr", "English", or "French".'),
|
|
110
|
+
match: navigationItemMatchSchema
|
|
111
|
+
.optional()
|
|
112
|
+
.describe('For mode "update", identifies the existing navigation item to update.'),
|
|
113
|
+
mode: z.enum(['append', 'replace', 'update']).default('append'),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const updateFooterInputSchema = z.strictObject({
|
|
117
|
+
copyright: z.record(z.string().trim().min(2).max(12), z.string().trim().min(1).max(500)).optional(),
|
|
118
|
+
languageCode: z
|
|
119
|
+
.string()
|
|
120
|
+
.trim()
|
|
121
|
+
.min(2)
|
|
122
|
+
.max(80)
|
|
123
|
+
.default('en')
|
|
124
|
+
.describe('Locale code or language name, for example "en", "fr", "English", or "French".'),
|
|
125
|
+
links: z.array(navigationItemInputSchema).min(1).max(30).optional(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const searchDocumentationInputSchema = z.strictObject({
|
|
129
|
+
limit: z.number().int().min(1).max(8).default(4),
|
|
130
|
+
query: z.string().trim().min(2).max(300),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const fetchEcommerceStatsInputSchema = z.object({
|
|
134
|
+
currency: z
|
|
135
|
+
.string()
|
|
136
|
+
.trim()
|
|
137
|
+
.min(3)
|
|
138
|
+
.max(3)
|
|
139
|
+
.optional()
|
|
140
|
+
.describe(
|
|
141
|
+
'Optional currency code for currency-specific monetary reports (e.g., USD, CAD). Do not set this for plain order-status counts unless the user asks for a currency.'
|
|
142
|
+
),
|
|
143
|
+
query: z.string().describe('The analytical question about orders, products, or revenue.'),
|
|
144
|
+
reportType: z
|
|
145
|
+
.enum(['revenue', 'orders', 'products', 'general'])
|
|
146
|
+
.optional()
|
|
147
|
+
.default('general')
|
|
148
|
+
.describe('The focus area of the statistical report.'),
|
|
149
|
+
timeRange: z
|
|
150
|
+
.enum(['today', 'this_month', 'last_7_days', 'last_30_days', 'last_month', 'last_90_days', 'all_time'])
|
|
151
|
+
.optional()
|
|
152
|
+
.default('all_time')
|
|
153
|
+
.describe('The time period for the report. Use all_time for current order-status counts unless the user names a specific period.'),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const cortexAiPageContextSchema = z.strictObject({
|
|
157
|
+
contentType: z.enum(['page', 'post', 'product']),
|
|
158
|
+
currentEditor: z
|
|
159
|
+
.strictObject({
|
|
160
|
+
blockId: z.union([z.number().int().positive(), z.string().trim().min(1).max(120)]).nullable().optional(),
|
|
161
|
+
blockType: z.string().trim().min(1).max(80).nullable().optional(),
|
|
162
|
+
field: z.string().trim().min(1).max(120).nullable().optional(),
|
|
163
|
+
})
|
|
164
|
+
.optional(),
|
|
165
|
+
entityId: z.union([z.number().int().positive(), z.string().trim().min(1).max(120)]),
|
|
166
|
+
languageId: z.number().int().positive().nullable().optional(),
|
|
167
|
+
slug: z.string().trim().min(1).max(300).nullable().optional(),
|
|
168
|
+
title: z.string().trim().min(1).max(300).nullable().optional(),
|
|
169
|
+
translationGroupId: z.string().trim().min(1).max(120).nullable().optional(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const readCurrentCmsItemInputSchema = z.strictObject({
|
|
173
|
+
includeBlockContent: z.boolean().default(false),
|
|
174
|
+
includeBlocks: z.boolean().default(true),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const updateCurrentCmsFieldsInputSchema = z.strictObject({
|
|
178
|
+
fields: z
|
|
179
|
+
.strictObject({
|
|
180
|
+
description_json: z.unknown().optional(),
|
|
181
|
+
excerpt: z.string().max(2000).nullable().optional(),
|
|
182
|
+
feature_image_id: z.string().trim().min(1).max(120).nullable().optional(),
|
|
183
|
+
label: z.string().max(120).nullable().optional(),
|
|
184
|
+
meta_description: z.string().max(500).nullable().optional(),
|
|
185
|
+
meta_title: z.string().max(160).nullable().optional(),
|
|
186
|
+
published_at: z.string().max(80).nullable().optional(),
|
|
187
|
+
short_description: z.string().max(2000).nullable().optional(),
|
|
188
|
+
slug: z.string().trim().min(1).max(300).optional(),
|
|
189
|
+
status: z.enum(['draft', 'published', 'active', 'archived']).optional(),
|
|
190
|
+
subtitle: z.string().max(300).nullable().optional(),
|
|
191
|
+
title: z.string().trim().min(1).max(300).optional(),
|
|
192
|
+
})
|
|
193
|
+
.partial(),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export const updateContentBlockInputSchema = z.strictObject({
|
|
197
|
+
blockId: z.number().int().positive(),
|
|
198
|
+
blockType: z.enum(availableCortexAiBlockTypes).optional(),
|
|
199
|
+
content: z.record(z.string(), z.unknown()),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
export const updateSectionColumnBlockInputSchema = z.strictObject({
|
|
203
|
+
blockIndex: z.number().int().min(0),
|
|
204
|
+
blockType: z.enum(availableCortexAiBlockTypes).optional(),
|
|
205
|
+
columnIndex: z.number().int().min(0),
|
|
206
|
+
content: z.record(z.string(), z.unknown()),
|
|
207
|
+
parentBlockId: z.number().int().positive(),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const cmsContentTypeSchema = z.enum(['page', 'post', 'product']);
|
|
211
|
+
const cmsTargetInputSchema = z.strictObject({
|
|
212
|
+
contentType: cmsContentTypeSchema.optional(),
|
|
213
|
+
entityId: z.union([z.number().int().positive(), z.string().trim().min(1).max(120)]).optional(),
|
|
214
|
+
slug: z.string().trim().min(1).max(300).optional(),
|
|
215
|
+
title: z.string().trim().min(1).max(300).optional(),
|
|
216
|
+
});
|
|
217
|
+
const createCmsBlockInputSchema = z.strictObject({
|
|
218
|
+
blockType: z.enum(availableCortexAiBlockTypes),
|
|
219
|
+
content: z.record(z.string(), z.unknown()),
|
|
220
|
+
order: z.number().int().min(0).optional(),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
export const insertContentBlockInputSchema = cmsTargetInputSchema.extend({
|
|
224
|
+
anchorBlockId: z.number().int().positive().optional(),
|
|
225
|
+
anchorBlockType: z.enum(availableCortexAiBlockTypes).optional(),
|
|
226
|
+
block: createCmsBlockInputSchema,
|
|
227
|
+
position: z.enum(['before', 'after', 'start', 'end']).default('end'),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
export const createCmsPageInputSchema = z.strictObject({
|
|
231
|
+
blocks: z.array(createCmsBlockInputSchema).max(20).optional(),
|
|
232
|
+
contactEmail: z.string().email().optional(),
|
|
233
|
+
feature_image_id: z.string().trim().min(1).max(120).nullable().optional(),
|
|
234
|
+
languageCode: z.string().trim().min(2).max(80).optional(),
|
|
235
|
+
meta_description: z.string().max(500).nullable().optional(),
|
|
236
|
+
meta_title: z.string().max(160).nullable().optional(),
|
|
237
|
+
slug: z.string().trim().min(1).max(300).optional(),
|
|
238
|
+
status: z.enum(['draft', 'published', 'archived']).default('draft'),
|
|
239
|
+
title: z.string().trim().min(1).max(300),
|
|
240
|
+
translationGroupId: z.string().trim().min(1).max(120).optional(),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
export const createCmsPostInputSchema = z.strictObject({
|
|
244
|
+
blocks: z.array(createCmsBlockInputSchema).max(20).optional(),
|
|
245
|
+
excerpt: z.string().max(2000).nullable().optional(),
|
|
246
|
+
feature_image_id: z.string().trim().min(1).max(120).nullable().optional(),
|
|
247
|
+
label: z.string().max(120).nullable().optional(),
|
|
248
|
+
languageCode: z.string().trim().min(2).max(80).optional(),
|
|
249
|
+
meta_description: z.string().max(500).nullable().optional(),
|
|
250
|
+
meta_title: z.string().max(160).nullable().optional(),
|
|
251
|
+
published_at: z.string().max(80).nullable().optional(),
|
|
252
|
+
slug: z.string().trim().min(1).max(300).optional(),
|
|
253
|
+
status: z.enum(['draft', 'published', 'archived']).default('draft'),
|
|
254
|
+
subtitle: z.string().max(300).nullable().optional(),
|
|
255
|
+
title: z.string().trim().min(1).max(300),
|
|
256
|
+
translationGroupId: z.string().trim().min(1).max(120).optional(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export const createCmsProductInputSchema = z.strictObject({
|
|
260
|
+
description_json: z.unknown().optional(),
|
|
261
|
+
freemius_plan_id: z.string().optional(),
|
|
262
|
+
freemius_product_id: z.string().optional(),
|
|
263
|
+
is_taxable: z.boolean().default(true),
|
|
264
|
+
languageCode: z.string().trim().min(2).max(80).optional(),
|
|
265
|
+
meta_description: z.string().max(500).nullable().optional(),
|
|
266
|
+
meta_title: z.string().max(160).nullable().optional(),
|
|
267
|
+
payment_provider: z.enum(['stripe', 'freemius']).default('stripe'),
|
|
268
|
+
price: z.number().min(0).default(0),
|
|
269
|
+
prices: z.record(z.string(), z.number().min(0)).optional(),
|
|
270
|
+
product_type: z.enum(['physical', 'digital']).default('physical'),
|
|
271
|
+
sale_price: z.number().min(0).nullable().optional(),
|
|
272
|
+
sale_prices: z.record(z.string(), z.number().min(0).nullable()).optional(),
|
|
273
|
+
short_description: z.string().max(2000).nullable().optional(),
|
|
274
|
+
sku: z.string().trim().min(1).max(120).optional(),
|
|
275
|
+
slug: z.string().trim().min(1).max(300).optional(),
|
|
276
|
+
status: z.enum(['draft', 'active', 'archived']).default('draft'),
|
|
277
|
+
stock: z.number().int().min(0).default(0),
|
|
278
|
+
title: z.string().trim().min(1).max(300),
|
|
279
|
+
trial_period_days: z.number().int().min(0).default(0),
|
|
280
|
+
trial_requires_payment_method: z.boolean().default(false),
|
|
281
|
+
translationGroupId: z.string().trim().min(1).max(120).optional(),
|
|
282
|
+
upc: z.string().max(120).nullable().optional(),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
export const updateCmsItemFieldInputSchema = cmsTargetInputSchema.extend({
|
|
286
|
+
currencyCode: z.string().trim().min(3).max(3).optional(),
|
|
287
|
+
endsAt: z.string().max(80).nullable().optional(),
|
|
288
|
+
field: z.string().trim().min(1).max(120),
|
|
289
|
+
startsAt: z.string().max(80).nullable().optional(),
|
|
290
|
+
value: z.unknown(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export const prepareDeleteCmsItemInputSchema = cmsTargetInputSchema;
|
|
294
|
+
export const deleteCmsItemInputSchema = cmsTargetInputSchema;
|
|
295
|
+
|
|
296
|
+
const wrappedCmsActionPlanActionSchema = z.discriminatedUnion('tool', [
|
|
297
|
+
z.strictObject({ input: createCmsPageInputSchema, tool: z.literal('create_cms_page') }),
|
|
298
|
+
z.strictObject({ input: createCmsPostInputSchema, tool: z.literal('create_cms_post') }),
|
|
299
|
+
z.strictObject({ input: createCmsProductInputSchema, tool: z.literal('create_cms_product') }),
|
|
300
|
+
z.strictObject({ input: deleteCmsItemInputSchema, tool: z.literal('delete_cms_item') }),
|
|
301
|
+
z.strictObject({ input: updateCmsItemFieldInputSchema, tool: z.literal('update_cms_item_field') }),
|
|
302
|
+
z.strictObject({ input: updateContentBlockInputSchema, tool: z.literal('update_content_block') }),
|
|
303
|
+
z.strictObject({ input: insertContentBlockInputSchema, tool: z.literal('insert_content_block') }),
|
|
304
|
+
z.strictObject({ input: updateCurrentCmsFieldsInputSchema, tool: z.literal('update_current_cms_fields') }),
|
|
305
|
+
z.strictObject({ input: updateFooterInputSchema, tool: z.literal('update_footer') }),
|
|
306
|
+
z.strictObject({ input: updateNavigationBarInputSchema, tool: z.literal('update_navigation_bar') }),
|
|
307
|
+
z.strictObject({ input: updateSectionColumnBlockInputSchema, tool: z.literal('update_section_column_block') }),
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
const flatCmsActionPlanActionSchema = z.union([
|
|
311
|
+
createCmsPageInputSchema
|
|
312
|
+
.extend({ tool: z.literal('create_cms_page') })
|
|
313
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
314
|
+
createCmsPostInputSchema
|
|
315
|
+
.extend({ tool: z.literal('create_cms_post') })
|
|
316
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
317
|
+
createCmsProductInputSchema
|
|
318
|
+
.extend({ tool: z.literal('create_cms_product') })
|
|
319
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
320
|
+
deleteCmsItemInputSchema
|
|
321
|
+
.extend({ tool: z.literal('delete_cms_item') })
|
|
322
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
323
|
+
updateCmsItemFieldInputSchema
|
|
324
|
+
.extend({ tool: z.literal('update_cms_item_field') })
|
|
325
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
326
|
+
updateContentBlockInputSchema
|
|
327
|
+
.extend({ tool: z.literal('update_content_block') })
|
|
328
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
329
|
+
insertContentBlockInputSchema
|
|
330
|
+
.extend({ tool: z.literal('insert_content_block') })
|
|
331
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
332
|
+
updateCurrentCmsFieldsInputSchema
|
|
333
|
+
.extend({ tool: z.literal('update_current_cms_fields') })
|
|
334
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
335
|
+
updateFooterInputSchema
|
|
336
|
+
.extend({ tool: z.literal('update_footer') })
|
|
337
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
338
|
+
updateNavigationBarInputSchema
|
|
339
|
+
.extend({ tool: z.literal('update_navigation_bar') })
|
|
340
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
341
|
+
updateSectionColumnBlockInputSchema
|
|
342
|
+
.extend({ tool: z.literal('update_section_column_block') })
|
|
343
|
+
.transform(({ tool, ...input }) => ({ input, tool })),
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
const commandStringCmsActionPlanActionSchema = z.string().transform((value, context) => {
|
|
347
|
+
const parsed = parseCmsActionPlanCommandString(value);
|
|
348
|
+
|
|
349
|
+
if (!parsed.success) {
|
|
350
|
+
context.addIssue({
|
|
351
|
+
code: 'custom',
|
|
352
|
+
message: parsed.message,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return z.NEVER;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return parsed.action;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const cmsActionPlanActionSchema = z.union([
|
|
362
|
+
wrappedCmsActionPlanActionSchema,
|
|
363
|
+
flatCmsActionPlanActionSchema,
|
|
364
|
+
commandStringCmsActionPlanActionSchema,
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
export const executeCmsActionPlanInputSchema = z.strictObject({
|
|
368
|
+
actions: z.array(cmsActionPlanActionSchema).min(1).max(8),
|
|
369
|
+
summary: z.string().trim().min(1).max(500).optional(),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
function splitTopLevelValues(value: string) {
|
|
373
|
+
const parts: string[] = [];
|
|
374
|
+
let current = '';
|
|
375
|
+
let depth = 0;
|
|
376
|
+
let quote: '"' | "'" | null = null;
|
|
377
|
+
let escaping = false;
|
|
378
|
+
|
|
379
|
+
for (const char of value) {
|
|
380
|
+
if (quote) {
|
|
381
|
+
current += char;
|
|
382
|
+
|
|
383
|
+
if (escaping) {
|
|
384
|
+
escaping = false;
|
|
385
|
+
} else if (char === '\\') {
|
|
386
|
+
escaping = true;
|
|
387
|
+
} else if (char === quote) {
|
|
388
|
+
quote = null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (char === '"' || char === "'") {
|
|
395
|
+
quote = char;
|
|
396
|
+
current += char;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (char === '[' || char === '{' || char === '(') {
|
|
401
|
+
depth++;
|
|
402
|
+
current += char;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (char === ']' || char === '}' || char === ')') {
|
|
407
|
+
depth = Math.max(0, depth - 1);
|
|
408
|
+
current += char;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (char === ',' && depth === 0) {
|
|
413
|
+
if (current.trim()) {
|
|
414
|
+
parts.push(current.trim());
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
current = '';
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
current += char;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (current.trim()) {
|
|
425
|
+
parts.push(current.trim());
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return parts;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function convertSingleQuotedJsonLikeToJson(value: string) {
|
|
432
|
+
let output = '';
|
|
433
|
+
|
|
434
|
+
for (let index = 0; index < value.length; index++) {
|
|
435
|
+
const char = value[index];
|
|
436
|
+
|
|
437
|
+
if (char !== "'") {
|
|
438
|
+
output += char;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let content = '';
|
|
443
|
+
let escaping = false;
|
|
444
|
+
index++;
|
|
445
|
+
|
|
446
|
+
for (; index < value.length; index++) {
|
|
447
|
+
const innerChar = value[index];
|
|
448
|
+
|
|
449
|
+
if (escaping) {
|
|
450
|
+
content += innerChar;
|
|
451
|
+
escaping = false;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (innerChar === '\\') {
|
|
456
|
+
escaping = true;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (innerChar === "'") {
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
content += innerChar;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
output += JSON.stringify(content);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return output
|
|
471
|
+
.replace(/\bTrue\b/g, 'true')
|
|
472
|
+
.replace(/\bFalse\b/g, 'false')
|
|
473
|
+
.replace(/\bNone\b/g, 'null');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function parseCmsActionPlanCommandValue(value: string) {
|
|
477
|
+
const trimmed = value.trim();
|
|
478
|
+
|
|
479
|
+
if (
|
|
480
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
481
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
482
|
+
) {
|
|
483
|
+
return JSON.parse(convertSingleQuotedJsonLikeToJson(trimmed));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
487
|
+
return JSON.parse(convertSingleQuotedJsonLikeToJson(trimmed));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
491
|
+
return Number(trimmed);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (trimmed === 'true' || trimmed === 'True') {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (trimmed === 'false' || trimmed === 'False') {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (trimmed === 'null' || trimmed === 'None') {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return trimmed;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function parseCmsActionPlanCommandArguments(value: string) {
|
|
510
|
+
const input: Record<string, unknown> = {};
|
|
511
|
+
|
|
512
|
+
for (const part of splitTopLevelValues(value)) {
|
|
513
|
+
const separatorIndex = part.indexOf('=');
|
|
514
|
+
|
|
515
|
+
if (separatorIndex <= 0) {
|
|
516
|
+
throw new Error(`Expected key=value argument, received "${part}".`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const key = part.slice(0, separatorIndex).trim();
|
|
520
|
+
|
|
521
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
522
|
+
throw new Error(`Invalid argument name "${key}".`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
input[key] = parseCmsActionPlanCommandValue(part.slice(separatorIndex + 1));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return input;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function parseCmsActionPlanCommandString(value: string):
|
|
532
|
+
| { action: z.infer<typeof wrappedCmsActionPlanActionSchema>; success: true }
|
|
533
|
+
| { message: string; success: false } {
|
|
534
|
+
const trimmed = value.trim();
|
|
535
|
+
const match = trimmed.match(/^([a-z_]+)\(([\s\S]*)\)$/);
|
|
536
|
+
|
|
537
|
+
if (!match) {
|
|
538
|
+
return {
|
|
539
|
+
message:
|
|
540
|
+
'Action plan actions must be JSON objects like { "tool": "create_cms_page", "input": { ... } }, not freeform text.',
|
|
541
|
+
success: false,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const toolName = match[1];
|
|
546
|
+
let input: Record<string, unknown>;
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
input = parseCmsActionPlanCommandArguments(match[2]);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return {
|
|
552
|
+
message: error instanceof Error ? error.message : 'Could not parse action-plan command arguments.',
|
|
553
|
+
success: false,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const action = { input, tool: toolName };
|
|
558
|
+
const parsedAction = wrappedCmsActionPlanActionSchema.safeParse(action);
|
|
559
|
+
|
|
560
|
+
if (!parsedAction.success) {
|
|
561
|
+
return {
|
|
562
|
+
message: `Invalid action-plan command "${toolName}": ${parsedAction.error.issues
|
|
563
|
+
.map((issue) => issue.message)
|
|
564
|
+
.join('; ')}`,
|
|
565
|
+
success: false,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
action: parsedAction.data,
|
|
571
|
+
success: true,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export type NavigationItemInput = z.infer<typeof navigationItemInputSchema>;
|
|
576
|
+
export type UpdateNavigationBarInput = z.infer<typeof updateNavigationBarInputSchema>;
|
|
577
|
+
export type UpdateFooterInput = z.infer<typeof updateFooterInputSchema>;
|
|
578
|
+
export type SearchDocumentationInput = z.infer<typeof searchDocumentationInputSchema>;
|
|
579
|
+
export type FetchEcommerceStatsInput = z.input<typeof fetchEcommerceStatsInputSchema>;
|
|
580
|
+
export type CortexAiPageContext = z.infer<typeof cortexAiPageContextSchema>;
|
|
581
|
+
export type ReadCurrentCmsItemInput = z.infer<typeof readCurrentCmsItemInputSchema>;
|
|
582
|
+
export type UpdateCurrentCmsFieldsInput = z.infer<typeof updateCurrentCmsFieldsInputSchema>;
|
|
583
|
+
export type UpdateContentBlockInput = z.infer<typeof updateContentBlockInputSchema>;
|
|
584
|
+
export type InsertContentBlockInput = z.infer<typeof insertContentBlockInputSchema>;
|
|
585
|
+
export type UpdateSectionColumnBlockInput = z.infer<typeof updateSectionColumnBlockInputSchema>;
|
|
586
|
+
export type CreateCmsPageInput = z.infer<typeof createCmsPageInputSchema>;
|
|
587
|
+
export type CreateCmsPostInput = z.infer<typeof createCmsPostInputSchema>;
|
|
588
|
+
export type CreateCmsProductInput = z.infer<typeof createCmsProductInputSchema>;
|
|
589
|
+
export type UpdateCmsItemFieldInput = z.infer<typeof updateCmsItemFieldInputSchema>;
|
|
590
|
+
export type PrepareDeleteCmsItemInput = z.infer<typeof prepareDeleteCmsItemInputSchema>;
|
|
591
|
+
export type DeleteCmsItemInput = z.infer<typeof deleteCmsItemInputSchema>;
|
|
592
|
+
export type ExecuteCmsActionPlanInput = z.infer<typeof executeCmsActionPlanInputSchema>;
|
|
593
|
+
|
|
594
|
+
function normalizePlannerText(value: string) {
|
|
595
|
+
return value
|
|
596
|
+
.normalize('NFD')
|
|
597
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
598
|
+
.toLowerCase()
|
|
599
|
+
.replace(/\s+/g, ' ')
|
|
600
|
+
.trim();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function mentionsAny(value: string, terms: string[]) {
|
|
604
|
+
return terms.some((term) => value.includes(term));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function buildVisibleContactIntroActionPlan(message: string): ExecuteCmsActionPlanInput | null {
|
|
608
|
+
const normalized = normalizePlannerText(message);
|
|
609
|
+
const asksToAdd = mentionsAny(normalized, ['add ', 'insert ', 'put ', 'create ']);
|
|
610
|
+
const asksVisibleCopy = mentionsAny(normalized, [
|
|
611
|
+
'title',
|
|
612
|
+
'heading',
|
|
613
|
+
'description',
|
|
614
|
+
'intro',
|
|
615
|
+
'copy',
|
|
616
|
+
'paragraph',
|
|
617
|
+
]);
|
|
618
|
+
const asksAboveForm =
|
|
619
|
+
normalized.includes('form') && mentionsAny(normalized, ['above', 'before']);
|
|
620
|
+
const asksContactPages =
|
|
621
|
+
normalized.includes('contact page') ||
|
|
622
|
+
normalized.includes('contact pages') ||
|
|
623
|
+
normalized.includes('contact us') ||
|
|
624
|
+
normalized.includes('contactez-nous');
|
|
625
|
+
const asksEnglishAndFrench =
|
|
626
|
+
normalized.includes('english') &&
|
|
627
|
+
mentionsAny(normalized, ['french', 'francais', 'francaise']);
|
|
628
|
+
|
|
629
|
+
if (!asksToAdd || !asksVisibleCopy || !asksAboveForm || !asksContactPages || !asksEnglishAndFrench) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
actions: [
|
|
635
|
+
{
|
|
636
|
+
input: {
|
|
637
|
+
anchorBlockType: 'form',
|
|
638
|
+
block: {
|
|
639
|
+
blockType: 'text',
|
|
640
|
+
content: {
|
|
641
|
+
html_content:
|
|
642
|
+
'<h2>Let us help you move faster</h2><p>Have a question, project idea, or need help choosing the right next step? Send us a message and the NextBlock team will get back to you soon.</p>',
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
contentType: 'page',
|
|
646
|
+
position: 'before',
|
|
647
|
+
slug: 'contact-us',
|
|
648
|
+
},
|
|
649
|
+
tool: 'insert_content_block',
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
input: {
|
|
653
|
+
anchorBlockType: 'form',
|
|
654
|
+
block: {
|
|
655
|
+
blockType: 'text',
|
|
656
|
+
content: {
|
|
657
|
+
html_content:
|
|
658
|
+
"<h2>Parlons de votre projet</h2><p>Vous avez une question, une idee de projet ou besoin d'aide pour avancer? Envoyez-nous un message et l'equipe NextBlock vous repondra rapidement.</p>",
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
contentType: 'page',
|
|
662
|
+
position: 'before',
|
|
663
|
+
slug: 'contactez-nous',
|
|
664
|
+
},
|
|
665
|
+
tool: 'insert_content_block',
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
summary:
|
|
669
|
+
'Add visible title and description copy above the forms on the English and French Contact pages.',
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
type DocumentationSnippet = {
|
|
674
|
+
excerpt: string;
|
|
675
|
+
source: 'page' | 'post';
|
|
676
|
+
title: string;
|
|
677
|
+
url: string;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
type BlockValidationResult = {
|
|
681
|
+
errors: string[];
|
|
682
|
+
isValid: boolean;
|
|
683
|
+
warnings: string[];
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const cortexAiBlockTypeSchema = z.enum(availableCortexAiBlockTypes);
|
|
687
|
+
const gradientSchema = z.object({
|
|
688
|
+
direction: z.string().optional(),
|
|
689
|
+
stops: z.array(z.object({ color: z.string(), position: z.number() })),
|
|
690
|
+
type: z.enum(['linear', 'radial']),
|
|
691
|
+
});
|
|
692
|
+
const backgroundSchema = z.object({
|
|
693
|
+
gradient: gradientSchema.optional(),
|
|
694
|
+
image: z
|
|
695
|
+
.object({
|
|
696
|
+
alt_text: z.string().optional(),
|
|
697
|
+
blur_data_url: z.string().optional(),
|
|
698
|
+
height: z.number().optional(),
|
|
699
|
+
media_id: z.string(),
|
|
700
|
+
object_key: z.string(),
|
|
701
|
+
overlay: z
|
|
702
|
+
.object({
|
|
703
|
+
gradient: gradientSchema,
|
|
704
|
+
type: z.literal('gradient'),
|
|
705
|
+
})
|
|
706
|
+
.optional(),
|
|
707
|
+
position: z.enum(['center', 'top', 'bottom', 'left', 'right']),
|
|
708
|
+
quality: z.number().nullable().optional(),
|
|
709
|
+
size: z.enum(['cover', 'contain']),
|
|
710
|
+
width: z.number().optional(),
|
|
711
|
+
})
|
|
712
|
+
.optional(),
|
|
713
|
+
min_height: z.string().optional(),
|
|
714
|
+
solid_color: z.string().optional(),
|
|
715
|
+
theme: z.enum(['primary', 'secondary', 'muted', 'accent', 'destructive']).optional(),
|
|
716
|
+
type: z.enum(['none', 'theme', 'solid', 'gradient', 'image']),
|
|
717
|
+
});
|
|
718
|
+
const blockInColumnSchema = z.object({
|
|
719
|
+
block_type: cortexAiBlockTypeSchema,
|
|
720
|
+
content: z.record(z.string(), z.any()),
|
|
721
|
+
temp_id: z.string().optional(),
|
|
722
|
+
});
|
|
723
|
+
const sectionBlockFallbackSchema = z.object({
|
|
724
|
+
background: backgroundSchema,
|
|
725
|
+
column_blocks: z.array(z.array(blockInColumnSchema)),
|
|
726
|
+
column_gap: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
|
|
727
|
+
container_type: z.enum(['full-width', 'container', 'container-sm', 'container-lg', 'container-xl']),
|
|
728
|
+
padding: z.object({
|
|
729
|
+
bottom: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
|
|
730
|
+
top: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
|
|
731
|
+
}),
|
|
732
|
+
responsive_columns: z.object({
|
|
733
|
+
desktop: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]),
|
|
734
|
+
mobile: z.union([z.literal(1), z.literal(2)]),
|
|
735
|
+
tablet: z.union([z.literal(1), z.literal(2), z.literal(3)]),
|
|
736
|
+
}),
|
|
737
|
+
vertical_alignment: z.enum(['start', 'center', 'end', 'stretch']).optional(),
|
|
738
|
+
});
|
|
739
|
+
const fallbackBlockSchemas: Record<BlockType, z.ZodTypeAny> = {
|
|
740
|
+
button: z.object({
|
|
741
|
+
position: z.enum(['left', 'center', 'right']).optional(),
|
|
742
|
+
size: z.enum(['default', 'sm', 'lg', 'full']).optional(),
|
|
743
|
+
text: z.string(),
|
|
744
|
+
url: z.string(),
|
|
745
|
+
variant: z.enum(['default', 'outline', 'secondary', 'ghost', 'link']).optional(),
|
|
746
|
+
}),
|
|
747
|
+
cart: z.object({}),
|
|
748
|
+
checkout: z.object({}),
|
|
749
|
+
featured_product: z.object({
|
|
750
|
+
imagePosition: z.enum(['left', 'right']).default('left'),
|
|
751
|
+
productId: z.string().min(1),
|
|
752
|
+
showBackground: z.boolean().default(false),
|
|
753
|
+
}),
|
|
754
|
+
form: z.object({
|
|
755
|
+
fields: z.array(
|
|
756
|
+
z.object({
|
|
757
|
+
field_type: z.enum(['text', 'email', 'textarea', 'select', 'radio', 'checkbox']),
|
|
758
|
+
is_required: z.boolean(),
|
|
759
|
+
label: z.string(),
|
|
760
|
+
options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
|
|
761
|
+
placeholder: z.string().optional(),
|
|
762
|
+
temp_id: z.string(),
|
|
763
|
+
})
|
|
764
|
+
),
|
|
765
|
+
recipient_email: z.string().email(),
|
|
766
|
+
submit_button_text: z.string(),
|
|
767
|
+
success_message: z.string(),
|
|
768
|
+
}),
|
|
769
|
+
heading: z.object({
|
|
770
|
+
level: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5), z.literal(6)]),
|
|
771
|
+
textAlign: z.enum(['left', 'center', 'right', 'justify']).optional(),
|
|
772
|
+
textColor: z.enum(['primary', 'secondary', 'accent', 'muted', 'destructive', 'background']).optional(),
|
|
773
|
+
text_content: z.string(),
|
|
774
|
+
}),
|
|
775
|
+
image: z.object({
|
|
776
|
+
alt_text: z.string().optional(),
|
|
777
|
+
caption: z.string().optional(),
|
|
778
|
+
height: z.number().nullable().optional(),
|
|
779
|
+
media_id: z.string().nullable(),
|
|
780
|
+
object_key: z.string().nullable().optional(),
|
|
781
|
+
width: z.number().nullable().optional(),
|
|
782
|
+
}),
|
|
783
|
+
posts_grid: z.object({
|
|
784
|
+
columns: z.number().min(1).max(6),
|
|
785
|
+
postsPerPage: z.number().min(1).max(50),
|
|
786
|
+
showPagination: z.boolean(),
|
|
787
|
+
title: z.string().optional(),
|
|
788
|
+
}),
|
|
789
|
+
product_details: z.object({}),
|
|
790
|
+
product_grid: z.object({
|
|
791
|
+
categoryId: z.string().optional(),
|
|
792
|
+
limit: z.number().min(1).max(20).default(6),
|
|
793
|
+
title: z.string().optional(),
|
|
794
|
+
type: z.enum(['latest', 'category']).default('latest'),
|
|
795
|
+
}),
|
|
796
|
+
section: sectionBlockFallbackSchema,
|
|
797
|
+
testimonial: z.object({
|
|
798
|
+
author_name: z.string().min(1),
|
|
799
|
+
author_title: z.string().optional(),
|
|
800
|
+
image_url: z.string().url().optional().or(z.literal('')),
|
|
801
|
+
quote: z.string().min(1),
|
|
802
|
+
}),
|
|
803
|
+
text: z.object({
|
|
804
|
+
html_content: z.string(),
|
|
805
|
+
}),
|
|
806
|
+
video_embed: z.object({
|
|
807
|
+
autoplay: z.boolean().optional(),
|
|
808
|
+
controls: z.boolean().optional(),
|
|
809
|
+
title: z.string().optional(),
|
|
810
|
+
url: z.string(),
|
|
811
|
+
}),
|
|
812
|
+
};
|
|
813
|
+
let runtimeBlockContentValidator:
|
|
814
|
+
| false
|
|
815
|
+
| ((blockType: BlockType, content: Record<string, any>) => BlockValidationResult)
|
|
816
|
+
| null = null;
|
|
817
|
+
|
|
818
|
+
function isValidBlockType(blockType: string): blockType is BlockType {
|
|
819
|
+
return (availableCortexAiBlockTypes as readonly string[]).includes(blockType);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function getRuntimeBlockContentValidator() {
|
|
823
|
+
if (runtimeBlockContentValidator !== null) {
|
|
824
|
+
return runtimeBlockContentValidator || null;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
const registry = require('./blocks/blockRegistry') as {
|
|
829
|
+
validateBlockContent?: (
|
|
830
|
+
blockType: BlockType,
|
|
831
|
+
content: Record<string, any>
|
|
832
|
+
) => BlockValidationResult;
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
runtimeBlockContentValidator =
|
|
836
|
+
typeof registry.validateBlockContent === 'function'
|
|
837
|
+
? registry.validateBlockContent
|
|
838
|
+
: false;
|
|
839
|
+
} catch {
|
|
840
|
+
runtimeBlockContentValidator = false;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return runtimeBlockContentValidator || null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function validateCortexBlockContent(blockType: BlockType, content: Record<string, unknown>) {
|
|
847
|
+
const runtimeValidator = getRuntimeBlockContentValidator();
|
|
848
|
+
|
|
849
|
+
if (runtimeValidator) {
|
|
850
|
+
return runtimeValidator(blockType, content);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const result = fallbackBlockSchemas[blockType].safeParse(content);
|
|
854
|
+
|
|
855
|
+
if (result.success) {
|
|
856
|
+
return { errors: [], isValid: true, warnings: [] };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
errors: result.error.issues.map((issue) => {
|
|
861
|
+
const path = issue.path.join('.');
|
|
862
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
863
|
+
}),
|
|
864
|
+
isValid: false,
|
|
865
|
+
warnings: [],
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function getEditorBlockDocumentSchema() {
|
|
870
|
+
return z.object({
|
|
871
|
+
content: z.array(z.any()).optional(),
|
|
872
|
+
type: z.literal('doc'),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function getDefaultRevalidatePath(): RevalidateFn | null {
|
|
877
|
+
try {
|
|
878
|
+
const { revalidatePath } = require('next/cache') as typeof import('next/cache');
|
|
879
|
+
return revalidatePath;
|
|
880
|
+
} catch {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function getSupabase(context?: ToolExecutionContext) {
|
|
886
|
+
if (!context?.supabase) {
|
|
887
|
+
throw new Error('A Supabase service client is required to execute Cortex AI global tools.');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return context.supabase;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function withTimeoutFallback<T>(
|
|
894
|
+
promise: Promise<T>,
|
|
895
|
+
timeoutMs: number,
|
|
896
|
+
createFallback: () => T
|
|
897
|
+
) {
|
|
898
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
899
|
+
const timeoutPromise = new Promise<T>((resolve) => {
|
|
900
|
+
timeoutId = setTimeout(() => resolve(createFallback()), timeoutMs);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
return Promise.race([
|
|
904
|
+
promise.finally(() => {
|
|
905
|
+
if (timeoutId) {
|
|
906
|
+
clearTimeout(timeoutId);
|
|
907
|
+
}
|
|
908
|
+
}),
|
|
909
|
+
timeoutPromise,
|
|
910
|
+
]);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function getCurrentCmsContext(context?: ToolExecutionContext) {
|
|
914
|
+
const parsed = cortexAiPageContextSchema.safeParse(context?.pageContext);
|
|
915
|
+
|
|
916
|
+
if (!parsed.success) {
|
|
917
|
+
throw new Error(
|
|
918
|
+
'No current CMS page context is available. Open a page, post, or product edit screen before using this editing tool.'
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return parsed.data;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function getNumericEntityId(pageContext: CortexAiPageContext) {
|
|
926
|
+
const id =
|
|
927
|
+
typeof pageContext.entityId === 'number'
|
|
928
|
+
? pageContext.entityId
|
|
929
|
+
: Number.parseInt(pageContext.entityId, 10);
|
|
930
|
+
|
|
931
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
932
|
+
throw new Error(`Current ${pageContext.contentType} id must be a positive integer.`);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return id;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function getStringEntityId(pageContext: CortexAiPageContext) {
|
|
939
|
+
const id = String(pageContext.entityId || '').trim();
|
|
940
|
+
|
|
941
|
+
if (!id) {
|
|
942
|
+
throw new Error(`Current ${pageContext.contentType} id is missing.`);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return id;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function getCmsEntityId(pageContext: CortexAiPageContext) {
|
|
949
|
+
return pageContext.contentType === 'product'
|
|
950
|
+
? getStringEntityId(pageContext)
|
|
951
|
+
: getNumericEntityId(pageContext);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function normalizePublicSlug(slug: unknown) {
|
|
955
|
+
return typeof slug === 'string' ? slug.trim().replace(/^\/+|\/+$/g, '') : '';
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function getPublicCmsPath(pageContext: CortexAiPageContext, slugOverride?: unknown) {
|
|
959
|
+
const slug = normalizePublicSlug(slugOverride ?? pageContext.slug);
|
|
960
|
+
|
|
961
|
+
if (!slug) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (pageContext.contentType === 'page') {
|
|
966
|
+
return slug === 'home' ? '/' : `/${slug}`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (pageContext.contentType === 'post') {
|
|
970
|
+
return `/article/${slug}`;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return `/product/${slug}`;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function getCmsEditPath(pageContext: CortexAiPageContext) {
|
|
977
|
+
const entityId = String(pageContext.entityId);
|
|
978
|
+
|
|
979
|
+
if (pageContext.contentType === 'page') {
|
|
980
|
+
return `/cms/pages/${entityId}/edit`;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (pageContext.contentType === 'post') {
|
|
984
|
+
return `/cms/posts/${entityId}/edit`;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return `/cms/products/${entityId}/edit`;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function revalidateCurrentCmsSurfaces(
|
|
991
|
+
context: ToolExecutionContext | undefined,
|
|
992
|
+
pageContext: CortexAiPageContext,
|
|
993
|
+
slugOverride?: unknown
|
|
994
|
+
) {
|
|
995
|
+
const revalidatePath = context?.revalidatePath ?? getDefaultRevalidatePath();
|
|
996
|
+
|
|
997
|
+
if (!revalidatePath) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
revalidatePath(getCmsEditPath(pageContext));
|
|
1002
|
+
|
|
1003
|
+
const publicPath = getPublicCmsPath(pageContext, slugOverride);
|
|
1004
|
+
|
|
1005
|
+
if (publicPath) {
|
|
1006
|
+
revalidatePath(publicPath);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (pageContext.contentType === 'product') {
|
|
1010
|
+
revalidatePath('/cms/products');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function revalidateGlobalCmsSurfaces(context?: ToolExecutionContext) {
|
|
1015
|
+
const revalidatePath = context?.revalidatePath ?? getDefaultRevalidatePath();
|
|
1016
|
+
|
|
1017
|
+
if (!revalidatePath) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
revalidatePath('/', 'layout');
|
|
1022
|
+
revalidatePath('/cms/navigation');
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function serializeError(error: unknown) {
|
|
1026
|
+
if (!error) {
|
|
1027
|
+
return 'Unknown database error.';
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (typeof error === 'object' && 'message' in error) {
|
|
1031
|
+
return String((error as { message?: unknown }).message || 'Unknown database error.');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return String(error);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function getEcommerceProductModule() {
|
|
1038
|
+
return import('./ai-global-agent-ecommerce');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function stableStringify(value: unknown): string {
|
|
1042
|
+
if (value === null || typeof value !== 'object') {
|
|
1043
|
+
return JSON.stringify(value);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (Array.isArray(value)) {
|
|
1047
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return `{${Object.keys(value as Record<string, unknown>)
|
|
1051
|
+
.filter((key) => key !== 'temp_id')
|
|
1052
|
+
.sort()
|
|
1053
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key])}`)
|
|
1054
|
+
.join(',')}}`;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function hashConfirmationPayload(value: unknown) {
|
|
1058
|
+
let hash = 0x811c9dc5;
|
|
1059
|
+
const serialized = stableStringify(value);
|
|
1060
|
+
|
|
1061
|
+
for (let index = 0; index < serialized.length; index++) {
|
|
1062
|
+
hash ^= serialized.charCodeAt(index);
|
|
1063
|
+
hash = Math.imul(hash, 0x01000193);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function normalizeConfirmationToken(value: string) {
|
|
1070
|
+
return value.replace(/\s+/g, ' ').trim().toUpperCase();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function buildConfirmationPhrase(action: string, subject: string, payload: unknown) {
|
|
1074
|
+
return `${normalizeConfirmationToken(`CONFIRM ${action} ${subject}`)} #${hashConfirmationPayload(payload)}`;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function buildConfirmationPreview(params: {
|
|
1078
|
+
action: string;
|
|
1079
|
+
payload: unknown;
|
|
1080
|
+
preview: Record<string, unknown>;
|
|
1081
|
+
subject: string;
|
|
1082
|
+
}) {
|
|
1083
|
+
const confirmationPhrase = buildConfirmationPhrase(
|
|
1084
|
+
params.action,
|
|
1085
|
+
params.subject,
|
|
1086
|
+
params.payload
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
confirmationPhrase,
|
|
1091
|
+
mutationExecuted: false,
|
|
1092
|
+
preview: params.preview,
|
|
1093
|
+
requiresConfirmation: true,
|
|
1094
|
+
success: true,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function readPreviewString(preview: Record<string, unknown>, key: string) {
|
|
1099
|
+
const value = preview[key];
|
|
1100
|
+
|
|
1101
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function readPreviewNumber(preview: Record<string, unknown>, key: string) {
|
|
1105
|
+
const value = Number(preview[key]);
|
|
1106
|
+
|
|
1107
|
+
return Number.isFinite(value) ? value : null;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function pluralize(count: number, singular: string, plural = `${singular}s`) {
|
|
1111
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function summarizeCmsMutationPreview(toolName: string, preview: Record<string, unknown>) {
|
|
1115
|
+
const explicitSummary = readPreviewString(preview, 'summary');
|
|
1116
|
+
|
|
1117
|
+
if (explicitSummary) {
|
|
1118
|
+
return explicitSummary;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const title = readPreviewString(preview, 'title');
|
|
1122
|
+
const slug = readPreviewString(preview, 'slug');
|
|
1123
|
+
const status = readPreviewString(preview, 'status');
|
|
1124
|
+
const contentType = readPreviewString(preview, 'contentType');
|
|
1125
|
+
const field = readPreviewString(preview, 'field');
|
|
1126
|
+
const mode = readPreviewString(preview, 'mode');
|
|
1127
|
+
const languageCode = readPreviewString(preview, 'languageCode');
|
|
1128
|
+
const blockCount = readPreviewNumber(preview, 'blockCount');
|
|
1129
|
+
const itemCount = readPreviewNumber(preview, 'itemCount');
|
|
1130
|
+
const affectedCount = readPreviewNumber(preview, 'affectedCount');
|
|
1131
|
+
|
|
1132
|
+
if (toolName === 'create_cms_page' || toolName === 'create_cms_post') {
|
|
1133
|
+
return `Create ${status || 'draft'} ${toolName === 'create_cms_page' ? 'page' : 'post'} "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}${blockCount !== null ? ` with ${pluralize(blockCount, 'content block')}` : ''}.`;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (toolName === 'create_cms_product') {
|
|
1137
|
+
return `Create ${status || 'draft'} product "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}.`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (toolName === 'update_cms_item_field') {
|
|
1141
|
+
return `Update ${field || 'one field'} on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (toolName === 'update_navigation_bar') {
|
|
1145
|
+
return `${mode === 'append' ? 'Add' : mode === 'update' ? 'Update' : 'Replace'} ${itemCount !== null ? pluralize(itemCount, 'navigation item') : 'navigation items'} in the ${languageCode || 'selected'} header navigation.`;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (toolName === 'update_footer') {
|
|
1149
|
+
const linkCount = readPreviewNumber(preview, 'linkCount');
|
|
1150
|
+
return `Update the ${languageCode || 'selected'} footer${linkCount !== null ? ` with ${pluralize(linkCount, 'link')}` : ''}.`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (toolName === 'update_content_block') {
|
|
1154
|
+
return `Update the selected ${readPreviewString(preview, 'blockType') || 'content'} block.`;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (toolName === 'insert_content_block') {
|
|
1158
|
+
return `Insert ${readPreviewString(preview, 'blockType') || 'content'} block on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (toolName === 'update_section_column_block') {
|
|
1162
|
+
return `Update the selected nested ${readPreviewString(preview, 'nestedBlockType') || 'section'} block.`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (toolName === 'delete_cms_item' || toolName === 'prepare_delete_cms_item') {
|
|
1166
|
+
return `Delete ${affectedCount !== null ? pluralize(affectedCount, contentType || 'CMS item') : `the selected ${contentType || 'CMS item'}`}${title || slug ? ` for "${title || slug}"` : ''}.`;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return 'Complete the requested CMS change.';
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function getConfirmationPreview(params: {
|
|
1173
|
+
action: string;
|
|
1174
|
+
context?: ToolExecutionContext;
|
|
1175
|
+
payload: unknown;
|
|
1176
|
+
preview: Record<string, unknown>;
|
|
1177
|
+
subject: string;
|
|
1178
|
+
}) {
|
|
1179
|
+
if (params.context?.skipConfirmation) {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const preview = buildConfirmationPreview(params);
|
|
1184
|
+
const latestUserMessage = normalizeConfirmationToken(params.context?.latestUserMessage || '');
|
|
1185
|
+
const expectedPhrase = normalizeConfirmationToken(preview.confirmationPhrase);
|
|
1186
|
+
|
|
1187
|
+
return latestUserMessage.includes(expectedPhrase) ? null : preview;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function getActorUserId(context?: ToolExecutionContext) {
|
|
1191
|
+
const actorUserId = context?.actorUserId;
|
|
1192
|
+
|
|
1193
|
+
if (!actorUserId) {
|
|
1194
|
+
throw new Error('A confirmed CMS mutation requires an authenticated admin actor.');
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return actorUserId;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function createId() {
|
|
1201
|
+
return globalThis.crypto?.randomUUID?.() || `id-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function slugify(value: string) {
|
|
1205
|
+
return value
|
|
1206
|
+
.toLowerCase()
|
|
1207
|
+
.trim()
|
|
1208
|
+
.replace(/\s+/g, '-')
|
|
1209
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
1210
|
+
.replace(/-+/g, '-')
|
|
1211
|
+
.replace(/^-|-$/g, '')
|
|
1212
|
+
.slice(0, 300);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function normalizeCurrencyCode(value: string | undefined) {
|
|
1216
|
+
return (value || 'USD').trim().toUpperCase();
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function minorUnitAmountToMajor(value: number, currencyCode: string) {
|
|
1220
|
+
const zeroDecimalCurrencies = new Set(['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF']);
|
|
1221
|
+
const precision = zeroDecimalCurrencies.has(normalizeCurrencyCode(currencyCode)) ? 0 : 2;
|
|
1222
|
+
|
|
1223
|
+
return value / 10 ** precision;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function maybeCentsToMajor(value: unknown, currencyCode: string) {
|
|
1227
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
1228
|
+
? minorUnitAmountToMajor(value, currencyCode)
|
|
1229
|
+
: 0;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function mapMinorPriceMapToMajor(value: unknown, fallbackCurrencyCode: string) {
|
|
1233
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1234
|
+
return {};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return Object.entries(value as Record<string, unknown>).reduce<Record<string, number>>(
|
|
1238
|
+
(prices, [currencyCode, amount]) => {
|
|
1239
|
+
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
|
1240
|
+
prices[normalizeCurrencyCode(currencyCode || fallbackCurrencyCode)] = minorUnitAmountToMajor(
|
|
1241
|
+
amount,
|
|
1242
|
+
currencyCode || fallbackCurrencyCode
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return prices;
|
|
1247
|
+
},
|
|
1248
|
+
{}
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function cloneJsonRecord(value: unknown, label: string) {
|
|
1253
|
+
if (!isPlainJsonRecord(value)) {
|
|
1254
|
+
throw new Error(`${label} content must be a JSON object.`);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return JSON.parse(JSON.stringify(value)) as Record<string, any>;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function cloneJsonValue<T>(value: T): T {
|
|
1261
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function isPlainJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
1265
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function mergeJsonRecords(
|
|
1269
|
+
base: Record<string, unknown>,
|
|
1270
|
+
patch: Record<string, unknown>
|
|
1271
|
+
): Record<string, unknown> {
|
|
1272
|
+
const merged = cloneJsonValue(base);
|
|
1273
|
+
|
|
1274
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1275
|
+
if (value === undefined) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (isPlainJsonRecord(value) && isPlainJsonRecord(merged[key])) {
|
|
1280
|
+
merged[key] = mergeJsonRecords(merged[key], value);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
merged[key] = cloneJsonValue(value);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
return merged;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function assertBlockBelongsToCurrentContext(block: any, pageContext: CortexAiPageContext) {
|
|
1291
|
+
if (pageContext.contentType === 'product') {
|
|
1292
|
+
throw new Error('Products do not have page/post content blocks in this editor context.');
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const parentId = getNumericEntityId(pageContext);
|
|
1296
|
+
const actualParentId =
|
|
1297
|
+
pageContext.contentType === 'page' ? Number(block.page_id) : Number(block.post_id);
|
|
1298
|
+
|
|
1299
|
+
if (actualParentId !== parentId) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`Block ${block.id} does not belong to the current ${pageContext.contentType} being edited.`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function resolveExistingBlockType(blockType: unknown, label: string): BlockType {
|
|
1307
|
+
const normalizedBlockType = typeof blockType === 'string' ? blockType : '';
|
|
1308
|
+
|
|
1309
|
+
if (!isValidBlockType(normalizedBlockType)) {
|
|
1310
|
+
throw new Error(`${label} has unsupported block type "${normalizedBlockType || 'unknown'}".`);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return normalizedBlockType;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function assertRequestedBlockTypeMatches(
|
|
1317
|
+
requestedBlockType: BlockType | undefined,
|
|
1318
|
+
existingBlockType: BlockType,
|
|
1319
|
+
label: string
|
|
1320
|
+
) {
|
|
1321
|
+
if (requestedBlockType && requestedBlockType !== existingBlockType) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`${label} is a "${existingBlockType}" block. Refusing to update it as "${requestedBlockType}".`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function assertValidBlockContent(blockType: BlockType, content: Record<string, unknown>, label: string) {
|
|
1329
|
+
const validation = validateCortexBlockContent(blockType, content);
|
|
1330
|
+
|
|
1331
|
+
if (!validation.isValid) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`${label} content is invalid for block type "${blockType}": ${validation.errors.join('; ')}`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function isSectionLikeBlock(blockType: BlockType) {
|
|
1339
|
+
return blockType === 'section';
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function inferNestedBlockTypeFromContent(content: Record<string, unknown>): BlockType | null {
|
|
1343
|
+
if (typeof content.html_content === 'string') {
|
|
1344
|
+
return 'text';
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (typeof content.text === 'string' && typeof content.url === 'string') {
|
|
1348
|
+
return 'button';
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (typeof content.text_content === 'string') {
|
|
1352
|
+
return 'heading';
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if ('media_id' in content || 'object_key' in content) {
|
|
1356
|
+
return 'image';
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (typeof content.quote === 'string' && typeof content.author_name === 'string') {
|
|
1360
|
+
return 'testimonial';
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
if (typeof content.url === 'string' && ('controls' in content || 'autoplay' in content || 'title' in content)) {
|
|
1364
|
+
return 'video_embed';
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (Array.isArray(content.fields) || typeof content.recipient_email === 'string') {
|
|
1368
|
+
return 'form';
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if ('postsPerPage' in content || 'showPagination' in content) {
|
|
1372
|
+
return 'posts_grid';
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (typeof content.productId === 'string') {
|
|
1376
|
+
return 'featured_product';
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if ('limit' in content && 'type' in content) {
|
|
1380
|
+
return 'product_grid';
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function createNestedTempId(blockType: BlockType) {
|
|
1387
|
+
return `ai-${blockType}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function normalizeNestedColumnBlock(value: unknown, label: string): ColumnBlock {
|
|
1391
|
+
if (!isPlainJsonRecord(value)) {
|
|
1392
|
+
throw new Error(`${label} must be a JSON object.`);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const rawBlockType = value.block_type ?? value.blockType;
|
|
1396
|
+
const blockType = resolveExistingBlockType(rawBlockType, label);
|
|
1397
|
+
|
|
1398
|
+
if (isSectionLikeBlock(blockType)) {
|
|
1399
|
+
throw new Error(`${label} cannot be a nested ${blockType} block.`);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const content = normalizeBlockContentForType(blockType, cloneJsonRecord(value.content, label), label);
|
|
1403
|
+
|
|
1404
|
+
const rawTempId = value.temp_id ?? value.tempId;
|
|
1405
|
+
const tempId = typeof rawTempId === 'string' && rawTempId.trim() ? rawTempId : createNestedTempId(blockType);
|
|
1406
|
+
|
|
1407
|
+
return {
|
|
1408
|
+
block_type: blockType,
|
|
1409
|
+
content,
|
|
1410
|
+
temp_id: tempId,
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function normalizeNestedBlocksToAppend(contentPatch: Record<string, unknown>): ColumnBlock[] {
|
|
1415
|
+
const blocks: ColumnBlock[] = [];
|
|
1416
|
+
|
|
1417
|
+
if ('append_block' in contentPatch) {
|
|
1418
|
+
blocks.push(normalizeNestedColumnBlock(contentPatch.append_block, 'Nested block to append'));
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if ('append_blocks' in contentPatch) {
|
|
1422
|
+
const appendBlocks = contentPatch.append_blocks;
|
|
1423
|
+
|
|
1424
|
+
if (!Array.isArray(appendBlocks)) {
|
|
1425
|
+
throw new Error('append_blocks must be an array of nested block objects.');
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
appendBlocks.forEach((block, index) => {
|
|
1429
|
+
blocks.push(normalizeNestedColumnBlock(block, `Nested block to append ${index}`));
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
return blocks;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function maybeInferSingleNestedBlockToAppend(contentPatch: Record<string, unknown>): ColumnBlock | null {
|
|
1437
|
+
if (
|
|
1438
|
+
'append_block' in contentPatch ||
|
|
1439
|
+
'append_blocks' in contentPatch ||
|
|
1440
|
+
'background' in contentPatch ||
|
|
1441
|
+
'column_blocks' in contentPatch ||
|
|
1442
|
+
'column_gap' in contentPatch ||
|
|
1443
|
+
'container_type' in contentPatch ||
|
|
1444
|
+
'padding' in contentPatch ||
|
|
1445
|
+
'responsive_columns' in contentPatch ||
|
|
1446
|
+
'vertical_alignment' in contentPatch
|
|
1447
|
+
) {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const blockType = inferNestedBlockTypeFromContent(contentPatch);
|
|
1452
|
+
|
|
1453
|
+
if (!blockType) {
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const content = cloneJsonRecord(contentPatch, `Nested ${blockType} block`);
|
|
1458
|
+
assertValidBlockContent(blockType, content, `Nested ${blockType} block`);
|
|
1459
|
+
|
|
1460
|
+
return {
|
|
1461
|
+
block_type: blockType,
|
|
1462
|
+
content,
|
|
1463
|
+
temp_id: createNestedTempId(blockType),
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function getAppendColumnIndex(contentPatch: Record<string, unknown>, existingColumnCount: number) {
|
|
1468
|
+
const rawColumnIndex = contentPatch.append_column_index ?? contentPatch.column_index;
|
|
1469
|
+
|
|
1470
|
+
if (rawColumnIndex === undefined) {
|
|
1471
|
+
return 0;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (typeof rawColumnIndex !== 'number' || !Number.isInteger(rawColumnIndex) || rawColumnIndex < 0) {
|
|
1475
|
+
throw new Error('append_column_index must be a non-negative integer.');
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (existingColumnCount > 0 && rawColumnIndex >= existingColumnCount) {
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
`append_column_index ${rawColumnIndex} is outside the existing ${existingColumnCount} column(s).`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return rawColumnIndex;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function buildNextTopLevelBlockContent(
|
|
1488
|
+
blockType: BlockType,
|
|
1489
|
+
existingContent: Record<string, unknown>,
|
|
1490
|
+
contentPatch: Record<string, unknown>
|
|
1491
|
+
) {
|
|
1492
|
+
if (!isSectionLikeBlock(blockType)) {
|
|
1493
|
+
return mergeJsonRecords(existingContent, contentPatch);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const nextContentPatch = { ...contentPatch };
|
|
1497
|
+
const blocksToAppend = normalizeNestedBlocksToAppend(nextContentPatch);
|
|
1498
|
+
const inferredBlock = maybeInferSingleNestedBlockToAppend(nextContentPatch);
|
|
1499
|
+
|
|
1500
|
+
if (inferredBlock) {
|
|
1501
|
+
blocksToAppend.push(inferredBlock);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
delete nextContentPatch.append_block;
|
|
1505
|
+
delete nextContentPatch.append_blocks;
|
|
1506
|
+
delete nextContentPatch.append_column_index;
|
|
1507
|
+
delete nextContentPatch.column_index;
|
|
1508
|
+
|
|
1509
|
+
const nextContent = mergeJsonRecords(existingContent, nextContentPatch) as SectionBlockContent;
|
|
1510
|
+
|
|
1511
|
+
if (blocksToAppend.length > 0) {
|
|
1512
|
+
const existingColumns = Array.isArray(existingContent.column_blocks)
|
|
1513
|
+
? cloneJsonValue(existingContent.column_blocks)
|
|
1514
|
+
: [];
|
|
1515
|
+
const targetColumnIndex = getAppendColumnIndex(contentPatch, existingColumns.length);
|
|
1516
|
+
const nextColumnBlocks = existingColumns.length > 0 ? existingColumns : [[]];
|
|
1517
|
+
|
|
1518
|
+
while (nextColumnBlocks.length <= targetColumnIndex) {
|
|
1519
|
+
nextColumnBlocks.push([]);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
nextColumnBlocks[targetColumnIndex] = [
|
|
1523
|
+
...(nextColumnBlocks[targetColumnIndex] || []),
|
|
1524
|
+
...blocksToAppend,
|
|
1525
|
+
];
|
|
1526
|
+
nextContent.column_blocks = nextColumnBlocks;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return nextContent;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function summarizeBlockRow(block: any, includeContent: boolean) {
|
|
1533
|
+
return {
|
|
1534
|
+
blockType: block.block_type,
|
|
1535
|
+
content: includeContent ? block.content : undefined,
|
|
1536
|
+
id: block.id,
|
|
1537
|
+
languageId: block.language_id,
|
|
1538
|
+
order: block.order,
|
|
1539
|
+
pageId: block.page_id,
|
|
1540
|
+
postId: block.post_id,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function normalizeNavigationUrl(value: unknown) {
|
|
1545
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function normalizeNavigationLabel(value: unknown) {
|
|
1549
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function countNavigationInputItems(items: NavigationItemInput[]) {
|
|
1553
|
+
return items.reduce((count, item) => count + 1 + (item.children?.length || 0), 0);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function normalizeLanguageLookup(value: unknown) {
|
|
1557
|
+
return typeof value === 'string'
|
|
1558
|
+
? value
|
|
1559
|
+
.trim()
|
|
1560
|
+
.toLowerCase()
|
|
1561
|
+
.normalize('NFD')
|
|
1562
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1563
|
+
: '';
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
async function getLanguageRecord(supabase: SupabaseLike, languageCode: string) {
|
|
1567
|
+
const requestedLanguage = languageCode.trim();
|
|
1568
|
+
const normalizedRequestedLanguage = normalizeLanguageLookup(requestedLanguage);
|
|
1569
|
+
const aliasCode = LANGUAGE_NAME_ALIASES[normalizedRequestedLanguage];
|
|
1570
|
+
const { data, error } = await supabase
|
|
1571
|
+
.from('languages')
|
|
1572
|
+
.select('id, code, name, is_active');
|
|
1573
|
+
|
|
1574
|
+
if (error) {
|
|
1575
|
+
throw new Error(`Failed to load language "${languageCode}": ${serializeError(error)}`);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
const languages = Array.isArray(data) ? data : [];
|
|
1579
|
+
const activeLanguages = languages.filter((language: any) => language.is_active !== false);
|
|
1580
|
+
const matchedLanguage = activeLanguages.find((language: any) => {
|
|
1581
|
+
const normalizedCode = normalizeLanguageLookup(language.code);
|
|
1582
|
+
const normalizedName = normalizeLanguageLookup(language.name);
|
|
1583
|
+
|
|
1584
|
+
return (
|
|
1585
|
+
normalizedCode === normalizedRequestedLanguage ||
|
|
1586
|
+
normalizedCode === aliasCode ||
|
|
1587
|
+
normalizedName === normalizedRequestedLanguage
|
|
1588
|
+
);
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
if (!matchedLanguage?.id || !matchedLanguage?.code) {
|
|
1592
|
+
const availableLanguages = activeLanguages
|
|
1593
|
+
.map((language: any) => language.code)
|
|
1594
|
+
.filter(Boolean)
|
|
1595
|
+
.join(', ');
|
|
1596
|
+
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
`Language "${languageCode}" was not found.${availableLanguages ? ` Available languages: ${availableLanguages}.` : ''}`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
return {
|
|
1603
|
+
code: String(matchedLanguage.code),
|
|
1604
|
+
id: Number(matchedLanguage.id),
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
async function getDefaultLanguageRecord(supabase: SupabaseLike, languageCode?: string) {
|
|
1609
|
+
if (languageCode) {
|
|
1610
|
+
return getLanguageRecord(supabase, languageCode);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const { data, error } = await supabase
|
|
1614
|
+
.from('languages')
|
|
1615
|
+
.select('id, code, name, is_active, is_default');
|
|
1616
|
+
|
|
1617
|
+
if (error) {
|
|
1618
|
+
throw new Error(`Failed to load active languages: ${serializeError(error)}`);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const activeLanguages = (Array.isArray(data) ? data : []).filter(
|
|
1622
|
+
(language: any) => language.is_active !== false
|
|
1623
|
+
);
|
|
1624
|
+
const language =
|
|
1625
|
+
activeLanguages.find((item: any) => item.is_default) ||
|
|
1626
|
+
activeLanguages.find((item: any) => normalizeLanguageLookup(item.code) === 'en') ||
|
|
1627
|
+
activeLanguages[0];
|
|
1628
|
+
|
|
1629
|
+
if (!language?.id || !language?.code) {
|
|
1630
|
+
throw new Error('No active CMS language is available for Cortex AI content creation.');
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return {
|
|
1634
|
+
code: String(language.code),
|
|
1635
|
+
id: Number(language.id),
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
async function getDefaultCurrencyCode(supabase: SupabaseLike) {
|
|
1640
|
+
try {
|
|
1641
|
+
const { data, error } = await supabase
|
|
1642
|
+
.from('currencies')
|
|
1643
|
+
.select('code, is_default, is_active')
|
|
1644
|
+
.eq('is_active', true);
|
|
1645
|
+
|
|
1646
|
+
if (error) {
|
|
1647
|
+
return 'USD';
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const currencies = Array.isArray(data) ? data : [];
|
|
1651
|
+
const currency = currencies.find((item: any) => item.is_default) || currencies[0];
|
|
1652
|
+
|
|
1653
|
+
return normalizeCurrencyCode(currency?.code || 'USD');
|
|
1654
|
+
} catch {
|
|
1655
|
+
return 'USD';
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
async function findSingleCmsItem(params: {
|
|
1660
|
+
contentType: CmsContentType;
|
|
1661
|
+
entityId?: string | number;
|
|
1662
|
+
slug?: string;
|
|
1663
|
+
supabase: SupabaseLike;
|
|
1664
|
+
title?: string;
|
|
1665
|
+
}) {
|
|
1666
|
+
const table =
|
|
1667
|
+
params.contentType === 'page'
|
|
1668
|
+
? 'pages'
|
|
1669
|
+
: params.contentType === 'post'
|
|
1670
|
+
? 'posts'
|
|
1671
|
+
: 'products';
|
|
1672
|
+
let column = 'id';
|
|
1673
|
+
let value: unknown = params.entityId;
|
|
1674
|
+
|
|
1675
|
+
if (value === undefined && params.slug) {
|
|
1676
|
+
column = 'slug';
|
|
1677
|
+
value = params.slug;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (value === undefined && params.title) {
|
|
1681
|
+
column = 'title';
|
|
1682
|
+
value = params.title;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (value === undefined) {
|
|
1686
|
+
throw new Error(`A ${params.contentType} target requires an id, slug, title, or current edit context.`);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const { data, error } = await params.supabase.from(table).select('*').eq(column, value);
|
|
1690
|
+
|
|
1691
|
+
if (error) {
|
|
1692
|
+
throw new Error(`Failed to resolve ${params.contentType}: ${serializeError(error)}`);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const rows = Array.isArray(data) ? data : data ? [data] : [];
|
|
1696
|
+
|
|
1697
|
+
if (rows.length !== 1) {
|
|
1698
|
+
throw new Error(
|
|
1699
|
+
rows.length === 0
|
|
1700
|
+
? `No ${params.contentType} matched ${column} "${String(value)}".`
|
|
1701
|
+
: `Multiple ${params.contentType}s matched ${column} "${String(value)}"; use an exact id.`
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
return rows[0];
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
async function resolveCmsTarget(
|
|
1709
|
+
input: z.infer<typeof cmsTargetInputSchema>,
|
|
1710
|
+
context?: ToolExecutionContext
|
|
1711
|
+
) {
|
|
1712
|
+
const pageContext = cortexAiPageContextSchema.safeParse(context?.pageContext).success
|
|
1713
|
+
? (context?.pageContext as CortexAiPageContext)
|
|
1714
|
+
: null;
|
|
1715
|
+
const contentType = input.contentType || pageContext?.contentType;
|
|
1716
|
+
|
|
1717
|
+
if (!contentType) {
|
|
1718
|
+
throw new Error('Target contentType is required when no current CMS edit context exists.');
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const hasExplicitTarget =
|
|
1722
|
+
input.entityId !== undefined || Boolean(input.slug) || Boolean(input.title);
|
|
1723
|
+
const entityId = input.entityId ?? (hasExplicitTarget ? undefined : pageContext?.entityId);
|
|
1724
|
+
const slug = input.slug ?? (hasExplicitTarget ? undefined : pageContext?.slug ?? undefined);
|
|
1725
|
+
const title = input.title ?? (hasExplicitTarget ? undefined : pageContext?.title ?? undefined);
|
|
1726
|
+
const item = await findSingleCmsItem({
|
|
1727
|
+
contentType,
|
|
1728
|
+
entityId,
|
|
1729
|
+
slug: entityId === undefined ? slug || undefined : undefined,
|
|
1730
|
+
supabase: getSupabase(context),
|
|
1731
|
+
title: entityId === undefined && !slug ? title || undefined : undefined,
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
return {
|
|
1735
|
+
contentType,
|
|
1736
|
+
item,
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
async function insertNavigationItem(params: {
|
|
1741
|
+
item: NavigationItemInput;
|
|
1742
|
+
languageId: number;
|
|
1743
|
+
menuKey: MenuKey;
|
|
1744
|
+
order: number;
|
|
1745
|
+
parentId?: number | null;
|
|
1746
|
+
supabase: SupabaseLike;
|
|
1747
|
+
}) {
|
|
1748
|
+
const linkedPage = await resolveLinkedPageForNavigationItem({
|
|
1749
|
+
item: params.item,
|
|
1750
|
+
languageId: params.languageId,
|
|
1751
|
+
menuKey: params.menuKey,
|
|
1752
|
+
supabase: params.supabase,
|
|
1753
|
+
});
|
|
1754
|
+
const insertPayload = {
|
|
1755
|
+
label: params.item.label,
|
|
1756
|
+
language_id: params.languageId,
|
|
1757
|
+
menu_key: params.menuKey,
|
|
1758
|
+
order: params.order,
|
|
1759
|
+
page_id: linkedPage?.pageId ?? null,
|
|
1760
|
+
parent_id: params.parentId ?? null,
|
|
1761
|
+
...(linkedPage?.navigationTranslationGroupId
|
|
1762
|
+
? { translation_group_id: linkedPage.navigationTranslationGroupId }
|
|
1763
|
+
: {}),
|
|
1764
|
+
url: params.item.url,
|
|
1765
|
+
};
|
|
1766
|
+
const { data, error } = await params.supabase
|
|
1767
|
+
.from('navigation_items')
|
|
1768
|
+
.insert(insertPayload)
|
|
1769
|
+
.select('id')
|
|
1770
|
+
.single();
|
|
1771
|
+
|
|
1772
|
+
if (error) {
|
|
1773
|
+
throw new Error(`Failed to insert ${params.menuKey} navigation item: ${serializeError(error)}`);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
return Number(data.id);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function getNavigationPageSlug(url: string) {
|
|
1780
|
+
const trimmedUrl = url.trim();
|
|
1781
|
+
|
|
1782
|
+
if (!trimmedUrl.startsWith('/') || trimmedUrl.startsWith('//')) {
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const path = trimmedUrl.split('?')[0]?.split('#')[0] || '';
|
|
1787
|
+
const slug = path === '/' ? 'home' : path.replace(/^\/+|\/+$/g, '');
|
|
1788
|
+
|
|
1789
|
+
return slug && !slug.includes('/') ? slug : null;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
async function resolveLinkedPageForNavigationItem(params: {
|
|
1793
|
+
item: NavigationItemInput;
|
|
1794
|
+
languageId: number;
|
|
1795
|
+
menuKey: MenuKey;
|
|
1796
|
+
supabase: SupabaseLike;
|
|
1797
|
+
}) {
|
|
1798
|
+
const slug = getNavigationPageSlug(params.item.url);
|
|
1799
|
+
|
|
1800
|
+
if (!slug) {
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
const { data: pageData, error: pageError } = await params.supabase
|
|
1805
|
+
.from('pages')
|
|
1806
|
+
.select('id, slug, translation_group_id, language_id')
|
|
1807
|
+
.eq('slug', slug)
|
|
1808
|
+
.eq('language_id', params.languageId);
|
|
1809
|
+
|
|
1810
|
+
if (pageError) {
|
|
1811
|
+
throw new Error(`Failed to resolve linked page for navigation item: ${serializeError(pageError)}`);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const page = Array.isArray(pageData) ? pageData[0] : pageData;
|
|
1815
|
+
|
|
1816
|
+
if (!page?.id) {
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
let navigationTranslationGroupId = createId();
|
|
1821
|
+
|
|
1822
|
+
if (page.translation_group_id) {
|
|
1823
|
+
const { data: relatedPages, error: relatedPagesError } = await params.supabase
|
|
1824
|
+
.from('pages')
|
|
1825
|
+
.select('id')
|
|
1826
|
+
.eq('translation_group_id', page.translation_group_id);
|
|
1827
|
+
|
|
1828
|
+
if (relatedPagesError) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
`Failed to inspect linked page translations for navigation item: ${serializeError(relatedPagesError)}`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
const relatedPageIds = new Set(
|
|
1835
|
+
(Array.isArray(relatedPages) ? relatedPages : [])
|
|
1836
|
+
.map((relatedPage: any) => String(relatedPage.id))
|
|
1837
|
+
.filter(Boolean)
|
|
1838
|
+
);
|
|
1839
|
+
|
|
1840
|
+
const { data: relatedNavigationItems, error: relatedNavigationItemsError } = await params.supabase
|
|
1841
|
+
.from('navigation_items')
|
|
1842
|
+
.select('id, page_id, translation_group_id, menu_key')
|
|
1843
|
+
.eq('menu_key', params.menuKey);
|
|
1844
|
+
|
|
1845
|
+
if (relatedNavigationItemsError) {
|
|
1846
|
+
throw new Error(
|
|
1847
|
+
`Failed to inspect related navigation translations: ${serializeError(relatedNavigationItemsError)}`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const relatedNavigationItem = (Array.isArray(relatedNavigationItems)
|
|
1852
|
+
? relatedNavigationItems
|
|
1853
|
+
: []
|
|
1854
|
+
).find((item: any) => item.translation_group_id && relatedPageIds.has(String(item.page_id)));
|
|
1855
|
+
|
|
1856
|
+
if (relatedNavigationItem?.translation_group_id) {
|
|
1857
|
+
navigationTranslationGroupId = relatedNavigationItem.translation_group_id;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return {
|
|
1862
|
+
navigationTranslationGroupId,
|
|
1863
|
+
pageId: Number(page.id),
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
async function replaceNavigationMenu<TMenuKey extends MenuKey>(params: {
|
|
1868
|
+
items: NavigationItemInput[];
|
|
1869
|
+
languageCode: string;
|
|
1870
|
+
menuKey: TMenuKey;
|
|
1871
|
+
supabase: SupabaseLike;
|
|
1872
|
+
}) {
|
|
1873
|
+
const language = await getLanguageRecord(params.supabase, params.languageCode);
|
|
1874
|
+
const { data: existingItems, error: existingItemsError } = await params.supabase
|
|
1875
|
+
.from('navigation_items')
|
|
1876
|
+
.select('id, parent_id')
|
|
1877
|
+
.eq('menu_key', params.menuKey)
|
|
1878
|
+
.eq('language_id', language.id);
|
|
1879
|
+
|
|
1880
|
+
if (existingItemsError) {
|
|
1881
|
+
throw new Error(
|
|
1882
|
+
`Failed to inspect existing ${params.menuKey} navigation items: ${serializeError(existingItemsError)}`
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const existingRows = Array.isArray(existingItems) ? existingItems : [];
|
|
1887
|
+
const existingTopLevelCount = existingRows.filter((item: any) => item.parent_id == null).length;
|
|
1888
|
+
const replacementItemCount = countNavigationInputItems(params.items);
|
|
1889
|
+
|
|
1890
|
+
assertNavigationReplacementIsSafe({
|
|
1891
|
+
existingItemCount: existingRows.length,
|
|
1892
|
+
existingTopLevelCount,
|
|
1893
|
+
languageCode: language.code,
|
|
1894
|
+
menuKey: params.menuKey,
|
|
1895
|
+
replacementItemCount,
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
const { error: deleteError } = await params.supabase
|
|
1899
|
+
.from('navigation_items')
|
|
1900
|
+
.delete()
|
|
1901
|
+
.eq('menu_key', params.menuKey)
|
|
1902
|
+
.eq('language_id', language.id);
|
|
1903
|
+
|
|
1904
|
+
if (deleteError) {
|
|
1905
|
+
throw new Error(`Failed to clear ${params.menuKey} navigation items: ${serializeError(deleteError)}`);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
let insertedCount = 0;
|
|
1909
|
+
|
|
1910
|
+
for (const [index, item] of params.items.entries()) {
|
|
1911
|
+
const parentId = await insertNavigationItem({
|
|
1912
|
+
item,
|
|
1913
|
+
languageId: language.id,
|
|
1914
|
+
menuKey: params.menuKey,
|
|
1915
|
+
order: index,
|
|
1916
|
+
supabase: params.supabase,
|
|
1917
|
+
});
|
|
1918
|
+
insertedCount++;
|
|
1919
|
+
|
|
1920
|
+
for (const [childIndex, child] of (item.children ?? []).entries()) {
|
|
1921
|
+
await insertNavigationItem({
|
|
1922
|
+
item: child,
|
|
1923
|
+
languageId: language.id,
|
|
1924
|
+
menuKey: params.menuKey,
|
|
1925
|
+
order: childIndex,
|
|
1926
|
+
parentId,
|
|
1927
|
+
supabase: params.supabase,
|
|
1928
|
+
});
|
|
1929
|
+
insertedCount++;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
return {
|
|
1934
|
+
insertedCount,
|
|
1935
|
+
languageCode: language.code,
|
|
1936
|
+
menuKey: params.menuKey,
|
|
1937
|
+
skippedCount: 0,
|
|
1938
|
+
updatedCount: 0,
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function assertNavigationReplacementIsSafe(params: {
|
|
1943
|
+
existingItemCount: number;
|
|
1944
|
+
existingTopLevelCount: number;
|
|
1945
|
+
languageCode: string;
|
|
1946
|
+
menuKey: MenuKey;
|
|
1947
|
+
replacementItemCount: number;
|
|
1948
|
+
}) {
|
|
1949
|
+
if (params.existingItemCount === 0 || params.replacementItemCount >= params.existingItemCount) {
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
throw new Error(
|
|
1954
|
+
`Refusing destructive ${params.menuKey} navigation replacement for ${params.languageCode}: existing menu has ${params.existingItemCount} items (${params.existingTopLevelCount} top-level), but the replacement only contains ${params.replacementItemCount}. Use mode "update" for renaming or changing a single link, or provide the full menu.`
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
async function assertNavigationReplacementInputIsSafe(params: {
|
|
1959
|
+
items: NavigationItemInput[];
|
|
1960
|
+
languageCode: string;
|
|
1961
|
+
menuKey: MenuKey;
|
|
1962
|
+
supabase: SupabaseLike;
|
|
1963
|
+
}) {
|
|
1964
|
+
const language = await getLanguageRecord(params.supabase, params.languageCode);
|
|
1965
|
+
const { data: existingItems, error: existingItemsError } = await params.supabase
|
|
1966
|
+
.from('navigation_items')
|
|
1967
|
+
.select('id, parent_id')
|
|
1968
|
+
.eq('menu_key', params.menuKey)
|
|
1969
|
+
.eq('language_id', language.id);
|
|
1970
|
+
|
|
1971
|
+
if (existingItemsError) {
|
|
1972
|
+
throw new Error(
|
|
1973
|
+
`Failed to inspect existing ${params.menuKey} navigation items: ${serializeError(existingItemsError)}`
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
const existingRows = Array.isArray(existingItems) ? existingItems : [];
|
|
1978
|
+
|
|
1979
|
+
assertNavigationReplacementIsSafe({
|
|
1980
|
+
existingItemCount: existingRows.length,
|
|
1981
|
+
existingTopLevelCount: existingRows.filter((item: any) => item.parent_id == null).length,
|
|
1982
|
+
languageCode: language.code,
|
|
1983
|
+
menuKey: params.menuKey,
|
|
1984
|
+
replacementItemCount: countNavigationInputItems(params.items),
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async function updateNavigationMenuItem(params: {
|
|
1989
|
+
items: NavigationItemInput[];
|
|
1990
|
+
languageCode: string;
|
|
1991
|
+
match?: z.infer<typeof navigationItemMatchSchema>;
|
|
1992
|
+
menuKey: MenuKey;
|
|
1993
|
+
supabase: SupabaseLike;
|
|
1994
|
+
}) {
|
|
1995
|
+
if (params.items.length !== 1) {
|
|
1996
|
+
throw new Error('mode "update" requires exactly one navigation item.');
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
const language = await getLanguageRecord(params.supabase, params.languageCode);
|
|
2000
|
+
const item = params.items[0];
|
|
2001
|
+
const matchUrl = normalizeNavigationUrl(params.match?.url) || normalizeNavigationUrl(item.url);
|
|
2002
|
+
const matchLabel = normalizeNavigationLabel(params.match?.label);
|
|
2003
|
+
const { data: existingItems, error: existingItemsError } = await params.supabase
|
|
2004
|
+
.from('navigation_items')
|
|
2005
|
+
.select('id, label, url, parent_id, order')
|
|
2006
|
+
.eq('menu_key', params.menuKey)
|
|
2007
|
+
.eq('language_id', language.id);
|
|
2008
|
+
|
|
2009
|
+
if (existingItemsError) {
|
|
2010
|
+
throw new Error(
|
|
2011
|
+
`Failed to load existing ${params.menuKey} navigation items: ${serializeError(existingItemsError)}`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
const existingRows = Array.isArray(existingItems) ? existingItems : [];
|
|
2016
|
+
const matchedItem = existingRows.find((row: any) => {
|
|
2017
|
+
const rowUrl = normalizeNavigationUrl(row.url);
|
|
2018
|
+
const rowLabel = normalizeNavigationLabel(row.label);
|
|
2019
|
+
|
|
2020
|
+
return Boolean(
|
|
2021
|
+
(matchUrl && rowUrl === matchUrl) ||
|
|
2022
|
+
(matchLabel && rowLabel === matchLabel)
|
|
2023
|
+
);
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
if (!matchedItem?.id) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`Could not find a ${params.menuKey} navigation item to update in ${language.code}. Use a matching label or url.`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
const { error: updateError } = await params.supabase
|
|
2033
|
+
.from('navigation_items')
|
|
2034
|
+
.update({
|
|
2035
|
+
label: item.label,
|
|
2036
|
+
url: item.url,
|
|
2037
|
+
})
|
|
2038
|
+
.eq('id', matchedItem.id);
|
|
2039
|
+
|
|
2040
|
+
if (updateError) {
|
|
2041
|
+
throw new Error(`Failed to update ${params.menuKey} navigation item: ${serializeError(updateError)}`);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
return {
|
|
2045
|
+
insertedCount: 0,
|
|
2046
|
+
languageCode: language.code,
|
|
2047
|
+
menuKey: params.menuKey,
|
|
2048
|
+
skippedCount: 0,
|
|
2049
|
+
updatedCount: 1,
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
async function appendNavigationMenuItems(params: {
|
|
2054
|
+
items: NavigationItemInput[];
|
|
2055
|
+
languageCode: string;
|
|
2056
|
+
menuKey: MenuKey;
|
|
2057
|
+
supabase: SupabaseLike;
|
|
2058
|
+
}) {
|
|
2059
|
+
const language = await getLanguageRecord(params.supabase, params.languageCode);
|
|
2060
|
+
const { data: existingItems, error: existingItemsError } = await params.supabase
|
|
2061
|
+
.from('navigation_items')
|
|
2062
|
+
.select('id, url, parent_id, order')
|
|
2063
|
+
.eq('menu_key', params.menuKey)
|
|
2064
|
+
.eq('language_id', language.id);
|
|
2065
|
+
|
|
2066
|
+
if (existingItemsError) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`Failed to load existing ${params.menuKey} navigation items: ${serializeError(existingItemsError)}`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
let insertedCount = 0;
|
|
2073
|
+
let skippedCount = 0;
|
|
2074
|
+
const existingRows = Array.isArray(existingItems) ? existingItems : [];
|
|
2075
|
+
const existingUrls = new Set(
|
|
2076
|
+
existingRows.map((item: any) => normalizeNavigationUrl(item.url)).filter(Boolean)
|
|
2077
|
+
);
|
|
2078
|
+
const topLevelOrders = existingRows
|
|
2079
|
+
.filter((item: any) => item.parent_id == null)
|
|
2080
|
+
.map((item: any) => Number(item.order))
|
|
2081
|
+
.filter(Number.isFinite);
|
|
2082
|
+
let nextOrder = topLevelOrders.length > 0 ? Math.max(...topLevelOrders) + 1 : existingRows.length;
|
|
2083
|
+
|
|
2084
|
+
for (const item of params.items) {
|
|
2085
|
+
const itemUrl = normalizeNavigationUrl(item.url);
|
|
2086
|
+
|
|
2087
|
+
if (itemUrl && existingUrls.has(itemUrl)) {
|
|
2088
|
+
skippedCount++;
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const parentId = await insertNavigationItem({
|
|
2093
|
+
item,
|
|
2094
|
+
languageId: language.id,
|
|
2095
|
+
menuKey: params.menuKey,
|
|
2096
|
+
order: nextOrder,
|
|
2097
|
+
supabase: params.supabase,
|
|
2098
|
+
});
|
|
2099
|
+
insertedCount++;
|
|
2100
|
+
nextOrder++;
|
|
2101
|
+
|
|
2102
|
+
if (itemUrl) {
|
|
2103
|
+
existingUrls.add(itemUrl);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
let nextChildOrder = 0;
|
|
2107
|
+
|
|
2108
|
+
for (const child of item.children ?? []) {
|
|
2109
|
+
const childUrl = normalizeNavigationUrl(child.url);
|
|
2110
|
+
|
|
2111
|
+
if (childUrl && existingUrls.has(childUrl)) {
|
|
2112
|
+
skippedCount++;
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
await insertNavigationItem({
|
|
2117
|
+
item: child,
|
|
2118
|
+
languageId: language.id,
|
|
2119
|
+
menuKey: params.menuKey,
|
|
2120
|
+
order: nextChildOrder,
|
|
2121
|
+
parentId,
|
|
2122
|
+
supabase: params.supabase,
|
|
2123
|
+
});
|
|
2124
|
+
insertedCount++;
|
|
2125
|
+
nextChildOrder++;
|
|
2126
|
+
|
|
2127
|
+
if (childUrl) {
|
|
2128
|
+
existingUrls.add(childUrl);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
return {
|
|
2134
|
+
insertedCount,
|
|
2135
|
+
languageCode: language.code,
|
|
2136
|
+
menuKey: params.menuKey,
|
|
2137
|
+
skippedCount,
|
|
2138
|
+
updatedCount: 0,
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function stringifyContentValue(value: unknown) {
|
|
2143
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function escapeHtml(value: string) {
|
|
2147
|
+
return value
|
|
2148
|
+
.replace(/&/g, '&')
|
|
2149
|
+
.replace(/</g, '<')
|
|
2150
|
+
.replace(/>/g, '>')
|
|
2151
|
+
.replace(/"/g, '"')
|
|
2152
|
+
.replace(/'/g, ''');
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function normalizeFormFieldType(field: Record<string, unknown>) {
|
|
2156
|
+
const rawType = stringifyContentValue(field.field_type ?? field.fieldType ?? field.type ?? field.input_type)
|
|
2157
|
+
.toLowerCase()
|
|
2158
|
+
.replace(/\s+/g, '_');
|
|
2159
|
+
const label = stringifyContentValue(field.label ?? field.name ?? field.placeholder).toLowerCase();
|
|
2160
|
+
|
|
2161
|
+
if (['email', 'textarea', 'select', 'radio', 'checkbox', 'text'].includes(rawType)) {
|
|
2162
|
+
return rawType;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
if (label.includes('email')) {
|
|
2166
|
+
return 'email';
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
if (label.includes('message') || label.includes('comment') || label.includes('details')) {
|
|
2170
|
+
return 'textarea';
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
return 'text';
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function normalizeFormFields(fields: unknown) {
|
|
2177
|
+
const sourceFields =
|
|
2178
|
+
Array.isArray(fields) && fields.length > 0
|
|
2179
|
+
? fields
|
|
2180
|
+
: [
|
|
2181
|
+
{ field_type: 'text', is_required: true, label: 'Name', placeholder: 'Your name' },
|
|
2182
|
+
{ field_type: 'email', is_required: true, label: 'Email', placeholder: 'you@example.com' },
|
|
2183
|
+
{ field_type: 'textarea', is_required: true, label: 'Message', placeholder: 'How can we help?' },
|
|
2184
|
+
];
|
|
2185
|
+
|
|
2186
|
+
return sourceFields.map((field, index) => {
|
|
2187
|
+
const fieldRecord = isPlainJsonRecord(field) ? field : {};
|
|
2188
|
+
const label =
|
|
2189
|
+
stringifyContentValue(fieldRecord.label ?? fieldRecord.name) ||
|
|
2190
|
+
(index === 0 ? 'Name' : index === 1 ? 'Email' : 'Message');
|
|
2191
|
+
const fieldType = normalizeFormFieldType({ ...fieldRecord, label });
|
|
2192
|
+
const rawRequired = fieldRecord.is_required ?? fieldRecord.isRequired ?? fieldRecord.required;
|
|
2193
|
+
|
|
2194
|
+
return {
|
|
2195
|
+
field_type: fieldType,
|
|
2196
|
+
is_required: typeof rawRequired === 'boolean' ? rawRequired : true,
|
|
2197
|
+
label,
|
|
2198
|
+
options: Array.isArray(fieldRecord.options) ? fieldRecord.options : undefined,
|
|
2199
|
+
placeholder: stringifyContentValue(fieldRecord.placeholder) || undefined,
|
|
2200
|
+
temp_id:
|
|
2201
|
+
stringifyContentValue(fieldRecord.temp_id ?? fieldRecord.tempId ?? fieldRecord.id) ||
|
|
2202
|
+
`field-${index + 1}`,
|
|
2203
|
+
};
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function normalizeBlockContentForType(
|
|
2208
|
+
blockType: BlockType,
|
|
2209
|
+
rawContent: Record<string, unknown>,
|
|
2210
|
+
label: string
|
|
2211
|
+
) {
|
|
2212
|
+
const content = cloneJsonValue(rawContent);
|
|
2213
|
+
|
|
2214
|
+
if (blockType === 'heading') {
|
|
2215
|
+
content.text_content =
|
|
2216
|
+
stringifyContentValue(content.text_content) ||
|
|
2217
|
+
stringifyContentValue(content.text) ||
|
|
2218
|
+
stringifyContentValue(content.title) ||
|
|
2219
|
+
stringifyContentValue(content.heading) ||
|
|
2220
|
+
'Untitled';
|
|
2221
|
+
content.level = typeof content.level === 'number' ? content.level : 1;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
if (blockType === 'text') {
|
|
2225
|
+
const htmlContent =
|
|
2226
|
+
stringifyContentValue(content.html_content) ||
|
|
2227
|
+
stringifyContentValue(content.html) ||
|
|
2228
|
+
stringifyContentValue(content.content) ||
|
|
2229
|
+
stringifyContentValue(content.text);
|
|
2230
|
+
|
|
2231
|
+
if (htmlContent) {
|
|
2232
|
+
content.html_content = /<\/?[a-z][\s\S]*>/i.test(htmlContent)
|
|
2233
|
+
? htmlContent
|
|
2234
|
+
: `<p>${escapeHtml(htmlContent)}</p>`;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
if (blockType === 'button') {
|
|
2239
|
+
content.text =
|
|
2240
|
+
stringifyContentValue(content.text) ||
|
|
2241
|
+
stringifyContentValue(content.label) ||
|
|
2242
|
+
stringifyContentValue(content.title) ||
|
|
2243
|
+
'Learn More';
|
|
2244
|
+
content.url =
|
|
2245
|
+
stringifyContentValue(content.url) ||
|
|
2246
|
+
stringifyContentValue(content.href) ||
|
|
2247
|
+
stringifyContentValue(content.link) ||
|
|
2248
|
+
'#';
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
if (blockType === 'form') {
|
|
2252
|
+
content.fields = normalizeFormFields(content.fields);
|
|
2253
|
+
content.submit_button_text =
|
|
2254
|
+
stringifyContentValue(content.submit_button_text ?? content.submitButtonText ?? content.button_text) ||
|
|
2255
|
+
'Send Message';
|
|
2256
|
+
content.success_message =
|
|
2257
|
+
stringifyContentValue(content.success_message ?? content.successMessage) ||
|
|
2258
|
+
'Thanks for reaching out. We will reply as soon as possible.';
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
assertValidBlockContent(blockType, content, label);
|
|
2262
|
+
|
|
2263
|
+
return content;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function normalizeCreateBlock(input: z.infer<typeof createCmsBlockInputSchema>, index: number) {
|
|
2267
|
+
const content = normalizeBlockContentForType(
|
|
2268
|
+
input.blockType,
|
|
2269
|
+
cloneJsonRecord(input.content, `Block ${index}`),
|
|
2270
|
+
`Block ${index}`
|
|
2271
|
+
);
|
|
2272
|
+
|
|
2273
|
+
return {
|
|
2274
|
+
block_type: input.blockType,
|
|
2275
|
+
content,
|
|
2276
|
+
order: input.order ?? index,
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function buildContactPageBlocks(contactEmail: string, title = 'Contact Us') {
|
|
2281
|
+
return [
|
|
2282
|
+
normalizeCreateBlock(
|
|
2283
|
+
{
|
|
2284
|
+
blockType: 'section',
|
|
2285
|
+
content: {
|
|
2286
|
+
is_hero: true,
|
|
2287
|
+
background: { type: 'none' },
|
|
2288
|
+
column_blocks: [
|
|
2289
|
+
[
|
|
2290
|
+
{
|
|
2291
|
+
block_type: 'heading',
|
|
2292
|
+
content: {
|
|
2293
|
+
level: 1,
|
|
2294
|
+
textAlign: 'center',
|
|
2295
|
+
text_content: title,
|
|
2296
|
+
},
|
|
2297
|
+
temp_id: createNestedTempId('heading'),
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
block_type: 'text',
|
|
2301
|
+
content: {
|
|
2302
|
+
html_content:
|
|
2303
|
+
'<p>Have a question, project, or support request? Send us a note and we will get back to you soon.</p>',
|
|
2304
|
+
},
|
|
2305
|
+
temp_id: createNestedTempId('text'),
|
|
2306
|
+
},
|
|
2307
|
+
],
|
|
2308
|
+
],
|
|
2309
|
+
column_gap: 'lg',
|
|
2310
|
+
container_type: 'container',
|
|
2311
|
+
padding: { bottom: 'xl', top: 'xl' },
|
|
2312
|
+
responsive_columns: { desktop: 1, mobile: 1, tablet: 1 },
|
|
2313
|
+
vertical_alignment: 'center',
|
|
2314
|
+
},
|
|
2315
|
+
},
|
|
2316
|
+
0
|
|
2317
|
+
),
|
|
2318
|
+
normalizeCreateBlock(
|
|
2319
|
+
{
|
|
2320
|
+
blockType: 'form',
|
|
2321
|
+
content: {
|
|
2322
|
+
fields: [
|
|
2323
|
+
{
|
|
2324
|
+
field_type: 'text',
|
|
2325
|
+
is_required: true,
|
|
2326
|
+
label: 'Name',
|
|
2327
|
+
placeholder: 'Your name',
|
|
2328
|
+
temp_id: 'field-name',
|
|
2329
|
+
},
|
|
2330
|
+
{
|
|
2331
|
+
field_type: 'email',
|
|
2332
|
+
is_required: true,
|
|
2333
|
+
label: 'Email',
|
|
2334
|
+
placeholder: 'you@example.com',
|
|
2335
|
+
temp_id: 'field-email',
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
field_type: 'textarea',
|
|
2339
|
+
is_required: true,
|
|
2340
|
+
label: 'Message',
|
|
2341
|
+
placeholder: 'How can we help?',
|
|
2342
|
+
temp_id: 'field-message',
|
|
2343
|
+
},
|
|
2344
|
+
],
|
|
2345
|
+
recipient_email: contactEmail,
|
|
2346
|
+
submit_button_text: 'Send Message',
|
|
2347
|
+
success_message: 'Thanks for reaching out. We will reply as soon as possible.',
|
|
2348
|
+
},
|
|
2349
|
+
},
|
|
2350
|
+
1
|
|
2351
|
+
),
|
|
2352
|
+
];
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function normalizeCreateBlocks(
|
|
2356
|
+
blocks: Array<z.infer<typeof createCmsBlockInputSchema>> | undefined,
|
|
2357
|
+
fallbackContactEmail?: string,
|
|
2358
|
+
title?: string
|
|
2359
|
+
) {
|
|
2360
|
+
if ((!blocks || blocks.length === 0) && fallbackContactEmail) {
|
|
2361
|
+
return buildContactPageBlocks(fallbackContactEmail, title);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
return (blocks || []).map((block, index) => normalizeCreateBlock(block, index));
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
async function assertUniqueSlug(params: {
|
|
2368
|
+
contentType: CmsContentType;
|
|
2369
|
+
languageId: number;
|
|
2370
|
+
slug: string;
|
|
2371
|
+
supabase: SupabaseLike;
|
|
2372
|
+
}) {
|
|
2373
|
+
const table =
|
|
2374
|
+
params.contentType === 'page'
|
|
2375
|
+
? 'pages'
|
|
2376
|
+
: params.contentType === 'post'
|
|
2377
|
+
? 'posts'
|
|
2378
|
+
: 'products';
|
|
2379
|
+
const { data, error } = await params.supabase
|
|
2380
|
+
.from(table)
|
|
2381
|
+
.select('id, title, slug, language_id')
|
|
2382
|
+
.eq('slug', params.slug)
|
|
2383
|
+
.eq('language_id', params.languageId);
|
|
2384
|
+
|
|
2385
|
+
if (error) {
|
|
2386
|
+
throw new Error(`Failed to check ${params.contentType} slug uniqueness: ${serializeError(error)}`);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const existingItems = Array.isArray(data) ? data : [];
|
|
2390
|
+
|
|
2391
|
+
if (existingItems.length > 0) {
|
|
2392
|
+
return {
|
|
2393
|
+
duplicate: true,
|
|
2394
|
+
existingItem: existingItems[0],
|
|
2395
|
+
mutationExecuted: false,
|
|
2396
|
+
success: false,
|
|
2397
|
+
message: `A ${params.contentType} with slug "${params.slug}" already exists for this language.`,
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
async function resolveCreateTranslationGroup(params: {
|
|
2405
|
+
contentType: CmsContentType;
|
|
2406
|
+
languageCode: string;
|
|
2407
|
+
languageId: number;
|
|
2408
|
+
suppliedTranslationGroupId?: string;
|
|
2409
|
+
supabase: SupabaseLike;
|
|
2410
|
+
}) {
|
|
2411
|
+
if (!params.suppliedTranslationGroupId) {
|
|
2412
|
+
return {
|
|
2413
|
+
translationGroupId: undefined,
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const table =
|
|
2418
|
+
params.contentType === 'page'
|
|
2419
|
+
? 'pages'
|
|
2420
|
+
: params.contentType === 'post'
|
|
2421
|
+
? 'posts'
|
|
2422
|
+
: 'products';
|
|
2423
|
+
const { data, error } = await params.supabase
|
|
2424
|
+
.from(table)
|
|
2425
|
+
.select('id, title, slug, language_id, translation_group_id')
|
|
2426
|
+
.eq('translation_group_id', params.suppliedTranslationGroupId);
|
|
2427
|
+
|
|
2428
|
+
if (error) {
|
|
2429
|
+
throw new Error(
|
|
2430
|
+
`Failed to inspect ${params.contentType} translation group: ${serializeError(error)}`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
const rows = Array.isArray(data) ? data : [];
|
|
2435
|
+
|
|
2436
|
+
if (rows.length === 0) {
|
|
2437
|
+
return {
|
|
2438
|
+
result: {
|
|
2439
|
+
message: `The ${params.contentType} translation group "${params.suppliedTranslationGroupId}" was not found.`,
|
|
2440
|
+
mutationExecuted: false,
|
|
2441
|
+
success: false,
|
|
2442
|
+
},
|
|
2443
|
+
translationGroupId: params.suppliedTranslationGroupId,
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
const existingTranslation = rows.find(
|
|
2448
|
+
(row: any) => Number(row.language_id) === params.languageId
|
|
2449
|
+
);
|
|
2450
|
+
|
|
2451
|
+
if (existingTranslation) {
|
|
2452
|
+
return {
|
|
2453
|
+
result: {
|
|
2454
|
+
duplicateTranslation: true,
|
|
2455
|
+
existingItem: existingTranslation,
|
|
2456
|
+
message: `A ${params.contentType} translation already exists for ${params.languageCode} in this translation group.`,
|
|
2457
|
+
mutationExecuted: false,
|
|
2458
|
+
success: false,
|
|
2459
|
+
},
|
|
2460
|
+
translationGroupId: params.suppliedTranslationGroupId,
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
return {
|
|
2465
|
+
translationGroupId: params.suppliedTranslationGroupId,
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
async function insertContentBlocks(params: {
|
|
2470
|
+
blocks: Array<{ block_type: BlockType; content: Record<string, unknown>; order: number }>;
|
|
2471
|
+
contentType: 'page' | 'post';
|
|
2472
|
+
itemId: number;
|
|
2473
|
+
languageId: number;
|
|
2474
|
+
supabase: SupabaseLike;
|
|
2475
|
+
}) {
|
|
2476
|
+
if (params.blocks.length === 0) {
|
|
2477
|
+
return [];
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
const blockRows = params.blocks.map((block, index) => ({
|
|
2481
|
+
block_type: block.block_type,
|
|
2482
|
+
content: block.content,
|
|
2483
|
+
language_id: params.languageId,
|
|
2484
|
+
order: block.order ?? index,
|
|
2485
|
+
page_id: params.contentType === 'page' ? params.itemId : null,
|
|
2486
|
+
post_id: params.contentType === 'post' ? params.itemId : null,
|
|
2487
|
+
}));
|
|
2488
|
+
const { data, error } = await params.supabase.from('blocks').insert(blockRows).select('*');
|
|
2489
|
+
|
|
2490
|
+
if (error) {
|
|
2491
|
+
throw new Error(`Failed to insert ${params.contentType} blocks: ${serializeError(error)}`);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
return Array.isArray(data) ? data : [];
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
async function rollbackCreatedCmsItem(params: {
|
|
2498
|
+
contentType: 'page' | 'post';
|
|
2499
|
+
itemId: number;
|
|
2500
|
+
supabase: SupabaseLike;
|
|
2501
|
+
}) {
|
|
2502
|
+
const table = params.contentType === 'page' ? 'pages' : 'posts';
|
|
2503
|
+
|
|
2504
|
+
await params.supabase.from(table).delete().eq('id', params.itemId);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function getCreateEditPath(contentType: CmsContentType, entityId: string | number) {
|
|
2508
|
+
if (contentType === 'page') {
|
|
2509
|
+
return `/cms/pages/${entityId}/edit`;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
if (contentType === 'post') {
|
|
2513
|
+
return `/cms/posts/${entityId}/edit`;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
return `/cms/products/${entityId}/edit`;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
function getCollectionPath(contentType: CmsContentType) {
|
|
2520
|
+
if (contentType === 'page') {
|
|
2521
|
+
return '/cms/pages';
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
if (contentType === 'post') {
|
|
2525
|
+
return '/cms/posts';
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
return '/cms/products';
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
export async function executeUpdateNavigationBar(
|
|
2532
|
+
input: UpdateNavigationBarInput,
|
|
2533
|
+
context?: ToolExecutionContext
|
|
2534
|
+
) {
|
|
2535
|
+
const parsed = updateNavigationBarInputSchema.parse(input);
|
|
2536
|
+
const supabase = getSupabase(context);
|
|
2537
|
+
|
|
2538
|
+
if (parsed.mode === 'replace') {
|
|
2539
|
+
await assertNavigationReplacementInputIsSafe({
|
|
2540
|
+
items: parsed.items,
|
|
2541
|
+
languageCode: parsed.languageCode,
|
|
2542
|
+
menuKey: 'HEADER',
|
|
2543
|
+
supabase,
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
const confirmation = getConfirmationPreview({
|
|
2548
|
+
action: 'UPDATE NAVIGATION',
|
|
2549
|
+
context,
|
|
2550
|
+
payload: { input: parsed, tool: 'update_navigation_bar' },
|
|
2551
|
+
preview: {
|
|
2552
|
+
itemCount: parsed.items.length,
|
|
2553
|
+
languageCode: parsed.languageCode,
|
|
2554
|
+
mode: parsed.mode,
|
|
2555
|
+
target: 'header navigation',
|
|
2556
|
+
},
|
|
2557
|
+
subject: `${parsed.mode} header`,
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2560
|
+
if (confirmation) {
|
|
2561
|
+
return confirmation;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
const result =
|
|
2565
|
+
parsed.mode === 'update'
|
|
2566
|
+
? await updateNavigationMenuItem({
|
|
2567
|
+
items: parsed.items,
|
|
2568
|
+
languageCode: parsed.languageCode,
|
|
2569
|
+
match: parsed.match,
|
|
2570
|
+
menuKey: 'HEADER',
|
|
2571
|
+
supabase,
|
|
2572
|
+
})
|
|
2573
|
+
: parsed.mode === 'append'
|
|
2574
|
+
? await appendNavigationMenuItems({
|
|
2575
|
+
items: parsed.items,
|
|
2576
|
+
languageCode: parsed.languageCode,
|
|
2577
|
+
menuKey: 'HEADER',
|
|
2578
|
+
supabase,
|
|
2579
|
+
})
|
|
2580
|
+
: await replaceNavigationMenu({
|
|
2581
|
+
items: parsed.items,
|
|
2582
|
+
languageCode: parsed.languageCode,
|
|
2583
|
+
menuKey: 'HEADER',
|
|
2584
|
+
supabase,
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
revalidateGlobalCmsSurfaces(context);
|
|
2588
|
+
|
|
2589
|
+
return {
|
|
2590
|
+
...result,
|
|
2591
|
+
mutationExecuted: true,
|
|
2592
|
+
mode: parsed.mode,
|
|
2593
|
+
success: true,
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
export async function executeUpdateFooter(input: UpdateFooterInput, context?: ToolExecutionContext) {
|
|
2598
|
+
const parsed = updateFooterInputSchema.parse(input);
|
|
2599
|
+
|
|
2600
|
+
if (!parsed.links?.length && !parsed.copyright) {
|
|
2601
|
+
throw new Error('update_footer requires links or copyright.');
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
const supabase = getSupabase(context);
|
|
2605
|
+
const confirmation = getConfirmationPreview({
|
|
2606
|
+
action: 'UPDATE FOOTER',
|
|
2607
|
+
context,
|
|
2608
|
+
payload: { input: parsed, tool: 'update_footer' },
|
|
2609
|
+
preview: {
|
|
2610
|
+
copyrightUpdated: Boolean(parsed.copyright),
|
|
2611
|
+
linkCount: parsed.links?.length || 0,
|
|
2612
|
+
languageCode: parsed.languageCode,
|
|
2613
|
+
target: 'footer',
|
|
2614
|
+
},
|
|
2615
|
+
subject: parsed.languageCode,
|
|
2616
|
+
});
|
|
2617
|
+
|
|
2618
|
+
if (confirmation) {
|
|
2619
|
+
return confirmation;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
let footerNavigation:
|
|
2623
|
+
| {
|
|
2624
|
+
insertedCount: number;
|
|
2625
|
+
languageCode: string;
|
|
2626
|
+
menuKey: 'FOOTER';
|
|
2627
|
+
skippedCount: number;
|
|
2628
|
+
updatedCount: number;
|
|
2629
|
+
}
|
|
2630
|
+
| null = null;
|
|
2631
|
+
|
|
2632
|
+
if (parsed.links?.length) {
|
|
2633
|
+
footerNavigation = await replaceNavigationMenu({
|
|
2634
|
+
items: parsed.links,
|
|
2635
|
+
languageCode: parsed.languageCode,
|
|
2636
|
+
menuKey: 'FOOTER',
|
|
2637
|
+
supabase,
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
if (parsed.copyright) {
|
|
2642
|
+
const { error } = await supabase.from('site_settings').upsert({
|
|
2643
|
+
key: 'footer_copyright',
|
|
2644
|
+
value: parsed.copyright,
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
if (error) {
|
|
2648
|
+
throw new Error(`Failed to update footer copyright: ${serializeError(error)}`);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
revalidateGlobalCmsSurfaces(context);
|
|
2653
|
+
|
|
2654
|
+
return {
|
|
2655
|
+
copyrightUpdated: Boolean(parsed.copyright),
|
|
2656
|
+
footerNavigation,
|
|
2657
|
+
mutationExecuted: true,
|
|
2658
|
+
success: true,
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function normalizeSearchText(value: unknown) {
|
|
2663
|
+
return typeof value === 'string' ? value.toLowerCase() : '';
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
function scoreDocument(queryTerms: string[], values: string[]) {
|
|
2667
|
+
const haystack = values.map(normalizeSearchText).join(' ');
|
|
2668
|
+
|
|
2669
|
+
return queryTerms.reduce((score, term) => score + (haystack.includes(term) ? 1 : 0), 0);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function pickSnippet(values: string[], queryTerms: string[]) {
|
|
2673
|
+
return (
|
|
2674
|
+
values.find((value) =>
|
|
2675
|
+
queryTerms.some((term) => normalizeSearchText(value).includes(term))
|
|
2676
|
+
) ||
|
|
2677
|
+
values.find((value) => value.trim().length > 0) ||
|
|
2678
|
+
'No excerpt available.'
|
|
2679
|
+
).slice(0, 500);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
export async function executeSearchDocumentation(
|
|
2683
|
+
input: SearchDocumentationInput,
|
|
2684
|
+
context?: ToolExecutionContext
|
|
2685
|
+
) {
|
|
2686
|
+
const parsed = searchDocumentationInputSchema.parse(input);
|
|
2687
|
+
const supabase = getSupabase(context);
|
|
2688
|
+
const queryTerms = parsed.query
|
|
2689
|
+
.toLowerCase()
|
|
2690
|
+
.split(/\s+/)
|
|
2691
|
+
.map((term) => term.trim())
|
|
2692
|
+
.filter(Boolean);
|
|
2693
|
+
|
|
2694
|
+
const [postsResult, pagesResult] = await Promise.all([
|
|
2695
|
+
supabase
|
|
2696
|
+
.from('posts')
|
|
2697
|
+
.select('id, title, slug, excerpt, subtitle, meta_description, status, updated_at')
|
|
2698
|
+
.eq('status', 'published')
|
|
2699
|
+
.limit(100),
|
|
2700
|
+
supabase
|
|
2701
|
+
.from('pages')
|
|
2702
|
+
.select('id, title, slug, meta_description, status, updated_at')
|
|
2703
|
+
.eq('status', 'published')
|
|
2704
|
+
.limit(100),
|
|
2705
|
+
]);
|
|
2706
|
+
|
|
2707
|
+
if (postsResult.error) {
|
|
2708
|
+
throw new Error(`Failed to search documentation posts: ${serializeError(postsResult.error)}`);
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
if (pagesResult.error) {
|
|
2712
|
+
throw new Error(`Failed to search documentation pages: ${serializeError(pagesResult.error)}`);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
const postSnippets: DocumentationSnippet[] = (postsResult.data ?? []).map((post: any) => ({
|
|
2716
|
+
excerpt: pickSnippet(
|
|
2717
|
+
[post.excerpt, post.subtitle, post.meta_description, post.slug].filter(Boolean),
|
|
2718
|
+
queryTerms
|
|
2719
|
+
),
|
|
2720
|
+
source: 'post',
|
|
2721
|
+
title: post.title,
|
|
2722
|
+
url: `/article/${post.slug}`,
|
|
2723
|
+
}));
|
|
2724
|
+
|
|
2725
|
+
const pageSnippets: DocumentationSnippet[] = (pagesResult.data ?? []).map((page: any) => ({
|
|
2726
|
+
excerpt: pickSnippet([page.meta_description, page.slug].filter(Boolean), queryTerms),
|
|
2727
|
+
source: 'page',
|
|
2728
|
+
title: page.title,
|
|
2729
|
+
url: page.slug === 'home' ? '/' : `/${page.slug}`,
|
|
2730
|
+
}));
|
|
2731
|
+
|
|
2732
|
+
const results = [...postSnippets, ...pageSnippets]
|
|
2733
|
+
.map((snippet) => ({
|
|
2734
|
+
...snippet,
|
|
2735
|
+
score: scoreDocument(queryTerms, [snippet.title, snippet.excerpt, snippet.url]),
|
|
2736
|
+
}))
|
|
2737
|
+
.filter((snippet) => snippet.score > 0)
|
|
2738
|
+
.sort((a, b) => b.score - a.score)
|
|
2739
|
+
.slice(0, parsed.limit)
|
|
2740
|
+
.map((snippet) => ({
|
|
2741
|
+
excerpt: snippet.excerpt,
|
|
2742
|
+
source: snippet.source,
|
|
2743
|
+
title: snippet.title,
|
|
2744
|
+
url: snippet.url,
|
|
2745
|
+
}));
|
|
2746
|
+
|
|
2747
|
+
return {
|
|
2748
|
+
query: parsed.query,
|
|
2749
|
+
results,
|
|
2750
|
+
success: true,
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
export async function executeSearchDocumentationWithTimeout(
|
|
2755
|
+
input: SearchDocumentationInput,
|
|
2756
|
+
context?: ToolExecutionContext,
|
|
2757
|
+
timeoutMs = SEARCH_DOCUMENTATION_TIMEOUT_MS
|
|
2758
|
+
) {
|
|
2759
|
+
const parsed = searchDocumentationInputSchema.safeParse(input);
|
|
2760
|
+
const query = parsed.success ? parsed.data.query : '';
|
|
2761
|
+
|
|
2762
|
+
return withTimeoutFallback(
|
|
2763
|
+
executeSearchDocumentation(input, context),
|
|
2764
|
+
timeoutMs,
|
|
2765
|
+
() => ({
|
|
2766
|
+
message:
|
|
2767
|
+
'Documentation search took too long to respond. Please try again or ask a more specific question.',
|
|
2768
|
+
query,
|
|
2769
|
+
results: [],
|
|
2770
|
+
success: false,
|
|
2771
|
+
timedOut: true,
|
|
2772
|
+
})
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
const knownOrderStatuses = [
|
|
2777
|
+
'pending',
|
|
2778
|
+
'trial',
|
|
2779
|
+
'paid',
|
|
2780
|
+
'shipped',
|
|
2781
|
+
'cancelled',
|
|
2782
|
+
'refunded',
|
|
2783
|
+
'failed',
|
|
2784
|
+
] as const;
|
|
2785
|
+
|
|
2786
|
+
type KnownOrderStatus = (typeof knownOrderStatuses)[number];
|
|
2787
|
+
|
|
2788
|
+
const orderStatusAliases: Record<string, KnownOrderStatus> = {
|
|
2789
|
+
awaiting: 'pending',
|
|
2790
|
+
canceled: 'cancelled',
|
|
2791
|
+
cancelled: 'cancelled',
|
|
2792
|
+
complete: 'paid',
|
|
2793
|
+
completed: 'paid',
|
|
2794
|
+
failed: 'failed',
|
|
2795
|
+
paid: 'paid',
|
|
2796
|
+
payment_pending: 'pending',
|
|
2797
|
+
pending: 'pending',
|
|
2798
|
+
refund: 'refunded',
|
|
2799
|
+
refunded: 'refunded',
|
|
2800
|
+
refunds: 'refunded',
|
|
2801
|
+
shipped: 'shipped',
|
|
2802
|
+
trial: 'trial',
|
|
2803
|
+
trials: 'trial',
|
|
2804
|
+
};
|
|
2805
|
+
|
|
2806
|
+
function normalizeOrderStatus(value: unknown) {
|
|
2807
|
+
const normalized = String(value ?? 'unknown').trim().toLowerCase();
|
|
2808
|
+
return normalized || 'unknown';
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function buildOrderStatusCounts(rows: any[]) {
|
|
2812
|
+
const counts: Record<string, number> = Object.fromEntries(
|
|
2813
|
+
knownOrderStatuses.map((status) => [status, 0])
|
|
2814
|
+
);
|
|
2815
|
+
|
|
2816
|
+
for (const row of rows) {
|
|
2817
|
+
const status = normalizeOrderStatus(row.status);
|
|
2818
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
return counts;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
function inferRequestedOrderStatus(query: string) {
|
|
2825
|
+
const normalizedQuery = query.toLowerCase().replace(/[^a-z0-9_]+/g, ' ');
|
|
2826
|
+
const terms = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
2827
|
+
|
|
2828
|
+
for (const term of terms) {
|
|
2829
|
+
const status = orderStatusAliases[term];
|
|
2830
|
+
|
|
2831
|
+
if (status) {
|
|
2832
|
+
return status;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
function toFiniteNumber(value: unknown) {
|
|
2840
|
+
const parsed = Number(value);
|
|
2841
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
export async function executeFetchEcommerceStats(
|
|
2845
|
+
input: FetchEcommerceStatsInput,
|
|
2846
|
+
context?: ToolExecutionContext
|
|
2847
|
+
) {
|
|
2848
|
+
const parsed = fetchEcommerceStatsInputSchema.parse(input);
|
|
2849
|
+
const supabase = getSupabase(context);
|
|
2850
|
+
|
|
2851
|
+
const now = new Date();
|
|
2852
|
+
let startDate: Date | null = null;
|
|
2853
|
+
let endDate: Date | null = null;
|
|
2854
|
+
|
|
2855
|
+
switch (parsed.timeRange) {
|
|
2856
|
+
case 'today':
|
|
2857
|
+
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
2858
|
+
break;
|
|
2859
|
+
case 'this_month':
|
|
2860
|
+
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
2861
|
+
break;
|
|
2862
|
+
case 'last_7_days':
|
|
2863
|
+
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
2864
|
+
break;
|
|
2865
|
+
case 'last_30_days':
|
|
2866
|
+
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
2867
|
+
break;
|
|
2868
|
+
case 'last_month':
|
|
2869
|
+
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
2870
|
+
endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
|
2871
|
+
break;
|
|
2872
|
+
case 'last_90_days':
|
|
2873
|
+
startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
|
2874
|
+
break;
|
|
2875
|
+
case 'all_time':
|
|
2876
|
+
startDate = null;
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
const currency = parsed.currency?.toUpperCase();
|
|
2881
|
+
const requestedStatus = inferRequestedOrderStatus(parsed.query);
|
|
2882
|
+
const orderQueryBuilder = supabase
|
|
2883
|
+
.from('orders')
|
|
2884
|
+
.select('id, status, total, currency, created_at, paid_at');
|
|
2885
|
+
|
|
2886
|
+
if (currency) {
|
|
2887
|
+
orderQueryBuilder.eq('currency', currency);
|
|
2888
|
+
}
|
|
2889
|
+
if (startDate) {
|
|
2890
|
+
orderQueryBuilder.gte('created_at', startDate.toISOString());
|
|
2891
|
+
}
|
|
2892
|
+
if (endDate) {
|
|
2893
|
+
orderQueryBuilder.lte('created_at', endDate.toISOString());
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
const shouldFetchLineItems =
|
|
2897
|
+
parsed.reportType === 'products' ||
|
|
2898
|
+
parsed.reportType === 'revenue' ||
|
|
2899
|
+
parsed.reportType === 'general';
|
|
2900
|
+
const lineItemsQueryBuilder = shouldFetchLineItems
|
|
2901
|
+
? supabase
|
|
2902
|
+
.from('order_items')
|
|
2903
|
+
.select(`
|
|
2904
|
+
quantity,
|
|
2905
|
+
price_at_purchase,
|
|
2906
|
+
products!inner (
|
|
2907
|
+
id,
|
|
2908
|
+
title,
|
|
2909
|
+
product_type
|
|
2910
|
+
),
|
|
2911
|
+
orders!inner (
|
|
2912
|
+
id,
|
|
2913
|
+
status,
|
|
2914
|
+
paid_at,
|
|
2915
|
+
currency
|
|
2916
|
+
)
|
|
2917
|
+
`)
|
|
2918
|
+
.eq('orders.status', 'paid')
|
|
2919
|
+
: null;
|
|
2920
|
+
|
|
2921
|
+
if (lineItemsQueryBuilder) {
|
|
2922
|
+
if (currency) {
|
|
2923
|
+
lineItemsQueryBuilder.eq('orders.currency', currency);
|
|
2924
|
+
}
|
|
2925
|
+
if (startDate) {
|
|
2926
|
+
lineItemsQueryBuilder.gte('orders.paid_at', startDate.toISOString());
|
|
2927
|
+
}
|
|
2928
|
+
if (endDate) {
|
|
2929
|
+
lineItemsQueryBuilder.lte('orders.paid_at', endDate.toISOString());
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
const shouldFetchAllTimeOrderStatuses =
|
|
2934
|
+
parsed.timeRange !== 'all_time' && (parsed.reportType === 'orders' || requestedStatus);
|
|
2935
|
+
const allTimeOrderQueryBuilder = shouldFetchAllTimeOrderStatuses
|
|
2936
|
+
? supabase.from('orders').select('id, status, currency')
|
|
2937
|
+
: null;
|
|
2938
|
+
|
|
2939
|
+
if (allTimeOrderQueryBuilder && currency) {
|
|
2940
|
+
allTimeOrderQueryBuilder.eq('currency', currency);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
const [
|
|
2944
|
+
{ data: orderData, error: orderError },
|
|
2945
|
+
{ data: lineItemData, error: lineItemError },
|
|
2946
|
+
allTimeOrderResult,
|
|
2947
|
+
] = await Promise.all([
|
|
2948
|
+
orderQueryBuilder,
|
|
2949
|
+
lineItemsQueryBuilder ?? Promise.resolve({ data: [], error: null }),
|
|
2950
|
+
allTimeOrderQueryBuilder ?? Promise.resolve({ data: null, error: null }),
|
|
2951
|
+
]);
|
|
2952
|
+
|
|
2953
|
+
if (orderError) {
|
|
2954
|
+
throw new Error(`Failed to fetch ecommerce stats: ${serializeError(orderError)}`);
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
if (lineItemError) {
|
|
2958
|
+
throw new Error(`Failed to fetch ecommerce stats: ${serializeError(lineItemError)}`);
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
if (allTimeOrderResult.error) {
|
|
2962
|
+
throw new Error(`Failed to fetch ecommerce stats: ${serializeError(allTimeOrderResult.error)}`);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
const orderRows = Array.isArray(orderData) ? orderData : [];
|
|
2966
|
+
const rows = Array.isArray(lineItemData) ? lineItemData : [];
|
|
2967
|
+
const orderStatusCounts = buildOrderStatusCounts(orderRows);
|
|
2968
|
+
const allTimeOrderRows = Array.isArray(allTimeOrderResult.data) ? allTimeOrderResult.data : null;
|
|
2969
|
+
const allTimeOrderStatusCounts = allTimeOrderRows ? buildOrderStatusCounts(allTimeOrderRows) : null;
|
|
2970
|
+
const revenueByCurrency: Record<string, number> = {};
|
|
2971
|
+
|
|
2972
|
+
for (const row of rows) {
|
|
2973
|
+
const order = Array.isArray(row.orders) ? row.orders[0] : row.orders;
|
|
2974
|
+
const orderCurrency = String(order?.currency || currency || 'unknown').toUpperCase();
|
|
2975
|
+
revenueByCurrency[orderCurrency] =
|
|
2976
|
+
(revenueByCurrency[orderCurrency] ?? 0) +
|
|
2977
|
+
(toFiniteNumber(row.quantity) * toFiniteNumber(row.price_at_purchase)) / 100;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
const report: Record<string, any> = {
|
|
2981
|
+
currency: currency ?? null,
|
|
2982
|
+
currencyFiltered: Boolean(currency),
|
|
2983
|
+
orderStatusCounts,
|
|
2984
|
+
paidOrderCount: orderStatusCounts.paid ?? 0,
|
|
2985
|
+
query: parsed.query,
|
|
2986
|
+
reportType: parsed.reportType,
|
|
2987
|
+
revenueByCurrency,
|
|
2988
|
+
timeRange: parsed.timeRange,
|
|
2989
|
+
totalOrders: orderRows.length,
|
|
2990
|
+
totalRevenue: Object.values(revenueByCurrency).reduce(
|
|
2991
|
+
(sum: number, revenue: number) => sum + revenue,
|
|
2992
|
+
0
|
|
2993
|
+
),
|
|
2994
|
+
};
|
|
2995
|
+
|
|
2996
|
+
if (allTimeOrderStatusCounts) {
|
|
2997
|
+
report.allTimeOrderStatusCounts = allTimeOrderStatusCounts;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
if (requestedStatus) {
|
|
3001
|
+
report.matchingOrderStatus = {
|
|
3002
|
+
allTimeCount: allTimeOrderStatusCounts?.[requestedStatus] ?? orderStatusCounts[requestedStatus] ?? 0,
|
|
3003
|
+
count: orderStatusCounts[requestedStatus] ?? 0,
|
|
3004
|
+
status: requestedStatus,
|
|
3005
|
+
timeRange: parsed.timeRange,
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
if (parsed.reportType === 'products' || parsed.reportType === 'revenue' || parsed.reportType === 'general') {
|
|
3010
|
+
const productStats: Record<string, { id: string; revenue: number; quantity: number; title: string; type: string }> = {};
|
|
3011
|
+
|
|
3012
|
+
for (const row of rows) {
|
|
3013
|
+
const product = Array.isArray(row.products) ? row.products[0] : row.products;
|
|
3014
|
+
|
|
3015
|
+
if (!product?.id) {
|
|
3016
|
+
continue;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
const productId = product.id;
|
|
3020
|
+
if (!productStats[productId]) {
|
|
3021
|
+
productStats[productId] = {
|
|
3022
|
+
id: productId,
|
|
3023
|
+
quantity: 0,
|
|
3024
|
+
revenue: 0,
|
|
3025
|
+
title: product.title,
|
|
3026
|
+
type: product.product_type,
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
productStats[productId].quantity += toFiniteNumber(row.quantity);
|
|
3030
|
+
productStats[productId].revenue +=
|
|
3031
|
+
(toFiniteNumber(row.quantity) * toFiniteNumber(row.price_at_purchase)) / 100;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
report.topProducts = Object.values(productStats)
|
|
3035
|
+
.sort((a, b) => b.revenue - a.revenue)
|
|
3036
|
+
.slice(0, 10);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
return {
|
|
3040
|
+
report,
|
|
3041
|
+
success: true,
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
export async function executeReadCurrentCmsItem(
|
|
3046
|
+
input: ReadCurrentCmsItemInput,
|
|
3047
|
+
context?: ToolExecutionContext
|
|
3048
|
+
) {
|
|
3049
|
+
const parsed = readCurrentCmsItemInputSchema.parse(input);
|
|
3050
|
+
const supabase = getSupabase(context);
|
|
3051
|
+
const pageContext = getCurrentCmsContext(context);
|
|
3052
|
+
const entityId = getCmsEntityId(pageContext);
|
|
3053
|
+
const table =
|
|
3054
|
+
pageContext.contentType === 'page'
|
|
3055
|
+
? 'pages'
|
|
3056
|
+
: pageContext.contentType === 'post'
|
|
3057
|
+
? 'posts'
|
|
3058
|
+
: 'products';
|
|
3059
|
+
const { data: item, error: itemError } = await supabase
|
|
3060
|
+
.from(table)
|
|
3061
|
+
.select('*')
|
|
3062
|
+
.eq('id', entityId)
|
|
3063
|
+
.single();
|
|
3064
|
+
|
|
3065
|
+
if (itemError || !item) {
|
|
3066
|
+
throw new Error(
|
|
3067
|
+
`Failed to read current ${pageContext.contentType}: ${serializeError(itemError)}`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
let blocks: ReturnType<typeof summarizeBlockRow>[] = [];
|
|
3072
|
+
|
|
3073
|
+
if (parsed.includeBlocks && pageContext.contentType !== 'product') {
|
|
3074
|
+
const blockParentColumn = pageContext.contentType === 'page' ? 'page_id' : 'post_id';
|
|
3075
|
+
const { data: blockRows, error: blocksError } = await supabase
|
|
3076
|
+
.from('blocks')
|
|
3077
|
+
.select('id, page_id, post_id, language_id, block_type, content, order')
|
|
3078
|
+
.eq(blockParentColumn, entityId);
|
|
3079
|
+
|
|
3080
|
+
if (blocksError) {
|
|
3081
|
+
throw new Error(`Failed to read current ${pageContext.contentType} blocks: ${serializeError(blocksError)}`);
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
blocks = (Array.isArray(blockRows) ? blockRows : [])
|
|
3085
|
+
.slice()
|
|
3086
|
+
.sort((a: any, b: any) => Number(a.order) - Number(b.order))
|
|
3087
|
+
.map((block: any) => summarizeBlockRow(block, parsed.includeBlockContent));
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
return {
|
|
3091
|
+
blocks,
|
|
3092
|
+
context: pageContext,
|
|
3093
|
+
item,
|
|
3094
|
+
success: true,
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
const PAGE_FIELD_NAMES = new Set([
|
|
3099
|
+
'feature_image_id',
|
|
3100
|
+
'language_id',
|
|
3101
|
+
'meta_description',
|
|
3102
|
+
'meta_title',
|
|
3103
|
+
'slug',
|
|
3104
|
+
'status',
|
|
3105
|
+
'title',
|
|
3106
|
+
]);
|
|
3107
|
+
const POST_FIELD_NAMES = new Set([
|
|
3108
|
+
'excerpt',
|
|
3109
|
+
'feature_image_id',
|
|
3110
|
+
'label',
|
|
3111
|
+
'language_id',
|
|
3112
|
+
'meta_description',
|
|
3113
|
+
'meta_title',
|
|
3114
|
+
'published_at',
|
|
3115
|
+
'slug',
|
|
3116
|
+
'status',
|
|
3117
|
+
'subtitle',
|
|
3118
|
+
'title',
|
|
3119
|
+
]);
|
|
3120
|
+
const PRODUCT_FIELD_NAMES = new Set([
|
|
3121
|
+
'description_json',
|
|
3122
|
+
'language_id',
|
|
3123
|
+
'meta_description',
|
|
3124
|
+
'meta_title',
|
|
3125
|
+
'short_description',
|
|
3126
|
+
'slug',
|
|
3127
|
+
'status',
|
|
3128
|
+
'title',
|
|
3129
|
+
]);
|
|
3130
|
+
const NULLABLE_TEXT_FIELD_NAMES = new Set([
|
|
3131
|
+
'excerpt',
|
|
3132
|
+
'feature_image_id',
|
|
3133
|
+
'label',
|
|
3134
|
+
'meta_description',
|
|
3135
|
+
'meta_title',
|
|
3136
|
+
'published_at',
|
|
3137
|
+
'short_description',
|
|
3138
|
+
'subtitle',
|
|
3139
|
+
]);
|
|
3140
|
+
|
|
3141
|
+
function getAllowedFieldNames(contentType: CortexAiPageContext['contentType']) {
|
|
3142
|
+
if (contentType === 'page') {
|
|
3143
|
+
return PAGE_FIELD_NAMES;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
if (contentType === 'post') {
|
|
3147
|
+
return POST_FIELD_NAMES;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
return PRODUCT_FIELD_NAMES;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
function normalizeCmsFieldValue(fieldName: string, value: unknown) {
|
|
3154
|
+
if (NULLABLE_TEXT_FIELD_NAMES.has(fieldName) && value === '') {
|
|
3155
|
+
return null;
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
return value;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
function assertValidStatusForContentType(
|
|
3162
|
+
contentType: CortexAiPageContext['contentType'],
|
|
3163
|
+
status: unknown
|
|
3164
|
+
) {
|
|
3165
|
+
if (typeof status !== 'string') {
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
const allowedStatuses =
|
|
3170
|
+
contentType === 'product'
|
|
3171
|
+
? ['active', 'archived', 'draft']
|
|
3172
|
+
: ['archived', 'draft', 'published'];
|
|
3173
|
+
|
|
3174
|
+
if (!allowedStatuses.includes(status)) {
|
|
3175
|
+
throw new Error(
|
|
3176
|
+
`Status "${status}" is not valid for ${contentType}. Allowed statuses: ${allowedStatuses.join(', ')}.`
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
function buildCurrentCmsFieldUpdate(
|
|
3182
|
+
fields: UpdateCurrentCmsFieldsInput['fields'],
|
|
3183
|
+
pageContext: CortexAiPageContext
|
|
3184
|
+
) {
|
|
3185
|
+
const allowedFieldNames = getAllowedFieldNames(pageContext.contentType);
|
|
3186
|
+
const updatePayload: Record<string, unknown> = {};
|
|
3187
|
+
|
|
3188
|
+
for (const [fieldName, rawValue] of Object.entries(fields)) {
|
|
3189
|
+
if (rawValue === undefined) {
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (!allowedFieldNames.has(fieldName)) {
|
|
3194
|
+
throw new Error(
|
|
3195
|
+
`Field "${fieldName}" cannot be updated for ${pageContext.contentType} content.`
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
if (fieldName === 'status') {
|
|
3200
|
+
assertValidStatusForContentType(pageContext.contentType, rawValue);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
|
|
3204
|
+
|
|
3205
|
+
updatePayload[fieldName] = normalizeCmsFieldValue(fieldName, rawValue);
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
return updatePayload;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
export async function executeUpdateCurrentCmsFields(
|
|
3212
|
+
input: UpdateCurrentCmsFieldsInput,
|
|
3213
|
+
context?: ToolExecutionContext
|
|
3214
|
+
) {
|
|
3215
|
+
const parsed = updateCurrentCmsFieldsInputSchema.parse(input);
|
|
3216
|
+
const supabase = getSupabase(context);
|
|
3217
|
+
const pageContext = getCurrentCmsContext(context);
|
|
3218
|
+
const entityId = getCmsEntityId(pageContext);
|
|
3219
|
+
const updatePayload = buildCurrentCmsFieldUpdate(parsed.fields, pageContext);
|
|
3220
|
+
const updatedFields = Object.keys(updatePayload);
|
|
3221
|
+
|
|
3222
|
+
if (updatedFields.length === 0) {
|
|
3223
|
+
throw new Error('update_current_cms_fields requires at least one supported field.');
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
const confirmation = getConfirmationPreview({
|
|
3227
|
+
action: 'UPDATE CMS FIELDS',
|
|
3228
|
+
context,
|
|
3229
|
+
payload: {
|
|
3230
|
+
contentType: pageContext.contentType,
|
|
3231
|
+
entityId,
|
|
3232
|
+
fields: updatePayload,
|
|
3233
|
+
tool: 'update_current_cms_fields',
|
|
3234
|
+
},
|
|
3235
|
+
preview: {
|
|
3236
|
+
contentType: pageContext.contentType,
|
|
3237
|
+
entityId,
|
|
3238
|
+
fields: updatedFields,
|
|
3239
|
+
slug: pageContext.slug,
|
|
3240
|
+
title: pageContext.title,
|
|
3241
|
+
},
|
|
3242
|
+
subject: `${pageContext.contentType} ${String(entityId)}`,
|
|
3243
|
+
});
|
|
3244
|
+
|
|
3245
|
+
if (confirmation) {
|
|
3246
|
+
return confirmation;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
const table =
|
|
3250
|
+
pageContext.contentType === 'page'
|
|
3251
|
+
? 'pages'
|
|
3252
|
+
: pageContext.contentType === 'post'
|
|
3253
|
+
? 'posts'
|
|
3254
|
+
: 'products';
|
|
3255
|
+
const { data: item, error } = await supabase
|
|
3256
|
+
.from(table)
|
|
3257
|
+
.update({
|
|
3258
|
+
...updatePayload,
|
|
3259
|
+
updated_at: new Date().toISOString(),
|
|
3260
|
+
})
|
|
3261
|
+
.eq('id', entityId)
|
|
3262
|
+
.select('id, language_id, slug, status, title')
|
|
3263
|
+
.single();
|
|
3264
|
+
|
|
3265
|
+
if (error || !item) {
|
|
3266
|
+
throw new Error(
|
|
3267
|
+
`Failed to update current ${pageContext.contentType}: ${serializeError(error)}`
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
revalidateCurrentCmsSurfaces(context, pageContext, item.slug);
|
|
3272
|
+
|
|
3273
|
+
return {
|
|
3274
|
+
contentType: pageContext.contentType,
|
|
3275
|
+
entityId,
|
|
3276
|
+
mutationExecuted: true,
|
|
3277
|
+
slug: item.slug,
|
|
3278
|
+
success: true,
|
|
3279
|
+
updatedFields,
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
export async function executeUpdateContentBlock(
|
|
3284
|
+
input: UpdateContentBlockInput,
|
|
3285
|
+
context?: ToolExecutionContext
|
|
3286
|
+
) {
|
|
3287
|
+
const parsed = updateContentBlockInputSchema.parse(input);
|
|
3288
|
+
const supabase = getSupabase(context);
|
|
3289
|
+
const pageContext = getCurrentCmsContext(context);
|
|
3290
|
+
const { data: block, error: blockError } = await supabase
|
|
3291
|
+
.from('blocks')
|
|
3292
|
+
.select('id, page_id, post_id, language_id, block_type, content, order')
|
|
3293
|
+
.eq('id', parsed.blockId)
|
|
3294
|
+
.single();
|
|
3295
|
+
|
|
3296
|
+
if (blockError || !block) {
|
|
3297
|
+
throw new Error(`Failed to read block ${parsed.blockId}: ${serializeError(blockError)}`);
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
assertBlockBelongsToCurrentContext(block, pageContext);
|
|
3301
|
+
|
|
3302
|
+
const existingBlockType = resolveExistingBlockType(block.block_type, `Block ${parsed.blockId}`);
|
|
3303
|
+
assertRequestedBlockTypeMatches(parsed.blockType, existingBlockType, `Block ${parsed.blockId}`);
|
|
3304
|
+
const existingContent = cloneJsonRecord(block.content, `Block ${parsed.blockId}`);
|
|
3305
|
+
const nextContent = buildNextTopLevelBlockContent(
|
|
3306
|
+
existingBlockType,
|
|
3307
|
+
existingContent,
|
|
3308
|
+
parsed.content
|
|
3309
|
+
);
|
|
3310
|
+
assertValidBlockContent(existingBlockType, nextContent, `Block ${parsed.blockId}`);
|
|
3311
|
+
|
|
3312
|
+
const confirmation = getConfirmationPreview({
|
|
3313
|
+
action: 'UPDATE CONTENT BLOCK',
|
|
3314
|
+
context,
|
|
3315
|
+
payload: {
|
|
3316
|
+
blockId: parsed.blockId,
|
|
3317
|
+
blockType: existingBlockType,
|
|
3318
|
+
content: nextContent,
|
|
3319
|
+
tool: 'update_content_block',
|
|
3320
|
+
},
|
|
3321
|
+
preview: {
|
|
3322
|
+
blockId: parsed.blockId,
|
|
3323
|
+
blockType: existingBlockType,
|
|
3324
|
+
contentType: pageContext.contentType,
|
|
3325
|
+
entityId: getCmsEntityId(pageContext),
|
|
3326
|
+
},
|
|
3327
|
+
subject: `${existingBlockType} block ${parsed.blockId}`,
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
if (confirmation) {
|
|
3331
|
+
return confirmation;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
const { data: updatedBlock, error: updateError } = await supabase
|
|
3335
|
+
.from('blocks')
|
|
3336
|
+
.update({
|
|
3337
|
+
content: nextContent,
|
|
3338
|
+
updated_at: new Date().toISOString(),
|
|
3339
|
+
})
|
|
3340
|
+
.eq('id', parsed.blockId)
|
|
3341
|
+
.select('id, block_type, order')
|
|
3342
|
+
.single();
|
|
3343
|
+
|
|
3344
|
+
if (updateError || !updatedBlock) {
|
|
3345
|
+
throw new Error(`Failed to update block ${parsed.blockId}: ${serializeError(updateError)}`);
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
revalidateCurrentCmsSurfaces(context, pageContext);
|
|
3349
|
+
|
|
3350
|
+
return {
|
|
3351
|
+
blockId: updatedBlock.id,
|
|
3352
|
+
blockType: updatedBlock.block_type,
|
|
3353
|
+
contentUpdated: true,
|
|
3354
|
+
mutationExecuted: true,
|
|
3355
|
+
success: true,
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
export async function executeInsertContentBlock(
|
|
3360
|
+
input: InsertContentBlockInput,
|
|
3361
|
+
context?: ToolExecutionContext
|
|
3362
|
+
) {
|
|
3363
|
+
const parsed = insertContentBlockInputSchema.parse(input);
|
|
3364
|
+
const supabase = getSupabase(context);
|
|
3365
|
+
const target = await resolveCmsTarget(parsed, context);
|
|
3366
|
+
|
|
3367
|
+
if (target.contentType === 'product') {
|
|
3368
|
+
throw new Error('Products do not have page/post content blocks in this editor context.');
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
const itemId = Number(target.item.id);
|
|
3372
|
+
const parentColumn = target.contentType === 'page' ? 'page_id' : 'post_id';
|
|
3373
|
+
const loadBlocks = async () => {
|
|
3374
|
+
const { data, error } = await supabase
|
|
3375
|
+
.from('blocks')
|
|
3376
|
+
.select('id, page_id, post_id, language_id, block_type, content, order')
|
|
3377
|
+
.eq(parentColumn, itemId);
|
|
3378
|
+
|
|
3379
|
+
if (error) {
|
|
3380
|
+
throw new Error(`Failed to read ${target.contentType} blocks: ${serializeError(error)}`);
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
return (Array.isArray(data) ? data : []).sort(
|
|
3384
|
+
(a: any, b: any) => Number(a.order) - Number(b.order)
|
|
3385
|
+
);
|
|
3386
|
+
};
|
|
3387
|
+
const resolveOrder = (blocks: any[]) => {
|
|
3388
|
+
if (parsed.position === 'start') {
|
|
3389
|
+
return 0;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
if (parsed.position === 'end') {
|
|
3393
|
+
const orders = blocks.map((block: any) => Number(block.order)).filter(Number.isFinite);
|
|
3394
|
+
return orders.length > 0 ? Math.max(...orders) + 1 : 0;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
const anchorBlock = parsed.anchorBlockId
|
|
3398
|
+
? blocks.find((block: any) => Number(block.id) === parsed.anchorBlockId)
|
|
3399
|
+
: parsed.anchorBlockType
|
|
3400
|
+
? blocks.find((block: any) => block.block_type === parsed.anchorBlockType)
|
|
3401
|
+
: null;
|
|
3402
|
+
|
|
3403
|
+
if (!anchorBlock) {
|
|
3404
|
+
throw new Error(
|
|
3405
|
+
parsed.anchorBlockType
|
|
3406
|
+
? `Could not find a ${parsed.anchorBlockType} block to insert ${parsed.position}.`
|
|
3407
|
+
: `Could not find block ${parsed.anchorBlockId} to insert ${parsed.position}.`
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
const anchorOrder = Number(anchorBlock.order);
|
|
3412
|
+
|
|
3413
|
+
return parsed.position === 'before' ? anchorOrder : anchorOrder + 1;
|
|
3414
|
+
};
|
|
3415
|
+
const normalizedBlock = normalizeCreateBlock(parsed.block, 0);
|
|
3416
|
+
const blocks = await loadBlocks();
|
|
3417
|
+
const newOrder = resolveOrder(blocks);
|
|
3418
|
+
const targetContext: CortexAiPageContext = {
|
|
3419
|
+
contentType: target.contentType,
|
|
3420
|
+
entityId: target.item.id,
|
|
3421
|
+
languageId: target.item.language_id,
|
|
3422
|
+
slug: target.item.slug,
|
|
3423
|
+
title: target.item.title,
|
|
3424
|
+
translationGroupId: target.item.translation_group_id,
|
|
3425
|
+
};
|
|
3426
|
+
const confirmation = getConfirmationPreview({
|
|
3427
|
+
action: 'INSERT CONTENT BLOCK',
|
|
3428
|
+
context,
|
|
3429
|
+
payload: {
|
|
3430
|
+
block: normalizedBlock,
|
|
3431
|
+
order: newOrder,
|
|
3432
|
+
target: {
|
|
3433
|
+
contentType: target.contentType,
|
|
3434
|
+
id: target.item.id,
|
|
3435
|
+
slug: target.item.slug,
|
|
3436
|
+
},
|
|
3437
|
+
tool: 'insert_content_block',
|
|
3438
|
+
},
|
|
3439
|
+
preview: {
|
|
3440
|
+
anchorBlockId: parsed.anchorBlockId,
|
|
3441
|
+
anchorBlockType: parsed.anchorBlockType,
|
|
3442
|
+
blockType: normalizedBlock.block_type,
|
|
3443
|
+
contentType: target.contentType,
|
|
3444
|
+
entityId: target.item.id,
|
|
3445
|
+
position: parsed.position,
|
|
3446
|
+
slug: target.item.slug,
|
|
3447
|
+
summary: `Insert ${normalizedBlock.block_type} block ${parsed.position} ${parsed.anchorBlockType ? `the first ${parsed.anchorBlockType} block` : parsed.anchorBlockId ? `block ${parsed.anchorBlockId}` : 'the content'} on ${target.contentType} "${target.item.title || target.item.slug}".`,
|
|
3448
|
+
title: target.item.title,
|
|
3449
|
+
},
|
|
3450
|
+
subject: `${normalizedBlock.block_type} block on ${target.contentType} ${target.item.id}`,
|
|
3451
|
+
});
|
|
3452
|
+
|
|
3453
|
+
if (confirmation) {
|
|
3454
|
+
return confirmation;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
const latestBlocks = await loadBlocks();
|
|
3458
|
+
const latestOrder = resolveOrder(latestBlocks);
|
|
3459
|
+
const blocksToShift = latestBlocks
|
|
3460
|
+
.filter((block: any) => Number(block.order) >= latestOrder)
|
|
3461
|
+
.sort((a: any, b: any) => Number(b.order) - Number(a.order));
|
|
3462
|
+
|
|
3463
|
+
for (const block of blocksToShift) {
|
|
3464
|
+
const { error } = await supabase
|
|
3465
|
+
.from('blocks')
|
|
3466
|
+
.update({
|
|
3467
|
+
order: Number(block.order) + 1,
|
|
3468
|
+
updated_at: new Date().toISOString(),
|
|
3469
|
+
})
|
|
3470
|
+
.eq('id', block.id);
|
|
3471
|
+
|
|
3472
|
+
if (error) {
|
|
3473
|
+
throw new Error(`Failed to shift block ${block.id}: ${serializeError(error)}`);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
const { data: insertedBlock, error: insertError } = await supabase
|
|
3478
|
+
.from('blocks')
|
|
3479
|
+
.insert({
|
|
3480
|
+
block_type: normalizedBlock.block_type,
|
|
3481
|
+
content: normalizedBlock.content,
|
|
3482
|
+
language_id: target.item.language_id,
|
|
3483
|
+
order: latestOrder,
|
|
3484
|
+
page_id: target.contentType === 'page' ? itemId : null,
|
|
3485
|
+
post_id: target.contentType === 'post' ? itemId : null,
|
|
3486
|
+
})
|
|
3487
|
+
.select('id, block_type, order')
|
|
3488
|
+
.single();
|
|
3489
|
+
|
|
3490
|
+
if (insertError || !insertedBlock) {
|
|
3491
|
+
throw new Error(`Failed to insert content block: ${serializeError(insertError)}`);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
revalidateCurrentCmsSurfaces(context, targetContext);
|
|
3495
|
+
|
|
3496
|
+
return {
|
|
3497
|
+
blockId: insertedBlock.id,
|
|
3498
|
+
blockType: insertedBlock.block_type,
|
|
3499
|
+
contentType: target.contentType,
|
|
3500
|
+
entityId: target.item.id,
|
|
3501
|
+
mutationExecuted: true,
|
|
3502
|
+
order: insertedBlock.order,
|
|
3503
|
+
success: true,
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
export async function executeUpdateSectionColumnBlock(
|
|
3508
|
+
input: UpdateSectionColumnBlockInput,
|
|
3509
|
+
context?: ToolExecutionContext
|
|
3510
|
+
) {
|
|
3511
|
+
const parsed = updateSectionColumnBlockInputSchema.parse(input);
|
|
3512
|
+
const supabase = getSupabase(context);
|
|
3513
|
+
const pageContext = getCurrentCmsContext(context);
|
|
3514
|
+
const { data: parentBlock, error: blockError } = await supabase
|
|
3515
|
+
.from('blocks')
|
|
3516
|
+
.select('id, page_id, post_id, language_id, block_type, content, order')
|
|
3517
|
+
.eq('id', parsed.parentBlockId)
|
|
3518
|
+
.single();
|
|
3519
|
+
|
|
3520
|
+
if (blockError || !parentBlock) {
|
|
3521
|
+
throw new Error(
|
|
3522
|
+
`Failed to read parent block ${parsed.parentBlockId}: ${serializeError(blockError)}`
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
assertBlockBelongsToCurrentContext(parentBlock, pageContext);
|
|
3527
|
+
|
|
3528
|
+
const parentBlockType = resolveExistingBlockType(
|
|
3529
|
+
parentBlock.block_type,
|
|
3530
|
+
`Parent block ${parsed.parentBlockId}`
|
|
3531
|
+
);
|
|
3532
|
+
|
|
3533
|
+
if (parentBlockType !== 'section') {
|
|
3534
|
+
throw new Error(
|
|
3535
|
+
`Parent block ${parsed.parentBlockId} must be a section block, not "${parentBlockType}".`
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
const parentContent = cloneJsonRecord(
|
|
3540
|
+
parentBlock.content,
|
|
3541
|
+
`Parent block ${parsed.parentBlockId}`
|
|
3542
|
+
) as SectionBlockContent;
|
|
3543
|
+
assertValidBlockContent(parentBlockType, parentContent, `Parent block ${parsed.parentBlockId}`);
|
|
3544
|
+
|
|
3545
|
+
const targetColumn = parentContent.column_blocks?.[parsed.columnIndex];
|
|
3546
|
+
const targetNestedBlock = targetColumn?.[parsed.blockIndex];
|
|
3547
|
+
|
|
3548
|
+
if (!targetNestedBlock) {
|
|
3549
|
+
throw new Error(
|
|
3550
|
+
`Nested block was not found at column ${parsed.columnIndex}, index ${parsed.blockIndex}.`
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
const nestedBlockType = resolveExistingBlockType(
|
|
3555
|
+
targetNestedBlock.block_type,
|
|
3556
|
+
`Nested block ${parsed.columnIndex}:${parsed.blockIndex}`
|
|
3557
|
+
);
|
|
3558
|
+
assertRequestedBlockTypeMatches(
|
|
3559
|
+
parsed.blockType,
|
|
3560
|
+
nestedBlockType,
|
|
3561
|
+
`Nested block ${parsed.columnIndex}:${parsed.blockIndex}`
|
|
3562
|
+
);
|
|
3563
|
+
assertValidBlockContent(
|
|
3564
|
+
nestedBlockType,
|
|
3565
|
+
parsed.content,
|
|
3566
|
+
`Nested block ${parsed.columnIndex}:${parsed.blockIndex}`
|
|
3567
|
+
);
|
|
3568
|
+
|
|
3569
|
+
const nextColumnBlocks = parentContent.column_blocks.map((column, columnIndex) =>
|
|
3570
|
+
columnIndex === parsed.columnIndex
|
|
3571
|
+
? column.map((nestedBlock, blockIndex) =>
|
|
3572
|
+
blockIndex === parsed.blockIndex
|
|
3573
|
+
? {
|
|
3574
|
+
...nestedBlock,
|
|
3575
|
+
content: parsed.content,
|
|
3576
|
+
}
|
|
3577
|
+
: nestedBlock
|
|
3578
|
+
)
|
|
3579
|
+
: column
|
|
3580
|
+
);
|
|
3581
|
+
const nextParentContent: SectionBlockContent = {
|
|
3582
|
+
...parentContent,
|
|
3583
|
+
column_blocks: nextColumnBlocks,
|
|
3584
|
+
};
|
|
3585
|
+
assertValidBlockContent(
|
|
3586
|
+
parentBlockType,
|
|
3587
|
+
nextParentContent,
|
|
3588
|
+
`Updated parent block ${parsed.parentBlockId}`
|
|
3589
|
+
);
|
|
3590
|
+
|
|
3591
|
+
const confirmation = getConfirmationPreview({
|
|
3592
|
+
action: 'UPDATE NESTED BLOCK',
|
|
3593
|
+
context,
|
|
3594
|
+
payload: {
|
|
3595
|
+
blockIndex: parsed.blockIndex,
|
|
3596
|
+
columnIndex: parsed.columnIndex,
|
|
3597
|
+
content: parsed.content,
|
|
3598
|
+
nestedBlockType,
|
|
3599
|
+
parentBlockId: parsed.parentBlockId,
|
|
3600
|
+
tool: 'update_section_column_block',
|
|
3601
|
+
},
|
|
3602
|
+
preview: {
|
|
3603
|
+
blockIndex: parsed.blockIndex,
|
|
3604
|
+
columnIndex: parsed.columnIndex,
|
|
3605
|
+
nestedBlockType,
|
|
3606
|
+
parentBlockId: parsed.parentBlockId,
|
|
3607
|
+
parentBlockType,
|
|
3608
|
+
},
|
|
3609
|
+
subject: `${nestedBlockType} nested block ${parsed.columnIndex}:${parsed.blockIndex}`,
|
|
3610
|
+
});
|
|
3611
|
+
|
|
3612
|
+
if (confirmation) {
|
|
3613
|
+
return confirmation;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
const { data: updatedParentBlock, error: updateError } = await supabase
|
|
3617
|
+
.from('blocks')
|
|
3618
|
+
.update({
|
|
3619
|
+
content: nextParentContent,
|
|
3620
|
+
updated_at: new Date().toISOString(),
|
|
3621
|
+
})
|
|
3622
|
+
.eq('id', parsed.parentBlockId)
|
|
3623
|
+
.select('id, block_type')
|
|
3624
|
+
.single();
|
|
3625
|
+
|
|
3626
|
+
if (updateError || !updatedParentBlock) {
|
|
3627
|
+
throw new Error(
|
|
3628
|
+
`Failed to update parent block ${parsed.parentBlockId}: ${serializeError(updateError)}`
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
revalidateCurrentCmsSurfaces(context, pageContext);
|
|
3633
|
+
|
|
3634
|
+
return {
|
|
3635
|
+
blockIndex: parsed.blockIndex,
|
|
3636
|
+
columnIndex: parsed.columnIndex,
|
|
3637
|
+
mutationExecuted: true,
|
|
3638
|
+
nestedBlockType,
|
|
3639
|
+
parentBlockId: updatedParentBlock.id,
|
|
3640
|
+
parentBlockType: updatedParentBlock.block_type,
|
|
3641
|
+
success: true,
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
export async function executeCreateCmsPage(input: CreateCmsPageInput, context?: ToolExecutionContext) {
|
|
3646
|
+
const parsed = createCmsPageInputSchema.parse(input);
|
|
3647
|
+
const supabase = getSupabase(context);
|
|
3648
|
+
const actorUserId = getActorUserId(context);
|
|
3649
|
+
const language = await getDefaultLanguageRecord(supabase, parsed.languageCode);
|
|
3650
|
+
const slug = slugify(parsed.slug || parsed.title);
|
|
3651
|
+
const blocks = normalizeCreateBlocks(parsed.blocks, parsed.contactEmail, parsed.title);
|
|
3652
|
+
const duplicate = await assertUniqueSlug({
|
|
3653
|
+
contentType: 'page',
|
|
3654
|
+
languageId: language.id,
|
|
3655
|
+
slug,
|
|
3656
|
+
supabase,
|
|
3657
|
+
});
|
|
3658
|
+
|
|
3659
|
+
if (duplicate) {
|
|
3660
|
+
return duplicate;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
const translationGroup = await resolveCreateTranslationGroup({
|
|
3664
|
+
contentType: 'page',
|
|
3665
|
+
languageCode: language.code,
|
|
3666
|
+
languageId: language.id,
|
|
3667
|
+
suppliedTranslationGroupId: parsed.translationGroupId,
|
|
3668
|
+
supabase,
|
|
3669
|
+
});
|
|
3670
|
+
|
|
3671
|
+
if (translationGroup.result) {
|
|
3672
|
+
return translationGroup.result;
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
const payload = {
|
|
3676
|
+
blocks,
|
|
3677
|
+
item: {
|
|
3678
|
+
feature_image_id: parsed.feature_image_id ?? null,
|
|
3679
|
+
language_id: language.id,
|
|
3680
|
+
meta_description: parsed.meta_description ?? null,
|
|
3681
|
+
meta_title: parsed.meta_title ?? null,
|
|
3682
|
+
slug,
|
|
3683
|
+
status: parsed.status,
|
|
3684
|
+
title: parsed.title,
|
|
3685
|
+
translation_group_id: translationGroup.translationGroupId,
|
|
3686
|
+
},
|
|
3687
|
+
tool: 'create_cms_page',
|
|
3688
|
+
};
|
|
3689
|
+
const confirmation = getConfirmationPreview({
|
|
3690
|
+
action: 'CREATE PAGE',
|
|
3691
|
+
context,
|
|
3692
|
+
payload,
|
|
3693
|
+
preview: {
|
|
3694
|
+
blockCount: blocks.length,
|
|
3695
|
+
languageCode: language.code,
|
|
3696
|
+
slug,
|
|
3697
|
+
status: parsed.status,
|
|
3698
|
+
title: parsed.title,
|
|
3699
|
+
translationGroupId: translationGroup.translationGroupId,
|
|
3700
|
+
},
|
|
3701
|
+
subject: slug,
|
|
3702
|
+
});
|
|
3703
|
+
|
|
3704
|
+
if (confirmation) {
|
|
3705
|
+
return confirmation;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
const translationGroupId = translationGroup.translationGroupId || createId();
|
|
3709
|
+
const { data: page, error } = await supabase
|
|
3710
|
+
.from('pages')
|
|
3711
|
+
.insert({
|
|
3712
|
+
...payload.item,
|
|
3713
|
+
author_id: actorUserId,
|
|
3714
|
+
translation_group_id: translationGroupId,
|
|
3715
|
+
})
|
|
3716
|
+
.select('id, language_id, slug, status, title, translation_group_id')
|
|
3717
|
+
.single();
|
|
3718
|
+
|
|
3719
|
+
if (error || !page?.id) {
|
|
3720
|
+
throw new Error(`Failed to create page: ${serializeError(error)}`);
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
try {
|
|
3724
|
+
await insertContentBlocks({
|
|
3725
|
+
blocks,
|
|
3726
|
+
contentType: 'page',
|
|
3727
|
+
itemId: Number(page.id),
|
|
3728
|
+
languageId: language.id,
|
|
3729
|
+
supabase,
|
|
3730
|
+
});
|
|
3731
|
+
} catch (error) {
|
|
3732
|
+
await rollbackCreatedCmsItem({ contentType: 'page', itemId: Number(page.id), supabase });
|
|
3733
|
+
throw error;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
revalidateCurrentCmsSurfaces(
|
|
3737
|
+
context,
|
|
3738
|
+
{ contentType: 'page', entityId: Number(page.id), languageId: language.id, slug, title: parsed.title },
|
|
3739
|
+
slug
|
|
3740
|
+
);
|
|
3741
|
+
context?.revalidatePath?.('/cms/pages');
|
|
3742
|
+
|
|
3743
|
+
return {
|
|
3744
|
+
blockCount: blocks.length,
|
|
3745
|
+
contentType: 'page',
|
|
3746
|
+
editPath: getCreateEditPath('page', page.id),
|
|
3747
|
+
entityId: page.id,
|
|
3748
|
+
mutationExecuted: true,
|
|
3749
|
+
slug,
|
|
3750
|
+
success: true,
|
|
3751
|
+
title: parsed.title,
|
|
3752
|
+
translationGroupId: page.translation_group_id,
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
export async function executeCreateCmsPost(input: CreateCmsPostInput, context?: ToolExecutionContext) {
|
|
3757
|
+
const parsed = createCmsPostInputSchema.parse(input);
|
|
3758
|
+
const supabase = getSupabase(context);
|
|
3759
|
+
const actorUserId = getActorUserId(context);
|
|
3760
|
+
const language = await getDefaultLanguageRecord(supabase, parsed.languageCode);
|
|
3761
|
+
const slug = slugify(parsed.slug || parsed.title);
|
|
3762
|
+
const blocks = normalizeCreateBlocks(parsed.blocks);
|
|
3763
|
+
const duplicate = await assertUniqueSlug({
|
|
3764
|
+
contentType: 'post',
|
|
3765
|
+
languageId: language.id,
|
|
3766
|
+
slug,
|
|
3767
|
+
supabase,
|
|
3768
|
+
});
|
|
3769
|
+
|
|
3770
|
+
if (duplicate) {
|
|
3771
|
+
return duplicate;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
const translationGroup = await resolveCreateTranslationGroup({
|
|
3775
|
+
contentType: 'post',
|
|
3776
|
+
languageCode: language.code,
|
|
3777
|
+
languageId: language.id,
|
|
3778
|
+
suppliedTranslationGroupId: parsed.translationGroupId,
|
|
3779
|
+
supabase,
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3782
|
+
if (translationGroup.result) {
|
|
3783
|
+
return translationGroup.result;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
const publishedAt =
|
|
3787
|
+
parsed.published_at && !Number.isNaN(new Date(parsed.published_at).getTime())
|
|
3788
|
+
? new Date(parsed.published_at).toISOString()
|
|
3789
|
+
: parsed.published_at ?? null;
|
|
3790
|
+
const payload = {
|
|
3791
|
+
blocks,
|
|
3792
|
+
item: {
|
|
3793
|
+
excerpt: parsed.excerpt ?? null,
|
|
3794
|
+
feature_image_id: parsed.feature_image_id ?? null,
|
|
3795
|
+
label: parsed.label ?? null,
|
|
3796
|
+
language_id: language.id,
|
|
3797
|
+
meta_description: parsed.meta_description ?? null,
|
|
3798
|
+
meta_title: parsed.meta_title ?? null,
|
|
3799
|
+
published_at: publishedAt,
|
|
3800
|
+
slug,
|
|
3801
|
+
status: parsed.status,
|
|
3802
|
+
subtitle: parsed.subtitle ?? null,
|
|
3803
|
+
title: parsed.title,
|
|
3804
|
+
translation_group_id: translationGroup.translationGroupId,
|
|
3805
|
+
},
|
|
3806
|
+
tool: 'create_cms_post',
|
|
3807
|
+
};
|
|
3808
|
+
const confirmation = getConfirmationPreview({
|
|
3809
|
+
action: 'CREATE POST',
|
|
3810
|
+
context,
|
|
3811
|
+
payload,
|
|
3812
|
+
preview: {
|
|
3813
|
+
blockCount: blocks.length,
|
|
3814
|
+
languageCode: language.code,
|
|
3815
|
+
slug,
|
|
3816
|
+
status: parsed.status,
|
|
3817
|
+
title: parsed.title,
|
|
3818
|
+
translationGroupId: translationGroup.translationGroupId,
|
|
3819
|
+
},
|
|
3820
|
+
subject: slug,
|
|
3821
|
+
});
|
|
3822
|
+
|
|
3823
|
+
if (confirmation) {
|
|
3824
|
+
return confirmation;
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
const translationGroupId = translationGroup.translationGroupId || createId();
|
|
3828
|
+
const { data: post, error } = await supabase
|
|
3829
|
+
.from('posts')
|
|
3830
|
+
.insert({
|
|
3831
|
+
...payload.item,
|
|
3832
|
+
author_id: actorUserId,
|
|
3833
|
+
translation_group_id: translationGroupId,
|
|
3834
|
+
})
|
|
3835
|
+
.select('id, language_id, slug, status, title, translation_group_id')
|
|
3836
|
+
.single();
|
|
3837
|
+
|
|
3838
|
+
if (error || !post?.id) {
|
|
3839
|
+
throw new Error(`Failed to create post: ${serializeError(error)}`);
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
try {
|
|
3843
|
+
await insertContentBlocks({
|
|
3844
|
+
blocks,
|
|
3845
|
+
contentType: 'post',
|
|
3846
|
+
itemId: Number(post.id),
|
|
3847
|
+
languageId: language.id,
|
|
3848
|
+
supabase,
|
|
3849
|
+
});
|
|
3850
|
+
} catch (error) {
|
|
3851
|
+
await rollbackCreatedCmsItem({ contentType: 'post', itemId: Number(post.id), supabase });
|
|
3852
|
+
throw error;
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
revalidateCurrentCmsSurfaces(
|
|
3856
|
+
context,
|
|
3857
|
+
{ contentType: 'post', entityId: Number(post.id), languageId: language.id, slug, title: parsed.title },
|
|
3858
|
+
slug
|
|
3859
|
+
);
|
|
3860
|
+
context?.revalidatePath?.('/cms/posts');
|
|
3861
|
+
context?.revalidatePath?.('/articles');
|
|
3862
|
+
|
|
3863
|
+
return {
|
|
3864
|
+
blockCount: blocks.length,
|
|
3865
|
+
contentType: 'post',
|
|
3866
|
+
editPath: getCreateEditPath('post', post.id),
|
|
3867
|
+
entityId: post.id,
|
|
3868
|
+
mutationExecuted: true,
|
|
3869
|
+
slug,
|
|
3870
|
+
success: true,
|
|
3871
|
+
title: parsed.title,
|
|
3872
|
+
translationGroupId: post.translation_group_id,
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
function buildGeneratedSku(title: string, slug: string) {
|
|
3877
|
+
return (slug || slugify(title) || 'product')
|
|
3878
|
+
.replace(/-/g, '')
|
|
3879
|
+
.slice(0, 24)
|
|
3880
|
+
.toUpperCase();
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
function validateProductDescriptionJson(value: unknown) {
|
|
3884
|
+
if (value === undefined) {
|
|
3885
|
+
return undefined;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
const validation = getEditorBlockDocumentSchema().safeParse(value);
|
|
3889
|
+
|
|
3890
|
+
if (!validation.success) {
|
|
3891
|
+
throw new Error(
|
|
3892
|
+
`Product description_json failed editor document validation: ${validation.error.issues
|
|
3893
|
+
.map((issue) => issue.message)
|
|
3894
|
+
.join('; ')}`
|
|
3895
|
+
);
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
return validation.data;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
export async function executeCreateCmsProduct(input: CreateCmsProductInput, context?: ToolExecutionContext) {
|
|
3902
|
+
const parsed = createCmsProductInputSchema.parse(input);
|
|
3903
|
+
const supabase = getSupabase(context);
|
|
3904
|
+
const language = await getDefaultLanguageRecord(supabase, parsed.languageCode);
|
|
3905
|
+
const slug = slugify(parsed.slug || parsed.title);
|
|
3906
|
+
const duplicate = await assertUniqueSlug({
|
|
3907
|
+
contentType: 'product',
|
|
3908
|
+
languageId: language.id,
|
|
3909
|
+
slug,
|
|
3910
|
+
supabase,
|
|
3911
|
+
});
|
|
3912
|
+
|
|
3913
|
+
if (duplicate) {
|
|
3914
|
+
return duplicate;
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
const translationGroup = await resolveCreateTranslationGroup({
|
|
3918
|
+
contentType: 'product',
|
|
3919
|
+
languageCode: language.code,
|
|
3920
|
+
languageId: language.id,
|
|
3921
|
+
suppliedTranslationGroupId: parsed.translationGroupId,
|
|
3922
|
+
supabase,
|
|
3923
|
+
});
|
|
3924
|
+
|
|
3925
|
+
if (translationGroup.result) {
|
|
3926
|
+
return translationGroup.result;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
const { createProduct: createEcommerceProduct, productSchema } = await getEcommerceProductModule();
|
|
3930
|
+
const isFreemiusProduct =
|
|
3931
|
+
parsed.product_type === 'digital' && parsed.payment_provider === 'freemius';
|
|
3932
|
+
const trialPeriodDays = isFreemiusProduct ? parsed.trial_period_days : 0;
|
|
3933
|
+
const productPayload = productSchema.parse({
|
|
3934
|
+
description_json: validateProductDescriptionJson(parsed.description_json),
|
|
3935
|
+
freemius_plan_id: parsed.freemius_plan_id || '',
|
|
3936
|
+
freemius_product_id: parsed.freemius_product_id || '',
|
|
3937
|
+
is_taxable: parsed.is_taxable,
|
|
3938
|
+
language_id: language.id,
|
|
3939
|
+
meta_description: parsed.meta_description ?? '',
|
|
3940
|
+
meta_title: parsed.meta_title ?? '',
|
|
3941
|
+
payment_provider: parsed.payment_provider,
|
|
3942
|
+
price: parsed.price,
|
|
3943
|
+
prices: parsed.prices || {},
|
|
3944
|
+
product_media: [],
|
|
3945
|
+
product_type: parsed.product_type,
|
|
3946
|
+
sale_price: parsed.sale_price ?? null,
|
|
3947
|
+
sale_prices: parsed.sale_prices || {},
|
|
3948
|
+
short_description: parsed.short_description ?? '',
|
|
3949
|
+
sku: parsed.sku || buildGeneratedSku(parsed.title, slug),
|
|
3950
|
+
slug,
|
|
3951
|
+
status: parsed.status,
|
|
3952
|
+
stock: parsed.stock,
|
|
3953
|
+
title: parsed.title,
|
|
3954
|
+
trial_period_days: trialPeriodDays,
|
|
3955
|
+
trial_requires_payment_method:
|
|
3956
|
+
trialPeriodDays > 0 ? parsed.trial_requires_payment_method : false,
|
|
3957
|
+
translation_group_id: translationGroup.translationGroupId,
|
|
3958
|
+
upc: parsed.upc ?? '',
|
|
3959
|
+
variation_attributes: [],
|
|
3960
|
+
variants: [],
|
|
3961
|
+
});
|
|
3962
|
+
const confirmation = getConfirmationPreview({
|
|
3963
|
+
action: 'CREATE PRODUCT',
|
|
3964
|
+
context,
|
|
3965
|
+
payload: { item: productPayload, tool: 'create_cms_product' },
|
|
3966
|
+
preview: {
|
|
3967
|
+
languageCode: language.code,
|
|
3968
|
+
price: productPayload.price,
|
|
3969
|
+
sku: productPayload.sku,
|
|
3970
|
+
slug,
|
|
3971
|
+
status: productPayload.status,
|
|
3972
|
+
stock: productPayload.stock,
|
|
3973
|
+
title: productPayload.title,
|
|
3974
|
+
translationGroupId: translationGroup.translationGroupId,
|
|
3975
|
+
},
|
|
3976
|
+
subject: slug,
|
|
3977
|
+
});
|
|
3978
|
+
|
|
3979
|
+
if (confirmation) {
|
|
3980
|
+
return confirmation;
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
const product = await createEcommerceProduct(supabase as any, productPayload);
|
|
3984
|
+
|
|
3985
|
+
if (!product?.id) {
|
|
3986
|
+
throw new Error('Failed to create product.');
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
revalidateCurrentCmsSurfaces(
|
|
3990
|
+
context,
|
|
3991
|
+
{ contentType: 'product', entityId: product.id, languageId: language.id, slug, title: parsed.title },
|
|
3992
|
+
slug
|
|
3993
|
+
);
|
|
3994
|
+
context?.revalidatePath?.('/cms/products');
|
|
3995
|
+
|
|
3996
|
+
return {
|
|
3997
|
+
contentType: 'product',
|
|
3998
|
+
editPath: getCreateEditPath('product', product.id),
|
|
3999
|
+
entityId: product.id,
|
|
4000
|
+
mutationExecuted: true,
|
|
4001
|
+
slug,
|
|
4002
|
+
success: true,
|
|
4003
|
+
title: parsed.title,
|
|
4004
|
+
translationGroupId: product.translation_group_id,
|
|
4005
|
+
};
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
function normalizeFieldName(value: string) {
|
|
4009
|
+
return value.trim().replace(/[\s-]+/g, '_').toLowerCase();
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
function normalizeStatusValue(contentType: CmsContentType, value: unknown) {
|
|
4013
|
+
const normalized = typeof value === 'string' ? normalizeFieldName(value) : value;
|
|
4014
|
+
|
|
4015
|
+
if (contentType === 'product') {
|
|
4016
|
+
if (normalized === 'public' || normalized === 'publish' || normalized === 'published') {
|
|
4017
|
+
return 'active';
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
return normalized;
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
if (normalized === 'public' || normalized === 'active' || normalized === 'publish') {
|
|
4024
|
+
return 'published';
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
return normalized;
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
function isUnsupportedDatedSpecial(input: UpdateCmsItemFieldInput) {
|
|
4031
|
+
const field = normalizeFieldName(input.field);
|
|
4032
|
+
|
|
4033
|
+
return Boolean(
|
|
4034
|
+
input.startsAt ||
|
|
4035
|
+
input.endsAt ||
|
|
4036
|
+
field.includes('start') ||
|
|
4037
|
+
field.includes('end') ||
|
|
4038
|
+
field.includes('schedule') ||
|
|
4039
|
+
field.includes('special_date')
|
|
4040
|
+
);
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
async function buildProductFormValuesFromRow(
|
|
4044
|
+
product: any,
|
|
4045
|
+
supabase: SupabaseLike,
|
|
4046
|
+
overrides: Record<string, unknown>
|
|
4047
|
+
) {
|
|
4048
|
+
const defaultCurrencyCode = await getDefaultCurrencyCode(supabase);
|
|
4049
|
+
const { productSchema } = await getEcommerceProductModule();
|
|
4050
|
+
|
|
4051
|
+
return productSchema.parse({
|
|
4052
|
+
description_json:
|
|
4053
|
+
overrides.description_json !== undefined
|
|
4054
|
+
? validateProductDescriptionJson(overrides.description_json)
|
|
4055
|
+
: product.description_json || undefined,
|
|
4056
|
+
freemius_plan_id: product.freemius_plan_id || '',
|
|
4057
|
+
freemius_product_id: product.freemius_product_id || '',
|
|
4058
|
+
is_taxable: overrides.is_taxable ?? product.is_taxable ?? true,
|
|
4059
|
+
language_id: overrides.language_id ?? product.language_id,
|
|
4060
|
+
meta_description: overrides.meta_description ?? product.meta_description ?? '',
|
|
4061
|
+
meta_title: overrides.meta_title ?? product.meta_title ?? '',
|
|
4062
|
+
payment_provider: overrides.payment_provider ?? product.payment_provider ?? 'stripe',
|
|
4063
|
+
price:
|
|
4064
|
+
overrides.price !== undefined
|
|
4065
|
+
? overrides.price
|
|
4066
|
+
: maybeCentsToMajor(product.price, defaultCurrencyCode),
|
|
4067
|
+
prices: overrides.prices ?? mapMinorPriceMapToMajor(product.prices, defaultCurrencyCode),
|
|
4068
|
+
product_media: undefined,
|
|
4069
|
+
product_type: overrides.product_type ?? product.product_type ?? 'physical',
|
|
4070
|
+
sale_price:
|
|
4071
|
+
overrides.sale_price !== undefined
|
|
4072
|
+
? overrides.sale_price
|
|
4073
|
+
: product.sale_price === null || product.sale_price === undefined
|
|
4074
|
+
? null
|
|
4075
|
+
: minorUnitAmountToMajor(Number(product.sale_price), defaultCurrencyCode),
|
|
4076
|
+
sale_prices: overrides.sale_prices ?? mapMinorPriceMapToMajor(product.sale_prices, defaultCurrencyCode),
|
|
4077
|
+
short_description: overrides.short_description ?? product.short_description ?? '',
|
|
4078
|
+
sku: overrides.sku ?? product.sku,
|
|
4079
|
+
slug: overrides.slug ?? product.slug,
|
|
4080
|
+
status: overrides.status ?? product.status ?? 'draft',
|
|
4081
|
+
stock: overrides.stock ?? product.stock ?? 0,
|
|
4082
|
+
title: overrides.title ?? product.title,
|
|
4083
|
+
trial_period_days: overrides.trial_period_days ?? product.trial_period_days ?? 0,
|
|
4084
|
+
trial_requires_payment_method:
|
|
4085
|
+
overrides.trial_requires_payment_method ??
|
|
4086
|
+
product.trial_requires_payment_method ??
|
|
4087
|
+
false,
|
|
4088
|
+
upc: overrides.upc ?? product.upc ?? '',
|
|
4089
|
+
variation_attributes: [],
|
|
4090
|
+
variants: [],
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
function buildSingleFieldUpdatePayload(
|
|
4095
|
+
input: UpdateCmsItemFieldInput,
|
|
4096
|
+
target: { contentType: CmsContentType; item: any }
|
|
4097
|
+
) {
|
|
4098
|
+
const field = normalizeFieldName(input.field);
|
|
4099
|
+
const value = field === 'status' ? normalizeStatusValue(target.contentType, input.value) : input.value;
|
|
4100
|
+
const aliases: Record<string, string> = {
|
|
4101
|
+
description: 'description_json',
|
|
4102
|
+
feature_image: 'feature_image_id',
|
|
4103
|
+
feature_image_id: 'feature_image_id',
|
|
4104
|
+
language: 'language_id',
|
|
4105
|
+
meta_description: 'meta_description',
|
|
4106
|
+
meta_title: 'meta_title',
|
|
4107
|
+
payment: 'payment_provider',
|
|
4108
|
+
provider: 'payment_provider',
|
|
4109
|
+
regular_price: 'price',
|
|
4110
|
+
sale: 'sale_price',
|
|
4111
|
+
sale_price: 'sale_price',
|
|
4112
|
+
short_description: 'short_description',
|
|
4113
|
+
taxable: 'is_taxable',
|
|
4114
|
+
trial: 'trial_period_days',
|
|
4115
|
+
trial_days: 'trial_period_days',
|
|
4116
|
+
trial_payment_method_required: 'trial_requires_payment_method',
|
|
4117
|
+
type: 'product_type',
|
|
4118
|
+
};
|
|
4119
|
+
const normalizedField = aliases[field] || field;
|
|
4120
|
+
|
|
4121
|
+
if (target.contentType !== 'product') {
|
|
4122
|
+
const pagePostFields = target.contentType === 'page' ? PAGE_FIELD_NAMES : POST_FIELD_NAMES;
|
|
4123
|
+
|
|
4124
|
+
if (!pagePostFields.has(normalizedField)) {
|
|
4125
|
+
throw new Error(`Field "${input.field}" cannot be updated for ${target.contentType}.`);
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
if (normalizedField === 'status') {
|
|
4129
|
+
assertValidStatusForContentType(target.contentType, value);
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
return {
|
|
4133
|
+
field: normalizedField,
|
|
4134
|
+
payload: {
|
|
4135
|
+
[normalizedField]: normalizeCmsFieldValue(normalizedField, value),
|
|
4136
|
+
},
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
const productFieldNames = new Set([
|
|
4141
|
+
'description_json',
|
|
4142
|
+
'freemius_plan_id',
|
|
4143
|
+
'freemius_product_id',
|
|
4144
|
+
'is_taxable',
|
|
4145
|
+
'language_id',
|
|
4146
|
+
'meta_description',
|
|
4147
|
+
'meta_title',
|
|
4148
|
+
'payment_provider',
|
|
4149
|
+
'price',
|
|
4150
|
+
'prices',
|
|
4151
|
+
'product_type',
|
|
4152
|
+
'sale_price',
|
|
4153
|
+
'sale_prices',
|
|
4154
|
+
'short_description',
|
|
4155
|
+
'sku',
|
|
4156
|
+
'slug',
|
|
4157
|
+
'status',
|
|
4158
|
+
'stock',
|
|
4159
|
+
'title',
|
|
4160
|
+
'trial_period_days',
|
|
4161
|
+
'trial_requires_payment_method',
|
|
4162
|
+
'upc',
|
|
4163
|
+
]);
|
|
4164
|
+
|
|
4165
|
+
if (!productFieldNames.has(normalizedField)) {
|
|
4166
|
+
throw new Error(`Field "${input.field}" cannot be updated for product.`);
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
if (normalizedField === 'status') {
|
|
4170
|
+
assertValidStatusForContentType('product', value);
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
if (normalizedField === 'price' || normalizedField === 'sale_price') {
|
|
4174
|
+
if (value !== null && (typeof value !== 'number' || value < 0)) {
|
|
4175
|
+
throw new Error(`${normalizedField} must be a non-negative number or null.`);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
if (normalizedField === 'stock' && (!Number.isInteger(value) || Number(value) < 0)) {
|
|
4180
|
+
throw new Error('stock must be a non-negative integer.');
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
if (normalizedField === 'trial_period_days' && (!Number.isInteger(value) || Number(value) < 0)) {
|
|
4184
|
+
throw new Error('trial_period_days must be a non-negative integer.');
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
if (normalizedField === 'trial_requires_payment_method' && typeof value !== 'boolean') {
|
|
4188
|
+
throw new Error('trial_requires_payment_method must be a boolean.');
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
return {
|
|
4192
|
+
field: normalizedField,
|
|
4193
|
+
payload: {
|
|
4194
|
+
[normalizedField]: value,
|
|
4195
|
+
},
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
export async function executeUpdateCmsItemField(
|
|
4200
|
+
input: UpdateCmsItemFieldInput,
|
|
4201
|
+
context?: ToolExecutionContext
|
|
4202
|
+
) {
|
|
4203
|
+
const parsed = updateCmsItemFieldInputSchema.parse(input);
|
|
4204
|
+
|
|
4205
|
+
if (isUnsupportedDatedSpecial(parsed)) {
|
|
4206
|
+
return {
|
|
4207
|
+
message:
|
|
4208
|
+
'Scheduled product specials are not supported by the current product schema yet. I can set or clear sale_price now, but not start/end dates.',
|
|
4209
|
+
mutationExecuted: false,
|
|
4210
|
+
success: false,
|
|
4211
|
+
unsupported: true,
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
const target = await resolveCmsTarget(parsed, context);
|
|
4216
|
+
const fieldUpdate = buildSingleFieldUpdatePayload(parsed, target);
|
|
4217
|
+
const field = fieldUpdate.field;
|
|
4218
|
+
let payload = fieldUpdate.payload;
|
|
4219
|
+
|
|
4220
|
+
if (field === 'language_id' && typeof payload.language_id === 'string') {
|
|
4221
|
+
const language = await getLanguageRecord(getSupabase(context), payload.language_id);
|
|
4222
|
+
payload = {
|
|
4223
|
+
...payload,
|
|
4224
|
+
language_id: language.id,
|
|
4225
|
+
};
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
const confirmation = getConfirmationPreview({
|
|
4229
|
+
action: 'UPDATE FIELD',
|
|
4230
|
+
context,
|
|
4231
|
+
payload: {
|
|
4232
|
+
contentType: target.contentType,
|
|
4233
|
+
entityId: target.item.id,
|
|
4234
|
+
field,
|
|
4235
|
+
payload,
|
|
4236
|
+
tool: 'update_cms_item_field',
|
|
4237
|
+
},
|
|
4238
|
+
preview: {
|
|
4239
|
+
contentType: target.contentType,
|
|
4240
|
+
field,
|
|
4241
|
+
from: target.item[field],
|
|
4242
|
+
slug: target.item.slug,
|
|
4243
|
+
title: target.item.title,
|
|
4244
|
+
to: payload[field],
|
|
4245
|
+
},
|
|
4246
|
+
subject: `${target.contentType} ${target.item.slug || target.item.id} ${field}`,
|
|
4247
|
+
});
|
|
4248
|
+
|
|
4249
|
+
if (confirmation) {
|
|
4250
|
+
return confirmation;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
if (target.contentType === 'product') {
|
|
4254
|
+
const { updateProduct: updateEcommerceProduct } = await getEcommerceProductModule();
|
|
4255
|
+
const productPayload = await buildProductFormValuesFromRow(target.item, getSupabase(context), payload);
|
|
4256
|
+
const product = await updateEcommerceProduct(getSupabase(context) as any, String(target.item.id), productPayload);
|
|
4257
|
+
|
|
4258
|
+
revalidateCurrentCmsSurfaces(
|
|
4259
|
+
context,
|
|
4260
|
+
{
|
|
4261
|
+
contentType: 'product',
|
|
4262
|
+
entityId: String(target.item.id),
|
|
4263
|
+
languageId: product?.language_id ?? target.item.language_id,
|
|
4264
|
+
slug: product?.slug ?? target.item.slug,
|
|
4265
|
+
title: product?.title ?? target.item.title,
|
|
4266
|
+
},
|
|
4267
|
+
product?.slug ?? target.item.slug
|
|
4268
|
+
);
|
|
4269
|
+
|
|
4270
|
+
return {
|
|
4271
|
+
contentType: 'product',
|
|
4272
|
+
entityId: target.item.id,
|
|
4273
|
+
field,
|
|
4274
|
+
mutationExecuted: true,
|
|
4275
|
+
slug: product?.slug ?? target.item.slug,
|
|
4276
|
+
success: true,
|
|
4277
|
+
updatedFields: [field],
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
const table = target.contentType === 'page' ? 'pages' : 'posts';
|
|
4282
|
+
const { data: item, error } = await getSupabase(context)
|
|
4283
|
+
.from(table)
|
|
4284
|
+
.update({
|
|
4285
|
+
...payload,
|
|
4286
|
+
updated_at: new Date().toISOString(),
|
|
4287
|
+
})
|
|
4288
|
+
.eq('id', target.item.id)
|
|
4289
|
+
.select('id, language_id, slug, status, title')
|
|
4290
|
+
.single();
|
|
4291
|
+
|
|
4292
|
+
if (error || !item) {
|
|
4293
|
+
throw new Error(`Failed to update ${target.contentType}: ${serializeError(error)}`);
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
revalidateCurrentCmsSurfaces(
|
|
4297
|
+
context,
|
|
4298
|
+
{
|
|
4299
|
+
contentType: target.contentType,
|
|
4300
|
+
entityId: Number(item.id),
|
|
4301
|
+
languageId: item.language_id,
|
|
4302
|
+
slug: item.slug,
|
|
4303
|
+
title: item.title,
|
|
4304
|
+
},
|
|
4305
|
+
item.slug
|
|
4306
|
+
);
|
|
4307
|
+
|
|
4308
|
+
return {
|
|
4309
|
+
contentType: target.contentType,
|
|
4310
|
+
entityId: item.id,
|
|
4311
|
+
field,
|
|
4312
|
+
mutationExecuted: true,
|
|
4313
|
+
slug: item.slug,
|
|
4314
|
+
success: true,
|
|
4315
|
+
updatedFields: [field],
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
async function buildDeletePreview(
|
|
4320
|
+
input: PrepareDeleteCmsItemInput | DeleteCmsItemInput,
|
|
4321
|
+
context?: ToolExecutionContext
|
|
4322
|
+
) {
|
|
4323
|
+
const parsed = prepareDeleteCmsItemInputSchema.parse(input);
|
|
4324
|
+
const target = await resolveCmsTarget(parsed, context);
|
|
4325
|
+
|
|
4326
|
+
if (target.contentType === 'product') {
|
|
4327
|
+
return {
|
|
4328
|
+
affectedCount: 1,
|
|
4329
|
+
collectionPath: getCollectionPath('product'),
|
|
4330
|
+
contentType: 'product' as const,
|
|
4331
|
+
item: target.item,
|
|
4332
|
+
navigationLinkCount: 0,
|
|
4333
|
+
publicPaths: target.item.slug ? [`/product/${target.item.slug}`] : [],
|
|
4334
|
+
targetIds: [target.item.id],
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
const table = target.contentType === 'page' ? 'pages' : 'posts';
|
|
4339
|
+
const { data, error } = await getSupabase(context)
|
|
4340
|
+
.from(table)
|
|
4341
|
+
.select('id, slug, title, translation_group_id')
|
|
4342
|
+
.eq('translation_group_id', target.item.translation_group_id);
|
|
4343
|
+
|
|
4344
|
+
if (error) {
|
|
4345
|
+
throw new Error(`Failed to inspect related ${target.contentType}s: ${serializeError(error)}`);
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
const rows = Array.isArray(data) ? data : [];
|
|
4349
|
+
const publicPaths = rows
|
|
4350
|
+
.map((row: any) =>
|
|
4351
|
+
target.contentType === 'page'
|
|
4352
|
+
? row.slug === 'home'
|
|
4353
|
+
? '/'
|
|
4354
|
+
: `/${row.slug}`
|
|
4355
|
+
: `/article/${row.slug}`
|
|
4356
|
+
)
|
|
4357
|
+
.filter(Boolean);
|
|
4358
|
+
const publicPathSet = new Set(publicPaths);
|
|
4359
|
+
const { data: navigationItems, error: navigationItemsError } = await getSupabase(context)
|
|
4360
|
+
.from('navigation_items')
|
|
4361
|
+
.select('id, url');
|
|
4362
|
+
|
|
4363
|
+
if (navigationItemsError) {
|
|
4364
|
+
throw new Error(`Failed to inspect related navigation links: ${serializeError(navigationItemsError)}`);
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
const navigationLinkCount = (Array.isArray(navigationItems) ? navigationItems : []).filter(
|
|
4368
|
+
(item: any) => publicPathSet.has(item.url)
|
|
4369
|
+
).length;
|
|
4370
|
+
|
|
4371
|
+
return {
|
|
4372
|
+
affectedCount: rows.length,
|
|
4373
|
+
collectionPath: getCollectionPath(target.contentType),
|
|
4374
|
+
contentType: target.contentType,
|
|
4375
|
+
item: target.item,
|
|
4376
|
+
navigationLinkCount,
|
|
4377
|
+
publicPaths,
|
|
4378
|
+
targetIds: rows.map((row: any) => row.id),
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
function summarizeDeletePreview(preview: Awaited<ReturnType<typeof buildDeletePreview>>) {
|
|
4383
|
+
const title = preview.item.title || preview.item.slug || 'selected item';
|
|
4384
|
+
const slug = preview.item.slug ? ` (${preview.item.slug})` : '';
|
|
4385
|
+
|
|
4386
|
+
if (preview.contentType === 'product') {
|
|
4387
|
+
return `Delete product "${title}"${slug}.`;
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
const details = [
|
|
4391
|
+
`${pluralize(preview.affectedCount, 'language version')}`,
|
|
4392
|
+
preview.navigationLinkCount > 0
|
|
4393
|
+
? `${pluralize(preview.navigationLinkCount, 'navigation link')}`
|
|
4394
|
+
: null,
|
|
4395
|
+
].filter(Boolean);
|
|
4396
|
+
|
|
4397
|
+
return `Delete ${preview.contentType} "${title}"${slug}, including ${details.join(' and ')}.`;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
export async function executePrepareDeleteCmsItem(
|
|
4401
|
+
input: PrepareDeleteCmsItemInput,
|
|
4402
|
+
context?: ToolExecutionContext
|
|
4403
|
+
) {
|
|
4404
|
+
const preview = await buildDeletePreview(input, context);
|
|
4405
|
+
const confirmation = buildConfirmationPreview({
|
|
4406
|
+
action: `DELETE ${preview.contentType}`,
|
|
4407
|
+
payload: {
|
|
4408
|
+
affectedCount: preview.affectedCount,
|
|
4409
|
+
contentType: preview.contentType,
|
|
4410
|
+
targetIds: preview.targetIds,
|
|
4411
|
+
tool: 'delete_cms_item',
|
|
4412
|
+
},
|
|
4413
|
+
preview: {
|
|
4414
|
+
affectedCount: preview.affectedCount,
|
|
4415
|
+
collectionPath: preview.collectionPath,
|
|
4416
|
+
contentType: preview.contentType,
|
|
4417
|
+
navigationLinkCount: preview.navigationLinkCount,
|
|
4418
|
+
publicPaths: preview.publicPaths,
|
|
4419
|
+
slug: preview.item.slug,
|
|
4420
|
+
summary: summarizeDeletePreview(preview),
|
|
4421
|
+
title: preview.item.title,
|
|
4422
|
+
},
|
|
4423
|
+
subject: `${preview.item.id} ${preview.item.slug || ''}`,
|
|
4424
|
+
});
|
|
4425
|
+
|
|
4426
|
+
return {
|
|
4427
|
+
...confirmation,
|
|
4428
|
+
preparedDelete: true,
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
export async function executeDeleteCmsItem(input: DeleteCmsItemInput, context?: ToolExecutionContext) {
|
|
4433
|
+
const parsed = deleteCmsItemInputSchema.parse(input);
|
|
4434
|
+
const preview = await buildDeletePreview(parsed, context);
|
|
4435
|
+
const confirmation = getConfirmationPreview({
|
|
4436
|
+
action: `DELETE ${preview.contentType}`,
|
|
4437
|
+
context,
|
|
4438
|
+
payload: {
|
|
4439
|
+
affectedCount: preview.affectedCount,
|
|
4440
|
+
contentType: preview.contentType,
|
|
4441
|
+
targetIds: preview.targetIds,
|
|
4442
|
+
tool: 'delete_cms_item',
|
|
4443
|
+
},
|
|
4444
|
+
preview: {
|
|
4445
|
+
affectedCount: preview.affectedCount,
|
|
4446
|
+
collectionPath: preview.collectionPath,
|
|
4447
|
+
contentType: preview.contentType,
|
|
4448
|
+
navigationLinkCount: preview.navigationLinkCount,
|
|
4449
|
+
publicPaths: preview.publicPaths,
|
|
4450
|
+
slug: preview.item.slug,
|
|
4451
|
+
summary: summarizeDeletePreview(preview),
|
|
4452
|
+
title: preview.item.title,
|
|
4453
|
+
},
|
|
4454
|
+
subject: `${preview.item.id} ${preview.item.slug || ''}`,
|
|
4455
|
+
});
|
|
4456
|
+
|
|
4457
|
+
if (confirmation) {
|
|
4458
|
+
return confirmation;
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
const supabase = getSupabase(context);
|
|
4462
|
+
|
|
4463
|
+
if (preview.contentType === 'product') {
|
|
4464
|
+
const { error } = await supabase.from('products').delete().eq('id', preview.item.id);
|
|
4465
|
+
|
|
4466
|
+
if (error) {
|
|
4467
|
+
throw new Error(`Failed to delete product: ${serializeError(error)}`);
|
|
4468
|
+
}
|
|
4469
|
+
} else {
|
|
4470
|
+
for (const publicPath of preview.publicPaths) {
|
|
4471
|
+
await supabase.from('navigation_items').delete().eq('url', publicPath);
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
const table = preview.contentType === 'page' ? 'pages' : 'posts';
|
|
4475
|
+
const { error } = await supabase
|
|
4476
|
+
.from(table)
|
|
4477
|
+
.delete()
|
|
4478
|
+
.eq('translation_group_id', preview.item.translation_group_id);
|
|
4479
|
+
|
|
4480
|
+
if (error) {
|
|
4481
|
+
throw new Error(`Failed to delete ${preview.contentType}: ${serializeError(error)}`);
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
const revalidatePath = context?.revalidatePath ?? getDefaultRevalidatePath();
|
|
4486
|
+
|
|
4487
|
+
if (revalidatePath) {
|
|
4488
|
+
revalidatePath(preview.collectionPath);
|
|
4489
|
+
revalidatePath('/cms/navigation');
|
|
4490
|
+
preview.publicPaths.forEach((path) => revalidatePath(path));
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
return {
|
|
4494
|
+
affectedCount: preview.affectedCount,
|
|
4495
|
+
collectionPath: preview.collectionPath,
|
|
4496
|
+
contentType: preview.contentType,
|
|
4497
|
+
mutationExecuted: true,
|
|
4498
|
+
redirectPath: preview.collectionPath,
|
|
4499
|
+
success: true,
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
async function executeActionPlanChild(
|
|
4504
|
+
action: z.infer<typeof cmsActionPlanActionSchema>,
|
|
4505
|
+
context?: ToolExecutionContext
|
|
4506
|
+
) {
|
|
4507
|
+
switch (action.tool) {
|
|
4508
|
+
case 'create_cms_page':
|
|
4509
|
+
return executeCreateCmsPage(action.input, context);
|
|
4510
|
+
case 'create_cms_post':
|
|
4511
|
+
return executeCreateCmsPost(action.input, context);
|
|
4512
|
+
case 'create_cms_product':
|
|
4513
|
+
return executeCreateCmsProduct(action.input, context);
|
|
4514
|
+
case 'delete_cms_item':
|
|
4515
|
+
return executeDeleteCmsItem(action.input, context);
|
|
4516
|
+
case 'update_cms_item_field':
|
|
4517
|
+
return executeUpdateCmsItemField(action.input, context);
|
|
4518
|
+
case 'update_content_block':
|
|
4519
|
+
return executeUpdateContentBlock(action.input, context);
|
|
4520
|
+
case 'insert_content_block':
|
|
4521
|
+
return executeInsertContentBlock(action.input, context);
|
|
4522
|
+
case 'update_current_cms_fields':
|
|
4523
|
+
return executeUpdateCurrentCmsFields(action.input, context);
|
|
4524
|
+
case 'update_footer':
|
|
4525
|
+
return executeUpdateFooter(action.input, context);
|
|
4526
|
+
case 'update_navigation_bar':
|
|
4527
|
+
return executeUpdateNavigationBar(action.input, context);
|
|
4528
|
+
case 'update_section_column_block':
|
|
4529
|
+
return executeUpdateSectionColumnBlock(action.input, context);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
function withActionPlanTranslationGroup(
|
|
4534
|
+
action: z.infer<typeof cmsActionPlanActionSchema>,
|
|
4535
|
+
translationGroupsByCreateTool: Partial<Record<'create_cms_page' | 'create_cms_post' | 'create_cms_product', string>>
|
|
4536
|
+
) {
|
|
4537
|
+
if (
|
|
4538
|
+
action.tool !== 'create_cms_page' &&
|
|
4539
|
+
action.tool !== 'create_cms_post' &&
|
|
4540
|
+
action.tool !== 'create_cms_product'
|
|
4541
|
+
) {
|
|
4542
|
+
return action;
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
if (action.input.translationGroupId || !action.input.languageCode) {
|
|
4546
|
+
return action;
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
const translationGroupId = translationGroupsByCreateTool[action.tool];
|
|
4550
|
+
|
|
4551
|
+
if (!translationGroupId) {
|
|
4552
|
+
return action;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
return {
|
|
4556
|
+
...action,
|
|
4557
|
+
input: {
|
|
4558
|
+
...action.input,
|
|
4559
|
+
translationGroupId,
|
|
4560
|
+
},
|
|
4561
|
+
} as z.infer<typeof cmsActionPlanActionSchema>;
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
export async function executeCmsActionPlan(
|
|
4565
|
+
input: ExecuteCmsActionPlanInput,
|
|
4566
|
+
context?: ToolExecutionContext
|
|
4567
|
+
) {
|
|
4568
|
+
const parsed = executeCmsActionPlanInputSchema.parse(input);
|
|
4569
|
+
|
|
4570
|
+
if (!context?.skipConfirmation) {
|
|
4571
|
+
const actionSummaries: string[] = [];
|
|
4572
|
+
|
|
4573
|
+
for (const action of parsed.actions) {
|
|
4574
|
+
const result = await executeActionPlanChild(action, {
|
|
4575
|
+
...context,
|
|
4576
|
+
latestUserMessage: null,
|
|
4577
|
+
});
|
|
4578
|
+
|
|
4579
|
+
if (!result || typeof result !== 'object') {
|
|
4580
|
+
return {
|
|
4581
|
+
message: `Could not prepare action ${actionSummaries.length + 1}.`,
|
|
4582
|
+
mutationExecuted: false,
|
|
4583
|
+
success: false,
|
|
4584
|
+
};
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
if ((result as any).success === false || (result as any).unsupported === true) {
|
|
4588
|
+
return result;
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
if ((result as any).requiresConfirmation === true && (result as any).preview) {
|
|
4592
|
+
actionSummaries.push(
|
|
4593
|
+
summarizeCmsMutationPreview(action.tool, (result as any).preview)
|
|
4594
|
+
);
|
|
4595
|
+
} else {
|
|
4596
|
+
actionSummaries.push(`Run ${action.tool.replace(/_/g, ' ')}.`);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
const summary =
|
|
4601
|
+
parsed.summary ||
|
|
4602
|
+
`Complete ${pluralize(parsed.actions.length, 'CMS action')}.`;
|
|
4603
|
+
const confirmation = getConfirmationPreview({
|
|
4604
|
+
action: 'EXECUTE CMS ACTION PLAN',
|
|
4605
|
+
context,
|
|
4606
|
+
payload: { actions: parsed.actions, tool: 'execute_cms_action_plan' },
|
|
4607
|
+
preview: {
|
|
4608
|
+
actionCount: parsed.actions.length,
|
|
4609
|
+
actionSummaries,
|
|
4610
|
+
summary,
|
|
4611
|
+
},
|
|
4612
|
+
subject: `${parsed.actions.length} actions`,
|
|
4613
|
+
});
|
|
4614
|
+
|
|
4615
|
+
if (confirmation) {
|
|
4616
|
+
return confirmation;
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
const childContext = {
|
|
4621
|
+
...context,
|
|
4622
|
+
skipConfirmation: true,
|
|
4623
|
+
};
|
|
4624
|
+
const results: Array<{ output: unknown; tool: string }> = [];
|
|
4625
|
+
let mutationExecuted = false;
|
|
4626
|
+
let editPath: string | null = null;
|
|
4627
|
+
let redirectPath: string | null = null;
|
|
4628
|
+
const translationGroupsByCreateTool: Partial<Record<'create_cms_page' | 'create_cms_post' | 'create_cms_product', string>> = {};
|
|
4629
|
+
|
|
4630
|
+
for (const [index, action] of parsed.actions.entries()) {
|
|
4631
|
+
const actionToExecute = withActionPlanTranslationGroup(action, translationGroupsByCreateTool);
|
|
4632
|
+
const output = await executeActionPlanChild(actionToExecute, childContext);
|
|
4633
|
+
|
|
4634
|
+
results.push({ output, tool: actionToExecute.tool });
|
|
4635
|
+
|
|
4636
|
+
if (output && typeof output === 'object') {
|
|
4637
|
+
const record = output as Record<string, unknown>;
|
|
4638
|
+
|
|
4639
|
+
if (record.mutationExecuted === true) {
|
|
4640
|
+
mutationExecuted = true;
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
if (!editPath && typeof record.editPath === 'string') {
|
|
4644
|
+
editPath = record.editPath;
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
if (typeof record.redirectPath === 'string') {
|
|
4648
|
+
redirectPath = record.redirectPath;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
if (
|
|
4652
|
+
(actionToExecute.tool === 'create_cms_page' ||
|
|
4653
|
+
actionToExecute.tool === 'create_cms_post' ||
|
|
4654
|
+
actionToExecute.tool === 'create_cms_product') &&
|
|
4655
|
+
typeof record.translationGroupId === 'string'
|
|
4656
|
+
) {
|
|
4657
|
+
translationGroupsByCreateTool[actionToExecute.tool] = record.translationGroupId;
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4660
|
+
if (record.success === false || record.unsupported === true) {
|
|
4661
|
+
return {
|
|
4662
|
+
actionCount: parsed.actions.length,
|
|
4663
|
+
failedActionIndex: index,
|
|
4664
|
+
failedTool: actionToExecute.tool,
|
|
4665
|
+
message:
|
|
4666
|
+
typeof record.message === 'string'
|
|
4667
|
+
? record.message
|
|
4668
|
+
: `Action ${index + 1} failed.`,
|
|
4669
|
+
mutationExecuted,
|
|
4670
|
+
results,
|
|
4671
|
+
success: false,
|
|
4672
|
+
...(editPath ? { editPath } : {}),
|
|
4673
|
+
...(redirectPath ? { redirectPath } : {}),
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
return {
|
|
4680
|
+
actionCount: parsed.actions.length,
|
|
4681
|
+
editPath: redirectPath ? undefined : editPath ?? undefined,
|
|
4682
|
+
mutationExecuted,
|
|
4683
|
+
redirectPath: redirectPath ?? undefined,
|
|
4684
|
+
results,
|
|
4685
|
+
success: true,
|
|
4686
|
+
summary: parsed.summary ?? null,
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
export function createCortexGlobalAgentTools(context?: ToolExecutionContext) {
|
|
4691
|
+
return {
|
|
4692
|
+
...createCortexDatabaseAgentTools(context),
|
|
4693
|
+
...createCortexCustomBlockTools(context),
|
|
4694
|
+
fetch_ecommerce_stats: tool({
|
|
4695
|
+
description:
|
|
4696
|
+
'Fetch quantitative ecommerce statistics and reports from the database. Use this to answer questions about revenue, order counts, order status counts such as pending or trial, and top-selling products over a time range. This tool is read-only and does not require confirmation.',
|
|
4697
|
+
execute: (input) => executeFetchEcommerceStats(input, context),
|
|
4698
|
+
inputSchema: fetchEcommerceStatsInputSchema,
|
|
4699
|
+
strict: true,
|
|
4700
|
+
}),
|
|
4701
|
+
read_current_cms_item: tool({
|
|
4702
|
+
description:
|
|
4703
|
+
'Read the CMS item currently being edited. Requires pageContext and returns page/post/product metadata plus page/post block summaries or content.',
|
|
4704
|
+
execute: (input) => executeReadCurrentCmsItem(input, context),
|
|
4705
|
+
inputSchema: readCurrentCmsItemInputSchema,
|
|
4706
|
+
strict: true,
|
|
4707
|
+
}),
|
|
4708
|
+
search_documentation: tool({
|
|
4709
|
+
description:
|
|
4710
|
+
'Search the NextBlock documentation database and return concise source snippets for factual CMS guidance.',
|
|
4711
|
+
execute: (input) => executeSearchDocumentationWithTimeout(input, context),
|
|
4712
|
+
inputSchema: searchDocumentationInputSchema,
|
|
4713
|
+
strict: true,
|
|
4714
|
+
}),
|
|
4715
|
+
create_cms_page: tool({
|
|
4716
|
+
description:
|
|
4717
|
+
'Create a new CMS page with metadata and optional validated page blocks. Mutating: first returns a confirmation phrase; only executes after the user replies with the exact phrase. For translations, pass translationGroupId from the source page/post/product context so the new language is linked to the same backend translation group. For contact pages, provide contactEmail or a form block with recipient_email and fields.',
|
|
4718
|
+
execute: (input) => executeCreateCmsPage(input, context),
|
|
4719
|
+
inputSchema: createCmsPageInputSchema,
|
|
4720
|
+
strict: true,
|
|
4721
|
+
}),
|
|
4722
|
+
create_cms_post: tool({
|
|
4723
|
+
description:
|
|
4724
|
+
'Create a new CMS post with metadata and optional validated post blocks. Mutating: first returns a confirmation phrase; only executes after the user replies with the exact phrase. For translations, pass translationGroupId from the source post context so the new language is linked to the same backend translation group.',
|
|
4725
|
+
execute: (input) => executeCreateCmsPost(input, context),
|
|
4726
|
+
inputSchema: createCmsPostInputSchema,
|
|
4727
|
+
strict: true,
|
|
4728
|
+
}),
|
|
4729
|
+
create_cms_product: tool({
|
|
4730
|
+
description:
|
|
4731
|
+
'Create a new draft-capable product. Defaults missing product fields safely: physical Stripe product, generated SKU, price 0, stock 0, taxable, draft. For translations, pass translationGroupId from the source product context. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4732
|
+
execute: (input) => executeCreateCmsProduct(input, context),
|
|
4733
|
+
inputSchema: createCmsProductInputSchema,
|
|
4734
|
+
strict: true,
|
|
4735
|
+
}),
|
|
4736
|
+
delete_cms_item: tool({
|
|
4737
|
+
description:
|
|
4738
|
+
'Delete a resolved page, post, or product after exact confirmation. Pages/posts delete all translations in the translation group and related navigation links. Mutating: refuses unless the latest user message includes the exact confirmation phrase.',
|
|
4739
|
+
execute: (input) => executeDeleteCmsItem(input, context),
|
|
4740
|
+
inputSchema: deleteCmsItemInputSchema,
|
|
4741
|
+
strict: true,
|
|
4742
|
+
}),
|
|
4743
|
+
prepare_delete_cms_item: tool({
|
|
4744
|
+
description:
|
|
4745
|
+
'Inspect the page, post, or product that would be deleted and return the exact confirmation phrase. This tool does not mutate data.',
|
|
4746
|
+
execute: (input) => executePrepareDeleteCmsItem(input, context),
|
|
4747
|
+
inputSchema: prepareDeleteCmsItemInputSchema,
|
|
4748
|
+
strict: true,
|
|
4749
|
+
}),
|
|
4750
|
+
update_footer: tool({
|
|
4751
|
+
description:
|
|
4752
|
+
'Replace the public footer links and/or footer copyright settings for a locale. Use links for footer navigation and copyright for locale text templates. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4753
|
+
execute: (input) => executeUpdateFooter(input, context),
|
|
4754
|
+
inputSchema: updateFooterInputSchema,
|
|
4755
|
+
strict: true,
|
|
4756
|
+
}),
|
|
4757
|
+
update_content_block: tool({
|
|
4758
|
+
description:
|
|
4759
|
+
'Update the JSON content of an existing top-level page/post block that belongs to the current CMS edit context. Content is merged with the existing block before validation. For section blocks, add nested blocks with content.append_block or content.append_blocks using objects like { block_type: "button", content: { text: "Contact Us", url: "/contact" } }; existing column_blocks and layout fields are preserved. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4760
|
+
execute: (input) => executeUpdateContentBlock(input, context),
|
|
4761
|
+
inputSchema: updateContentBlockInputSchema,
|
|
4762
|
+
strict: true,
|
|
4763
|
+
}),
|
|
4764
|
+
insert_content_block: tool({
|
|
4765
|
+
description:
|
|
4766
|
+
'Insert a new validated top-level page/post block before or after an existing block, or at the start/end. Use this for visible content additions like adding a rich text title and paragraph above a form. For "above the form", use position "before" with anchorBlockType "form" and blockType "text" containing html_content. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4767
|
+
execute: (input) => executeInsertContentBlock(input, context),
|
|
4768
|
+
inputSchema: insertContentBlockInputSchema,
|
|
4769
|
+
strict: true,
|
|
4770
|
+
}),
|
|
4771
|
+
update_current_cms_fields: tool({
|
|
4772
|
+
description:
|
|
4773
|
+
'Update validated metadata fields on the current page, post, or product. For products, description_json must be a valid NextBlock editor document JSON object. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4774
|
+
execute: (input) => executeUpdateCurrentCmsFields(input, context),
|
|
4775
|
+
inputSchema: updateCurrentCmsFieldsInputSchema,
|
|
4776
|
+
strict: true,
|
|
4777
|
+
}),
|
|
4778
|
+
update_cms_item_field: tool({
|
|
4779
|
+
description:
|
|
4780
|
+
'Update one field on a page, post, or product, resolving by current edit context, id, slug, or exact title. Use this for requests like changing price, stock, title, slug, status, sale_price, or meta fields. Interpret public as published for pages/posts and active for products. Scheduled sale date ranges are not supported and will be refused without mutation. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4781
|
+
execute: (input) => executeUpdateCmsItemField(input, context),
|
|
4782
|
+
inputSchema: updateCmsItemFieldInputSchema,
|
|
4783
|
+
strict: true,
|
|
4784
|
+
}),
|
|
4785
|
+
update_navigation_bar: tool({
|
|
4786
|
+
description:
|
|
4787
|
+
'Update the public header navigation bar for a locale. Use mode "append" when adding links while preserving existing navigation. Use mode "update" when renaming or changing an existing single link. Use mode "replace" only when the user asks to rebuild the complete header and you provide the full menu; destructive partial replacements are refused. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4788
|
+
execute: (input) => executeUpdateNavigationBar(input, context),
|
|
4789
|
+
inputSchema: updateNavigationBarInputSchema,
|
|
4790
|
+
strict: true,
|
|
4791
|
+
}),
|
|
4792
|
+
update_section_column_block: tool({
|
|
4793
|
+
description:
|
|
4794
|
+
'Update the content of one existing nested block inside a section block that belongs to the current CMS edit context. This tool must not change the nested block type. To add a new nested block, update the parent section with update_content_block and preserve existing column_blocks. Mutating: first returns a confirmation phrase; only executes after exact confirmation.',
|
|
4795
|
+
execute: (input) => executeUpdateSectionColumnBlock(input, context),
|
|
4796
|
+
inputSchema: updateSectionColumnBlockInputSchema,
|
|
4797
|
+
strict: true,
|
|
4798
|
+
}),
|
|
4799
|
+
execute_cms_action_plan: tool({
|
|
4800
|
+
description:
|
|
4801
|
+
'Execute multiple CMS mutations as one confirmed plan. Use this whenever the user asks for more than one change in the same prompt, such as creating a page and adding a navigation link. First returns one combined confirmation preview and Confirm button; after confirmation, runs each action in order and stops on the first failure.',
|
|
4802
|
+
execute: (input) => executeCmsActionPlan(input, context),
|
|
4803
|
+
inputSchema: executeCmsActionPlanInputSchema,
|
|
4804
|
+
strict: true,
|
|
4805
|
+
}),
|
|
4806
|
+
};
|
|
4807
|
+
}
|