import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiFetch, isPremium } from '../lib/api'; import { SettingsSkeleton } from './Skeleton'; import type { AIProvider, ScanSchedule, Settings as SettingsType } from '../lib/types'; import { labelsForRulesets, RULESET_PRESETS, presetValueForStandards, standardsForPresetValue, DEFAULT_RULESET_PRESET } from '../lib/rulesets'; import { PROVIDERS } from '../lib/providers'; const SCHEDULES: { value: ScanSchedule; label: string }[] = [ { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' }, { value: 'monthly', label: 'Monthly' }, ]; export default function Settings() { const qc = useQueryClient(); const { data: saved, isLoading } = useQuery({ queryKey: ['settings'], queryFn: () => apiFetch('/settings'), retry: false, }); const [provider, setProvider] = useState('none'); const [apiKey, setApiKey] = useState(''); const [showKey, setShowKey] = useState(false); const [model, setModel] = useState(''); const [schedule, setSchedule] = useState('weekly'); const [emailAlerts, setEmailAlerts] = useState(true); const [defaultRulesets, setDefaultRulesets] = useState(['wcag22aa']); const [saveOk, setSaveOk] = useState(false); const [saveAttempted, setSaveAttempted] = useState(false); // Hydrate form from server once loaded. useEffect(() => { if (!saved) return; setProvider(saved.ai_provider); setSchedule(saved.scan_schedule); setEmailAlerts(saved.email_alerts ?? true); setModel(saved.ai_model || (PROVIDERS.find((p) => p.value === saved.ai_provider)?.models[0] ?? '')); setDefaultRulesets(saved.default_rulesets?.length ? saved.default_rulesets : [DEFAULT_RULESET_PRESET]); }, [saved]); const save = useMutation({ mutationFn: () => apiFetch<{ saved: boolean }>('/settings', { method: 'POST', body: JSON.stringify({ ai_provider: provider, ai_key: apiKey, ai_model: model, scan_schedule: schedule, default_rulesets: defaultRulesets, email_alerts: emailAlerts }), }), onSuccess: () => { setSaveOk(true); setSaveAttempted(false); setApiKey(''); qc.invalidateQueries({ queryKey: ['settings'] }); setTimeout(() => setSaveOk(false), 3000); }, }); const testConnection = useMutation({ mutationFn: () => apiFetch<{ text?: string; error?: string }>('/ai/test', { method: 'POST', body: JSON.stringify({ ai_provider: provider, ai_key: apiKey, ai_model: model }), }), }); const removeKey = useMutation({ mutationFn: () => apiFetch<{ saved: boolean }>('/settings', { method: 'POST', body: JSON.stringify({ ai_provider: provider, ai_key: '', remove_key: true, ai_model: model, scan_schedule: schedule, default_rulesets: defaultRulesets, email_alerts: emailAlerts }), }), onSuccess: () => { setApiKey(''); testConnection.reset(); qc.invalidateQueries({ queryKey: ['settings'] }); }, }); const providerDef = PROVIDERS.find((p) => p.value === provider)!; // A saved key only belongs to the provider it was entered for — if the // dropdown has been switched to a different (unsaved) provider, the // previously-saved key doesn't apply here and must not be treated as usable. const hasUsableSavedKey = !!saved?.ai_key_set && saved?.ai_provider === provider; const keyRequired = providerDef.needsKey && !apiKey && !hasUsableSavedKey; if (isLoading) return ; return (

Settings

{/* AI Provider */}

AI Provider

AI features are optional. The API key is stored encrypted in{' '} wp_options and never sent to the browser.

Select AI provider {PROVIDERS.map((p) => ( ))}
{providerDef.models.length > 0 && ( )} {provider !== 'none' && (
{providerDef.needsKey && ( <>
{ setApiKey(e.target.value); testConnection.reset(); setSaveAttempted(false); }} placeholder={hasUsableSavedKey ? '••••••••••••••••' : 'Paste your API key'} autoComplete="off" required aria-required="true" aria-describedby={keyRequired && saveAttempted ? 'aai-key-error' : undefined} /> {hasUsableSavedKey && !apiKey && ( )}
{/* API key required validation */} {keyRequired && saveAttempted && ( )} {removeKey.isSuccess &&

Key removed.

} )} {/* A key-less provider (Ollama) still needs a way to verify the connection. */} {!providerDef.needsKey && ( )} {/* Test connection feedback */} {testConnection.isSuccess && !testConnection.data?.error && (

Connection verified.

)} {(testConnection.isError || testConnection.data?.error) && (

{testConnection.data?.error ?? (testConnection.error as Error)?.message ?? 'Connection failed.'}

)}
)}
{/* Default Ruleset */}

Default WCAG Standard

This ruleset is used by default when scanning pages from the admin dashboard and the frontend overlay.

{/* Scheduled Scans — Pro only; free users don't see a control for a feature that doesn't exist for them */} {isPremium && (

Scheduled Scans

AccessMate automatically re-scans your entire site on this schedule, keeping the Dashboard and Report up to date.

)} {/* Accessibility note */}

Accessibility note: AccessMate uses automated scanning to identify and help fix accessibility issues. Automated tools can detect only a subset of accessibility problems. For full {labelsForRulesets(defaultRulesets) || 'WCAG 2.2 AA'} compliance, manual testing — including testing with assistive technologies such as screen readers — is recommended.

AccessMate helps you find and fix accessibility issues faster. It does not replace a full manual accessibility audit.

{/* Save */}
{saveOk && Saved.} {save.isError && ( {(save.error as Error).message} )}
); }