@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/__tests__/views/ai-settings.render.test.js +38 -4
  3. package/dist/__tests__/views/ai-settings.render.test.js.map +1 -1
  4. package/dist/actuate-admin.css +1 -1
  5. package/dist/components/AICoauthorBubbleMenu.d.ts.map +1 -1
  6. package/dist/components/AICoauthorBubbleMenu.js +5 -1
  7. package/dist/components/AICoauthorBubbleMenu.js.map +1 -1
  8. package/dist/components/BrandVoiceAlignmentBadge.d.ts +6 -0
  9. package/dist/components/BrandVoiceAlignmentBadge.d.ts.map +1 -0
  10. package/dist/components/BrandVoiceAlignmentBadge.js +11 -0
  11. package/dist/components/BrandVoiceAlignmentBadge.js.map +1 -0
  12. package/dist/components/CoauthorResultPopover.d.ts +5 -1
  13. package/dist/components/CoauthorResultPopover.d.ts.map +1 -1
  14. package/dist/components/CoauthorResultPopover.js +35 -6
  15. package/dist/components/CoauthorResultPopover.js.map +1 -1
  16. package/dist/components/CommandPalette.d.ts.map +1 -1
  17. package/dist/components/CommandPalette.js +5 -1
  18. package/dist/components/CommandPalette.js.map +1 -1
  19. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -1
  20. package/dist/components/seo/SeoEditorDrawer.js +43 -2
  21. package/dist/components/seo/SeoEditorDrawer.js.map +1 -1
  22. package/dist/components/ui/Badge.d.ts +1 -1
  23. package/dist/lib/brand-voice-client.d.ts +21 -0
  24. package/dist/lib/brand-voice-client.d.ts.map +1 -0
  25. package/dist/lib/brand-voice-client.js +27 -0
  26. package/dist/lib/brand-voice-client.js.map +1 -0
  27. package/dist/lib/coauthor-client.d.ts +2 -0
  28. package/dist/lib/coauthor-client.d.ts.map +1 -1
  29. package/dist/lib/coauthor-client.js +1 -0
  30. package/dist/lib/coauthor-client.js.map +1 -1
  31. package/dist/lib/seo-service.d.ts +12 -0
  32. package/dist/lib/seo-service.d.ts.map +1 -1
  33. package/dist/lib/seo-service.js +0 -10
  34. package/dist/lib/seo-service.js.map +1 -1
  35. package/dist/views/Settings.js +1 -1
  36. package/dist/views/Settings.js.map +1 -1
  37. package/dist/views/settings/AISettingsTab.d.ts +3 -1
  38. package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
  39. package/dist/views/settings/AISettingsTab.js +30 -2
  40. package/dist/views/settings/AISettingsTab.js.map +1 -1
  41. package/dist/views/settings/BrandVoiceCard.d.ts +44 -2
  42. package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
  43. package/dist/views/settings/BrandVoiceCard.js +215 -8
  44. package/dist/views/settings/BrandVoiceCard.js.map +1 -1
  45. package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.d.ts +13 -0
  46. package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.d.ts.map +1 -0
  47. package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js +74 -0
  48. package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js.map +1 -0
  49. package/dist/views/settings/brand-voice/BrandVoiceRulesSections.d.ts +7 -0
  50. package/dist/views/settings/brand-voice/BrandVoiceRulesSections.d.ts.map +1 -0
  51. package/dist/views/settings/brand-voice/BrandVoiceRulesSections.js +54 -0
  52. package/dist/views/settings/brand-voice/BrandVoiceRulesSections.js.map +1 -0
  53. package/dist/views/settings/useAiSettings.d.ts +32 -0
  54. package/dist/views/settings/useAiSettings.d.ts.map +1 -1
  55. package/dist/views/settings/useAiSettings.js +85 -3
  56. package/dist/views/settings/useAiSettings.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/__tests__/views/ai-settings.render.test.tsx +54 -4
  59. package/src/components/AICoauthorBubbleMenu.tsx +12 -0
  60. package/src/components/BrandVoiceAlignmentBadge.tsx +39 -0
  61. package/src/components/CoauthorResultPopover.tsx +62 -4
  62. package/src/components/CommandPalette.tsx +12 -0
  63. package/src/components/seo/SeoEditorDrawer.tsx +65 -1
  64. package/src/lib/brand-voice-client.ts +38 -0
  65. package/src/lib/coauthor-client.ts +4 -0
  66. package/src/lib/seo-service.ts +2 -0
  67. package/src/views/Settings.tsx +1 -1
  68. package/src/views/settings/AISettingsTab.tsx +47 -3
  69. package/src/views/settings/BrandVoiceCard.tsx +608 -75
  70. package/src/views/settings/brand-voice/BrandVoiceAlignmentPanel.tsx +205 -0
  71. package/src/views/settings/brand-voice/BrandVoiceRulesSections.tsx +321 -0
  72. 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 (bvKeys.some((k) => form.brandVoice[k] !== baseline.brandVoice[k])) return true
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,