plugin_name = $plugin_name; $this->version = $version; } public function enqueue_styles( $hook_suffix ) { if ( ! $this->is_plugin_screen( $hook_suffix ) ) { return; } wp_enqueue_style( $this->admin_handle(), plugin_dir_url( __FILE__ ) . 'css/ai-chat-bedrock-admin.css', array( 'wp-color-picker' ), $this->version ); if ( false !== strpos( $hook_suffix, $this->plugin_name . '-mcp' ) ) { wp_enqueue_style( $this->plugin_name . '-mcp', plugin_dir_url( __FILE__ ) . 'css/ai-chat-bedrock-mcp.css', array(), $this->version ); } } public function enqueue_scripts( $hook_suffix ) { if ( ! $this->is_plugin_screen( $hook_suffix ) ) { return; } wp_enqueue_script( $this->admin_handle(), plugin_dir_url( __FILE__ ) . 'js/ai-chat-bedrock-admin.js', array( 'jquery', 'common', 'wp-a11y', 'wp-color-picker' ), $this->version, true ); if ( false !== strpos( (string) $hook_suffix, $this->plugin_name . '-eval' ) ) { wp_enqueue_script( $this->plugin_name . '-eval', plugin_dir_url( __FILE__ ) . 'js/ai-chat-bedrock-eval.js', array(), $this->version, true ); wp_localize_script( $this->plugin_name . '-eval', 'AICFABEval', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'aicfab_eval' ), 'i18n' => array( 'question' => __( 'Question', 'ai-chat-for-amazon-bedrock' ), 'expect' => __( 'Expect', 'ai-chat-for-amazon-bedrock' ), 'mustInclude' => __( 'Must include, comma separated', 'ai-chat-for-amazon-bedrock' ), 'mustNotInclude' => __( 'Must not include, comma separated', 'ai-chat-for-amazon-bedrock' ), 'cite' => __( 'Page the answer should credit', 'ai-chat-for-amazon-bedrock' ), 'minRelevance' => __( 'Minimum match, 0 to 1', 'ai-chat-for-amazon-bedrock' ), 'tokenCeiling' => __( 'Output token ceiling', 'ai-chat-for-amazon-bedrock' ), 'remove' => __( 'Remove', 'ai-chat-for-amazon-bedrock' ), /* translators: %s: the question of an answer-check case. */ 'removeCase' => __( 'Remove the case: %s', 'ai-chat-for-amazon-bedrock' ), 'expectLabels' => array( 'grounded' => __( 'grounded: from the site', 'ai-chat-for-amazon-bedrock' ), 'unsupported' => __( 'unsupported: nothing on it', 'ai-chat-for-amazon-bedrock' ), 'tool' => __( 'tool: calls a tool', 'ai-chat-for-amazon-bedrock' ), ), 'removed' => __( 'Case removed. Save to keep the change.', 'ai-chat-for-amazon-bedrock' ), 'saving' => __( 'Saving cases…', 'ai-chat-for-amazon-bedrock' ), 'saved' => __( 'Cases saved.', 'ai-chat-for-amazon-bedrock' ), /* translators: %d: number of cases that were dropped. */ 'savedWithDrops' => __( 'Cases saved. %d could not be run and were dropped: a question and an expectation are both required.', 'ai-chat-for-amazon-bedrock' ), 'proposing' => __( 'Reading recorded questions…', 'ai-chat-for-amazon-bedrock' ), /* translators: %d: number of proposed questions. */ 'proposed' => __( '%d question(s) proposed. Add the ones you want, then state what a good answer contains.', 'ai-chat-for-amazon-bedrock' ), 'addThis' => __( 'Add as a case', 'ai-chat-for-amazon-bedrock' ), 'addedUnsaved' => __( 'Added to the table. Save to keep it.', 'ai-chat-for-amazon-bedrock' ), 'running' => __( 'Running the checks. One Amazon Bedrock request per case.', 'ai-chat-for-amazon-bedrock' ), 'ran' => __( 'Run finished.', 'ai-chat-for-amazon-bedrock' ), 'failed' => __( 'The request failed.', 'ai-chat-for-amazon-bedrock' ), 'pass' => __( 'Pass', 'ai-chat-for-amazon-bedrock' ), 'fail' => __( 'Fail', 'ai-chat-for-amazon-bedrock' ), 'casesPassed' => __( 'cases passed', 'ai-chat-for-amazon-bedrock' ), 'neverChecked' => __( 'Never checked in this run:', 'ai-chat-for-amazon-bedrock' ), 'notComparable' => __( 'not comparable with the previous run, the number of checks changed', 'ai-chat-for-amazon-bedrock' ), 'sinceLastRun' => __( 'since the previous run', 'ai-chat-for-amazon-bedrock' ), ), ) ); } if ( false !== strpos( (string) $hook_suffix, $this->plugin_name . '-scaffold' ) ) { wp_enqueue_script( $this->plugin_name . '-scaffold', plugin_dir_url( __FILE__ ) . 'js/ai-chat-bedrock-scaffold.js', array(), $this->version, true ); wp_localize_script( $this->plugin_name . '-scaffold', 'aicfabScaffold', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'aicfab_scaffold' ), 'i18n' => array( 'describeFirst' => __( 'Describe the site first.', 'ai-chat-for-amazon-bedrock' ), 'thinking' => __( 'Working out which pages this site needs…', 'ai-chat-for-amazon-bedrock' ), 'writing' => __( 'Writing…', 'ai-chat-for-amazon-bedrock' ), 'skippedByYou' => __( 'Skipped.', 'ai-chat-for-amazon-bedrock' ), 'editDraft' => __( 'Edit the draft', 'ai-chat-for-amazon-bedrock' ), /* translators: 1: pages handled so far, 2: pages in total. */ 'progress' => __( '%1$d of %2$d done…', 'ai-chat-for-amazon-bedrock' ), /* translators: 1: drafts created, 2: pages skipped, 3: pages that failed. */ 'finished' => __( 'Finished. Drafts created: %1$d. Skipped: %2$d. Failed: %3$d.', 'ai-chat-for-amazon-bedrock' ), 'unexpected' => __( 'The request could not be completed.', 'ai-chat-for-amazon-bedrock' ), 'includeLabel' => __( 'Include', 'ai-chat-for-amazon-bedrock' ), 'titleLabel' => __( 'Page title', 'ai-chat-for-amazon-bedrock' ), ), ) ); } wp_localize_script( $this->admin_handle(), 'ai_chat_bedrock_admin', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'ai_chat_bedrock_admin' ), 'mcp_nonce' => wp_create_nonce( 'ai_chat_bedrock_mcp_nonce' ), 's3v_nonce' => wp_create_nonce( 'ai_chat_bedrock_s3_vectors' ), 'rest_nonce' => wp_create_nonce( 'wp_rest' ), 'generate_url' => rest_url( AI_Chat_Bedrock_WP_MCP_Server::NAMESPACE_V1 . AI_Chat_Bedrock_Generator_Stream::REST_ROUTE ), 'i18n' => array( 'generating' => __( 'Writing the draft…', 'ai-chat-for-amazon-bedrock' ), 'indexing' => __( 'Indexing…', 'ai-chat-for-amazon-bedrock' ), 'index_button' => __( 'Index content now', 'ai-chat-for-amazon-bedrock' ), /* translators: 1: items indexed so far, 2: items still pending. */ 'index_progress' => __( '%1$d indexed, %2$d remaining…', 'ai-chat-for-amazon-bedrock' ), /* translators: 1: items indexed, 2: items skipped, 3: items that failed. */ 'index_done' => __( 'Finished: %1$d indexed, %2$d already current, %3$d failed.', 'ai-chat-for-amazon-bedrock' ), /* translators: %s: title the model chose for the draft. */ 'generating_titled' => __( 'Writing “%s”…', 'ai-chat-for-amazon-bedrock' ), 'generate_button' => __( 'Generate draft', 'ai-chat-for-amazon-bedrock' ), 'generate_topic' => __( 'Enter a topic first.', 'ai-chat-for-amazon-bedrock' ), /* translators: 1: draft title, 2: word count. */ 'generate_done' => __( 'Draft "%1$s" created with about %2$d words.', 'ai-chat-for-amazon-bedrock' ), 'generate_edit' => __( 'Open the draft', 'ai-chat-for-amazon-bedrock' ), /* translators: %s: model identifier that answered instead of the main model. */ 'generate_fallback' => __( 'The main model was unavailable, so %s wrote this draft.', 'ai-chat-for-amazon-bedrock' ), 'ajax_error' => __( 'The request could not be completed.', 'ai-chat-for-amazon-bedrock' ), 'testing' => __( 'Testing…', 'ai-chat-for-amazon-bedrock' ), 'status_pass' => __( 'Pass', 'ai-chat-for-amazon-bedrock' ), 'status_off' => __( 'Not in use', 'ai-chat-for-amazon-bedrock' ), 'status_warn' => __( 'Review', 'ai-chat-for-amazon-bedrock' ), 'status_fail' => __( 'Action required', 'ai-chat-for-amazon-bedrock' ), 'no_servers' => __( 'No MCP servers registered.', 'ai-chat-for-amazon-bedrock' ), 'no_servers_hint' => __( 'Add one below to let the chat call its tools, such as an Amazon Bedrock AgentCore Gateway.', 'ai-chat-for-amazon-bedrock' ), 'missing_fields' => __( 'A server name and HTTPS URL are required.', 'ai-chat-for-amazon-bedrock' ), 'adding' => __( 'Adding…', 'ai-chat-for-amazon-bedrock' ), 'add_server' => __( 'Add server', 'ai-chat-for-amazon-bedrock' ), 'removing' => __( 'Removing…', 'ai-chat-for-amazon-bedrock' ), 'remove' => __( 'Remove', 'ai-chat-for-amazon-bedrock' ), 'refreshing' => __( 'Refreshing…', 'ai-chat-for-amazon-bedrock' ), 'refresh' => __( 'Refresh', 'ai-chat-for-amazon-bedrock' ), 'loading_tools' => __( 'Loading tools…', 'ai-chat-for-amazon-bedrock' ), 'no_tools' => __( 'No tools found.', 'ai-chat-for-amazon-bedrock' ), 'no_parameters' => __( 'No parameters required.', 'ai-chat-for-amazon-bedrock' ), 'available' => __( 'Available', 'ai-chat-for-amazon-bedrock' ), 'unavailable' => __( 'Unavailable', 'ai-chat-for-amazon-bedrock' ), 'confirm_remove_server' => __( 'Remove this MCP server?', 'ai-chat-for-amazon-bedrock' ), 'view_tools' => __( 'View tools', 'ai-chat-for-amazon-bedrock' ), 'parameters' => __( 'Parameters', 'ai-chat-for-amazon-bedrock' ), ), ) ); if ( false !== strpos( $hook_suffix, $this->plugin_name . '-mcp' ) ) { wp_enqueue_script( $this->plugin_name . '-mcp', plugin_dir_url( __FILE__ ) . 'js/ai-chat-bedrock-mcp.js', array( 'jquery', $this->admin_handle() ), $this->version, true ); } } public function add_plugin_admin_menu() { add_menu_page( __( 'AI Chatbot, Agents & MCP for Amazon Bedrock', 'ai-chat-for-amazon-bedrock' ), __( 'AI Chat Bedrock', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name, array( $this, 'display_plugin_admin_page' ), 'dashicons-format-chat', 100 ); // The first item opens the menu's own page; named for what it is, not the plugin again. add_submenu_page( $this->plugin_name, __( 'Overview', 'ai-chat-for-amazon-bedrock' ), __( 'Overview', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name, array( $this, 'display_plugin_admin_page' ) ); // Daily work first: trying the chat, reading what visitors asked and what they need. add_submenu_page( $this->plugin_name, __( 'Test Chat', 'ai-chat-for-amazon-bedrock' ), __( 'Test Chat', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name . '-test', array( $this, 'display_plugin_admin_test_page' ) ); add_submenu_page( $this->plugin_name, __( 'Conversations', 'ai-chat-for-amazon-bedrock' ), __( 'Conversations', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name . '-conversations', array( $this, 'display_plugin_admin_conversations_page' ) ); // Listed while requests are taken, or while any are still stored. $waiting = AI_Chat_Bedrock_Leads::enabled() ? AI_Chat_Bedrock_Leads::waiting() : 0; if ( AI_Chat_Bedrock_Leads::enabled() || AI_Chat_Bedrock_Leads::query( array( 'per_page' => 1 ) )['total'] > 0 ) { $bubble = $waiting > 0 ? ' ' . esc_html( number_format_i18n( $waiting ) ) . '' : ''; add_submenu_page( $this->plugin_name, __( 'Contact requests', 'ai-chat-for-amazon-bedrock' ), __( 'Contact requests', 'ai-chat-for-amazon-bedrock' ) . $bubble, AI_Chat_Bedrock_Leads::CAPABILITY, $this->plugin_name . '-leads', array( $this, 'display_plugin_admin_leads_page' ) ); } add_submenu_page( $this->plugin_name, __( 'Answer checks', 'ai-chat-for-amazon-bedrock' ), __( 'Answer checks', 'ai-chat-for-amazon-bedrock' ), AI_Chat_Bedrock_Eval::CAPABILITY, $this->plugin_name . '-eval', array( $this, 'display_plugin_admin_eval_page' ) ); if ( AI_Chat_Bedrock_Metrics::enabled() ) { add_submenu_page( $this->plugin_name, __( 'Business insights', 'ai-chat-for-amazon-bedrock' ), __( 'Business insights', 'ai-chat-for-amazon-bedrock' ), AI_Chat_Bedrock_Metrics::CAPABILITY, $this->plugin_name . '-metrics', array( $this, 'display_plugin_admin_metrics_page' ) ); } // Making content. add_submenu_page( $this->plugin_name, __( 'Content Generator', 'ai-chat-for-amazon-bedrock' ), __( 'Content Generator', 'ai-chat-for-amazon-bedrock' ), 'edit_posts', $this->plugin_name . '-generator', array( $this, 'display_plugin_admin_generator_page' ) ); add_submenu_page( $this->plugin_name, __( 'Site Pages', 'ai-chat-for-amazon-bedrock' ), __( 'Site Pages', 'ai-chat-for-amazon-bedrock' ), AI_Chat_Bedrock_Scaffold::CAPABILITY, $this->plugin_name . '-scaffold', array( $this, 'display_plugin_admin_scaffold_page' ) ); if ( class_exists( 'AI_Chat_Bedrock_YouTube' ) ) { AI_Chat_Bedrock_YouTube::add_page( $this->plugin_name ); } // Setting it up. add_submenu_page( $this->plugin_name, __( 'Chat Profiles', 'ai-chat-for-amazon-bedrock' ), __( 'Chat Profiles', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name . '-profiles', array( $this, 'display_plugin_admin_profiles_page' ) ); add_submenu_page( $this->plugin_name, __( 'MCP Settings', 'ai-chat-for-amazon-bedrock' ), __( 'MCP Settings', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name . '-mcp', array( $this, 'display_plugin_admin_mcp_page' ) ); add_submenu_page( $this->plugin_name, __( 'Settings', 'ai-chat-for-amazon-bedrock' ), __( 'Settings', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name . '-settings', array( $this, 'display_plugin_admin_settings_page' ) ); add_submenu_page( $this->plugin_name, __( 'Diagnostics', 'ai-chat-for-amazon-bedrock' ), __( 'Diagnostics', 'ai-chat-for-amazon-bedrock' ), 'manage_options', $this->plugin_name . '-diagnostics', array( $this, 'display_plugin_admin_diagnostics_page' ) ); } /** * Contact requests screen. */ public function display_plugin_admin_leads_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-leads.php'; } public function display_plugin_admin_generator_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-generator.php'; } /** * Answer checks screen. * * @return void */ public function display_plugin_admin_eval_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-eval.php'; } public function display_plugin_admin_scaffold_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-scaffold.php'; } public function display_plugin_admin_profiles_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-profiles.php'; } public function display_plugin_admin_conversations_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-conversations.php'; } public function display_plugin_admin_diagnostics_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-diagnostics.php'; } /** * Business insights screen. * * @return void */ public function display_plugin_admin_metrics_page() { include plugin_dir_path( __FILE__ ) . 'partials/ai-chat-bedrock-admin-metrics.php'; } /** * Offer alt text generation from the media library list view. * * @param array $actions Existing actions. * @param WP_Post $post Attachment. * @return array */ public function add_media_row_action( $actions, $post ) { if ( ! AI_Chat_Bedrock_Media_Assistant::enabled() || ! $post instanceof WP_Post ) { return $actions; } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return $actions; } if ( ! in_array( (string) get_post_mime_type( $post ), AI_Chat_Bedrock_Media_Assistant::supported_types(), true ) ) { return $actions; } $url = wp_nonce_url( add_query_arg( array( 'action' => 'ai_chat_bedrock_alt_text', 'attachment' => $post->ID, ), admin_url( 'admin-post.php' ) ), 'ai_chat_bedrock_alt_text_' . $post->ID ); $actions['aicfab_alt_text'] = '' . esc_html__( 'Generate alt text', 'ai-chat-for-amazon-bedrock' ) . ''; return $this->add_image_edit_actions( $actions, $post ); } /** * Offer background removal and upscaling for an image. * * Upscaling is offered only for images the upscaler takes, at most about one megapixel. * * @param array $actions Existing actions. * @param WP_Post $post Attachment. * @return array */ private function add_image_edit_actions( $actions, $post ) { if ( ! AI_Chat_Bedrock_Images::editing_enabled() || ! current_user_can( 'upload_files' ) ) { return $actions; } if ( ! in_array( (string) get_post_mime_type( $post ), AI_Chat_Bedrock_Images::editable_types(), true ) ) { return $actions; } $edits = array( 'remove_background' => __( 'Remove background', 'ai-chat-for-amazon-bedrock' ), ); $meta = wp_get_attachment_metadata( $post->ID ); if ( is_array( $meta ) && ! empty( $meta['width'] ) && ! empty( $meta['height'] ) && (int) $meta['width'] * (int) $meta['height'] <= AI_Chat_Bedrock_Images::UPSCALE_MAX_PIXELS ) { $edits['upscale'] = __( 'Upscale 4×', 'ai-chat-for-amazon-bedrock' ); } foreach ( $edits as $edit => $label ) { $url = wp_nonce_url( add_query_arg( array( 'action' => 'ai_chat_bedrock_edit_image', 'edit' => $edit, 'attachment' => $post->ID, ), admin_url( 'admin-post.php' ) ), 'ai_chat_bedrock_edit_image_' . $edit . '_' . $post->ID ); $actions[ 'aicfab_' . $edit ] = '' . esc_html( $label ) . ''; } return $actions; } /** * Remove the background of an image, or upscale it, into a new attachment. * * The outcome is kept for the current user for a minute and shown on the Media Library, * so an error message never travels in the URL. */ public function handle_edit_image_action() { $attachment = isset( $_GET['attachment'] ) ? absint( wp_unslash( $_GET['attachment'] ) ) : 0; $edit = isset( $_GET['edit'] ) ? sanitize_key( wp_unslash( $_GET['edit'] ) ) : ''; if ( ! in_array( $edit, array( 'remove_background', 'upscale' ), true ) ) { wp_die( esc_html__( 'Unknown image edit.', 'ai-chat-for-amazon-bedrock' ), '', array( 'response' => 400 ) ); } check_admin_referer( 'ai_chat_bedrock_edit_image_' . $edit . '_' . $attachment ); if ( ! current_user_can( 'edit_post', $attachment ) || ! current_user_can( 'upload_files' ) ) { wp_die( esc_html__( 'Permission denied.', 'ai-chat-for-amazon-bedrock' ), '', array( 'response' => 403 ) ); } if ( ! AI_Chat_Bedrock_Images::editing_enabled() ) { wp_die( esc_html__( 'Image editing is off. Turn on the media helpers and choose an image model first.', 'ai-chat-for-amazon-bedrock' ), '', array( 'response' => 400 ) ); } $result = 'upscale' === $edit ? AI_Chat_Bedrock_Images::upscale( $attachment ) : AI_Chat_Bedrock_Images::remove_background( $attachment ); set_transient( 'aicfab_image_edit_' . get_current_user_id(), is_wp_error( $result ) ? array( 'error' => $result->get_error_message() ) : array( 'attachment' => (int) $result ), MINUTE_IN_SECONDS ); wp_safe_redirect( add_query_arg( 'aicfab-image', '1', admin_url( 'upload.php' ) ) ); exit; } /** * Report the outcome of an image edit on the media screen. */ private function render_image_edit_notice() { if ( ! isset( $_GET['aicfab-image'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $key = 'aicfab_image_edit_' . get_current_user_id(); $outcome = get_transient( $key ); delete_transient( $key ); if ( ! is_array( $outcome ) ) { return; } if ( ! empty( $outcome['attachment'] ) ) { printf( '
%1$s %3$s
%s
%2$s
%2$s
%1$s %2$s %4$s
%1$s %3$s
' . esc_html__( 'Prefer wp-config.php constants or an IAM role. Database credentials are encrypted with WordPress salts and are never displayed after saving.', 'ai-chat-for-amazon-bedrock' ) . '
'; echo 'AI_CHAT_BEDROCK_API_KEY, AI_CHAT_BEDROCK_AWS_ACCESS_KEY, AI_CHAT_BEDROCK_AWS_SECRET_KEY, AI_CHAT_BEDROCK_AWS_SESSION_TOKEN
' . esc_html__( 'Active credential source:', 'ai-chat-for-amazon-bedrock' ) . ' ' . esc_html( $status['message'] ); if ( $status['temporary'] ) { echo ' ' . esc_html__( '(temporary credentials)', 'ai-chat-for-amazon-bedrock' ) . ''; } echo '
'; } else { echo '' . esc_html__( 'No usable AWS credentials were found.', 'ai-chat-for-amazon-bedrock' ) . '
'; } } public function aws_region_render() { $this->select( 'aws_region', AI_Chat_Bedrock_Models::regions(), 'us-east-1' ); } public function bedrock_api_key_render() { $this->credential_input( 'bedrock_api_key', true ); echo '' . esc_html__( 'The quickest way to connect: create a long-term API key in the Amazon Bedrock console under API keys, choose the same Region as above, and paste it here. It is used for chat, the model list and embeddings. Knowledge Bases, Prompt Management and AgentCore still need access keys or an IAM role.', 'ai-chat-for-amazon-bedrock' ) . '
'; if ( AI_Chat_Bedrock_Core_AI::connectors_available() ) { echo '' . wp_kses_post( sprintf( /* translators: %s: link to the Settings > Connectors screen. */ __( 'A key can also be entered under %s, where it is checked with Amazon Bedrock before it is kept. A key here takes precedence.', 'ai-chat-for-amazon-bedrock' ), '' . esc_html__( 'Settings > Connectors', 'ai-chat-for-amazon-bedrock' ) . '' ) ) . '
'; } if ( '' !== $this->option( 'bedrock_api_key', '' ) ) { echo ''; } } public function aws_access_key_render() { $this->credential_input( 'aws_access_key', false ); } public function aws_secret_key_render() { $this->credential_input( 'aws_secret_key', true ); } public function aws_session_token_render() { $this->credential_input( 'aws_session_token', true ); echo '' . esc_html__( 'Required only for temporary AWS credentials.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function aws_use_role_credentials_render() { $options = get_option( 'ai_chat_bedrock_settings', array() ); $options = is_array( $options ) ? $options : array(); $checked = ! isset( $options['aws_use_role_credentials'] ) || ! empty( $options['aws_use_role_credentials'] ); echo ''; echo '' . esc_html__( 'Supports environment variables, ECS or EKS task roles, and EC2 instance roles, so no long-lived AWS keys are stored in WordPress.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function enable_streaming_render() { $options = get_option( 'ai_chat_bedrock_settings', array() ); $options = is_array( $options ) ? $options : array(); $checked = ! isset( $options['enable_streaming'] ) || 'off' !== $options['enable_streaming']; $supported = AI_Chat_Bedrock_AWS::streaming_supported(); echo ''; if ( $supported ) { echo '' . esc_html__( 'Streaming uses one authenticated POST request per message and falls back to a buffered response automatically.', 'ai-chat-for-amazon-bedrock' ) . '
'; } else { echo '' . esc_html__( 'This server cannot stream because the PHP cURL extension is unavailable. Buffered responses will be used.', 'ai-chat-for-amazon-bedrock' ) . '
'; } } public function governance_section_callback() { echo '' . esc_html__( 'Who may chat and how often, a daily cap, an Amazon Bedrock guardrail, and what is logged. Counters store request and token totals only.', 'ai-chat-for-amazon-bedrock' ) . '
'; $today = AI_Chat_Bedrock_Usage::today_totals(); $week = AI_Chat_Bedrock_Usage::totals( 7 ); echo '' . esc_html( AI_Chat_Bedrock_Translation::sentences( sprintf( /* translators: 1: requests today, 2: input tokens today, 3: output tokens today. */ _n( 'Today: %1$s request, %2$s input tokens, %3$s output tokens.', 'Today: %1$s requests, %2$s input tokens, %3$s output tokens.', (int) $today['requests'], 'ai-chat-for-amazon-bedrock' ), number_format_i18n( (int) $today['requests'] ), number_format_i18n( (int) $today['input_tokens'] ), number_format_i18n( (int) $today['output_tokens'] ) ), sprintf( /* translators: %s: requests in the last seven days. */ _n( 'Last 7 days: %s request.', 'Last 7 days: %s requests.', (int) $week['requests'], 'ai-chat-for-amazon-bedrock' ), number_format_i18n( (int) $week['requests'] ) ) ) ) . '
'; } public function guardrail_id_render() { $this->text_input( 'guardrail_id', '', 200 ); echo '' . esc_html__( 'Optional. Applies an existing Amazon Bedrock guardrail to every request.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function guardrail_version_render() { $this->text_input( 'guardrail_version', 'DRAFT', 20 ); echo '' . esc_html__( 'Use DRAFT or a published numeric version.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function daily_request_limit_render() { $value = absint( $this->option( 'daily_request_limit', 0 ) ); echo ''; echo '' . esc_html__( 'Maximum Bedrock requests per day for the whole site. Use 0 for no plugin-side limit. With a limit, each visitor may ask a tenth of it a day, and at least 20 questions, so one account cannot use up the day for everyone; administrators are not limited. Embeddings for search and indexing are not counted. This is not a billing guarantee; configure AWS Budgets as well.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function knowledge_section_callback() { echo '' . esc_html__( 'Ground answers in your own content. Retrieved passages are passed to the model as reference data, never as instructions, and only published content is used.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function enable_site_context_render() { $checked = ! empty( $this->option( 'enable_site_context', false ) ); echo ''; echo '' . esc_html__( 'Uses the built-in WordPress search. Password-protected, private and draft content is excluded.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function context_results_render() { $value = absint( $this->option( 'context_results', 3 ) ); echo ''; echo '' . esc_html__( 'More passages improve grounding but increase input tokens and cost.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function include_noindex_render() { $checked = ! empty( $this->option( 'include_noindex', false ) ); echo ''; echo '' . esc_html__( 'Off by default. A page kept out of search engines, such as a thank-you page with a download or a campaign landing page, is usually kept out for a reason, so the chat and agents leave it out too. The noindex settings of Yoast SEO, Rank Math and SEOPress are read for each page and post type; All in One SEO for each page. Reading a post aloud is not affected.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function show_sources_render() { $checked = ! empty( $this->option( 'show_sources', false ) ); echo ''; echo '' . esc_html__( 'Up to three links under each answer, to published pages and knowledge base documents with a web address. Pages that only share a single word with the question are not listed.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function knowledge_base_id_render() { $this->text_input( 'knowledge_base_id', '', 64 ); echo '' . esc_html__( 'Optional. Queries an existing Amazon Bedrock knowledge base with the Retrieve API. Requires bedrock:Retrieve permission for that knowledge base.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function rerank_model_render() { $choices = array( '' => __( 'Off', 'ai-chat-for-amazon-bedrock' ) ) + AI_Chat_Bedrock_Retrieval::rerank_models(); $this->select( 'rerank_model_id', $choices, '' ); echo '' . esc_html__( 'Optional. Gathers more passages from site content and the knowledge base, then has a reranking model keep the ones that best answer the question. It adds one rerank request per question, billed per query, and the passages are used in their original order if it fails. Requires bedrock:Rerank, and bedrock:InvokeModel on the reranking model, in the chat region.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function abilities_tools_render() { $checked = ! empty( $this->option( 'abilities_tools', false ) ); $available = AI_Chat_Bedrock_Abilities::available(); echo ''; if ( $available ) { echo '' . esc_html__( 'Abilities follow the same tool policy: a capability is required, permission callbacks are honored, and abilities that change data need explicit approval.', 'ai-chat-for-amazon-bedrock' ) . '
'; $sources = ( new AI_Chat_Bedrock_Abilities() )->source_labels(); $policy = '' . esc_html__( 'MCP > Tool policy', 'ai-chat-for-amazon-bedrock' ) . ''; if ( ! empty( $sources ) ) { echo '' . sprintf( /* translators: 1: names of plugins, 2: link to the tool policy. */ esc_html__( 'Found on this site: %1$s. Choose plugins and abilities under %2$s.', 'ai-chat-for-amazon-bedrock' ), esc_html( AI_Chat_Bedrock_Translation::items( $sources ) ), $policy // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped parts above. ) . '
'; } if ( ! get_option( 'ai_chat_bedrock_enable_mcp', false ) ) { echo '' . esc_html__( 'Tools in chat are switched off on the MCP screen, so no ability is offered until they are switched on there.', 'ai-chat-for-amazon-bedrock' ) . '
'; } } else { echo '' . esc_html__( 'The WordPress Abilities API is not available on this site, so this option is inactive.', 'ai-chat-for-amazon-bedrock' ) . '
'; } } public function site_abilities_render() { $checked = ! empty( $this->option( 'site_abilities', false ) ); $available = AI_Chat_Bedrock_Abilities::available(); echo ''; if ( $available ) { echo '' . esc_html__( 'Registers search, read, SEO suggestion and WooCommerce product lookup as read-only abilities, plus one write ability that creates drafts. Nothing is published, updated or deleted, and orders and customers are never exposed.', 'ai-chat-for-amazon-bedrock' ) . '
'; } else { echo '' . esc_html__( 'Requires the WordPress Abilities API.', 'ai-chat-for-amazon-bedrock' ) . '
'; } } public function site_ontology_render() { $checked = ! empty( $this->option( 'site_ontology', false ) ); echo ''; echo '' . esc_html__( 'Adds a read-only describe-site ability and MCP tool. It lists what the site holds as schema.org types, with counts per language, how they relate and which data an AI may see, using the same IDs as Yoast SEO and WooCommerce. Passages given to the model also say whether they come from a post, page or product, and in which language. Only published, public content is described.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function llms_txt_render() { $checked = ! empty( $this->option( 'llms_txt', false ) ); echo ''; echo '' . esc_html__( 'Lists the site\'s public posts, pages and products by type, in every language, at the address below. It follows the same rules as the site description: no members-only content, drafts or pages hidden from search engines. A real llms.txt file, a page at that address and a plugin that answers before this one go first; a plugin that answers later can claim the address with a filter.', 'ai-chat-for-amazon-bedrock' ) . '
'; if ( class_exists( 'AI_Chat_Bedrock_Llms_Txt' ) ) { if ( AI_Chat_Bedrock_Llms_Txt::file_exists() ) { echo '' . esc_html__( 'An llms.txt file is already at the site root, so the web server serves that file instead.', 'ai-chat-for-amazon-bedrock' ) . '
'; } elseif ( $checked ) { echo '' . esc_html( AI_Chat_Bedrock_Llms_Txt::url() ) . '
'; } } } public function business_metrics_render() { $checked = ! empty( $this->option( 'business_metrics', false ) ); echo ''; echo '' . esc_html__( 'Adds a Business insights screen with periods, comparisons, charts, a CSV download and questions in words, and a read-only query-metrics ability and MCP tool. Figures are read from data the site already keeps, including WooCommerce Analytics. Figures from fewer than five questions or orders are withheld, question figures stay on the screen, and no figure is sent to Amazon Bedrock.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function editor_assistant_render() { $checked = ! empty( $this->option( 'editor_assistant', false ) ); echo ''; echo '' . esc_html__( 'Improve, shorten, expand, summarize, suggest titles or translate. Suggestions are never saved automatically and require the edit_posts capability.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function log_conversations_render() { $checked = AI_Chat_Bedrock_Conversations::enabled(); $retention = AI_Chat_Bedrock_Conversations::retention_days(); echo ''; echo '' . esc_html__( 'Disabled by default. When enabled, chat content is stored in the database, capped at 200 recent entries, and visible to administrators. Disclose this to your visitors.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function chat_memory_render() { $current = AI_Chat_Bedrock_Chat_History::mode( get_option( 'ai_chat_bedrock_settings', array() ) ); $days = AI_Chat_Bedrock_Chat_History::retention_days(); $choices = array( '' => __( 'Off (default): each page starts a new conversation', 'ai-chat-for-amazon-bedrock' ), 'tab' => __( 'Keep it while the visitor browses, in their browser tab', 'ai-chat-for-amazon-bedrock' ), 'account' => __( 'Also save it for signed-in visitors, across visits and devices', 'ai-chat-for-amazon-bedrock' ), ); echo ''; echo '' . esc_html__( 'Kept in the browser tab, the conversation is stored only in the visitor\'s browser and is gone when the tab is closed. Saved for signed-in visitors, it is also stored on this site with their account, included in personal data exports and erasures, and deleted after the days above. Clear in the chat deletes it, and switching saving off deletes every saved conversation. Disclose this to your visitors.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function speech_render() { $options = get_option( 'ai_chat_bedrock_settings', array() ); $engine = AI_Chat_Bedrock_Speech::engine( $options ); $engines = array( 'neural' => __( 'Neural (default)', 'ai-chat-for-amazon-bedrock' ), 'generative' => __( 'Generative: more natural, about twice the price, not every voice or Region', 'ai-chat-for-amazon-bedrock' ), ); echo ''; echo '' . esc_html__( 'Off by default. Amazon Polly reads the text aloud, in a voice for its language, and is billed per character; 0 removes the daily limit.', 'ai-chat-for-amazon-bedrock' ) . '
'; echo '' . esc_html__( 'Only answers this chat gave can be read, by the visitor they were given to. A post is read as a signed-out visitor sees it, so members-only content is never sent; its audio is saved in the uploads folder and made again when the post changes. The AWS identity needs polly:SynthesizeSpeech.', 'ai-chat-for-amazon-bedrock' ) . '
' . esc_html( $visitor > 0 /* translators: %s: number of characters. */ ? sprintf( __( 'Saved audio plays for everyone at no cost. Making new audio is limited to %s characters per visitor a day, so one visitor or script cannot use up the day for the rest, and crawlers and scripts cannot have posts read at all. Administrators are not limited.', 'ai-chat-for-amazon-bedrock' ), number_format_i18n( $visitor ) ) : __( 'Saved audio plays for everyone at no cost. Crawlers and scripts cannot have posts read. Without a daily limit for the site, visitors have none either.', 'ai-chat-for-amazon-bedrock' ) ) . '
'; } public function analytics_events_render() { echo ''; echo '' . esc_html__( 'Off by default. The chat tells the site\'s analytics tag when it is opened, a question is asked or answered, a source or product in an answer is followed, an answer is rated and a contact request is sent, with no message text or contact details.', 'ai-chat-for-amazon-bedrock' ) . '
'; echo '' . esc_html__( 'Google Analytics (through Site Kit, MonsterInsights or a gtag snippet), Google Tag Manager, Matomo and Plausible receive the events; mark ai_chat_contact as a key event to count contact requests as conversions. With a consent plugin that uses the WP Consent API, events wait until the visitor allows statistics.', 'ai-chat-for-amazon-bedrock' ) . '
' . esc_html( sprintf( __( 'Found on this site: %s.', 'ai-chat-for-amazon-bedrock' ), implode( ', ', $tools ) ) ) . '
'; } } public function leads_render() { $options = get_option( 'ai_chat_bedrock_settings', array() ); $options = is_array( $options ) ? $options : array(); $link = isset( $options['leads_link'] ) ? AI_Chat_Bedrock_Leads::clean_link( $options['leads_link'] ) : ''; $joinchat = AI_Chat_Bedrock_Leads::joinchat_url(); echo ''; $found = array(); if ( class_exists( 'Flamingo_Inbound_Message' ) ) { $found[] = __( 'Flamingo is active, so each request is also filed in its inbox.', 'ai-chat-for-amazon-bedrock' ); } if ( AI_Chat_Bedrock_Leads::uses_akismet() ) { $found[] = __( 'Akismet is set up, so each request is checked for spam, as a contact form is.', 'ai-chat-for-amazon-bedrock' ); } if ( '' !== $joinchat ) { $found[] = __( 'Joinchat is active: while the link is empty, its WhatsApp number is offered.', 'ai-chat-for-amazon-bedrock' ); } echo '' . esc_html__( 'Off by default. A Contact a person button appears below the chat, and the assistant points to it when it cannot help.', 'ai-chat-for-amazon-bedrock' ) . '
'; echo '' . esc_html__( 'Visitors give an email address or phone number and must agree before anything is stored. Requests are kept on this site and listed under Contact requests, where they can be exported. The link can be a web, mailto: or tel: address.', 'ai-chat-for-amazon-bedrock' ) . '
' . esc_html__( 'Disclose this in your privacy policy.', 'ai-chat-for-amazon-bedrock' ) . '
'; foreach ( $found as $line ) { echo '' . esc_html( $line ) . '
'; } } public function suggested_questions_render() { $value = (string) $this->option( 'suggested_questions', '' ); echo ''; echo '' . esc_html__( 'One question per line, up to four. They appear as buttons above the input so visitors know what to ask, and disappear once the conversation starts.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function role_limits_render() { $fallback = max( 1, absint( $this->option( 'rate_limit_per_minute', 5 ) ) ); $limits = AI_Chat_Bedrock_Rate_Limits::all(); echo ''; echo '' . esc_html__( 'Optional. Leave a row empty to use the site-wide limit above. A visitor with several roles gets the most permissive of them, and requests are still counted per profile.', 'ai-chat-for-amazon-bedrock' ) . '
'; echo '' . esc_html( AI_Chat_Bedrock_Rate_Limits::describe( $fallback ) ) . '
'; } public function managed_prompt_render() { echo ''; echo '' . esc_html__( 'Optional. Point at a prompt in Amazon Bedrock Prompt Management and its text replaces the system prompt above, so one prompt can be reviewed in AWS and reused by every site. The prompt must live in the same region as the chat. Leave the version empty to follow the draft.', 'ai-chat-for-amazon-bedrock' ) . '
'; if ( ! AI_Chat_Bedrock_Prompts::enabled() ) { return; } $text = AI_Chat_Bedrock_Prompts::text(); if ( is_wp_error( $text ) ) { echo '' . esc_html( $text->get_error_message() ) . '
' . esc_html__( 'While the prompt cannot be read, the system prompt above is used instead.', 'ai-chat-for-amazon-bedrock' ) . '
'; return; } echo '' . esc_html__( 'Prompt in use:', 'ai-chat-for-amazon-bedrock' ) . '
'; echo '' . esc_html( $text ) . ''; $unresolved = AI_Chat_Bedrock_Prompts::unresolved( $text ); if ( ! empty( $unresolved ) ) { echo '
'; printf( /* translators: %s: comma separated list of variable names. */ esc_html__( 'These variables are sent literally because this plugin cannot resolve them: %s', 'ai-chat-for-amazon-bedrock' ), esc_html( AI_Chat_Bedrock_Translation::items( $unresolved ) ) ); echo '
'; } } public function embedding_model_render() { $choices = array( '' => __( 'Off, use keyword search only', 'ai-chat-for-amazon-bedrock' ) ) + AI_Chat_Bedrock_Embeddings::models(); $this->select( 'embedding_model_id', $choices, '' ); echo '' . esc_html__( 'Finds content by meaning instead of shared words. It adds one embedding request per question, plus one per passage while indexing. Keyword search still runs when nothing relevant is found. Only what a signed-out visitor can read is indexed.', 'ai-chat-for-amazon-bedrock' ) . '
'; // Always printed: a checkbox that is missing from the form is saved as unchecked. $background = ! empty( $this->option( 'embedding_background', false ) ); echo ''; if ( ! AI_Chat_Bedrock_Embeddings::enabled() ) { return; } $status = AI_Chat_Bedrock_Embeddings::status(); echo ''; echo esc_html( AI_Chat_Bedrock_Translation::sentences( sprintf( /* translators: 1: indexed item count, 2: total published item count. */ _n( 'Indexed %1$s of %2$s published item.', 'Indexed %1$s of %2$s published items.', (int) $status['total'], 'ai-chat-for-amazon-bedrock' ), number_format_i18n( (int) $status['indexed'] ), number_format_i18n( (int) $status['total'] ) ), empty( $status['delete_pending'] ) ? '' : sprintf( /* translators: %s: number of vectors waiting to be deleted. */ _n( '%s removed passage is still waiting to be deleted from S3 Vectors.', '%s removed passages are still waiting to be deleted from S3 Vectors.', (int) $status['delete_pending'], 'ai-chat-for-amazon-bedrock' ), number_format_i18n( (int) $status['delete_pending'] ) ) ) ); echo '
'; if ( isset( $status['searchable'] ) && (int) $status['total'] > (int) $status['searchable'] ) { echo '' . esc_html( sprintf( /* translators: %s: number of items compared per question. */ __( 'With vectors stored in the WordPress database, each question is compared with the %s most recent items only. Choose Amazon S3 Vectors below to search everything.', 'ai-chat-for-amazon-bedrock' ), number_format_i18n( (int) $status['searchable'] ) ) ) . '
'; if ( $background ) { $next = wp_next_scheduled( AI_Chat_Bedrock_Embeddings::CRON_HOOK ); echo '
'; if ( $next ) { printf( /* translators: %s: human readable time until the next scheduled run. */ esc_html__( 'Next background batch in %s. On a large site, run wp ai-chat-bedrock index once to finish faster.', 'ai-chat-for-amazon-bedrock' ), esc_html( human_time_diff( time(), (int) $next ) ) ); } else { esc_html_e( 'The background run will be scheduled on the next page load.', 'ai-chat-for-amazon-bedrock' ); } echo '
'; } echo '' . esc_html__( 'Indexing runs in small batches. Editing a post marks it for re-indexing on the next run.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function vector_store_render() { $store = AI_Chat_Bedrock_Embeddings::store( get_option( 'ai_chat_bedrock_settings', array() ) ); echo ''; echo '' . esc_html__( 'Create the vector bucket in the Amazon S3 console, then check or create the index here. Several sites can share one index: each only reads and deletes its own vectors. Needs s3vectors:PutVectors, QueryVectors, GetVectors, DeleteVectors, ListVectors and GetIndex, plus CreateIndex to create it from here.', 'ai-chat-for-amazon-bedrock' ) . '
'; if ( 's3_vectors' === $store && AI_Chat_Bedrock_S3_Vectors::enabled() ) { echo ''; } } public function integrations_section_callback() { echo '
' . esc_html__( 'Optional adjustments to plugins this site runs. Each is off until you turn it on, and does nothing while the plugin it adjusts is inactive.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function github_read_scope_render() { $this->integration_checkbox( 'github_read_scope', 'fluentauth', __( 'Ask GitHub for read-only access when visitors sign in with GitHub', 'ai-chat-for-amazon-bedrock' ), __( 'FluentAuth requests the "user" scope, which also lets the site change the visitor\'s GitHub profile. Signing in only needs read:user and user:email.', 'ai-chat-for-amazon-bedrock' ), __( 'Requires FluentAuth.', 'ai-chat-for-amazon-bedrock' ) ); } public function social_only_registration_render() { $this->integration_checkbox( 'social_only_registration', 'fluentauth', __( 'Sign up through social login only', 'ai-chat-for-amazon-bedrock' ), __( 'Social sign-up needs "Anyone can register", which also opens the WordPress registration form. That form emails a password link and attracts spam accounts. This sends it to the login page, where the social buttons are, and hides the Register link.', 'ai-chat-for-amazon-bedrock' ), __( 'Requires FluentAuth.', 'ai-chat-for-amazon-bedrock' ) ); } public function hreflang_x_default_render() { $detected = AI_Chat_Bedrock_Integrations::detected(); $languages = AI_Chat_Bedrock_Integrations::languages(); $current = (string) $this->option( 'hreflang_x_default', '' ); if ( '' !== $current && ! isset( $languages[ $current ] ) ) { $languages[ $current ] = $current; } echo ''; if ( ! $detected['polylang'] && '' !== $current ) { echo ''; } echo '' . esc_html( $detected['polylang'] ? __( 'Polylang lists each translation of a page for search engines but no x-default, so they guess which one to show a reader whose language the site does not have. The edition chosen here is named as the default.', 'ai-chat-for-amazon-bedrock' ) : __( 'Requires Polylang.', 'ai-chat-for-amazon-bedrock' ) ) . '
'; } public function organization_author_render() { $this->integration_checkbox( 'organization_author', 'yoast', __( 'Credit articles to the organization', 'ai-chat-for-amazon-bedrock' ), __( 'Yoast SEO names the account that published a post as its author. When Yoast is set to represent an organization, this names the organization instead, in structured data, the author meta tag and Slack previews.', 'ai-chat-for-amazon-bedrock' ), __( 'Requires Yoast SEO.', 'ai-chat-for-amazon-bedrock' ) ); } public function woocommerce_section_callback() { echo '' . esc_html__( 'Answers about products and orders from the store itself, and help writing product pages. Each is off until you turn it on.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function woo_catalog_render() { $checked = ! empty( $this->option( 'woo_catalog', false ) ); echo ''; echo '' . esc_html__( 'Matching products are looked up for each question, by name, description, SKU or category, and their current price, stock, rating and attributes are given to the model, so it does not quote an old price. The products are shown as cards under the answer, with a link to add simple products to the cart. On a product page, that product comes first. Products that are not published, are hidden from the shop or search, or are out of stock on a store that hides those, are never used.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function woo_catalog_limit_render() { $value = AI_Chat_Bedrock_WooCommerce::limit( array( 'woo_catalog_limit' => $this->option( 'woo_catalog_limit', AI_Chat_Bedrock_WooCommerce::DEFAULT_PRODUCTS ) ) ); echo ''; echo '' . esc_html__( 'More products give the model more to compare but increase input tokens and cost.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function woo_orders_render() { $checked = ! empty( $this->option( 'woo_orders', false ) ); echo ''; echo '' . esc_html__( 'Only when a question is about orders, shipping or returns, the customer\'s five latest orders are given to the model: number, dates, status, items, total, shipping method and tracking number. Addresses, email, phone and payment details are never sent, and nobody can see another customer\'s orders. This sends customer data to Amazon Bedrock, so add the suggested text under Settings, Privacy, Policy Guide to your privacy policy first.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function woo_product_assistant_render() { $checked = ! empty( $this->option( 'woo_product_assistant', false ) ); echo ''; echo '' . esc_html__( 'Adds a box to the product edit screen that writes the short and the full description from the product\'s own attributes, categories and text, without inventing specifications, and sums up what reviews praise and criticize, without reviewer names. Drafts go into the editor for you to review and are saved only when you update the product.', 'ai-chat-for-amazon-bedrock' ) . '
'; } private function integration_checkbox( $key, $plugin, $label, $description, $missing ) { $detected = AI_Chat_Bedrock_Integrations::detected(); $active = ! empty( $detected[ $plugin ] ); $checked = ! empty( $this->option( $key, false ) ); echo ''; echo '' . esc_html( $active ? $description : $missing ) . '
'; if ( ! $active && $checked ) { // A disabled checkbox is not submitted, so keep the choice until the plugin is back. echo ''; } } public function media_assistant_render() { $checked = ! empty( $this->option( 'media_assistant', false ) ); echo ''; echo '' . esc_html__( 'Adds a Generate alt text action to the media library and an excerpt helper for posts. Alt text needs a model that accepts images, such as Claude. Existing alt text is never replaced unless you ask.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function model_id_render() { $options = AI_Chat_Bedrock_Models::options(); $this->select( 'model_id', $options, AI_Chat_Bedrock_Models::DEFAULT_MODEL ); echo '' . esc_html__( 'The list is discovered from your AWS account and region. Many current models require a cross-region inference profile ID.', 'ai-chat-for-amazon-bedrock' ) . '
'; echo ''; } public function fallback_model_render() { $options = array( '' => __( 'No fallback', 'ai-chat-for-amazon-bedrock' ) ) + AI_Chat_Bedrock_Models::options(); $this->select( 'fallback_model_id', $options, '' ); echo '
' . esc_html__( 'Used only when the main model cannot answer because access was denied, the request was throttled, or Amazon Bedrock was unreachable. The reply states which model answered. Requests rejected for other reasons are never retried.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function image_model_render() { $choices = array( '' => __( 'Off', 'ai-chat-for-amazon-bedrock' ) ) + AI_Chat_Bedrock_Images::models(); $this->select( 'image_model_id', $choices, '' ); echo '' . esc_html__( 'Lets plugins that use the WordPress AI Client generate images with Stability AI, and adds Remove background and Upscale to images in the Media Library when the media helpers are on. Results are saved as new images; originals are never changed. Each image is one paid request and counts toward the daily limit.', 'ai-chat-for-amazon-bedrock' ) . '
'; echo '' . esc_html( sprintf( /* translators: %s: AWS region code, such as us-west-2. */ __( 'The Stability models run in %s, whatever region the chat uses, so prompts and images are processed there. The guardrail, if set, checks each prompt first in your own region.', 'ai-chat-for-amazon-bedrock' ), AI_Chat_Bedrock_Images::region() ) ) . '
'; } public function max_tokens_render() { $value = $this->option( 'max_tokens', 1000 ); echo ''; } public function temperature_render() { $value = $this->option( 'temperature', 0.7 ); echo ''; echo '' . esc_html__( 'Not sent to Claude Opus 4.7, Sonnet 5, Opus 5 and newer Claude models, because Amazon Bedrock rejects it for them.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function system_prompt_render() { echo ''; } public function chat_title_render() { $this->text_input( 'chat_title', 'Chat with AI', 120 ); } public function welcome_message_render() { $this->text_input( 'welcome_message', 'Hello! How can I help you today?', 500, 'large-text' ); } public function allow_public_chat_render() { $checked = ! empty( $this->option( 'allow_public_chat', false ) ); echo ''; echo '' . esc_html__( 'Disabled by default. Enabling this can incur AWS charges even with rate limiting.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function rate_limit_render() { $value = $this->option( 'rate_limit_per_minute', 5 ); echo ''; } public function popup_site_wide_render() { $checked = ! empty( $this->option( 'popup_site_wide', false ) ); echo ''; echo '' . esc_html__( 'Pages that already contain the chat block or shortcode are left unchanged. Use a profile with guest access if visitors should be able to chat.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function chat_color_scheme_render() { $current = AI_Chat_Bedrock_Chat_Request::color_scheme( $this->option( 'chat_color_scheme', 'light' ) ); $choices = array( 'light' => __( 'Light (default)', 'ai-chat-for-amazon-bedrock' ), 'dark' => __( 'Dark', 'ai-chat-for-amazon-bedrock' ), 'auto' => __( 'Follow the visitor\'s device', 'ai-chat-for-amazon-bedrock' ), ); echo ''; echo '' . esc_html__( 'Choose what matches your theme. Following the device turns the chat dark for visitors in dark mode, even on a light theme.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function chat_accent_color_render() { echo ''; echo '' . esc_html__( 'The colour of the chat button, your own messages and Send, such as your theme\'s main colour. Leave it empty for the default blue. Text on it is white or black, whichever is easier to read.', 'ai-chat-for-amazon-bedrock' ) . '
'; } public function debug_mode_render() { $checked = 'on' === $this->option( 'debug_mode', 'off' ); echo ''; } /** * Register a settings notice when the environment can show one. * * WordPress only defines add_settings_error() inside wp-admin. The validator is also run * by the * configuration import, so calling it directly made the validator admin-only. * * @param string $code Notice code. * @param string $message Notice text. * @param string $type Notice type. */ private function notice( $code, $message, $type = 'error' ) { if ( function_exists( 'add_settings_error' ) ) { add_settings_error( 'ai_chat_bedrock_settings', $code, $message, $type ); } } public function validate_settings( $input ) { $input = is_array( $input ) ? $input : array(); $current = get_option( 'ai_chat_bedrock_settings', array() ); $current = is_array( $current ) ? $current : array(); $output = array(); // A tab only submits its own fields, so remember which keys it owns. $submitted = isset( $input['_aicfab_fields'] ) ? (array) $input['_aicfab_fields'] : array(); $submitted = array_filter( array_map( 'sanitize_key', $submitted ) ); unset( $input['_aicfab_fields'] ); $regions = AI_Chat_Bedrock_Models::regions(); $region = isset( $input['aws_region'] ) ? sanitize_key( $input['aws_region'] ) : 'us-east-1'; $model = isset( $input['model_id'] ) ? sanitize_text_field( $input['model_id'] ) : AI_Chat_Bedrock_Models::DEFAULT_MODEL; $output['aws_region'] = isset( $regions[ $region ] ) ? $region : 'us-east-1'; $output['model_id'] = AI_Chat_Bedrock_Models::is_valid_id( $model ) ? $model : AI_Chat_Bedrock_Models::DEFAULT_MODEL; $fallback = isset( $input['fallback_model_id'] ) ? sanitize_text_field( $input['fallback_model_id'] ) : ''; if ( '' !== $fallback && ( ! AI_Chat_Bedrock_Models::is_valid_id( $fallback ) || $fallback === $output['model_id'] ) ) { $this->notice( 'fallback_model_id', __( 'The fallback model must be a valid model that differs from the main model; it was cleared.', 'ai-chat-for-amazon-bedrock' ) ); $fallback = ''; } $output['fallback_model_id'] = $fallback; $image = isset( $input['image_model_id'] ) ? sanitize_text_field( $input['image_model_id'] ) : ''; if ( '' !== $image && ! isset( AI_Chat_Bedrock_Images::models()[ $image ] ) ) { $this->notice( 'image_model_id', __( 'That image model is not supported; image generation was left off.', 'ai-chat-for-amazon-bedrock' ) ); $image = ''; } $output['image_model_id'] = $image; if ( ! AI_Chat_Bedrock_Models::is_valid_id( $model ) ) { $this->notice( 'model_id', __( 'The submitted model ID was not valid; the default model was kept.', 'ai-chat-for-amazon-bedrock' ) ); } if ( isset( $current['aws_region'] ) && $current['aws_region'] !== $output['aws_region'] ) { AI_Chat_Bedrock_Models::flush_cache(); } foreach ( array( 'aws_access_key', 'aws_secret_key', 'aws_session_token' ) as $key ) { $new_value = isset( $input[ $key ] ) ? trim( sanitize_text_field( $input[ $key ] ) ) : ''; if ( '' === $new_value ) { $output[ $key ] = isset( $current[ $key ] ) ? $current[ $key ] : ''; continue; } $encrypted = AI_Chat_Bedrock_Security::encrypt_secret( $new_value ); if ( '' === $encrypted ) { $this->notice( 'credential_encryption', __( 'The credential could not be encrypted; the existing value was preserved.', 'ai-chat-for-amazon-bedrock' ) ); $output[ $key ] = isset( $current[ $key ] ) ? $current[ $key ] : ''; } else { $output[ $key ] = $encrypted; } } // An API key is a bearer token, so a malformed paste is refused rather than stored. $raw_api_key = isset( $input['bedrock_api_key'] ) && is_string( $input['bedrock_api_key'] ) ? trim( $input['bedrock_api_key'] ) : ''; $new_api_key = AI_Chat_Bedrock_AWS_Credentials::clean_api_key( $raw_api_key ); if ( ! empty( $input['bedrock_api_key_clear'] ) ) { $output['bedrock_api_key'] = ''; } elseif ( '' !== $new_api_key ) { $encrypted = AI_Chat_Bedrock_Security::encrypt_secret( $new_api_key ); if ( '' === $encrypted ) { $this->notice( 'credential_encryption', __( 'The credential could not be encrypted; the existing value was preserved.', 'ai-chat-for-amazon-bedrock' ) ); $output['bedrock_api_key'] = isset( $current['bedrock_api_key'] ) ? $current['bedrock_api_key'] : ''; } else { $output['bedrock_api_key'] = $encrypted; } } else { if ( '' !== $raw_api_key ) { $this->notice( 'bedrock_api_key', __( 'That does not look like an Amazon Bedrock API key, so it was not saved.', 'ai-chat-for-amazon-bedrock' ) ); } $output['bedrock_api_key'] = isset( $current['bedrock_api_key'] ) ? $current['bedrock_api_key'] : ''; } unset( $output['bedrock_api_key_clear'] ); $output['max_tokens'] = max( 100, min( 4000, isset( $input['max_tokens'] ) ? absint( $input['max_tokens'] ) : 1000 ) ); $output['temperature'] = max( 0, min( 1, isset( $input['temperature'] ) ? (float) $input['temperature'] : 0.7 ) ); $output['system_prompt'] = isset( $input['system_prompt'] ) ? AI_Chat_Bedrock_Security::string_substr( sanitize_textarea_field( $input['system_prompt'] ), 0, 8000 ) : ''; $output['chat_title'] = isset( $input['chat_title'] ) ? sanitize_text_field( $input['chat_title'] ) : 'Chat with AI'; $output['welcome_message'] = isset( $input['welcome_message'] ) ? sanitize_text_field( $input['welcome_message'] ) : 'Hello! How can I help you today?'; $output['suggested_questions'] = isset( $input['suggested_questions'] ) ? AI_Chat_Bedrock_Chat_Request::sanitize_suggestions( $input['suggested_questions'] ) : ''; $output['allow_public_chat'] = ! empty( $input['allow_public_chat'] ); $output['rate_limit_per_minute'] = max( 1, min( 60, isset( $input['rate_limit_per_minute'] ) ? absint( $input['rate_limit_per_minute'] ) : 5 ) ); if ( in_array( 'role_limits', $submitted, true ) || isset( $input['role_limits'] ) ) { // Stored separately from the settings blob so role changes cannot bloat it. AI_Chat_Bedrock_Rate_Limits::save( isset( $input['role_limits'] ) ? (array) $input['role_limits'] : array() ); } unset( $output['role_limits'] ); $output['aws_use_role_credentials'] = ! empty( $input['aws_use_role_credentials'] ); $output['enable_streaming'] = empty( $input['enable_streaming'] ) ? 'off' : 'on'; $output['debug_mode'] = ! empty( $input['debug_mode'] ) ? 'on' : 'off'; $output['popup_site_wide'] = ! empty( $input['popup_site_wide'] ); $output['popup_profile'] = isset( $input['popup_profile'] ) ? AI_Chat_Bedrock_Profiles::sanitize_key( $input['popup_profile'] ) : ''; $output['chat_color_scheme'] = AI_Chat_Bedrock_Chat_Request::color_scheme( isset( $input['chat_color_scheme'] ) ? $input['chat_color_scheme'] : '' ); $output['chat_accent_color'] = AI_Chat_Bedrock_Chat_Request::accent_color( isset( $input['chat_accent_color'] ) ? $input['chat_accent_color'] : '' ); $guardrail_id = isset( $input['guardrail_id'] ) ? trim( sanitize_text_field( $input['guardrail_id'] ) ) : ''; $output['guardrail_id'] = preg_match( '#^[A-Za-z0-9._:/-]{0,200}$#', $guardrail_id ) ? $guardrail_id : ''; if ( '' !== $guardrail_id && '' === $output['guardrail_id'] ) { $this->notice( 'guardrail_id', __( 'The guardrail identifier contained unsupported characters and was cleared.', 'ai-chat-for-amazon-bedrock' ) ); } $guardrail_version = isset( $input['guardrail_version'] ) ? strtoupper( trim( sanitize_text_field( $input['guardrail_version'] ) ) ) : 'DRAFT'; $output['guardrail_version'] = preg_match( '/^(?:DRAFT|[0-9]{1,10})$/', $guardrail_version ) ? $guardrail_version : 'DRAFT'; $output['daily_request_limit'] = min( 100000, isset( $input['daily_request_limit'] ) ? absint( $input['daily_request_limit'] ) : 0 ); $output['enable_site_context'] = ! empty( $input['enable_site_context'] ); $output['context_results'] = max( 1, min( 8, isset( $input['context_results'] ) ? absint( $input['context_results'] ) : 3 ) ); $output['show_sources'] = ! empty( $input['show_sources'] ); $output['include_noindex'] = ! empty( $input['include_noindex'] ); $output['abilities_tools'] = ! empty( $input['abilities_tools'] ); $output['site_abilities'] = ! empty( $input['site_abilities'] ); $output['site_ontology'] = ! empty( $input['site_ontology'] ); $output['llms_txt'] = ! empty( $input['llms_txt'] ); $output['business_metrics'] = ! empty( $input['business_metrics'] ); $output['editor_assistant'] = ! empty( $input['editor_assistant'] ); $output['log_conversations'] = ! empty( $input['log_conversations'] ); $output['media_assistant'] = ! empty( $input['media_assistant'] ); $embedding = isset( $input['embedding_model_id'] ) ? sanitize_text_field( $input['embedding_model_id'] ) : ''; $known = AI_Chat_Bedrock_Embeddings::models(); if ( '' !== $embedding && ! isset( $known[ $embedding ] ) ) { $this->notice( 'embedding_model_id', __( 'That embedding model is not supported; semantic search was left off.', 'ai-chat-for-amazon-bedrock' ) ); $embedding = ''; } $output['embedding_model_id'] = $embedding; $output['embedding_background'] = ! empty( $input['embedding_background'] ); $store = isset( $input['vector_store'] ) ? sanitize_key( $input['vector_store'] ) : 'post_meta'; $output['vector_store'] = 's3_vectors' === $store ? 's3_vectors' : 'post_meta'; foreach ( array( 's3_vectors_bucket', 's3_vectors_index' ) as $key ) { $name = isset( $input[ $key ] ) ? strtolower( trim( sanitize_text_field( $input[ $key ] ) ) ) : ''; $output[ $key ] = '' === $name || AI_Chat_Bedrock_S3_Vectors::valid_name( $name ) ? $name : ''; if ( '' !== $name && '' === $output[ $key ] ) { $this->notice( $key, __( 'S3 Vectors bucket and index names are 3 to 63 lowercase letters, digits, hyphens or dots; the value was cleared.', 'ai-chat-for-amazon-bedrock' ) ); } } $vector_region = isset( $input['s3_vectors_region'] ) ? sanitize_key( $input['s3_vectors_region'] ) : ''; $output['s3_vectors_region'] = isset( $regions[ $vector_region ] ) ? $vector_region : ''; if ( 's3_vectors' === $output['vector_store'] && ( '' === $output['s3_vectors_bucket'] || '' === $output['s3_vectors_index'] ) ) { $this->notice( 'vector_store', __( 'Amazon S3 Vectors needs a bucket and an index name. Until both are set, semantic search is off.', 'ai-chat-for-amazon-bedrock' ), 'warning' ); } $output['github_read_scope'] = ! empty( $input['github_read_scope'] ); $output['social_only_registration'] = ! empty( $input['social_only_registration'] ); $output['organization_author'] = ! empty( $input['organization_author'] ); $output = $this->validate_publishing( $input, $output, $current ); $output['woo_catalog'] = ! empty( $input['woo_catalog'] ); $output['woo_catalog_limit'] = AI_Chat_Bedrock_WooCommerce::limit( $input ); $output['woo_orders'] = ! empty( $input['woo_orders'] ); $output['woo_product_assistant'] = ! empty( $input['woo_product_assistant'] ); $x_default = isset( $input['hreflang_x_default'] ) ? sanitize_key( $input['hreflang_x_default'] ) : ''; $languages = AI_Chat_Bedrock_Integrations::languages(); $output['hreflang_x_default'] = '' === $x_default || empty( $languages ) || isset( $languages[ $x_default ] ) ? $x_default : ''; $prompt_id = isset( $input['prompt_id'] ) ? trim( sanitize_text_field( $input['prompt_id'] ) ) : ''; if ( '' !== $prompt_id && ! preg_match( '#^[A-Za-z0-9:._/-]{1,2048}$#', $prompt_id ) ) { $this->notice( 'prompt_id', __( 'The managed prompt identifier contained unsupported characters and was cleared.', 'ai-chat-for-amazon-bedrock' ) ); $prompt_id = ''; } $output['prompt_id'] = $prompt_id; $prompt_version = isset( $input['prompt_version'] ) ? strtoupper( trim( sanitize_text_field( $input['prompt_version'] ) ) ) : ''; $output['prompt_version'] = preg_match( '/^(?:DRAFT|[0-9]{1,10})$/', $prompt_version ) ? $prompt_version : ''; if ( class_exists( 'AI_Chat_Bedrock_Prompts' ) ) { // The cache key includes the identifier and version, so clear both the old and new entries. AI_Chat_Bedrock_Prompts::flush( $current ); AI_Chat_Bedrock_Prompts::flush( $output ); } $retention_days = isset( $input['log_retention_days'] ) ? absint( $input['log_retention_days'] ) : AI_Chat_Bedrock_Conversations::DEFAULT_DAYS; $output['log_retention_days'] = max( 1, min( AI_Chat_Bedrock_Conversations::MAX_DAYS, $retention_days ) ); $output['chat_memory'] = AI_Chat_Bedrock_Chat_History::mode( $input ); $output['chat_memory_days'] = AI_Chat_Bedrock_Chat_History::retention_days( $input ); $output['speech_replies'] = ! empty( $input['speech_replies'] ); $output['speech_posts'] = ! empty( $input['speech_posts'] ); $output['speech_posts_signed_in'] = ! empty( $input['speech_posts_signed_in'] ); $output['speech_engine'] = AI_Chat_Bedrock_Speech::engine( $input ); $output['speech_daily_chars'] = AI_Chat_Bedrock_Speech::daily_characters( $input ); $output['analytics_events'] = ! empty( $input['analytics_events'] ); $output = $this->validate_wechat( $input, $output, $current ); $output['leads_enabled'] = ! empty( $input['leads_enabled'] ); $output['leads_notify'] = ! empty( $input['leads_notify'] ); $output['leads_days'] = AI_Chat_Bedrock_Leads::retention_days( array( 'leads_days' => isset( $input['leads_days'] ) ? $input['leads_days'] : 0 ) ); $output['leads_link'] = isset( $input['leads_link'] ) ? AI_Chat_Bedrock_Leads::clean_link( $input['leads_link'] ) : ''; if ( isset( $input['leads_link'] ) && is_scalar( $input['leads_link'] ) && '' !== trim( (string) $input['leads_link'] ) && '' === $output['leads_link'] ) { $this->notice( 'leads_link', __( 'The contact link must be a web, mailto: or tel: address; it was cleared.', 'ai-chat-for-amazon-bedrock' ) ); } $knowledge_base = isset( $input['knowledge_base_id'] ) ? trim( sanitize_text_field( $input['knowledge_base_id'] ) ) : ''; if ( '' === $knowledge_base || preg_match( '/^[A-Za-z0-9]{1,64}$/', $knowledge_base ) ) { $output['knowledge_base_id'] = $knowledge_base; } else { $output['knowledge_base_id'] = ''; $this->notice( 'knowledge_base_id', __( 'The knowledge base ID must be alphanumeric; the value was cleared.', 'ai-chat-for-amazon-bedrock' ) ); } $rerank = isset( $input['rerank_model_id'] ) ? sanitize_text_field( $input['rerank_model_id'] ) : ''; if ( '' !== $rerank && ! isset( AI_Chat_Bedrock_Retrieval::rerank_models()[ $rerank ] ) ) { $this->notice( 'rerank_model_id', __( 'That reranking model is not supported; reranking was left off.', 'ai-chat-for-amazon-bedrock' ) ); $rerank = ''; } $output['rerank_model_id'] = $rerank; // A saved model, Region or credential change may be what fixes a refused rerank request. delete_transient( AI_Chat_Bedrock_Retrieval::RERANK_PAUSED ); if ( class_exists( 'AI_Chat_Bedrock_AWS_Credentials' ) ) { AI_Chat_Bedrock_AWS_Credentials::flush_cache(); } /* * Preserve settings owned by tabs that were not part of this submission. * * If the field list is missing, for example because a cached form was submitted, * fall back to the keys that were actually posted. Losing unrelated settings is * worse than ignoring a cleared checkbox in that rare case. */ if ( empty( $submitted ) && ! empty( $current ) ) { $submitted = array_filter( array_map( 'sanitize_key', array_keys( $input ) ) ); } if ( ! empty( $submitted ) ) { $merged = $current; foreach ( $submitted as $key ) { if ( array_key_exists( $key, $output ) ) { $merged[ $key ] = $output[ $key ]; } } // Keys a field renders next to its own control, which the field list does not name. foreach ( self::COMPANION_FIELDS as $owner => $companions ) { if ( ! in_array( $owner, $submitted, true ) ) { continue; } foreach ( $companions as $companion ) { if ( array_key_exists( $companion, $output ) ) { $merged[ $companion ] = $output[ $companion ]; } } } $output = $merged; } /* * Some settings are also kept in options of their own, which other code reads without * loading the settings array. They are written from the merged result, so saving one * tab no longer switched off the conversation log and site abilities set on another. */ update_option( AI_Chat_Bedrock_Conversations::OPTION_ENABLED, ! empty( $output['log_conversations'] ), false ); update_option( AI_Chat_Bedrock_Conversations::OPTION_RETENTION, isset( $output['log_retention_days'] ) ? (int) $output['log_retention_days'] : AI_Chat_Bedrock_Conversations::DEFAULT_DAYS, false ); update_option( 'ai_chat_bedrock_site_abilities', ! empty( $output['site_abilities'] ), false ); // WordPress registers its own "Settings saved" against the 'general' slug, which a // settings_errors() call filtered to this plugin's slug never shows. Without this the // page came back silently and there was no way to tell whether the save worked. $this->notice( 'aicfab_settings_saved', __( 'Settings saved.', 'ai-chat-for-amazon-bedrock' ), 'success' ); return $output; } /** * The one-off notice flags the plugin's redirects add. WordPress takes them out of the * address bar once the page has loaded, as it does its own, so reloading or bookmarking * the page does not show the notice again. * * @param array $args Query arguments WordPress removes. * @return array */ public function removable_query_args( $args ) { $args = is_array( $args ) ? $args : array(); return array_merge( $args, array( 'aicfab-alt', 'aicfab-alt-done', 'aicfab-alt-skipped', 'aicfab-alt-failed', 'aicfab-applied', 'aicfab-cleared', 'aicfab-generated', 'aicfab-image', 'aicfab-log', 'aicfab-mcp', 'aicfab-message', 'aicfab-profile', 'aicfab-skipped', 'aicfab-transfer' ) ); } /** * The handle of the admin script and stylesheet. The chat's own assets use the bare plugin name, * and the Test Chat screen needs both: sharing the handle made WordPress keep whichever was * registered first, the admin one, so the chat there had neither its script nor its styles. */ private function admin_handle() { return $this->plugin_name . '-admin'; } private function is_plugin_screen( $hook_suffix ) { return false !== strpos( (string) $hook_suffix, $this->plugin_name ); } /** * Settings rendered inside another field's row, saved whenever that field's tab is. */ const COMPANION_FIELDS = array( 'log_conversations' => array( 'log_retention_days' ), 'chat_memory' => array( 'chat_memory_days' ), 'speech_replies' => array( 'speech_posts', 'speech_posts_signed_in', 'speech_engine', 'speech_daily_chars' ), 'leads_enabled' => array( 'leads_notify', 'leads_days', 'leads_link' ), 'wechat_enabled' => array( 'wechat_token', 'wechat_aes_key', 'wechat_app_id', 'wechat_model_id', 'wechat_hourly', 'wechat_menu' ), 'wechat_drafts_enabled' => array( 'wechat_app_secret', 'wechat_drafts_category', 'wechat_drafts_schedule', 'wechat_drafts_count', 'wechat_drafts_author', 'wechat_drafts_notify', 'wechat_drafts_sync' ), 'wxgame_enabled' => array( 'wxgame_app_id', 'wxgame_token', 'wxgame_aes_key', 'wxgame_app_secret', 'wxgame_welcome', 'wxgame_answers', 'wxgame_fallback' ), 'distribution_enabled' => array( 'distribution_links' ), 'youtube_client_id' => array( 'youtube_client_secret', 'youtube_daily_uploads' ), 'popup_site_wide' => array( 'popup_profile' ), 'prompt_id' => array( 'prompt_version' ), 'embedding_model_id' => array( 'embedding_background' ), 'vector_store' => array( 's3_vectors_bucket', 's3_vectors_index', 's3_vectors_region' ), ); const CHECKBOX_FIELDS = array( 'embedding_background', 'abilities_tools', 'allow_public_chat', 'aws_use_role_credentials', 'debug_mode', 'editor_assistant', 'enable_site_context', 'enable_streaming', 'log_conversations', 'media_assistant', 'popup_site_wide', 'site_abilities', 'site_ontology', 'llms_txt', 'business_metrics', 'show_sources', 'include_noindex', 'speech_replies', 'speech_posts', 'leads_enabled', 'leads_notify', 'analytics_events', 'wechat_enabled', 'wxgame_enabled', 'wechat_drafts_enabled', 'wechat_drafts_notify', 'publish_kit', 'wechat_drafts_sync', 'github_read_scope', 'social_only_registration', 'organization_author', 'distribution_enabled', 'bilibili_embeds', 'woo_catalog', 'woo_orders', 'woo_product_assistant', ); /** * DOM id used to tie a settings row title to its control. * * @param string $key Setting key. * @return string */ public static function control_id( $key ) { return 'aicfab_field_' . sanitize_key( (string) $key ); } private function field( $id, $title, $callback, $section ) { $pages = array( 'aicfab_aws' => 'aicfab_tab_aws', 'aicfab_model' => 'aicfab_tab_model', 'aicfab_governance' => 'aicfab_tab_governance', 'aicfab_knowledge' => 'aicfab_tab_knowledge', 'aicfab_chat' => 'aicfab_tab_chat', 'aicfab_integrations' => 'aicfab_tab_integrations', 'aicfab_publishing' => 'aicfab_tab_publishing', 'aicfab_wechat' => 'aicfab_tab_wechat', 'aicfab_wxgame' => 'aicfab_tab_wxgame', 'aicfab_agents' => 'aicfab_tab_agents', 'aicfab_woocommerce' => 'aicfab_tab_woocommerce', ); $page = isset( $pages[ $section ] ) ? $pages[ $section ] : 'aicfab_tab_chat'; /* * Associate the row title with the control so screen readers announce a name. * Checkbox fields already carry their own inline label and would be read twice. */ $args = in_array( $id, self::CHECKBOX_FIELDS, true ) ? array() : array( 'label_for' => self::control_id( $id ) ); add_settings_field( $id, $title, array( $this, $callback ), $page, $section, $args ); self::$tab_fields[ $page ][] = $id; } /** * Settings tabs and the fields each one owns. * * @return array */ public static function tabs() { // In the order a site is set up: connect, choose the model, ground and shape the chat, // add tools and channels, then limit what it may spend. return array( 'aws' => array( 'page' => 'aicfab_tab_aws', 'label' => __( 'AWS', 'ai-chat-for-amazon-bedrock' ), ), 'model' => array( 'page' => 'aicfab_tab_model', 'label' => __( 'Model', 'ai-chat-for-amazon-bedrock' ), ), 'knowledge' => array( 'page' => 'aicfab_tab_knowledge', 'label' => __( 'Grounding', 'ai-chat-for-amazon-bedrock' ), ), 'chat' => array( 'page' => 'aicfab_tab_chat', 'label' => __( 'Chat', 'ai-chat-for-amazon-bedrock' ), ), 'agents' => array( 'page' => 'aicfab_tab_agents', 'label' => __( 'Agents and tools', 'ai-chat-for-amazon-bedrock' ), ), // The slug stays publishing, so links to it keep working. Its first section is the // page a plain link to the tab opens. 'publishing' => array( 'page' => 'aicfab_tab_wechat', 'label' => __( 'Channels', 'ai-chat-for-amazon-bedrock' ), 'sections' => array( 'wechat' => array( 'page' => 'aicfab_tab_wechat', 'label' => __( 'WeChat Official Account', 'ai-chat-for-amazon-bedrock' ), ), 'wxgame' => array( 'page' => 'aicfab_tab_wxgame', 'label' => __( 'WeChat mini game', 'ai-chat-for-amazon-bedrock' ), ), 'video' => array( 'page' => 'aicfab_tab_publishing', 'label' => __( 'Video and social platforms', 'ai-chat-for-amazon-bedrock' ), ), ), ), 'governance' => array( 'page' => 'aicfab_tab_governance', 'label' => __( 'Safety and spend', 'ai-chat-for-amazon-bedrock' ), ), 'integrations' => array( 'page' => 'aicfab_tab_integrations', 'label' => __( 'Integrations', 'ai-chat-for-amazon-bedrock' ), ), ) + ( class_exists( 'AI_Chat_Bedrock_WooCommerce' ) && AI_Chat_Bedrock_WooCommerce::active() ? array( 'woocommerce' => array( 'page' => 'aicfab_tab_woocommerce', 'label' => __( 'WooCommerce', 'ai-chat-for-amazon-bedrock' ), ), ) : array() ); } /** * Field identifiers registered for one tab page. * * @param string $page Tab page slug. * @return array */ public static function fields_for_page( $page ) { return isset( self::$tab_fields[ $page ] ) ? self::$tab_fields[ $page ] : array(); } private function option( $key, $fallback = '' ) { $options = get_option( 'ai_chat_bedrock_settings', array() ); return isset( $options[ $key ] ) ? $options[ $key ] : $fallback; } private function text_input( $key, $fallback, $maxlength, $css_class = 'regular-text' ) { echo ''; } private function credential_input( $key, $password ) { $constants = array( 'aws_access_key' => 'AI_CHAT_BEDROCK_AWS_ACCESS_KEY', 'aws_secret_key' => 'AI_CHAT_BEDROCK_AWS_SECRET_KEY', 'aws_session_token' => 'AI_CHAT_BEDROCK_AWS_SESSION_TOKEN', 'bedrock_api_key' => 'AI_CHAT_BEDROCK_API_KEY', ); $configured = ( isset( $constants[ $key ] ) && defined( $constants[ $key ] ) ) || '' !== $this->option( $key, '' ); echo ''; } private function select( $key, $values, $fallback ) { $current = $this->option( $key, $fallback ); echo ''; } }