'links_daily_neurons', 'search' => 'search_daily_neurons', 'insights' => 'insights_daily_neurons', 'chat' => 'chat_daily_neurons', 'agents' => 'agents_daily_neurons', ]; /** @return array */ public static function defaults(): array { return [ 'related_enabled' => 0, 'related_auto' => 0, 'related_count' => 5, 'related_min_score' => 0.55, // Store queries a day for working out lists (Vectorize free plan: 30M queried dimensions a month). 'related_daily_queries' => 500, 'related_position' => 'after', 'related_paragraph' => 3, 'related_skip_types' => [], 'related_title' => '', 'related_heading' => 'h2', 'related_excerpt' => 0, 'related_thumbnail' => 0, 'related_why' => 0, 'links_enabled' => 0, 'links_daily_neurons' => 1000, 'search_enabled' => 0, 'search_capability' => 'read', 'search_takeover' => 0, 'search_daily_neurons' => 500, 'search_min_score' => 0.5, 'insights_enabled' => 0, 'insights_retention_days' => 90, 'insights_daily_rows' => 2000, 'insights_daily_neurons' => 500, 'insights_gap_threshold' => 0.5, 'chat_enabled' => 0, 'chat_capability' => 'read', 'chat_floating' => 1, 'chat_admin' => 1, 'chat_name' => '', 'chat_welcome' => '', // bge-m3: a Marathi question about an English post matched at 0.44 on whoisabhi.com; // the model says "not found" when weak passages do not answer. 'chat_min_score' => 0.35, 'chat_rerank' => 1, 'chat_min_match' => 0.08, 'search_rerank' => 0, 'agents_enabled' => 0, 'agents_capability' => 'manage_options', 'agents_daily_neurons' => 500, 'chat_daily_neurons' => 1000, 'chat_subtitle' => '', 'chat_avatar' => '', 'chat_accent' => '#1d4ed8', 'chat_position' => 'right', 'chat_chips' => [], 'chat_placeholder' => '', 'chat_no_match' => '', 'chat_offline' => '', 'chat_ask_name' => 1, 'chat_consent_text' => '', 'chat_store' => 1, 'chat_retention_days' => 90, // Public mode: visitors may use this share of the daily limit, and so many questions each a day. 'chat_public_share' => 50, 'chat_visitor_daily' => 20, 'chat_turnstile' => 1, // Guardrails the owner adds to the built-in ones (ChatGuard). 'chat_rules' => '', 'chat_blocked' => [], 'chat_blocked_reply' => '', 'chat_safety' => 0, 'chat_safety_answers' => 0, 'chat_length' => 'medium', 'chat_disclaimer' => '', ]; } /** * A setting, falling back to its default. * * @return mixed */ public static function get(string $key) { return get_plugin_settings()[$key] ?? self::defaults()[$key] ?? null; } /** * A feature's daily neuron limit; 0 means no limit of its own (the site-wide guard still applies). */ public static function limit(string $task): int { $key = self::LIMITS[$task] ?? ''; return $key === '' ? 0 : max(0, (int) self::get($key)); } /** * @param array $clean * @param array $input * @return array */ public static function sanitize(array $clean, array $input): array { $prev = get_plugin_settings(); $tab = isset($input['_tab']) ? sanitize_key((string) $input['_tab']) : ''; $d = self::defaults(); // 2.0: each content tool has its own page, saved as its own tab (related, search, // links, insights). It owns the keys with its prefix; everything else is kept. $feature = in_array($tab, \WordPress\CloudflareWorkersAiProvider\Admin\SettingsPage::FEATURE_TABS, true); if ($tab !== '' && $tab !== 'content' && !$feature) { foreach (self::KEYS as $key) { $clean[$key] = $prev[$key] ?? $d[$key]; } return $clean; } if ($feature) { foreach (self::KEYS as $key) { if (strpos($key, $tab . '_') !== 0) { $input[$key] = $prev[$key] ?? $d[$key]; } } } // The site assistant has its own page (Cloudflare AI > Site assistant): the Content tools // tab does not show its fields, so saving that tab keeps them as they are. if ($tab === 'content') { foreach (self::KEYS as $key) { if (strpos($key, 'chat_') === 0) { $input[$key] = $prev[$key] ?? $d[$key]; } } } // Saved without a tab (code calling update_option): an absent key keeps its value. // Saved from the tab: an absent checkbox means off. if ($tab === '') { foreach (self::KEYS as $key) { if (!array_key_exists($key, $input)) { $input[$key] = $prev[$key] ?? $d[$key]; } } } $flag = static fn(string $k): int => empty($input[$k]) ? 0 : 1; $int = static fn(string $k, int $min, int $max): int => max($min, min($max, (int) ($input[$k] ?? $d[$k]))); $score = static fn(string $k): float => round(max(0.0, min(1.0, (float) ($input[$k] ?? $d[$k]))), 2); $clean['related_enabled'] = $flag('related_enabled'); $clean['related_auto'] = $flag('related_auto'); $clean['related_count'] = $int('related_count', 1, 12); $clean['related_min_score'] = $score('related_min_score'); $clean['related_daily_queries'] = $int('related_daily_queries', 0, 1000000); $position = (string) ($input['related_position'] ?? $d['related_position']); $clean['related_position'] = in_array($position, self::RELATED_POSITIONS, true) ? $position : $d['related_position']; $clean['related_paragraph'] = $int('related_paragraph', 1, 50); // The tab shows a box for each embedded post type, ticked where the list is added: the // unticked ones are kept, so a post type embedded later gets the list. if (isset($input['related_types_shown'])) { $shown = array_map('sanitize_key', (array) ($input['related_types_shown'] ?? [])); $on = array_map('sanitize_key', (array) ($input['related_types'] ?? [])); $skip = array_values(array_diff($shown, $on)); } else { $skip = array_map('sanitize_key', (array) ($input['related_skip_types'] ?? $d['related_skip_types'])); } $clean['related_skip_types'] = array_values(array_unique(array_filter($skip))); $clean['related_title'] = mb_substr(sanitize_text_field((string) ($input['related_title'] ?? '')), 0, 80); $heading = (string) ($input['related_heading'] ?? $d['related_heading']); $clean['related_heading'] = in_array($heading, ['h2', 'h3', 'h4', 'p'], true) ? $heading : $d['related_heading']; $clean['related_excerpt'] = $flag('related_excerpt'); $clean['related_thumbnail'] = $flag('related_thumbnail'); $clean['related_why'] = $flag('related_why'); $clean['links_enabled'] = $flag('links_enabled'); // At least 1: a tool anyone with an account can use must never be without a limit. $clean['links_daily_neurons'] = $int('links_daily_neurons', 1, 10000000); $cap = (string) ($input['search_capability'] ?? $d['search_capability']); $clean['search_enabled'] = $flag('search_enabled'); $clean['search_capability'] = in_array($cap, self::SEARCH_CAPABILITIES, true) ? $cap : $d['search_capability']; $clean['search_takeover'] = $flag('search_takeover'); $clean['search_daily_neurons'] = $int('search_daily_neurons', 1, 10000000); $clean['search_min_score'] = $score('search_min_score'); $clean['insights_enabled'] = $flag('insights_enabled'); $clean['insights_retention_days'] = $int('insights_retention_days', 7, 730); $clean['insights_daily_rows'] = $int('insights_daily_rows', 100, 100000); $clean['insights_daily_neurons'] = $int('insights_daily_neurons', 1, 10000000); $clean['insights_gap_threshold'] = $score('insights_gap_threshold'); $cap = (string) ($input['chat_capability'] ?? $d['chat_capability']); $clean['chat_enabled'] = $flag('chat_enabled'); $clean['chat_capability'] = in_array($cap, self::CHAT_ACCESS, true) ? $cap : $d['chat_capability']; $clean['chat_floating'] = $flag('chat_floating'); $clean['chat_admin'] = $flag('chat_admin'); $clean['chat_name'] = mb_substr(sanitize_text_field((string) ($input['chat_name'] ?? '')), 0, 40); $clean['chat_welcome'] = mb_substr(sanitize_text_field((string) ($input['chat_welcome'] ?? '')), 0, 200); $clean['chat_min_score'] = $score('chat_min_score'); $clean['chat_rerank'] = $flag('chat_rerank'); $clean['chat_min_match'] = $score('chat_min_match'); $clean['search_rerank'] = $flag('search_rerank'); $cap = (string) ($input['agents_capability'] ?? $d['agents_capability']); $clean['agents_enabled'] = $flag('agents_enabled'); $clean['agents_capability'] = in_array($cap, self::SEARCH_CAPABILITIES, true) ? $cap : $d['agents_capability']; $clean['agents_daily_neurons'] = $int('agents_daily_neurons', 1, 10000000); $clean['chat_daily_neurons'] = $int('chat_daily_neurons', 1, 10000000); $text = static fn(string $k, int $max): string => mb_substr(sanitize_text_field((string) ($input[$k] ?? '')), 0, $max); $clean['chat_subtitle'] = $text('chat_subtitle', 80); $clean['chat_avatar'] = mb_substr(trim(sanitize_text_field((string) ($input['chat_avatar'] ?? ''))), 0, 4); $accent = sanitize_hex_color((string) ($input['chat_accent'] ?? '')); $clean['chat_accent'] = is_string($accent) && $accent !== '' ? $accent : $d['chat_accent']; $clean['chat_position'] = ($input['chat_position'] ?? '') === 'left' ? 'left' : 'right'; $chips = $input['chat_chips'] ?? []; $chips = is_array($chips) ? $chips : preg_split('/\R/', (string) $chips); $clean['chat_chips'] = array_values(array_slice(array_filter(array_map(static fn($c): string => mb_substr(sanitize_text_field((string) $c), 0, 80), (array) $chips), static fn(string $c): bool => $c !== ''), 0, 3)); $clean['chat_placeholder'] = $text('chat_placeholder', 80); $clean['chat_no_match'] = $text('chat_no_match', 300); $clean['chat_offline'] = $text('chat_offline', 300); $clean['chat_ask_name'] = $flag('chat_ask_name'); $clean['chat_consent_text'] = $text('chat_consent_text', 300); $clean['chat_store'] = $flag('chat_store'); $clean['chat_retention_days'] = $int('chat_retention_days', 1, 730); $clean['chat_public_share'] = $int('chat_public_share', 1, 100); $clean['chat_visitor_daily'] = $int('chat_visitor_daily', 1, 500); $clean['chat_turnstile'] = $flag('chat_turnstile'); $clean['chat_rules'] = mb_substr(sanitize_textarea_field((string) ($input['chat_rules'] ?? '')), 0, 1500); $blocked = $input['chat_blocked'] ?? []; $blocked = is_array($blocked) ? $blocked : preg_split('/[\r\n,]+/', (string) $blocked); $clean['chat_blocked'] = array_values(array_slice(array_unique(array_filter(array_map(static fn($t): string => mb_substr(trim(sanitize_text_field((string) $t)), 0, 60), (array) $blocked), static fn(string $t): bool => $t !== '')), 0, 100)); $clean['chat_blocked_reply'] = $text('chat_blocked_reply', 300); $clean['chat_safety'] = $flag('chat_safety'); $clean['chat_safety_answers'] = $flag('chat_safety_answers'); $clean['chat_length'] = in_array($input['chat_length'] ?? '', ['short', 'medium', 'long'], true) ? (string) $input['chat_length'] : 'medium'; $clean['chat_disclaimer'] = $text('chat_disclaimer', 200); return $clean; } }