version = $version;
}
// -----------------------------------------------------------------------
// Asset enqueuing
// -----------------------------------------------------------------------
/**
* Enqueue admin stylesheets (only on this plugin's pages).
*
* @param string $hook_suffix Current admin page hook.
*/
public function enqueue_styles( $hook_suffix ) {
if ( ! $this->is_plugin_page( $hook_suffix ) ) {
return;
}
// No Bootstrap CSS – all styles are custom-scoped to avoid WP admin conflicts.
wp_enqueue_style(
'ai-chatbot-admin',
AI_CHATBOT_URL . 'admin/css/ai-chatbot-admin.css',
array(),
$this->version
);
}
/**
* Enqueue admin scripts (only on this plugin's pages).
*
* @param string $hook_suffix Current admin page hook.
*/
public function enqueue_scripts( $hook_suffix ) {
if ( ! $this->is_plugin_page( $hook_suffix ) ) {
return;
}
// Bootstrap JS is no longer needed – tabs and modals use custom JS.
wp_enqueue_script(
'ai-chatbot-admin',
AI_CHATBOT_URL . 'admin/js/ai-chatbot-admin.js',
array( 'jquery', 'wp-i18n' ),
$this->version,
true
);
// Deliver JS translations from the languages/ folder.
wp_set_script_translations(
'ai-chatbot-admin',
AI_CHATBOT_DOMAIN,
AI_CHATBOT_PATH . 'languages'
);
wp_enqueue_script(
'chartjs',
'https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js',
array(),
'4.4.3',
true
);
wp_localize_script( 'ai-chatbot-admin', 'aiChatbotAdmin', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ai_chatbot_nonce' ),
'pluginUrl' => AI_CHATBOT_URL,
'siteUrl' => get_site_url(),
) );
}
/**
* Check whether the current admin page belongs to this plugin.
*
* @param string $hook_suffix
* @return bool
*/
private function is_plugin_page( $hook_suffix ) {
$plugin_pages = array(
'toplevel_page_ai-chatbot',
'ai-chatbot_page_ai-chatbot-stats',
'admin_page_ai-chatbot-agent', // null-parent hidden page hook suffix
);
return in_array( $hook_suffix, $plugin_pages, true );
}
// -----------------------------------------------------------------------
// Menu registration
// -----------------------------------------------------------------------
/**
* Register the top-level admin menu item.
*/
public function register_menu() {
add_menu_page(
esc_html( PLUGIN_NAME ),
esc_html( PLUGIN_NAME ),
'manage_options',
'ai-chatbot',
array( $this, 'render_main_page' ),
'dashicons-format-chat',
80
);
// Hidden page: Detailed Agent Management View (Phase 2).
// Using null parent is the WordPress-recommended way to register a page
// that is routable via ?page= but never shown in the sidebar menu.
// This avoids the remove_submenu_page() pattern which breaks get_admin_page_parent()
// and causes "Sorry, you are not allowed to access this page." errors.
add_submenu_page(
null, // no parent → hidden from all menus
esc_html__( 'Manage Agent', AI_CHATBOT_DOMAIN ),
esc_html__( 'Manage Agent', AI_CHATBOT_DOMAIN ),
'manage_options',
'ai-chatbot-agent',
array( $this, 'render_agent_page' )
);
}
// -----------------------------------------------------------------------
// State machine helpers
// -----------------------------------------------------------------------
/**
* Determine the current plugin state.
*
* State 1 – Not authenticated (no cookie saved).
* State 2 – Authenticated but no project ID yet.
* State 3 – Authenticated and project ID saved.
*
* @return int 1, 2, or 3.
*/
private function get_state() {
if ( ! Ai_Chatbot_API::get_saved_cookie() ) {
return 1;
}
if ( ! get_option( Ai_Chatbot_API::opt_project_id() ) ) {
return 2;
}
return 3;
}
/**
* Ensure the project is provisioned (State 2 handler).
*
* Calls GET /v1/projects; if empty calls POST /v1/projects.
* Saves the resulting project_id to the database.
*
* @return string|WP_Error Project ID on success.
*/
private function ensure_project() {
$projects = Ai_Chatbot_API::get_projects();
if ( is_wp_error( $projects ) ) {
return $projects;
}
if ( ! empty( $projects ) && isset( $projects[0]['project_id'] ) ) {
$project_id = sanitize_text_field( $projects[0]['project_id'] );
update_option( Ai_Chatbot_API::opt_project_id(), $project_id );
return $project_id;
}
// No projects – auto-create one.
$result = Ai_Chatbot_API::create_project();
if ( is_wp_error( $result ) ) {
return $result;
}
return get_option( Ai_Chatbot_API::opt_project_id() );
}
// -----------------------------------------------------------------------
// Page renderers
// -----------------------------------------------------------------------
/**
* Render the main admin page (state machine entry point).
*/
public function render_main_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions.', AI_CHATBOT_DOMAIN ) );
}
$state = $this->get_state();
if ( 1 === $state ) {
require_once AI_CHATBOT_PATH . 'admin/partials/ai-chatbot-admin-auth.php';
return;
}
// State 2: ensure project is provisioned before showing the dashboard.
if ( 2 === $state ) {
$project_id = $this->ensure_project();
if ( is_wp_error( $project_id ) ) {
echo '
' . esc_html( $project_id->get_error_message() ) . '
';
return;
}
}
// State 3: show agents.
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$agents = Ai_Chatbot_API::get_agents( $project_id );
if ( is_wp_error( $agents ) ) {
// Likely session expired and refresh failed – bounce to login.
if ( 'auth_expired' === $agents->get_error_code() ) {
Ai_Chatbot_API::clear_auth();
require_once AI_CHATBOT_PATH . 'admin/partials/ai-chatbot-admin-auth.php';
return;
}
echo '' . esc_html( $agents->get_error_message() ) . '
';
return;
}
require_once AI_CHATBOT_PATH . 'admin/partials/ai-chatbot-admin-dashboard.php';
}
/**
* Render the Detailed Agent Management page (Phase 2).
*/
public function render_agent_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions.', AI_CHATBOT_DOMAIN ) );
}
if ( ! Ai_Chatbot_API::get_saved_cookie() ) {
wp_safe_redirect( admin_url( 'admin.php?page=ai-chatbot' ) );
exit;
}
$chatbot_id = isset( $_GET['chatbot_id'] ) ? sanitize_text_field( wp_unslash( $_GET['chatbot_id'] ) ) : '';
$chatbot_name = isset( $_GET['chatbot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['chatbot_name'] ) ) : __( 'Agent', AI_CHATBOT_DOMAIN );
$active_tab = isset( $_GET['active_tab'] ) ? sanitize_text_field( wp_unslash( $_GET['active_tab'] ) ) : 'aic-agent-panel-stats';
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
if ( empty( $chatbot_id ) ) {
wp_safe_redirect( admin_url( 'admin.php?page=ai-chatbot' ) );
exit;
}
require_once AI_CHATBOT_PATH . 'admin/partials/ai-chatbot-admin-agent-view.php';
}
/**
* Render the Stats sub-page.
*/
public function render_stats_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions.', AI_CHATBOT_DOMAIN ) );
}
$chatbot_id = isset( $_GET['chatbot_id'] ) ? sanitize_text_field( wp_unslash( $_GET['chatbot_id'] ) ) : '';
$chatbot_name = isset( $_GET['chatbot_name'] ) ? sanitize_text_field( wp_unslash( $_GET['chatbot_name'] ) ) : '';
if ( empty( $chatbot_id ) ) {
wp_safe_redirect( admin_url( 'admin.php?page=ai-chatbot' ) );
exit;
}
require_once AI_CHATBOT_PATH . 'admin/partials/ai-chatbot-admin-stats.php';
}
// -----------------------------------------------------------------------
// AJAX handlers – Authentication
// -----------------------------------------------------------------------
/**
* Helper: verify nonce and return capability.
*/
private function verify_nonce() {
check_ajax_referer( 'ai_chatbot_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', AI_CHATBOT_DOMAIN ) ), 403 );
}
}
/**
* AJAX: sign up.
*/
public function ajax_signup() {
$this->verify_nonce();
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
$password = sanitize_text_field( wp_unslash( $_POST['password'] ?? '' ) );
$first_name = sanitize_text_field( wp_unslash( $_POST['first_name'] ?? '' ) );
$last_name = sanitize_text_field( wp_unslash( $_POST['last_name'] ?? '' ) );
$org_name = sanitize_text_field( wp_unslash( $_POST['org_name'] ?? '' ) );
if ( ! is_email( $email ) || empty( $password ) || empty( $first_name ) || empty( $org_name ) ) {
wp_send_json_error( array( 'message' => __( 'Please fill in all required fields.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::signup( $email, $password, $first_name, $last_name, $org_name );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'message' => __( 'Account created successfully!', AI_CHATBOT_DOMAIN ) ) );
}
/**
* AJAX: login with email + password.
*/
public function ajax_login() {
$this->verify_nonce();
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
$password = sanitize_text_field( wp_unslash( $_POST['password'] ?? '' ) );
if ( ! is_email( $email ) || empty( $password ) ) {
wp_send_json_error( array( 'message' => __( 'Email and password are required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::login( $email, $password );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'message' => __( 'Logged in successfully!', AI_CHATBOT_DOMAIN ) ) );
}
/**
* AJAX: initiate OTP login.
*/
public function ajax_otp_init() {
$this->verify_nonce();
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
if ( ! is_email( $email ) ) {
wp_send_json_error( array( 'message' => __( 'A valid email address is required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::login_otp_init( $email );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
$session = isset( $result['session'] ) ? $result['session'] : ( is_string( $result ) ? $result : '' );
wp_send_json_success( array(
'message' => __( 'OTP sent to your email.', AI_CHATBOT_DOMAIN ),
'session' => $session,
) );
}
/**
* AJAX: verify OTP.
*/
public function ajax_otp_verify() {
$this->verify_nonce();
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
$otp = sanitize_text_field( wp_unslash( $_POST['otp'] ?? '' ) );
$session = sanitize_text_field( wp_unslash( $_POST['session'] ?? '' ) );
if ( ! is_email( $email ) || empty( $otp ) || empty( $session ) ) {
wp_send_json_error( array( 'message' => __( 'Email, OTP, and session are required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::verify_otp( $email, $otp, $session );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'message' => __( 'Logged in successfully!', AI_CHATBOT_DOMAIN ) ) );
}
/**
* AJAX: logout.
*/
public function ajax_logout() {
$this->verify_nonce();
Ai_Chatbot_API::clear_auth();
wp_send_json_success( array( 'message' => __( 'Logged out.', AI_CHATBOT_DOMAIN ) ) );
}
// -----------------------------------------------------------------------
// AJAX handlers – Agents
// -----------------------------------------------------------------------
/**
* AJAX: create agent, then immediately crawl the website.
* Both operations run server-side in one request to avoid any
* browser-abort race conditions from JS page reloads.
*/
public function ajax_create_agent() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) );
$language = sanitize_text_field( wp_unslash( $_POST['language'] ?? 'en' ) );
$website_url = esc_url_raw( wp_unslash( $_POST['website_url'] ?? '' ) );
if ( empty( $name ) ) {
wp_send_json_error( array( 'message' => __( 'Agent name is required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::create_agent( $project_id, $name, $language, '' );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
// The create_agent API returns the chatbot ID as a plain string directly
// in the `data` field: { "data": "chatbot_id_value" }.
// After request() unwraps `data`, $result is that string.
// Handle both that confirmed case and any future array-based response.
if ( is_string( $result ) && '' !== $result ) {
$chatbot_id = $result;
} elseif ( is_array( $result ) ) {
$chatbot_id = '';
foreach ( array( 'id', 'chatbotId', 'chatbot_id' ) as $key ) {
if ( ! empty( $result[ $key ] ) ) {
$chatbot_id = (string) $result[ $key ];
break;
}
}
} else {
$chatbot_id = '';
}
// Crawl the website after agent creation (single attempt).
$crawl_message = '';
if ( ! empty( $chatbot_id ) && ! empty( $website_url ) ) {
$crawl = Ai_Chatbot_API::crawl_website( $project_id, $chatbot_id, $website_url );
if ( ! is_wp_error( $crawl ) ) {
/* translators: %s: website URL */
$crawl_message = sprintf( __( ' Website crawl started for %s.', AI_CHATBOT_DOMAIN ), $website_url );
}
}
wp_send_json_success( array(
'message' => __( 'Agent created successfully!', AI_CHATBOT_DOMAIN ) . $crawl_message,
'chatbot_id' => $chatbot_id,
) );
}
/**
* AJAX: update agent.
*/
public function ajax_update_agent() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) );
$description = sanitize_textarea_field( wp_unslash( $_POST['description'] ?? 'You are a helpful AI Agent' ) );
$avatar_url = esc_url_raw( wp_unslash( $_POST['avatar_url'] ?? '' ) );
if ( empty( $chatbot_id ) || empty( $name ) ) {
wp_send_json_error( array( 'message' => __( 'Agent ID and name are required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::update_agent( $project_id, $chatbot_id, $name, $description, $avatar_url );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'message' => __( 'Agent updated successfully!', AI_CHATBOT_DOMAIN ) ) );
}
/**
* AJAX: crawl website for a newly created agent.
*/
public function ajax_crawl_website() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$url = esc_url_raw( wp_unslash( $_POST['website_url'] ?? '' ) );
if ( empty( $chatbot_id ) || empty( $url ) ) {
wp_send_json_error( array( 'message' => __( 'Chatbot ID and website URL are required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::crawl_website( $project_id, $chatbot_id, $url );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array(
'message' => sprintf(
/* translators: %s: website URL */
__( 'Website crawl started for %s', AI_CHATBOT_DOMAIN ),
$url
),
) );
}
/**
* AJAX: delete agent.
*/
public function ajax_delete_agent() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) {
wp_send_json_error( array( 'message' => __( 'Agent ID is required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::delete_agent( $project_id, $chatbot_id );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'message' => __( 'Agent deleted.', AI_CHATBOT_DOMAIN ) ) );
}
/**
* AJAX: get agent stats.
*/
public function ajax_get_stats() {
$this->verify_nonce();
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$start = sanitize_text_field( wp_unslash( $_POST['start_datetime'] ?? '' ) );
$end = sanitize_text_field( wp_unslash( $_POST['end_datetime'] ?? '' ) );
$time_offset = (int) ( $_POST['time_offset'] ?? 0 );
if ( empty( $chatbot_id ) || empty( $start ) || empty( $end ) ) {
wp_send_json_error( array( 'message' => __( 'chatbot_id, start_datetime, and end_datetime are required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::get_agent_stats( $chatbot_id, $start, $end, $time_offset );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( $result );
}
// -----------------------------------------------------------------------
// AJAX handlers – Phase 2 Agent Management
// -----------------------------------------------------------------------
/** AJAX: get appearance settings. */
public function ajax_get_appearance() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::get_appearance_settings( $project_id, $chatbot_id );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( $result );
}
/** AJAX: update appearance settings. */
public function ajax_update_appearance() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
// Decode the full settings payload (sent as JSON string to preserve all fields).
// Use wp_unslash only – sanitize_text_field would strip HTML-like content within
// JSON string values and corrupt the payload before json_decode runs.
$payload_raw = wp_unslash( $_POST['payload'] ?? '{}' );
$payload = json_decode( $payload_raw, true );
if ( ! is_array( $payload ) ) { wp_send_json_error( array( 'message' => __( 'Invalid payload.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::update_appearance_settings( $project_id, $chatbot_id, $payload );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( array( 'message' => __( 'Appearance settings saved!', AI_CHATBOT_DOMAIN ) ) );
}
// -----------------------------------------------------------------------
// Widget Tabs (Tab Integrations)
// -----------------------------------------------------------------------
/** AJAX: load widget integrations for an agent. */
public function ajax_get_widget_tabs() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::get_appearance_settings( $project_id, $chatbot_id );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
$integrations = isset( $result['widget_integrations'] ) && is_array( $result['widget_integrations'] )
? $result['widget_integrations']
: array();
wp_send_json_success( $integrations );
}
/** AJAX: save widget integrations for an agent (fetch-merge-put strategy). */
public function ajax_save_widget_tabs() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$raw = wp_unslash( $_POST['integrations'] ?? '[]' );
$integrations = json_decode( $raw, true );
if ( ! is_array( $integrations ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid integrations payload.', AI_CHATBOT_DOMAIN ) ) );
}
if ( count( $integrations ) > 5 ) {
wp_send_json_error( array( 'message' => __( 'Maximum 5 integrations allowed.', AI_CHATBOT_DOMAIN ) ) );
}
$allowed_providers = array( 'calendly', 'cal', 'google-calendar', 'whatsapp', 'linkedin', 'twitter', 'tiktok', 'instagram', 'facebook', 'youtube', 'custom', 'other' );
$allowed_icons = array( 'calendar', 'phone', 'chat', 'globe', 'link', 'social', 'mail', 'star' );
$allowed_types = array( 'iframe', 'link', 'whatsapp', 'social' );
$allowed_methods = array( 'url', 'code' );
$allowed_platforms = array( 'linkedin', 'twitter', 'instagram', 'tiktok', 'facebook', 'youtube', 'other', '' );
$allowed_embed_html = array(
'iframe' => array( 'src' => true, 'width' => true, 'height' => true, 'frameborder' => true, 'allowfullscreen' => true, 'allow' => true, 'style' => true, 'class' => true, 'title' => true, 'referrerpolicy' => true, 'sandbox' => true, 'loading' => true ),
'script' => array( 'src' => true, 'type' => true, 'async' => true, 'defer' => true ),
'div' => array( 'id' => true, 'class' => true, 'style' => true ),
);
// These fields are optional but enforce a 1-N character range when
// provided, so an empty string fails validation while null does not
// (same partial-update contract as the appearance settings endpoint).
$null_if_blank = function( $value ) {
return ( '' === trim( (string) $value ) ) ? null : $value;
};
$sanitized = array();
foreach ( $integrations as $item ) {
if ( ! is_array( $item ) ) { continue; }
$type = sanitize_text_field( $item['integration_type'] ?? 'link' );
if ( ! in_array( $type, $allowed_types, true ) ) { $type = 'link'; }
$provider = sanitize_text_field( $item['integration_provider'] ?? 'custom' );
if ( ! in_array( $provider, $allowed_providers, true ) ) { $provider = 'custom'; }
$icon = sanitize_text_field( $item['tab_icon'] ?? 'globe' );
if ( ! in_array( $icon, $allowed_icons, true ) ) { $icon = 'globe'; }
$method = sanitize_text_field( $item['embed_method'] ?? 'url' );
if ( ! in_array( $method, $allowed_methods, true ) ) { $method = 'url'; }
$platform = sanitize_text_field( $item['social_platform'] ?? '' );
if ( ! in_array( $platform, $allowed_platforms, true ) ) { $platform = 'other'; }
// Server requires 7-15 digits, digits only.
$whatsapp_number = preg_replace( '/\D/', '', (string) ( $item['whatsapp_number'] ?? '' ) );
if ( strlen( $whatsapp_number ) < 7 || strlen( $whatsapp_number ) > 15 ) {
$whatsapp_number = '';
}
$sanitized[] = array(
'tab_name' => $null_if_blank( sanitize_text_field( $item['tab_name'] ?? '' ) ),
'tab_icon' => $icon,
'header_title' => $null_if_blank( sanitize_text_field( $item['header_title'] ?? '' ) ),
'header_sub_title' => $null_if_blank( sanitize_text_field( $item['header_sub_title'] ?? '' ) ),
'integration_type' => $type,
'is_enable' => ! empty( $item['is_enable'] ),
'integration_provider' => $provider,
'embed_method' => $method,
'url' => $null_if_blank( esc_url_raw( $item['url'] ?? '' ) ),
'embed_code' => $null_if_blank( wp_kses( $item['embed_code'] ?? '', $allowed_embed_html ) ),
'button_text' => $null_if_blank( sanitize_text_field( $item['button_text'] ?? '' ) ),
'open_in_new_tab' => ! empty( $item['open_in_new_tab'] ),
'whatsapp_number' => $null_if_blank( $whatsapp_number ),
'prefilled_message' => $null_if_blank( sanitize_textarea_field( $item['prefilled_message'] ?? '' ) ),
'social_platform' => $platform,
);
}
// Fetch-merge-PUT: preserve all other appearance fields.
$current = Ai_Chatbot_API::get_appearance_settings( $project_id, $chatbot_id );
if ( is_wp_error( $current ) ) { wp_send_json_error( array( 'message' => $current->get_error_message() ) ); }
if ( ! is_array( $current ) ) { $current = array(); }
$current['widget_integrations'] = $sanitized;
$result = Ai_Chatbot_API::update_appearance_settings( $project_id, $chatbot_id, $current );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( array( 'message' => __( 'Widget tabs saved successfully!', AI_CHATBOT_DOMAIN ) ) );
}
// -----------------------------------------------------------------------
// WooCommerce Integration
// -----------------------------------------------------------------------
/**
* AJAX: get WooCommerce integration status for the current agent.
*
* Checks the agent's tool linkages (per-chatbot) rather than the project-level
* credentials so the status is accurate per chatbot.
*/
public function ajax_get_woocommerce_status() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
if ( empty( $project_id ) ) {
wp_send_json_error( array( 'message' => __( 'Project not found.', AI_CHATBOT_DOMAIN ) ) );
}
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) {
wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) );
}
$linkages = Ai_Chatbot_API::get_tool_linkages( $project_id, $chatbot_id );
if ( is_wp_error( $linkages ) ) {
wp_send_json_error( array( 'message' => $linkages->get_error_message() ) );
}
$wc_linkage = null;
if ( is_array( $linkages ) ) {
foreach ( $linkages as $link ) {
if ( isset( $link['integration_id'] ) && 1 === (int) $link['integration_id'] ) {
$wc_linkage = $link;
break;
}
}
}
wp_send_json_success( array(
'enabled' => ! empty( $wc_linkage ),
) );
}
/**
* AJAX: enable or disable WooCommerce integration for the current agent.
*
* On every call (enable or disable) existing linkages and credentials for
* integration_id = 1 are cleaned up first. If enabling, fresh WooCommerce
* API credentials are generated, stored in the backend, and linked to the
* agent. If anything fails during enable the rollback path cleans up.
*/
public function ajax_toggle_woocommerce() {
$this->verify_nonce();
global $wpdb;
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
if ( empty( $project_id ) ) {
wp_send_json_error( array( 'message' => __( 'Project not found.', AI_CHATBOT_DOMAIN ) ) );
}
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) {
wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) );
}
$enable = ( isset( $_POST['enable'] ) && 'true' === sanitize_text_field( wp_unslash( $_POST['enable'] ) ) );
// ── Step 1: Remove any existing WooCommerce linkage for this agent ──────
$linkages = Ai_Chatbot_API::get_tool_linkages( $project_id, $chatbot_id );
if ( ! is_wp_error( $linkages ) && is_array( $linkages ) ) {
foreach ( $linkages as $link ) {
if ( isset( $link['integration_id'] ) && 1 === (int) $link['integration_id'] && isset( $link['linkage_id'] ) ) {
Ai_Chatbot_API::unlink_tool_credential( $project_id, $chatbot_id, $link['linkage_id'] );
}
}
}
// ── Step 2: Remove any existing WooCommerce credential for this project ─
$credentials = Ai_Chatbot_API::get_tool_credentials( $project_id );
if ( ! is_wp_error( $credentials ) && is_array( $credentials ) ) {
foreach ( $credentials as $cred ) {
if ( isset( $cred['integration_id'] ) && 1 === (int) $cred['integration_id'] && isset( $cred['id'] ) ) {
Ai_Chatbot_API::delete_tool_credential( $project_id, $cred['id'] );
}
}
}
// ── Step 3: Remove the local WooCommerce API key (if one was stored) ────
$existing_key_id = get_option( PLUGIN_SLUG . '_wc_key_id' );
if ( $existing_key_id ) {
$wpdb->delete(
$wpdb->prefix . 'woocommerce_api_keys',
array( 'key_id' => (int) $existing_key_id ),
array( '%d' )
);
}
delete_option( PLUGIN_SLUG . '_wc_key_id' );
// If disabling, we are done.
if ( ! $enable ) {
wp_send_json_success( array(
'message' => __( 'WooCommerce integration disabled.', AI_CHATBOT_DOMAIN ),
'enabled' => false,
) );
}
// ── Step 4: Verify WooCommerce is active before generating credentials ──
if ( ! class_exists( 'WooCommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'WooCommerce is not active on this site.', AI_CHATBOT_DOMAIN ) ) );
}
// ── Step 5: Generate fresh WooCommerce REST API credentials ─────────────
$consumer_key = 'ck_' . wc_rand_hash();
$consumer_secret = 'cs_' . wc_rand_hash();
$store_url = get_site_url();
$inserted = $wpdb->insert(
$wpdb->prefix . 'woocommerce_api_keys',
array(
'user_id' => get_current_user_id(),
'description' => 'AI Chatbot WooCommerce Integration',
'permissions' => 'read',
'consumer_key' => wc_api_hash( $consumer_key ),
'consumer_secret' => $consumer_secret,
'truncated_key' => substr( $consumer_key, -7 ),
),
array( '%d', '%s', '%s', '%s', '%s', '%s' )
);
if ( ! $inserted ) {
wp_send_json_error( array( 'message' => __( 'Failed to generate WooCommerce API credentials.', AI_CHATBOT_DOMAIN ) ) );
}
$key_id = $wpdb->insert_id;
update_option( PLUGIN_SLUG . '_wc_key_id', $key_id );
// ── Step 6: Register the credentials with the AI backend ─────────────────
$auth_data = array(
'store_url' => $store_url,
'consumer_key' => $consumer_key,
'consumer_secret' => $consumer_secret,
);
$cred_result = Ai_Chatbot_API::create_tool_credential(
$project_id,
1,
'basic',
wp_json_encode( $auth_data ),
'woocommerce store credentials'
);
if ( is_wp_error( $cred_result ) ) {
// Rollback: remove the WooCommerce key we just created.
$wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) );
delete_option( PLUGIN_SLUG . '_wc_key_id' );
wp_send_json_error( array( 'message' => $cred_result->get_error_message() ) );
}
// The POST response may return the credential ID under different field names
// depending on the API version. Try the most common variants.
$credential_id = null;
foreach ( array( 'id', 'credential_id', '_id' ) as $_key ) {
if ( ! empty( $cred_result[ $_key ] ) ) {
$credential_id = $cred_result[ $_key ];
break;
}
}
// If the create response didn't include the ID, re-fetch the credentials list
// and locate the newly-created WooCommerce credential by integration_id.
// This handles API versions that return a minimal or differently-shaped response.
if ( empty( $credential_id ) ) {
$fresh_creds = Ai_Chatbot_API::get_tool_credentials( $project_id );
if ( ! is_wp_error( $fresh_creds ) && is_array( $fresh_creds ) ) {
foreach ( $fresh_creds as $c ) {
if ( isset( $c['integration_id'] ) && 1 === (int) $c['integration_id'] ) {
foreach ( array( 'id', 'credential_id', '_id' ) as $_key ) {
if ( ! empty( $c[ $_key ] ) ) {
$credential_id = $c[ $_key ];
break 2;
}
}
}
}
}
}
// If we still can't resolve the credential ID after all attempts, clean up and bail.
if ( empty( $credential_id ) ) {
$wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) );
delete_option( PLUGIN_SLUG . '_wc_key_id' );
wp_send_json_error( array( 'message' => __( 'Could not retrieve credential ID from API response.', AI_CHATBOT_DOMAIN ) ) );
}
// ── Step 7: Link the credential to this agent ─────────────────────────────
$link_result = Ai_Chatbot_API::link_tool_credential( $project_id, $chatbot_id, 1, $credential_id );
if ( is_wp_error( $link_result ) ) {
// Rollback: delete credential and WooCommerce key.
Ai_Chatbot_API::delete_tool_credential( $project_id, $credential_id );
$wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) );
delete_option( PLUGIN_SLUG . '_wc_key_id' );
wp_send_json_error( array( 'message' => $link_result->get_error_message() ) );
}
wp_send_json_success( array(
'message' => __( 'WooCommerce integration enabled successfully!', AI_CHATBOT_DOMAIN ),
'enabled' => true,
) );
}
/** AJAX: get AI settings. */
public function ajax_get_ai_settings() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::get_ai_settings( $project_id, $chatbot_id );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( $result );
}
/** AJAX: update AI settings. */
public function ajax_update_ai_settings() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
// Use wp_unslash only - sanitize_text_field would strip HTML-like
// content and collapse newlines within multi-line values (e.g.
// system_prompt_text) before json_decode runs, corrupting the payload.
$payload_raw = wp_unslash( $_POST['payload'] ?? '{}' );
$payload = json_decode( $payload_raw, true );
if ( ! is_array( $payload ) ) { wp_send_json_error( array( 'message' => __( 'Invalid payload.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::update_ai_settings( $project_id, $chatbot_id, $payload );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( array( 'message' => __( 'AI settings saved!', AI_CHATBOT_DOMAIN ) ) );
}
/** AJAX: get knowledge-base content (paginated). */
public function ajax_get_kb() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$page_number = max( 1, (int) ( $_POST['page_number'] ?? 1 ) );
$page_size = max( 1, min( 50, (int) ( $_POST['page_size'] ?? 10 ) ) );
$asset_type = sanitize_text_field( wp_unslash( $_POST['assetType'] ?? 'WebsiteURL' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::get_linked_content( $project_id, $chatbot_id, $page_number, $page_size, $asset_type );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( $result );
}
/** AJAX: link content to agent (add to knowledge base). */
public function ajax_link_content() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$content = sanitize_textarea_field( wp_unslash( $_POST['content'] ?? '' ) );
$file_name = sanitize_text_field( wp_unslash( $_POST['file_name'] ?? '' ) );
if ( empty( $chatbot_id ) || empty( $content ) || empty( $file_name ) ) {
wp_send_json_error( array( 'message' => __( 'Agent ID, content, and file name are required.', AI_CHATBOT_DOMAIN ) ) );
}
$result = Ai_Chatbot_API::link_content( $project_id, $chatbot_id, $content, $file_name );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( array( 'message' => __( 'Content added to knowledge base!', AI_CHATBOT_DOMAIN ), 'item' => $result ) );
}
/** AJAX: delink content from agent (remove from knowledge base). */
public function ajax_delink_content() {
$this->verify_nonce();
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$linkage_id = (int) ( $_POST['linkage_id'] ?? 0 );
$asset_id = (int) ( $_POST['asset_id'] ?? 0 );
$asset_type = sanitize_text_field( wp_unslash( $_POST['asset_type'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::delink_content( $linkage_id, $chatbot_id, $asset_id, $asset_type );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
$delinked = ( true === $result ) || ( isset( $result['is_delinked'] ) && $result['is_delinked'] );
if ( ! $delinked ) { wp_send_json_error( array( 'message' => __( 'Failed to remove content.', AI_CHATBOT_DOMAIN ) ) ); }
wp_send_json_success( array( 'message' => __( 'Content removed from knowledge base.', AI_CHATBOT_DOMAIN ) ) );
}
// -----------------------------------------------------------------------
// AJAX handler – Link a WordPress post/page/product to the knowledge base
// -----------------------------------------------------------------------
/**
* Extract all public content from a WordPress post and send it to the
* Robofy knowledge-base API.
*
* POST params: chatbot_id, post_id
*/
public function ajax_link_wp_post() {
$this->verify_nonce();
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$post_id = (int) ( $_POST['post_id'] ?? 0 );
if ( empty( $chatbot_id ) || $post_id <= 0 ) {
wp_send_json_error( array( 'message' => __( 'Agent ID and post ID are required.', AI_CHATBOT_DOMAIN ) ) );
}
$post = get_post( $post_id );
if ( ! $post || 'publish' !== $post->post_status ) {
wp_send_json_error( array( 'message' => __( 'Post not found or not published.', AI_CHATBOT_DOMAIN ) ) );
}
$content = $this->build_post_content( $post );
if ( empty( $content ) ) {
wp_send_json_error( array( 'message' => __( 'No extractable content found for this post.', AI_CHATBOT_DOMAIN ) ) );
}
$file_name = get_permalink( $post_id );
if ( empty( $file_name ) ) {
$file_name = get_the_title( $post_id );
if ( empty( $file_name ) ) {
$file_name = (string) $post_id;
}
}
$result = Ai_Chatbot_API::link_content( $project_id, $chatbot_id, $content, $file_name );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
// Persist the linkage so the auto-refresh hook can update it on future saves.
$existing = get_post_meta( $post_id, '_ai_chatbot_kb_linkage', true );
if ( ! is_array( $existing ) ) {
$existing = array();
}
// If this chatbot was linked before, replace its old entry (handles re-linking).
$existing = array_values( array_filter( $existing, function ( $e ) use ( $chatbot_id ) {
return isset( $e['chatbot_id'] ) && $e['chatbot_id'] !== $chatbot_id;
} ) );
$existing[] = array(
'chatbot_id' => $chatbot_id,
'linkage_id' => isset( $result['linkage_id'] ) ? (int) $result['linkage_id'] : 0,
'asset_id' => isset( $result['asset_id'] ) ? (int) $result['asset_id'] : 0,
);
update_post_meta( $post_id, '_ai_chatbot_kb_linkage', $existing );
wp_send_json_success( array(
'message' => __( 'Content added to knowledge base.', AI_CHATBOT_DOMAIN ),
'item' => $result,
) );
}
// -----------------------------------------------------------------------
// Content extraction helper
// -----------------------------------------------------------------------
/**
* Build a plain-text content string from a WordPress post or product.
* Used by ajax_link_wp_post() and on_post_saved().
*
* @param WP_Post $post
* @return string Empty string when nothing could be extracted.
*/
private function build_post_content( WP_Post $post ): string {
$post_id = $post->ID;
$parts = array();
// ── Title ──────────────────────────────────────────────────────────
$title = get_the_title( $post );
if ( $title ) {
$parts[] = 'Title: ' . $title;
}
// ── Meta description (Yoast → RankMath → AIOSEO) ──────────────────
$meta_desc = get_post_meta( $post_id, '_yoast_wpseo_metadesc', true );
if ( empty( $meta_desc ) ) {
$meta_desc = get_post_meta( $post_id, 'rank_math_description', true );
}
if ( empty( $meta_desc ) ) {
$meta_desc = get_post_meta( $post_id, '_aioseop_description', true );
}
if ( ! empty( $meta_desc ) ) {
$parts[] = 'Meta Description: ' . $meta_desc;
}
// ── SEO focus keyword (Yoast → RankMath) ──────────────────────────
$seo_keywords = get_post_meta( $post_id, '_yoast_wpseo_focuskw', true );
if ( empty( $seo_keywords ) ) {
$seo_keywords = get_post_meta( $post_id, 'rank_math_focus_keyword', true );
}
if ( ! empty( $seo_keywords ) ) {
$parts[] = 'SEO Keywords: ' . $seo_keywords;
}
// ── Taxonomy: categories & tags ────────────────────────────────────
if ( in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
$categories = wp_get_post_categories( $post_id, array( 'fields' => 'names' ) );
if ( ! empty( $categories ) && ! is_wp_error( $categories ) ) {
$parts[] = 'Categories: ' . implode( ', ', $categories );
}
$tags = wp_get_post_tags( $post_id, array( 'fields' => 'names' ) );
if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {
$parts[] = 'Tags: ' . implode( ', ', $tags );
}
}
// ── WooCommerce product-specific fields ────────────────────────────
if ( 'product' === $post->post_type && function_exists( 'wc_get_product' ) ) {
$product = wc_get_product( $post_id );
if ( $product ) {
$price = wp_strip_all_tags( $product->get_price_html() );
if ( $price ) {
$parts[] = 'Price: ' . $price;
}
$short_desc = wp_strip_all_tags( $product->get_short_description() );
if ( $short_desc ) {
$parts[] = 'Short Description: ' . $short_desc;
}
$sku = $product->get_sku();
if ( $sku ) {
$parts[] = 'SKU: ' . $sku;
}
$prod_cats = wc_get_product_category_list( $post_id, ', ' );
if ( $prod_cats ) {
$parts[] = 'Product Categories: ' . wp_strip_all_tags( $prod_cats );
}
$prod_tags = wc_get_product_tag_list( $post_id, ', ' );
if ( $prod_tags ) {
$parts[] = 'Product Tags: ' . wp_strip_all_tags( $prod_tags );
}
}
}
// ── Body content ───────────────────────────────────────────────────
$body = wp_strip_all_tags( apply_filters( 'the_content', $post->post_content ) );
$body = html_entity_decode( $body, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
$body = preg_replace( '/\s{3,}/', "\n\n", trim( $body ) );
if ( ! empty( $body ) ) {
$parts[] = 'Content: ' . $body;
}
return implode( "\n\n", $parts );
}
// -----------------------------------------------------------------------
// save_post hook – auto-refresh KB when a linked post/product is updated
// -----------------------------------------------------------------------
/**
* Auto-refresh chatbot knowledge base when a linked post/product is saved.
* Hooked to save_post at priority 99 (after WooCommerce finishes at 34).
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object after the save.
* @param bool $update True on update, false on first insert.
*/
public function on_post_saved( $post_id, $post, $update ) {
// Skip revisions and auto-saves — only process real user-initiated saves.
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
// Only refresh when the post is published.
if ( 'publish' !== $post->post_status ) {
return;
}
// Deduplicate: WordPress (and WooCommerce) can fire save_post several
// times for the same post ID in a single request.
static $processed = array();
if ( isset( $processed[ $post_id ] ) ) {
return;
}
$processed[ $post_id ] = true;
// Only act on posts that were previously linked to a chatbot.
$linkages = get_post_meta( $post_id, '_ai_chatbot_kb_linkage', true );
if ( empty( $linkages ) || ! is_array( $linkages ) ) {
return;
}
$project_id = get_option( Ai_Chatbot_API::opt_project_id() );
if ( empty( $project_id ) ) {
return;
}
// Build fresh content. Wrap in try/catch so a faulty third-party
// content filter can never abort the post save operation.
try {
$content = $this->build_post_content( $post );
} catch ( \Exception $e ) {
return;
} catch ( \Throwable $e ) {
return;
}
if ( empty( $content ) ) {
return;
}
$file_name = get_permalink( $post_id );
if ( empty( $file_name ) ) {
$file_name = get_the_title( $post_id );
if ( empty( $file_name ) ) {
$file_name = (string) $post_id;
}
}
$updated_linkages = array();
foreach ( $linkages as $linkage ) {
// Skip malformed entries.
if ( ! isset( $linkage['chatbot_id'], $linkage['linkage_id'], $linkage['asset_id'] ) ) {
continue;
}
$chatbot_id = $linkage['chatbot_id'];
if ( empty( $chatbot_id ) ) {
continue;
}
// Remove the stale KB entry. Errors are silently ignored — a stale
// entry is harmless and must not block the fresh upload.
if ( ! empty( $linkage['linkage_id'] ) || ! empty( $linkage['asset_id'] ) ) {
Ai_Chatbot_API::delink_content(
(int) $linkage['linkage_id'],
$chatbot_id,
(int) $linkage['asset_id']
);
}
// Upload fresh content.
$result = Ai_Chatbot_API::link_content( $project_id, $chatbot_id, $content, $file_name );
if ( ! is_wp_error( $result ) && isset( $result['linkage_id'] ) ) {
$updated_linkages[] = array(
'chatbot_id' => $chatbot_id,
'linkage_id' => (int) $result['linkage_id'],
'asset_id' => isset( $result['asset_id'] ) ? (int) $result['asset_id'] : 0,
);
} else {
// Keep the old entry so the next save can retry.
$updated_linkages[] = $linkage;
}
}
if ( ! empty( $updated_linkages ) ) {
update_post_meta( $post_id, '_ai_chatbot_kb_linkage', $updated_linkages );
}
}
// -----------------------------------------------------------------------
// AJAX handler – WordPress site content (pages / posts / products)
// -----------------------------------------------------------------------
/**
* Return paginated WordPress content for the Knowledge Base content picker.
*
* POST params: post_type (page|post|product), page, per_page, search
*/
public function ajax_get_site_content() {
$this->verify_nonce();
$allowed_types = array( 'page', 'post' );
if ( class_exists( 'WooCommerce' ) ) {
$allowed_types[] = 'product';
}
$post_type = sanitize_key( wp_unslash( $_POST['post_type'] ?? 'page' ) );
if ( ! in_array( $post_type, $allowed_types, true ) ) {
$post_type = 'page';
}
$page = max( 1, (int) ( $_POST['page'] ?? 1 ) );
$per_page = max( 1, min( 50, (int) ( $_POST['per_page'] ?? 20 ) ) );
$search = sanitize_text_field( wp_unslash( $_POST['search'] ?? '' ) );
$args = array(
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => 'modified',
'order' => 'DESC',
'no_found_rows' => false,
);
if ( ! empty( $search ) ) {
$args['s'] = $search;
}
$query = new WP_Query( $args );
$items = array();
foreach ( $query->posts as $post ) {
$items[] = array(
'id' => $post->ID,
'title' => $post->post_title ?: __( '(no title)', AI_CHATBOT_DOMAIN ),
'type' => $post->post_type,
'modified' => $post->post_modified,
'url' => get_permalink( $post->ID ),
);
}
wp_reset_postdata();
wp_send_json_success( array(
'items' => $items,
'total' => (int) $query->found_posts,
'pages' => (int) $query->max_num_pages,
'page' => $page,
'per_page' => $per_page,
) );
}
// -----------------------------------------------------------------------
// AJAX handler – Frontend embed toggle
// -----------------------------------------------------------------------
/**
* Enable or disable this agent as the active frontend embed.
*
* POST params: chatbot_id, project_id, enable (1|0)
*/
public function ajax_toggle_frontend_embed() {
$this->verify_nonce();
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$project_id = sanitize_text_field( wp_unslash( $_POST['project_id'] ?? '' ) );
$enable = ! empty( $_POST['enable'] ) && '1' === (string) $_POST['enable'];
if ( empty( $chatbot_id ) ) {
wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) );
}
$opt_key = PLUGIN_SLUG . '_frontend_embed';
if ( $enable ) {
update_option( $opt_key, array(
'chatbot_id' => $chatbot_id,
'project_id' => $project_id,
) );
wp_send_json_success( array(
'enabled' => true,
'message' => __( 'Chatbot enabled on your website.', AI_CHATBOT_DOMAIN ),
) );
} else {
// Only disable if this agent is the currently active one.
$current = get_option( $opt_key );
if ( ! empty( $current['chatbot_id'] ) && $current['chatbot_id'] === $chatbot_id ) {
delete_option( $opt_key );
}
wp_send_json_success( array(
'enabled' => false,
'message' => __( 'Chatbot disabled on your website.', AI_CHATBOT_DOMAIN ),
) );
}
}
/** AJAX: get inbox sessions */
public function ajax_get_inbox_sessions() {
$this->verify_nonce();
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$next_page_token = sanitize_text_field( wp_unslash( $_POST['next_page_token'] ?? '' ) );
if ( empty( $chatbot_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::get_inbox_sessions( $chatbot_id, $next_page_token );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( $result );
}
/** AJAX: get inbox chats */
public function ajax_get_inbox_chats() {
$this->verify_nonce();
$chatbot_id = sanitize_text_field( wp_unslash( $_POST['chatbot_id'] ?? '' ) );
$session_id = sanitize_text_field( wp_unslash( $_POST['session_id'] ?? '' ) );
if ( empty( $chatbot_id ) || empty( $session_id ) ) { wp_send_json_error( array( 'message' => __( 'Agent ID and Session ID required.', AI_CHATBOT_DOMAIN ) ) ); }
$result = Ai_Chatbot_API::get_inbox_chats( $chatbot_id, $session_id );
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( $result );
}
/** AJAX: get account limits */
public function ajax_get_account_limits() {
$this->verify_nonce();
$result = Ai_Chatbot_API::get_account_limits();
if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); }
wp_send_json_success( $result );
}
}