create-nextblock 0.12.16 → 0.13.2
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/package.json +1 -1
- package/templates/nextblock-template/app/actions/interactions.ts +27 -4
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +287 -48
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +238 -209
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +103 -8
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +31 -2
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +37 -15
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +26 -15
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +123 -46
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +8 -1
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +62 -22
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +40 -2
- package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -0
- package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +138 -71
- package/templates/nextblock-template/app/cms/media/import-external-image.ts +289 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +13 -10
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +14 -3
- package/templates/nextblock-template/app/cms/pages/actions.ts +59 -6
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +21 -11
- package/templates/nextblock-template/app/cms/posts/actions.ts +45 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +11 -9
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +463 -227
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +220 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +20 -1
- package/templates/nextblock-template/app/cms/users/actions.ts +69 -0
- package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +4 -1
- package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +12 -3
- package/templates/nextblock-template/app/lib/homepage.ts +36 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +13 -6
- package/templates/nextblock-template/app/page.tsx +55 -12
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +56 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +60 -30
- package/templates/nextblock-template/components/blocks/renderers/StockPhotoCredit.tsx +167 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +94 -6
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +7 -1
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -3
- package/templates/nextblock-template/lib/search/server.ts +11 -1
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +82 -72
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +5 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx
CHANGED
|
@@ -20,19 +20,27 @@ import {
|
|
|
20
20
|
AlertTriangle,
|
|
21
21
|
Brain,
|
|
22
22
|
CheckCircle2,
|
|
23
|
+
ChevronDown,
|
|
24
|
+
ChevronRight,
|
|
23
25
|
Cpu,
|
|
26
|
+
ImageIcon,
|
|
24
27
|
KeyRound,
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
RotateCcw,
|
|
29
|
+
SlidersHorizontal,
|
|
27
30
|
Trash2,
|
|
28
31
|
} from 'lucide-react';
|
|
29
32
|
|
|
30
33
|
import type { CortexAiStoredModelSelection } from '@nextblock-cms/cortex/client';
|
|
34
|
+
import type { CortexAiAgentSettings } from '@nextblock-cms/cortex';
|
|
31
35
|
import {
|
|
32
36
|
clearCortexAiModelSelectionAction,
|
|
33
37
|
clearOpenRouterApiKeyAction,
|
|
38
|
+
clearStockPhotoKeysAction,
|
|
39
|
+
resetCortexAiAgentSettingsAction,
|
|
40
|
+
saveCortexAiAgentSettingsAction,
|
|
34
41
|
saveCortexAiModelSelectionAction,
|
|
35
42
|
saveOpenRouterApiKeyAction,
|
|
43
|
+
saveStockPhotoKeysAction,
|
|
36
44
|
} from './actions';
|
|
37
45
|
|
|
38
46
|
const CORTEX_AI_SETTINGS_CHANGED_EVENT = 'nextblock:cortex-ai-settings-changed';
|
|
@@ -54,6 +62,16 @@ type StoredCortexAiSettingsClientProps = {
|
|
|
54
62
|
selectedModelUpdatedAt: string | null;
|
|
55
63
|
hasEncryptionKey: boolean;
|
|
56
64
|
modelCatalogError: string | null;
|
|
65
|
+
activeStockProvider: 'pexels' | 'unsplash' | null;
|
|
66
|
+
hasStoredPexelsKey: boolean;
|
|
67
|
+
maskedStoredPexelsKey: string | null;
|
|
68
|
+
hasStoredUnsplashKey: boolean;
|
|
69
|
+
maskedStoredUnsplashKey: string | null;
|
|
70
|
+
hasEnvPexelsKey: boolean;
|
|
71
|
+
hasEnvUnsplashKey: boolean;
|
|
72
|
+
stockKeysUpdatedAt: string | null;
|
|
73
|
+
unsplashAppName: string | null;
|
|
74
|
+
agentSettings: CortexAiAgentSettings;
|
|
57
75
|
successMessage?: string;
|
|
58
76
|
errorMessage?: string;
|
|
59
77
|
};
|
|
@@ -78,6 +96,31 @@ function notifyCortexAiSettingsChanged() {
|
|
|
78
96
|
window.dispatchEvent(new Event(CORTEX_AI_SETTINGS_CHANGED_EVENT));
|
|
79
97
|
}
|
|
80
98
|
|
|
99
|
+
function StatusPill({
|
|
100
|
+
label,
|
|
101
|
+
value,
|
|
102
|
+
active,
|
|
103
|
+
detail,
|
|
104
|
+
}: {
|
|
105
|
+
label: string;
|
|
106
|
+
value: string;
|
|
107
|
+
active: boolean;
|
|
108
|
+
detail?: string | null;
|
|
109
|
+
}) {
|
|
110
|
+
return (
|
|
111
|
+
<div className="flex min-w-[8rem] flex-col gap-1 rounded-md border bg-muted/30 px-3 py-2">
|
|
112
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
113
|
+
{label}
|
|
114
|
+
</span>
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<span className={`h-2 w-2 rounded-full ${active ? 'bg-emerald-500' : 'bg-muted-foreground/40'}`} />
|
|
117
|
+
<span className="text-sm font-medium">{value}</span>
|
|
118
|
+
</div>
|
|
119
|
+
{detail && <span className="truncate font-mono text-[11px] text-muted-foreground">{detail}</span>}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
81
124
|
export function StoredCortexAiSettingsClient({
|
|
82
125
|
compatibleModels,
|
|
83
126
|
isPackageActive,
|
|
@@ -90,18 +133,38 @@ export function StoredCortexAiSettingsClient({
|
|
|
90
133
|
selectedModelUpdatedAt,
|
|
91
134
|
hasEncryptionKey,
|
|
92
135
|
modelCatalogError,
|
|
136
|
+
activeStockProvider,
|
|
137
|
+
hasStoredPexelsKey,
|
|
138
|
+
maskedStoredPexelsKey,
|
|
139
|
+
hasStoredUnsplashKey,
|
|
140
|
+
maskedStoredUnsplashKey,
|
|
141
|
+
hasEnvPexelsKey,
|
|
142
|
+
hasEnvUnsplashKey,
|
|
143
|
+
unsplashAppName,
|
|
144
|
+
agentSettings,
|
|
93
145
|
successMessage,
|
|
94
146
|
errorMessage,
|
|
95
147
|
}: StoredCortexAiSettingsClientProps) {
|
|
96
148
|
const [apiKeyInput, setApiKeyInput] = useState('');
|
|
97
149
|
const [modelInput, setModelInput] = useState<string>(selectedModel?.modelId || '');
|
|
150
|
+
const [pexelsInput, setPexelsInput] = useState('');
|
|
151
|
+
const [unsplashInput, setUnsplashInput] = useState('');
|
|
152
|
+
const [appNameInput, setAppNameInput] = useState(unsplashAppName || '');
|
|
153
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
154
|
+
const [unlimitedTokens, setUnlimitedTokens] = useState(agentSettings.maxOutputTokens === null);
|
|
155
|
+
const [maxTokensInput, setMaxTokensInput] = useState(String(agentSettings.maxOutputTokens ?? 16000));
|
|
156
|
+
const [maxStepsInput, setMaxStepsInput] = useState(String(agentSettings.maxSteps));
|
|
157
|
+
const [temperatureInput, setTemperatureInput] = useState(String(agentSettings.temperature));
|
|
158
|
+
const [timeoutInput, setTimeoutInput] = useState(String(Math.round(agentSettings.responseTimeoutMs / 1000)));
|
|
98
159
|
|
|
99
160
|
const isKeyDirty = apiKeyInput.trim().length > 0;
|
|
100
161
|
const isModelDirty = modelInput !== (selectedModel?.modelId || '');
|
|
162
|
+
const isStockDirty =
|
|
163
|
+
pexelsInput.trim().length > 0 ||
|
|
164
|
+
unsplashInput.trim().length > 0 ||
|
|
165
|
+
appNameInput.trim() !== (unsplashAppName || '');
|
|
101
166
|
|
|
102
|
-
const selectedModelIsInCatalog = compatibleModels.some(
|
|
103
|
-
(model) => model.id === selectedModel?.modelId
|
|
104
|
-
);
|
|
167
|
+
const selectedModelIsInCatalog = compatibleModels.some((model) => model.id === selectedModel?.modelId);
|
|
105
168
|
const modelOptions =
|
|
106
169
|
selectedModel && !selectedModelIsInCatalog
|
|
107
170
|
? [
|
|
@@ -123,16 +186,29 @@ export function StoredCortexAiSettingsClient({
|
|
|
123
186
|
description: `${model.id} - ${formatModelPricing(model.pricing as any)}`,
|
|
124
187
|
}));
|
|
125
188
|
|
|
189
|
+
const keySourceValue = hasStoredOpenRouterKey ? 'Stored BYOK' : hasEnvOpenRouterKey ? 'Environment' : 'None';
|
|
190
|
+
// Ordered by preference (Pexels primary, Unsplash fallback). Configured = a
|
|
191
|
+
// stored DB key OR an env var for that provider.
|
|
192
|
+
const configuredStockProviders = (
|
|
193
|
+
[
|
|
194
|
+
hasStoredPexelsKey || hasEnvPexelsKey ? { name: 'Pexels', stored: hasStoredPexelsKey } : null,
|
|
195
|
+
hasStoredUnsplashKey || hasEnvUnsplashKey ? { name: 'Unsplash', stored: hasStoredUnsplashKey } : null,
|
|
196
|
+
].filter(Boolean) as Array<{ name: string; stored: boolean }>
|
|
197
|
+
);
|
|
198
|
+
const stockValue = configuredStockProviders.length > 0
|
|
199
|
+
? configuredStockProviders.map((provider) => provider.name).join(' + ')
|
|
200
|
+
: 'Off';
|
|
201
|
+
|
|
126
202
|
return (
|
|
127
|
-
<div className="mx-auto w-full max-w-
|
|
128
|
-
<div>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
<h1 className="text-
|
|
203
|
+
<div className="mx-auto w-full max-w-5xl space-y-4 px-4 py-6">
|
|
204
|
+
<div className="flex items-center gap-2.5">
|
|
205
|
+
<Brain className="h-6 w-6 text-primary" />
|
|
206
|
+
<div>
|
|
207
|
+
<h1 className="text-xl font-semibold leading-tight">NextBlock Cortex AI</h1>
|
|
208
|
+
<p className="text-xs text-muted-foreground">
|
|
209
|
+
Manage activation, the OpenRouter model key, and stock-photo providers.
|
|
210
|
+
</p>
|
|
132
211
|
</div>
|
|
133
|
-
<p className="mt-2 text-sm text-muted-foreground">
|
|
134
|
-
Manage premium activation and the OpenRouter key used by Cortex AI.
|
|
135
|
-
</p>
|
|
136
212
|
</div>
|
|
137
213
|
|
|
138
214
|
{successMessage && (
|
|
@@ -151,151 +227,56 @@ export function StoredCortexAiSettingsClient({
|
|
|
151
227
|
</Alert>
|
|
152
228
|
)}
|
|
153
229
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<Card>
|
|
171
|
-
<CardHeader>
|
|
172
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
173
|
-
<ServerCog className="h-4 w-4" />
|
|
174
|
-
Environment
|
|
175
|
-
</CardTitle>
|
|
176
|
-
<CardDescription>OPENROUTER_API_KEY</CardDescription>
|
|
177
|
-
</CardHeader>
|
|
178
|
-
<CardContent className="space-y-2">
|
|
179
|
-
<Badge variant={hasEnvOpenRouterKey ? 'default' : 'outline'}>
|
|
180
|
-
{hasEnvOpenRouterKey ? 'Configured' : 'Not set'}
|
|
181
|
-
</Badge>
|
|
182
|
-
{maskedEnvOpenRouterKey && (
|
|
183
|
-
<p className="font-mono text-xs text-muted-foreground">
|
|
184
|
-
{maskedEnvOpenRouterKey}
|
|
185
|
-
</p>
|
|
186
|
-
)}
|
|
187
|
-
</CardContent>
|
|
188
|
-
</Card>
|
|
189
|
-
|
|
190
|
-
<Card>
|
|
191
|
-
<CardHeader>
|
|
192
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
193
|
-
<KeyRound className="h-4 w-4" />
|
|
194
|
-
Stored BYOK
|
|
195
|
-
</CardTitle>
|
|
196
|
-
<CardDescription>Encrypted database key</CardDescription>
|
|
197
|
-
</CardHeader>
|
|
198
|
-
<CardContent className="space-y-2">
|
|
199
|
-
<Badge variant={hasStoredOpenRouterKey ? 'default' : 'outline'}>
|
|
200
|
-
{hasStoredOpenRouterKey ? 'Stored' : 'Empty'}
|
|
201
|
-
</Badge>
|
|
202
|
-
{maskedStoredOpenRouterKey && (
|
|
203
|
-
<p className="font-mono text-xs text-muted-foreground">
|
|
204
|
-
{maskedStoredOpenRouterKey}
|
|
205
|
-
</p>
|
|
206
|
-
)}
|
|
207
|
-
{storedKeyUpdatedAt && (
|
|
208
|
-
<p className="text-xs text-muted-foreground">{storedKeyUpdatedAt}</p>
|
|
209
|
-
)}
|
|
210
|
-
</CardContent>
|
|
211
|
-
</Card>
|
|
212
|
-
|
|
213
|
-
<Card>
|
|
214
|
-
<CardHeader>
|
|
215
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
216
|
-
<Cpu className="h-4 w-4" />
|
|
217
|
-
Model
|
|
218
|
-
</CardTitle>
|
|
219
|
-
<CardDescription>Stored BYOK routing</CardDescription>
|
|
220
|
-
</CardHeader>
|
|
221
|
-
<CardContent className="space-y-2">
|
|
222
|
-
<Badge variant={selectedModel ? 'default' : 'outline'}>
|
|
223
|
-
{selectedModel ? 'Selected' : 'Free registry'}
|
|
224
|
-
</Badge>
|
|
225
|
-
{selectedModel && (
|
|
226
|
-
<>
|
|
227
|
-
<p className="text-xs font-medium">{selectedModel.name}</p>
|
|
228
|
-
<p className="break-all font-mono text-xs text-muted-foreground">
|
|
229
|
-
{selectedModel.modelId}
|
|
230
|
-
</p>
|
|
231
|
-
{selectedModelUpdatedAt && (
|
|
232
|
-
<p className="text-xs text-muted-foreground">{selectedModelUpdatedAt}</p>
|
|
233
|
-
)}
|
|
234
|
-
</>
|
|
235
|
-
)}
|
|
236
|
-
</CardContent>
|
|
237
|
-
</Card>
|
|
230
|
+
{/* Compact status strip */}
|
|
231
|
+
<div className="flex flex-wrap gap-2">
|
|
232
|
+
<StatusPill label="Package" value={isPackageActive ? 'Active' : 'Inactive'} active={isPackageActive} />
|
|
233
|
+
<StatusPill
|
|
234
|
+
label="Model key"
|
|
235
|
+
value={keySourceValue}
|
|
236
|
+
active={hasStoredOpenRouterKey || hasEnvOpenRouterKey}
|
|
237
|
+
detail={maskedStoredOpenRouterKey || maskedEnvOpenRouterKey}
|
|
238
|
+
/>
|
|
239
|
+
<StatusPill
|
|
240
|
+
label="Model"
|
|
241
|
+
value={selectedModel ? selectedModel.name : 'Free registry'}
|
|
242
|
+
active={Boolean(selectedModel)}
|
|
243
|
+
/>
|
|
244
|
+
<StatusPill label="Stock photos" value={stockValue} active={Boolean(activeStockProvider)} />
|
|
238
245
|
</div>
|
|
239
246
|
|
|
240
|
-
{hasEnvOpenRouterKey && !hasStoredOpenRouterKey && (
|
|
241
|
-
<Alert>
|
|
242
|
-
<ServerCog className="h-4 w-4" />
|
|
243
|
-
<AlertTitle>Sandbox free-model lock active</AlertTitle>
|
|
244
|
-
<AlertDescription>
|
|
245
|
-
Cortex AI will only use the three configured free OpenRouter models until a
|
|
246
|
-
stored BYOK is saved.
|
|
247
|
-
</AlertDescription>
|
|
248
|
-
</Alert>
|
|
249
|
-
)}
|
|
250
|
-
|
|
251
|
-
{hasEnvOpenRouterKey && hasStoredOpenRouterKey && (
|
|
252
|
-
<Alert>
|
|
253
|
-
<KeyRound className="h-4 w-4" />
|
|
254
|
-
<AlertTitle>Stored BYOK active</AlertTitle>
|
|
255
|
-
<AlertDescription>
|
|
256
|
-
Cortex AI will use the stored BYOK before the server environment key, so the
|
|
257
|
-
selected compatible OpenRouter model can run across the website.
|
|
258
|
-
</AlertDescription>
|
|
259
|
-
</Alert>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
247
|
{!hasEncryptionKey && (
|
|
263
248
|
<Alert variant="warning">
|
|
264
249
|
<AlertTriangle className="h-4 w-4" />
|
|
265
250
|
<AlertTitle>Encryption key missing</AlertTitle>
|
|
266
251
|
<AlertDescription>
|
|
267
|
-
Set CORTEX_AI_ENCRYPTION_KEY before saving
|
|
252
|
+
Set CORTEX_AI_ENCRYPTION_KEY (or rely on the Supabase service-role fallback) before saving keys here.
|
|
268
253
|
</AlertDescription>
|
|
269
254
|
</Alert>
|
|
270
255
|
)}
|
|
271
256
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<form action={saveOpenRouterApiKeyAction} onSubmit={notifyCortexAiSettingsChanged}>
|
|
296
|
-
<div className="flex items-end gap-3">
|
|
297
|
-
<div className="flex-1 space-y-2">
|
|
298
|
-
<Label htmlFor="openrouter_api_key">OpenRouter API key</Label>
|
|
257
|
+
{/* OpenRouter BYOK + Model side by side */}
|
|
258
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
259
|
+
<Card>
|
|
260
|
+
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
|
261
|
+
<div>
|
|
262
|
+
<CardTitle className="text-base">OpenRouter key</CardTitle>
|
|
263
|
+
<CardDescription className="text-xs">Encrypted, masked after saving.</CardDescription>
|
|
264
|
+
</div>
|
|
265
|
+
{hasStoredOpenRouterKey && (
|
|
266
|
+
<form action={clearOpenRouterApiKeyAction} onSubmit={notifyCortexAiSettingsChanged}>
|
|
267
|
+
<Button type="submit" variant="ghost" size="sm" className="h-7 text-destructive hover:text-destructive">
|
|
268
|
+
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
|
|
269
|
+
Clear
|
|
270
|
+
</Button>
|
|
271
|
+
</form>
|
|
272
|
+
)}
|
|
273
|
+
</CardHeader>
|
|
274
|
+
<CardContent className="pt-0">
|
|
275
|
+
<form action={saveOpenRouterApiKeyAction} onSubmit={notifyCortexAiSettingsChanged} className="flex items-end gap-2">
|
|
276
|
+
<div className="flex-1 space-y-1.5">
|
|
277
|
+
<Label htmlFor="openrouter_api_key" className="text-xs">
|
|
278
|
+
API key
|
|
279
|
+
</Label>
|
|
299
280
|
<Input
|
|
300
281
|
id="openrouter_api_key"
|
|
301
282
|
name="openrouter_api_key"
|
|
@@ -308,103 +289,358 @@ export function StoredCortexAiSettingsClient({
|
|
|
308
289
|
required
|
|
309
290
|
/>
|
|
310
291
|
</div>
|
|
311
|
-
<Button type="submit" disabled={!isKeyDirty}>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
<KeyRound className="mr-2 h-4 w-4" />
|
|
315
|
-
Save Key
|
|
316
|
-
</>
|
|
317
|
-
) : (
|
|
318
|
-
<>
|
|
319
|
-
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
320
|
-
Saved
|
|
321
|
-
</>
|
|
322
|
-
)}
|
|
292
|
+
<Button type="submit" disabled={!isKeyDirty} size="sm">
|
|
293
|
+
<KeyRound className="mr-1.5 h-3.5 w-3.5" />
|
|
294
|
+
Save
|
|
323
295
|
</Button>
|
|
296
|
+
</form>
|
|
297
|
+
</CardContent>
|
|
298
|
+
</Card>
|
|
299
|
+
|
|
300
|
+
<Card>
|
|
301
|
+
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
|
302
|
+
<div>
|
|
303
|
+
<CardTitle className="text-base">OpenRouter model</CardTitle>
|
|
304
|
+
<CardDescription className="text-xs">Needs a stored key; supports tools + structured output.</CardDescription>
|
|
324
305
|
</div>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
306
|
+
{selectedModel && (
|
|
307
|
+
<form action={clearCortexAiModelSelectionAction} onSubmit={notifyCortexAiSettingsChanged}>
|
|
308
|
+
<Button type="submit" variant="ghost" size="sm" className="h-7 text-destructive hover:text-destructive">
|
|
309
|
+
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
|
|
310
|
+
Clear
|
|
311
|
+
</Button>
|
|
312
|
+
</form>
|
|
313
|
+
)}
|
|
314
|
+
</CardHeader>
|
|
315
|
+
<CardContent className="space-y-2 pt-0">
|
|
316
|
+
{modelCatalogError && (
|
|
317
|
+
<Alert variant="warning">
|
|
318
|
+
<AlertTriangle className="h-4 w-4" />
|
|
319
|
+
<AlertTitle>Model catalog unavailable</AlertTitle>
|
|
320
|
+
<AlertDescription>{modelCatalogError}</AlertDescription>
|
|
321
|
+
</Alert>
|
|
322
|
+
)}
|
|
323
|
+
<form action={saveCortexAiModelSelectionAction} onSubmit={notifyCortexAiSettingsChanged} className="flex items-end gap-2">
|
|
324
|
+
<input type="hidden" name="openrouter_model_id" value={modelInput} />
|
|
325
|
+
<div className="flex-1 space-y-1.5">
|
|
326
|
+
<Label htmlFor="openrouter_model_id_select" className="text-xs">
|
|
327
|
+
Model
|
|
328
|
+
</Label>
|
|
329
|
+
<SearchableSelect
|
|
330
|
+
options={searchableOptions}
|
|
331
|
+
value={modelInput}
|
|
332
|
+
onChange={(val) => setModelInput(val)}
|
|
333
|
+
disabled={!canSelectModel}
|
|
334
|
+
placeholder="Select a compatible model..."
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
<Button type="submit" disabled={!canSelectModel || !isModelDirty} size="sm">
|
|
338
|
+
<Cpu className="mr-1.5 h-3.5 w-3.5" />
|
|
339
|
+
Save
|
|
340
|
+
</Button>
|
|
341
|
+
</form>
|
|
342
|
+
<p className="text-[11px] text-muted-foreground">
|
|
343
|
+
{canSelectModel
|
|
344
|
+
? `${compatibleModels.length} compatible models available.`
|
|
345
|
+
: 'Cortex AI uses the free registry until a stored key + model are set.'}
|
|
346
|
+
</p>
|
|
347
|
+
</CardContent>
|
|
348
|
+
</Card>
|
|
349
|
+
</div>
|
|
328
350
|
|
|
351
|
+
{/* Stock photos */}
|
|
329
352
|
<Card>
|
|
330
|
-
<CardHeader className="flex flex-row items-start justify-between">
|
|
353
|
+
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
|
331
354
|
<div>
|
|
332
|
-
<CardTitle className="text-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
355
|
+
<CardTitle className="flex flex-wrap items-center gap-2 text-base">
|
|
356
|
+
<ImageIcon className="h-4 w-4" />
|
|
357
|
+
Stock photos
|
|
358
|
+
{configuredStockProviders.length > 0 ? (
|
|
359
|
+
configuredStockProviders.map((provider, index) => (
|
|
360
|
+
<Badge
|
|
361
|
+
key={provider.name}
|
|
362
|
+
variant={index === 0 ? 'default' : 'secondary'}
|
|
363
|
+
className="ml-0.5 font-normal"
|
|
364
|
+
>
|
|
365
|
+
{provider.name} · {index === 0 ? 'primary' : 'fallback'}
|
|
366
|
+
{!provider.stored && ' (env)'}
|
|
367
|
+
</Badge>
|
|
368
|
+
))
|
|
369
|
+
) : (
|
|
370
|
+
<Badge variant="outline" className="ml-0.5">
|
|
371
|
+
Not configured
|
|
372
|
+
</Badge>
|
|
373
|
+
)}
|
|
374
|
+
</CardTitle>
|
|
375
|
+
<CardDescription className="text-xs">
|
|
376
|
+
Free Pexels/Unsplash key so Cortex inserts real photos into pages. Pexels is used first; Cortex
|
|
377
|
+
automatically falls back to Unsplash if Pexels is rate-limited. Optional but recommended.
|
|
336
378
|
</CardDescription>
|
|
337
379
|
</div>
|
|
338
|
-
{
|
|
339
|
-
<form action={
|
|
340
|
-
<Button
|
|
341
|
-
|
|
342
|
-
variant="outline"
|
|
343
|
-
size="sm"
|
|
344
|
-
className="text-destructive hover:text-destructive"
|
|
345
|
-
>
|
|
346
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
380
|
+
{(hasStoredPexelsKey || hasStoredUnsplashKey) && (
|
|
381
|
+
<form action={clearStockPhotoKeysAction} onSubmit={notifyCortexAiSettingsChanged}>
|
|
382
|
+
<Button type="submit" variant="ghost" size="sm" className="h-7 text-destructive hover:text-destructive">
|
|
383
|
+
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
|
|
347
384
|
Clear
|
|
348
385
|
</Button>
|
|
349
386
|
</form>
|
|
350
387
|
)}
|
|
351
388
|
</CardHeader>
|
|
352
|
-
<CardContent className="space-y-4">
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
389
|
+
<CardContent className="space-y-4 pt-0">
|
|
390
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
391
|
+
{/* Why + how */}
|
|
392
|
+
<div className="rounded-md border bg-muted/20 p-3 text-sm">
|
|
393
|
+
<p className="font-medium">Why add a key?</p>
|
|
394
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
395
|
+
When you ask Cortex to build or revamp a page, a stock key lets it fetch relevant, high-quality
|
|
396
|
+
photos for the hero and sections automatically — instant, zero image-generation cost, and you can
|
|
397
|
+
save any photo into your media library with one click. Without a key Cortex still builds pages, but
|
|
398
|
+
uses gradient/theme backgrounds instead of photos, and it will not call the photo tool at all.
|
|
399
|
+
</p>
|
|
400
|
+
<p className="mt-3 font-medium">Get a free key (pick one):</p>
|
|
401
|
+
<ol className="mt-1 list-decimal space-y-1 pl-4 text-xs text-muted-foreground">
|
|
402
|
+
<li>
|
|
403
|
+
<span className="font-medium text-foreground">Pexels</span> — open{' '}
|
|
404
|
+
<span className="font-mono">pexels.com/api</span>, sign in, click “Get Started / Your API Key”,
|
|
405
|
+
copy the key.
|
|
406
|
+
</li>
|
|
407
|
+
<li>
|
|
408
|
+
<span className="font-medium text-foreground">or Unsplash</span> — open{' '}
|
|
409
|
+
<span className="font-mono">unsplash.com/developers</span>, create a New Application, copy its
|
|
410
|
+
“Access Key”.
|
|
411
|
+
</li>
|
|
412
|
+
<li>Paste it on the right and Save. You only need one provider.</li>
|
|
413
|
+
</ol>
|
|
414
|
+
<p className="mt-2 text-[11px] text-muted-foreground">
|
|
415
|
+
Keys are encrypted and stored in your database, readable only by admins. Pexels is used first when
|
|
416
|
+
both are set.
|
|
417
|
+
</p>
|
|
418
|
+
</div>
|
|
370
419
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
<
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
420
|
+
{/* Key form */}
|
|
421
|
+
<form
|
|
422
|
+
action={saveStockPhotoKeysAction}
|
|
423
|
+
onSubmit={notifyCortexAiSettingsChanged}
|
|
424
|
+
className="space-y-3"
|
|
425
|
+
>
|
|
426
|
+
<div className="space-y-1.5">
|
|
427
|
+
<Label htmlFor="pexels_api_key" className="text-xs">
|
|
428
|
+
Pexels API key{' '}
|
|
429
|
+
{hasStoredPexelsKey && maskedStoredPexelsKey && (
|
|
430
|
+
<span className="font-mono text-[11px] text-muted-foreground">({maskedStoredPexelsKey})</span>
|
|
431
|
+
)}
|
|
432
|
+
{!hasStoredPexelsKey && hasEnvPexelsKey && (
|
|
433
|
+
<span className="text-[11px] text-muted-foreground">(set via env)</span>
|
|
434
|
+
)}
|
|
435
|
+
</Label>
|
|
436
|
+
<Input
|
|
437
|
+
id="pexels_api_key"
|
|
438
|
+
name="pexels_api_key"
|
|
439
|
+
type="password"
|
|
440
|
+
autoComplete="off"
|
|
441
|
+
placeholder={hasStoredPexelsKey ? 'Enter new key to overwrite...' : 'Paste Pexels API key'}
|
|
442
|
+
value={pexelsInput}
|
|
443
|
+
onChange={(e) => setPexelsInput(e.target.value)}
|
|
384
444
|
/>
|
|
385
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
386
|
-
{canSelectModel
|
|
387
|
-
? `${compatibleModels.length} compatible models available`
|
|
388
|
-
: 'Cortex AI will use the free registry until model selection is available.'}
|
|
389
|
-
</p>
|
|
390
445
|
</div>
|
|
391
|
-
<
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
446
|
+
<div className="space-y-1.5">
|
|
447
|
+
<Label htmlFor="unsplash_access_key" className="text-xs">
|
|
448
|
+
Unsplash Access key{' '}
|
|
449
|
+
{hasStoredUnsplashKey && maskedStoredUnsplashKey && (
|
|
450
|
+
<span className="font-mono text-[11px] text-muted-foreground">({maskedStoredUnsplashKey})</span>
|
|
451
|
+
)}
|
|
452
|
+
{!hasStoredUnsplashKey && hasEnvUnsplashKey && (
|
|
453
|
+
<span className="text-[11px] text-muted-foreground">(set via env)</span>
|
|
454
|
+
)}
|
|
455
|
+
</Label>
|
|
456
|
+
<Input
|
|
457
|
+
id="unsplash_access_key"
|
|
458
|
+
name="unsplash_access_key"
|
|
459
|
+
type="password"
|
|
460
|
+
autoComplete="off"
|
|
461
|
+
placeholder={hasStoredUnsplashKey ? 'Enter new key to overwrite...' : 'Paste Unsplash Access key'}
|
|
462
|
+
value={unsplashInput}
|
|
463
|
+
onChange={(e) => setUnsplashInput(e.target.value)}
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
<div className="space-y-1.5">
|
|
467
|
+
<Label htmlFor="unsplash_app_name" className="text-xs">
|
|
468
|
+
Unsplash app name{' '}
|
|
469
|
+
<span className="text-[11px] text-muted-foreground">
|
|
470
|
+
(for attribution links — must match your registered Unsplash app)
|
|
471
|
+
</span>
|
|
472
|
+
</Label>
|
|
473
|
+
<Input
|
|
474
|
+
id="unsplash_app_name"
|
|
475
|
+
name="unsplash_app_name"
|
|
476
|
+
type="text"
|
|
477
|
+
autoComplete="off"
|
|
478
|
+
placeholder="e.g. My Site Name"
|
|
479
|
+
value={appNameInput}
|
|
480
|
+
onChange={(e) => setAppNameInput(e.target.value)}
|
|
481
|
+
/>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="flex items-center justify-between">
|
|
484
|
+
<span className="text-[11px] text-muted-foreground">
|
|
485
|
+
{configuredStockProviders.length > 0
|
|
486
|
+
? `Using ${configuredStockProviders.map((provider) => provider.name).join(', ')}.`
|
|
487
|
+
: 'No provider configured yet.'}
|
|
488
|
+
</span>
|
|
489
|
+
<Button type="submit" disabled={!isStockDirty} size="sm">
|
|
490
|
+
<ImageIcon className="mr-1.5 h-3.5 w-3.5" />
|
|
491
|
+
Save
|
|
492
|
+
</Button>
|
|
493
|
+
</div>
|
|
494
|
+
</form>
|
|
495
|
+
</div>
|
|
406
496
|
</CardContent>
|
|
407
497
|
</Card>
|
|
498
|
+
|
|
499
|
+
{/* Advanced settings (collapsed by default) */}
|
|
500
|
+
<div>
|
|
501
|
+
<button
|
|
502
|
+
type="button"
|
|
503
|
+
onClick={() => setShowAdvanced((open) => !open)}
|
|
504
|
+
className="flex w-full items-center gap-2 rounded-md border bg-muted/20 px-3 py-2 text-left text-sm font-medium hover:bg-muted/40"
|
|
505
|
+
>
|
|
506
|
+
<SlidersHorizontal className="h-4 w-4" />
|
|
507
|
+
Advanced settings
|
|
508
|
+
<span className="ml-auto text-xs text-muted-foreground">
|
|
509
|
+
{agentSettings.maxOutputTokens === null ? 'Unlimited output' : `${agentSettings.maxOutputTokens} tokens`} · {agentSettings.maxSteps} steps
|
|
510
|
+
</span>
|
|
511
|
+
{showAdvanced ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
512
|
+
</button>
|
|
513
|
+
|
|
514
|
+
{showAdvanced && (
|
|
515
|
+
<Card className="mt-2">
|
|
516
|
+
<CardHeader className="pb-3">
|
|
517
|
+
<CardTitle className="text-base">Agent tuning</CardTitle>
|
|
518
|
+
<CardDescription className="text-xs">
|
|
519
|
+
Controls how much room the page-building agent has. Leave the defaults unless a big build gets cut
|
|
520
|
+
off — then raise the output tokens (or set Unlimited) and steps.
|
|
521
|
+
</CardDescription>
|
|
522
|
+
</CardHeader>
|
|
523
|
+
<CardContent className="space-y-4 pt-0">
|
|
524
|
+
<form action={saveCortexAiAgentSettingsAction} onSubmit={notifyCortexAiSettingsChanged} className="space-y-4">
|
|
525
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
526
|
+
<div className="space-y-1.5">
|
|
527
|
+
<Label
|
|
528
|
+
htmlFor="max_output_tokens"
|
|
529
|
+
className="text-xs"
|
|
530
|
+
title="Range 256–200,000 tokens, or Unlimited. The per-step output budget; also counts a tool call's JSON, so raise it (or use Unlimited) if a big page rewrite gets cut off. Default 16,000."
|
|
531
|
+
>
|
|
532
|
+
Max output tokens per step
|
|
533
|
+
</Label>
|
|
534
|
+
<Input
|
|
535
|
+
id="max_output_tokens"
|
|
536
|
+
name="max_output_tokens"
|
|
537
|
+
type="number"
|
|
538
|
+
min={256}
|
|
539
|
+
max={200000}
|
|
540
|
+
step={256}
|
|
541
|
+
value={maxTokensInput}
|
|
542
|
+
onChange={(e) => setMaxTokensInput(e.target.value)}
|
|
543
|
+
disabled={unlimitedTokens}
|
|
544
|
+
/>
|
|
545
|
+
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
546
|
+
<input
|
|
547
|
+
type="checkbox"
|
|
548
|
+
name="max_output_unlimited"
|
|
549
|
+
checked={unlimitedTokens}
|
|
550
|
+
onChange={(e) => setUnlimitedTokens(e.target.checked)}
|
|
551
|
+
className="h-3.5 w-3.5"
|
|
552
|
+
/>
|
|
553
|
+
Unlimited (use the model's full output budget)
|
|
554
|
+
</label>
|
|
555
|
+
<p className="text-[11px] text-muted-foreground">Range 256–200,000, or Unlimited. Default 16,000.</p>
|
|
556
|
+
</div>
|
|
557
|
+
<div className="space-y-1.5">
|
|
558
|
+
<Label
|
|
559
|
+
htmlFor="max_steps"
|
|
560
|
+
className="text-xs"
|
|
561
|
+
title="Range 2–100. Each step is one full model call (a page rewrite is ~3–4). This is also the runaway-loop backstop, so a high value can cost more. Default 8."
|
|
562
|
+
>
|
|
563
|
+
Max tool steps
|
|
564
|
+
</Label>
|
|
565
|
+
<Input
|
|
566
|
+
id="max_steps"
|
|
567
|
+
name="max_steps"
|
|
568
|
+
type="number"
|
|
569
|
+
min={2}
|
|
570
|
+
max={100}
|
|
571
|
+
step={1}
|
|
572
|
+
value={maxStepsInput}
|
|
573
|
+
onChange={(e) => setMaxStepsInput(e.target.value)}
|
|
574
|
+
/>
|
|
575
|
+
<p className="text-[11px] text-muted-foreground">
|
|
576
|
+
Range 2–100 tool-call rounds. Default 8; each step is one model call.
|
|
577
|
+
</p>
|
|
578
|
+
</div>
|
|
579
|
+
<div className="space-y-1.5">
|
|
580
|
+
<Label
|
|
581
|
+
htmlFor="temperature"
|
|
582
|
+
className="text-xs"
|
|
583
|
+
title="Range 0–2. This is Cortex's default (0.1), not the model's universal default (usually ~0.7–1.0). Low keeps structured tool-calls reliable; raise for more variety in copy."
|
|
584
|
+
>
|
|
585
|
+
Temperature
|
|
586
|
+
</Label>
|
|
587
|
+
<Input
|
|
588
|
+
id="temperature"
|
|
589
|
+
name="temperature"
|
|
590
|
+
type="number"
|
|
591
|
+
min={0}
|
|
592
|
+
max={2}
|
|
593
|
+
step={0.1}
|
|
594
|
+
value={temperatureInput}
|
|
595
|
+
onChange={(e) => setTemperatureInput(e.target.value)}
|
|
596
|
+
/>
|
|
597
|
+
<p className="text-[11px] text-muted-foreground">
|
|
598
|
+
Range 0–2. Cortex default 0.1 (low = reliable; most models default higher).
|
|
599
|
+
</p>
|
|
600
|
+
</div>
|
|
601
|
+
<div className="space-y-1.5">
|
|
602
|
+
<Label
|
|
603
|
+
htmlFor="response_timeout_seconds"
|
|
604
|
+
className="text-xs"
|
|
605
|
+
title="Range 15–600 seconds. Aborts an attempt only after this long with NO stream activity — not a hard cap on total time. Default 120."
|
|
606
|
+
>
|
|
607
|
+
Response timeout (seconds)
|
|
608
|
+
</Label>
|
|
609
|
+
<Input
|
|
610
|
+
id="response_timeout_seconds"
|
|
611
|
+
name="response_timeout_seconds"
|
|
612
|
+
type="number"
|
|
613
|
+
min={15}
|
|
614
|
+
max={600}
|
|
615
|
+
step={5}
|
|
616
|
+
value={timeoutInput}
|
|
617
|
+
onChange={(e) => setTimeoutInput(e.target.value)}
|
|
618
|
+
/>
|
|
619
|
+
<p className="text-[11px] text-muted-foreground">
|
|
620
|
+
Range 15–600s. Default 120; aborts only after this long with no activity.
|
|
621
|
+
</p>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
<div className="flex items-center justify-between">
|
|
625
|
+
<span className="text-[11px] text-muted-foreground">
|
|
626
|
+
Values are clamped to safe ranges. Applies to the global page-building agent.
|
|
627
|
+
</span>
|
|
628
|
+
<Button type="submit" size="sm">
|
|
629
|
+
<SlidersHorizontal className="mr-1.5 h-3.5 w-3.5" />
|
|
630
|
+
Save
|
|
631
|
+
</Button>
|
|
632
|
+
</div>
|
|
633
|
+
</form>
|
|
634
|
+
<form action={resetCortexAiAgentSettingsAction} onSubmit={notifyCortexAiSettingsChanged}>
|
|
635
|
+
<Button type="submit" variant="ghost" size="sm" className="h-7 text-muted-foreground">
|
|
636
|
+
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
|
637
|
+
Reset to defaults
|
|
638
|
+
</Button>
|
|
639
|
+
</form>
|
|
640
|
+
</CardContent>
|
|
641
|
+
</Card>
|
|
642
|
+
)}
|
|
643
|
+
</div>
|
|
408
644
|
</div>
|
|
409
645
|
);
|
|
410
646
|
}
|