Site assistant: its settings with a live preview, today's use, the * questions it could not answer, and the chats members agreed to save. */ final class AssistantPage { public const SLUG = 'aipcf-assistant'; private const PER_PAGE = 20; /** Checkboxes on the form: absent means off. */ private const FLAGS = ['chat_enabled', 'chat_floating', 'chat_admin', 'chat_ask_name', 'chat_store', 'chat_turnstile', 'chat_safety', 'chat_safety_answers', 'chat_rerank']; private const FIELDS = [ 'chat_enabled', 'chat_capability', 'chat_floating', 'chat_admin', 'chat_name', 'chat_subtitle', 'chat_avatar', 'chat_accent', 'chat_position', 'chat_welcome', 'chat_placeholder', 'chat_chips', 'chat_no_match', 'chat_offline', 'chat_ask_name', 'chat_store', 'chat_consent_text', 'chat_retention_days', 'chat_daily_neurons', 'chat_min_score', 'chat_rerank', 'chat_min_match', 'chat_public_share', 'chat_visitor_daily', 'chat_turnstile', 'chat_rules', 'chat_blocked', 'chat_blocked_reply', 'chat_safety', 'chat_safety_answers', 'chat_length', 'chat_disclaimer', ]; public static function boot(): void { // Before the Link assistant (20): Settings, Site assistant, Link assistant, Search insights, Activity. add_action('admin_menu', [self::class, 'registerMenu'], 19); add_action('admin_post_aipcf_chat_save', [self::class, 'handleSave']); add_action('admin_post_aipcf_chat_export', [self::class, 'handleExport']); add_action('admin_post_aipcf_chat_clear', [self::class, 'handleClear']); add_action('admin_post_aipcf_chat_toggle', [self::class, 'handleToggle']); add_action('admin_enqueue_scripts', [self::class, 'enqueue']); } public static function registerMenu(): void { // 2.0: opened from its card on Features, not from the menu (which then highlights Features). add_submenu_page('', __('Site assistant', 'aipcf-ai-provider-for-cloudflare'), __('Site assistant', 'aipcf-ai-provider-for-cloudflare'), 'manage_options', self::SLUG, [self::class, 'render']); } public static function pageUrl(array $args = []): string { return add_query_arg($args, admin_url('admin.php?page=' . self::SLUG)); } /** * The widget's own styles, for the live preview. */ public static function enqueue(string $hook): void { if (strpos($hook, self::SLUG) === false) { return; } wp_enqueue_style('aipcf-chat', plugin_dir_url(AIPCF_FILE) . 'assets/chat/chat.css', [], AIPCF_VERSION); } public static function render(): 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); } $on = !empty(Settings::get('chat_enabled')); $online = Chat::online(); echo '
' . esc_html__('For the button, the member\'s messages and the links. The text on it turns black or white to stay readable.', 'aipcf-ai-provider-for-cloudflare') . '
'; echo ''; self::pane('messages'); self::text('chat_welcome', __('Welcome message', 'aipcf-ai-provider-for-cloudflare'), Chat::welcome('{name}'), 200, __('{name} becomes the member\'s first name. Leave empty for the text shown.', 'aipcf-ai-provider-for-cloudflare')); self::text('chat_placeholder', __('Text in the empty question box', 'aipcf-ai-provider-for-cloudflare'), __('Ask a question', 'aipcf-ai-provider-for-cloudflare'), 80); $chips = array_pad(array_values((array) $g('chat_chips')), 3, ''); $examples = self::chipExamples(); echo ''; self::area('chat_no_match', __('Reply when the site has no answer', 'aipcf-ai-provider-for-cloudflare'), Chat::noMatch(), __('Sent without asking the AI, so it costs nothing. Leave empty for the text shown.', 'aipcf-ai-provider-for-cloudflare')); self::area('chat_offline', __('Reply when the daily limit is used up', 'aipcf-ai-provider-for-cloudflare'), Chat::offlineText(), __('Shown instead of the question box until midnight UTC. Say when to come back, or where else to get help (a contact page, an email address).', 'aipcf-ai-provider-for-cloudflare')); echo ''; self::guardPane(); self::pane('saving'); self::check('chat_ask_name', __('Ask for a name (no email or phone) before the first question', 'aipcf-ai-provider-for-cloudflare')); self::check('chat_store', __('Offer to save the chat, so you can read and analyse it here', 'aipcf-ai-provider-for-cloudflare'), __('Only chats the member agrees to save are kept: the name they gave, their questions and the answers. Nothing is kept for members who leave the box unticked.', 'aipcf-ai-provider-for-cloudflare')); self::area('chat_consent_text', __('Consent text', 'aipcf-ai-provider-for-cloudflare'), Chat::consentText(), __('Next to the box members tick. Leave empty for the text shown.', 'aipcf-ai-provider-for-cloudflare')); printf( '
%s
%s
%s
', esc_html__('Daily limit', 'aipcf-ai-provider-for-cloudflare'), (int) $g('chat_daily_neurons'), esc_html__('neurons a day', 'aipcf-ai-provider-for-cloudflare'), esc_html__('Each answer is one short embedding and one call to your default text model: usually 1 to 5 neurons, so 1,000 is a few hundred answers. A question with no match costs only the embedding. When the limit is used up, the chat goes offline until midnight UTC. It also counts against the daily budget.', 'aipcf-ai-provider-for-cloudflare') ); self::check('chat_rerank', __('Check the best parts of posts again with a second model', 'aipcf-ai-provider-for-cloudflare'), __('About 2 neurons more an answer. Better answers, and an off-topic question gets the "no answer" reply without asking the AI, which saves 10 to 30 neurons.', 'aipcf-ai-provider-for-cloudflare')); printf( '%s
', esc_html__('Minimum match, when checked again', 'aipcf-ai-provider-for-cloudflare'), esc_attr((string) $g('chat_min_match')), esc_html__('0 to 1. 0.08 suits most sites: right answers usually score far higher, off-topic questions near 0.', 'aipcf-ai-provider-for-cloudflare') ); printf( '%s
', esc_html__('Minimum match, without the check', 'aipcf-ai-provider-for-cloudflare'), esc_attr((string) $g('chat_min_score')), esc_html__('0 to 1, by meaning only. Parts of posts that match less are left out, unless they contain the question\'s own words. Around 0.35.', 'aipcf-ai-provider-for-cloudflare') ); echo '' . esc_html__('It reads the text Embeddings keep for each post, including text inside shortcodes. If parts of your posts are meant only for some members, leave those posts out of Embeddings or keep the assistant off.', 'aipcf-ai-provider-for-cloudflare') . '
'; echo ''; echo ''; echo ''; // The live preview: the widget's own markup and styles, updated as the fields change. $u = wp_get_current_user(); $first = trim((string) $u->first_name) !== '' ? (string) $u->first_name : (string) strtok((string) $u->display_name, ' '); $samples = array_map(static fn(\WP_Post $p): string => html_entity_decode(get_the_title($p), ENT_QUOTES, 'UTF-8'), get_posts(['numberposts' => 2, 'post_type' => 'post', 'post_status' => 'publish'])); echo '' . esc_html__('Preview', 'aipcf-ai-provider-for-cloudflare') . '
'; echo '' . esc_html__('Visitors can use it too.', 'aipcf-ai-provider-for-cloudflare') . ' ' . esc_html__('Anyone, including bots, can then ask questions, and each answer costs neurons. These protect the daily limit and your members:', 'aipcf-ai-provider-for-cloudflare') . '
%s
%s
' . esc_html__('Always on', 'aipcf-ai-provider-for-cloudflare') . '
%s
%s
%s
', esc_attr($id), esc_attr($id), esc_attr($key), checked(!empty(Settings::get($key)), true, false), esc_html($label), $help !== '' ? '
%s
%s
' . esc_html__('From saved chats: what members asked that your posts do not cover yet. Ideas for what to write next.', 'aipcf-ai-provider-for-cloudflare') . '
'; if ($rows === []) { echo '' . esc_html__('None yet.', 'aipcf-ai-provider-for-cloudflare') . '
| ' . esc_html__('Question', 'aipcf-ai-provider-for-cloudflare') . ' | ' . esc_html__('Times', 'aipcf-ai-provider-for-cloudflare') . ' | ' . esc_html__('Last asked', 'aipcf-ai-provider-for-cloudflare') . ' |
|---|---|---|
| %s | %s | %s |
' . esc_html(sprintf( /* translators: %d: number of days */ _n('Chats members agreed to save, kept for %d day.', 'Chats members agreed to save, kept for %d days.', (int) Settings::get('chat_retention_days'), 'aipcf-ai-provider-for-cloudflare'), (int) Settings::get('chat_retention_days') )) . '
'; if ($convs === []) { echo '' . esc_html__('None yet.', 'aipcf-ai-provider-for-cloudflare') . '