/** * BoostMedia AI Content Generator Admin - Reporters Page * * @package BoostMedia_AI * @license GPL-2.0-or-later */ import { useCallback, useEffect, useMemo, useState } from 'react' import { Copy, Plus, Sparkles, Star, Trash2, UserRound } from 'lucide-react' import { Header } from '../components/layout/Header' import { Badge, Button, Card } from '../components/common' import { SetupBanner } from '../components/onboarding/SetupBanner' import { TagInput } from '../components/generator/TagInput' import { ReporterBuilder } from '../components/reporters/ReporterBuilder' import { endpoints } from '../api/client' import type { Reporter } from '../types' import { t } from '../lib/i18n' type ReporterFilter = 'all' | 'default' | 'recent' | 'archived' type ReporterDraft = Omit< Reporter, 'id' | 'uuid' | 'slug' | 'created_at' | 'updated_at' | 'last_used_at' > function createEmptyDraft(): ReporterDraft { return { name: '', language: window.bmaiSettings?.dataLanguage || 'he', writing_language: window.bmaiSettings?.dataLanguage || 'he', is_default: 0, status: 'active', specializations: [], writing_style: 'conversational', depth_level: 'detailed', tone: 'friendly', perspective: 'third_person', gender: 'neutral', language_quirks: [], audience: '', custom_instructions: '', source_mode: 'manual', builder_summary: '', } } function formatReporterSummary(reporter: Reporter | ReporterDraft): string { return [reporter.writing_style, reporter.tone, reporter.depth_level] .filter(Boolean) .map((item) => t(item)) .join(' / ') } function formatDate(value: string | null): string { if (!value) { return t('Never used') } const date = new Date(value) if (Number.isNaN(date.getTime())) { return t('Never used') } return date.toLocaleString() } export default function ReportersPage() { const [reporters, setReporters] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [filter, setFilter] = useState('all') const [selectedId, setSelectedId] = useState(null) const [draft, setDraft] = useState(createEmptyDraft()) const [isCreating, setIsCreating] = useState(false) const [statusMessage, setStatusMessage] = useState(null) const [builderOpen, setBuilderOpen] = useState(false) const [builderReporter, setBuilderReporter] = useState(null) const loadReporters = useCallback(async () => { setLoading(true) try { const res = await endpoints.getReporters('all') const items = (res.data as Reporter[]) || [] setReporters(items) } finally { setLoading(false) } }, []) useEffect(() => { void loadReporters() }, [loadReporters]) useEffect(() => { if (reporters.length > 0 && !selectedId && !isCreating) { const defaultReporter = reporters.find((r) => r.is_default) || reporters[0] if (defaultReporter) setSelectedId(defaultReporter.id) } }, [reporters, isCreating]) const selectedReporter = useMemo( () => reporters.find((reporter) => reporter.id === selectedId) || null, [reporters, selectedId], ) useEffect(() => { if (selectedReporter && !isCreating) { setDraft({ name: selectedReporter.name, language: selectedReporter.language, writing_language: selectedReporter.writing_language || selectedReporter.language, is_default: selectedReporter.is_default, status: selectedReporter.status, specializations: selectedReporter.specializations || [], writing_style: selectedReporter.writing_style, depth_level: selectedReporter.depth_level, tone: selectedReporter.tone, perspective: selectedReporter.perspective, gender: selectedReporter.gender || 'neutral', language_quirks: selectedReporter.language_quirks || [], audience: selectedReporter.audience || '', custom_instructions: selectedReporter.custom_instructions || '', source_mode: selectedReporter.source_mode, builder_summary: selectedReporter.builder_summary || '', }) } }, [isCreating, selectedReporter]) const filteredReporters = useMemo(() => { switch (filter) { case 'default': return reporters.filter((r) => r.is_default && r.status !== 'archived') case 'recent': return [...reporters] .filter((r) => r.last_used_at && r.status !== 'archived') .sort((a, b) => new Date(b.last_used_at || 0).getTime() - new Date(a.last_used_at || 0).getTime()) case 'archived': return reporters.filter((r) => r.status === 'archived') default: return reporters } }, [filter, reporters]) const activeReportersCount = useMemo( () => reporters.filter((reporter) => reporter.status === 'active').length, [reporters], ) const hasOnlyDefaultReporter = activeReportersCount <= 1 && reporters.some((reporter) => reporter.is_default) const updateDraft = (key: K, value: ReporterDraft[K]) => { setDraft((current) => ({ ...current, [key]: value })) } const handleCreate = () => { setIsCreating(true) setSelectedId(null) setDraft(createEmptyDraft()) setStatusMessage(null) } const handleSelect = (reporter: Reporter) => { setIsCreating(false) setSelectedId(reporter.id) setStatusMessage(null) } const openCreateBuilder = () => { setBuilderReporter(null) setBuilderOpen(true) } const openRefineBuilder = () => { if (!selectedReporter) { return } setBuilderReporter(selectedReporter) setBuilderOpen(true) } const handleCancel = () => { setStatusMessage(null) setIsCreating(false) if (reporters.length > 0) { const fallback = reporters.find((r) => r.is_default) || reporters[0] if (fallback) setSelectedId(fallback.id) } else { setSelectedId(null) setDraft(createEmptyDraft()) } } const handleSave = async () => { if (!draft.name.trim()) { setStatusMessage(t('Reporter name is required')) return } setSaving(true) setStatusMessage(null) try { if (isCreating) { const res = await endpoints.createReporter(draft) const created = res.data as Reporter setIsCreating(false) setSelectedId(created.id) } else if (selectedId) { await endpoints.updateReporter(selectedId, draft) } await loadReporters() setStatusMessage(t('Reporter saved')) } catch { setStatusMessage(t('Failed to save reporter')) } finally { setSaving(false) } } const handleDuplicate = async (reporterId: number) => { setSaving(true) setStatusMessage(null) try { const res = await endpoints.duplicateReporter(reporterId) const duplicated = res.data as Reporter await loadReporters() setSelectedId(duplicated.id) setIsCreating(false) setStatusMessage(t('Reporter duplicated')) } catch { setStatusMessage(t('Failed to duplicate reporter')) } finally { setSaving(false) } } const handleDelete = async (reporterId: number) => { if (!confirm(t('Archive this reporter?'))) { return } setSaving(true) setStatusMessage(null) try { await endpoints.deleteReporter(reporterId) const fallback = reporters.find((reporter) => reporter.id !== reporterId && reporter.status === 'active') setSelectedId(fallback?.id ?? null) setIsCreating(false) await loadReporters() setStatusMessage(t('Reporter archived')) } catch { setStatusMessage(t('Failed to archive reporter')) } finally { setSaving(false) } } const handleSetDefault = async (reporterId: number) => { setSaving(true) setStatusMessage(null) try { await endpoints.setDefaultReporter(reporterId) await loadReporters() setSelectedId(reporterId) setStatusMessage(t('Default reporter updated')) } catch { setStatusMessage(t('Failed to update default reporter')) } finally { setSaving(false) } } return (

{t('Reporters')}

{t('Create, edit, duplicate, archive, and choose the default reporter')}

{loading ? (
{t('Loading...')}
) : filteredReporters.length === 0 ? (
{t('No reporters found')}
) : (
{hasOnlyDefaultReporter ? (
{t('Create your first custom reporter')} {t('Start with AI for a guided setup, or create one manually if you already know the voice you want.')}
) : null} {filteredReporters.map((reporter) => ( ))}
)}

{isCreating ? t('Create Reporter') : selectedReporter ? t('Edit Reporter') : t('Reporter Details')}

{isCreating ? t('Define a new reporter profile manually') : t('Edit the selected reporter profile')}

{!isCreating && selectedReporter ? (
{!selectedReporter.is_default ? ( ) : null}
) : null}
{statusMessage ? (
{statusMessage}
) : null}
{t('Specializations')} updateDraft('specializations', value)} placeholder={t('Add specialization...')} maxTags={12} />
{t('Language Quirks')} updateDraft('language_quirks', value)} placeholder={t('Add language quirk...')} maxTags={12} />
{t('Custom Instructions')}