bool, 'status' => int, 'data' => array|null, 'raw' => string, 'error' => string ).
*/
function rubik_chat_api_request( $method, $path, $body = null, $timeout = 60 ) {
$token = get_option( 'rubik_token', '' );
$headers = array( 'Accept' => 'application/json' );
if ( $token !== '' ) {
$headers['Authorization'] = 'Bearer ' . $token;
}
$args = array(
'method' => strtoupper( $method ),
'timeout' => $timeout,
'headers' => $headers,
);
if ( $body !== null ) {
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = wp_json_encode( $body );
}
$url = RUBIK_CHAT_API_BASE . $path;
$response = wp_remote_request( $url, $args );
rubik_chat_log_api_call( $url, $args['method'], $args, $response );
if ( is_wp_error( $response ) ) {
return array( 'ok' => false, 'status' => 0, 'data' => null, 'raw' => '', 'error' => $response->get_error_message() );
}
$status = (int) wp_remote_retrieve_response_code( $response );
$raw = (string) wp_remote_retrieve_body( $response );
$data = json_decode( $raw, true );
$ok = $status >= 200 && $status < 300;
$error = '';
if ( ! $ok ) {
$error = is_array( $data ) ? ( $data['message'] ?? $data['error'] ?? '' ) : '';
if ( is_array( $data ) && ! empty( $data['errors'] ) && is_array( $data['errors'] ) ) {
$parts = array();
foreach ( $data['errors'] as $messages ) {
$parts[] = is_array( $messages ) ? implode( ', ', $messages ) : (string) $messages;
}
$error = implode( ' | ', $parts );
}
if ( ! is_string( $error ) || $error === '' ) {
$error = 'RubikChat API error (HTTP ' . $status . ').';
}
if ( $status === 401 ) {
$error = 'Your RubikChat session has expired. Please disconnect and connect again.';
}
}
return array( 'ok' => $ok, 'status' => $status, 'data' => is_array( $data ) ? $data : null, 'raw' => $raw, 'error' => $error );
}
/** The organization slug (organization.url) used in API paths. */
function rubik_chat_org_slug() {
$org_slug = trim( (string) get_option( 'rubik_organization_url', '' ) );
if ( preg_match( '#^https?://#i', $org_slug ) ) {
$parsed = wp_parse_url( $org_slug );
$org_slug = ! empty( $parsed['host'] ) ? $parsed['host'] : $org_slug;
}
return $org_slug;
}
/** Agent instructions; page content goes to the knowledge base, not here. */
function rubik_chat_default_instructions( $custom_instructions = '' ) {
$custom_instructions = trim( (string) $custom_instructions );
if ( $custom_instructions !== '' ) {
return $custom_instructions;
}
$site_name = trim( (string) get_bloginfo( 'name' ) );
if ( $site_name === '' ) {
$site_name = 'this website';
}
return "You are the official AI assistant for {$site_name}.\n"
. "- Answer questions using the website knowledge base you have been given.\n"
. "- Be friendly, clear and concise. Use bullet points for lists.\n"
. "- If you don't know the answer, say so and offer to take the visitor's contact details.\n"
. "- Reply in the language the visitor writes in.";
}
/** One page as a text knowledge source: title and URL for context, then the page text. */
function rubik_chat_page_source_text( $page_id ) {
$content = get_post_field( 'post_content', $page_id );
$content = wp_strip_all_tags( strip_shortcodes( (string) $content ) );
$content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
$content = trim( preg_replace( '/\s+/u', ' ', $content ) );
if ( $content === '' ) {
return '';
}
$text = 'Page: ' . get_the_title( $page_id ) . "\nURL: " . get_permalink( $page_id ) . "\n\n" . $content;
// Keep each source a reasonable size
return function_exists( 'mb_substr' ) ? mb_substr( $text, 0, 60000, 'UTF-8' ) : substr( $text, 0, 60000 );
}
/**
* The knowledge base the plugin adds pages to: the stored one, else the agent's first one,
* else a new "{name} Knowledge Base" (attached to the agent when there is one).
*/
function rubik_chat_ensure_knowledge_base( $bot_name ) {
$kb_id = absint( get_option( 'rubik_kb_id', 0 ) );
if ( $kb_id ) {
return array( 'ok' => true, 'id' => $kb_id );
}
$org_slug = rubik_chat_org_slug();
$bot_id = get_option( 'rubik_bot_id', '' );
$existing = array();
if ( $bot_id !== '' ) {
$agent = rubik_chat_api_request( 'GET', '/agents/get/' . rawurlencode( $bot_id ) );
if ( $agent['ok'] && is_array( $agent['data'] ) ) {
$agent_data = $agent['data']['agent'] ?? $agent['data']['data'] ?? $agent['data'];
$kbs = $agent_data['knowledge_bases'] ?? $agent_data['knowledgeBases'] ?? array();
foreach ( (array) $kbs as $kb ) {
if ( ! empty( $kb['id'] ) ) {
$existing[] = (int) $kb['id'];
}
}
}
if ( ! empty( $existing ) ) {
update_option( 'rubik_kb_id', $existing[0] );
return array( 'ok' => true, 'id' => $existing[0] );
}
}
$name = trim( (string) $bot_name ) !== '' ? trim( (string) $bot_name ) : get_bloginfo( 'name' );
$created = rubik_chat_api_request( 'POST', '/organizations/' . rawurlencode( $org_slug ) . '/knowledge-bases', array(
'name' => $name . ' Knowledge Base',
'provider' => 'open_ai',
) );
$kb_id = absint( $created['data']['data']['id'] ?? 0 );
if ( ! $created['ok'] || ! $kb_id ) {
return array( 'ok' => false, 'error' => 'Could not create the knowledge base: ' . ( $created['error'] ?: 'no id returned' ) );
}
update_option( 'rubik_kb_id', $kb_id );
// An existing agent: attach the new knowledge base (keeping any others it has)
if ( $bot_id !== '' ) {
$attach = rubik_chat_api_request( 'PUT', '/agents/' . rawurlencode( $bot_id ) . '/knowledge-bases', array(
'knowledge_base_ids' => array_values( array_unique( array_merge( $existing, array( $kb_id ) ) ) ),
) );
if ( ! $attach['ok'] ) {
return array( 'ok' => false, 'error' => 'Could not attach the knowledge base to the agent: ' . $attach['error'] );
}
}
return array( 'ok' => true, 'id' => $kb_id );
}
/**
* Makes the knowledge base match the selected pages: updates pages already added, adds new
* ones and removes pages that are no longer selected. Training runs on RubikChat.
*/
function rubik_chat_sync_page_sources( $kb_id, $page_ids ) {
$base = '/organizations/' . rawurlencode( rubik_chat_org_slug() ) . '/knowledge-bases/' . absint( $kb_id ) . '/sources';
$map = get_option( 'rubik_page_sources', array() );
$map = is_array( $map ) ? $map : array();
$result = array( 'added' => 0, 'updated' => 0, 'removed' => 0, 'errors' => array() );
$wanted = array();
foreach ( $page_ids as $page_id ) {
$page_id = absint( $page_id );
$text = $page_id ? rubik_chat_page_source_text( $page_id ) : '';
if ( $text === '' ) {
continue;
}
$wanted[ $page_id ] = true;
if ( ! empty( $map[ $page_id ] ) ) {
$updated = rubik_chat_api_request( 'PUT', $base . '/' . absint( $map[ $page_id ] ), array( 'text' => $text ) );
if ( $updated['ok'] ) {
$result['updated']++;
continue;
}
if ( $updated['status'] !== 404 ) {
$result['errors'][] = get_the_title( $page_id ) . ': ' . $updated['error'];
continue;
}
// Removed on RubikChat: add it again
unset( $map[ $page_id ] );
}
$added = rubik_chat_api_request( 'POST', $base, array( 'source_type' => 'text', 'text' => $text ) );
$source_id = absint( $added['data']['data']['id'] ?? 0 );
if ( $added['ok'] && $source_id ) {
$map[ $page_id ] = $source_id;
$result['added']++;
} else {
$result['errors'][] = get_the_title( $page_id ) . ': ' . ( $added['error'] ?: 'no source id returned' );
}
}
foreach ( $map as $page_id => $source_id ) {
if ( isset( $wanted[ $page_id ] ) ) {
continue;
}
$deleted = rubik_chat_api_request( 'DELETE', $base . '/' . absint( $source_id ) );
if ( $deleted['ok'] || $deleted['status'] === 404 ) {
unset( $map[ $page_id ] );
$result['removed']++;
} else {
$result['errors'][] = 'Page #' . $page_id . ': ' . $deleted['error'];
}
}
update_option( 'rubik_page_sources', $map );
return $result;
}
function rubik_chat_add_menu() {
add_menu_page(
__( 'Welcome to RubikChat', 'rubikchat' ),
__( 'RubikChat', 'rubikchat' ),
'manage_options',
'rubik-chat',
'rubik_chat_settings',
rubik_chat_menu_icon(),
80
);
add_submenu_page(
'rubik-chat',
__( 'Settings', 'rubikchat' ),
__( 'Settings', 'rubikchat' ),
'manage_options',
'rubik-chat',
'rubik_chat_settings'
);
$agent_type = get_option( 'rubik_agent_type', '' );
if ( $agent_type === 'agent_customize' ) {
add_submenu_page(
'rubik-chat',
__( 'Logs', 'rubikchat' ),
__( 'Logs', 'rubikchat' ),
'manage_options',
'rubik-chat-logs',
'rubik_chat_logs_page'
);
}
}
add_action( 'admin_menu', 'rubik_chat_add_menu' );
/**
* Safe no-op: prevents fatal if an old install hooks this on admin_footer.
*/
function rubik_chat_admin_styles( $hook ) {
if ( strpos( $hook, 'rubik-chat' ) === false ) {
return;
}
$css_file = plugin_dir_path( __FILE__ ) . 'assets/styles.css';
if ( file_exists( $css_file ) ) {
wp_enqueue_style(
'rubik-chat-admin-styles',
plugins_url( 'assets/styles.css', __FILE__ ),
array(),
filemtime( $css_file )
);
}
// API Logs page: show/hide details, confirm clearing
if ( strpos( $hook, 'rubik-chat-logs' ) !== false ) {
wp_enqueue_script( 'rubik-chat-logs', plugins_url( 'assets/logs.js', __FILE__ ), array(), RUBIKCHAT_VERSION, true );
return;
}
if ( $hook !== 'toplevel_page_rubik-chat' ) {
return;
}
// A clean settings screen: no admin footer or other plugins' notices
wp_add_inline_style( 'rubik-chat-admin-styles', '#wpfooter, #wpbody-content > .notice, .update-nag { display: none !important; } #wpbody-content { padding-bottom: 24px !important; } .rubik-chat-wrap { margin-bottom: 12px !important; }' );
wp_enqueue_script( 'rubik-chat-admin', plugins_url( 'assets/admin.js', __FILE__ ), array(), RUBIKCHAT_VERSION, true );
wp_localize_script( 'rubik-chat-admin', 'RubikChatAdmin', array(
'nonce' => wp_create_nonce( 'rubik_chat_agent_nonce' ),
) );
// Live test on the settings page: the same RubikChat widget as the website
$bot_id = get_option( 'rubik_bot_id', '' );
if ( $bot_id !== '' && get_option( 'rubik_connected', 'no' ) === 'yes' ) {
wp_enqueue_script( 'rubik-chatbot-widget', RUBIK_CHAT_WIDGET_SCRIPT, array(), RUBIKCHAT_VERSION, true );
wp_add_inline_script(
'rubik-chatbot-widget',
'window.embeddedChatbotConfig = ' . wp_json_encode( array( 'chatbotId' => (string) $bot_id, 'domain' => 'widget.rubikchat.com' ) ) . ';',
'before'
);
}
}
/** The admin menu icon: the RubikChat mark as a data URI, so WordPress colors it like the other menu icons. */
function rubik_chat_menu_icon() {
$svg_path = plugin_dir_path( __FILE__ ) . 'assets/icon.svg';
if ( ! file_exists( $svg_path ) ) {
return 'dashicons-format-chat';
}
$svg = str_replace( 'currentColor', 'black', (string) file_get_contents( $svg_path ) );
return 'data:image/svg+xml;base64,' . base64_encode( $svg );
}
add_action( 'admin_enqueue_scripts', 'rubik_chat_admin_styles' );
/* =====================================================
HELPERS
===================================================== */
function rubik_chat_log_api_call( $url, $method, $request_args, $response ) {
$logs = get_option( 'rubikchat_api_logs', array() );
if ( ! is_array( $logs ) ) {
$logs = array();
}
$status_code = '';
$response_body = '';
if ( is_wp_error( $response ) ) {
$status_code = 'ERROR';
$response_body = $response->get_error_message();
} else {
$status_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
}
$entry = array(
'time' => current_time( 'mysql' ),
'url' => $url,
'method' => strtoupper( $method ),
'request' => isset( $request_args['body'] ) ? $request_args['body'] : '',
'status' => $status_code,
'response' => $response_body,
);
array_unshift( $logs, $entry );
if ( count( $logs ) > 100 ) {
$logs = array_slice( $logs, 0, 100 );
}
update_option( 'rubikchat_api_logs', $logs );
}
function rubik_chat_page_character_count( $post_id ) {
$content = get_post_field( 'post_content', $post_id );
$content = wp_strip_all_tags( strip_shortcodes( $content ) );
$content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
$content = preg_replace( '/\s+/u', ' ', $content );
$content = trim( $content );
return function_exists( 'mb_strlen' ) ? mb_strlen( $content, 'UTF-8' ) : strlen( $content );
}
function rubik_chat_get_selected_pages() {
$saved = get_option( 'rubik_selected_pages', array() );
return is_array( $saved ) ? $saved : array();
}
function rubik_chat_get_published_pages() {
return get_posts( array(
'post_type' => 'page',
'post_status' => array( 'publish', 'private', 'draft', 'pending', 'future', 'protected' ),
'posts_per_page' => -1,
'orderby' => 'menu_order title',
'order' => 'ASC',
) );
}
/**
* Human-readable page visibility label for admin UI.
*/
/**
* Approximate color name from hex for summary UI.
*/
function rubik_chat_color_name( $hex ) {
$hex = strtolower( trim( (string) $hex ) );
if ( $hex === '' ) {
return 'Default';
}
if ( $hex[0] !== '#' ) {
$hex = '#' . $hex;
}
if ( ! preg_match( '/^#[0-9a-f]{6}$/', $hex ) ) {
return strtoupper( $hex );
}
$r = hexdec( substr( $hex, 1, 2 ) );
$g = hexdec( substr( $hex, 3, 2 ) );
$b = hexdec( substr( $hex, 5, 2 ) );
$palette = array(
'Black' => array( 15, 23, 42 ),
'Charcoal' => array( 38, 38, 38 ),
'Gray' => array( 100, 116, 139 ),
'White' => array( 255, 255, 255 ),
'Red' => array( 239, 68, 68 ),
'Rose' => array( 244, 63, 94 ),
'Orange' => array( 249, 115, 22 ),
'Amber' => array( 245, 158, 11 ),
'Yellow' => array( 234, 179, 8 ),
'Green' => array( 34, 197, 94 ),
'Emerald' => array( 16, 185, 129 ),
'Teal' => array( 20, 184, 166 ),
'Cyan' => array( 6, 182, 212 ),
'Sky' => array( 14, 165, 233 ),
'Blue' => array( 59, 130, 246 ),
'Indigo' => array( 79, 70, 229 ),
'Violet' => array( 139, 92, 246 ),
'Purple' => array( 168, 85, 247 ),
'Fuchsia' => array( 217, 70, 239 ),
'Pink' => array( 236, 72, 153 ),
);
$best = 'Custom';
$best_d = PHP_INT_MAX;
foreach ( $palette as $name => $rgb ) {
$d = ( $r - $rgb[0] ) ** 2 + ( $g - $rgb[1] ) ** 2 + ( $b - $rgb[2] ) ** 2;
if ( $d < $best_d ) {
$best_d = $d;
$best = $name;
}
}
return $best;
}
function rubik_chat_page_visibility_label( $post ) {
if ( ! $post ) {
return 'Unknown';
}
// Password-protected pages still have status publish but post_password set
if ( ! empty( $post->post_password ) ) {
return 'Protected';
}
$status = $post->post_status;
$map = array(
'publish' => 'Public',
'private' => 'Private',
'draft' => 'Draft',
'pending' => 'Pending',
'future' => 'Scheduled',
);
return $map[ $status ] ?? ucfirst( (string) $status );
}
function rubik_chat_generate_password( $length = 12 ) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$password = '';
for ( $i = 0; $i < $length; $i++ ) {
$password .= $chars[ wp_rand( 0, strlen( $chars ) - 1 ) ];
}
return $password;
}
/**
* Extract rubikchat_session cookie value from an HTTP response.
*/
function rubik_chat_is_valid_bot_id_format( $bot_id ) {
$bot_id = trim( (string) $bot_id );
if ( $bot_id === '' || strlen( $bot_id ) < 12 ) {
return false;
}
if ( ! preg_match( '/^[A-Za-z0-9_-]+$/', $bot_id ) ) {
return false;
}
return true;
}
function rubik_chat_is_generic_name( $name ) {
$n = strtolower( trim( (string) $name ) );
return $n === '' || $n === 'connected agent' || $n === 'your agent' || $n === 'ai agent';
}
/**
* Parse agent name from welcome / initial messages when API display_name is empty.
* e.g. "👋 Hi! I am RubikChat AI, ask me anything..." → "RubikChat AI"
*/
function rubik_chat_extract_name_from_messages( $messages ) {
if ( empty( $messages ) ) {
return '';
}
if ( ! is_array( $messages ) ) {
$messages = array( (string) $messages );
}
$text = '';
foreach ( $messages as $m ) {
if ( is_string( $m ) && trim( $m ) !== '' ) {
$text = trim( $m );
break;
}
if ( is_array( $m ) && ! empty( $m['message'] ) && is_string( $m['message'] ) ) {
$text = trim( $m['message'] );
break;
}
}
if ( $text === '' ) {
return '';
}
$text = preg_replace( '/^[\p{So}\p{Sk}\s]+/u', '', $text );
$text = trim( $text );
if ( preg_match( '/(?:^|[.!?\s])(?:i\s+am|i\'m|my\s+name\s+is)\s+([A-Za-z0-9][A-Za-z0-9\s&\-]{0,40}?)(?:[,\.\!\?]|\s+ask|\s+how|\s+what|\s+about|$)/iu', $text, $m ) ) {
$name = trim( $m[1] );
if ( $name !== '' && strlen( $name ) <= 40 ) {
return sanitize_text_field( $name );
}
}
return '';
}
/**
* Fetch agent data for a specific botId from RubikChat.
* Name order: display_name → botName → name → welcome message.
*/
function rubik_chat_fetch_agent_from_api( $bot_id ) {
if ( ! rubik_chat_is_valid_bot_id_format( $bot_id ) ) {
return array(
'ok' => false,
'message' => 'Please enter valid Agent ID',
);
}
$url = RUBIK_CHAT_STYLES_URL . rawurlencode( $bot_id );
$request_args = array(
'timeout' => 20,
'headers' => array( 'Accept' => 'application/json' ),
);
$response = wp_remote_get( $url, $request_args );
rubik_chat_log_api_call( $url, 'GET', $request_args, $response );
if ( is_wp_error( $response ) ) {
return array(
'ok' => false,
'message' => 'Could not reach RubikChat: ' . $response->get_error_message(),
);
}
$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( $code === 404 || $code === 401 || $code === 403 ) {
return array(
'ok' => false,
'message' => 'Please enter valid Agent ID',
);
}
if ( $code < 200 || $code >= 300 || ! is_array( $data ) ) {
return array(
'ok' => false,
'message' => 'Please enter valid Agent ID',
);
}
if ( ! isset( $data['styles'] ) || ! is_array( $data['styles'] ) ) {
return array(
'ok' => false,
'message' => 'Please enter valid Agent ID',
);
}
$styles = $data['styles'];
$messages = isset( $data['initialMessages'] ) ? $data['initialMessages'] : array();
$name = '';
if ( ! empty( $styles['display_name'] ) ) {
$name = sanitize_text_field( $styles['display_name'] );
}
if ( $name === '' && ! empty( $data['botName'] ) ) {
$name = sanitize_text_field( $data['botName'] );
}
if ( $name === '' && ! empty( $data['name'] ) ) {
$name = sanitize_text_field( $data['name'] );
}
// When API display_name is empty, recover name from welcome text for this botId
if ( $name === '' ) {
$name = rubik_chat_extract_name_from_messages( $messages );
}
return array(
'ok' => true,
'botName' => $name,
'display_name' => isset( $styles['display_name'] ) ? (string) $styles['display_name'] : '',
'styles' => $styles,
'initialMessages' => $messages,
'raw' => $data,
);
}
/**
* Resolve botName for this botId only (never mix names across bots).
*/
function rubik_chat_resolve_agent_name( $bot_id, $api_name = '' ) {
$api_name = is_string( $api_name ) ? trim( $api_name ) : '';
// 1) Name fetched for this botId from API / welcome
if ( ! rubik_chat_is_generic_name( $api_name ) ) {
return $api_name;
}
// 2) Saved name only if it belongs to the same botId
$prev_id = get_option( 'rubik_bot_id', '' );
$prev_config = get_option( 'rubik_agent_config', array() );
if ( ! is_array( $prev_config ) ) {
$prev_config = array();
}
$prev_name = isset( $prev_config['botName'] ) ? trim( (string) $prev_config['botName'] ) : '';
if ( (string) $prev_id === (string) $bot_id && ! rubik_chat_is_generic_name( $prev_name ) ) {
return $prev_name;
}
// 3) Build payload only if botId matches
$build = get_option( 'rubik_agent_build', array() );
if ( is_array( $build ) ) {
$build_bot = $build['botId']
?? $build['bot_id']
?? $build['chatbotId']
?? $build['chatbot_id']
?? $build['id']
?? ( $build['data']['botId'] ?? $build['data']['id'] ?? '' );
$build_name = $build['botName']
?? $build['bot_name']
?? $build['name']
?? ( $build['data']['botName'] ?? $build['data']['name'] ?? '' );
if ( (string) $build_bot === (string) $bot_id && is_string( $build_name ) && ! rubik_chat_is_generic_name( $build_name ) ) {
return sanitize_text_field( trim( $build_name ) );
}
}
return 'Your Agent';
}
/**
* On settings load: if name is still generic, re-fetch for current botId and save botName.
*/
function rubik_chat_maybe_refresh_agent_name() {
$bot_id = get_option( 'rubik_bot_id', '' );
if ( empty( $bot_id ) || get_option( 'rubik_connected', 'no' ) !== 'yes' ) {
return;
}
$config = get_option( 'rubik_agent_config', array() );
if ( ! is_array( $config ) ) {
$config = array();
}
$current = isset( $config['botName'] ) ? trim( (string) $config['botName'] ) : '';
if ( ! rubik_chat_is_generic_name( $current ) ) {
return;
}
$fetched = rubik_chat_fetch_agent_from_api( $bot_id );
if ( empty( $fetched['ok'] ) ) {
return;
}
$name = rubik_chat_resolve_agent_name( $bot_id, $fetched['botName'] ?? '' );
if ( rubik_chat_is_generic_name( $name ) ) {
return;
}
$config['botName'] = $name;
update_option( 'rubik_agent_config', $config );
}
/* =====================================================
SETTINGS PAGE
===================================================== */
function rubik_chat_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Refresh botName for current botId if still generic
rubik_chat_maybe_refresh_agent_name();
$rubik_bot_id = get_option( 'rubik_bot_id', '' );
$is_connected = get_option( 'rubik_connected', 'no' ) === 'yes';
$saved_pages = rubik_chat_get_selected_pages();
$saved_config = get_option( 'rubik_agent_config', array() );
if ( ! is_array( $saved_config ) ) {
$saved_config = array();
}
$saved_bot_name = $saved_config['botName'] ?? '';
$saved_instructions = $saved_config['instructions'] ?? '';
$saved_llm = $saved_config['llm'] ?? 'gpt-4o-mini';
$saved_temperature = $saved_config['temperature'] ?? '0';
$saved_initial_message = $saved_config['initial_messages'] ?? '';
$saved_colour = $saved_config['color'] ?? '#4f46e5';
$saved_theme = $saved_config['theme'] ?? 'light';
$agent_is_built = ! empty( $rubik_bot_id );
$agent_type = get_option( 'rubik_agent_type', '' );
$agent_visible = get_option( 'rubik_visible_agent', 'yes' ) !== 'no';
$pages = rubik_chat_get_published_pages();
// Default agent name from domain + " Agent"
$host_for_name = (string) wp_parse_url( home_url(), PHP_URL_HOST );
$base_for_name = strtolower( $host_for_name );
$base_for_name = preg_replace( '/\.local$/', '', $base_for_name );
$base_for_name = preg_replace( '/\.localhost$/', '', $base_for_name );
if ( $base_for_name === 'localhost' || $base_for_name === '127.0.0.1' || $base_for_name === '::1' || $base_for_name === '' ) {
$base_for_name = get_bloginfo( 'name' ) ?: 'Site';
}
$slug_for_name = preg_replace( '/[^a-z0-9]+/i', '', $base_for_name );
if ( $slug_for_name === '' ) {
$slug_for_name = 'Site';
}
$default_domain_agent_name = ucfirst( $slug_for_name ) . ' Agent';
if ( $saved_bot_name === '' || rubik_chat_is_generic_name( $saved_bot_name ) ) {
$saved_bot_name = $default_domain_agent_name;
}
?>
'Permission denied.' ), 403 );
}
$raw_pages = isset( $_POST['pages'] ) ? sanitize_text_field( wp_unslash( $_POST['pages'] ) ) : '[]';
$pages = json_decode( $raw_pages, true );
if ( ! is_array( $pages ) ) {
wp_send_json_error( array( 'message' => 'Invalid page selection.' ), 400 );
}
$selected = array();
foreach ( $pages as $page ) {
$page_id = isset( $page['page_id'] ) ? absint( $page['page_id'] ) : 0;
if ( ! $page_id ) continue;
$post = get_post( $page_id );
if ( ! $post || $post->post_type !== 'page' ) continue;
$selected[] = array(
'page_id' => $page_id,
'title' => get_the_title( $page_id ),
'url' => get_permalink( $page_id ),
'character_count' => rubik_chat_page_character_count( $page_id ),
);
}
update_option( 'rubik_selected_pages', $selected );
wp_send_json_success( array( 'message' => 'Pages saved.', 'count' => count( $selected ) ) );
}
add_action( 'wp_ajax_rubik_chat_save_selected_pages', 'rubik_chat_save_selected_pages' );
function rubik_chat_create_agent_auto() {
check_ajax_referer( 'rubik_chat_agent_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
}
$current_user = wp_get_current_user();
$email = $current_user->user_email;
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
$company_name = get_bloginfo( 'name' );
$password = rubik_chat_generate_password( 12 );
// Organization domain from site host name only:
// tutorial18 / tutorial18.local → tutorial18com
// nsu.local → nsucom
// boot.local → bootcom
$host = (string) $domain;
if ( $host === '' ) {
$host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
}
$is_local = (
$host === '' ||
strpos( $host, '.local' ) !== false ||
strpos( $host, 'localhost' ) !== false ||
$host === '127.0.0.1' ||
$host === '::1'
);
if ( $is_local ) {
// Drop TLD (.local etc.), keep the site name only, then append "com"
$base = strtolower( $host );
$base = preg_replace( '/\.local$/', '', $base );
$base = preg_replace( '/\.localhost$/', '', $base );
if ( $base === 'localhost' || $base === '127.0.0.1' || $base === '::1' || $base === '' ) {
$base = 'local';
}
// letters/numbers only
$slug = preg_replace( '/[^a-z0-9]/', '', $base );
if ( $slug === '' ) {
$slug = 'local';
}
$domain = $slug . 'com';
}
if ( empty( $email ) || empty( $company_name ) ) {
wp_send_json_error( array( 'message' => 'Could not get site information.' ), 400 );
}
$request_args = array(
'timeout' => 30,
'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json' ),
'body' => wp_json_encode( array(
'email' => $email,
'password' => $password,
'domain' => $domain,
'company_name' => $company_name,
) ),
);
$register_response = wp_remote_post( RUBIK_CHAT_REGISTER_URL, $request_args );
rubik_chat_log_api_call( RUBIK_CHAT_REGISTER_URL, 'POST', $request_args, $register_response );
if ( is_wp_error( $register_response ) ) {
wp_send_json_error( array( 'message' => 'Registration failed: ' . $register_response->get_error_message() ), 500 );
}
$register_status = wp_remote_retrieve_response_code( $register_response );
$register_body = wp_remote_retrieve_body( $register_response );
$register_data = json_decode( $register_body, true );
$email_taken = is_array( $register_data ) && ! empty( $register_data['errors']['email'] );
if ( $email_taken || ( $register_status >= 400 && isset( $register_data['message'] ) && stripos( $register_data['message'], 'already' ) !== false ) ) {
wp_send_json_error( array( 'message' => 'You already have an account. Please click "Connect Existing Agent".' ), 400 );
}
if ( ! is_array( $register_data ) || $register_status < 200 || $register_status >= 300 ) {
$msg = $register_data['message'] ?? 'Registration failed.';
if ( ! empty( $register_data['errors'] ) ) {
$errors = array();
foreach ( $register_data['errors'] as $field => $messages ) {
$errors[] = is_array( $messages ) ? implode( ', ', $messages ) : $messages;
}
$msg = implode( ' | ', $errors );
}
wp_send_json_error( array( 'message' => $msg ), 400 );
}
update_option( 'rubik_user_email', $email );
// Only the login token is kept (it doesn't expire); the password is not stored
delete_option( 'rubik_api_secret' );
$organization = $register_data['organization'] ?? array();
$org_url = $organization['url'] ?? '';
$org_name = $organization['name'] ?? $company_name;
$org_id = $organization['id'] ?? '';
$user = $register_data['user'] ?? array();
$user_id = $user['id'] ?? '';
if ( empty( $org_url ) ) {
wp_send_json_error( array( 'message' => 'Organization URL not returned by API.' ), 500 );
}
$login_args = array(
'timeout' => 30,
'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json' ),
'body' => wp_json_encode( array( 'email' => $email, 'password' => $password ) ),
);
$login_response = wp_remote_post( RUBIK_CHAT_LOGIN_URL, $login_args );
rubik_chat_log_api_call( RUBIK_CHAT_LOGIN_URL, 'POST', $login_args, $login_response );
if ( is_wp_error( $login_response ) ) {
wp_send_json_error( array( 'message' => 'Account created but login failed.' ), 500 );
}
$login_status = wp_remote_retrieve_response_code( $login_response );
$login_body = wp_remote_retrieve_body( $login_response );
$login_data = json_decode( $login_body, true );
if ( ! is_array( $login_data ) || $login_status < 200 || $login_status >= 300 ) {
wp_send_json_error( array( 'message' => 'Account created but login failed.' ), 500 );
}
$api_data = $login_data['data'] ?? $login_data;
$token = $api_data['token'] ?? $api_data['access_token'] ?? $login_data['token'] ?? '';
if ( empty( $token ) ) {
wp_send_json_error( array( 'message' => 'No token received after login.' ), 500 );
}
// Prefer organization.url from login payload when present (source of truth for train route)
$login_org = $api_data['organization'] ?? $login_data['organization'] ?? array();
if ( is_array( $login_org ) && ! empty( $login_org['url'] ) ) {
$org_url = $login_org['url'];
$org_name = $login_org['name'] ?? $org_name;
$org_id = $login_org['id'] ?? $org_id;
}
if ( empty( $org_url ) && ! empty( $api_data['organization_url'] ) ) {
$org_url = $api_data['organization_url'];
}
if ( empty( $org_url ) && ! empty( $api_data['org_url'] ) ) {
$org_url = $api_data['org_url'];
}
if ( empty( $org_url ) ) {
wp_send_json_error( array( 'message' => 'Organization URL missing from API response.' ), 500 );
}
update_option( 'rubik_organization_name', sanitize_text_field( $org_name ) );
update_option( 'rubik_organization_url', sanitize_text_field( $org_url ) );
update_option( 'rubik_user_id', sanitize_text_field( (string) $user_id ) );
update_option( 'rubik_organization_id', sanitize_text_field( (string) $org_id ) );
update_option( 'rubik_token', sanitize_text_field( $token ) );
update_option( 'rubik_connected', 'yes' );
update_option( 'rubik_agent_type', 'agent_customize' );
update_option( 'rubik_visible_agent', 'yes' );
delete_option( 'rubik_bot_id' );
delete_option( 'rubik_selected_pages' );
delete_option( 'rubik_agent_config' );
delete_option( 'rubik_agent_build' );
delete_option( 'rubik_kb_id' );
delete_option( 'rubik_page_sources' );
delete_option( 'rubik_session_cookie' );
wp_send_json_success( array(
'message' => 'Agent created and connected successfully.',
'organization_url' => $org_url,
) );
}
add_action( 'wp_ajax_rubik_chat_create_agent_auto', 'rubik_chat_create_agent_auto' );
/**
* Connect existing agent by Bot ID.
* Fetches name for THAT botId and saves as botName.
*/
/**
* Connect existing agent by Bot ID.
* - Always validates Bot ID via API.
* - If Bot ID matches the one already stored in the database and a real agent name exists there,
* reuse that agent_name from the database (do not overwrite config).
* - Otherwise keep the original flow (fetch name, save defaults).
*/
function rubik_chat_connect_with_bot_id() {
check_ajax_referer( 'rubik_chat_agent_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
}
$bot_id = isset( $_POST['bot_id'] ) ? sanitize_text_field( wp_unslash( $_POST['bot_id'] ) ) : '';
$bot_id = trim( $bot_id );
if ( $bot_id === '' ) {
wp_send_json_error( array( 'message' => 'Please enter valid Agent ID' ), 400 );
}
// Always validate Bot ID against RubikChat API.
$fetched = rubik_chat_fetch_agent_from_api( $bot_id );
if ( empty( $fetched['ok'] ) ) {
wp_send_json_error( array( 'message' => $fetched['message'] ?? 'Please enter valid Agent ID' ), 400 );
}
$prev_bot = get_option( 'rubik_bot_id', '' );
$prev_config = get_option( 'rubik_agent_config', array() );
if ( ! is_array( $prev_config ) ) {
$prev_config = array();
}
$prev_name = isset( $prev_config['botName'] ) ? trim( (string) $prev_config['botName'] ) : '';
// Bot ID matches the one in the database → use agent_name from database.
if ( (string) $prev_bot === (string) $bot_id && $prev_name !== '' && ! rubik_chat_is_generic_name( $prev_name ) ) {
update_option( 'rubik_bot_id', $bot_id );
update_option( 'rubik_connected', 'yes' );
update_option( 'rubik_agent_type', 'agent_exist' );
// Keep existing rubik_agent_config untouched so the stored agent name remains.
wp_send_json_success( array(
'message' => 'Connected successfully.',
'botId' => $bot_id,
'botName' => $prev_name,
) );
return;
}
// Different Bot ID (or no usable name in DB) → original flow.
$bot_name = rubik_chat_resolve_agent_name( $bot_id, $fetched['botName'] ?? '' );
if ( (string) $prev_bot !== (string) $bot_id ) {
delete_option( 'rubik_selected_pages' );
delete_option( 'rubik_kb_id' );
delete_option( 'rubik_page_sources' );
}
update_option( 'rubik_bot_id', $bot_id );
update_option( 'rubik_agent_config', array(
'botName' => $bot_name,
'instructions' => '',
'llm' => 'gpt-4o-mini',
'temperature' => '0',
'initial_messages' => '',
'color' => '#4f46e5',
'theme' => 'light',
) );
update_option( 'rubik_connected', 'yes' );
update_option( 'rubik_agent_type', 'agent_exist' );
if ( get_option( 'rubik_visible_agent', '' ) === '' ) { update_option( 'rubik_visible_agent', 'yes' ); }
wp_send_json_success( array(
'message' => 'Connected successfully.',
'botId' => $bot_id,
'botName' => $bot_name,
) );
}
add_action( 'wp_ajax_rubik_chat_connect_with_bot_id', 'rubik_chat_connect_with_bot_id' );
function rubik_chat_set_visibility() {
check_ajax_referer( 'rubik_chat_agent_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
}
$visible = isset( $_POST['visible'] ) ? sanitize_text_field( wp_unslash( $_POST['visible'] ) ) : 'yes';
$visible = ( $visible === 'yes' || $visible === '1' || $visible === 'true' ) ? 'yes' : 'no';
update_option( 'rubik_visible_agent', $visible );
wp_send_json_success( array(
'visible' => $visible === 'yes',
'message' => $visible === 'yes' ? 'Agent visible on website.' : 'Agent hidden on website.',
) );
}
add_action( 'wp_ajax_rubik_chat_set_visibility', 'rubik_chat_set_visibility' );
function rubik_chat_logout() {
check_ajax_referer( 'rubik_chat_agent_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
}
delete_option( 'rubik_connected' );
delete_option( 'rubik_bot_id' );
delete_option( 'rubik_agent_type' );
delete_option( 'rubik_selected_pages' );
delete_option( 'rubik_agent_config' );
delete_option( 'rubik_agent_build' );
delete_option( 'rubik_user_email' );
delete_option( 'rubik_api_secret' );
delete_option( 'rubik_visible_agent' );
delete_option( 'rubikchat_api_logs' );
delete_option( 'rubik_organization_name' );
delete_option( 'rubik_organization_url' );
delete_option( 'rubik_user_id' );
delete_option( 'rubik_organization_id' );
delete_option( 'rubik_token' );
delete_option( 'rubik_session_cookie' );
delete_option( 'rubik_kb_id' );
delete_option( 'rubik_page_sources' );
wp_send_json_success( array( 'message' => 'Disconnected successfully.' ) );
}
add_action( 'wp_ajax_rubik_chat_logout', 'rubik_chat_logout' );
/**
* Build agent instructions from selected page knowledge + site identity.
* Matches RubikChat train-agent instructions format (role + grounding + knowledge).
*/
function rubik_chat_build_instructions_from_pages( $bot_name, $website_rows, $custom_instructions = '' ) {
// Build instructions EXACTLY like sample API agent response (agent.instructions).
// Selected page content goes in the final "5. About {Site}" block (knowledge).
$site_name = get_bloginfo( 'name' );
if ( ! is_string( $site_name ) || trim( $site_name ) === '' ) {
$site_name = ( is_string( $bot_name ) && $bot_name !== '' ) ? $bot_name : 'this website';
}
$site_name = trim( $site_name );
// Knowledge from selected pages → same role as sample "About RubikChat" line
$about_lines = array();
if ( is_array( $website_rows ) ) {
foreach ( $website_rows as $row ) {
$url = isset( $row['url'] ) ? trim( (string) $row['url'] ) : '';
$body = isset( $row['content'] ) ? trim( (string) $row['content'] ) : '';
$title = '';
if ( $url !== '' ) {
$pid = function_exists( 'url_to_postid' ) ? url_to_postid( $url ) : 0;
if ( $pid ) {
$title = get_the_title( $pid );
}
}
if ( ! is_string( $title ) ) {
$title = '';
}
$title = trim( $title );
if ( $body === '' && $title === '' && $url === '' ) {
continue;
}
// Cap each page so instructions stay within agent character limits
if ( function_exists( 'mb_strlen' ) && mb_strlen( $body, 'UTF-8' ) > 6000 ) {
$body = mb_substr( $body, 0, 6000, 'UTF-8' ) . '…';
} elseif ( strlen( $body ) > 6000 ) {
$body = substr( $body, 0, 6000 ) . '…';
}
// Format like sample: "Name | short description" then optional body
$line = $title !== '' ? $title : $site_name;
if ( $body !== '' ) {
// Prefer first sentence / short blurb style when possible
$line .= ' | ' . $body;
} elseif ( $url !== '' ) {
$line .= ' | ' . $url;
}
$about_lines[] = $line;
}
}
if ( empty( $about_lines ) ) {
$about_lines[] = $site_name;
}
$about_block = implode( "\r\n", $about_lines );
// Character-for-character structure of sample API agent instructions
$nl = "\r\n";
$out = 'You are the professional AI assistant for ' . $site_name . '.' . $nl;
$out .= '1. Role & Identity' . $nl;
$out .= '- You are the official AI assistant representing ' . $site_name . '.' . $nl;
$out .= '- You act as the primary digital contact for all user inquiries.' . $nl;
$out .= '- Your primary goal: provide accurate information, answer questions, and guide users to take action.' . $nl;
$out .= '2. Tone & Style' . $nl;
$out .= '- Professional, clear, and respectful at all times.' . $nl;
$out .= '- Use proper grammar and complete sentences.' . $nl;
$out .= '- Avoid slang, humor, or overly casual language.' . $nl;
$out .= '- Use bullet points or numbered steps when presenting multiple items.' . $nl;
$out .= '3. Opening Behavior' . $nl;
$out .= '- When a user sends their FIRST message or greeting (e.g., "hi", "hello"), you MUST:' . $nl;
$out .= ' a) Greet them warmly.' . $nl;
$out .= ' b) Introduce yourself: "I am your AI assistant for ' . $site_name . '."' . $nl;
$out .= ' c) Briefly state what you can help with.' . $nl;
$out .= ' d) Ask how you can assist them today.' . $nl;
$out .= '4. Grounding & Accuracy' . $nl;
$out .= '- You MUST ONLY answer factual questions based on the provided knowledge base and enabled tools.' . $nl;
$out .= '- If a factual question is NOT in your knowledge base or tools, say: "I don\'t have that information right now, but I\'d be happy to connect you with our team."' . $nl;
$out .= '- Never guess, fabricate, or assume factual information.' . $nl;
$out .= '- Avoid off-topic discussions unrelated to ' . $site_name . ' and its services.' . $nl;
$out .= '- IMPORTANT: Short conversational replies like "ok", "yes", "sure", "great", "sounds good", "proceed", "go ahead" are NOT questions. They are confirmations. Always treat them as the user agreeing to continue — never respond with "I can\'t help with that". Instead, continue guiding the user through the next step of the current workflow.' . $nl;
$out .= '5. Conversation Best Practices' . $nl;
$out .= '- Always respond in the same language the user is writing in.' . $nl;
$out .= '- You are encouraged to use relevant emojis to make the conversation engaging and friendly.' . $nl;
$out .= '- After completing any task, ask: "Is there anything else I can help you with?"' . $nl;
$out .= '- If a user seems stuck or confused, proactively offer guidance.' . $nl;
$out .= '- Keep responses concise — avoid walls of text unless the user asks for detail.' . $nl;
$out .= '5. About ' . $site_name . $nl;
$out .= $about_block;
return $out;
}
function rubik_chat_build_agent() {
try {
check_ajax_referer( 'rubik_chat_agent_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
}
$org_slug = rubik_chat_org_slug();
if ( $org_slug === '' || get_option( 'rubik_token', '' ) === '' ) {
wp_send_json_error( array( 'message' => 'Please connect an agent first.' ), 400 );
}
// Prefer pages from this request, else DB
$selected_pages = array();
if ( isset( $_POST['pages'] ) ) {
$page_ids = json_decode( sanitize_text_field( wp_unslash( $_POST['pages'] ) ), true );
if ( is_array( $page_ids ) ) {
foreach ( $page_ids as $pid ) {
$page_id = absint( is_array( $pid ) ? ( $pid['page_id'] ?? 0 ) : $pid );
if ( ! $page_id ) {
continue;
}
$post = get_post( $page_id );
if ( ! $post || $post->post_type !== 'page' ) {
continue;
}
$selected_pages[] = array(
'page_id' => $page_id,
'title' => get_the_title( $page_id ),
'url' => get_permalink( $page_id ),
'character_count' => rubik_chat_page_character_count( $page_id ),
);
}
}
}
if ( empty( $selected_pages ) ) {
$from_db = get_option( 'rubik_selected_pages', array() );
if ( is_array( $from_db ) ) {
$selected_pages = $from_db;
}
}
if ( empty( $selected_pages ) ) {
wp_send_json_error( array( 'message' => 'Please select at least one page.' ), 400 );
}
// Always persist so Selected Pages tab shows after build
update_option( 'rubik_selected_pages', $selected_pages );
$botName = isset( $_POST['botName'] ) ? sanitize_text_field( wp_unslash( $_POST['botName'] ) ) : 'My AI Agent';
$custom_instr = isset( $_POST['instructions'] ) ? sanitize_textarea_field( wp_unslash( $_POST['instructions'] ) ) : '';
$llm = isset( $_POST['llm'] ) ? sanitize_text_field( wp_unslash( $_POST['llm'] ) ) : 'gpt-4o-mini';
$temperature = isset( $_POST['temperature'] ) ? floatval( $_POST['temperature'] ) : 0;
$initial_messages = isset( $_POST['initial_messages'] ) ? sanitize_textarea_field( wp_unslash( $_POST['initial_messages'] ) ) : 'Hello! How can I help you today?';
$color = isset( $_POST['color'] ) ? sanitize_hex_color( wp_unslash( $_POST['color'] ) ) : '#4f46e5';
$theme = isset( $_POST['theme'] ) ? sanitize_key( wp_unslash( $_POST['theme'] ) ) : 'light';
if ( empty( $botName ) ) {
$botName = 'My AI Agent';
}
if ( empty( $initial_messages ) ) {
$initial_messages = 'Hello! How can I help you today?';
}
// The selected pages go to the knowledge base, so the instructions stay short
$instructions = rubik_chat_default_instructions( $custom_instr );
update_option( 'rubik_agent_config', array(
'botName' => $botName,
'instructions' => $instructions,
'llm' => $llm,
'temperature' => $temperature,
'initial_messages' => $initial_messages,
'color' => $color,
'theme' => $theme,
) );
// 1. Knowledge base for the pages
$kb = rubik_chat_ensure_knowledge_base( $botName );
if ( empty( $kb['ok'] ) ) {
wp_send_json_error( array( 'message' => $kb['error'] ), 200 );
}
// 2. The agent, with the knowledge base attached (skipped when it already exists)
$bot_id = get_option( 'rubik_bot_id', '' );
if ( $bot_id === '' ) {
$created = rubik_chat_api_request( 'POST', '/agents/onboarding/create/' . rawurlencode( $org_slug ), array(
'botName' => $botName,
'instructions' => $instructions,
'llm' => $llm,
'temperature' => (string) $temperature,
// The API expects these lists as JSON strings
'initial_messages' => wp_json_encode( array( $initial_messages ) ),
'suggested_messages' => '[]',
'theme' => $theme,
'color' => $color ? $color : '#4f46e5',
'is_streaming' => '1',
'businessType' => wp_json_encode( array( 'label' => 'Other', 'value' => 'Other', 'type' => 'other' ) ),
'knowledge_base_ids' => wp_json_encode( array( (int) $kb['id'] ) ),
// Lets the widget run on this site (Share & Embed > Allowed websites)
'website' => home_url(),
), 120 );
$bot_id = (string) ( $created['data']['agent']['chatbot_key'] ?? '' );
if ( ! $created['ok'] || $bot_id === '' ) {
$msg = $created['error'] ?: 'Agent was not created (no Agent ID returned).';
if ( ! empty( $created['data']['details'] ) && is_string( $created['data']['details'] ) ) {
$msg .= ' ' . $created['data']['details'];
}
wp_send_json_error( array( 'message' => $msg, 'status' => $created['status'] ), 200 );
}
update_option( 'rubik_bot_id', sanitize_text_field( $bot_id ) );
}
// Also for an agent created before RubikChat took the website on create
rubik_chat_allow_this_site();
// 3. The selected pages as knowledge (RubikChat trains them in the background)
$synced = rubik_chat_sync_page_sources( $kb['id'], wp_list_pluck( $selected_pages, 'page_id' ) );
$build_data = array(
'botId' => $bot_id,
'botName' => $botName,
'knowledge_base_id' => (int) $kb['id'],
'pages' => $synced,
);
update_option( 'rubik_agent_build', $build_data );
$message = 'Agent built successfully! Your pages are being trained and will be used in answers shortly.';
if ( ! empty( $synced['errors'] ) ) {
$message .= ' Some pages could not be added: ' . implode( '; ', $synced['errors'] );
}
wp_send_json_success( array( 'message' => $message, 'botId' => $bot_id, 'botName' => $botName ) );
} catch ( Throwable $e ) {
wp_send_json_error( array( 'message' => 'Fatal error: ' . $e->getMessage() ), 500 );
}
}
add_action( 'wp_ajax_rubik_chat_build_agent', 'rubik_chat_build_agent' );
function rubik_chat_retrain_agent() {
check_ajax_referer( 'rubik_chat_agent_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
}
try {
$bot_id = get_option( 'rubik_bot_id', '' );
if ( empty( $bot_id ) ) {
wp_send_json_error( array( 'message' => 'Agent Bot ID is missing. Please create an agent first.' ), 400 );
}
if ( get_option( 'rubik_token', '' ) === '' ) {
wp_send_json_error( array( 'message' => 'Your RubikChat session is missing. Please disconnect and connect again.' ), 401 );
}
$selected_page_ids = isset( $_POST['selected_pages'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['selected_pages'] ) ) : array();
$selected_page_ids = array_values( array_filter( $selected_page_ids ) );
if ( empty( $selected_page_ids ) ) {
wp_send_json_error( array( 'message' => 'No valid pages found to retrain.' ), 400 );
}
$config = get_option( 'rubik_agent_config', array() );
$kb = rubik_chat_ensure_knowledge_base( is_array( $config ) ? ( $config['botName'] ?? '' ) : '' );
if ( empty( $kb['ok'] ) ) {
wp_send_json_error( array( 'message' => $kb['error'] ), 200 );
}
$synced = rubik_chat_sync_page_sources( $kb['id'], $selected_page_ids );
if ( ! empty( $synced['errors'] ) && $synced['added'] + $synced['updated'] === 0 ) {
wp_send_json_error( array( 'message' => 'Retrain failed: ' . implode( '; ', $synced['errors'] ) ), 200 );
}
$selected_to_save = array();
foreach ( $selected_page_ids as $pid ) {
$selected_to_save[] = array(
'page_id' => $pid,
'title' => get_the_title( $pid ),
'url' => get_permalink( $pid ),
'character_count' => rubik_chat_page_character_count( $pid ),
);
}
update_option( 'rubik_selected_pages', $selected_to_save );
$message = 'Pages attached successfully and shortly updated data will be available to answer user questions.';
if ( ! empty( $synced['errors'] ) ) {
$message .= ' Some pages could not be updated: ' . implode( '; ', $synced['errors'] );
}
wp_send_json_success( array( 'message' => $message ) );
} catch ( Throwable $e ) {
wp_send_json_error( array( 'message' => 'Fatal error: ' . $e->getMessage() ), 500 );
}
}
add_action( 'wp_ajax_rubik_chat_retrain_agent', 'rubik_chat_retrain_agent' );
/* =====================================================
FRONTEND EMBED
===================================================== */
/**
* Playground test chat — proxy a message to RubikChat public/widget chat API.
*/
function rubik_chat_playground_message() {
check_ajax_referer( 'rubik_chat_agent_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Unauthorized.' ), 403 );
}
$message = isset( $_POST['message'] ) ? sanitize_text_field( wp_unslash( $_POST['message'] ) ) : '';
$bot_id = get_option( 'rubik_bot_id', '' );
if ( $message === '' || $bot_id === '' ) {
wp_send_json_error( array( 'message' => 'Missing message or Agent ID.' ) );
}
$user_id = get_current_user_id();
$conversation_key = 'rubik_playground_conversation_' . $user_id . '_' . md5( $bot_id );
$guest_user_id = get_user_meta( $user_id, 'rubik_playground_guest_id', true );
if ( ! $guest_user_id ) {
$guest_user_id = wp_generate_uuid4();
update_user_meta( $user_id, 'rubik_playground_guest_id', $guest_user_id );
}
// "New chat" from the admin screen starts a fresh conversation
if ( ! empty( $_POST['reset'] ) ) {
delete_transient( $conversation_key );
}
$conversation_id = get_transient( $conversation_key );
$url = RUBIK_CHAT_API_BASE . '/chat';
$args = array(
'timeout' => 90,
'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'text/plain' ),
'body' => wp_json_encode( array(
'agent' => $bot_id,
'message' => $message,
'conversation_id' => $conversation_id ? $conversation_id : wp_generate_uuid4(),
'guest_user_id' => $guest_user_id,
'source' => 'playground',
'media' => array(),
'queryParams' => (object) array(),
) ),
);
$response = wp_remote_post( $url, $args );
rubik_chat_log_api_call( $url, 'POST', $args, $response );
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => 'Could not reach RubikChat: ' . $response->get_error_message() ) );
}
$status = (int) wp_remote_retrieve_response_code( $response );
$raw = (string) wp_remote_retrieve_body( $response );
if ( $status < 200 || $status >= 300 ) {
$data = json_decode( $raw, true );
$msg = is_array( $data ) ? ( $data['message'] ?? $data['error'] ?? '' ) : '';
wp_send_json_error( array( 'message' => $msg ? $msg : 'Chat API error (HTTP ' . $status . ').' ) );
}
// The reply is a stream of "prefix: payload" lines (data:, completeMessage:, conversation:, ...)
$streamed = '';
$complete = '';
$error = '';
foreach ( preg_split( "/\r?\n/", $raw ) as $line ) {
if ( strpos( $line, 'completeMessage:' ) === 0 ) {
$complete = trim( substr( $line, strlen( 'completeMessage:' ) ) );
} elseif ( strpos( $line, 'data_ui:' ) === 0 ) {
continue;
} elseif ( strpos( $line, 'data:' ) === 0 ) {
$chunk = substr( $line, strlen( 'data:' ) );
$streamed .= ( strpos( $chunk, ' ' ) === 0 ) ? substr( $chunk, 1 ) : $chunk;
} elseif ( strpos( $line, 'conversation:' ) === 0 ) {
$conversation = json_decode( trim( substr( $line, strlen( 'conversation:' ) ) ), true );
if ( ! empty( $conversation[0]['id'] ) ) {
set_transient( $conversation_key, sanitize_text_field( $conversation[0]['id'] ), DAY_IN_SECONDS );
}
} elseif ( strpos( $line, 'credits:' ) === 0 ) {
$credits = json_decode( trim( substr( $line, strlen( 'credits:' ) ) ), true );
$error = is_array( $credits ) ? ( $credits['message'] ?? 'Your RubikChat plan has no messages left.' ) : 'Your RubikChat plan has no messages left.';
} elseif ( strpos( $line, 'error:' ) === 0 ) {
$decoded = json_decode( trim( substr( $line, strlen( 'error:' ) ) ), true );
$error = is_string( $decoded ) ? $decoded : trim( substr( $line, strlen( 'error:' ) ) );
}
}
$reply = $complete !== '' ? $complete : $streamed;
// The API marks line breaks as "
"
$reply = trim( preg_replace( '#\s*
\s*#i', "\n", $reply ) );
if ( $reply === '' ) {
wp_send_json_error( array( 'message' => $error !== '' ? $error : 'The agent did not return a reply. Please try again.' ) );
}
wp_send_json_success( array( 'reply' => $reply ) );
}
add_action( 'wp_ajax_rubik_chat_playground', 'rubik_chat_playground_message' );
function rubik_chat_embed_chatbot_script() {
$bot_id = get_option( 'rubik_bot_id', '' );
if ( empty( $bot_id ) ) {
return;
}
if ( get_option( 'rubik_connected', 'no' ) !== 'yes' ) {
return;
}
// Show Agent on Website toggle
if ( get_option( 'rubik_visible_agent', 'yes' ) === 'no' ) {
return;
}
$frontend_css = plugin_dir_path( __FILE__ ) . 'assets/frontend.css';
if ( file_exists( $frontend_css ) ) {
wp_enqueue_style(
'rubik-chat-frontend',
plugins_url( 'assets/frontend.css', __FILE__ ),
array(),
filemtime( $frontend_css )
);
}
$script_handle = 'rubik-chatbot-widget';
wp_enqueue_script(
$script_handle,
RUBIK_CHAT_WIDGET_SCRIPT,
array(),
'2.0.0',
true
);
wp_localize_script(
$script_handle,
'embeddedChatbotConfig',
array(
'chatbotId' => esc_attr( $bot_id ),
'domain' => 'widget.rubikchat.com',
)
);
}
add_action( 'wp_enqueue_scripts', 'rubik_chat_embed_chatbot_script' );
function rubik_chat_logs_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( isset( $_POST['clear_logs'] ) && check_admin_referer( 'rubik_chat_clear_logs' ) ) {
delete_option( 'rubikchat_api_logs' );
echo '';
}
$logs = get_option( 'rubikchat_api_logs', array() );
if ( ! is_array( $logs ) ) {
$logs = array();
}
?>
array_values( $allowed ),
'visitor_daily_message_cap' => $current['data']['visitor_daily_message_cap'] ?? null,
'max_message_length' => $current['data']['max_message_length'] ?? null,
), 20 );
if ( ! $saved['ok'] ) {
return false;
}
}
update_option( 'rubik_site_allowed', $bot_id . '|' . $domain );
return true;
}
/** Once a day at most, when the plugin's page loads: fixes agents created by 2.0.0. */
function rubik_chat_heal_allowed_site() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( get_option( 'rubik_bot_id', '' ) === '' || get_transient( 'rubik_site_allowed_check' ) ) {
return;
}
set_transient( 'rubik_site_allowed_check', 1, DAY_IN_SECONDS );
rubik_chat_allow_this_site();
}
add_action( 'load-toplevel_page_rubik-chat', 'rubik_chat_heal_allowed_site' );