__('After the content', 'aipcf-ai-provider-for-cloudflare'), 'before' => __('Before the content', 'aipcf-ai-provider-for-cloudflare'), 'paragraph' => __('After paragraph', 'aipcf-ai-provider-for-cloudflare'), ]; printf(' '; printf( '', esc_attr($opt), esc_attr((string) Settings::get('related_paragraph')), esc_attr__('Paragraph number', 'aipcf-ai-provider-for-cloudflare') ); echo FreeTierSettings::help(__('With "After paragraph", a post with fewer paragraphs gets the list at the end. Paragraphs inside quotes, lists, tables and groups are not counted.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_related', ['label_for' => 'aipcf-related-position', 'class' => 'aipcf-related-auto-row']); add_settings_field('related_types', __('On', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { $types = array_values(array_filter((array) (get_plugin_settings()['embed_post_types'] ?? []), 'post_type_exists')); $skip = (array) Settings::get('related_skip_types'); if ($types === []) { echo FreeTierSettings::help(__('No post types are in the content index yet (Settings > Content index).', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes return; } echo '
' . esc_html__('Post types that get the list', 'aipcf-ai-provider-for-cloudflare') . ''; foreach ($types as $i => $type) { $object = get_post_type_object($type); printf( '', esc_attr($opt), esc_attr($type), checked(!in_array($type, $skip, true), true, false), esc_html($object ? (string) $object->labels->name : $type), $i === 0 ? ' id="aipcf-related-types"' : '' ); } echo '
'; echo FreeTierSettings::help(__('The embedded post types that get the list. A post type you embed later gets it too until you untick it here.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_related', ['label_for' => 'aipcf-related-types', 'class' => 'aipcf-related-auto-row']); } /** * The list's title and look, for "Add automatically" and the shortcode (whose attributes * still override them). */ private static function relatedLook(): void { $opt = AIPCF_SETTINGS_OPTION; add_settings_field('related_title', __('Title', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { printf( ' ', esc_attr($opt), esc_attr((string) Settings::get('related_title')), esc_attr__('Related posts', 'aipcf-ai-provider-for-cloudflare') ); $heading = (string) Settings::get('related_heading'); printf(''; echo FreeTierSettings::help(__('Empty means "Related posts", in the visitor\'s language.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_related', ['label_for' => 'aipcf-related-title']); add_settings_field('related_excerpt', __('With each post', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { echo '
' . esc_html__('With each post', 'aipcf-ai-provider-for-cloudflare') . ''; $choices = [ 'related_why' => __('Why it is related', 'aipcf-ai-provider-for-cloudflare'), 'related_excerpt' => __('A short excerpt', 'aipcf-ai-provider-for-cloudflare'), 'related_thumbnail' => __('The featured image', 'aipcf-ai-provider-for-cloudflare'), ]; foreach ($choices as $key => $label) { printf( '', $key === 'related_why' ? ' id="aipcf-related-excerpt"' : '', esc_attr($opt), esc_attr($key), checked(!empty(Settings::get($key)), true, false), esc_html($label) ); } echo '
'; echo FreeTierSettings::help(__('"Why it is related" names the tags and categories the two posts share, the most specific first ("Also about Malcolm Gladwell and Success"). Otherwise it says how close in subject they are. No AI is used. The shortcode can still set its own: [aipcf_related title="…" heading="h3" excerpt="yes" thumbnail="yes" why="yes" count="3"].', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_related', ['label_for' => 'aipcf-related-excerpt']); } private static function registerChat(): void { add_settings_section('aipcf_chat', __('Site assistant', 'aipcf-ai-provider-for-cloudflare'), [self::class, 'renderChatIntro'], self::PAGE, SettingsPage::panel()); } public static function renderChatIntro(): void { echo FreeTierSettings::help(__('A chat for logged-in members that answers only from your posts and pages, with links to the posts it used. Its settings, its saved chats and the questions it could not answer are on their own page.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes self::needsEmbeddings(); printf( '

%s %s

', esc_attr(!empty(Settings::get('chat_enabled')) ? 'is-ok' : ''), esc_html(!empty(Settings::get('chat_enabled')) ? __('On', 'aipcf-ai-provider-for-cloudflare') : __('Off', 'aipcf-ai-provider-for-cloudflare')), esc_url(AssistantPage::pageUrl()), esc_html__('Open Site assistant', 'aipcf-ai-provider-for-cloudflare') ); } private static function registerInsights(): void { add_settings_section('aipcf_insights', __('Search insights', 'aipcf-ai-provider-for-cloudflare'), [self::class, 'renderInsightsIntro'], self::PAGE, SettingsPage::panel()); self::checkbox('insights_enabled', __('Search insights', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_insights', __('Record searches made on the site and show the content gaps', 'aipcf-ai-provider-for-cloudflare')); self::number('insights_gap_threshold', __('Gap threshold', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_insights', __('0 to 1. 0.5 means 50% match. A search whose closest post is a weaker match than this counts as a content gap.', 'aipcf-ai-provider-for-cloudflare'), 0, 1, 0.05); self::number('insights_retention_days', __('Keep searches for', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_insights', __('Older searches are deleted, including after Search insights is switched off.', 'aipcf-ai-provider-for-cloudflare'), 7, 730, 1, true, __('days', 'aipcf-ai-provider-for-cloudflare')); self::number('insights_daily_rows', __('Searches recorded', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_insights', __('A limit, so a flood of searches cannot fill the database.', 'aipcf-ai-provider-for-cloudflare'), 100, 100000, 1, true, __('a day', 'aipcf-ai-provider-for-cloudflare')); self::limit('insights_daily_neurons', 'aipcf_insights', __('For comparing searches with your content (about 0.03 neurons each) and the AI analysis. At least 1. It also counts against the daily budget.', 'aipcf-ai-provider-for-cloudflare')); } public static function renderInsightsIntro(): void { echo FreeTierSettings::help( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes __('Records searches made with the site\'s WordPress search (by everyone) and with Member search: the query, the number of results, the best match, and whether the searcher was logged in. No user, IP address or browser details are stored, and known bots are skipped. Searches are compared with your content in the background, never while a visitor waits.', 'aipcf-ai-provider-for-cloudflare') ); // Search insights records searches without Embeddings; only the content gaps need them. self::needsEmbeddings(__('Content gaps need the content index', 'aipcf-ai-provider-for-cloudflare')); if (\WordPress\CloudflareWorkersAiProvider\Features\Insights::enabled()) { printf('

%s

', esc_url(InsightsTab::pageUrl()), esc_html__('Open Search insights', 'aipcf-ai-provider-for-cloudflare')); } } private static function registerSearch(): void { add_settings_section('aipcf_search', __('Member search', 'aipcf-ai-provider-for-cloudflare'), [self::class, 'renderSearchIntro'], self::PAGE, SettingsPage::panel()); self::checkbox('search_enabled', __('Member search', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_search', __('Let logged-in members search by meaning with [aipcf_search]', 'aipcf-ai-provider-for-cloudflare')); self::capability('search_capability', 'aipcf_search', __('Visitors who are not logged in always get the normal WordPress search.', 'aipcf-ai-provider-for-cloudflare')); self::checkbox('search_takeover', __('The site\'s search', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_search', __('For these members, show the best matches above the site\'s normal search results', 'aipcf-ai-provider-for-cloudflare')); self::limit('search_daily_neurons', 'aipcf_search', __('For searches: about 0.03 neurons each, so 500 neurons a day is about 15,000 searches. At least 1. It also counts against the daily budget.', 'aipcf-ai-provider-for-cloudflare')); self::number('search_min_score', __('Minimum similarity', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_search', __('0 to 1. 0.55 means 55% match. Weaker matches are not shown. Off-topic searches usually match under 50%.', 'aipcf-ai-provider-for-cloudflare'), 0, 1, 0.05, true); } public static function renderSearchIntro(): void { echo FreeTierSettings::help( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes __('Finds posts by what they mean, not only the words typed, for logged-in members. Results only, no generated answers. Each search is one short embedding and one vector store query. Member search has a daily limit, allows 20 searches a minute per member, and keeps results for an hour.', 'aipcf-ai-provider-for-cloudflare') ); self::needsEmbeddings(); } private static function registerLinks(): void { add_settings_section('aipcf_links', __('Link assistant', 'aipcf-ai-provider-for-cloudflare'), [self::class, 'renderLinksIntro'], self::PAGE, SettingsPage::panel()); self::checkbox('links_enabled', __('Link assistant', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_links', __('Add AIPCF > Link assistant for everyone who can edit posts', 'aipcf-ai-provider-for-cloudflare')); self::limit('links_daily_neurons', 'aipcf_links', __('For the chat only. The suggestions themselves cost nothing. At least 1. It also counts against the daily budget.', 'aipcf-ai-provider-for-cloudflare')); } public static function renderLinksIntro(): void { echo FreeTierSettings::help(__('Suggests internal links for a post: which posts it should link to and which should link back, with the exact word and sentence for each, taken from your text. A chat answers questions about the suggestions. Nothing is changed in your posts.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes self::needsEmbeddings(); if (LinkAssistant::enabled()) { printf('

%s

', esc_url(LinkAssistant::pageUrl()), esc_html__('Open Link assistant', 'aipcf-ai-provider-for-cloudflare')); } } public static function renderIntro(): void { echo '

' . esc_html__('Features that use your content in the vector store. Each is off until you switch it on here.', 'aipcf-ai-provider-for-cloudflare') . '

'; if (!EmbeddingSettings::enabled()) { printf( '

%s %s

', esc_html__('Embeddings are off, so the Site assistant, Related posts, Link assistant, Member search and the content gaps of Search insights have nothing to work from yet.', 'aipcf-ai-provider-for-cloudflare'), esc_url(SettingsPage::tabUrl('embeddings')), esc_html__('Set up Embeddings', 'aipcf-ai-provider-for-cloudflare') ); } } /** * A short warning in a tool's section while Embeddings are off, so switching the tool on is * not a silent no-op. $label overrides the generic tag text (already translated). */ private static function needsEmbeddings(?string $label = null): void { if (!EmbeddingSettings::enabled()) { echo '

' . esc_html($label ?? __('Needs Embeddings', 'aipcf-ai-provider-for-cloudflare')) . '

'; } } public static function renderRelatedIntro(): void { $help = FreeTierSettings::help( __('A list of the posts closest in meaning, built in the background from the vectors already in the vector store. It uses no neurons, and nothing runs when a visitor opens a page.', 'aipcf-ai-provider-for-cloudflare'), __('The shortcode', 'aipcf-ai-provider-for-cloudflare'), '

[aipcf_related] ' . esc_html__('or with options:', 'aipcf-ai-provider-for-cloudflare') . ' [aipcf_related count="3" title="Read next" heading="h3" excerpt="yes" thumbnail="yes"]

' ); echo $help; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes; the extra is escaped above self::needsEmbeddings(); if (!Related::enabled()) { return; } $c = IndexTable::relatedCounts(); $queries = (int) (UsageTracker::todayTotals()['queries'][Related::TASK] ?? 0); echo ''; if ($c['waiting'] > 0) { $minutes = (int) ceil(Related::SETTLE / 60); printf( '

%s %s

', esc_attr__('Building...', 'aipcf-ai-provider-for-cloudflare'), esc_url(wp_nonce_url(admin_url('admin-post.php?action=aipcf_related_build'), 'aipcf_related_build')), esc_html__('Build related lists now', 'aipcf-ai-provider-for-cloudflare'), esc_html(sprintf( /* translators: %d: number of minutes */ _n('Related lists are also built in the background after each Embeddings run. Posts embedded in the last %d minute wait until the vector store can find them.', 'Related lists are also built in the background after each Embeddings run. Posts embedded in the last %d minutes wait until the vector store can find them.', $minutes, 'aipcf-ai-provider-for-cloudflare'), $minutes )) ); } } /** * One item of a counts line: "12 waiting" from a "%s waiting" pattern. */ private static function count(string $pattern, int $n): void { $parts = explode('%s', $pattern, 2); echo '' . esc_html($parts[0]) . '' . esc_html(number_format_i18n($n)) . '' . esc_html($parts[1] ?? '') . ''; } public static function handleRelatedBuild(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to do this.', 'aipcf-ai-provider-for-cloudflare'), 403); } check_admin_referer('aipcf_related_build'); $r = Related::refresh(microtime(true) + 20); /* translators: %d: number of posts */ $message = sprintf(_n('Related list built for %d post.', 'Related lists built for %d posts.', $r['done'], 'aipcf-ai-provider-for-cloudflare'), $r['done']); set_transient('aipcf_notice_' . get_current_user_id(), ['type' => $r['stopped'] === '' ? 'success' : 'warning', 'message' => trim($message . ' ' . $r['stopped'])], 60); wp_safe_redirect(FeaturesPage::url('related') . '#aipcf-related-status'); exit; } /** * "Who can use it": a capability, from every member down to administrators. */ private static function capability(string $key, string $section, string $help): void { $opt = AIPCF_SETTINGS_OPTION; $id = 'aipcf-' . str_replace('_', '-', $key); add_settings_field($key, __('Who can use it', 'aipcf-ai-provider-for-cloudflare'), static function () use ($key, $opt, $id, $help): void { $labels = [ 'read' => __('Every logged-in member', 'aipcf-ai-provider-for-cloudflare'), 'edit_posts' => __('Contributors and up', 'aipcf-ai-provider-for-cloudflare'), 'publish_posts' => __('Authors and up', 'aipcf-ai-provider-for-cloudflare'), 'edit_others_posts' => __('Editors and up', 'aipcf-ai-provider-for-cloudflare'), 'manage_options' => __('Administrators only', 'aipcf-ai-provider-for-cloudflare'), ]; printf(''; echo FreeTierSettings::help($help); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, $section, ['label_for' => $id]); } private static function checkbox(string $key, string $label, string $section, string $desc): void { $opt = AIPCF_SETTINGS_OPTION; $id = 'aipcf-' . str_replace('_', '-', $key); add_settings_field($key, $label, static function () use ($key, $opt, $desc, $id): void { printf('', esc_attr($id), esc_attr($opt), esc_attr($key), checked(!empty(get_plugin_settings()[$key]), true, false), esc_html($desc)); }, self::PAGE, $section, ['label_for' => $id]); } /** * A tool's own daily limit: "Daily limit", the "neurons a day" unit, and the help (what it * pays for, "At least 1." and that it also counts against the daily budget). At least 1 (a * security decision: these tools run for visitors or many users). */ private static function limit(string $key, string $section, string $help, bool $advanced = false): void { $opt = AIPCF_SETTINGS_OPTION; $id = 'aipcf-' . str_replace('_', '-', $key); add_settings_field($key, __('Daily limit', 'aipcf-ai-provider-for-cloudflare'), static function () use ($key, $opt, $help, $id): void { printf( '%s', esc_attr($id), esc_attr($opt), esc_attr($key), esc_attr((string) Settings::get($key)), esc_html__('neurons a day', 'aipcf-ai-provider-for-cloudflare') ); echo FreeTierSettings::help($help); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, $section, FreeTierSettings::fieldArgs($advanced) + ['label_for' => $id]); } /** * @param int|float $min * @param int|float $max */ private static function number(string $key, string $label, string $section, string $desc, $min, $max, float $step = 1, bool $advanced = false, string $unit = ''): void { $opt = AIPCF_SETTINGS_OPTION; $id = 'aipcf-' . str_replace('_', '-', $key); add_settings_field($key, $label, static function () use ($key, $opt, $desc, $min, $max, $step, $id, $unit): void { printf( '', esc_attr($id), esc_attr($opt), esc_attr($key), esc_attr((string) Settings::get($key)), esc_attr((string) $min), esc_attr((string) $max), esc_attr((string) $step) ); if ($unit !== '') { echo '' . esc_html($unit) . ''; } if ($desc !== '') { echo FreeTierSettings::help($desc); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes } }, self::PAGE, $section, FreeTierSettings::fieldArgs($advanced) + ['label_for' => $id]); } }