@actuate-media/cms-admin 0.56.2 → 0.57.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/CHANGELOG.md +15 -0
- package/dist/__tests__/views/ai-settings.render.test.js +38 -4
- package/dist/__tests__/views/ai-settings.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/AICoauthorBubbleMenu.d.ts.map +1 -1
- package/dist/components/AICoauthorBubbleMenu.js +5 -1
- package/dist/components/AICoauthorBubbleMenu.js.map +1 -1
- package/dist/components/BrandVoiceAlignmentBadge.d.ts +6 -0
- package/dist/components/BrandVoiceAlignmentBadge.d.ts.map +1 -0
- package/dist/components/BrandVoiceAlignmentBadge.js +11 -0
- package/dist/components/BrandVoiceAlignmentBadge.js.map +1 -0
- package/dist/components/CoauthorResultPopover.d.ts +5 -1
- package/dist/components/CoauthorResultPopover.d.ts.map +1 -1
- package/dist/components/CoauthorResultPopover.js +35 -6
- package/dist/components/CoauthorResultPopover.js.map +1 -1
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +5 -1
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorDrawer.js +43 -2
- package/dist/components/seo/SeoEditorDrawer.js.map +1 -1
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/lib/brand-voice-client.d.ts +21 -0
- package/dist/lib/brand-voice-client.d.ts.map +1 -0
- package/dist/lib/brand-voice-client.js +27 -0
- package/dist/lib/brand-voice-client.js.map +1 -0
- package/dist/lib/coauthor-client.d.ts +2 -0
- package/dist/lib/coauthor-client.d.ts.map +1 -1
- package/dist/lib/coauthor-client.js +1 -0
- package/dist/lib/coauthor-client.js.map +1 -1
- package/dist/lib/seo-service.d.ts +12 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +0 -10
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/Settings.js +1 -1
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/settings/AISettingsTab.d.ts +3 -1
- package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
- package/dist/views/settings/AISettingsTab.js +30 -2
- package/dist/views/settings/AISettingsTab.js.map +1 -1
- package/dist/views/settings/BrandVoiceCard.d.ts +44 -2
- package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
- package/dist/views/settings/BrandVoiceCard.js +215 -8
- package/dist/views/settings/BrandVoiceCard.js.map +1 -1
- package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.d.ts +13 -0
- package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.d.ts.map +1 -0
- package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js +74 -0
- package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js.map +1 -0
- package/dist/views/settings/brand-voice/BrandVoiceRulesSections.d.ts +7 -0
- package/dist/views/settings/brand-voice/BrandVoiceRulesSections.d.ts.map +1 -0
- package/dist/views/settings/brand-voice/BrandVoiceRulesSections.js +54 -0
- package/dist/views/settings/brand-voice/BrandVoiceRulesSections.js.map +1 -0
- package/dist/views/settings/useAiSettings.d.ts +32 -0
- package/dist/views/settings/useAiSettings.d.ts.map +1 -1
- package/dist/views/settings/useAiSettings.js +85 -3
- package/dist/views/settings/useAiSettings.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/views/ai-settings.render.test.tsx +54 -4
- package/src/components/AICoauthorBubbleMenu.tsx +12 -0
- package/src/components/BrandVoiceAlignmentBadge.tsx +39 -0
- package/src/components/CoauthorResultPopover.tsx +62 -4
- package/src/components/CommandPalette.tsx +12 -0
- package/src/components/seo/SeoEditorDrawer.tsx +65 -1
- package/src/lib/brand-voice-client.ts +38 -0
- package/src/lib/coauthor-client.ts +4 -0
- package/src/lib/seo-service.ts +2 -0
- package/src/views/Settings.tsx +1 -1
- package/src/views/settings/AISettingsTab.tsx +47 -3
- package/src/views/settings/BrandVoiceCard.tsx +608 -75
- package/src/views/settings/brand-voice/BrandVoiceAlignmentPanel.tsx +205 -0
- package/src/views/settings/brand-voice/BrandVoiceRulesSections.tsx +321 -0
- package/src/views/settings/useAiSettings.ts +134 -3
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
defaultStructuredProfile,
|
|
6
|
+
parseStructuredProfile,
|
|
7
|
+
type BrandAlignmentScoreResult,
|
|
8
|
+
type BrandVoiceStructuredProfile,
|
|
9
|
+
} from '@actuate-media/cms-core/brand-voice'
|
|
4
10
|
import { cmsApi } from '../../lib/api.js'
|
|
5
11
|
|
|
6
12
|
/**
|
|
@@ -101,8 +107,15 @@ export interface BrandVoiceForm {
|
|
|
101
107
|
audience: string
|
|
102
108
|
guidelines: string
|
|
103
109
|
sampleText: string
|
|
110
|
+
document: string
|
|
111
|
+
version: number
|
|
112
|
+
status: 'draft' | 'reviewed' | 'approved'
|
|
113
|
+
generatedFrom: string[]
|
|
114
|
+
structured: BrandVoiceStructuredProfile
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
export type BrandVoiceEditMode = 'form' | 'document'
|
|
118
|
+
|
|
106
119
|
const EMPTY_BRAND_VOICE: BrandVoiceForm = {
|
|
107
120
|
enabled: false,
|
|
108
121
|
name: '',
|
|
@@ -110,6 +123,11 @@ const EMPTY_BRAND_VOICE: BrandVoiceForm = {
|
|
|
110
123
|
audience: '',
|
|
111
124
|
guidelines: '',
|
|
112
125
|
sampleText: '',
|
|
126
|
+
document: '',
|
|
127
|
+
version: 1,
|
|
128
|
+
status: 'draft',
|
|
129
|
+
generatedFrom: [],
|
|
130
|
+
structured: defaultStructuredProfile(),
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
/** The buffered (Save-bar) portion of AI settings. */
|
|
@@ -134,6 +152,10 @@ interface AiSettingsResponse {
|
|
|
134
152
|
audience?: string
|
|
135
153
|
guidelines?: string
|
|
136
154
|
sampleText?: string
|
|
155
|
+
document?: string
|
|
156
|
+
version?: number
|
|
157
|
+
status?: 'draft' | 'reviewed' | 'approved'
|
|
158
|
+
generatedFrom?: string[]
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
providers: AiProviderView[]
|
|
@@ -162,6 +184,13 @@ function formFromResponse(res: AiSettingsResponse): AiSettingsForm {
|
|
|
162
184
|
audience: res.settings.brandVoice?.audience ?? '',
|
|
163
185
|
guidelines: res.settings.brandVoice?.guidelines ?? '',
|
|
164
186
|
sampleText: res.settings.brandVoice?.sampleText ?? '',
|
|
187
|
+
document: res.settings.brandVoice?.document ?? '',
|
|
188
|
+
version: res.settings.brandVoice?.version ?? 1,
|
|
189
|
+
status: res.settings.brandVoice?.status ?? 'draft',
|
|
190
|
+
generatedFrom: res.settings.brandVoice?.generatedFrom ?? [],
|
|
191
|
+
structured: res.settings.brandVoice?.document?.trim()
|
|
192
|
+
? parseStructuredProfile(res.settings.brandVoice.document)
|
|
193
|
+
: defaultStructuredProfile(),
|
|
165
194
|
},
|
|
166
195
|
}
|
|
167
196
|
}
|
|
@@ -182,7 +211,8 @@ function brandVoiceIsEmpty(bv: BrandVoiceForm): boolean {
|
|
|
182
211
|
!bv.tone.trim() &&
|
|
183
212
|
!bv.audience.trim() &&
|
|
184
213
|
!bv.guidelines.trim() &&
|
|
185
|
-
!bv.sampleText.trim()
|
|
214
|
+
!bv.sampleText.trim() &&
|
|
215
|
+
!bv.document.trim()
|
|
186
216
|
)
|
|
187
217
|
}
|
|
188
218
|
|
|
@@ -213,6 +243,25 @@ export interface UseAiSettings {
|
|
|
213
243
|
setFeatureModel: (key: string, modelId: string) => void
|
|
214
244
|
/** Patch one or more brand voice fields. */
|
|
215
245
|
setBrandVoice: (patch: Partial<BrandVoiceForm>) => void
|
|
246
|
+
/** Whether saves should trust the markdown document or quick-edit fields. */
|
|
247
|
+
brandVoiceEditMode: BrandVoiceEditMode
|
|
248
|
+
setBrandVoiceEditMode: (mode: BrandVoiceEditMode) => void
|
|
249
|
+
/** Scan published content and draft brand voice fields (does not save). */
|
|
250
|
+
analyzeBrandVoice: (input: {
|
|
251
|
+
collections: string[]
|
|
252
|
+
urls?: string[]
|
|
253
|
+
uploads?: Array<{ name: string; text: string }>
|
|
254
|
+
}) => Promise<{
|
|
255
|
+
ok: boolean
|
|
256
|
+
draft?: Partial<BrandVoiceForm>
|
|
257
|
+
sampleCount?: number
|
|
258
|
+
error?: string
|
|
259
|
+
}>
|
|
260
|
+
scoreBrandVoice: (input: { content: string; channel?: string }) => Promise<{
|
|
261
|
+
ok: boolean
|
|
262
|
+
result?: BrandAlignmentScoreResult
|
|
263
|
+
error?: string
|
|
264
|
+
}>
|
|
216
265
|
dirty: boolean
|
|
217
266
|
budgetError: string | null
|
|
218
267
|
saveState: SaveState
|
|
@@ -248,6 +297,7 @@ export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
|
248
297
|
const [saveState, setSaveState] = useState<SaveState>('idle')
|
|
249
298
|
const [saveError, setSaveError] = useState<string | null>(null)
|
|
250
299
|
const [busyProviderId, setBusyProviderId] = useState<string | null>(null)
|
|
300
|
+
const [brandVoiceEditMode, setBrandVoiceEditMode] = useState<BrandVoiceEditMode>('form')
|
|
251
301
|
const [reloadKey, setReloadKey] = useState(0)
|
|
252
302
|
|
|
253
303
|
const loadModels = useCallback(async () => {
|
|
@@ -329,6 +379,56 @@ export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
|
329
379
|
setSaveState('idle')
|
|
330
380
|
}, [])
|
|
331
381
|
|
|
382
|
+
const analyzeBrandVoice = useCallback(
|
|
383
|
+
async (input: {
|
|
384
|
+
collections: string[]
|
|
385
|
+
urls?: string[]
|
|
386
|
+
uploads?: Array<{ name: string; text: string }>
|
|
387
|
+
}) => {
|
|
388
|
+
if (!canEdit) return { ok: false, error: 'You do not have permission to do this.' }
|
|
389
|
+
try {
|
|
390
|
+
const res = await cmsApi<{
|
|
391
|
+
draft: Partial<BrandVoiceForm> & { document?: string }
|
|
392
|
+
sampleCount: number
|
|
393
|
+
}>('/ai/brand-voice/analyze', {
|
|
394
|
+
method: 'POST',
|
|
395
|
+
body: JSON.stringify(input),
|
|
396
|
+
})
|
|
397
|
+
if (res.error) return { ok: false, error: res.error }
|
|
398
|
+
if (!res.data?.draft) return { ok: false, error: 'No draft profile was returned.' }
|
|
399
|
+
const structured = res.data.draft.document?.trim()
|
|
400
|
+
? parseStructuredProfile(res.data.draft.document)
|
|
401
|
+
: defaultStructuredProfile()
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
draft: { ...res.data.draft, structured },
|
|
405
|
+
sampleCount: res.data.sampleCount,
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
return { ok: false, error: 'Brand voice analysis failed.' }
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
[canEdit],
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
const scoreBrandVoice = useCallback(
|
|
415
|
+
async (input: { content: string; channel?: string }) => {
|
|
416
|
+
if (!canEdit) return { ok: false, error: 'You do not have permission to do this.' }
|
|
417
|
+
try {
|
|
418
|
+
const res = await cmsApi<BrandAlignmentScoreResult>('/ai/brand-voice/score', {
|
|
419
|
+
method: 'POST',
|
|
420
|
+
body: JSON.stringify(input),
|
|
421
|
+
})
|
|
422
|
+
if (res.error) return { ok: false, error: res.error }
|
|
423
|
+
if (!res.data) return { ok: false, error: 'No score was returned.' }
|
|
424
|
+
return { ok: true, result: res.data }
|
|
425
|
+
} catch {
|
|
426
|
+
return { ok: false, error: 'Brand voice scoring failed.' }
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
[canEdit],
|
|
430
|
+
)
|
|
431
|
+
|
|
332
432
|
const budgetError = useMemo(() => {
|
|
333
433
|
if (form.monthlyTokenLimit === '') return null
|
|
334
434
|
const n = Number(form.monthlyTokenLimit)
|
|
@@ -348,8 +448,30 @@ export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
|
348
448
|
'audience',
|
|
349
449
|
'guidelines',
|
|
350
450
|
'sampleText',
|
|
451
|
+
'document',
|
|
452
|
+
'version',
|
|
453
|
+
'status',
|
|
454
|
+
'generatedFrom',
|
|
455
|
+
'structured',
|
|
351
456
|
]
|
|
352
|
-
if (
|
|
457
|
+
if (
|
|
458
|
+
bvKeys.some((k) => {
|
|
459
|
+
if (k === 'generatedFrom') {
|
|
460
|
+
return (
|
|
461
|
+
JSON.stringify(form.brandVoice.generatedFrom) !==
|
|
462
|
+
JSON.stringify(baseline.brandVoice.generatedFrom)
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
if (k === 'structured') {
|
|
466
|
+
return (
|
|
467
|
+
JSON.stringify(form.brandVoice.structured) !==
|
|
468
|
+
JSON.stringify(baseline.brandVoice.structured)
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
return form.brandVoice[k] !== baseline.brandVoice[k]
|
|
472
|
+
})
|
|
473
|
+
)
|
|
474
|
+
return true
|
|
353
475
|
return form.features.some((f, i) => {
|
|
354
476
|
const base = baseline.features[i]
|
|
355
477
|
return f.enabled !== base?.enabled || (f.modelId ?? '') !== (base?.modelId ?? '')
|
|
@@ -384,6 +506,11 @@ export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
|
384
506
|
audience: form.brandVoice.audience,
|
|
385
507
|
guidelines: form.brandVoice.guidelines,
|
|
386
508
|
sampleText: form.brandVoice.sampleText,
|
|
509
|
+
document: form.brandVoice.document,
|
|
510
|
+
version: form.brandVoice.version,
|
|
511
|
+
status: form.brandVoice.status,
|
|
512
|
+
generatedFrom: form.brandVoice.generatedFrom,
|
|
513
|
+
preferDocument: brandVoiceEditMode === 'document',
|
|
387
514
|
},
|
|
388
515
|
}),
|
|
389
516
|
})
|
|
@@ -397,7 +524,7 @@ export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
|
397
524
|
setSaveState('error')
|
|
398
525
|
setSaveError(err instanceof Error ? err.message : 'Failed to save AI settings.')
|
|
399
526
|
}
|
|
400
|
-
}, [canEdit, budgetError, form])
|
|
527
|
+
}, [canEdit, budgetError, form, brandVoiceEditMode])
|
|
401
528
|
|
|
402
529
|
const reset = useCallback(() => {
|
|
403
530
|
setForm(baseline)
|
|
@@ -535,6 +662,10 @@ export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
|
535
662
|
toggleFeature,
|
|
536
663
|
setFeatureModel,
|
|
537
664
|
setBrandVoice,
|
|
665
|
+
analyzeBrandVoice,
|
|
666
|
+
scoreBrandVoice,
|
|
667
|
+
brandVoiceEditMode,
|
|
668
|
+
setBrandVoiceEditMode,
|
|
538
669
|
dirty,
|
|
539
670
|
budgetError,
|
|
540
671
|
saveState,
|