import { useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { AlertTriangle } from 'lucide-react' import { getAdminSettings, sendTestEmail, updateAdminSettings, type AiLevel, type SettingsOut, type SettingsUpdateIn, type TestEmailOut, } from '@/api/admin' import type { SummaryRecipientsMode } from '@/api/conferences' import { ApiError, errorDetail } from '@/api/client' import { useAuth } from '@/auth/useAuth' import { Select } from '@/components/ui/Select' import { useToast } from '@/components/ui/ToastProvider' const SUMMARY_RECIPIENTS_OPTIONS = [ { value: 'all', label: 'Всем участникам' }, { value: 'owner', label: 'Только организатору' }, ] const AI_LEVEL_LABEL: Record = { min: 'Минимальный (CPU, faster-whisper small + Qwen2.5-3B)', medium: 'Средний', max: 'Максимальный', } /** * Вкладка «Настройки» админки — раздела нет в макете `admin.html`, построена * по паттерну того же макета (`section-tab`/`table-card` → карточки-секции * с `toggle-row`/radio). * Тумблер «Транскрибация и суммаризация» — единый переключатель AI-модуля * (см. `SettingsOut.transcription_enabled`). * * Форма (`AdminSettingsForm`) вынесена отдельно и монтируется только после * загрузки `data` — локальное состояние инициализируется прямо из пропсов * при монтировании, без `useEffect`-синхронизации (react-hooks/set-state-in-effect). */ export function AdminSettingsTab() { const { data, isLoading } = useQuery({ queryKey: ['admin', 'settings'], queryFn: getAdminSettings }) if (isLoading || !data) { return

Загрузка настроек…

} return } function AdminSettingsForm({ data }: { data: SettingsOut }) { const queryClient = useQueryClient() const toast = useToast() const { user } = useAuth() const [chatEnabled, setChatEnabled] = useState(data.chat_enabled) const [aiEnabled, setAiEnabled] = useState(data.transcription_enabled) const [aiLevel, setAiLevel] = useState(data.ai_level) const [recipients, setRecipients] = useState(data.summary_recipients) const [timezone, setTimezone] = useState(data.display_timezone) const [teamChoiceEnabled, setTeamChoiceEnabled] = useState(data.registration_team_choice) const [domainVerificationEnabled, setDomainVerificationEnabled] = useState(data.registration_email_domain_enabled) const [emailDomain, setEmailDomain] = useState(data.registration_email_domain ?? '') const [contactEmailEnabled, setContactEmailEnabled] = useState(data.contact_email_enabled) const [contactEmail, setContactEmail] = useState(data.contact_email ?? '') const [testEmailTo, setTestEmailTo] = useState('') const [testEmailResult, setTestEmailResult] = useState(null) const mutation = useMutation({ mutationFn: (payload: SettingsUpdateIn) => updateAdminSettings(payload), onSuccess: (result) => { queryClient.setQueryData(['admin', 'settings'], result) toast.show('Настройки сохранены', 'success') }, onError: (err: unknown) => { if (err instanceof ApiError && err.status === 400) { toast.show(errorDetail(err) ?? 'Недоступное значение — проверьте уровень AI, таймзону и домен почты', 'error') } else { toast.show('Не удалось сохранить настройки', 'error') } }, }) const testEmailMutation = useMutation({ mutationFn: () => sendTestEmail(testEmailTo.trim() ? { to: testEmailTo.trim() } : {}), onSuccess: (result) => setTestEmailResult(result), onError: (err: unknown) => { setTestEmailResult({ success: false, message: (err instanceof ApiError && errorDetail(err)) || 'Не удалось отправить тестовое письмо', smtp_host: null, smtp_port: null, }) }, }) function handleSave() { mutation.mutate({ chat_enabled: chatEnabled, transcription_enabled: aiEnabled, ai_level: aiLevel, summary_recipients: recipients, display_timezone: timezone.trim(), registration_team_choice: teamChoiceEnabled, registration_email_domain_enabled: domainVerificationEnabled, registration_email_domain: emailDomain.trim() || null, contact_email_enabled: contactEmailEnabled, contact_email: contactEmail.trim() || null, }) } return (

Модули

Включение и отключение возможностей инстанса — переключатели плагинов из поставки.

Чат конференции Текстовый чат внутри комнаты для участников
Транскрибация и суммаризация (AI) Батч-обработка записи после конференции: расшифровка речи и саммари {aiEnabled && !data.transcription_queue_served && ( )}
Разрешить выбор команды при регистрации На экране регистрации появится поле «Команда» со списком команд инстанса
Верификация по домену почты Регистрация только с email эталонного домена (см. плитку «Эталон mail-домена»)
{/* Анонс записи конференций. Чекбокс не имеет состояния и не участвует в сохранении формы — функция появится в одной из следующих версий, здесь только заглушка-анонс в общем стиле «Модулей». */}
Возможность записи конференций Функция появится в ближайших версиях

Уровень AI

Качество распознавания и суммаризации — определяет размер моделей и требования к железу.

{data.ai_levels.map((levelStatus) => ( ))}

Эталон mail-домена

Домен, с которым сверяется email при регистрации, если включена верификация в «Модулях».

setEmailDomain(e.target.value)} />

Рассылка саммари

Кому по умолчанию отправляется письмо с саммари после конференции (переопределяется в карточке конференции).

setTimezone(e.target.value)} />

Контактный адрес

Адрес для ответов на письма от инстанса (уходят от no-reply — этот адрес подставляется в заголовок «Reply-To»).

Контактный адрес включён Подставляется в Reply-To подтверждения регистрации, приглашений и саммари
setContactEmail(e.target.value)} />

Тестовое письмо

Отправить проверочное письмо синхронно, чтобы сразу увидеть результат почтовой конфигурации.

setTestEmailTo(e.target.value)} />
{testEmailResult && (

{testEmailResult.message} {testEmailResult.smtp_host && ` (${testEmailResult.smtp_host}:${testEmailResult.smtp_port})`}

)}
) }