create-nextblock 0.13.12 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,19 +18,23 @@ import {
18
18
  } from '@nextblock-cms/ui';
19
19
  import {
20
20
  AlertTriangle,
21
- BrainCircuit,
21
+ Brain,
22
22
  CheckCircle2,
23
+ ChevronDown,
24
+ ChevronRight,
23
25
  Cpu,
26
+ ImageIcon,
27
+ Info,
24
28
  KeyRound,
25
- ServerCog,
26
- ShieldCheck,
29
+ Lock,
30
+ SlidersHorizontal,
27
31
  Trash2,
28
- Info,
29
32
  } from 'lucide-react';
30
33
  import {
31
34
  createCortexAiStoredModelSelection,
32
35
  type CortexAiStoredModelSelection,
33
36
  } from '@nextblock-cms/cortex/client';
37
+ import type { CortexAiAgentSettings } from '@nextblock-cms/cortex';
34
38
 
35
39
  const CORTEX_AI_SANDBOX_KEY_LOCAL_STORAGE = 'cortex_ai_sandbox_openrouter_api_key';
36
40
  const CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE = 'cortex_ai_sandbox_openrouter_model_selection';
@@ -58,27 +62,31 @@ type SandboxCortexAiSettingsClientProps = {
58
62
  hasEnvOpenRouterKey: boolean;
59
63
  maskedEnvOpenRouterKey: string | null;
60
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
+ unsplashAppName: string | null;
73
+ agentSettings: CortexAiAgentSettings;
61
74
  };
62
75
 
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
- };
76
+ function formatTokenPrice(value: string | undefined) {
77
+ const amount = Number(value);
78
+ if (!Number.isFinite(amount)) return null;
79
+ if (amount === 0) return '$0';
80
+ const perMillion = amount * 1_000_000;
81
+ return `$${perMillion < 0.01 ? perMillion.toFixed(4) : perMillion.toFixed(2)}`;
82
+ }
80
83
 
81
- return `${formatPrice(amount)}/1M input - ${formatPrice(completionAmount)}/1M output`;
84
+ function formatModelPricing(pricing: Record<string, string>) {
85
+ const promptPrice = formatTokenPrice(pricing.prompt);
86
+ const completionPrice = formatTokenPrice(pricing.completion);
87
+ if (promptPrice === '$0' && completionPrice === '$0') return 'Free';
88
+ if (promptPrice && completionPrice) return `${promptPrice}/1M input - ${completionPrice}/1M output`;
89
+ return 'Pricing varies';
82
90
  }
83
91
 
84
92
  function getMaskedKey(key: string) {
@@ -90,12 +98,70 @@ function notifyCortexAiSettingsChanged() {
90
98
  window.dispatchEvent(new Event(CORTEX_AI_SETTINGS_CHANGED_EVENT));
91
99
  }
92
100
 
101
+ function StatusPill({
102
+ label,
103
+ value,
104
+ active,
105
+ detail,
106
+ }: {
107
+ label: string;
108
+ value: string;
109
+ active: boolean;
110
+ detail?: string | null;
111
+ }) {
112
+ return (
113
+ <div className="flex min-w-[8rem] flex-col gap-1 rounded-md border bg-muted/30 px-3 py-2">
114
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
115
+ {label}
116
+ </span>
117
+ <div className="flex items-center gap-2">
118
+ <span className={`h-2 w-2 rounded-full ${active ? 'bg-emerald-500' : 'bg-muted-foreground/40'}`} />
119
+ <span className="text-sm font-medium">{value}</span>
120
+ </div>
121
+ {detail && <span className="truncate font-mono text-[11px] text-muted-foreground">{detail}</span>}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ // Sandbox mirrors the production layout, but every server-backed setting is
127
+ // read-only: the settings actions refuse to write to the shared sandbox DB, and
128
+ // only the OpenRouter key + model have a per-visitor override channel
129
+ // (localStorage -> x-sandbox-openrouter-* headers).
130
+ function ReadOnlyRow({
131
+ label,
132
+ value,
133
+ detail,
134
+ }: {
135
+ label: string;
136
+ value: string;
137
+ detail?: string | null;
138
+ }) {
139
+ return (
140
+ <div className="flex items-center justify-between gap-3 rounded-md border bg-muted/20 px-3 py-2">
141
+ <span className="text-xs font-medium">{label}</span>
142
+ <div className="flex items-center gap-2 text-right">
143
+ <span className="text-xs text-muted-foreground">{value}</span>
144
+ {detail && <span className="font-mono text-[11px] text-muted-foreground">{detail}</span>}
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+
93
150
  export function SandboxCortexAiSettingsClient({
94
151
  compatibleModels,
95
152
  isPackageActive,
96
153
  hasEnvOpenRouterKey,
97
154
  maskedEnvOpenRouterKey,
98
155
  modelCatalogError,
156
+ activeStockProvider,
157
+ hasStoredPexelsKey,
158
+ maskedStoredPexelsKey,
159
+ hasStoredUnsplashKey,
160
+ maskedStoredUnsplashKey,
161
+ hasEnvPexelsKey,
162
+ hasEnvUnsplashKey,
163
+ unsplashAppName,
164
+ agentSettings,
99
165
  }: SandboxCortexAiSettingsClientProps) {
100
166
  const [mounted, setMounted] = useState(false);
101
167
  const [sandboxKey, setSandboxKey] = useState<string | null>(null);
@@ -103,6 +169,7 @@ export function SandboxCortexAiSettingsClient({
103
169
  const [inputValue, setInputValue] = useState('');
104
170
  const [modelInput, setModelInput] = useState<string>('');
105
171
  const [successMessage, setSuccessMessage] = useState<string | null>(null);
172
+ const [showAdvanced, setShowAdvanced] = useState(false);
106
173
 
107
174
  useEffect(() => {
108
175
  try {
@@ -165,11 +232,11 @@ export function SandboxCortexAiSettingsClient({
165
232
  if (!selectedModel) return;
166
233
 
167
234
  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
235
  const storedSelection = createCortexAiStoredModelSelection(selectedModel as any);
172
- window.localStorage.setItem(CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE, JSON.stringify(storedSelection));
236
+ window.localStorage.setItem(
237
+ CORTEX_AI_SANDBOX_MODEL_LOCAL_STORAGE,
238
+ JSON.stringify(storedSelection)
239
+ );
173
240
  setSandboxModel(storedSelection);
174
241
  notifyCortexAiSettingsChanged();
175
242
  setSuccessMessage('Sandbox Cortex AI model selection saved to your browser.');
@@ -199,7 +266,7 @@ export function SandboxCortexAiSettingsClient({
199
266
  const selectedModelIsInCatalog = compatibleModels.some(
200
267
  (model) => model.id === sandboxModel?.modelId
201
268
  );
202
-
269
+
203
270
  const modelOptions =
204
271
  sandboxModel && !selectedModelIsInCatalog
205
272
  ? [
@@ -215,7 +282,7 @@ export function SandboxCortexAiSettingsClient({
215
282
  ...compatibleModels,
216
283
  ]
217
284
  : compatibleModels;
218
-
285
+
219
286
  const canSelectModel = !!sandboxKey && compatibleModels.length > 0;
220
287
  const maskedSandboxKey = sandboxKey ? getMaskedKey(sandboxKey) : null;
221
288
  const isKeyDirty = inputValue.trim().length > 0;
@@ -227,16 +294,35 @@ export function SandboxCortexAiSettingsClient({
227
294
  description: `${model.id} - ${formatModelPricing(model.pricing as any)}`,
228
295
  }));
229
296
 
297
+ const keySourceValue = sandboxKey ? 'Sandbox BYOK' : hasEnvOpenRouterKey ? 'Environment' : 'None';
298
+ // Ordered by preference (Pexels primary, Unsplash fallback). Configured = a
299
+ // stored DB key OR an env var for that provider.
300
+ const configuredStockProviders = [
301
+ hasStoredPexelsKey || hasEnvPexelsKey ? { name: 'Pexels', stored: hasStoredPexelsKey } : null,
302
+ hasStoredUnsplashKey || hasEnvUnsplashKey
303
+ ? { name: 'Unsplash', stored: hasStoredUnsplashKey }
304
+ : null,
305
+ ].filter(Boolean) as Array<{ name: string; stored: boolean }>;
306
+ const stockValue =
307
+ configuredStockProviders.length > 0
308
+ ? configuredStockProviders.map((provider) => provider.name).join(' + ')
309
+ : 'Off';
310
+
230
311
  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>
312
+ <div className="mx-auto w-full max-w-5xl space-y-4 px-4 py-6">
313
+ <div className="flex items-center gap-2.5">
314
+ <Brain className="h-6 w-6 text-primary" />
315
+ <div>
316
+ <h1 className="text-xl font-semibold leading-tight">
317
+ NextBlock Cortex AI{' '}
318
+ <Badge variant="secondary" className="ml-1 align-middle font-normal">
319
+ Sandbox
320
+ </Badge>
321
+ </h1>
322
+ <p className="text-xs text-muted-foreground">
323
+ Manage the OpenRouter model key used by Cortex AI in your own browser session.
324
+ </p>
236
325
  </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
326
  </div>
241
327
 
242
328
  {successMessage && (
@@ -247,143 +333,84 @@ export function SandboxCortexAiSettingsClient({
247
333
  </Alert>
248
334
  )}
249
335
 
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">
336
+ {/* Compact status strip */}
337
+ <div className="flex flex-wrap gap-2">
338
+ <StatusPill
339
+ label="Package"
340
+ value={isPackageActive ? 'Active' : 'Inactive'}
341
+ active={isPackageActive}
342
+ />
343
+ <StatusPill
344
+ label="Model key"
345
+ value={keySourceValue}
346
+ active={Boolean(sandboxKey) || hasEnvOpenRouterKey}
347
+ detail={maskedSandboxKey || maskedEnvOpenRouterKey}
348
+ />
349
+ <StatusPill
350
+ label="Model"
351
+ value={sandboxModel ? sandboxModel.name : 'Free registry'}
352
+ active={Boolean(sandboxModel)}
353
+ />
354
+ <StatusPill
355
+ label="Stock photos"
356
+ value={stockValue}
357
+ active={Boolean(activeStockProvider)}
358
+ />
359
+ </div>
360
+
361
+ <Alert
362
+ variant="warning"
363
+ className="border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200"
364
+ >
251
365
  <Info className="h-4 w-4" />
252
- <AlertTitle>Sandbox Environment Active</AlertTitle>
366
+ <AlertTitle>Sandbox environment active</AlertTitle>
253
367
  <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.
368
+ The key and model you set here are stored{' '}
369
+ <strong>only in your own browser (localStorage)</strong> and are never written to the
370
+ shared sandbox database. Server-backed settings below are read-only here.
255
371
  </AlertDescription>
256
372
  </Alert>
257
373
 
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
374
  {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
375
  <Alert>
351
376
  <KeyRound className="h-4 w-4" />
352
- <AlertTitle>Sandbox BYOK active</AlertTitle>
377
+ <AlertTitle>Free-model lock active</AlertTitle>
353
378
  <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.
379
+ Cortex AI will only use the configured free OpenRouter models until you save a sandbox
380
+ key to your browser.
356
381
  </AlertDescription>
357
382
  </Alert>
358
383
  )}
359
384
 
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(); }}>
385
+ {/* OpenRouter BYOK + Model side by side */}
386
+ <div className="grid gap-4 lg:grid-cols-2">
387
+ <Card>
388
+ <CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
389
+ <div>
390
+ <CardTitle className="text-base">OpenRouter key</CardTitle>
391
+ <CardDescription className="text-xs">
392
+ Saved to your browser only, never uploaded.
393
+ </CardDescription>
394
+ </div>
395
+ {sandboxKey && (
370
396
  <Button
371
- type="submit"
372
- variant="outline"
397
+ type="button"
398
+ onClick={handleClearKey}
399
+ variant="ghost"
373
400
  size="sm"
374
- className="text-destructive hover:text-destructive"
401
+ className="h-7 text-destructive hover:text-destructive"
375
402
  >
376
- <Trash2 className="mr-2 h-4 w-4" />
403
+ <Trash2 className="mr-1.5 h-3.5 w-3.5" />
377
404
  Clear
378
405
  </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>
406
+ )}
407
+ </CardHeader>
408
+ <CardContent className="pt-0">
409
+ <form onSubmit={handleSaveKey} className="flex items-end gap-2">
410
+ <div className="flex-1 space-y-1.5">
411
+ <Label htmlFor="openrouter_api_key" className="text-xs">
412
+ API key
413
+ </Label>
387
414
  <Input
388
415
  id="openrouter_api_key"
389
416
  name="openrouter_api_key"
@@ -396,72 +423,49 @@ export function SandboxCortexAiSettingsClient({
396
423
  required
397
424
  />
398
425
  </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
- )}
426
+ <Button type="submit" disabled={!isKeyDirty} size="sm">
427
+ <KeyRound className="mr-1.5 h-3.5 w-3.5" />
428
+ Save
411
429
  </Button>
412
- </div>
413
- </form>
414
- </CardContent>
415
- </Card>
430
+ </form>
431
+ </CardContent>
432
+ </Card>
416
433
 
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(); }}>
434
+ <Card>
435
+ <CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
436
+ <div>
437
+ <CardTitle className="text-base">OpenRouter model</CardTitle>
438
+ <CardDescription className="text-xs">
439
+ Needs a sandbox key; supports tools + structured output.
440
+ </CardDescription>
441
+ </div>
442
+ {sandboxModel && (
428
443
  <Button
429
- type="submit"
430
- variant="outline"
444
+ type="button"
445
+ onClick={handleClearModel}
446
+ variant="ghost"
431
447
  size="sm"
432
- className="text-destructive hover:text-destructive"
448
+ className="h-7 text-destructive hover:text-destructive"
433
449
  >
434
- <Trash2 className="mr-2 h-4 w-4" />
450
+ <Trash2 className="mr-1.5 h-3.5 w-3.5" />
435
451
  Clear
436
452
  </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>
453
+ )}
454
+ </CardHeader>
455
+ <CardContent className="space-y-2 pt-0">
456
+ {modelCatalogError && (
457
+ <Alert variant="warning">
458
+ <AlertTriangle className="h-4 w-4" />
459
+ <AlertTitle>Model catalog unavailable</AlertTitle>
460
+ <AlertDescription>{modelCatalogError}</AlertDescription>
461
+ </Alert>
462
+ )}
463
+ <form onSubmit={handleSaveModel} className="flex items-end gap-2">
464
+ <input type="hidden" name="openrouter_model_id" value={modelInput} />
465
+ <div className="flex-1 space-y-1.5">
466
+ <Label htmlFor="openrouter_model_id_select" className="text-xs">
467
+ Model
468
+ </Label>
465
469
  <SearchableSelect
466
470
  options={searchableOptions}
467
471
  value={modelInput}
@@ -469,29 +473,216 @@ export function SandboxCortexAiSettingsClient({
469
473
  disabled={!canSelectModel}
470
474
  placeholder="Select a compatible model..."
471
475
  />
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
476
  </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
- )}
477
+ <Button type="submit" disabled={!canSelectModel || !isModelDirty} size="sm">
478
+ <Cpu className="mr-1.5 h-3.5 w-3.5" />
479
+ Save
490
480
  </Button>
481
+ </form>
482
+ <p className="text-[11px] text-muted-foreground">
483
+ {canSelectModel
484
+ ? `${compatibleModels.length} compatible models available.`
485
+ : 'Cortex AI uses the free registry until a sandbox key + model are set.'}
486
+ </p>
487
+ </CardContent>
488
+ </Card>
489
+ </div>
490
+
491
+ {/* Stock photos - read-only in sandbox */}
492
+ <Card>
493
+ <CardHeader className="pb-3">
494
+ <CardTitle className="flex flex-wrap items-center gap-2 text-base">
495
+ <ImageIcon className="h-4 w-4" />
496
+ Stock photos
497
+ {configuredStockProviders.length > 0 ? (
498
+ configuredStockProviders.map((provider, index) => (
499
+ <Badge
500
+ key={provider.name}
501
+ variant={index === 0 ? 'default' : 'secondary'}
502
+ className="ml-0.5 font-normal"
503
+ >
504
+ {provider.name} · {index === 0 ? 'primary' : 'fallback'}
505
+ {!provider.stored && ' (env)'}
506
+ </Badge>
507
+ ))
508
+ ) : (
509
+ <Badge variant="outline" className="ml-0.5">
510
+ Not configured
511
+ </Badge>
512
+ )}
513
+ <Badge variant="outline" className="ml-auto gap-1 font-normal">
514
+ <Lock className="h-3 w-3" />
515
+ Read-only
516
+ </Badge>
517
+ </CardTitle>
518
+ <CardDescription className="text-xs">
519
+ A Pexels/Unsplash key lets Cortex insert real photos into pages. Pexels is used first;
520
+ Cortex automatically falls back to Unsplash if Pexels is rate-limited.
521
+ </CardDescription>
522
+ </CardHeader>
523
+ <CardContent className="space-y-4 pt-0">
524
+ <div className="grid gap-4 md:grid-cols-2">
525
+ <div className="rounded-md border bg-muted/20 p-3 text-sm">
526
+ <p className="font-medium">What this does</p>
527
+ <p className="mt-1 text-xs text-muted-foreground">
528
+ When you ask Cortex to build or revamp a page, a stock key lets it fetch relevant,
529
+ high-quality photos for the hero and sections automatically — instant, zero
530
+ image-generation cost, and you can save any photo into your media library with one
531
+ click. Without a key Cortex still builds pages, but uses gradient/theme backgrounds
532
+ instead of photos, and it will not call the photo tool at all.
533
+ </p>
534
+ <p className="mt-3 text-[11px] text-muted-foreground">
535
+ In your own NextBlock install you add these keys on this page and they are encrypted
536
+ into your database. In the shared sandbox they are provided by the host environment
537
+ and cannot be changed.
538
+ </p>
539
+ </div>
540
+
541
+ <div className="space-y-2">
542
+ <ReadOnlyRow
543
+ label="Pexels API key"
544
+ value={
545
+ hasStoredPexelsKey
546
+ ? 'Configured (stored)'
547
+ : hasEnvPexelsKey
548
+ ? 'Configured (env)'
549
+ : 'Not set'
550
+ }
551
+ detail={maskedStoredPexelsKey}
552
+ />
553
+ <ReadOnlyRow
554
+ label="Unsplash Access key"
555
+ value={
556
+ hasStoredUnsplashKey
557
+ ? 'Configured (stored)'
558
+ : hasEnvUnsplashKey
559
+ ? 'Configured (env)'
560
+ : 'Not set'
561
+ }
562
+ detail={maskedStoredUnsplashKey}
563
+ />
564
+ <ReadOnlyRow label="Unsplash app name" value={unsplashAppName || 'Not set'} />
565
+ <p className="text-[11px] text-muted-foreground">
566
+ {configuredStockProviders.length > 0
567
+ ? `Using ${configuredStockProviders.map((provider) => provider.name).join(', ')}.`
568
+ : 'No provider configured in this sandbox.'}
569
+ </p>
491
570
  </div>
492
- </form>
571
+ </div>
493
572
  </CardContent>
494
573
  </Card>
574
+
575
+ {/* Advanced settings (collapsed by default) - read-only in sandbox */}
576
+ <div>
577
+ <button
578
+ type="button"
579
+ onClick={() => setShowAdvanced((open) => !open)}
580
+ 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"
581
+ >
582
+ <SlidersHorizontal className="h-4 w-4" />
583
+ Advanced settings
584
+ <span className="ml-auto text-xs text-muted-foreground">
585
+ {agentSettings.maxOutputTokens === null
586
+ ? 'Unlimited output'
587
+ : `${agentSettings.maxOutputTokens} tokens`}{' '}
588
+ · {agentSettings.maxSteps} steps
589
+ </span>
590
+ {showAdvanced ? (
591
+ <ChevronDown className="h-4 w-4" />
592
+ ) : (
593
+ <ChevronRight className="h-4 w-4" />
594
+ )}
595
+ </button>
596
+
597
+ {showAdvanced && (
598
+ <Card className="mt-2">
599
+ <CardHeader className="pb-3">
600
+ <CardTitle className="flex items-center gap-2 text-base">
601
+ Agent tuning
602
+ <Badge variant="outline" className="gap-1 font-normal">
603
+ <Lock className="h-3 w-3" />
604
+ Read-only
605
+ </Badge>
606
+ </CardTitle>
607
+ <CardDescription className="text-xs">
608
+ Controls how much room the page-building agent has. These are set by the sandbox host
609
+ and shared by every visitor, so they cannot be edited here.
610
+ </CardDescription>
611
+ </CardHeader>
612
+ <CardContent className="space-y-4 pt-0">
613
+ <div className="grid gap-4 sm:grid-cols-2">
614
+ <div className="space-y-1.5">
615
+ <Label htmlFor="max_output_tokens" className="text-xs">
616
+ Max output tokens per step
617
+ </Label>
618
+ <Input
619
+ id="max_output_tokens"
620
+ type="text"
621
+ value={
622
+ agentSettings.maxOutputTokens === null
623
+ ? 'Unlimited'
624
+ : String(agentSettings.maxOutputTokens)
625
+ }
626
+ readOnly
627
+ disabled
628
+ />
629
+ <p className="text-[11px] text-muted-foreground">
630
+ The per-step output budget. Range 256–200,000, or Unlimited. Default 16,000.
631
+ </p>
632
+ </div>
633
+ <div className="space-y-1.5">
634
+ <Label htmlFor="max_steps" className="text-xs">
635
+ Max tool steps
636
+ </Label>
637
+ <Input
638
+ id="max_steps"
639
+ type="text"
640
+ value={String(agentSettings.maxSteps)}
641
+ readOnly
642
+ disabled
643
+ />
644
+ <p className="text-[11px] text-muted-foreground">
645
+ Range 2–100 tool-call rounds. Default 8; each step is one model call.
646
+ </p>
647
+ </div>
648
+ <div className="space-y-1.5">
649
+ <Label htmlFor="temperature" className="text-xs">
650
+ Temperature
651
+ </Label>
652
+ <Input
653
+ id="temperature"
654
+ type="text"
655
+ value={String(agentSettings.temperature)}
656
+ readOnly
657
+ disabled
658
+ />
659
+ <p className="text-[11px] text-muted-foreground">
660
+ Range 0–2. Cortex default 0.1 (low = reliable; most models default higher).
661
+ </p>
662
+ </div>
663
+ <div className="space-y-1.5">
664
+ <Label htmlFor="response_timeout_seconds" className="text-xs">
665
+ Response timeout (seconds)
666
+ </Label>
667
+ <Input
668
+ id="response_timeout_seconds"
669
+ type="text"
670
+ value={String(Math.round(agentSettings.responseTimeoutMs / 1000))}
671
+ readOnly
672
+ disabled
673
+ />
674
+ <p className="text-[11px] text-muted-foreground">
675
+ Range 15–600s. Default 120; aborts only after this long with no activity.
676
+ </p>
677
+ </div>
678
+ </div>
679
+ <p className="text-[11px] text-muted-foreground">
680
+ Applies to the global page-building agent. Editable on a self-hosted install.
681
+ </p>
682
+ </CardContent>
683
+ </Card>
684
+ )}
685
+ </div>
495
686
  </div>
496
687
  );
497
688
  }