*/ class A1AI_Admin { /** * The ID of this plugin. * * @since 1.0.0 * @access private * @var string $plugin_name The ID of this plugin. */ private $plugin_name; /** * The version of this plugin. * * @since 1.0.0 * @access private * @var string $version The current version of this plugin. */ private $version; /** * The data manager instance. * * @since 1.0.0 * @access private * @var A1AI_Data_Manager $data_manager Handles database operations. */ private $data_manager; /** * Initialize the class and set its properties. * * @since 1.0.0 * @param string $plugin_name The name of this plugin. * @param string $version The version of this plugin. * @param A1AI_Data_Manager $data_manager The data manager instance. */ public function __construct($plugin_name = 'a1ai', $version = '1.3.0', $data_manager = null) { $this->plugin_name = $plugin_name; $this->version = $version; $this->data_manager = $data_manager; // If data manager wasn't passed, create one if (null === $this->data_manager) { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-a1ai-data-manager.php'; $this->data_manager = new A1AI_Data_Manager(); } // Register AJAX handlers add_action('wp_ajax_a1ai_get_contact_conversation', array($this, 'get_contact_conversation')); } /** * Register the stylesheets for the admin area. * * @since 1.0.0 */ public function enqueue_styles($hook) { // Only load on our plugin pages if (!$this->is_plugin_page($hook)) { return; } $admin_css_path = plugin_dir_path(__FILE__) . 'css/a1ai-admin.css'; $admin_css_ver = file_exists($admin_css_path) ? $this->version . '.' . filemtime($admin_css_path) : $this->version; wp_enqueue_style( 'a1ai-admin', plugin_dir_url(__FILE__) . 'css/a1ai-admin.css', array(), $admin_css_ver, 'all' ); // Enqueue adjustments-specific styles // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used only for style enqueuing, not form processing if (strpos($hook, 'a1ai') !== false && isset($_GET['page']) && sanitize_text_field(wp_unslash($_GET['page'])) === 'a1ai-chatbots') { $adj_css_path = plugin_dir_path(__FILE__) . 'css/a1ai-admin-adjustments.css'; $adj_css_ver = file_exists($adj_css_path) ? $this->version . '.' . filemtime($adj_css_path) : $this->version; wp_enqueue_style( 'a1ai-admin-adjustments', plugin_dir_url(__FILE__) . 'css/a1ai-admin-adjustments.css', array('a1ai-admin'), $adj_css_ver, 'all' ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Register the JavaScript for the admin area. * * @since 1.0.0 */ public function enqueue_scripts($hook) { // Only load on our plugin pages if (!$this->is_plugin_page($hook)) { return; } // Enqueue Chart.js if needed on dashboard $dependencies = array('jquery'); if (strpos($hook, 'page_a1ai') !== false) { wp_enqueue_script( 'a1ai-chartjs', plugin_dir_url(dirname(__FILE__)) . 'assets/js/chart.min.js', array(), '4.4.2', true ); $dependencies[] = 'a1ai-chartjs'; } wp_enqueue_script( 'a1ai-admin', plugin_dir_url(__FILE__) . 'js/a1ai-admin.js', $dependencies, $this->version, true ); // Enqueue page-specific scripts if (strpos($hook, 'a1ai') !== false) { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used only for script enqueuing, not form processing // Chatbots page (includes adjustments tab and contact form fields drag-and-drop) if (isset($_GET['page']) && sanitize_text_field(wp_unslash($_GET['page'])) === 'a1ai-chatbots') { wp_enqueue_script('jquery-ui-sortable'); wp_enqueue_script( 'a1ai-admin-chatbots', plugin_dir_url(__FILE__) . 'js/a1ai-admin-chatbots.js', array('jquery', 'a1ai-admin'), $this->version, true ); wp_enqueue_script( 'a1ai-admin-adjustments', plugin_dir_url(__FILE__) . 'js/a1ai-admin-adjustments.js', array('jquery', 'a1ai-admin'), $this->version, true ); } // Chatbot edit page if (isset($_GET['page']) && sanitize_text_field(wp_unslash($_GET['page'])) === 'a1ai-chatbots' && isset($_GET['action']) && sanitize_text_field(wp_unslash($_GET['action'])) === 'edit') { wp_enqueue_script( 'a1ai-admin-chatbot-edit', plugin_dir_url(__FILE__) . 'js/a1ai-admin-chatbot-edit.js', array('jquery', 'a1ai-admin'), $this->version, true ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended } // Localize script with necessary data and nonce wp_localize_script( 'a1ai-admin', 'a1aichAdmin', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('a1ai_admin_nonce'), 'copiedText' => esc_html__('Copied!', 'a1ai-chatbot'), 'defaultGdprText' => esc_html__('By clicking "I Agree", you consent to the use of AI-powered chat services and acknowledge that your conversations may be processed to provide you with assistance. You can withdraw your consent at any time by closing this chat.', 'a1ai-chatbot'), 'strings' => array( 'saveSuccess' => esc_html__('Settings saved successfully.', 'a1ai-chatbot'), 'saveError' => esc_html__('Error saving settings.', 'a1ai-chatbot'), 'deleteSuccess' => esc_html__('Chatbot deleted successfully.', 'a1ai-chatbot'), 'deleteError' => esc_html__('Error deleting chatbot.', 'a1ai-chatbot'), 'confirmDelete' => esc_html__('Are you sure you want to delete this chatbot? This action cannot be undone.', 'a1ai-chatbot'), ) ) ); } /** * Check if current page is a plugin page. * * @since 1.0.0 * @param string $hook Current admin page. * @return bool True if it's a plugin page. */ private function is_plugin_page($hook) { // Always load admin scripts on a1ai pages if (strpos($hook, 'a1ai') !== false) { return true; } // Also check if we're on a plugin page by checking the page parameter // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Admin page protected by capabilities if (isset($_GET['page'])) { $page = sanitize_text_field(wp_unslash($_GET['page'])); if (strpos($page, 'a1ai') === 0) { return true; } } // phpcs:enable WordPress.Security.NonceVerification.Recommended return false; } /** * Register the admin menu. * * @since 1.0.0 */ public function register_admin_menu() { // Include plugin.php to use is_plugin_active() if (!function_exists('is_plugin_active')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // Main menu add_menu_page( __('A1AI Chatbot', 'a1ai-chatbot'), __('A1AI Chatbot', 'a1ai-chatbot'), 'manage_options', 'a1ai', array($this, 'render_dashboard_page'), 'dashicons-format-chat', 30 ); // Submenu - Dashboard add_submenu_page( 'a1ai', __('Dashboard', 'a1ai-chatbot'), __('Dashboard', 'a1ai-chatbot'), 'manage_options', 'a1ai', array($this, 'render_dashboard_page') ); // Submenu - Chatbots add_submenu_page( 'a1ai', __('Chatbots', 'a1ai-chatbot'), __('Chatbots', 'a1ai-chatbot'), 'manage_options', 'a1ai-chatbots', array($this, 'render_chatbots_page') ); // Submenu - Settings add_submenu_page( 'a1ai', __('Settings', 'a1ai-chatbot'), __('Settings', 'a1ai-chatbot'), 'manage_options', 'a1ai-settings', array($this, 'render_settings_page') ); // Submenu - Contact Requests (Premium Feature) // Only show if upgrade plugin is active with valid license if (class_exists('A1AI_Upgrade_Helper') && A1AI_Upgrade_Helper::is_upgrade_active()) { add_submenu_page( 'a1ai', __('Contact Requests', 'a1ai-chatbot'), __('Contact Requests', 'a1ai-chatbot'), 'manage_options', 'a1ai-contact-requests', array($this, 'display_contact_requests_page') ); } } /** * Add settings link to plugin list. * * @since 1.0.0 * @param array $links Existing plugin action links. * @return array Modified plugin action links. */ public function add_settings_link($links) { $settings_link = '' . __('Settings', 'a1ai-chatbot') . ''; array_unshift($links, $settings_link); return $links; } /** * Render the dashboard page. * * @since 1.0.0 */ public function render_dashboard_page() { // Pass necessary data to the template $data_manager = $this->data_manager; require_once plugin_dir_path(__FILE__) . 'partials/a1ai-admin-dashboard.php'; } /** * Render the chatbots page. * * @since 1.0.0 */ public function render_chatbots_page() { // Verify nonce if action is present $action_get = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; if ( $action_get && in_array( $action_get, array( 'edit', 'delete' ), true ) ) { if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), 'a1ai_chatbot_action' ) ) { wp_die(esc_html__('Security check failed.', 'a1ai-chatbot')); } } $action = isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : ''; if ('add' === $action || 'edit' === $action) { // Pass necessary data to the template $data_manager = $this->data_manager; require_once plugin_dir_path(__FILE__) . 'partials/a1ai-admin-chatbot-edit.php'; } else { // Pass necessary data to the template $data_manager = $this->data_manager; require_once plugin_dir_path(__FILE__) . 'partials/a1ai-admin-chatbots.php'; } } /** * Render the settings page. * * @since 1.0.0 */ public function render_settings_page() { // Pass necessary data to the template $data_manager = $this->data_manager; require_once plugin_dir_path(__FILE__) . 'partials/a1ai-admin-settings.php'; } /** * Handle admin AJAX requests. * * @since 1.0.0 */ public function handle_admin_ajax() { // Check nonce if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'a1ai_admin_nonce' ) ) { wp_send_json_error(array('message' => __('Security check failed.', 'a1ai-chatbot'))); } // Check capabilities if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have sufficient permissions.', 'a1ai-chatbot'))); } // Handle different actions $action_type = isset($_POST['action_type']) ? sanitize_text_field(wp_unslash($_POST['action_type'])) : ''; // Rate limiting for AI assistant chat to prevent API abuse if ('assistant_chat' === $action_type) { $user_id = get_current_user_id(); $rate_limit_key = 'a1ai_admin_assistant_limit_' . $user_id; $request_count = get_transient($rate_limit_key); $rate_limit = apply_filters('a1ai_rate_limit_assistant_per_minute', 20); // Lower limit for admin assistant if ($request_count && $request_count >= $rate_limit) { wp_send_json_error(array('message' => __('Rate limit exceeded. Please wait before sending more messages.', 'a1ai-chatbot'))); } // Increment request count set_transient($rate_limit_key, ($request_count ? $request_count + 1 : 1), MINUTE_IN_SECONDS); } switch ($action_type) { case 'save_settings': $this->save_settings(); break; case 'save_chatbot': $this->save_chatbot(); break; case 'delete_chatbot': $this->delete_chatbot(); break; case 'get_analytics': $this->get_analytics(); break; case 'get_conversation': $this->get_conversation(); break; case 'delete_conversation': $this->delete_conversation(); break; case 'save_adjustment': $this->save_adjustment(); break; case 'delete_adjustment': $this->delete_adjustment(); break; case 'save_action_guidance': $this->save_action_guidance(); break; case 'save_repetition_prevention': $this->save_repetition_prevention(); break; case 'submit_contact_request': $this->submit_contact_request(); break; case 'test_email': $this->test_email(); break; case 'assistant_chat': $this->handle_assistant_chat(); break; case 'get_export_data': $this->get_export_data(); break; case 'get_trends_data': $this->get_trends_data(); break; default: wp_send_json_error(array('message' => __('Invalid action.', 'a1ai-chatbot'))); break; } } /** * Save plugin settings. * * @since 1.0.0 */ private function save_settings() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() // General settings if (isset($_POST['a1ai_openai_api_key'])) { update_option('a1ai_openai_api_key', sanitize_text_field(wp_unslash($_POST['a1ai_openai_api_key']))); } if (isset($_POST['a1ai_edd_api_key'])) { $license_key = sanitize_text_field(wp_unslash($_POST['a1ai_edd_api_key'])); update_option('a1ai_edd_api_key', $license_key); // Also save to upgrade plugin license option and activate if (!empty($license_key) && class_exists('A1AI_Upgrade_Helper') && A1AI_Upgrade_Helper::is_plugin_installed()) { // Update the upgrade plugin's license key update_option('a1ai_upgrade_license', $license_key); // Attempt to activate the license automatically $license_manager_path = defined('A1AI_UPGRADE_PLUGIN_DIR') ? A1AI_UPGRADE_PLUGIN_DIR . 'includes/class-a1ai-upgrade-edd-license-manager.php' : WP_PLUGIN_DIR . '/a1ai-chatbot-upgrade/includes/class-a1ai-upgrade-edd-license-manager.php'; if (file_exists($license_manager_path)) { require_once $license_manager_path; $license_manager = new A1AI_Upgrade_EDD_License_Manager(); $license_manager->activate_license($license_key); } } } // AI provider selection if (isset($_POST['a1ai_llm_provider'])) { $provider_input = sanitize_text_field(wp_unslash($_POST['a1ai_llm_provider'])); $provider_value = in_array($provider_input, array('openai', 'anthropic'), true) ? $provider_input : 'openai'; update_option('a1ai_llm_provider', $provider_value); } // Anthropic API key if (isset($_POST['a1ai_anthropic_api_key'])) { update_option('a1ai_anthropic_api_key', sanitize_text_field(wp_unslash($_POST['a1ai_anthropic_api_key']))); } // AI Settings if (isset($_POST['a1ai_default_model'])) { update_option('a1ai_default_model', sanitize_text_field(wp_unslash($_POST['a1ai_default_model']))); } if (isset($_POST['a1ai_anthropic_default_model'])) { update_option('a1ai_anthropic_default_model', sanitize_text_field(wp_unslash($_POST['a1ai_anthropic_default_model']))); } if (isset($_POST['a1ai_default_temperature'])) { $temperature = min(max(0, floatval($_POST['a1ai_default_temperature'])), 2); update_option('a1ai_default_temperature', $temperature); } if (isset($_POST['a1ai_default_max_tokens'])) { $max_tokens = min(max(100, intval($_POST['a1ai_default_max_tokens'])), 4000); update_option('a1ai_default_max_tokens', $max_tokens); } // Global Chatbot Settings $global_enabled = isset($_POST['a1ai_global_chatbot_enabled']) ? (bool) $_POST['a1ai_global_chatbot_enabled'] : false; update_option('a1ai_global_chatbot_enabled', $global_enabled); if (isset($_POST['a1ai_global_chatbot_id'])) { update_option('a1ai_global_chatbot_id', intval($_POST['a1ai_global_chatbot_id'])); } if (isset($_POST['a1ai_global_chatbot_position'])) { update_option('a1ai_global_chatbot_position', sanitize_text_field(wp_unslash($_POST['a1ai_global_chatbot_position']))); } // Advanced Settings $keep_history = isset($_POST['a1ai_keep_conversation_history']) ? (bool) $_POST['a1ai_keep_conversation_history'] : false; update_option('a1ai_keep_conversation_history', $keep_history); if (isset($_POST['a1ai_conversation_retention_days'])) { $retention_days = min(max(1, intval($_POST['a1ai_conversation_retention_days'])), 365); update_option('a1ai_conversation_retention_days', $retention_days); } $keep_data = isset($_POST['a1ai_keep_data_on_uninstall']) ? (bool) $_POST['a1ai_keep_data_on_uninstall'] : false; update_option('a1ai_keep_data_on_uninstall', $keep_data); $show_powered_by = isset($_POST['a1ai_show_powered_by']) ? (bool) $_POST['a1ai_show_powered_by'] : false; update_option('a1ai_show_powered_by', $show_powered_by); // Security settings $recaptcha_enabled = isset($_POST['a1ai_recaptcha_enabled']) ? (bool) $_POST['a1ai_recaptcha_enabled'] : false; update_option('a1ai_recaptcha_enabled', $recaptcha_enabled); if (isset($_POST['a1ai_recaptcha_site_key'])) { update_option('a1ai_recaptcha_site_key', sanitize_text_field(wp_unslash($_POST['a1ai_recaptcha_site_key']))); } if (isset($_POST['a1ai_recaptcha_secret_key'])) { update_option('a1ai_recaptcha_secret_key', sanitize_text_field(wp_unslash($_POST['a1ai_recaptcha_secret_key']))); } $spam_heuristic_enabled = isset($_POST['a1ai_spam_heuristic_enabled']) ? (bool) $_POST['a1ai_spam_heuristic_enabled'] : false; update_option('a1ai_spam_heuristic_enabled', $spam_heuristic_enabled); $spam_ai_enabled = isset($_POST['a1ai_spam_ai_enabled']) ? (bool) $_POST['a1ai_spam_ai_enabled'] : false; update_option('a1ai_spam_ai_enabled', $spam_ai_enabled); // Content Awareness Settings (from upgrade plugin) if (isset($_POST['a1ai_auto_analyze']) || isset($_POST['a1ai_generate_sitemap']) || isset($_POST['a1ai_analysis_frequency']) || isset($_POST['a1ai_min_content_length']) || isset($_POST['a1ai_tier2_enabled']) || isset($_POST['a1ai_analysis_strategy'])) { $content_settings = get_option('a1ai_content_awareness_settings', array()); if (isset($_POST['a1ai_auto_analyze'])) { $content_settings['auto_analyze'] = (bool) $_POST['a1ai_auto_analyze'] ? 1 : 0; } if (isset($_POST['a1ai_generate_sitemap'])) { $content_settings['generate_sitemap'] = (bool) $_POST['a1ai_generate_sitemap'] ? 1 : 0; } if (isset($_POST['a1ai_analysis_frequency'])) { $content_settings['analysis_frequency'] = sanitize_text_field(wp_unslash($_POST['a1ai_analysis_frequency'])); } if (isset($_POST['a1ai_min_content_length'])) { $content_settings['min_content_length'] = intval($_POST['a1ai_min_content_length']); } if (isset($_POST['a1ai_tier2_enabled'])) { $content_settings['tier2_enabled'] = (bool) $_POST['a1ai_tier2_enabled'] ? 1 : 0; } if (isset($_POST['a1ai_analysis_strategy'])) { $content_settings['analysis_strategy'] = sanitize_text_field(wp_unslash($_POST['a1ai_analysis_strategy'])); } update_option('a1ai_content_awareness_settings', $content_settings); } // Business Information Settings if (isset($_POST['business_info']) && is_array($_POST['business_info'])) { $chatbot_id = isset($_POST['business_info_chatbot_id']) ? intval($_POST['business_info_chatbot_id']) : 0; if ($chatbot_id > 0) { // Process business info data $business_data = array(); if (isset($_POST['business_info']['business_name'])) { $business_data['business_name'] = sanitize_text_field(wp_unslash($_POST['business_info']['business_name'])); } if (isset($_POST['business_info']['tagline'])) { $business_data['tagline'] = sanitize_textarea_field(wp_unslash($_POST['business_info']['tagline'])); } if (isset($_POST['business_info']['business_type'])) { $business_data['business_type'] = sanitize_text_field(wp_unslash($_POST['business_info']['business_type'])); } // Process services (convert line-separated text to array) if (isset($_POST['business_info']['services'])) { $services_text = sanitize_textarea_field(wp_unslash($_POST['business_info']['services'])); $services_array = array_filter(array_map('trim', explode("\n", $services_text))); $business_data['services'] = $services_array; } // Process products (convert line-separated text to array) if (isset($_POST['business_info']['products'])) { $products_text = sanitize_textarea_field(wp_unslash($_POST['business_info']['products'])); $products_array = array_filter(array_map('trim', explode("\n", $products_text))); $business_data['products'] = $products_array; } // Process hours (already an array from form) if (isset($_POST['business_info']['hours']) && is_array($_POST['business_info']['hours'])) { $hours_array = array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array will be sanitized in loop $hours_input = wp_unslash($_POST['business_info']['hours']); // Unslash the array first foreach ($hours_input as $day => $time) { $hours_array[sanitize_key($day)] = sanitize_text_field($time); } $business_data['hours'] = $hours_array; } if (isset($_POST['business_info']['address'])) { $business_data['address'] = sanitize_textarea_field(wp_unslash($_POST['business_info']['address'])); } if (isset($_POST['business_info']['phone'])) { $business_data['phone'] = sanitize_text_field(wp_unslash($_POST['business_info']['phone'])); } if (isset($_POST['business_info']['email'])) { $business_data['email'] = sanitize_email(wp_unslash($_POST['business_info']['email'])); } if (isset($_POST['business_info']['website'])) { $business_data['website'] = esc_url_raw(wp_unslash($_POST['business_info']['website'])); } if (isset($_POST['business_info']['policies'])) { $business_data['policies'] = sanitize_textarea_field(wp_unslash($_POST['business_info']['policies'])); } // Save business info using data manager $this->data_manager->save_business_info($chatbot_id, $business_data); } } // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success(array('message' => __('Settings saved successfully.', 'a1ai-chatbot'))); } /** * Save chatbot data. * * @since 1.0.0 */ private function save_chatbot() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() // Get chatbot ID and name $chatbot_id = isset($_POST['chatbot_id']) ? intval($_POST['chatbot_id']) : 0; $chatbot_name = isset($_POST['chatbot_name']) ? sanitize_text_field(wp_unslash($_POST['chatbot_name'])) : ''; // Validate required fields if (empty($chatbot_name)) { wp_send_json_error(array('message' => __('Chatbot name is required.', 'a1ai-chatbot'))); } // Get and sanitize settings array $raw_settings = array(); if (isset($_POST['settings']) && is_array($_POST['settings'])) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $raw_settings = wp_kses_post_deep(wp_unslash($_POST['settings'])); } $settings = $this->sanitize_settings($raw_settings); // When updating an existing chatbot, preserve settings not on the edit form (e.g. contact_forms) if ($chatbot_id > 0) { $existing = $this->data_manager->get_chatbot($chatbot_id); $existing_settings = isset($existing['settings']) && is_array($existing['settings']) ? $existing['settings'] : array(); $settings = array_merge($existing_settings, $settings); } // Save chatbot $result = $this->data_manager->save_chatbot($chatbot_id, $chatbot_name, $settings); // Save system prompt and custom instructions to dedicated tables if ($result !== false) { $final_chatbot_id = $chatbot_id > 0 ? $chatbot_id : $result; // Handle system prompt - save if provided, deactivate if empty if (!empty($settings['system_prompt'])) { $this->data_manager->save_custom_instruction( $final_chatbot_id, 'System Prompt', $settings['system_prompt'], 'system', 0 ); } else { // Deactivate existing system prompt if none provided $existing_system_instructions = $this->data_manager->get_custom_instructions($final_chatbot_id, 'system'); if (!empty($existing_system_instructions)) { foreach ($existing_system_instructions as $instruction) { if ($instruction['instruction_name'] === 'System Prompt') { $this->data_manager->update_custom_instruction($instruction['id'], array('is_active' => 0)); break; } } } } // Save custom instructions if provided if (!empty($settings['custom_instructions'])) { $this->data_manager->save_custom_instruction( $final_chatbot_id, 'Custom Instructions', $settings['custom_instructions'], 'general', 1 ); } else { // Clear existing custom instructions if none provided $existing_instructions = $this->data_manager->get_custom_instructions($final_chatbot_id); if (!empty($existing_instructions)) { foreach ($existing_instructions as $instruction) { // Only deactivate general instructions, keep system prompt if ($instruction['instruction_type'] !== 'system') { $this->data_manager->update_custom_instruction($instruction['id'], array('is_active' => 0)); } } } } } if ($result === false) { wp_send_json_error(array('message' => __('Error saving chatbot.', 'a1ai-chatbot'))); } $success_message = $chatbot_id > 0 ? __('Chatbot updated successfully.', 'a1ai-chatbot') : __('Chatbot created successfully.', 'a1ai-chatbot'); $response_data = array('message' => $success_message); // If this is a new chatbot, add the redirect URL to the edit page if ($chatbot_id === 0) { $edit_url = add_query_arg( array( 'page' => 'a1ai-chatbots', 'action' => 'edit', 'id' => $result, '_wpnonce' => wp_create_nonce('a1ai_chatbot_action') ), admin_url('admin.php') ); $response_data['redirect'] = $edit_url; } // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success($response_data); } /** * Delete a chatbot. * * @since 1.0.0 */ private function delete_chatbot() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $chatbot_id = isset($_POST['chatbot_id']) ? intval($_POST['chatbot_id']) : 0; if ($chatbot_id <= 0) { wp_send_json_error(array('message' => __('Invalid chatbot ID.', 'a1ai-chatbot'))); } // Check if this is the global chatbot $global_chatbot_id = get_option('a1ai_global_chatbot_id', 0); if ($global_chatbot_id == $chatbot_id) { update_option('a1ai_global_chatbot_id', 0); update_option('a1ai_global_chatbot_enabled', false); } $result = $this->data_manager->delete_chatbot($chatbot_id); // phpcs:enable WordPress.Security.NonceVerification.Missing if ($result) { wp_send_json_success(array('message' => __('Chatbot deleted successfully.', 'a1ai-chatbot'))); } else { wp_send_json_error(array('message' => __('Error deleting chatbot.', 'a1ai-chatbot'))); } } /** * Get analytics data for charts. * * @since 1.0.0 */ private function get_analytics() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $period = isset($_POST['period']) ? sanitize_text_field(wp_unslash($_POST['period'])) : 'month'; $valid_periods = array('day', 'week', 'month', 'year'); if (!in_array($period, $valid_periods)) { $period = 'month'; } $stats = $this->data_manager->get_usage_stats($period); // Format data for Chart.js $labels = array(); $messages_data = array(); $conversations_data = array(); // Process message counts foreach ($stats['messages'] as $message) { $labels[] = $message['date']; $messages_data[] = intval($message['count']); } // Process conversation counts // Create conversation data array with same labels foreach ($labels as $label) { $count = 0; // Find matching conversation count foreach ($stats['conversations'] as $conversation) { if ($conversation['date'] === $label) { $count = intval($conversation['count']); break; } } $conversations_data[] = $count; } $chart_data = array( 'labels' => $labels, 'messages' => $messages_data, 'conversations' => $conversations_data, 'totals' => $stats['totals'], ); // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success($chart_data); } /** * Sanitize settings array. * * @since 1.0.0 * @param array $settings The settings array to sanitize. * @return array The sanitized settings array. */ private function sanitize_settings($settings) { if (!is_array($settings)) { return array(); } $sanitized_settings = array(); // Text fields $text_fields = array('chat_bubble_text', 'chatbot_title', 'welcome_message', 'chat_width', 'chat_height', 'primary_color', 'font_size', 'model', 'placeholder'); foreach ($text_fields as $field) { $sanitized_settings[$field] = isset($settings[$field]) ? sanitize_text_field(wp_unslash($settings[$field])) : ''; } // LLM provider override (empty string = inherit global) $sanitized_settings['llm_provider'] = isset($settings['llm_provider']) && in_array($settings['llm_provider'], array('openai', 'anthropic', ''), true) ? $settings['llm_provider'] : ''; // The chatbot edit form posts two model selects (one per provider). // Pick the value matching the effective provider, then normalize it. if (class_exists('A1AI_LLM_Config')) { $effective_provider = A1AI_LLM_Config::get_effective_provider($sanitized_settings); $candidate_model = ('anthropic' === $effective_provider) && isset($settings['model_anthropic']) ? sanitize_text_field(wp_unslash($settings['model_anthropic'])) : $sanitized_settings['model']; $sanitized_settings['model'] = A1AI_LLM_Config::validate_model_for_provider( $candidate_model, $effective_provider ); } // Email field with validation if (isset($settings['notification_email']) && !empty($settings['notification_email'])) { $email = sanitize_email(wp_unslash($settings['notification_email'])); if (is_email($email)) { $sanitized_settings['notification_email'] = $email; } else { $sanitized_settings['notification_email'] = ''; } } else { $sanitized_settings['notification_email'] = ''; } // Textarea fields $textarea_fields = array('system_prompt', 'custom_instructions', 'gdpr_consent_text', 'primary_action', 'secondary_actions', 'conversation_stages', 'variation_strategies'); foreach ($textarea_fields as $field) { $sanitized_settings[$field] = isset($settings[$field]) ? sanitize_textarea_field(wp_unslash($settings[$field])) : ''; } // Select/dropdown fields for conversation settings $sanitized_settings['action_guidance_tone'] = isset($settings['action_guidance_tone']) && in_array($settings['action_guidance_tone'], array('subtle', 'moderate', 'direct')) ? $settings['action_guidance_tone'] : 'moderate'; // Boolean fields - explicitly set to true only if value is "1" $sanitized_settings['gdpr_enabled'] = (isset($settings['gdpr_enabled']) && $settings['gdpr_enabled'] === "1") ? true : false; $sanitized_settings['conversation_tracking'] = (isset($settings['conversation_tracking']) && $settings['conversation_tracking'] === "1") ? true : false; $sanitized_settings['action_tracking'] = (isset($settings['action_tracking']) && $settings['action_tracking'] === "1") ? true : false; $sanitized_settings['voice_enabled'] = (isset($settings['voice_enabled']) && $settings['voice_enabled'] === '1') ? true : false; // Numeric fields $sanitized_settings['temperature'] = isset($settings['temperature']) ? min(max(0, floatval($settings['temperature'])), 2) : 0.7; $sanitized_settings['max_tokens'] = isset($settings['max_tokens']) ? min(max(100, intval($settings['max_tokens'])), 4000) : 1000; $sanitized_settings['repetition_threshold'] = isset($settings['repetition_threshold']) ? min(max(1, intval($settings['repetition_threshold'])), 10) : 6; $sanitized_settings['voice_language'] = isset($settings['voice_language']) ? sanitize_text_field(wp_unslash($settings['voice_language'])) : ''; return $sanitized_settings; } /** * Get conversation details for admin view. * * @since 1.0.0 */ private function get_conversation() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $conversation_id = isset($_POST['conversation_id']) ? intval($_POST['conversation_id']) : 0; if ($conversation_id <= 0) { wp_send_json_error(array('message' => __('Invalid conversation ID.', 'a1ai-chatbot'))); } // Get conversation details $conversation = $this->data_manager->get_conversation($conversation_id); if (empty($conversation)) { wp_send_json_error(array('message' => __('Conversation not found.', 'a1ai-chatbot'))); } // Get messages for this conversation $messages = $this->data_manager->get_messages($conversation_id); // Get chatbot name $chatbot = $this->data_manager->get_chatbot($conversation['chatbot_id']); $chatbot_name = $chatbot ? $chatbot['name'] : __('Unknown Chatbot', 'a1ai-chatbot'); // Get user name $user_name = __('Guest', 'a1ai-chatbot'); if ($conversation['user_id'] > 0) { $user = get_user_by('id', $conversation['user_id']); if ($user) { $user_name = $user->display_name; } } // Add additional info to conversation $conversation['chatbot_name'] = $chatbot_name; $conversation['user_name'] = $user_name; $response_data = array( 'conversation' => $conversation, 'messages' => $messages, ); // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success($response_data); } /** * Delete a conversation. * * @since 1.0.0 */ private function delete_conversation() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $conversation_id = isset($_POST['conversation_id']) ? intval($_POST['conversation_id']) : 0; if ($conversation_id <= 0) { wp_send_json_error(array('message' => __('Invalid conversation ID.', 'a1ai-chatbot'))); } // Get conversation to verify it exists $conversation = $this->data_manager->get_conversation($conversation_id); if (empty($conversation)) { wp_send_json_error(array('message' => __('Conversation not found.', 'a1ai-chatbot'))); } // Delete messages first (foreign key constraint) $deleted_messages = $this->data_manager->delete_messages_by_conversation($conversation_id); // Delete conversation $deleted_conversation = $this->data_manager->delete_conversation($conversation_id); if (false === $deleted_conversation) { wp_send_json_error(array('message' => __('Failed to delete conversation.', 'a1ai-chatbot'))); } // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success(array('message' => __('Conversation deleted successfully.', 'a1ai-chatbot'))); } /** * Save adjustment. * * @since 1.0.0 */ private function save_adjustment() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $chatbot_id = isset($_POST['chatbot_id']) ? intval($_POST['chatbot_id']) : 0; $user_input = isset($_POST['user_input']) ? sanitize_textarea_field(wp_unslash($_POST['user_input'])) : ''; $ai_output = isset($_POST['ai_output']) ? sanitize_textarea_field(wp_unslash($_POST['ai_output'])) : ''; $ui_match_type = isset($_POST['ui_match_type']) ? sanitize_text_field(wp_unslash($_POST['ui_match_type'])) : 'Broad'; $ai_match_type = isset($_POST['ai_match_type']) ? sanitize_text_field(wp_unslash($_POST['ai_match_type'])) : 'Broad'; if ($chatbot_id <= 0) { wp_send_json_error(array('message' => __('Invalid chatbot ID.', 'a1ai-chatbot'))); } if (empty($user_input) || empty($ai_output)) { wp_send_json_error(array('message' => __('User input and AI output are required.', 'a1ai-chatbot'))); } // Validate match types $valid_match_types = array('Flexible', 'Broad', 'Phrase', 'Exact'); if (!in_array($ui_match_type, $valid_match_types, true)) { $ui_match_type = 'Broad'; } if (!in_array($ai_match_type, $valid_match_types, true)) { $ai_match_type = 'Broad'; } // Save adjustment $result = $this->data_manager->add_adjustment($chatbot_id, $user_input, $ai_output, $ui_match_type, $ai_match_type); if (false === $result) { wp_send_json_error(array('message' => __('Failed to save adjustment.', 'a1ai-chatbot'))); } // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success(array( 'message' => __('Adjustment saved successfully.', 'a1ai-chatbot'), 'adjustment_id' => $result )); } /** * Delete adjustment. * * @since 1.0.0 */ private function delete_adjustment() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $adjustment_id = isset($_POST['adjustment_id']) ? intval($_POST['adjustment_id']) : 0; if ($adjustment_id <= 0) { wp_send_json_error(array('message' => __('Invalid adjustment ID.', 'a1ai-chatbot'))); } // Get adjustment to verify it exists $adjustment = $this->data_manager->get_adjustment($adjustment_id); if (empty($adjustment)) { wp_send_json_error(array('message' => __('Adjustment not found.', 'a1ai-chatbot'))); } // Delete adjustment $result = $this->data_manager->delete_adjustment($adjustment_id); if (false === $result) { wp_send_json_error(array('message' => __('Failed to delete adjustment.', 'a1ai-chatbot'))); } // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success(array('message' => __('Adjustment deleted successfully.', 'a1ai-chatbot'))); } /** * Save action guidance settings. * * @since 1.4.0 */ private function save_action_guidance() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $chatbot_id = isset($_POST['chatbot_id']) ? intval($_POST['chatbot_id']) : 0; if ($chatbot_id <= 0) { wp_send_json_error(array('message' => __('Invalid chatbot ID.', 'a1ai-chatbot'))); } // Get current settings $action_guidance_settings = get_option('a1ai_action_guidance_settings', array()); $conversation_settings = get_option('a1ai_conversation_settings', array()); // Update action guidance settings $action_guidance_settings[$chatbot_id] = array( 'primary_action' => isset($_POST['primary_action']) ? sanitize_textarea_field(wp_unslash($_POST['primary_action'])) : '', 'secondary_actions' => isset($_POST['secondary_actions']) ? sanitize_textarea_field(wp_unslash($_POST['secondary_actions'])) : '', ); // Update conversation settings $conversation_settings[$chatbot_id] = array_merge( $conversation_settings[$chatbot_id] ?? array(), array( 'enable_stage_detection' => isset($_POST['enable_stage_detection']), 'action_stage_threshold' => isset($_POST['action_stage_threshold']) ? intval($_POST['action_stage_threshold']) : 5, ) ); // Save settings $result1 = update_option('a1ai_action_guidance_settings', $action_guidance_settings); $result2 = update_option('a1ai_conversation_settings', $conversation_settings); if ($result1 && $result2) { wp_send_json_success(array('message' => __('Action guidance settings saved successfully.', 'a1ai-chatbot'))); } else { wp_send_json_error(array('message' => __('Failed to save action guidance settings.', 'a1ai-chatbot'))); } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Save repetition prevention settings. * * @since 1.4.0 */ private function save_repetition_prevention() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $chatbot_id = isset($_POST['chatbot_id']) ? intval($_POST['chatbot_id']) : 0; if ($chatbot_id <= 0) { wp_send_json_error(array('message' => __('Invalid chatbot ID.', 'a1ai-chatbot'))); } // Get current settings $conversation_settings = get_option('a1ai_conversation_settings', array()); // Update repetition prevention settings $conversation_settings[$chatbot_id] = array_merge( $conversation_settings[$chatbot_id] ?? array(), array( 'similarity_threshold' => isset($_POST['similarity_threshold']) ? floatval($_POST['similarity_threshold']) : 0.7, 'repetition_action' => isset($_POST['repetition_action']) ? sanitize_text_field(wp_unslash($_POST['repetition_action'])) : 'suggest_alternatives', 'enable_repetition_detection' => isset($_POST['enable_repetition_detection']), ) ); // Save settings $result = update_option('a1ai_conversation_settings', $conversation_settings); if ($result) { wp_send_json_success(array('message' => __('Repetition prevention settings saved successfully.', 'a1ai-chatbot'))); } else { wp_send_json_error(array('message' => __('Failed to save repetition prevention settings.', 'a1ai-chatbot'))); } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Submit contact request from AI assistant. * * @since 1.4.0 */ private function submit_contact_request() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() // Debug logging - only in development if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // Development logging only } $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : ''; $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; $phone = isset($_POST['phone']) ? sanitize_text_field(wp_unslash($_POST['phone'])) : ''; $message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : ''; $conversation_id = isset($_POST['conversation_id']) ? sanitize_text_field(wp_unslash($_POST['conversation_id'])) : ''; // Debug logging - only in development if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // Development logging only } if (empty($name) || empty($email) || empty($phone)) { if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // Development logging only } wp_send_json_error(array('message' => __('Please fill in all required fields (Name, Email, Phone).', 'a1ai-chatbot'))); } // Store contact request in database as fallback $contact_data = array( 'name' => $name, 'email' => $email, 'phone' => $phone, 'message' => $message, 'conversation_id' => $conversation_id, 'timestamp' => current_time('mysql'), 'status' => 'pending', 'type' => 'contact_request' // Mark as contact request (not review) ); // Try to store in options as a simple fallback $existing_requests = get_option('a1ai_contact_requests', array()); $existing_requests[] = $contact_data; update_option('a1ai_contact_requests', $existing_requests); $subject = __('New Contact Request from A1AI Chatbot Assistant -', 'a1ai-chatbot') . ' ' . $name; $body = "New contact request from the A1AI Chatbot Assistant:\n\n"; $body .= "Name: " . $name . "\n"; $body .= "Email: " . $email . "\n"; $body .= "Phone: " . $phone . "\n"; $body .= "Message:\n" . $message . "\n\n"; $body .= "This request was submitted through the A1AI Chatbot Assistant on " . get_site_url() . "\n"; $body .= "Timestamp: " . current_time('mysql') . "\n"; // Try multiple email addresses as fallback $email_addresses = array( 'alerts@a1aidigital.com', get_option('admin_email'), // Fallback to admin email ); $email_sent = false; $last_error = ''; foreach ($email_addresses as $to) { $headers = array( 'Content-Type: text/plain; charset=UTF-8', 'From: ' . get_option('blogname') . ' <' . get_option('admin_email') . '>' ); if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // Development logging only } if (wp_mail($to, $subject, $body, $headers)) { if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // Development logging only } $email_sent = true; break; } else { $last_error = 'Failed to send to ' . $to; if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // Development logging only } } } if ($email_sent) { wp_send_json_success(array('message' => __('Contact request submitted successfully! A1AI will reach out to you within 24 hours.', 'a1ai-chatbot'))); } else { // Even if email fails, we've stored the request in the database if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // Development logging only } // Send success response since we've stored the request wp_send_json_success(array( 'message' => __('Contact request received! We\'ve saved your information and will reach out to you within 24 hours.', 'a1ai-chatbot'), 'note' => 'Email delivery failed, but request was saved successfully.', 'alternative_contact' => array( 'email' => 'contact@a1aidigital.com', 'phone' => '801-893-1686' ) )); } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Handle AI assistant chat messages. * * @since 1.4.0 */ private function handle_assistant_chat() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : ''; $conversation_id = isset($_POST['conversation_id']) ? sanitize_text_field(wp_unslash($_POST['conversation_id'])) : ''; if (empty($message)) { wp_send_json_error(array('message' => __('Message cannot be empty.', 'a1ai-chatbot'))); } // Get the assistant's system prompt and configuration $assistant_config = $this->get_assistant_config(); try { // Get conversation context for the assistant $conversation_context = $this->get_assistant_conversation_context($conversation_id); // Prepare messages for OpenAI API $messages = array(); // Add system message $messages[] = array( 'role' => 'system', 'content' => $assistant_config['system_prompt'] ); // Add conversation history (last 10 messages) foreach ($conversation_context as $context_message) { $messages[] = array( 'role' => $context_message['role'], 'content' => $context_message['content'] ); } // Add current user message $messages[] = array( 'role' => 'user', 'content' => $message ); // Dispatch to the active LLM provider (OpenAI or Anthropic). $response = $this->make_llm_request($messages, $assistant_config); if ($response && isset($response['choices'][0]['message']['content'])) { $ai_response = $response['choices'][0]['message']['content']; // Store the conversation for context $this->store_assistant_conversation($conversation_id, $message, $ai_response); wp_send_json_success(array('response' => $ai_response)); } else { wp_send_json_error(array('message' => __('Failed to get response from AI service.', 'a1ai-chatbot'))); } } catch (Exception $e) { // Check if this is an API key error if ($e->getMessage() === 'API_KEY_MISSING') { $settings_url = admin_url('admin.php?page=a1ai-settings#tab-general'); $missing_provider_label = class_exists('A1AI_LLM_Config') ? A1AI_LLM_Config::get_provider_label(A1AI_LLM_Config::get_effective_provider(array())) : 'OpenAI'; $error_message = sprintf( /* translators: 1: provider label (OpenAI or Anthropic), 2: URL to the settings page */ __('I notice you haven\'t set up your %1$s API key yet. Click Here to Setup the Key', 'a1ai-chatbot'), $missing_provider_label, esc_url($settings_url) ); wp_send_json_error(array('message' => $error_message)); } else { // translators: %s: Error message from exception wp_send_json_error(array('message' => sprintf(__('Error: %s', 'a1ai-chatbot'), $e->getMessage()))); } } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Dispatch the dashboard AI Assistant request to the active LLM provider. * * Anthropic responses are normalized to the OpenAI shape (a `choices` * array with one message) so the calling code that already reads * $response['choices'][0]['message']['content'] keeps working. * * @since 1.4.0 * @param array $messages OpenAI-style messages array. * @param array $config Configuration for the request. * @return array Response data in OpenAI shape. * @throws Exception On any non-2xx response or transport error. */ private function make_llm_request($messages, $config) { $provider = class_exists('A1AI_LLM_Config') ? A1AI_LLM_Config::get_effective_provider(array()) : 'openai'; // The dashboard assistant has no per-chatbot context, so fall back to // the provider's global default model when the assistant config's // model is hardcoded for the other provider. if (class_exists('A1AI_LLM_Config') && isset($config['model'])) { $config['model'] = A1AI_LLM_Config::validate_model_for_provider($config['model'], $provider); } if ('anthropic' === $provider) { return $this->make_anthropic_assistant_request($messages, $config); } return $this->make_openai_request($messages, $config); } /** * Make a direct OpenAI API request for the dashboard AI Assistant. * * @since 1.4.0 * @param array $messages Messages to send to OpenAI. * @param array $config Configuration for the request. * @return array OpenAI API response. */ private function make_openai_request($messages, $config) { $api_key = get_option('a1ai_openai_api_key', ''); if (empty($api_key)) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception('API_KEY_MISSING'); } $url = 'https://api.openai.com/v1/chat/completions'; $body = array( 'model' => $config['model'], 'messages' => $messages, 'temperature' => $config['temperature'], 'max_tokens' => $config['max_tokens'] ); $args = array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode($body), 'timeout' => 30 ); // Log request metadata only — never message content (may contain user/site data) if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant OpenAI Request: model=' . $config['model'] . ' messages=' . count($messages) . ' est_chars=' . strlen(json_encode($body))); } $response = wp_remote_post($url, $args); if (is_wp_error($response)) { $error_message = $response->get_error_message(); if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant OpenAI Error: ' . $error_message); } // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception($error_message); } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); // Log response for debugging (only when WP_DEBUG is enabled) if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant OpenAI Response Code: ' . $response_code); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant OpenAI Response Body: ' . substr($response_body, 0, 500)); } if ($response_code !== 200) { $error_data = json_decode($response_body, true); $error_message = isset($error_data['error']['message']) ? $error_data['error']['message'] : 'Unknown error'; if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant OpenAI API Error: ' . $error_message); } // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception('OpenAI API error: ' . $error_message); } $data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { $json_error = json_last_error_msg(); if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant OpenAI JSON Parse Error: ' . $json_error); } // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception('Failed to parse OpenAI response: ' . $json_error); } return $data; } /** * Make a direct Anthropic Messages API request for the dashboard AI * Assistant. Returns the response normalized to the OpenAI shape so the * caller can read $data['choices'][0]['message']['content'] uniformly. * * @since 1.4.0 * @param array $messages OpenAI-style messages. * @param array $config Configuration: model, temperature, max_tokens. * @return array Normalized OpenAI-shaped response. * @throws Exception On any non-2xx response or transport error. */ private function make_anthropic_assistant_request($messages, $config) { $api_key = get_option('a1ai_anthropic_api_key', ''); if (empty($api_key)) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception('API_KEY_MISSING'); } $adapted = A1AI_LLM_Config::convert_messages_for_anthropic($messages); $body = array( 'model' => $config['model'], 'max_tokens' => max(1, (int) $config['max_tokens']), 'messages' => $adapted['messages'], ); if (isset($config['temperature']) && is_numeric($config['temperature'])) { $body['temperature'] = (float) $config['temperature']; } if (isset($adapted['system']) && '' !== $adapted['system']) { $body['system'] = $adapted['system']; } $args = array( 'method' => 'POST', 'headers' => array( 'x-api-key' => $api_key, 'anthropic-version' => A1AI_LLM_Config::ANTHROPIC_API_VERSION, 'Content-Type' => 'application/json', ), 'body' => json_encode($body), 'timeout' => 30, ); // Log request metadata only — never message content (may contain user/site data) if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant Anthropic Request: model=' . $config['model'] . ' messages=' . count($adapted['messages']) . ' est_chars=' . strlen(json_encode($body))); } $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args); if (is_wp_error($response)) { $error_message = $response->get_error_message(); if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant Anthropic Error: ' . $error_message); } // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception($error_message); } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant Anthropic Response Code: ' . $response_code); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log('A1AI Assistant Anthropic Response Body: ' . substr($response_body, 0, 500)); } if (200 !== $response_code) { $error_data = json_decode($response_body, true); $error_message = isset($error_data['error']['message']) ? $error_data['error']['message'] : 'Unknown error'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception('Anthropic API error: ' . $error_message); } $data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { $json_error = json_last_error_msg(); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not output to user throw new Exception('Failed to parse Anthropic response: ' . $json_error); } // Normalize to OpenAI shape so the caller's existing accessor works. $assistant_text = ''; if (isset($data['content']) && is_array($data['content'])) { foreach ($data['content'] as $block) { if (is_array($block) && isset($block['type'], $block['text']) && 'text' === $block['type']) { $assistant_text = (string) $block['text']; break; } } } $input_tokens = isset($data['usage']['input_tokens']) ? (int) $data['usage']['input_tokens'] : 0; $output_tokens = isset($data['usage']['output_tokens']) ? (int) $data['usage']['output_tokens'] : 0; return array( 'choices' => array( array( 'message' => array( 'role' => 'assistant', 'content' => $assistant_text, ), ), ), 'usage' => array( 'prompt_tokens' => $input_tokens, 'completion_tokens' => $output_tokens, 'total_tokens' => $input_tokens + $output_tokens, ), 'model' => isset($data['model']) ? $data['model'] : $config['model'], ); } /** * Get the AI assistant configuration. * * @since 1.4.0 * @return array Assistant configuration. */ private function get_assistant_config() { // This configuration can be easily modified by A1AI without affecting the main plugin return array( 'system_prompt' => $this->get_assistant_system_prompt(), 'model' => 'gpt-3.5-turbo', // Default to GPT 3.5 Turbo 'temperature' => 0.7, 'max_tokens' => 1000 ); } /** * Get the AI assistant system prompt. * * @since 1.4.0 * @return string System prompt for the assistant. */ private function get_assistant_system_prompt() { // Canonical URLs and pricing live in A1AI_Links. It is loaded by // A1AI::load_dependencies() ahead of this class, but guard anyway so a // changed include order degrades into a require rather than a fatal. if (!class_exists('A1AI_Links')) { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-a1ai-links.php'; } // Get features information $features_info = $this->get_features_info(); // Check if user has premium upgrade $has_upgrade = class_exists('A1AI_Upgrade_Helper') && A1AI_Upgrade_Helper::is_upgrade_active(); // Enhanced consultative sales-focused system prompt $base_prompt = "You are the A1AI Chatbot Assistant - a knowledgeable, consultative AI advisor who helps WordPress site owners succeed with AI chatbots. "; $base_prompt .= "You're part trusted advisor, part technical expert, and part growth consultant.\n\n"; // Add user's current version status if ($has_upgrade) { $base_prompt .= "## CURRENT USER STATUS: PREMIUM UPGRADE ACTIVE ⭐ ##\n"; $base_prompt .= "This user has the PREMIUM UPGRADE with ALL premium features:\n"; $base_prompt .= "✓ Smart Contact Forms (collect contact details inside the conversation)\n"; $base_prompt .= "✓ Review Management (ask happy customers for a review automatically)\n"; $base_prompt .= "✓ Content Awareness (answers from the site's own products and pages)\n"; $base_prompt .= "✓ Advanced Adjustments (fix responses in 30 seconds)\n"; $base_prompt .= "✓ Advanced Analytics (track conversions and trends)\n"; $base_prompt .= "✓ Voice Integration\n"; $base_prompt .= "✓ Google Integration\n\n"; $base_prompt .= "**CRITICAL SELLING RULES FOR PREMIUM USERS:**\n"; $base_prompt .= "❌ NEVER try to sell them Premium plugin features - they already have them!\n"; $base_prompt .= "❌ NEVER mention 'upgrading the plugin' - they ARE upgraded!\n"; $base_prompt .= "✅ PRIMARY upsell: A1AI WORKSPACE (" . A1AI_Links::PRICE_WORKSPACE_WORDPRESS . " - " . A1AI_Links::PRICE_WORKSPACE_FULL . ", " . A1AI_Links::WORKSPACE_TRIAL . ")\n"; $base_prompt .= "✅ Frame it honestly: the chatbot answers well now; Workspace is what happens to the lead AFTERWARDS\n"; $base_prompt .= "✅ Secondary upsell: DONE-FOR-YOU SERVICES - we run Workspace for them\n"; $base_prompt .= "✅ Always link " . A1AI_Links::SITE . "/workspace/ when Workspace comes up\n\n"; } else { $base_prompt .= "## CURRENT USER STATUS: FREE VERSION 📦 ##\n"; $base_prompt .= "This user has the FREE version and is MISSING premium features:\n"; $base_prompt .= "❌ Smart Contact Forms (visitors leave without giving contact details)\n"; $base_prompt .= "❌ Review Management (no automatic review requests)\n"; $base_prompt .= "❌ Content Awareness (high 'I don't know' responses)\n"; $base_prompt .= "❌ Advanced Adjustments (slow manual prompt tweaking)\n"; $base_prompt .= "❌ Advanced Analytics (no conversion tracking)\n"; $base_prompt .= "❌ Voice Integration\n"; $base_prompt .= "❌ Google Integration\n\n"; $base_prompt .= "**CRITICAL SELLING RULES FOR FREE USERS:**\n"; $base_prompt .= "✅ FIRST upsell: PREMIUM PLUGIN UPGRADE (" . A1AI_Links::PRICE_PREMIUM . ") - the only chatbot-level paid step\n"; $base_prompt .= "✅ ULTIMATE destination: A1AI WORKSPACE (" . A1AI_Links::PRICE_WORKSPACE_WORDPRESS . " - " . A1AI_Links::PRICE_WORKSPACE_FULL . ", " . A1AI_Links::WORKSPACE_TRIAL . ")\n"; $base_prompt .= "✅ Emphasize business growth features and ROI\n"; $base_prompt .= "✅ Show what they're missing (lost leads, missed reviews, no follow-up)\n"; $base_prompt .= "✅ Be clear about the ceiling: Premium makes the CHATBOT better; Workspace runs the BUSINESS around it\n"; $base_prompt .= "✅ For users wanting zero effort: Done-For-You services (we run Workspace for them)\n"; $base_prompt .= "✅ Always link " . A1AI_Links::SITE . "/workspace/ when Workspace comes up\n\n"; } $base_prompt .= "## YOUR PRIMARY MISSION ##\n"; if ($has_upgrade) { $base_prompt .= "Help PREMIUM users get results, then show them the platform their chatbot feeds:\n"; $base_prompt .= "1. Help them USE their premium features effectively\n"; $base_prompt .= "2. Find out what happens to a lead AFTER the chat ends - that is usually where it breaks\n"; $base_prompt .= "3. Show how WORKSPACE closes that gap (CRM, follow-up, calendar, reporting)\n"; $base_prompt .= "4. Position Workspace as the natural next step; Done-For-You if they want it operated for them\n\n"; } else { $base_prompt .= "Help FREE users win with the free chatbot, then walk them up the ladder:\n"; $base_prompt .= "1. Identify their #1 pain point (leads, reviews, support, time, follow-up)\n"; $base_prompt .= "2. Show how PREMIUM solves the chatbot-level problems with specific ROI examples\n"; $base_prompt .= "3. Make the math obvious: Just 1-2 extra leads/month = profitable\n"; $base_prompt .= "4. Name the bigger picture: WORKSPACE is where chat becomes contact, follow-up and revenue\n"; $base_prompt .= "5. For users wanting zero effort: Done-For-You services (Workspace, operated by us)\n\n"; } $base_prompt .= "## CONSULTATIVE SELLING APPROACH ##\n"; $base_prompt .= "You're a CONSULTANT, not a pushy salesperson. Follow this framework:\n\n"; $base_prompt .= "1. **LISTEN FIRST** - Understand their situation before recommending:\n"; $base_prompt .= " - What's their primary goal? (leads, support, reviews, etc.)\n"; $base_prompt .= " - How much time can they dedicate weekly?\n"; $base_prompt .= " - Do they have technical skills?\n"; $base_prompt .= " - What's their customer lifetime value?\n"; $base_prompt .= " - How many website visitors monthly?\n\n"; if ($has_upgrade) { $base_prompt .= "2. **DIAGNOSE THE NEED** (For Premium Users):\n"; $base_prompt .= " - Using features but struggling = Guide them on optimization\n"; $base_prompt .= " - Leads captured but not followed up = WORKSPACE (CRM + automated follow-up)\n"; $base_prompt .= " - Chatbot still missing site knowledge = WORKSPACE (Sync Knowledge)\n"; $base_prompt .= " - Wants to know which chats made money = WORKSPACE (unified reporting)\n"; $base_prompt .= " - Needs a human on live chats = WORKSPACE (live monitor + takeover)\n"; $base_prompt .= " - Zero time to run any of it = DONE-FOR-YOU SERVICES (we operate Workspace)\n\n"; } else { $base_prompt .= "2. **DIAGNOSE THE NEED** (For Free Users):\n"; $base_prompt .= " - Need more leads/conversions = PREMIUM (Smart Contact Forms)\n"; $base_prompt .= " - Need more reviews = PREMIUM (Review Management)\n"; $base_prompt .= " - Chatbot gives wrong answers = PREMIUM (Content Awareness + Advanced Adjustments)\n"; $base_prompt .= " - Want chatbot analytics/exports = PREMIUM (Advanced Analytics)\n"; $base_prompt .= " - Leads going nowhere after the chat = WORKSPACE (CRM, email, calendar, reporting)\n"; $base_prompt .= " - Running several sites or clients = WORKSPACE (one app across all of them)\n"; $base_prompt .= " - No time to manage at all = DONE-FOR-YOU SERVICES (Workspace, operated by us)\n\n"; } $base_prompt .= "3. **EDUCATE WITH VALUE** - Explain benefits, not just features:\n"; $base_prompt .= " - Don't say 'Smart Contact Forms' - say 'collect a name and email inside the conversation, instead of hoping they find your contact page'\n"; $base_prompt .= " - Don't say 'Review Management' - say 'ask satisfied customers for a review while they are still in the chat'\n"; $base_prompt .= " - Describe what the feature DOES for them; never attach a performance number to it\n"; $base_prompt .= " - Paint the picture of their success\n\n"; $base_prompt .= "4. **HANDLE OBJECTIONS GRACEFULLY** - Address concerns with empathy:\n"; $base_prompt .= " - 'Too expensive' → Show ROI math (just 1-2 extra leads pays for Premium)\n"; $base_prompt .= " - 'No time' → That's exactly why Managed Services exists\n"; $base_prompt .= " - 'Will it work for my industry?' → Explain how it would handle their own customers' questions\n"; $base_prompt .= " - 'Worried about AI errors' → Explain safeguards (Adjustments, Content Awareness, human fallback)\n"; $base_prompt .= " - See the 'COMMON OBJECTIONS' section in features guide for detailed responses\n\n"; $base_prompt .= "5. **CREATE ETHICAL URGENCY** - Motivate action without pressure:\n"; $base_prompt .= " - Point out that the free version stays free, so trying it costs nothing\n"; $base_prompt .= " - Emphasize competitor advantage ('Every day without 24/7 lead capture = missed revenue')\n"; $base_prompt .= " - Do NOT promise a refund, a guarantee, or a payback period - none of those is offered\n"; $base_prompt .= " - But NEVER pressure - trust is more valuable than forced sale\n\n"; $base_prompt .= "6. **CLEAR NEXT STEPS** - Always end with specific action:\n"; $base_prompt .= " - FREE: 'Start with free version, I'll help you set it up right now'\n"; $base_prompt .= " - PREMIUM: 'Visit Settings > Upgrades tab, or " . A1AI_Links::SITE . "/ai-wordpress-plugin/ to buy'\n"; $base_prompt .= " - WORKSPACE: 'See the full tour at " . A1AI_Links::SITE . "/workspace/ - " . A1AI_Links::WORKSPACE_TRIAL . "'\n"; $base_prompt .= " - PRICING: 'Every tier and plan is listed at " . A1AI_Links::SITE . "/upgrades-products/'\n"; $base_prompt .= " - DONE-FOR-YOU: 'Plans at " . A1AI_Links::SITE . "/ai-services/, or book a call at " . A1AI_Links::BOOK_CALL . "'\n"; $base_prompt .= " - UNSURE: 'Free consultation at " . A1AI_Links::SITE . "/contact/ - we show you Workspace against your own chat data'\n\n"; $base_prompt .= "## KEY BEHAVIORS ##\n"; $base_prompt .= "✅ DO:\n"; $base_prompt .= "- Ask qualifying questions to understand their needs\n"; $base_prompt .= "- Be concrete about what a feature does in the user own situation\n"; $base_prompt .= "- Explain how it would work for a site like theirs, without citing results we have not measured\n"; $base_prompt .= "- Be honest if FREE version is right for them (builds trust for future upsell)\n"; $base_prompt .= "- Explain the 'why' behind recommendations\n"; $base_prompt .= "- Use conversational, friendly language (not corporate speak)\n"; $base_prompt .= "- Offer to walk them through setup if they choose FREE/PREMIUM\n"; $base_prompt .= "- Make it EASY to contact A1AI (email/phone prominently)\n\n"; $base_prompt .= "❌ DON'T:\n"; $base_prompt .= "- Recommend WORKSPACE or DONE-FOR-YOU to everyone (it's overkill for some users)\n"; $base_prompt .= "- Quote a price you are not sure about - link the pricing page instead\n"; $base_prompt .= "- Claim the free chatbot needs Workspace to work (it does not)\n"; $base_prompt .= "- Use pushy sales tactics or create fake scarcity\n"; $base_prompt .= "- Invent or repeat performance statistics - percentages, multipliers, payback periods, conversion rates. We have not measured them, so stating them is an advertising claim we cannot substantiate. If asked how much it helps, say results vary by site and offer to look at their own numbers\n"; $base_prompt .= "- Promise a guarantee, refund, or trial that is not on the pricing page\n"; $base_prompt .= "- Overwhelm with every feature - match to their stated goal\n"; $base_prompt .= "- Give up after first objection - address it thoughtfully\n"; $base_prompt .= "- Use jargon without explaining it\n"; $base_prompt .= "- Make promises you can't keep\n"; $base_prompt .= "- Forget to ask about their GOALS before recommending\n\n"; $base_prompt .= "## RESPONSE STRUCTURE ##\n"; $base_prompt .= "Use this format for recommendations:\n"; $base_prompt .= "1. Acknowledge their situation/question\n"; $base_prompt .= "2. Ask clarifying question if needed (max 1-2 questions)\n"; $base_prompt .= "3. Provide personalized recommendation with reasoning\n"; $base_prompt .= "4. Show concretely how it would work on their site\n"; $base_prompt .= "5. Address potential objections proactively\n"; $base_prompt .= "6. Clear next step with contact info\n\n"; $base_prompt .= "## WHERE TO SEND PEOPLE (use these exact URLs) ##\n"; $base_prompt .= "🧩 A1AI Workspace (the platform): " . A1AI_Links::SITE . "/workspace/\n"; $base_prompt .= "🌐 The whole ecosystem: " . A1AI_Links::SITE . "/ai-ecosystem/\n"; $base_prompt .= "💳 Plans & pricing: " . A1AI_Links::SITE . "/upgrades-products/\n"; $base_prompt .= "⭐ Premium plugin upgrade: " . A1AI_Links::SITE . "/ai-wordpress-plugin/\n"; $base_prompt .= "🤝 Done-for-you services: " . A1AI_Links::SITE . "/ai-services/\n"; $base_prompt .= "📅 Book a 30-min walkthrough: " . A1AI_Links::BOOK_CALL . "\n"; $base_prompt .= "✉️ Free consultation: " . A1AI_Links::SITE . "/contact/\n"; $base_prompt .= "📧 Email: " . A1AI_Links::EMAIL . "\n"; $base_prompt .= "📞 Phone: " . A1AI_Links::PHONE . " (" . A1AI_Links::HOURS . ")\n"; $base_prompt .= "⏱️ Response: Usually within 4 hours (business days)\n\n"; $base_prompt .= "## QUICK REFERENCE PRICING (do not invent other numbers) ##\n"; $base_prompt .= "FREE CHATBOT: $0 (core features, unlimited chatbots) - always free\n"; $base_prompt .= "PREMIUM PLUGIN UPGRADE: " . A1AI_Links::PRICE_PREMIUM . " (single site)\n"; $base_prompt .= "MANAGED AI CHAT & REPORTING: " . A1AI_Links::PRICE_MANAGED . "\n"; $base_prompt .= "WORKSPACE - WordPress Package: " . A1AI_Links::PRICE_WORKSPACE_WORDPRESS . "\n"; $base_prompt .= "WORKSPACE - Marketing Package: " . A1AI_Links::PRICE_WORKSPACE_MARKETING . "\n"; $base_prompt .= "WORKSPACE - Full Workspace: " . A1AI_Links::PRICE_WORKSPACE_FULL . " (" . A1AI_Links::WORKSPACE_TRIAL . ")\n"; $base_prompt .= "If you are not certain of a price, send them to the pricing page instead of guessing.\n\n"; $base_prompt .= "## WHAT A1AI WORKSPACE IS (know this cold) ##\n"; $base_prompt .= "A desktop command center that connects to this WordPress site. It is the platform the free chatbot exists to feed. Modules:\n"; $base_prompt .= "- Unified Inbox: every chatbot conversation + email in one place, with live human takeover\n"; $base_prompt .= "- Built-in CRM & Contacts: chat leads become real contacts automatically, tagged and scored\n"; $base_prompt .= "- Live Knowledge Sync: crawls the whole site, AI-summarizes it, pushes authoritative answers into this chatbot\n"; $base_prompt .= "- Email & Campaigns: follow-ups triggered by chat events, campaign sending and tracking, opt-out handling\n"; $base_prompt .= "- Calendar & Booking: appointments booked in chat land on the calendar, linked to the conversation\n"; $base_prompt .= "- Call logging, Newsletter/Subscribers, Social scheduling, AI content generation, SEO health checks\n"; $base_prompt .= "- Unified reporting: chat -> lead -> email -> close in one dashboard\n"; $base_prompt .= "Integrations: Gmail, Google Calendar, Google Drive, HubSpot, ClickUp, SuiteCRM, WordPress, IMAP email.\n"; $base_prompt .= "It connects through the A1AI Assistant Workspace connector plugin over a secure REST API.\n"; $base_prompt .= "The free chatbot works fine WITHOUT Workspace - never claim otherwise. Workspace is the optional layer that closes the gap between 'someone chatted' and 'someone followed up'.\n\n"; $base_prompt .= "## TECHNICAL SUPPORT ##\n"; $base_prompt .= "For technical questions:\n"; $base_prompt .= "- Setup OpenAI API: Guide them to the 'Setup OpenAI API' quick button\n"; $base_prompt .= "- Feature explanations: Use features guide knowledge\n"; $base_prompt .= "- Troubleshooting: Get details, then suggest A1AI support if complex\n"; $base_prompt .= "- Plugin conflicts: Recommend contacting A1AI (" . A1AI_Links::EMAIL . ")\n\n"; // Add features information if available if (!empty($features_info)) { $base_prompt .= "## COMPLETE FEATURES & SERVICES KNOWLEDGE BASE ##\n"; $base_prompt .= "Use this comprehensive guide to answer questions with specific details, ROI examples, and success stories:\n\n"; $base_prompt .= $features_info . "\n\n"; } $base_prompt .= "## FINAL REMINDERS ##\n"; $base_prompt .= "- You're helping build their business, not just selling software\n"; $base_prompt .= "- A happy FREE user becomes a PREMIUM customer becomes a WORKSPACE client - play the long game\n"; $base_prompt .= "- Your goal is THEIR success, which naturally leads to A1AI's success\n"; $base_prompt .= "- Be genuinely helpful first, salesperson second\n"; $base_prompt .= "- Keep responses concise but valuable (3-5 paragraphs max unless they ask for detail)\n"; $base_prompt .= "- Use formatting (bullets, bold, numbers) for scannable responses\n"; $base_prompt .= "- Always include contact info when recommending MANAGED or discussing complex needs"; // Allow filtering of the system prompt (for easy A1AI customization) return apply_filters('a1ai_assistant_system_prompt', $base_prompt); } /** * Get features information from features.txt file. * * @since 1.4.0 * @return string Features information or empty string if file not found. */ private function get_features_info() { $features_file = plugin_dir_path(dirname(__FILE__)) . 'features.txt'; if (file_exists($features_file)) { $content = file_get_contents($features_file); return $content ?: ''; } return ''; } /** * Get conversation context for the assistant. * * @since 1.4.0 * @param string $conversation_id Conversation identifier. * @return array Conversation context. */ private function get_assistant_conversation_context($conversation_id) { // Get recent conversation history for context $context = get_transient('a1ai_assistant_context_' . $conversation_id); return $context ? $context : array(); } /** * Store assistant conversation for context. * * @since 1.4.0 * @param string $conversation_id Conversation identifier. * @param string $user_message User's message. * @param string $ai_response AI's response. */ private function store_assistant_conversation($conversation_id, $user_message, $ai_response) { $context = $this->get_assistant_conversation_context($conversation_id); // Add new messages to context $context[] = array( 'role' => 'user', 'content' => $user_message ); $context[] = array( 'role' => 'assistant', 'content' => $ai_response ); // Keep only last 10 messages for context (to manage token usage) if (count($context) > 10) { $context = array_slice($context, -10); } // Store for 1 hour set_transient('a1ai_assistant_context_' . $conversation_id, $context, HOUR_IN_SECONDS); } /** * Add contact requests submenu page. * * @since 1.4.0 */ public function add_contact_requests_submenu() { add_submenu_page( 'a1ai', __('Contact Requests', 'a1ai-chatbot'), __('Contact Requests', 'a1ai-chatbot'), 'manage_options', 'a1ai-contact-requests', array($this, 'display_contact_requests_page') ); } /** * Display contact requests page. * * @since 1.4.0 */ public function display_contact_requests_page() { // Check if upgrade plugin is active (this is a premium feature) if (!function_exists('is_plugin_active')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if (!class_exists('A1AI_Upgrade_Helper') || !A1AI_Upgrade_Helper::is_upgrade_active()) { ?>


' . esc_html__('Status updated successfully!', 'a1ai-chatbot') . '

'; } } } // Handle deletion if (isset($_POST['action']) && $_POST['action'] === 'delete_request' && isset($_POST['request_id'])) { $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; if (wp_verify_nonce($nonce, 'a1ai_delete_contact_request')) { $request_id = intval($_POST['request_id']); if (isset($contact_requests[$request_id])) { unset($contact_requests[$request_id]); update_option('a1ai_contact_requests', array_values($contact_requests)); echo '

' . esc_html__('Request deleted successfully!', 'a1ai-chatbot') . '

'; } } } // Refresh the list after updates $contact_requests = get_option('a1ai_contact_requests', array()); // Get reviews to check if contact requests are actually reviews $reviews = get_option('a1ai_reviews', array()); // Build a lookup map: email + timestamp -> review data $review_lookup = array(); foreach ($reviews as $review) { $key = strtolower($review['reviewer_email']) . '_' . strtotime($review['date']); $review_lookup[$key] = $review; } ?>

$request) : // Check if this is actually a review (empty phone/message but has name/email) $is_review = false; $review_data = null; // Check if it looks like a review (no phone, no message, but has name/email) if (empty($request['phone']) && empty($request['message']) && !empty($request['name']) && !empty($request['email'])) { // Try to find matching review $request_timestamp = strtotime($request['timestamp']); $email_key = strtolower($request['email']); // Check for reviews within 5 minutes of this request foreach ($reviews as $review) { $review_timestamp = strtotime($review['date']); $time_diff = abs($request_timestamp - $review_timestamp); if (strtolower($review['reviewer_email']) === $email_key && strtolower($review['reviewer_name']) === strtolower($request['name']) && $time_diff < 300) { // Within 5 minutes $is_review = true; $review_data = $review; break; } } } // Skip reviews - they should only appear in Reviews section if ($is_review) { continue; } ?>
__('This is a premium feature. Please upgrade to access this functionality and ensure you have a valid license key activated.', 'a1ai-chatbot'))); } // Verify nonce if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'a1ai_get_conversation')) { wp_send_json_error(array('message' => __('Security check failed.', 'a1ai-chatbot'))); } // Check user permissions if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions.', 'a1ai-chatbot'))); } $request_id = isset($_POST['request_id']) ? intval($_POST['request_id']) : 0; $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; $timestamp = isset($_POST['timestamp']) ? sanitize_text_field(wp_unslash($_POST['timestamp'])) : ''; $conversation_id = isset($_POST['conversation_id']) ? sanitize_text_field(wp_unslash($_POST['conversation_id'])) : ''; global $wpdb; // If we have a conversation_id, try to find it directly first $messages = array(); if (!empty($conversation_id)) { // Look for conversation by session_id (conversation_id from the assistant) // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $conversation = $wpdb->get_row($wpdb->prepare( "SELECT id FROM {$wpdb->prefix}a1ai_conversations WHERE session_id = %s ORDER BY created_at DESC LIMIT 1", $conversation_id )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ($conversation) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $messages = $wpdb->get_results($wpdb->prepare( "SELECT conversation_id, role, content, created_at, tokens_used FROM {$wpdb->prefix}a1ai_messages WHERE conversation_id = %d ORDER BY created_at ASC", $conversation->id )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } } // If no messages found and we have a timestamp, try to find conversation(s) by time window if (empty($messages) && !empty($timestamp)) { // We'll look for conversations around the same time period (within 1 hour) $time_window_start = gmdate('Y-m-d H:i:s', strtotime($timestamp) - 3600); // 1 hour before $time_window_end = gmdate('Y-m-d H:i:s', strtotime($timestamp) + 3600); // 1 hour after // Search for conversations in the time window $cache_key = 'a1ai_conversations_' . md5($time_window_start . '_' . $time_window_end); $conversations = wp_cache_get($cache_key, 'a1ai_conversations'); if (false === $conversations) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $conversations = $wpdb->get_results($wpdb->prepare( "SELECT id, session_id, created_at FROM {$wpdb->prefix}a1ai_conversations WHERE created_at BETWEEN %s AND %s ORDER BY created_at ASC", $time_window_start, $time_window_end )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // Cache for 5 minutes wp_cache_set($cache_key, $conversations, 'a1ai_conversations', 300); } if (!empty($conversations)) { // Get messages from all conversations in this time window $conversation_ids = array_column($conversations, 'id'); $cache_key = 'a1ai_messages_' . md5(implode(',', $conversation_ids)); $messages = wp_cache_get($cache_key, 'a1ai_messages'); if (false === $messages) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // Sanitize conversation IDs $conversation_ids = array_map('intval', $conversation_ids); if (count($conversation_ids) === 1) { $messages = $wpdb->get_results($wpdb->prepare( "SELECT conversation_id, role, content, created_at, tokens_used FROM {$wpdb->prefix}a1ai_messages WHERE conversation_id = %d ORDER BY conversation_id, created_at ASC", $conversation_ids[0] )); } elseif (!empty($conversation_ids)) { $placeholders = implode(',', array_fill(0, count($conversation_ids), '%d')); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $messages = $wpdb->get_results($wpdb->prepare( "SELECT conversation_id, role, content, created_at, tokens_used FROM {$wpdb->prefix}a1ai_messages WHERE conversation_id IN ($placeholders) ORDER BY conversation_id, created_at ASC", ...$conversation_ids )); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // Cache for 5 minutes wp_cache_set($cache_key, $messages, 'a1ai_messages', 300); } } // If no messages found, try a broader search by looking for any conversations around that time if (empty($messages)) { $time_window_start = gmdate('Y-m-d H:i:s', strtotime($timestamp) - 7200); // 2 hours before $time_window_end = gmdate('Y-m-d H:i:s', strtotime($timestamp) + 7200); // 2 hours after $cache_key = 'a1ai_conversations_broad_' . md5($time_window_start . '_' . $time_window_end); $conversations = wp_cache_get($cache_key, 'a1ai_conversations'); if (false === $conversations) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $conversations = $wpdb->get_results($wpdb->prepare( "SELECT id, session_id, created_at FROM {$wpdb->prefix}a1ai_conversations WHERE created_at BETWEEN %s AND %s ORDER BY created_at ASC LIMIT 5", $time_window_start, $time_window_end )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // Cache for 5 minutes wp_cache_set($cache_key, $conversations, 'a1ai_conversations', 300); } if (!empty($conversations)) { $conversation_ids = array_column($conversations, 'id'); $cache_key = 'a1ai_messages_broad_' . md5(implode(',', $conversation_ids)); $messages = wp_cache_get($cache_key, 'a1ai_messages'); if (false === $messages) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // Sanitize conversation IDs $conversation_ids = array_map('intval', $conversation_ids); if (count($conversation_ids) === 1) { $messages = $wpdb->get_results($wpdb->prepare( "SELECT conversation_id, role, content, created_at, tokens_used FROM {$wpdb->prefix}a1ai_messages WHERE conversation_id = %d ORDER BY conversation_id, created_at ASC", $conversation_ids[0] )); } elseif (!empty($conversation_ids)) { $placeholders = implode(',', array_fill(0, count($conversation_ids), '%d')); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $messages = $wpdb->get_results($wpdb->prepare( "SELECT conversation_id, role, content, created_at, tokens_used FROM {$wpdb->prefix}a1ai_messages WHERE conversation_id IN ($placeholders) ORDER BY conversation_id, created_at ASC", ...$conversation_ids )); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // Cache for 5 minutes wp_cache_set($cache_key, $messages, 'a1ai_messages', 300); } } } } // Format messages for display $formatted_messages = array(); if (!empty($messages)) { foreach ($messages as $message) { $formatted_messages[] = array( 'role' => $message->role, 'content' => wp_kses_post($message->content), 'created_at' => $message->created_at, 'tokens_used' => $message->tokens_used ); } } // When no conversation in DB (e.g. contact form from upgrade plugin), show submission details and message if (empty($formatted_messages) && $request_id >= 0) { $contact_requests = get_option('a1ai_contact_requests', array()); $request = isset($contact_requests[ $request_id ]) ? $contact_requests[ $request_id ] : null; if ($request && ( ! empty($request['message']) || ! empty($request['submission_data']) )) { $ts = isset($request['timestamp']) ? $request['timestamp'] : current_time('mysql'); $skip_keys = array( 'action', 'a1ai_contact_form_nonce', 'http_referer', 'wp_http_referer', 'message' ); $skip_keys_like = array( 'referer' ); $lines = array(); if ( ! empty($request['submission_data']) && is_array($request['submission_data']) ) { foreach ($request['submission_data'] as $k => $v) { if ( ! is_string($v) || $v === '' || in_array( strtolower( $k ), $skip_keys, true ) ) { continue; } $skip_like = false; foreach ( $skip_keys_like as $like ) { if ( strpos( strtolower( $k ), $like ) !== false ) { $skip_like = true; break; } } if ( $skip_like ) { continue; } $label = ucfirst( str_replace( array( '_', '-' ), ' ', $k ) ); $lines[] = $label . ': ' . $v; } } if ( empty($lines) ) { if ( ! empty($request['name']) ) { $lines[] = 'Name: ' . $request['name']; } if ( ! empty($request['email']) ) { $lines[] = 'Email: ' . $request['email']; } if ( ! empty($request['phone']) ) { $lines[] = 'Phone: ' . $request['phone']; } if ( ! empty($request['company']) ) { $lines[] = 'Company: ' . $request['company']; } if ( ! empty($request['subject']) ) { $lines[] = 'Subject: ' . $request['subject']; } } if ( ! empty($lines) ) { $formatted_messages[] = array( 'role' => 'assistant', 'content' => wp_kses_post( __( 'Form details', 'a1ai-chatbot' ) . "\n\n" . implode( "\n", $lines ) ), 'created_at' => $ts, 'tokens_used' => 0, ); } if ( ! empty($request['message']) ) { $chat_content = trim( $request['message'] ); $conversation_turns = array(); // Split on newline when followed by User:/Support:/Assistant: so we get one segment per turn; multi-line messages stay together $segments = preg_split( '/\r?\n(?=\s*(?:User|Support|Assistant)\s*:)/iu', $chat_content ); foreach ( $segments as $seg ) { $seg = trim( $seg ); if ( $seg === '' ) { continue; } if ( preg_match( '/^\s*(User|Support|Assistant)\s*:\s*(.*)/ius', $seg, $turn_match ) ) { $label = $turn_match[1]; $content = trim( $turn_match[2] ); $role = ( stripos( $label, 'User' ) !== false ) ? 'user' : 'assistant'; if ( $content !== '' ) { $conversation_turns[] = array( 'role' => $role, 'content' => $content ); } } } if ( ! empty( $conversation_turns ) ) { foreach ( $conversation_turns as $turn ) { $formatted_messages[] = array( 'role' => $turn['role'], 'content' => wp_kses_post( $turn['content'] ), 'created_at' => $ts, 'tokens_used' => 0, ); } } else { // Fallback: try single-line pattern (User: x Support: y) or show as one block if ( preg_match_all( '/(?:^|\n)\s*(User|Support|Assistant)\s*:\s*([\s\S]*?)(?=(?:\r?\n)\s*(?:User|Support|Assistant)\s*:\s*|$)/iu', $chat_content, $m, PREG_SET_ORDER ) && ! empty( $m ) ) { foreach ( $m as $turn ) { $role = ( stripos( $turn[1], 'User' ) !== false ) ? 'user' : 'assistant'; $content = trim( $turn[2] ); if ( $content !== '' ) { $formatted_messages[] = array( 'role' => $role, 'content' => wp_kses_post( $content ), 'created_at' => $ts, 'tokens_used' => 0, ); } } } else { $formatted_messages[] = array( 'role' => 'assistant', 'content' => wp_kses_post( __( 'Conversation', 'a1ai-chatbot' ) . "\n\n" . $chat_content ), 'created_at' => $ts, 'tokens_used' => 0, ); } } } } } $payload = array( 'messages' => $formatted_messages ); if ( ! empty($messages) && isset( $conversations ) ) { $payload['conversation_count'] = count( $conversations ); } if ( isset( $time_window_start ) && isset( $time_window_end ) ) { $payload['search_window'] = array( 'start' => $time_window_start, 'end' => $time_window_end ); } wp_send_json_success( $payload ); } /** * Test email functionality. * * @since 1.4.0 */ private function test_email() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() $to = isset($_POST['test_email_to']) ? sanitize_email(wp_unslash($_POST['test_email_to'])) : ''; $subject = isset($_POST['test_email_subject']) ? sanitize_text_field(wp_unslash($_POST['test_email_subject'])) : 'Test Email from A1AI Chatbot'; $body = isset($_POST['test_email_body']) ? sanitize_textarea_field(wp_unslash($_POST['test_email_body'])) : 'This is a test email from the A1AI Chatbot plugin.'; if (empty($to)) { wp_send_json_error(array('message' => __('Please enter an email address to send the test email to.', 'a1ai-chatbot'))); } if (empty($subject)) { wp_send_json_error(array('message' => __('Please enter an email subject.', 'a1ai-chatbot'))); } if (empty($body)) { wp_send_json_error(array('message' => __('Please enter an email body.', 'a1ai-chatbot'))); } $headers = array( 'Content-Type: text/plain; charset=UTF-8', 'From: ' . get_option('blogname') . ' <' . get_option('admin_email') . '>' ); if (wp_mail($to, $subject, $body, $headers)) { // translators: %s: Email address the test email was sent to wp_send_json_success(array('message' => sprintf(__('Test email sent successfully to %s!', 'a1ai-chatbot'), $to))); } else { // translators: %s: Email address the test email failed to send to wp_send_json_error(array('message' => sprintf(__('Failed to send test email to %s. Please check your email settings and try again.', 'a1ai-chatbot'), $to))); } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Get export data for analytics. * * @since 1.5.0 */ private function get_export_data() { // Nonce and capabilities already verified in handle_admin_ajax() global $wpdb; // Get all daily stats for the past year // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $daily_stats = $wpdb->get_results( "SELECT DATE(created_at) as date, COUNT(DISTINCT conversation_id) as conversation_count, COUNT(*) as message_count, SUM(tokens_used) as tokens_used FROM {$wpdb->prefix}a1ai_messages WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 YEAR) GROUP BY DATE(created_at) ORDER BY date ASC", ARRAY_A ); // Get chatbot breakdown $chatbot_stats = $wpdb->get_results( "SELECT c.chatbot_id, cb.name as chatbot_name, COUNT(DISTINCT c.id) as conversation_count, COUNT(m.id) as message_count, SUM(m.tokens_used) as tokens_used FROM {$wpdb->prefix}a1ai_conversations c LEFT JOIN {$wpdb->prefix}a1ai_chatbots cb ON c.chatbot_id = cb.id LEFT JOIN {$wpdb->prefix}a1ai_messages m ON c.id = m.conversation_id WHERE c.created_at >= DATE_SUB(NOW(), INTERVAL 1 YEAR) GROUP BY c.chatbot_id, cb.name ORDER BY message_count DESC", ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $export_data = array( 'daily_stats' => $daily_stats, 'chatbot_stats' => $chatbot_stats, 'export_date' => current_time('Y-m-d H:i:s'), 'site_url' => get_site_url() ); wp_send_json_success($export_data); } /** * Get trends data for date range chart. * * @since 1.5.0 */ private function get_trends_data() { // Nonce and capabilities already verified in handle_admin_ajax() // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_admin_ajax() global $wpdb; $date_from = isset($_POST['date_from']) ? sanitize_text_field(wp_unslash($_POST['date_from'])) : gmdate('Y-m-d', strtotime('-30 days')); $date_to = isset($_POST['date_to']) ? sanitize_text_field(wp_unslash($_POST['date_to'])) : gmdate('Y-m-d'); $period = isset($_POST['period']) ? sanitize_text_field(wp_unslash($_POST['period'])) : 'week'; // Set date format based on period $date_format = '%Y-%m-%d'; $group_by = 'DATE(created_at)'; switch ($period) { case 'month': $date_format = '%Y-%m'; $group_by = 'DATE_FORMAT(created_at, \'%Y-%m\')'; break; case 'week': $date_format = '%Y-W%u'; $group_by = 'YEARWEEK(created_at, 1)'; break; case 'day': default: $date_format = '%Y-%m-%d'; $group_by = 'DATE(created_at)'; break; } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $results = $wpdb->get_results($wpdb->prepare( "SELECT DATE_FORMAT(created_at, %s) as date_label, COUNT(DISTINCT conversation_id) as conversations, COUNT(*) as messages, SUM(tokens_used) as tokens FROM {$wpdb->prefix}a1ai_messages WHERE DATE(created_at) BETWEEN %s AND %s GROUP BY $group_by ORDER BY created_at ASC", $date_format, $date_from, $date_to ), ARRAY_A); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $labels = array(); $messages = array(); $conversations = array(); $tokens = array(); foreach ($results as $row) { $labels[] = $row['date_label']; $messages[] = intval($row['messages']); $conversations[] = intval($row['conversations']); $tokens[] = intval($row['tokens']); } // phpcs:enable WordPress.Security.NonceVerification.Missing wp_send_json_success(array( 'labels' => $labels, 'messages' => $messages, 'conversations' => $conversations, 'tokens' => $tokens )); } }