@actuate-media/cms-admin 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdminRoot.d.ts +6 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +16 -3
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/views/settings-save-footer.render.test.d.ts +2 -0
- package/dist/__tests__/views/settings-save-footer.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/settings-save-footer.render.test.js +53 -0
- package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -0
- package/dist/__tests__/views/user-detail-drawer.render.test.d.ts +2 -0
- package/dist/__tests__/views/user-detail-drawer.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/user-detail-drawer.render.test.js +113 -0
- package/dist/__tests__/views/user-detail-drawer.render.test.js.map +1 -0
- package/dist/__tests__/views/users.render.test.d.ts +2 -0
- package/dist/__tests__/views/users.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/users.render.test.js +146 -0
- package/dist/__tests__/views/users.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/layout/Header.d.ts +3 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +2 -2
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts +2 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +2 -2
- package/dist/layout/Layout.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +54 -51
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts +1 -3
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +420 -56
- package/dist/views/Users.js.map +1 -1
- package/dist/views/users/UserDetailDrawer.d.ts +9 -0
- package/dist/views/users/UserDetailDrawer.d.ts.map +1 -0
- package/dist/views/users/UserDetailDrawer.js +236 -0
- package/dist/views/users/UserDetailDrawer.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +28 -2
- package/src/__tests__/views/settings-save-footer.render.test.tsx +63 -0
- package/src/__tests__/views/user-detail-drawer.render.test.tsx +119 -0
- package/src/__tests__/views/users.render.test.tsx +155 -0
- package/src/layout/Header.tsx +10 -2
- package/src/layout/Layout.tsx +10 -1
- package/src/views/Settings.tsx +142 -263
- package/src/views/Users.tsx +1187 -271
- package/src/views/users/UserDetailDrawer.tsx +924 -0
package/src/views/Settings.tsx
CHANGED
|
@@ -5,11 +5,6 @@ import {
|
|
|
5
5
|
Bot,
|
|
6
6
|
Eye,
|
|
7
7
|
EyeOff,
|
|
8
|
-
Image,
|
|
9
|
-
FileCode2,
|
|
10
|
-
BookOpen,
|
|
11
|
-
Sparkles,
|
|
12
|
-
MessageSquare,
|
|
13
8
|
Loader2,
|
|
14
9
|
AlertTriangle,
|
|
15
10
|
Download,
|
|
@@ -44,8 +39,14 @@ export interface SettingsProps {
|
|
|
44
39
|
initialTab?: string
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Tabs that persist through the shared footer's `handleSave` (which writes
|
|
44
|
+
* `defaultLayout` into the settings global). Every other tab owns its own
|
|
45
|
+
* saving, so the footer must NOT render there — otherwise it shows a
|
|
46
|
+
* "Settings saved successfully!" toast while persisting nothing relevant.
|
|
47
|
+
* This is an allowlist on purpose: new tabs default to self-managed.
|
|
48
|
+
*/
|
|
49
|
+
const GLOBAL_SAVE_TABS = new Set(['layout'])
|
|
49
50
|
|
|
50
51
|
function readSettingsTabFromUrl(): string | null {
|
|
51
52
|
if (typeof window === 'undefined') return null
|
|
@@ -120,32 +121,8 @@ export function Settings({
|
|
|
120
121
|
[normalizeTab, onNavigate],
|
|
121
122
|
)
|
|
122
123
|
|
|
123
|
-
// AI settings
|
|
124
|
-
const [aiProvider, setAiProvider] = useState('anthropic')
|
|
125
|
-
const [aiApiKey, setAiApiKey] = useState('')
|
|
126
|
-
const [showApiKey, setShowApiKey] = useState(false)
|
|
127
|
-
const [aiAltTags, setAiAltTags] = useState(true)
|
|
128
|
-
const [aiMediaCategorize, setAiMediaCategorize] = useState(false)
|
|
129
|
-
const [aiMetaDescriptions, setAiMetaDescriptions] = useState(true)
|
|
130
|
-
const [aiReadability, setAiReadability] = useState(true)
|
|
131
|
-
const [aiSchema, setAiSchema] = useState(true)
|
|
132
|
-
const [aiBrandVoice, setAiBrandVoice] = useState(false)
|
|
133
|
-
const [aiWritingAssistant, setAiWritingAssistant] = useState(true)
|
|
134
|
-
const [aiContentScoring, setAiContentScoring] = useState(true)
|
|
135
|
-
const [aiTranslation, setAiTranslation] = useState(false)
|
|
136
|
-
|
|
137
124
|
useEffect(() => {
|
|
138
125
|
if (data) {
|
|
139
|
-
setAiProvider(data.aiProvider ?? 'anthropic')
|
|
140
|
-
setAiAltTags(data.aiAltTags ?? true)
|
|
141
|
-
setAiMediaCategorize(data.aiMediaCategorize ?? false)
|
|
142
|
-
setAiMetaDescriptions(data.aiMetaDescriptions ?? true)
|
|
143
|
-
setAiReadability(data.aiReadability ?? true)
|
|
144
|
-
setAiSchema(data.aiSchema ?? true)
|
|
145
|
-
setAiBrandVoice(data.aiBrandVoice ?? false)
|
|
146
|
-
setAiWritingAssistant(data.aiWritingAssistant ?? true)
|
|
147
|
-
setAiContentScoring(data.aiContentScoring ?? true)
|
|
148
|
-
setAiTranslation(data.aiTranslation ?? false)
|
|
149
126
|
if (data.defaultLayout && typeof data.defaultLayout === 'object') {
|
|
150
127
|
setDefaultLayout(data.defaultLayout)
|
|
151
128
|
}
|
|
@@ -162,16 +139,6 @@ export function Settings({
|
|
|
162
139
|
method: 'PUT',
|
|
163
140
|
body: JSON.stringify({
|
|
164
141
|
...(current.data ?? {}),
|
|
165
|
-
aiProvider,
|
|
166
|
-
aiAltTags,
|
|
167
|
-
aiMediaCategorize,
|
|
168
|
-
aiMetaDescriptions,
|
|
169
|
-
aiReadability,
|
|
170
|
-
aiSchema,
|
|
171
|
-
aiBrandVoice,
|
|
172
|
-
aiWritingAssistant,
|
|
173
|
-
aiContentScoring,
|
|
174
|
-
aiTranslation,
|
|
175
142
|
...layoutPayload,
|
|
176
143
|
}),
|
|
177
144
|
})
|
|
@@ -351,198 +318,7 @@ export function Settings({
|
|
|
351
318
|
</Tabs.Content>
|
|
352
319
|
|
|
353
320
|
<Tabs.Content value="ai" className="space-y-4">
|
|
354
|
-
<
|
|
355
|
-
<h3 className="mb-1 text-sm font-semibold text-gray-900">AI Provider & API Key</h3>
|
|
356
|
-
<p className="mb-4 text-xs text-gray-500">
|
|
357
|
-
Connect an AI provider to enable intelligent content features
|
|
358
|
-
</p>
|
|
359
|
-
<div className="space-y-4">
|
|
360
|
-
<div>
|
|
361
|
-
<label className="mb-1 block text-sm font-medium text-gray-700">Provider</label>
|
|
362
|
-
<select
|
|
363
|
-
value={aiProvider}
|
|
364
|
-
onChange={(e) => setAiProvider(e.target.value)}
|
|
365
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
366
|
-
>
|
|
367
|
-
<option value="anthropic">Anthropic (Claude)</option>
|
|
368
|
-
<option value="openai">OpenAI (GPT)</option>
|
|
369
|
-
<option value="google">Google (Gemini)</option>
|
|
370
|
-
</select>
|
|
371
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
372
|
-
{aiProvider === 'anthropic' &&
|
|
373
|
-
'Recommended. Best for content analysis, brand voice learning, and nuanced writing.'}
|
|
374
|
-
{aiProvider === 'openai' &&
|
|
375
|
-
'Strong alternative. Good for general content generation and image understanding.'}
|
|
376
|
-
{aiProvider === 'google' &&
|
|
377
|
-
'Multimodal. Excellent for image analysis and multilingual content.'}
|
|
378
|
-
</p>
|
|
379
|
-
</div>
|
|
380
|
-
<div>
|
|
381
|
-
<label className="mb-1 block text-sm font-medium text-gray-700">API Key</label>
|
|
382
|
-
<div className="flex gap-2">
|
|
383
|
-
<div className="relative flex-1">
|
|
384
|
-
<input
|
|
385
|
-
type={showApiKey ? 'text' : 'password'}
|
|
386
|
-
value={aiApiKey}
|
|
387
|
-
onChange={(e) => setAiApiKey(e.target.value)}
|
|
388
|
-
placeholder={
|
|
389
|
-
aiProvider === 'anthropic'
|
|
390
|
-
? 'sk-ant-...'
|
|
391
|
-
: aiProvider === 'openai'
|
|
392
|
-
? 'sk-...'
|
|
393
|
-
: 'AIza...'
|
|
394
|
-
}
|
|
395
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2 pr-10 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
396
|
-
/>
|
|
397
|
-
<button
|
|
398
|
-
type="button"
|
|
399
|
-
onClick={() => setShowApiKey(!showApiKey)}
|
|
400
|
-
className="absolute top-1/2 right-2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600"
|
|
401
|
-
>
|
|
402
|
-
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
403
|
-
</button>
|
|
404
|
-
</div>
|
|
405
|
-
<button
|
|
406
|
-
type="button"
|
|
407
|
-
onClick={() => {
|
|
408
|
-
if (aiApiKey) toast.success('API key verified successfully')
|
|
409
|
-
else toast.error('Please enter an API key first')
|
|
410
|
-
}}
|
|
411
|
-
className="rounded-lg border border-gray-300 px-4 py-2 text-sm transition-colors hover:bg-gray-50"
|
|
412
|
-
>
|
|
413
|
-
Verify
|
|
414
|
-
</button>
|
|
415
|
-
</div>
|
|
416
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
417
|
-
Your key is encrypted at rest (AES-256-GCM) and never exposed to the client
|
|
418
|
-
</p>
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
424
|
-
<div className="mb-1 flex items-center gap-2">
|
|
425
|
-
<Image className="h-4 w-4 text-blue-600" />
|
|
426
|
-
<h3 className="text-sm font-semibold text-gray-900">Media Intelligence</h3>
|
|
427
|
-
</div>
|
|
428
|
-
<p className="mb-4 text-xs text-gray-500">AI-powered image and media analysis</p>
|
|
429
|
-
<div className="space-y-4">
|
|
430
|
-
<ToggleSetting
|
|
431
|
-
label="Auto Alt-Tag Generation"
|
|
432
|
-
description="Scan uploaded images and automatically generate descriptive alt text for accessibility and SEO"
|
|
433
|
-
checked={aiAltTags}
|
|
434
|
-
onChange={setAiAltTags}
|
|
435
|
-
/>
|
|
436
|
-
<ToggleSetting
|
|
437
|
-
label="Media Auto-Categorization"
|
|
438
|
-
description="Automatically tag and categorize uploaded media based on visual content analysis"
|
|
439
|
-
checked={aiMediaCategorize}
|
|
440
|
-
onChange={setAiMediaCategorize}
|
|
441
|
-
/>
|
|
442
|
-
</div>
|
|
443
|
-
</div>
|
|
444
|
-
|
|
445
|
-
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
446
|
-
<div className="mb-1 flex items-center gap-2">
|
|
447
|
-
<FileCode2 className="h-4 w-4 text-purple-600" />
|
|
448
|
-
<h3 className="text-sm font-semibold text-gray-900">Content SEO</h3>
|
|
449
|
-
</div>
|
|
450
|
-
<p className="mb-4 text-xs text-gray-500">
|
|
451
|
-
AI-driven SEO optimization for your content
|
|
452
|
-
</p>
|
|
453
|
-
<div className="space-y-4">
|
|
454
|
-
<ToggleSetting
|
|
455
|
-
label="Meta Description Generation"
|
|
456
|
-
description="Auto-generate optimized meta descriptions by digesting page content"
|
|
457
|
-
checked={aiMetaDescriptions}
|
|
458
|
-
onChange={setAiMetaDescriptions}
|
|
459
|
-
/>
|
|
460
|
-
<ToggleSetting
|
|
461
|
-
label="Readability Analysis"
|
|
462
|
-
description="Score content readability (Flesch-Kincaid, Gunning Fog) with improvement suggestions"
|
|
463
|
-
checked={aiReadability}
|
|
464
|
-
onChange={setAiReadability}
|
|
465
|
-
/>
|
|
466
|
-
<ToggleSetting
|
|
467
|
-
label="Schema.org Enrichment"
|
|
468
|
-
description="Automatically detect content types and generate appropriate Schema.org JSON-LD markup"
|
|
469
|
-
checked={aiSchema}
|
|
470
|
-
onChange={setAiSchema}
|
|
471
|
-
/>
|
|
472
|
-
</div>
|
|
473
|
-
</div>
|
|
474
|
-
|
|
475
|
-
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
476
|
-
<div className="mb-1 flex items-center gap-2">
|
|
477
|
-
<MessageSquare className="h-4 w-4 text-green-600" />
|
|
478
|
-
<h3 className="text-sm font-semibold text-gray-900">Brand Voice & Writing</h3>
|
|
479
|
-
</div>
|
|
480
|
-
<p className="mb-4 text-xs text-gray-500">
|
|
481
|
-
AI that understands and writes in your brand's voice
|
|
482
|
-
</p>
|
|
483
|
-
<div className="space-y-4">
|
|
484
|
-
<ToggleSetting
|
|
485
|
-
label="Brand Voice Training"
|
|
486
|
-
description="Analyze existing content to learn your brand's tone, style, and vocabulary"
|
|
487
|
-
checked={aiBrandVoice}
|
|
488
|
-
onChange={setAiBrandVoice}
|
|
489
|
-
/>
|
|
490
|
-
<ToggleSetting
|
|
491
|
-
label="AI Writing Assistant"
|
|
492
|
-
description="In-editor AI helper for drafting, rewriting, and expanding content in your brand voice"
|
|
493
|
-
checked={aiWritingAssistant}
|
|
494
|
-
onChange={setAiWritingAssistant}
|
|
495
|
-
/>
|
|
496
|
-
</div>
|
|
497
|
-
</div>
|
|
498
|
-
|
|
499
|
-
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
500
|
-
<div className="mb-1 flex items-center gap-2">
|
|
501
|
-
<Sparkles className="h-4 w-4 text-yellow-600" />
|
|
502
|
-
<h3 className="text-sm font-semibold text-gray-900">Content Quality</h3>
|
|
503
|
-
</div>
|
|
504
|
-
<p className="mb-4 text-xs text-gray-500">
|
|
505
|
-
Automated quality scoring and content intelligence
|
|
506
|
-
</p>
|
|
507
|
-
<div className="space-y-4">
|
|
508
|
-
<ToggleSetting
|
|
509
|
-
label="Content Scoring"
|
|
510
|
-
description="Automatically score content quality based on structure, readability, SEO, and completeness"
|
|
511
|
-
checked={aiContentScoring}
|
|
512
|
-
onChange={setAiContentScoring}
|
|
513
|
-
/>
|
|
514
|
-
<ToggleSetting
|
|
515
|
-
label="AI-Powered Translation"
|
|
516
|
-
description="Translate content into target languages while preserving tone and meaning"
|
|
517
|
-
checked={aiTranslation}
|
|
518
|
-
onChange={setAiTranslation}
|
|
519
|
-
/>
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
|
|
523
|
-
<div className="rounded-lg border border-indigo-200 bg-indigo-50 p-4">
|
|
524
|
-
<div className="mb-1 flex items-center gap-2">
|
|
525
|
-
<BookOpen className="h-4 w-4 text-indigo-600" />
|
|
526
|
-
<h3 className="text-sm font-semibold text-indigo-900">AI Usage This Month</h3>
|
|
527
|
-
</div>
|
|
528
|
-
<div className="mt-3 grid grid-cols-3 gap-4">
|
|
529
|
-
<div>
|
|
530
|
-
<div className="text-lg font-semibold text-indigo-900">0</div>
|
|
531
|
-
<div className="text-xs text-indigo-700">API Calls</div>
|
|
532
|
-
</div>
|
|
533
|
-
<div>
|
|
534
|
-
<div className="text-lg font-semibold text-indigo-900">0</div>
|
|
535
|
-
<div className="text-xs text-indigo-700">Tokens Used</div>
|
|
536
|
-
</div>
|
|
537
|
-
<div>
|
|
538
|
-
<div className="text-lg font-semibold text-indigo-900">$0.00</div>
|
|
539
|
-
<div className="text-xs text-indigo-700">Estimated Cost</div>
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
<p className="mt-2 text-xs text-indigo-600">
|
|
543
|
-
Add an API key above to start using AI features
|
|
544
|
-
</p>
|
|
545
|
-
</div>
|
|
321
|
+
<AiStatusTab />
|
|
546
322
|
</Tabs.Content>
|
|
547
323
|
|
|
548
324
|
<Tabs.Content value="users">
|
|
@@ -558,7 +334,7 @@ export function Settings({
|
|
|
558
334
|
</Tabs.Content>
|
|
559
335
|
</Tabs.Root>
|
|
560
336
|
|
|
561
|
-
{
|
|
337
|
+
{GLOBAL_SAVE_TABS.has(activeTab) && (
|
|
562
338
|
<div className="mt-6 flex justify-end">
|
|
563
339
|
<button
|
|
564
340
|
type="button"
|
|
@@ -574,37 +350,140 @@ export function Settings({
|
|
|
574
350
|
)
|
|
575
351
|
}
|
|
576
352
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// AI Status (read-only)
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
interface AiStatus {
|
|
358
|
+
installed: boolean
|
|
359
|
+
configured: boolean
|
|
360
|
+
provider: string | null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const PROVIDER_LABELS: Record<string, string> = {
|
|
364
|
+
anthropic: 'Anthropic (Claude)',
|
|
365
|
+
openai: 'OpenAI (GPT)',
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Honest, read-only view of the AI integration. AI is configured server-side
|
|
370
|
+
* via the `@actuate-media/plugin-ai` plugin reading environment variables —
|
|
371
|
+
* there is intentionally no API-key field here, because the dashboard never
|
|
372
|
+
* stores provider keys. This panel just reports current status.
|
|
373
|
+
*/
|
|
374
|
+
function AiStatusTab() {
|
|
375
|
+
const { data, loading, error, refetch } = useApiData<AiStatus>('/ai/status')
|
|
376
|
+
|
|
377
|
+
if (loading) {
|
|
378
|
+
return (
|
|
379
|
+
<div className="flex h-40 items-center justify-center">
|
|
380
|
+
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
|
381
|
+
</div>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (error) {
|
|
386
|
+
return (
|
|
387
|
+
<div className="flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
388
|
+
<AlertTriangle className="h-5 w-5 shrink-0 text-red-600" />
|
|
389
|
+
<span className="flex-1 text-sm text-red-800">{error}</span>
|
|
390
|
+
<button
|
|
391
|
+
onClick={refetch}
|
|
392
|
+
className="rounded-lg border border-red-300 px-3 py-1 text-sm text-red-700 transition-colors hover:bg-red-100"
|
|
393
|
+
>
|
|
394
|
+
Retry
|
|
395
|
+
</button>
|
|
396
|
+
</div>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const installed = data?.installed ?? false
|
|
401
|
+
const configured = data?.configured ?? false
|
|
402
|
+
const providerLabel = data?.provider ? (PROVIDER_LABELS[data.provider] ?? data.provider) : null
|
|
403
|
+
|
|
404
|
+
const status: { tone: 'ok' | 'warn' | 'off'; title: string; detail: string } = !installed
|
|
405
|
+
? {
|
|
406
|
+
tone: 'off',
|
|
407
|
+
title: 'AI features are not installed',
|
|
408
|
+
detail:
|
|
409
|
+
'Add the @actuate-media/plugin-ai plugin to your CMS config to enable AI-powered content features.',
|
|
410
|
+
}
|
|
411
|
+
: configured
|
|
412
|
+
? {
|
|
413
|
+
tone: 'ok',
|
|
414
|
+
title: providerLabel ? `AI is active — ${providerLabel}` : 'AI is active',
|
|
415
|
+
detail:
|
|
416
|
+
'A provider is configured and ready. AI-powered features in the editor and media library will use it.',
|
|
417
|
+
}
|
|
418
|
+
: {
|
|
419
|
+
tone: 'warn',
|
|
420
|
+
title: 'AI plugin installed, but no provider is configured',
|
|
421
|
+
detail:
|
|
422
|
+
'Set a provider and API key where you register the plugin, e.g. aiPlugin({ provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY }).',
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const toneStyles: Record<typeof status.tone, string> = {
|
|
426
|
+
ok: 'border-green-200 bg-green-50',
|
|
427
|
+
warn: 'border-amber-200 bg-amber-50',
|
|
428
|
+
off: 'border-gray-200 bg-gray-50',
|
|
429
|
+
}
|
|
430
|
+
|
|
588
431
|
return (
|
|
589
|
-
<div className="
|
|
590
|
-
<div className=
|
|
591
|
-
|
|
592
|
-
|
|
432
|
+
<div className="space-y-4">
|
|
433
|
+
<div className={`flex items-start gap-3 rounded-lg border p-4 ${toneStyles[status.tone]}`}>
|
|
434
|
+
{status.tone === 'ok' ? (
|
|
435
|
+
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-green-600" />
|
|
436
|
+
) : status.tone === 'warn' ? (
|
|
437
|
+
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-600" />
|
|
438
|
+
) : (
|
|
439
|
+
<Bot className="mt-0.5 h-5 w-5 shrink-0 text-gray-500" />
|
|
440
|
+
)}
|
|
441
|
+
<div className="flex-1">
|
|
442
|
+
<h3 className="text-sm font-semibold text-gray-900">{status.title}</h3>
|
|
443
|
+
<p className="mt-0.5 text-xs text-gray-600">{status.detail}</p>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
448
|
+
<h3 className="mb-1 text-sm font-semibold text-gray-900">How AI is configured</h3>
|
|
449
|
+
<p className="mb-3 text-xs text-gray-500">
|
|
450
|
+
AI is set up in code, not from this dashboard. Provider credentials live in environment
|
|
451
|
+
variables and are never entered or stored here.
|
|
452
|
+
</p>
|
|
453
|
+
<ul className="space-y-1.5 text-xs text-gray-600">
|
|
454
|
+
<li className="flex items-start gap-2">
|
|
455
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-[10px] font-bold text-blue-600">
|
|
456
|
+
1
|
|
457
|
+
</span>
|
|
458
|
+
<span>
|
|
459
|
+
Install the AI plugin:{' '}
|
|
460
|
+
<code className="rounded bg-gray-100 px-1 font-mono">@actuate-media/plugin-ai</code>.
|
|
461
|
+
</span>
|
|
462
|
+
</li>
|
|
463
|
+
<li className="flex items-start gap-2">
|
|
464
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-[10px] font-bold text-blue-600">
|
|
465
|
+
2
|
|
466
|
+
</span>
|
|
467
|
+
<span>
|
|
468
|
+
Set <code className="rounded bg-gray-100 px-1 font-mono">ANTHROPIC_API_KEY</code> or{' '}
|
|
469
|
+
<code className="rounded bg-gray-100 px-1 font-mono">OPENAI_API_KEY</code> in your
|
|
470
|
+
environment.
|
|
471
|
+
</span>
|
|
472
|
+
</li>
|
|
473
|
+
<li className="flex items-start gap-2">
|
|
474
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-[10px] font-bold text-blue-600">
|
|
475
|
+
3
|
|
476
|
+
</span>
|
|
477
|
+
<span>
|
|
478
|
+
Register it in your CMS config:{' '}
|
|
479
|
+
<code className="rounded bg-gray-100 px-1 font-mono">
|
|
480
|
+
aiPlugin({ provider, apiKey })
|
|
481
|
+
</code>
|
|
482
|
+
.
|
|
483
|
+
</span>
|
|
484
|
+
</li>
|
|
485
|
+
</ul>
|
|
593
486
|
</div>
|
|
594
|
-
<button
|
|
595
|
-
type="button"
|
|
596
|
-
role="switch"
|
|
597
|
-
aria-checked={checked}
|
|
598
|
-
aria-label={label}
|
|
599
|
-
onClick={() => onChange(!checked)}
|
|
600
|
-
className={`focus:ring-primary relative h-6 w-11 shrink-0 rounded-full transition-colors focus:ring-2 focus:outline-none ${checked ? 'bg-primary' : 'bg-muted'}`}
|
|
601
|
-
>
|
|
602
|
-
<span
|
|
603
|
-
className={`absolute top-0.5 block h-5 w-5 rounded-full bg-white transition-transform ${
|
|
604
|
-
checked ? 'translate-x-[22px]' : 'translate-x-0.5'
|
|
605
|
-
}`}
|
|
606
|
-
/>
|
|
607
|
-
</button>
|
|
608
487
|
</div>
|
|
609
488
|
)
|
|
610
489
|
}
|