@actuate-media/cms-admin 0.35.0 → 0.36.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.
- package/dist/__tests__/views/ai-settings.render.test.js +22 -0
- package/dist/__tests__/views/ai-settings.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +14 -13
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/Profile.d.ts.map +1 -1
- package/dist/views/Profile.js +1 -1
- package/dist/views/Profile.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +25 -20
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/Users.js.map +1 -1
- package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
- package/dist/views/settings/AISettingsTab.js +42 -43
- package/dist/views/settings/AISettingsTab.js.map +1 -1
- package/dist/views/settings/AiFeaturesCard.d.ts.map +1 -1
- package/dist/views/settings/AiFeaturesCard.js +2 -3
- package/dist/views/settings/AiFeaturesCard.js.map +1 -1
- package/dist/views/settings/BrandVoiceCard.d.ts +12 -0
- package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -0
- package/dist/views/settings/BrandVoiceCard.js +25 -0
- package/dist/views/settings/BrandVoiceCard.js.map +1 -0
- package/dist/views/settings/GeneralSettingsTab.js +1 -1
- package/dist/views/settings/LanguageRegionCard.d.ts.map +1 -1
- package/dist/views/settings/LanguageRegionCard.js +2 -1
- package/dist/views/settings/LanguageRegionCard.js.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.js +3 -3
- package/dist/views/settings/MonthlyUsageCard.js.map +1 -1
- package/dist/views/settings/ProviderConnectionCard.js +1 -1
- package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
- package/dist/views/settings/SecurityStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SecurityStatusCard.js +2 -3
- package/dist/views/settings/SecurityStatusCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.js +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.js +3 -4
- package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -1
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +5 -8
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.js +3 -2
- package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
- package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
- package/dist/views/settings/SiteInformationCard.js +4 -4
- package/dist/views/settings/SiteInformationCard.js.map +1 -1
- package/dist/views/settings/components.d.ts +31 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +22 -0
- package/dist/views/settings/components.js.map +1 -1
- package/dist/views/settings/securityPrimitives.d.ts +11 -3
- package/dist/views/settings/securityPrimitives.d.ts.map +1 -1
- package/dist/views/settings/securityPrimitives.js +10 -3
- package/dist/views/settings/securityPrimitives.js.map +1 -1
- package/dist/views/settings/useAiSettings.d.ts +12 -0
- package/dist/views/settings/useAiSettings.d.ts.map +1 -1
- package/dist/views/settings/useAiSettings.js +51 -0
- package/dist/views/settings/useAiSettings.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/views/ai-settings.render.test.tsx +36 -0
- package/src/views/ApiKeys.tsx +190 -154
- package/src/views/Profile.tsx +4 -2
- package/src/views/Settings.tsx +154 -143
- package/src/views/Users.tsx +1 -1
- package/src/views/settings/AISettingsTab.tsx +66 -76
- package/src/views/settings/AiFeaturesCard.tsx +6 -12
- package/src/views/settings/BrandVoiceCard.tsx +139 -0
- package/src/views/settings/GeneralSettingsTab.tsx +1 -1
- package/src/views/settings/LanguageRegionCard.tsx +5 -7
- package/src/views/settings/MonthlyUsageCard.tsx +6 -10
- package/src/views/settings/ProviderConnectionCard.tsx +1 -1
- package/src/views/settings/SecurityStatusCard.tsx +8 -11
- package/src/views/settings/SecurityTab.tsx +1 -1
- package/src/views/settings/SeoRobotsDefaultsCard.tsx +7 -14
- package/src/views/settings/SeoSettingsTab.tsx +17 -26
- package/src/views/settings/SiteIndexStatusCard.tsx +10 -22
- package/src/views/settings/SiteInformationCard.tsx +3 -9
- package/src/views/settings/components.tsx +72 -1
- package/src/views/settings/securityPrimitives.tsx +14 -9
- package/src/views/settings/useAiSettings.ts +76 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { AlertTriangle,
|
|
3
|
+
import { AlertTriangle, Info, Loader2, Plus } from 'lucide-react'
|
|
4
4
|
import { useId, useMemo, useState } from 'react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { AiFeaturesCard } from './AiFeaturesCard.js'
|
|
7
|
+
import { BrandVoiceCard } from './BrandVoiceCard.js'
|
|
7
8
|
import { ModelSelector } from './ModelSelector.js'
|
|
8
9
|
import { MonthlyUsageCard } from './MonthlyUsageCard.js'
|
|
9
10
|
import { ProviderConnectionCard } from './ProviderConnectionCard.js'
|
|
10
|
-
import { SettingsSaveBar } from './components.js'
|
|
11
|
+
import { SettingsCard, SettingsSaveBar } from './components.js'
|
|
11
12
|
import {
|
|
12
13
|
useAiSettings,
|
|
13
14
|
type AiProviderKey,
|
|
@@ -40,7 +41,7 @@ export function AISettingsTab({ role }: AISettingsTabProps) {
|
|
|
40
41
|
<div className="space-y-4" aria-busy="true" aria-live="polite">
|
|
41
42
|
<span className="sr-only">Loading AI settings…</span>
|
|
42
43
|
{[0, 1, 2].map((i) => (
|
|
43
|
-
<div key={i} className="border-border bg-card rounded-lg border p-
|
|
44
|
+
<div key={i} className="border-border bg-card rounded-lg border p-6">
|
|
44
45
|
<div className="bg-muted mb-4 h-4 w-40 animate-pulse rounded" />
|
|
45
46
|
<div className="space-y-3">
|
|
46
47
|
<div className="bg-muted h-9 w-full animate-pulse rounded" />
|
|
@@ -88,6 +89,7 @@ export function AISettingsTab({ role }: AISettingsTabProps) {
|
|
|
88
89
|
onToggle={ai.toggleFeature}
|
|
89
90
|
onSetFeatureModel={ai.setFeatureModel}
|
|
90
91
|
/>
|
|
92
|
+
<BrandVoiceCard value={ai.form.brandVoice} canEdit={canEdit} onChange={ai.setBrandVoice} />
|
|
91
93
|
<MonthlyUsageCard
|
|
92
94
|
usage={ai.usage}
|
|
93
95
|
monthlyTokenLimit={ai.form.monthlyTokenLimit}
|
|
@@ -123,7 +125,6 @@ export function AISettingsTab({ role }: AISettingsTabProps) {
|
|
|
123
125
|
// ---------------------------------------------------------------------------
|
|
124
126
|
|
|
125
127
|
function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean }) {
|
|
126
|
-
const headingId = useId()
|
|
127
128
|
const providerSelectId = useId()
|
|
128
129
|
const [refreshing, setRefreshing] = useState(false)
|
|
129
130
|
|
|
@@ -148,21 +149,10 @@ function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean })
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
return (
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
</span>
|
|
156
|
-
<div>
|
|
157
|
-
<h3 id={headingId} className="text-foreground text-base font-medium">
|
|
158
|
-
AI Assistant
|
|
159
|
-
</h3>
|
|
160
|
-
<p className="text-muted-foreground text-sm">
|
|
161
|
-
The default provider and model used across AI features.
|
|
162
|
-
</p>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
152
|
+
<SettingsCard
|
|
153
|
+
title="AI Assistant"
|
|
154
|
+
description="The default provider and model used across AI features."
|
|
155
|
+
>
|
|
166
156
|
{ai.providers.length === 0 ? (
|
|
167
157
|
<div className="border-border bg-muted/40 text-muted-foreground rounded-md border border-dashed p-4 text-sm">
|
|
168
158
|
No AI providers are connected yet. Add one below to enable AI features.
|
|
@@ -204,7 +194,7 @@ function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean })
|
|
|
204
194
|
/>
|
|
205
195
|
</div>
|
|
206
196
|
)}
|
|
207
|
-
</
|
|
197
|
+
</SettingsCard>
|
|
208
198
|
)
|
|
209
199
|
}
|
|
210
200
|
|
|
@@ -213,16 +203,14 @@ function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean })
|
|
|
213
203
|
// ---------------------------------------------------------------------------
|
|
214
204
|
|
|
215
205
|
function ProviderConnectionsSection({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean }) {
|
|
216
|
-
const headingId = useId()
|
|
217
206
|
const [adding, setAdding] = useState(false)
|
|
218
207
|
|
|
219
208
|
return (
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{canEdit && !adding && (
|
|
209
|
+
<SettingsCard
|
|
210
|
+
title="Provider connections"
|
|
211
|
+
description="Connect Anthropic, OpenAI, xAI, or a custom OpenAI-compatible endpoint."
|
|
212
|
+
actions={
|
|
213
|
+
canEdit && !adding ? (
|
|
226
214
|
<button
|
|
227
215
|
type="button"
|
|
228
216
|
onClick={() => setAdding(true)}
|
|
@@ -231,57 +219,59 @@ function ProviderConnectionsSection({ ai, canEdit }: { ai: UseAiSettings; canEdi
|
|
|
231
219
|
<Plus size={14} aria-hidden="true" />
|
|
232
220
|
Add provider
|
|
233
221
|
</button>
|
|
222
|
+
) : undefined
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
<div className="space-y-3">
|
|
226
|
+
{adding && (
|
|
227
|
+
<AddProviderForm
|
|
228
|
+
ai={ai}
|
|
229
|
+
onDone={() => setAdding(false)}
|
|
230
|
+
onCancel={() => setAdding(false)}
|
|
231
|
+
/>
|
|
234
232
|
)}
|
|
235
|
-
</div>
|
|
236
233
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}}
|
|
273
|
-
onDisconnect={async () => {
|
|
274
|
-
const res = await ai.deleteProvider(p.id)
|
|
275
|
-
if (!res.ok) toast.error(res.error ?? 'Failed to disconnect.')
|
|
276
|
-
else toast.success(`${p.displayName} disconnected.`)
|
|
277
|
-
}}
|
|
278
|
-
/>
|
|
279
|
-
))}
|
|
234
|
+
{ai.providers.map((p) => (
|
|
235
|
+
<ProviderConnectionCard
|
|
236
|
+
key={p.id}
|
|
237
|
+
provider={p}
|
|
238
|
+
canEdit={canEdit}
|
|
239
|
+
busy={ai.busyProviderId === p.id}
|
|
240
|
+
onTest={async () => {
|
|
241
|
+
const res = await ai.testProvider(p.id)
|
|
242
|
+
if (!res.ok) toast.error(res.error ?? res.message ?? 'Connection test failed.')
|
|
243
|
+
else toast.success(res.message ?? 'Connection successful.')
|
|
244
|
+
}}
|
|
245
|
+
onSyncModels={async () => {
|
|
246
|
+
const res = await ai.syncModels(p.id)
|
|
247
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to refresh models.')
|
|
248
|
+
else if (res.fromFallback)
|
|
249
|
+
toast.warning(res.warning ?? 'Showing the curated model list.')
|
|
250
|
+
else toast.success('Model list refreshed.')
|
|
251
|
+
}}
|
|
252
|
+
onUpdateKey={async (apiKey) => {
|
|
253
|
+
const res = await ai.updateProvider(p.id, { apiKey })
|
|
254
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to update key.')
|
|
255
|
+
else toast.success('API key updated.')
|
|
256
|
+
}}
|
|
257
|
+
onUpdateBaseUrl={async (baseUrl) => {
|
|
258
|
+
const res = await ai.updateProvider(p.id, { baseUrl })
|
|
259
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to update base URL.')
|
|
260
|
+
else toast.success('Base URL updated.')
|
|
261
|
+
}}
|
|
262
|
+
onDisconnect={async () => {
|
|
263
|
+
const res = await ai.deleteProvider(p.id)
|
|
264
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to disconnect.')
|
|
265
|
+
else toast.success(`${p.displayName} disconnected.`)
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
))}
|
|
280
269
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
270
|
+
{ai.providers.length === 0 && !adding && (
|
|
271
|
+
<p className="text-muted-foreground text-sm">No providers connected.</p>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</SettingsCard>
|
|
285
275
|
)
|
|
286
276
|
}
|
|
287
277
|
|
|
@@ -330,7 +320,7 @@ function AddProviderForm({
|
|
|
330
320
|
}
|
|
331
321
|
|
|
332
322
|
return (
|
|
333
|
-
<div className="border-border bg-
|
|
323
|
+
<div className="border-border bg-background rounded-lg border p-4">
|
|
334
324
|
<h4 className="text-foreground mb-3 text-base font-medium">Add a provider</h4>
|
|
335
325
|
<div className="space-y-3">
|
|
336
326
|
<div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { useId } from 'react'
|
|
5
|
-
import { SettingsToggleRow } from './components.js'
|
|
5
|
+
import { SettingsCard, SettingsToggleRow } from './components.js'
|
|
6
6
|
import { capabilityLabel, effectiveFeatureModel, isModelCompatible } from './featureModel.js'
|
|
7
7
|
import type { AiFeatureView, AiModelView } from './useAiSettings.js'
|
|
8
8
|
|
|
@@ -31,17 +31,11 @@ export function AiFeaturesCard({
|
|
|
31
31
|
onToggle: (key: string, enabled: boolean) => void
|
|
32
32
|
onSetFeatureModel: (key: string, modelId: string) => void
|
|
33
33
|
}) {
|
|
34
|
-
const headingId = useId()
|
|
35
34
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<p className="text-muted-foreground mb-4 text-sm">
|
|
41
|
-
Turn individual AI-powered features on or off, and optionally route each to a specific
|
|
42
|
-
model.
|
|
43
|
-
</p>
|
|
44
|
-
|
|
35
|
+
<SettingsCard
|
|
36
|
+
title="AI Features"
|
|
37
|
+
description="Turn individual AI-powered features on or off, and optionally route each to a specific model."
|
|
38
|
+
>
|
|
45
39
|
{features.length === 0 ? (
|
|
46
40
|
<p className="text-muted-foreground text-sm">No AI features are available.</p>
|
|
47
41
|
) : (
|
|
@@ -59,7 +53,7 @@ export function AiFeaturesCard({
|
|
|
59
53
|
))}
|
|
60
54
|
</div>
|
|
61
55
|
)}
|
|
62
|
-
</
|
|
56
|
+
</SettingsCard>
|
|
63
57
|
)
|
|
64
58
|
}
|
|
65
59
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react'
|
|
4
|
+
import { SettingsCard, SettingsToggleRow } from './components.js'
|
|
5
|
+
import type { BrandVoiceForm } from './useAiSettings.js'
|
|
6
|
+
|
|
7
|
+
const INPUT_CLASS =
|
|
8
|
+
'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60'
|
|
9
|
+
const TEXTAREA_CLASS = `${INPUT_CLASS} min-h-[88px] resize-y`
|
|
10
|
+
const LABEL_CLASS = 'mb-1 block text-sm font-medium text-foreground'
|
|
11
|
+
|
|
12
|
+
const TONE_MAX = 400
|
|
13
|
+
const GUIDELINES_MAX = 2000
|
|
14
|
+
const SAMPLE_MAX = 2000
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Brand voice — a reusable description of how generated copy should sound. When
|
|
18
|
+
* enabled, it is injected into the system prompt of content-generation features
|
|
19
|
+
* (co-author, SEO titles/descriptions) so every AI surface writes on-brand.
|
|
20
|
+
*/
|
|
21
|
+
export function BrandVoiceCard({
|
|
22
|
+
value,
|
|
23
|
+
canEdit,
|
|
24
|
+
onChange,
|
|
25
|
+
}: {
|
|
26
|
+
value: BrandVoiceForm
|
|
27
|
+
canEdit: boolean
|
|
28
|
+
onChange: (patch: Partial<BrandVoiceForm>) => void
|
|
29
|
+
}) {
|
|
30
|
+
const nameId = useId()
|
|
31
|
+
const toneId = useId()
|
|
32
|
+
const audienceId = useId()
|
|
33
|
+
const guidelinesId = useId()
|
|
34
|
+
const sampleId = useId()
|
|
35
|
+
const disabled = !canEdit
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<SettingsCard
|
|
39
|
+
title="Brand voice"
|
|
40
|
+
description="Teach the AI how your brand sounds. Applied to content writing and SEO copy."
|
|
41
|
+
>
|
|
42
|
+
<div className="space-y-4">
|
|
43
|
+
<SettingsToggleRow
|
|
44
|
+
label="Apply brand voice"
|
|
45
|
+
description="When on, generated copy follows the tone and guidelines below."
|
|
46
|
+
checked={value.enabled}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
onChange={(v) => onChange({ enabled: v })}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<div className="border-border space-y-4 border-t pt-4">
|
|
52
|
+
<div>
|
|
53
|
+
<label htmlFor={nameId} className={LABEL_CLASS}>
|
|
54
|
+
Name
|
|
55
|
+
</label>
|
|
56
|
+
<input
|
|
57
|
+
id={nameId}
|
|
58
|
+
type="text"
|
|
59
|
+
value={value.name}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
placeholder="e.g. Acme Marketing"
|
|
62
|
+
onChange={(e) => onChange({ name: e.target.value })}
|
|
63
|
+
className={INPUT_CLASS}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div>
|
|
68
|
+
<label htmlFor={toneId} className={LABEL_CLASS}>
|
|
69
|
+
Tone
|
|
70
|
+
</label>
|
|
71
|
+
<input
|
|
72
|
+
id={toneId}
|
|
73
|
+
type="text"
|
|
74
|
+
value={value.tone}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
maxLength={TONE_MAX}
|
|
77
|
+
placeholder="e.g. confident, warm, plain-spoken"
|
|
78
|
+
onChange={(e) => onChange({ tone: e.target.value })}
|
|
79
|
+
className={INPUT_CLASS}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div>
|
|
84
|
+
<label htmlFor={audienceId} className={LABEL_CLASS}>
|
|
85
|
+
Audience <span className="text-muted-foreground font-normal">(optional)</span>
|
|
86
|
+
</label>
|
|
87
|
+
<input
|
|
88
|
+
id={audienceId}
|
|
89
|
+
type="text"
|
|
90
|
+
value={value.audience}
|
|
91
|
+
disabled={disabled}
|
|
92
|
+
placeholder="e.g. busy IT directors at mid-market SaaS companies"
|
|
93
|
+
onChange={(e) => onChange({ audience: e.target.value })}
|
|
94
|
+
className={INPUT_CLASS}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div>
|
|
99
|
+
<label htmlFor={guidelinesId} className={LABEL_CLASS}>
|
|
100
|
+
Guidelines <span className="text-muted-foreground font-normal">(optional)</span>
|
|
101
|
+
</label>
|
|
102
|
+
<textarea
|
|
103
|
+
id={guidelinesId}
|
|
104
|
+
value={value.guidelines}
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
maxLength={GUIDELINES_MAX}
|
|
107
|
+
placeholder={
|
|
108
|
+
'Do: lead with the benefit, use active voice.\nDon\u2019t: use jargon, exclamation points, or hype.'
|
|
109
|
+
}
|
|
110
|
+
onChange={(e) => onChange({ guidelines: e.target.value })}
|
|
111
|
+
className={TEXTAREA_CLASS}
|
|
112
|
+
/>
|
|
113
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
114
|
+
{value.guidelines.length}/{GUIDELINES_MAX}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div>
|
|
119
|
+
<label htmlFor={sampleId} className={LABEL_CLASS}>
|
|
120
|
+
Sample copy <span className="text-muted-foreground font-normal">(optional)</span>
|
|
121
|
+
</label>
|
|
122
|
+
<textarea
|
|
123
|
+
id={sampleId}
|
|
124
|
+
value={value.sampleText}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
maxLength={SAMPLE_MAX}
|
|
127
|
+
placeholder="Paste a paragraph of on-brand copy the AI can pattern-match against."
|
|
128
|
+
onChange={(e) => onChange({ sampleText: e.target.value })}
|
|
129
|
+
className={TEXTAREA_CLASS}
|
|
130
|
+
/>
|
|
131
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
132
|
+
{value.sampleText.length}/{SAMPLE_MAX}
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</SettingsCard>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -25,7 +25,7 @@ export function GeneralSettingsTab({ onNavigate, role }: GeneralSettingsTabProps
|
|
|
25
25
|
<div className="space-y-4" aria-busy="true" aria-live="polite">
|
|
26
26
|
<span className="sr-only">Loading settings…</span>
|
|
27
27
|
{[0, 1, 2, 3].map((i) => (
|
|
28
|
-
<div key={i} className="border-border bg-card rounded-lg border p-
|
|
28
|
+
<div key={i} className="border-border bg-card rounded-lg border p-6">
|
|
29
29
|
<div className="bg-muted mb-4 h-4 w-40 animate-pulse rounded" />
|
|
30
30
|
<div className="space-y-3">
|
|
31
31
|
<div className="bg-muted h-9 w-full animate-pulse rounded" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useId, useMemo } from 'react'
|
|
4
|
+
import { SettingsCard } from './components.js'
|
|
4
5
|
import type { GeneralSettingsForm, ValidationIssue } from './useGeneralSettings.js'
|
|
5
6
|
|
|
6
7
|
const INPUT_CLASS =
|
|
@@ -58,13 +59,10 @@ export function LanguageRegionCard({ form, setField, issues, canEdit }: Language
|
|
|
58
59
|
}, [form.timezone])
|
|
59
60
|
|
|
60
61
|
return (
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
<SettingsCard
|
|
63
|
+
title="Language & Region"
|
|
64
|
+
description="Default language and timezone applied across the admin and public site."
|
|
64
65
|
>
|
|
65
|
-
<h3 id={`${langId}-heading`} className="text-foreground mb-4 text-base font-medium">
|
|
66
|
-
Language & Region
|
|
67
|
-
</h3>
|
|
68
66
|
<div className="space-y-4">
|
|
69
67
|
<div>
|
|
70
68
|
<label htmlFor={langId} className={LABEL_CLASS}>
|
|
@@ -114,6 +112,6 @@ export function LanguageRegionCard({ form, setField, issues, canEdit }: Language
|
|
|
114
112
|
)}
|
|
115
113
|
</div>
|
|
116
114
|
</div>
|
|
117
|
-
</
|
|
115
|
+
</SettingsCard>
|
|
118
116
|
)
|
|
119
117
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useId } from 'react'
|
|
4
|
+
import { SettingsCard } from './components.js'
|
|
4
5
|
import type { AiUsageView } from './useAiSettings.js'
|
|
5
6
|
|
|
6
7
|
const INPUT_CLASS =
|
|
@@ -31,7 +32,6 @@ export function MonthlyUsageCard({
|
|
|
31
32
|
budgetError: string | null
|
|
32
33
|
canEdit: boolean
|
|
33
34
|
}) {
|
|
34
|
-
const headingId = useId()
|
|
35
35
|
const limitId = useId()
|
|
36
36
|
|
|
37
37
|
const used = usage?.totalTokens ?? 0
|
|
@@ -44,14 +44,10 @@ export function MonthlyUsageCard({
|
|
|
44
44
|
: 'This month'
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<p className="text-muted-foreground mb-4 text-sm">
|
|
52
|
-
Token usage for {periodLabel}. Aggregated from completed AI runs.
|
|
53
|
-
</p>
|
|
54
|
-
|
|
47
|
+
<SettingsCard
|
|
48
|
+
title="Monthly Usage"
|
|
49
|
+
description={`Token usage for ${periodLabel}. Aggregated from completed AI runs.`}
|
|
50
|
+
>
|
|
55
51
|
<div className="space-y-4">
|
|
56
52
|
<div>
|
|
57
53
|
<div className="mb-1 flex items-center justify-between text-sm">
|
|
@@ -113,6 +109,6 @@ export function MonthlyUsageCard({
|
|
|
113
109
|
)}
|
|
114
110
|
</div>
|
|
115
111
|
</div>
|
|
116
|
-
</
|
|
112
|
+
</SettingsCard>
|
|
117
113
|
)
|
|
118
114
|
}
|
|
@@ -94,7 +94,7 @@ export function ProviderConnectionCard({
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
return (
|
|
97
|
-
<div className="border-border bg-
|
|
97
|
+
<div className="border-border bg-background rounded-lg border p-4">
|
|
98
98
|
<div className="mb-3 flex items-start justify-between gap-3">
|
|
99
99
|
<div>
|
|
100
100
|
<div className="flex items-center gap-2">
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useId } from 'react'
|
|
4
3
|
import { Shield, ShieldAlert, ShieldCheck, ShieldX } from 'lucide-react'
|
|
5
4
|
import type { SecurityStatus, SecurityWarning } from '@actuate-media/cms-core/security-center'
|
|
5
|
+
import { SettingsCard } from './components.js'
|
|
6
6
|
import { CheckIcon } from './securityPrimitives.js'
|
|
7
7
|
|
|
8
8
|
const LEVEL_STYLES: Record<
|
|
@@ -40,16 +40,14 @@ export function SecurityStatusCard({
|
|
|
40
40
|
status: SecurityStatus
|
|
41
41
|
onNavigate?: (path: string) => void
|
|
42
42
|
}) {
|
|
43
|
-
const headingId = useId()
|
|
44
43
|
const style = LEVEL_STYLES[status.status]
|
|
45
44
|
const { Icon } = style
|
|
46
45
|
|
|
47
46
|
return (
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</h3>
|
|
47
|
+
<SettingsCard
|
|
48
|
+
title="Security Status"
|
|
49
|
+
description={status.description}
|
|
50
|
+
actions={
|
|
53
51
|
<span
|
|
54
52
|
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-sm font-medium ${style.pill}`}
|
|
55
53
|
>
|
|
@@ -57,9 +55,8 @@ export function SecurityStatusCard({
|
|
|
57
55
|
{/* Visible text label means the status never relies on color alone. */}
|
|
58
56
|
{style.word}
|
|
59
57
|
</span>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
}
|
|
59
|
+
>
|
|
63
60
|
<div className="grid grid-cols-1 gap-x-6 gap-y-1 sm:grid-cols-2">
|
|
64
61
|
{status.checks.map((check) => (
|
|
65
62
|
<button
|
|
@@ -105,6 +102,6 @@ export function SecurityStatusCard({
|
|
|
105
102
|
))}
|
|
106
103
|
</div>
|
|
107
104
|
)}
|
|
108
|
-
</
|
|
105
|
+
</SettingsCard>
|
|
109
106
|
)
|
|
110
107
|
}
|
|
@@ -12,7 +12,7 @@ import { useSecuritySettings } from './useSecuritySettings.js'
|
|
|
12
12
|
|
|
13
13
|
function CardSkeleton() {
|
|
14
14
|
return (
|
|
15
|
-
<div className="border-border bg-card animate-pulse rounded-lg border p-
|
|
15
|
+
<div className="border-border bg-card animate-pulse rounded-lg border p-6">
|
|
16
16
|
<div className="bg-muted mb-3 h-4 w-40 rounded" />
|
|
17
17
|
<div className="bg-muted mb-4 h-3 w-72 rounded" />
|
|
18
18
|
<div className="space-y-3">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { ConfirmDangerousSettingDialog, SettingsToggleRow } from './components.js'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { ConfirmDangerousSettingDialog, SettingsCard, SettingsToggleRow } from './components.js'
|
|
5
5
|
import type { GeneralSettingsForm, SiteIndexStatus } from './useGeneralSettings.js'
|
|
6
6
|
|
|
7
7
|
interface SeoRobotsDefaultsCardProps {
|
|
@@ -29,7 +29,6 @@ export function SeoRobotsDefaultsCard({
|
|
|
29
29
|
canEdit,
|
|
30
30
|
onNavigate,
|
|
31
31
|
}: SeoRobotsDefaultsCardProps) {
|
|
32
|
-
const headingId = useId()
|
|
33
32
|
const [pending, setPending] = useState<PendingChange | null>(null)
|
|
34
33
|
|
|
35
34
|
// Some toggles can take the whole production site out of search results, so
|
|
@@ -65,16 +64,10 @@ export function SeoRobotsDefaultsCard({
|
|
|
65
64
|
const sitemapStatus = sitemapCheck ? (sitemapCheck.value === 'Yes' ? 'Enabled' : 'Disabled') : '—'
|
|
66
65
|
|
|
67
66
|
return (
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<p className="text-muted-foreground mb-4 text-sm">
|
|
73
|
-
Set the site-wide default for search engine indexing. Individual pages can inherit or
|
|
74
|
-
override these rules in their SEO panel. These share the same configuration as the SEO
|
|
75
|
-
section.
|
|
76
|
-
</p>
|
|
77
|
-
|
|
67
|
+
<SettingsCard
|
|
68
|
+
title="SEO & Robots Defaults"
|
|
69
|
+
description="Set the site-wide default for search engine indexing. Individual pages can inherit or override these rules in their SEO panel. These share the same configuration as the SEO section."
|
|
70
|
+
>
|
|
78
71
|
<div className="space-y-4">
|
|
79
72
|
<SettingsToggleRow
|
|
80
73
|
label="Default No Index"
|
|
@@ -133,7 +126,7 @@ export function SeoRobotsDefaultsCard({
|
|
|
133
126
|
}}
|
|
134
127
|
onCancel={() => setPending(null)}
|
|
135
128
|
/>
|
|
136
|
-
</
|
|
129
|
+
</SettingsCard>
|
|
137
130
|
)
|
|
138
131
|
}
|
|
139
132
|
|