create-nextblock 0.13.9 → 0.13.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-template/.dockerignore +23 -23
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +1 -1
- package/project.json +19 -19
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.dockerignore +23 -23
- package/templates/nextblock-template/README.md +34 -34
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/postActions.ts +146 -146
- package/templates/nextblock-template/app/actions/productGridActions.ts +40 -40
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/ecommerce/product-picker/route.ts +151 -151
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5609
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +10 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +5 -1
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +55 -24
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/components/MultiEntityPicker.tsx +263 -251
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/EcommerceActiveContext.tsx +27 -27
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -189
- package/templates/nextblock-template/app/cms/layout.tsx +73 -73
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
- package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageDetectionPanel.tsx +188 -188
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -217
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -44
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/sitemap.ts +130 -130
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/ProductGridClient.tsx +114 -114
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +8 -1
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +29 -23
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +69 -0
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -0
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -464
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
- package/templates/nextblock-template/docs/README.md +39 -39
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +78 -78
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +21 -1
- package/templates/nextblock-template/lib/blocks/product-grid-data.ts +210 -210
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
- package/templates/nextblock-template/lib/i18n/country-languages.ts +247 -247
- package/templates/nextblock-template/lib/i18n/detection.test.ts +197 -197
- package/templates/nextblock-template/lib/i18n/detection.ts +192 -192
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
- package/templates/nextblock-template/lib/media/youtube.ts +81 -0
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -82
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx
CHANGED
|
@@ -1,497 +1,497 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useState } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
Alert,
|
|
6
|
-
AlertDescription,
|
|
7
|
-
AlertTitle,
|
|
8
|
-
Badge,
|
|
9
|
-
Button,
|
|
10
|
-
Card,
|
|
11
|
-
CardContent,
|
|
12
|
-
CardDescription,
|
|
13
|
-
CardHeader,
|
|
14
|
-
CardTitle,
|
|
15
|
-
Input,
|
|
16
|
-
Label,
|
|
17
|
-
SearchableSelect,
|
|
18
|
-
} from '@nextblock-cms/ui';
|
|
19
|
-
import {
|
|
20
|
-
AlertTriangle,
|
|
21
|
-
BrainCircuit,
|
|
22
|
-
CheckCircle2,
|
|
23
|
-
Cpu,
|
|
24
|
-
KeyRound,
|
|
25
|
-
ServerCog,
|
|
26
|
-
ShieldCheck,
|
|
27
|
-
Trash2,
|
|
28
|
-
Info,
|
|
29
|
-
} from 'lucide-react';
|
|
30
|
-
import {
|
|
31
|
-
createCortexAiStoredModelSelection,
|
|
32
|
-
type CortexAiStoredModelSelection,
|
|
33
|
-
} from '@nextblock-cms/cortex/client';
|
|
34
|
-
|
|
35
|
-
const CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE = 'cortex_ai_sandbox_openrouter_api_key';
|
|
36
|
-
const CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE = 'cortex_ai_sandbox_openrouter_model_selection';
|
|
37
|
-
const CORTEX_AI_SETTINGS_CHANGED_EVENT = 'nextblock:cortex-ai-settings-changed';
|
|
38
|
-
|
|
39
|
-
type SandboxCortexAiSettingsClientProps = {
|
|
40
|
-
compatibleModels: Array<{
|
|
41
|
-
id: string;
|
|
42
|
-
name: string;
|
|
43
|
-
pricing: Record<string, string>;
|
|
44
|
-
context_length: number | null;
|
|
45
|
-
created?: number | null;
|
|
46
|
-
architecture?: {
|
|
47
|
-
modality?: string;
|
|
48
|
-
tokenizer?: string;
|
|
49
|
-
instruct_type?: string | null;
|
|
50
|
-
} | null;
|
|
51
|
-
description?: string;
|
|
52
|
-
top_provider?: {
|
|
53
|
-
max_completion_tokens?: number | null;
|
|
54
|
-
is_moderated?: boolean;
|
|
55
|
-
} | null;
|
|
56
|
-
}>;
|
|
57
|
-
isPackageActive: boolean;
|
|
58
|
-
hasEnvOpenRouterKey: boolean;
|
|
59
|
-
maskedEnvOpenRouterKey: string | null;
|
|
60
|
-
modelCatalogError: string | null;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
function formatModelPricing(pricing: Record<string, string>) {
|
|
64
|
-
const amount = Number(pricing.prompt);
|
|
65
|
-
const completionAmount = Number(pricing.completion);
|
|
66
|
-
|
|
67
|
-
if (!Number.isFinite(amount) || !Number.isFinite(completionAmount)) {
|
|
68
|
-
return 'Pricing varies';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (amount === 0 && completionAmount === 0) {
|
|
72
|
-
return 'Free';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const formatPrice = (val: number) => {
|
|
76
|
-
if (val === 0) return '$0';
|
|
77
|
-
const perMillion = val * 1_000_000;
|
|
78
|
-
return `$${perMillion < 0.01 ? perMillion.toFixed(4) : perMillion.toFixed(2)}`;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
return `${formatPrice(amount)}/1M input - ${formatPrice(completionAmount)}/1M output`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getMaskedKey(key: string) {
|
|
85
|
-
if (key.length <= 8) return '****';
|
|
86
|
-
return `**** ${key.slice(-4)}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function notifyCortexAiSettingsChanged() {
|
|
90
|
-
window.dispatchEvent(new Event(CORTEX_AI_SETTINGS_CHANGED_EVENT));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function SandboxCortexAiSettingsClient({
|
|
94
|
-
compatibleModels,
|
|
95
|
-
isPackageActive,
|
|
96
|
-
hasEnvOpenRouterKey,
|
|
97
|
-
maskedEnvOpenRouterKey,
|
|
98
|
-
modelCatalogError,
|
|
99
|
-
}: SandboxCortexAiSettingsClientProps) {
|
|
100
|
-
const [mounted, setMounted] = useState(false);
|
|
101
|
-
const [sandboxKey, setSandboxKey] = useState<string | null>(null);
|
|
102
|
-
const [sandboxModel, setSandboxModel] = useState<CortexAiStoredModelSelection | null>(null);
|
|
103
|
-
const [inputValue, setInputValue] = useState('');
|
|
104
|
-
const [modelInput, setModelInput] = useState<string>('');
|
|
105
|
-
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
106
|
-
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
try {
|
|
109
|
-
const storedKey = window.localStorage.getItem(CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE);
|
|
110
|
-
if (storedKey) {
|
|
111
|
-
setSandboxKey(storedKey);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const storedModel = window.localStorage.getItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE);
|
|
115
|
-
if (storedModel) {
|
|
116
|
-
const parsed = JSON.parse(storedModel) as CortexAiStoredModelSelection;
|
|
117
|
-
setSandboxModel(parsed);
|
|
118
|
-
setModelInput(parsed.modelId);
|
|
119
|
-
}
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error('Failed to read Cortex AI sandbox settings from localStorage', error);
|
|
122
|
-
}
|
|
123
|
-
setMounted(true);
|
|
124
|
-
}, []);
|
|
125
|
-
|
|
126
|
-
const handleSaveKey = (e: React.FormEvent) => {
|
|
127
|
-
e.preventDefault();
|
|
128
|
-
const key = inputValue.trim();
|
|
129
|
-
if (!key) return;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
window.localStorage.setItem(CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE, key);
|
|
133
|
-
setSandboxKey(key);
|
|
134
|
-
setInputValue('');
|
|
135
|
-
notifyCortexAiSettingsChanged();
|
|
136
|
-
setSuccessMessage('Sandbox OpenRouter key saved to your browser.');
|
|
137
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.error('Failed to save sandbox key', error);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const handleClearKey = () => {
|
|
144
|
-
try {
|
|
145
|
-
window.localStorage.removeItem(CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE);
|
|
146
|
-
window.localStorage.removeItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE);
|
|
147
|
-
setSandboxKey(null);
|
|
148
|
-
setSandboxModel(null);
|
|
149
|
-
setModelInput('');
|
|
150
|
-
notifyCortexAiSettingsChanged();
|
|
151
|
-
setSuccessMessage('Sandbox OpenRouter key cleared from your browser.');
|
|
152
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
153
|
-
} catch (error) {
|
|
154
|
-
console.error('Failed to clear sandbox key', error);
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const handleSaveModel = (e: React.FormEvent<HTMLFormElement>) => {
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
const formData = new FormData(e.currentTarget);
|
|
161
|
-
const modelId = String(formData.get('openrouter_model_id') || '').trim();
|
|
162
|
-
if (!modelId) return;
|
|
163
|
-
|
|
164
|
-
const selectedModel = compatibleModels.find((m) => m.id === modelId);
|
|
165
|
-
if (!selectedModel) return;
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
// Map to the shape createCortexAiStoredModelSelection expects, or something similar.
|
|
169
|
-
// createCortexAiStoredModelSelection needs a specific shape but we can mimic it.
|
|
170
|
-
// We pass the raw model data to it.
|
|
171
|
-
const storedSelection = createCortexAiStoredModelSelection(selectedModel as any);
|
|
172
|
-
window.localStorage.setItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE, JSON.stringify(storedSelection));
|
|
173
|
-
setSandboxModel(storedSelection);
|
|
174
|
-
notifyCortexAiSettingsChanged();
|
|
175
|
-
setSuccessMessage('Sandbox Cortex AI model selection saved to your browser.');
|
|
176
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
console.error('Failed to save sandbox model', error);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const handleClearModel = () => {
|
|
183
|
-
try {
|
|
184
|
-
window.localStorage.removeItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE);
|
|
185
|
-
setSandboxModel(null);
|
|
186
|
-
setModelInput('');
|
|
187
|
-
notifyCortexAiSettingsChanged();
|
|
188
|
-
setSuccessMessage('Sandbox Cortex AI model selection cleared.');
|
|
189
|
-
setTimeout(() => setSuccessMessage(null), 3000);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
console.error('Failed to clear sandbox model', error);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
if (!mounted) {
|
|
196
|
-
return null; // Avoid hydration mismatch
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const selectedModelIsInCatalog = compatibleModels.some(
|
|
200
|
-
(model) => model.id === sandboxModel?.modelId
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
const modelOptions =
|
|
204
|
-
sandboxModel && !selectedModelIsInCatalog
|
|
205
|
-
? [
|
|
206
|
-
{
|
|
207
|
-
context_length: sandboxModel.contextLength,
|
|
208
|
-
created: null,
|
|
209
|
-
expirationDate: null,
|
|
210
|
-
id: sandboxModel.modelId,
|
|
211
|
-
name: `${sandboxModel.name} (saved)`,
|
|
212
|
-
pricing: sandboxModel.pricing,
|
|
213
|
-
supportedParameters: sandboxModel.supportedParameters,
|
|
214
|
-
},
|
|
215
|
-
...compatibleModels,
|
|
216
|
-
]
|
|
217
|
-
: compatibleModels;
|
|
218
|
-
|
|
219
|
-
const canSelectModel = !!sandboxKey && compatibleModels.length > 0;
|
|
220
|
-
const maskedSandboxKey = sandboxKey ? getMaskedKey(sandboxKey) : null;
|
|
221
|
-
const isKeyDirty = inputValue.trim().length > 0;
|
|
222
|
-
const isModelDirty = modelInput !== (sandboxModel?.modelId || '');
|
|
223
|
-
|
|
224
|
-
const searchableOptions = modelOptions.map((model) => ({
|
|
225
|
-
value: model.id,
|
|
226
|
-
label: model.name,
|
|
227
|
-
description: `${model.id} - ${formatModelPricing(model.pricing as any)}`,
|
|
228
|
-
}));
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<div className="mx-auto w-full max-w-6xl space-y-6 px-6 py-8">
|
|
232
|
-
<div>
|
|
233
|
-
<div className="flex items-center gap-3">
|
|
234
|
-
<BrainCircuit className="h-7 w-7 text-primary" />
|
|
235
|
-
<h1 className="text-2xl font-semibold">NextBlock Cortex AI (Sandbox)</h1>
|
|
236
|
-
</div>
|
|
237
|
-
<p className="mt-2 text-sm text-muted-foreground">
|
|
238
|
-
Manage premium activation and the OpenRouter key used by Cortex AI in your local browser environment.
|
|
239
|
-
</p>
|
|
240
|
-
</div>
|
|
241
|
-
|
|
242
|
-
{successMessage && (
|
|
243
|
-
<Alert variant="success">
|
|
244
|
-
<CheckCircle2 className="h-4 w-4" />
|
|
245
|
-
<AlertTitle>Saved</AlertTitle>
|
|
246
|
-
<AlertDescription>{successMessage}</AlertDescription>
|
|
247
|
-
</Alert>
|
|
248
|
-
)}
|
|
249
|
-
|
|
250
|
-
<Alert variant="warning" className="bg-amber-50 dark:bg-amber-950/30 text-amber-900 dark:text-amber-200 border-amber-200 dark:border-amber-800">
|
|
251
|
-
<Info className="h-4 w-4" />
|
|
252
|
-
<AlertTitle>Sandbox Environment Active</AlertTitle>
|
|
253
|
-
<AlertDescription>
|
|
254
|
-
Keys and model selections entered here are stored <strong>only in your private browser (localStorage)</strong>. They will not be saved to the database to prevent accidental leaks in the shared sandbox area.
|
|
255
|
-
</AlertDescription>
|
|
256
|
-
</Alert>
|
|
257
|
-
|
|
258
|
-
<div className="grid gap-4 md:grid-cols-4">
|
|
259
|
-
<Card>
|
|
260
|
-
<CardHeader>
|
|
261
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
262
|
-
<ShieldCheck className="h-4 w-4" />
|
|
263
|
-
Package
|
|
264
|
-
</CardTitle>
|
|
265
|
-
<CardDescription>Premium access</CardDescription>
|
|
266
|
-
</CardHeader>
|
|
267
|
-
<CardContent>
|
|
268
|
-
<Badge variant={isPackageActive ? 'default' : 'outline'}>
|
|
269
|
-
{isPackageActive ? 'Active' : 'Inactive'}
|
|
270
|
-
</Badge>
|
|
271
|
-
</CardContent>
|
|
272
|
-
</Card>
|
|
273
|
-
|
|
274
|
-
<Card>
|
|
275
|
-
<CardHeader>
|
|
276
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
277
|
-
<ServerCog className="h-4 w-4" />
|
|
278
|
-
Environment
|
|
279
|
-
</CardTitle>
|
|
280
|
-
<CardDescription>OPENROUTER_API_KEY</CardDescription>
|
|
281
|
-
</CardHeader>
|
|
282
|
-
<CardContent className="space-y-2">
|
|
283
|
-
<Badge variant={hasEnvOpenRouterKey ? 'default' : 'outline'}>
|
|
284
|
-
{hasEnvOpenRouterKey ? 'Configured' : 'Not set'}
|
|
285
|
-
</Badge>
|
|
286
|
-
{maskedEnvOpenRouterKey && (
|
|
287
|
-
<p className="font-mono text-xs text-muted-foreground">
|
|
288
|
-
{maskedEnvOpenRouterKey}
|
|
289
|
-
</p>
|
|
290
|
-
)}
|
|
291
|
-
</CardContent>
|
|
292
|
-
</Card>
|
|
293
|
-
|
|
294
|
-
<Card>
|
|
295
|
-
<CardHeader>
|
|
296
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
297
|
-
<KeyRound className="h-4 w-4" />
|
|
298
|
-
Sandbox BYOK
|
|
299
|
-
</CardTitle>
|
|
300
|
-
<CardDescription>Browser local storage</CardDescription>
|
|
301
|
-
</CardHeader>
|
|
302
|
-
<CardContent className="space-y-2">
|
|
303
|
-
<Badge variant={sandboxKey ? 'default' : 'outline'}>
|
|
304
|
-
{sandboxKey ? 'Stored' : 'Empty'}
|
|
305
|
-
</Badge>
|
|
306
|
-
{maskedSandboxKey && (
|
|
307
|
-
<p className="font-mono text-xs text-muted-foreground">
|
|
308
|
-
{maskedSandboxKey}
|
|
309
|
-
</p>
|
|
310
|
-
)}
|
|
311
|
-
</CardContent>
|
|
312
|
-
</Card>
|
|
313
|
-
|
|
314
|
-
<Card>
|
|
315
|
-
<CardHeader>
|
|
316
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
317
|
-
<Cpu className="h-4 w-4" />
|
|
318
|
-
Model
|
|
319
|
-
</CardTitle>
|
|
320
|
-
<CardDescription>Sandbox routing</CardDescription>
|
|
321
|
-
</CardHeader>
|
|
322
|
-
<CardContent className="space-y-2">
|
|
323
|
-
<Badge variant={sandboxModel ? 'default' : 'outline'}>
|
|
324
|
-
{sandboxModel ? 'Selected' : 'Free registry'}
|
|
325
|
-
</Badge>
|
|
326
|
-
{sandboxModel && (
|
|
327
|
-
<>
|
|
328
|
-
<p className="text-xs font-medium">{sandboxModel.name}</p>
|
|
329
|
-
<p className="break-all font-mono text-xs text-muted-foreground">
|
|
330
|
-
{sandboxModel.modelId}
|
|
331
|
-
</p>
|
|
332
|
-
</>
|
|
333
|
-
)}
|
|
334
|
-
</CardContent>
|
|
335
|
-
</Card>
|
|
336
|
-
</div>
|
|
337
|
-
|
|
338
|
-
{hasEnvOpenRouterKey && !sandboxKey && (
|
|
339
|
-
<Alert>
|
|
340
|
-
<ServerCog className="h-4 w-4" />
|
|
341
|
-
<AlertTitle>Sandbox free-model lock active</AlertTitle>
|
|
342
|
-
<AlertDescription>
|
|
343
|
-
Cortex AI will only use the three configured free OpenRouter models until a
|
|
344
|
-
sandbox BYOK is saved to your browser.
|
|
345
|
-
</AlertDescription>
|
|
346
|
-
</Alert>
|
|
347
|
-
)}
|
|
348
|
-
|
|
349
|
-
{hasEnvOpenRouterKey && !!sandboxKey && (
|
|
350
|
-
<Alert>
|
|
351
|
-
<KeyRound className="h-4 w-4" />
|
|
352
|
-
<AlertTitle>Sandbox BYOK active</AlertTitle>
|
|
353
|
-
<AlertDescription>
|
|
354
|
-
Cortex AI will use your browser's sandbox BYOK before the server environment key, so the
|
|
355
|
-
selected compatible OpenRouter model can run across the website for you.
|
|
356
|
-
</AlertDescription>
|
|
357
|
-
</Alert>
|
|
358
|
-
)}
|
|
359
|
-
|
|
360
|
-
<Card>
|
|
361
|
-
<CardHeader className="flex flex-row items-start justify-between">
|
|
362
|
-
<div>
|
|
363
|
-
<CardTitle className="text-lg">OpenRouter BYOK</CardTitle>
|
|
364
|
-
<CardDescription>
|
|
365
|
-
The saved key is stored locally in your browser. It is not uploaded or saved to the database.
|
|
366
|
-
</CardDescription>
|
|
367
|
-
</div>
|
|
368
|
-
{sandboxKey && (
|
|
369
|
-
<form onSubmit={(e) => { e.preventDefault(); handleClearKey(); }}>
|
|
370
|
-
<Button
|
|
371
|
-
type="submit"
|
|
372
|
-
variant="outline"
|
|
373
|
-
size="sm"
|
|
374
|
-
className="text-destructive hover:text-destructive"
|
|
375
|
-
>
|
|
376
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
377
|
-
Clear
|
|
378
|
-
</Button>
|
|
379
|
-
</form>
|
|
380
|
-
)}
|
|
381
|
-
</CardHeader>
|
|
382
|
-
<CardContent>
|
|
383
|
-
<form onSubmit={handleSaveKey}>
|
|
384
|
-
<div className="flex items-end gap-3">
|
|
385
|
-
<div className="flex-1 space-y-2">
|
|
386
|
-
<Label htmlFor="openrouter_api_key">OpenRouter API key</Label>
|
|
387
|
-
<Input
|
|
388
|
-
id="openrouter_api_key"
|
|
389
|
-
name="openrouter_api_key"
|
|
390
|
-
type="password"
|
|
391
|
-
autoComplete="off"
|
|
392
|
-
minLength={12}
|
|
393
|
-
placeholder={sandboxKey ? 'Enter new key to overwrite...' : 'sk-or-v1-...'}
|
|
394
|
-
value={inputValue}
|
|
395
|
-
onChange={(e) => setInputValue(e.target.value)}
|
|
396
|
-
required
|
|
397
|
-
/>
|
|
398
|
-
</div>
|
|
399
|
-
<Button type="submit" disabled={!isKeyDirty}>
|
|
400
|
-
{isKeyDirty ? (
|
|
401
|
-
<>
|
|
402
|
-
<KeyRound className="mr-2 h-4 w-4" />
|
|
403
|
-
Save Key to Browser
|
|
404
|
-
</>
|
|
405
|
-
) : (
|
|
406
|
-
<>
|
|
407
|
-
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
408
|
-
Saved
|
|
409
|
-
</>
|
|
410
|
-
)}
|
|
411
|
-
</Button>
|
|
412
|
-
</div>
|
|
413
|
-
</form>
|
|
414
|
-
</CardContent>
|
|
415
|
-
</Card>
|
|
416
|
-
|
|
417
|
-
<Card>
|
|
418
|
-
<CardHeader className="flex flex-row items-start justify-between">
|
|
419
|
-
<div>
|
|
420
|
-
<CardTitle className="text-lg">OpenRouter Model</CardTitle>
|
|
421
|
-
<CardDescription>
|
|
422
|
-
Sandbox BYOK can use compatible text models that support structured outputs and
|
|
423
|
-
tool calling.
|
|
424
|
-
</CardDescription>
|
|
425
|
-
</div>
|
|
426
|
-
{sandboxModel && (
|
|
427
|
-
<form onSubmit={(e) => { e.preventDefault(); handleClearModel(); }}>
|
|
428
|
-
<Button
|
|
429
|
-
type="submit"
|
|
430
|
-
variant="outline"
|
|
431
|
-
size="sm"
|
|
432
|
-
className="text-destructive hover:text-destructive"
|
|
433
|
-
>
|
|
434
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
435
|
-
Clear
|
|
436
|
-
</Button>
|
|
437
|
-
</form>
|
|
438
|
-
)}
|
|
439
|
-
</CardHeader>
|
|
440
|
-
<CardContent className="space-y-4">
|
|
441
|
-
{!sandboxKey && (
|
|
442
|
-
<Alert>
|
|
443
|
-
<KeyRound className="h-4 w-4" />
|
|
444
|
-
<AlertTitle>Sandbox BYOK required</AlertTitle>
|
|
445
|
-
<AlertDescription>
|
|
446
|
-
Save an OpenRouter key to your browser before choosing a paid model.
|
|
447
|
-
</AlertDescription>
|
|
448
|
-
</Alert>
|
|
449
|
-
)}
|
|
450
|
-
|
|
451
|
-
{modelCatalogError && (
|
|
452
|
-
<Alert variant="warning">
|
|
453
|
-
<AlertTriangle className="h-4 w-4" />
|
|
454
|
-
<AlertTitle>Model catalog unavailable</AlertTitle>
|
|
455
|
-
<AlertDescription>{modelCatalogError}</AlertDescription>
|
|
456
|
-
</Alert>
|
|
457
|
-
)}
|
|
458
|
-
|
|
459
|
-
<form onSubmit={handleSaveModel}>
|
|
460
|
-
<input type="hidden" name="openrouter_model_id" value={modelInput} />
|
|
461
|
-
|
|
462
|
-
<div className="flex items-end gap-3">
|
|
463
|
-
<div className="flex-1 space-y-2">
|
|
464
|
-
<Label htmlFor="openrouter_model_id_select">Model</Label>
|
|
465
|
-
<SearchableSelect
|
|
466
|
-
options={searchableOptions}
|
|
467
|
-
value={modelInput}
|
|
468
|
-
onChange={(val) => setModelInput(val)}
|
|
469
|
-
disabled={!canSelectModel}
|
|
470
|
-
placeholder="Select a compatible model..."
|
|
471
|
-
/>
|
|
472
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
473
|
-
{canSelectModel
|
|
474
|
-
? `${compatibleModels.length} compatible models available`
|
|
475
|
-
: 'Cortex AI will use the free registry until model selection is available.'}
|
|
476
|
-
</p>
|
|
477
|
-
</div>
|
|
478
|
-
<Button type="submit" disabled={!canSelectModel || !isModelDirty}>
|
|
479
|
-
{isModelDirty ? (
|
|
480
|
-
<>
|
|
481
|
-
<Cpu className="mr-2 h-4 w-4" />
|
|
482
|
-
Save Model to Browser
|
|
483
|
-
</>
|
|
484
|
-
) : (
|
|
485
|
-
<>
|
|
486
|
-
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
487
|
-
Saved
|
|
488
|
-
</>
|
|
489
|
-
)}
|
|
490
|
-
</Button>
|
|
491
|
-
</div>
|
|
492
|
-
</form>
|
|
493
|
-
</CardContent>
|
|
494
|
-
</Card>
|
|
495
|
-
</div>
|
|
496
|
-
);
|
|
497
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Alert,
|
|
6
|
+
AlertDescription,
|
|
7
|
+
AlertTitle,
|
|
8
|
+
Badge,
|
|
9
|
+
Button,
|
|
10
|
+
Card,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardDescription,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
Input,
|
|
16
|
+
Label,
|
|
17
|
+
SearchableSelect,
|
|
18
|
+
} from '@nextblock-cms/ui';
|
|
19
|
+
import {
|
|
20
|
+
AlertTriangle,
|
|
21
|
+
BrainCircuit,
|
|
22
|
+
CheckCircle2,
|
|
23
|
+
Cpu,
|
|
24
|
+
KeyRound,
|
|
25
|
+
ServerCog,
|
|
26
|
+
ShieldCheck,
|
|
27
|
+
Trash2,
|
|
28
|
+
Info,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
import {
|
|
31
|
+
createCortexAiStoredModelSelection,
|
|
32
|
+
type CortexAiStoredModelSelection,
|
|
33
|
+
} from '@nextblock-cms/cortex/client';
|
|
34
|
+
|
|
35
|
+
const CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE = 'cortex_ai_sandbox_openrouter_api_key';
|
|
36
|
+
const CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE = 'cortex_ai_sandbox_openrouter_model_selection';
|
|
37
|
+
const CORTEX_AI_SETTINGS_CHANGED_EVENT = 'nextblock:cortex-ai-settings-changed';
|
|
38
|
+
|
|
39
|
+
type SandboxCortexAiSettingsClientProps = {
|
|
40
|
+
compatibleModels: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
pricing: Record<string, string>;
|
|
44
|
+
context_length: number | null;
|
|
45
|
+
created?: number | null;
|
|
46
|
+
architecture?: {
|
|
47
|
+
modality?: string;
|
|
48
|
+
tokenizer?: string;
|
|
49
|
+
instruct_type?: string | null;
|
|
50
|
+
} | null;
|
|
51
|
+
description?: string;
|
|
52
|
+
top_provider?: {
|
|
53
|
+
max_completion_tokens?: number | null;
|
|
54
|
+
is_moderated?: boolean;
|
|
55
|
+
} | null;
|
|
56
|
+
}>;
|
|
57
|
+
isPackageActive: boolean;
|
|
58
|
+
hasEnvOpenRouterKey: boolean;
|
|
59
|
+
maskedEnvOpenRouterKey: string | null;
|
|
60
|
+
modelCatalogError: string | null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function formatModelPricing(pricing: Record<string, string>) {
|
|
64
|
+
const amount = Number(pricing.prompt);
|
|
65
|
+
const completionAmount = Number(pricing.completion);
|
|
66
|
+
|
|
67
|
+
if (!Number.isFinite(amount) || !Number.isFinite(completionAmount)) {
|
|
68
|
+
return 'Pricing varies';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (amount === 0 && completionAmount === 0) {
|
|
72
|
+
return 'Free';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const formatPrice = (val: number) => {
|
|
76
|
+
if (val === 0) return '$0';
|
|
77
|
+
const perMillion = val * 1_000_000;
|
|
78
|
+
return `$${perMillion < 0.01 ? perMillion.toFixed(4) : perMillion.toFixed(2)}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return `${formatPrice(amount)}/1M input - ${formatPrice(completionAmount)}/1M output`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getMaskedKey(key: string) {
|
|
85
|
+
if (key.length <= 8) return '****';
|
|
86
|
+
return `**** ${key.slice(-4)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function notifyCortexAiSettingsChanged() {
|
|
90
|
+
window.dispatchEvent(new Event(CORTEX_AI_SETTINGS_CHANGED_EVENT));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function SandboxCortexAiSettingsClient({
|
|
94
|
+
compatibleModels,
|
|
95
|
+
isPackageActive,
|
|
96
|
+
hasEnvOpenRouterKey,
|
|
97
|
+
maskedEnvOpenRouterKey,
|
|
98
|
+
modelCatalogError,
|
|
99
|
+
}: SandboxCortexAiSettingsClientProps) {
|
|
100
|
+
const [mounted, setMounted] = useState(false);
|
|
101
|
+
const [sandboxKey, setSandboxKey] = useState<string | null>(null);
|
|
102
|
+
const [sandboxModel, setSandboxModel] = useState<CortexAiStoredModelSelection | null>(null);
|
|
103
|
+
const [inputValue, setInputValue] = useState('');
|
|
104
|
+
const [modelInput, setModelInput] = useState<string>('');
|
|
105
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
try {
|
|
109
|
+
const storedKey = window.localStorage.getItem(CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE);
|
|
110
|
+
if (storedKey) {
|
|
111
|
+
setSandboxKey(storedKey);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const storedModel = window.localStorage.getItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE);
|
|
115
|
+
if (storedModel) {
|
|
116
|
+
const parsed = JSON.parse(storedModel) as CortexAiStoredModelSelection;
|
|
117
|
+
setSandboxModel(parsed);
|
|
118
|
+
setModelInput(parsed.modelId);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Failed to read Cortex AI sandbox settings from localStorage', error);
|
|
122
|
+
}
|
|
123
|
+
setMounted(true);
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const handleSaveKey = (e: React.FormEvent) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
const key = inputValue.trim();
|
|
129
|
+
if (!key) return;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
window.localStorage.setItem(CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE, key);
|
|
133
|
+
setSandboxKey(key);
|
|
134
|
+
setInputValue('');
|
|
135
|
+
notifyCortexAiSettingsChanged();
|
|
136
|
+
setSuccessMessage('Sandbox OpenRouter key saved to your browser.');
|
|
137
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('Failed to save sandbox key', error);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleClearKey = () => {
|
|
144
|
+
try {
|
|
145
|
+
window.localStorage.removeItem(CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE);
|
|
146
|
+
window.localStorage.removeItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE);
|
|
147
|
+
setSandboxKey(null);
|
|
148
|
+
setSandboxModel(null);
|
|
149
|
+
setModelInput('');
|
|
150
|
+
notifyCortexAiSettingsChanged();
|
|
151
|
+
setSuccessMessage('Sandbox OpenRouter key cleared from your browser.');
|
|
152
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Failed to clear sandbox key', error);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const handleSaveModel = (e: React.FormEvent<HTMLFormElement>) => {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
const formData = new FormData(e.currentTarget);
|
|
161
|
+
const modelId = String(formData.get('openrouter_model_id') || '').trim();
|
|
162
|
+
if (!modelId) return;
|
|
163
|
+
|
|
164
|
+
const selectedModel = compatibleModels.find((m) => m.id === modelId);
|
|
165
|
+
if (!selectedModel) return;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Map to the shape createCortexAiStoredModelSelection expects, or something similar.
|
|
169
|
+
// createCortexAiStoredModelSelection needs a specific shape but we can mimic it.
|
|
170
|
+
// We pass the raw model data to it.
|
|
171
|
+
const storedSelection = createCortexAiStoredModelSelection(selectedModel as any);
|
|
172
|
+
window.localStorage.setItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE, JSON.stringify(storedSelection));
|
|
173
|
+
setSandboxModel(storedSelection);
|
|
174
|
+
notifyCortexAiSettingsChanged();
|
|
175
|
+
setSuccessMessage('Sandbox Cortex AI model selection saved to your browser.');
|
|
176
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('Failed to save sandbox model', error);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleClearModel = () => {
|
|
183
|
+
try {
|
|
184
|
+
window.localStorage.removeItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE);
|
|
185
|
+
setSandboxModel(null);
|
|
186
|
+
setModelInput('');
|
|
187
|
+
notifyCortexAiSettingsChanged();
|
|
188
|
+
setSuccessMessage('Sandbox Cortex AI model selection cleared.');
|
|
189
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Failed to clear sandbox model', error);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (!mounted) {
|
|
196
|
+
return null; // Avoid hydration mismatch
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const selectedModelIsInCatalog = compatibleModels.some(
|
|
200
|
+
(model) => model.id === sandboxModel?.modelId
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const modelOptions =
|
|
204
|
+
sandboxModel && !selectedModelIsInCatalog
|
|
205
|
+
? [
|
|
206
|
+
{
|
|
207
|
+
context_length: sandboxModel.contextLength,
|
|
208
|
+
created: null,
|
|
209
|
+
expirationDate: null,
|
|
210
|
+
id: sandboxModel.modelId,
|
|
211
|
+
name: `${sandboxModel.name} (saved)`,
|
|
212
|
+
pricing: sandboxModel.pricing,
|
|
213
|
+
supportedParameters: sandboxModel.supportedParameters,
|
|
214
|
+
},
|
|
215
|
+
...compatibleModels,
|
|
216
|
+
]
|
|
217
|
+
: compatibleModels;
|
|
218
|
+
|
|
219
|
+
const canSelectModel = !!sandboxKey && compatibleModels.length > 0;
|
|
220
|
+
const maskedSandboxKey = sandboxKey ? getMaskedKey(sandboxKey) : null;
|
|
221
|
+
const isKeyDirty = inputValue.trim().length > 0;
|
|
222
|
+
const isModelDirty = modelInput !== (sandboxModel?.modelId || '');
|
|
223
|
+
|
|
224
|
+
const searchableOptions = modelOptions.map((model) => ({
|
|
225
|
+
value: model.id,
|
|
226
|
+
label: model.name,
|
|
227
|
+
description: `${model.id} - ${formatModelPricing(model.pricing as any)}`,
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="mx-auto w-full max-w-6xl space-y-6 px-6 py-8">
|
|
232
|
+
<div>
|
|
233
|
+
<div className="flex items-center gap-3">
|
|
234
|
+
<BrainCircuit className="h-7 w-7 text-primary" />
|
|
235
|
+
<h1 className="text-2xl font-semibold">NextBlock Cortex AI (Sandbox)</h1>
|
|
236
|
+
</div>
|
|
237
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
238
|
+
Manage premium activation and the OpenRouter key used by Cortex AI in your local browser environment.
|
|
239
|
+
</p>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{successMessage && (
|
|
243
|
+
<Alert variant="success">
|
|
244
|
+
<CheckCircle2 className="h-4 w-4" />
|
|
245
|
+
<AlertTitle>Saved</AlertTitle>
|
|
246
|
+
<AlertDescription>{successMessage}</AlertDescription>
|
|
247
|
+
</Alert>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
<Alert variant="warning" className="bg-amber-50 dark:bg-amber-950/30 text-amber-900 dark:text-amber-200 border-amber-200 dark:border-amber-800">
|
|
251
|
+
<Info className="h-4 w-4" />
|
|
252
|
+
<AlertTitle>Sandbox Environment Active</AlertTitle>
|
|
253
|
+
<AlertDescription>
|
|
254
|
+
Keys and model selections entered here are stored <strong>only in your private browser (localStorage)</strong>. They will not be saved to the database to prevent accidental leaks in the shared sandbox area.
|
|
255
|
+
</AlertDescription>
|
|
256
|
+
</Alert>
|
|
257
|
+
|
|
258
|
+
<div className="grid gap-4 md:grid-cols-4">
|
|
259
|
+
<Card>
|
|
260
|
+
<CardHeader>
|
|
261
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
262
|
+
<ShieldCheck className="h-4 w-4" />
|
|
263
|
+
Package
|
|
264
|
+
</CardTitle>
|
|
265
|
+
<CardDescription>Premium access</CardDescription>
|
|
266
|
+
</CardHeader>
|
|
267
|
+
<CardContent>
|
|
268
|
+
<Badge variant={isPackageActive ? 'default' : 'outline'}>
|
|
269
|
+
{isPackageActive ? 'Active' : 'Inactive'}
|
|
270
|
+
</Badge>
|
|
271
|
+
</CardContent>
|
|
272
|
+
</Card>
|
|
273
|
+
|
|
274
|
+
<Card>
|
|
275
|
+
<CardHeader>
|
|
276
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
277
|
+
<ServerCog className="h-4 w-4" />
|
|
278
|
+
Environment
|
|
279
|
+
</CardTitle>
|
|
280
|
+
<CardDescription>OPENROUTER_API_KEY</CardDescription>
|
|
281
|
+
</CardHeader>
|
|
282
|
+
<CardContent className="space-y-2">
|
|
283
|
+
<Badge variant={hasEnvOpenRouterKey ? 'default' : 'outline'}>
|
|
284
|
+
{hasEnvOpenRouterKey ? 'Configured' : 'Not set'}
|
|
285
|
+
</Badge>
|
|
286
|
+
{maskedEnvOpenRouterKey && (
|
|
287
|
+
<p className="font-mono text-xs text-muted-foreground">
|
|
288
|
+
{maskedEnvOpenRouterKey}
|
|
289
|
+
</p>
|
|
290
|
+
)}
|
|
291
|
+
</CardContent>
|
|
292
|
+
</Card>
|
|
293
|
+
|
|
294
|
+
<Card>
|
|
295
|
+
<CardHeader>
|
|
296
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
297
|
+
<KeyRound className="h-4 w-4" />
|
|
298
|
+
Sandbox BYOK
|
|
299
|
+
</CardTitle>
|
|
300
|
+
<CardDescription>Browser local storage</CardDescription>
|
|
301
|
+
</CardHeader>
|
|
302
|
+
<CardContent className="space-y-2">
|
|
303
|
+
<Badge variant={sandboxKey ? 'default' : 'outline'}>
|
|
304
|
+
{sandboxKey ? 'Stored' : 'Empty'}
|
|
305
|
+
</Badge>
|
|
306
|
+
{maskedSandboxKey && (
|
|
307
|
+
<p className="font-mono text-xs text-muted-foreground">
|
|
308
|
+
{maskedSandboxKey}
|
|
309
|
+
</p>
|
|
310
|
+
)}
|
|
311
|
+
</CardContent>
|
|
312
|
+
</Card>
|
|
313
|
+
|
|
314
|
+
<Card>
|
|
315
|
+
<CardHeader>
|
|
316
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
317
|
+
<Cpu className="h-4 w-4" />
|
|
318
|
+
Model
|
|
319
|
+
</CardTitle>
|
|
320
|
+
<CardDescription>Sandbox routing</CardDescription>
|
|
321
|
+
</CardHeader>
|
|
322
|
+
<CardContent className="space-y-2">
|
|
323
|
+
<Badge variant={sandboxModel ? 'default' : 'outline'}>
|
|
324
|
+
{sandboxModel ? 'Selected' : 'Free registry'}
|
|
325
|
+
</Badge>
|
|
326
|
+
{sandboxModel && (
|
|
327
|
+
<>
|
|
328
|
+
<p className="text-xs font-medium">{sandboxModel.name}</p>
|
|
329
|
+
<p className="break-all font-mono text-xs text-muted-foreground">
|
|
330
|
+
{sandboxModel.modelId}
|
|
331
|
+
</p>
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</CardContent>
|
|
335
|
+
</Card>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{hasEnvOpenRouterKey && !sandboxKey && (
|
|
339
|
+
<Alert>
|
|
340
|
+
<ServerCog className="h-4 w-4" />
|
|
341
|
+
<AlertTitle>Sandbox free-model lock active</AlertTitle>
|
|
342
|
+
<AlertDescription>
|
|
343
|
+
Cortex AI will only use the three configured free OpenRouter models until a
|
|
344
|
+
sandbox BYOK is saved to your browser.
|
|
345
|
+
</AlertDescription>
|
|
346
|
+
</Alert>
|
|
347
|
+
)}
|
|
348
|
+
|
|
349
|
+
{hasEnvOpenRouterKey && !!sandboxKey && (
|
|
350
|
+
<Alert>
|
|
351
|
+
<KeyRound className="h-4 w-4" />
|
|
352
|
+
<AlertTitle>Sandbox BYOK active</AlertTitle>
|
|
353
|
+
<AlertDescription>
|
|
354
|
+
Cortex AI will use your browser's sandbox BYOK before the server environment key, so the
|
|
355
|
+
selected compatible OpenRouter model can run across the website for you.
|
|
356
|
+
</AlertDescription>
|
|
357
|
+
</Alert>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
<Card>
|
|
361
|
+
<CardHeader className="flex flex-row items-start justify-between">
|
|
362
|
+
<div>
|
|
363
|
+
<CardTitle className="text-lg">OpenRouter BYOK</CardTitle>
|
|
364
|
+
<CardDescription>
|
|
365
|
+
The saved key is stored locally in your browser. It is not uploaded or saved to the database.
|
|
366
|
+
</CardDescription>
|
|
367
|
+
</div>
|
|
368
|
+
{sandboxKey && (
|
|
369
|
+
<form onSubmit={(e) => { e.preventDefault(); handleClearKey(); }}>
|
|
370
|
+
<Button
|
|
371
|
+
type="submit"
|
|
372
|
+
variant="outline"
|
|
373
|
+
size="sm"
|
|
374
|
+
className="text-destructive hover:text-destructive"
|
|
375
|
+
>
|
|
376
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
377
|
+
Clear
|
|
378
|
+
</Button>
|
|
379
|
+
</form>
|
|
380
|
+
)}
|
|
381
|
+
</CardHeader>
|
|
382
|
+
<CardContent>
|
|
383
|
+
<form onSubmit={handleSaveKey}>
|
|
384
|
+
<div className="flex items-end gap-3">
|
|
385
|
+
<div className="flex-1 space-y-2">
|
|
386
|
+
<Label htmlFor="openrouter_api_key">OpenRouter API key</Label>
|
|
387
|
+
<Input
|
|
388
|
+
id="openrouter_api_key"
|
|
389
|
+
name="openrouter_api_key"
|
|
390
|
+
type="password"
|
|
391
|
+
autoComplete="off"
|
|
392
|
+
minLength={12}
|
|
393
|
+
placeholder={sandboxKey ? 'Enter new key to overwrite...' : 'sk-or-v1-...'}
|
|
394
|
+
value={inputValue}
|
|
395
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
396
|
+
required
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
<Button type="submit" disabled={!isKeyDirty}>
|
|
400
|
+
{isKeyDirty ? (
|
|
401
|
+
<>
|
|
402
|
+
<KeyRound className="mr-2 h-4 w-4" />
|
|
403
|
+
Save Key to Browser
|
|
404
|
+
</>
|
|
405
|
+
) : (
|
|
406
|
+
<>
|
|
407
|
+
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
408
|
+
Saved
|
|
409
|
+
</>
|
|
410
|
+
)}
|
|
411
|
+
</Button>
|
|
412
|
+
</div>
|
|
413
|
+
</form>
|
|
414
|
+
</CardContent>
|
|
415
|
+
</Card>
|
|
416
|
+
|
|
417
|
+
<Card>
|
|
418
|
+
<CardHeader className="flex flex-row items-start justify-between">
|
|
419
|
+
<div>
|
|
420
|
+
<CardTitle className="text-lg">OpenRouter Model</CardTitle>
|
|
421
|
+
<CardDescription>
|
|
422
|
+
Sandbox BYOK can use compatible text models that support structured outputs and
|
|
423
|
+
tool calling.
|
|
424
|
+
</CardDescription>
|
|
425
|
+
</div>
|
|
426
|
+
{sandboxModel && (
|
|
427
|
+
<form onSubmit={(e) => { e.preventDefault(); handleClearModel(); }}>
|
|
428
|
+
<Button
|
|
429
|
+
type="submit"
|
|
430
|
+
variant="outline"
|
|
431
|
+
size="sm"
|
|
432
|
+
className="text-destructive hover:text-destructive"
|
|
433
|
+
>
|
|
434
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
435
|
+
Clear
|
|
436
|
+
</Button>
|
|
437
|
+
</form>
|
|
438
|
+
)}
|
|
439
|
+
</CardHeader>
|
|
440
|
+
<CardContent className="space-y-4">
|
|
441
|
+
{!sandboxKey && (
|
|
442
|
+
<Alert>
|
|
443
|
+
<KeyRound className="h-4 w-4" />
|
|
444
|
+
<AlertTitle>Sandbox BYOK required</AlertTitle>
|
|
445
|
+
<AlertDescription>
|
|
446
|
+
Save an OpenRouter key to your browser before choosing a paid model.
|
|
447
|
+
</AlertDescription>
|
|
448
|
+
</Alert>
|
|
449
|
+
)}
|
|
450
|
+
|
|
451
|
+
{modelCatalogError && (
|
|
452
|
+
<Alert variant="warning">
|
|
453
|
+
<AlertTriangle className="h-4 w-4" />
|
|
454
|
+
<AlertTitle>Model catalog unavailable</AlertTitle>
|
|
455
|
+
<AlertDescription>{modelCatalogError}</AlertDescription>
|
|
456
|
+
</Alert>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
<form onSubmit={handleSaveModel}>
|
|
460
|
+
<input type="hidden" name="openrouter_model_id" value={modelInput} />
|
|
461
|
+
|
|
462
|
+
<div className="flex items-end gap-3">
|
|
463
|
+
<div className="flex-1 space-y-2">
|
|
464
|
+
<Label htmlFor="openrouter_model_id_select">Model</Label>
|
|
465
|
+
<SearchableSelect
|
|
466
|
+
options={searchableOptions}
|
|
467
|
+
value={modelInput}
|
|
468
|
+
onChange={(val) => setModelInput(val)}
|
|
469
|
+
disabled={!canSelectModel}
|
|
470
|
+
placeholder="Select a compatible model..."
|
|
471
|
+
/>
|
|
472
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
473
|
+
{canSelectModel
|
|
474
|
+
? `${compatibleModels.length} compatible models available`
|
|
475
|
+
: 'Cortex AI will use the free registry until model selection is available.'}
|
|
476
|
+
</p>
|
|
477
|
+
</div>
|
|
478
|
+
<Button type="submit" disabled={!canSelectModel || !isModelDirty}>
|
|
479
|
+
{isModelDirty ? (
|
|
480
|
+
<>
|
|
481
|
+
<Cpu className="mr-2 h-4 w-4" />
|
|
482
|
+
Save Model to Browser
|
|
483
|
+
</>
|
|
484
|
+
) : (
|
|
485
|
+
<>
|
|
486
|
+
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
487
|
+
Saved
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
490
|
+
</Button>
|
|
491
|
+
</div>
|
|
492
|
+
</form>
|
|
493
|
+
</CardContent>
|
|
494
|
+
</Card>
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
}
|