PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.6
MxChat – AI Chatbot & Content Generation for WordPress v2.4.6
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-integrator.php +5241 -2014 2.0.32.4.6 View file →
@@ -9,50 +9,50 @@
9 9 private $chat_count;
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 + private $last_similarity_analysis = null;
13 14
15 +
16 +/**
17 + * Class constructor
18 + */
14 19 public function __construct() {
15 20 $this->options = get_option('mxchat_options');
16 21 $this->prompts_options = get_option('mxchat_prompts_options', array());
17 -
18 22 $this->chat_count = get_option('mxchat_chat_count', 0);
19 23 $this->word_handler = new MXChat_Word_Handler($this->options);
20 -
24 +
25 + // Add all action hooks
21 26 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 27 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 28 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 -
25 29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31 +
27 32 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 33 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 34 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30 -
31 35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33 -
34 37 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
35 38 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
36 -
37 - if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
38 - wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
39 - }
40 -
39 +
41 40 // Add REST API routes registration
42 41 add_action('rest_api_init', array($this, 'register_routes'));
43 -
44 42 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
45 43 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
46 -
44 +
45 + // Rate limit action - notice we removed the old schedule setup
47 46 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
48 -
47 +
48 + // File upload and handling actions
49 49 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 50 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 51 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 52 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53 -
54 - // Add these with your other add_action hooks
53 +
54 + // Word document handling actions
55 55 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 56 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
@@ -57,15 +57,47 @@
57 57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
59 59 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
60 60 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
61 -
61 +
62 + // Email handling actions
62 63 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
63 64 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 65 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
65 66 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
67 +
68 + add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
69 + add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
70 +
71 + // Testing panel AJAX actions
72 + add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
73 + add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
74 + add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
75 + add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
76 + // Add to your existing constructor, in the section with other AJAX actions:
77 + add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
78 + add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
79 + add_action('wp_ajax_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
80 + add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
81 + // Add chat mode checking actions
82 + add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
83 + add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
84 +
85 + add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
86 +
87 +
66 88 }
67 89
90 +// In your core plugin's check_actions_for_addons method:
91 +public function check_actions_for_addons($default, $message, $user_id, $session_id) {
92 + //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
93 +
94 + $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
95 +
96 + //error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
97 +
98 + return $result;
99 +}
68 100
69 101 private function mxchat_increment_chat_count() {
70 102 $chat_count = get_option('mxchat_chat_count', 0);
71 103 $chat_count++;
@@ -96,24 +128,9 @@
96 128 'chat_mode' => $chat_mode
97 129 ]);
98 130 wp_die();
99 131 }
100 -private function mxchat_fetch_conversation_history_for_ajax($session_id) {
101 - $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
102 - $formatted_history = [];
103 132
104 - // Format the history to align with the expected structure for OpenAI
105 - foreach ($history as $entry) {
106 - $formatted_history[] = [
107 - 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
108 - 'content' => $entry['content']
109 - ];
110 - }
111 -
112 - return $formatted_history;
113 -}
114 -
115 -
116 133 private function mxchat_fetch_conversation_history_for_ai($session_id) {
117 134 $history = get_option("mxchat_history_{$session_id}", []);
118 135 $formatted_history = [];
119 136
@@ -150,9 +167,9 @@
150 167 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
151 168 continue;
152 169 }
153 170
154 - // More accurate token estimation (1 token ≈ 4 characters)
171 + // More accurate token estimation (1 token ≈ 4 characters)
155 172 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
156 173
157 174 // Check token budget with the new estimate
158 175 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
@@ -206,8 +223,14 @@
206 223 'methods' => 'POST',
207 224 'callback' => [$this, 'handle_slack_interaction'],
208 225 'permission_callback' => [$this, 'verify_slack_request'],
209 226 ]);
227 +
228 + register_rest_route('mxchat/v1', '/slack-messages', [
229 + 'methods' => 'POST',
230 + 'callback' => [$this, 'handle_slack_messages'],
231 + 'permission_callback' => [$this, 'verify_slack_request'],
232 + ]);
210 233
211 234 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
212 235 }
213 236
@@ -295,20 +318,33 @@
295 318
296 319
297 320
298 321
299 -private function mxchat_save_chat_message($session_id, $role, $message) {
322 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
300 323 global $wpdb;
301 -
302 324 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
303 325 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
304 -
326 +
327 + // Check if this is the first message in a new session (before any other database operations)
328 + $is_new_session = false;
329 + if ($role === 'user') { // Only check for user messages, not bot responses
330 + $existing_messages = $wpdb->get_var($wpdb->prepare(
331 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
332 + $session_id
333 + ));
334 + $is_new_session = ($existing_messages == 0);
335 +
336 + // Log for debugging
337 + if ($is_new_session) {
338 + //error_log("[DEBUG] This is a NEW session - first message");
339 + }
340 + }
341 +
305 342 // 1) Extract agent name if present
306 343 $agent_name = '';
307 344 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
308 345 $agent_name = $matches[1];
309 346 $message = str_replace("Agent: $agent_name - ", '', $message);
310 -
311 347 $session_meta_key = "mxchat_agent_name_{$session_id}";
312 348 if (empty(get_option($session_meta_key))) {
313 349 update_option($session_meta_key, $agent_name);
314 350 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
@@ -313,42 +349,57 @@
313 349 update_option($session_meta_key, $agent_name);
314 350 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
315 351 }
316 352 }
317 -
353 +
318 354 // 2) Generate unique message_id
319 355 $message_id = uniqid();
320 356 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
321 -
357 +
322 358 // 3) Determine user_id
323 359 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
324 -
360 +
325 361 // 4) Determine user_identifier
326 362 $user_identifier = $agent_name
327 363 ? $agent_name
328 364 : MxChat_User::mxchat_get_user_identifier();
329 -
365 +
330 366 // 5) Determine displayed_name
331 367 $user_email = MxChat_User::mxchat_get_user_email();
332 368 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
333 -
369 +
334 370 // 6) Check for a saved email in wp_options
335 371 $email_option_key = "mxchat_email_{$session_id}";
336 372 $saved_email = get_option($email_option_key);
337 373 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
338 -
339 - // If found, update DB user_email
340 - if ($saved_email) {
341 - $update_res = $wpdb->update(
342 - $table_name,
343 - ['user_email' => $saved_email],
344 - ['session_id' => $session_id],
345 - ['%s'],
346 - ['%s']
347 - );
348 - //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
374 +
375 + // Check for a saved name in wp_options
376 + $name_option_key = "mxchat_name_{$session_id}";
377 + $saved_name = get_option($name_option_key);
378 + //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
379 +
380 + // If found, update DB user_email and user_name
381 + if ($saved_email || $saved_name) {
382 + $update_data = [];
383 + if ($saved_email) {
384 + $update_data['user_email'] = $saved_email;
385 + }
386 + if ($saved_name) {
387 + $update_data['user_name'] = $saved_name;
388 + }
389 +
390 + if (!empty($update_data)) {
391 + $update_res = $wpdb->update(
392 + $table_name,
393 + $update_data,
394 + ['session_id' => $session_id],
395 + array_fill(0, count($update_data), '%s'),
396 + ['%s']
397 + );
398 + //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email/user_name update for session_id {$session_id}. update_res: {$update_res}");
399 + }
349 400 }
350 -
401 +
351 402 // 7) Save to session history in wp_options
352 403 $history_key = "mxchat_history_{$session_id}";
353 404 $history = get_option($history_key, []);
354 405 $history[] = [
@@ -357,34 +408,149 @@
357 408 'content' => $message,
358 409 'timestamp' => round(microtime(true) * 1000),
359 410 'agent_name' => $displayed_name,
360 411 ];
361 - update_option($history_key, $history);
412 + update_option($history_key, $history, 'no');
362 413 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
363 -
414 +
364 415 // 8) Save the message to DB (INSERT)
365 416 $insert_data = [
366 417 'user_id' => $user_id,
367 418 'user_identifier'=> $user_identifier,
368 419 'user_email' => $saved_email ?: $user_email,
420 + 'user_name' => $saved_name ?: '', // Add name to insert data
369 421 'session_id' => $session_id,
370 422 'role' => $role,
371 423 'message' => $message,
372 424 'timestamp' => current_time('mysql', 1),
373 425 ];
426 +
427 + // IMPROVED: Handle originating page data
428 + $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
429 +
430 + if ($columns_exist) {
431 + if ($is_new_session && $role === 'user') {
432 + // For the first user message, set originating page data
433 +
434 + // First check if we have it from the parameter
435 + if ($originating_page && !empty($originating_page['url'])) {
436 + $insert_data['originating_page_url'] = $originating_page['url'];
437 + $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
438 +
439 + //error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
440 + }
441 + // Otherwise check if it's stored in the instance property
442 + else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
443 + $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
444 + $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
445 +
446 + //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
447 +
448 + // Clear after using
449 + unset($this->pending_originating_page);
450 + }
451 + // Fallback to HTTP_REFERER if nothing else is available
452 + else if (isset($_SERVER['HTTP_REFERER'])) {
453 + $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
454 + $insert_data['originating_page_url'] = $referer_url;
455 +
456 + // Generate title from URL
457 + $parsed_url = parse_url($referer_url);
458 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
459 +
460 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
461 + $insert_data['originating_page_title'] = 'Homepage';
462 + } else {
463 + $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
464 + $insert_data['originating_page_title'] = ucwords(trim($title));
465 + }
466 +
467 + //error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
468 + }
469 +
470 + // Store for this session so all messages have the same originating page
471 + if (!empty($insert_data['originating_page_url'])) {
472 + update_option("mxchat_originating_page_{$session_id}", [
473 + 'url' => $insert_data['originating_page_url'],
474 + 'title' => $insert_data['originating_page_title']
475 + ], 'no');
476 + }
477 + } else {
478 + // For subsequent messages in the session, use the stored originating page
479 + $stored_originating = get_option("mxchat_originating_page_{$session_id}");
480 + if ($stored_originating && !empty($stored_originating['url'])) {
481 + $insert_data['originating_page_url'] = $stored_originating['url'];
482 + $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
483 + }
484 + }
485 + }
486 +
374 487 $wpdb->insert($table_name, $insert_data);
375 488 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
376 -
489 +
490 + // 9) Send notification email if this is the first user message in a new session
491 + if ($wpdb->insert_id && $is_new_session && $role === 'user') {
492 + $this->send_new_chat_notification($session_id, array(
493 + 'identifier' => $user_identifier,
494 + 'email' => $saved_email ?: $user_email,
495 + 'ip' => $_SERVER['REMOTE_ADDR']
496 + ));
497 + }
498 +
377 499 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
378 500 return $message_id;
379 501 }
502 +private function send_new_chat_notification($session_id, $user_info = array()) {
503 + $options = get_option('mxchat_transcripts_options');
504 +
505 + // Check if notifications are enabled
506 + if (empty($options['mxchat_enable_notifications'])) {
507 + return false;
508 + }
509 +
510 + // Get notification email
511 + $to = !empty($options['mxchat_notification_email']) ?
512 + $options['mxchat_notification_email'] :
513 + get_option('admin_email');
514 +
515 + if (!is_email($to)) {
516 + return false;
517 + }
518 +
519 + // Prepare email content
520 + $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
521 +
522 + $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
523 + $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
524 + $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
525 +
526 + $message = sprintf(
527 + "A new chat session has started on your website.\n\n" .
528 + "Session ID: %s\n" .
529 + "User: %s\n" .
530 + "Email: %s\n" .
531 + "IP Address: %s\n" .
532 + "Time: %s\n\n" .
533 + "View transcripts: %s",
534 + $session_id,
535 + $user_identifier,
536 + $user_email,
537 + $user_ip,
538 + current_time('mysql'),
539 + admin_url('admin.php?page=mxchat-transcripts')
540 + );
541 +
542 + // Send email
543 + return wp_mail($to, $subject, $message);
544 +}
380 545
381 546 public function mxchat_handle_save_email_and_response() {
382 547 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
548 + //error_log('DEBUG: POST data: ' . print_r($_POST, true));
383 549
384 550 // Validate nonce
385 551 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
386 - error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
552 + //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
387 553 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
388 554 wp_die();
389 555 }
390 556
@@ -389,10 +555,11 @@
389 555 }
390 556
391 557 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
392 558 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
559 + $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
393 560
394 - //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
561 + //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
395 562
396 563 if (empty($session_id) || empty($email)) {
397 564 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
398 565 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
@@ -398,13 +565,31 @@
398 565 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
399 566 wp_die();
400 567 }
401 568
402 - // 1) Always store in wp_options
403 - $option_key = "mxchat_email_{$session_id}";
404 - update_option($option_key, $email);
405 - //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$option_key} => {$email}");
569 + // Validate name if provided (check if name field is enabled and name is required)
570 + $options = get_option('mxchat_options', []);
571 + $name_field_enabled = isset($options['enable_name_field']) &&
572 + ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
573 +
574 + if ($name_field_enabled && (empty($name) || strlen(trim($name)) < 2 || strlen(trim($name)) > 100)) {
575 + //error_log("[ERROR] Invalid name: {$name} (enabled: {$name_field_enabled})");
576 + wp_send_json_error(['message' => esc_html__('Name must be between 2 and 100 characters.', 'mxchat')]);
577 + wp_die();
578 + }
406 579
580 + // 1) Always store email in wp_options
581 + $email_option_key = "mxchat_email_{$session_id}";
582 + update_option($email_option_key, $email);
583 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
584 +
585 + // Store name in wp_options if provided
586 + if (!empty($name)) {
587 + $name_option_key = "mxchat_name_{$session_id}";
588 + update_option($name_option_key, $name);
589 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
590 + }
591 +
407 592 // 2) (Optional) Also store in DB if a row already exists
408 593 global $wpdb;
409 594 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
410 595
@@ -414,21 +599,30 @@
414 599
415 600 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
416 601
417 602 if ($session_count) {
418 - // Update user_email if row(s) exist
419 - $update_sql = $wpdb->prepare(
420 - "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
421 - $email,
422 - $session_id
423 - );
603 + // Update both user_email and user_name if row(s) exist
604 + if (!empty($name)) {
605 + $update_sql = $wpdb->prepare(
606 + "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
607 + $email,
608 + $name,
609 + $session_id
610 + );
611 + } else {
612 + $update_sql = $wpdb->prepare(
613 + "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
614 + $email,
615 + $session_id
616 + );
617 + }
424 618 $wpdb->query($update_sql);
425 619 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
426 620 } else {
427 - //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
621 + //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email/name is only in wp_options.");
428 622 }
429 623
430 - // Provide success response
624 + // Provide success response (same as original)
431 625 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
432 626 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
433 627 wp_send_json_success(['message' => $bot_message]);
434 628 wp_die();
@@ -451,113 +645,93 @@
451 645 // Check if the user is logged in
452 646 if (is_user_logged_in()) {
453 647 $current_user = wp_get_current_user();
454 648 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
455 - wp_send_json_success(['logged_in' => true, 'email' => $current_user->user_email]);
649 +
650 + // Get user's display name for logged in users
651 + $user_name = !empty($current_user->display_name) ? $current_user->display_name :
652 + (!empty($current_user->first_name) ? $current_user->first_name : '');
653 +
654 + $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
655 + if (!empty($user_name)) {
656 + $response_data['name'] = $user_name;
657 + }
658 +
659 + wp_send_json_success($response_data);
456 660 }
457 661
458 - $option_key = "mxchat_email_{$session_id}";
459 - $stored_email = get_option($option_key, '');
662 + // Check if name field is required
663 + $options = get_option('mxchat_options', []);
664 + $name_field_enabled = isset($options['enable_name_field']) &&
665 + ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
460 666
461 - //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
667 + $email_option_key = "mxchat_email_{$session_id}";
668 + $stored_email = get_option($email_option_key, '');
669 +
670 + // Check for stored name
671 + $name_option_key = "mxchat_name_{$session_id}";
672 + $stored_name = get_option($name_option_key, '');
462 673
463 - if (!empty($stored_email)) {
464 - //error_log("[DEBUG] mxchat_check_email_provided -> Email found, returning success");
465 - wp_send_json_success(['email' => $stored_email]);
466 - } else {
467 - //error_log("[DEBUG] mxchat_check_email_provided -> No email found, returning error");
468 - wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
469 - }
470 -}
674 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
675 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
471 676
472 -
473 -// First, add this helper function to get the highest rate limit for a user's roles
474 -private function get_user_role_rate_limit($user_id) {
475 - //error_log(esc_html__("Checking rate limit for user ID: ", 'mxchat') . $user_id);
476 -
477 - if (!$user_id) {
478 - //error_log(esc_html__("No user ID provided, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
479 - return $this->options['rate_limit_logged_out'] ?? '10';
677 + // Check if we have email and name (if name is required)
678 + $has_required_info = !empty($stored_email);
679 +
680 + if ($name_field_enabled) {
681 + $has_required_info = $has_required_info && !empty($stored_name);
480 682 }
481 683
482 - $user = get_userdata($user_id);
483 - if (!$user || !$user->roles) {
484 - //error_log(esc_html__("No user data or roles found, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
485 - return $this->options['rate_limit_logged_out'] ?? '10';
486 - }
487 -
488 - //error_log(esc_html__("User roles: ", 'mxchat') . print_r($user->roles, true));
489 - //error_log(esc_html__("Available role rate limits: ", 'mxchat') . print_r($this->options['role_rate_limits'] ?? [], true));
490 -
491 - $max_limit = 0;
492 - foreach ($user->roles as $role) {
493 - //error_log(esc_html__("Checking limit for role: ", 'mxchat') . $role);
494 - if (isset($this->options['role_rate_limits'][$role])) {
495 - $role_limit = $this->options['role_rate_limits'][$role];
496 - //error_log(esc_html__("Found limit for role ", 'mxchat') . $role . esc_html__(": ", 'mxchat') . $role_limit);
497 -
498 - if ($role_limit === 'unlimited') {
499 - //error_log(esc_html__("Returning unlimited for role: ", 'mxchat') . $role);
500 - return 'unlimited';
501 - }
502 -
503 - $max_limit = max($max_limit, (int)$role_limit);
504 - //error_log(esc_html__("Current max limit: ", 'mxchat') . $max_limit);
505 - } else {
506 - //error_log(esc_html__("No limit found for role: ", 'mxchat') . $role);
684 + if ($has_required_info) {
685 + //error_log("[DEBUG] mxchat_check_email_provided -> Required info found, returning success");
686 +
687 + $response_data = ['email' => $stored_email];
688 + if (!empty($stored_name)) {
689 + $response_data['name'] = $stored_name;
507 690 }
691 +
692 + wp_send_json_success($response_data);
693 + } else {
694 + //error_log("[DEBUG] mxchat_check_email_provided -> Required info missing, returning error");
695 + wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
508 696 }
509 -
510 - $final_limit = $max_limit > 0 ? (string)$max_limit : '100';
511 - //error_log(esc_html__("Final rate limit: ", 'mxchat') . $final_limit);
512 - return $final_limit;
513 697 }
514 698
699 +public function mxchat_handle_chat_request() {
700 + global $wpdb;
515 701
516 -// Add this to your plugin's main PHP file
517 -public function mxchat_check_new_messages() {
518 - if (!isset($_POST['session_id']) || !isset($_POST['last_seen_id'])) {
519 - wp_send_json_error(['message' => 'Missing required parameters']);
520 - wp_die();
521 - }
702 + // Debug: Log incoming bot_id
703 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
704 + error_log("=== MXCHAT DEBUG: Starting chat request ===");
705 + error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
706 +
707 + // Get bot-specific options
708 + $bot_options = $this->get_bot_options($bot_id);
709 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
522 710
523 - $session_id = sanitize_text_field($_POST['session_id']);
524 - $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
525 -
526 - // Get chat history
527 - $history = get_option("mxchat_history_{$session_id}", []);
528 -
529 - if (empty($history)) {
530 - wp_send_json_success([
531 - 'hasNewMessages' => false,
532 - 'new_messages' => []
533 - ]);
534 - wp_die();
711 + // Check if this is a streaming request
712 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
713 + isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on';
714 +
715 + // Set streaming headers if needed
716 + if ($is_streaming) {
717 + // Disable output buffering
718 + while (ob_get_level()) {
719 + ob_end_flush(); // Changed from ob_end_clean()
720 + }
721 +
722 + // Set headers for SSE
723 + header('Content-Type: text/event-stream');
724 + header('Cache-Control: no-cache');
725 + header('Connection: keep-alive');
726 + header('X-Accel-Buffering: no');
727 +
728 + // Add these new lines:
729 + ob_implicit_flush(true);
730 + flush();
535 731 }
536 732
537 - // Filter new messages
538 - $new_messages = array_filter($history, function($message) use ($last_seen_id) {
539 - return isset($message['id']) && $message['id'] > $last_seen_id;
540 - });
541 -
542 - // Sort by ID to ensure proper order
543 - usort($new_messages, function($a, $b) {
544 - return $a['id'] <=> $b['id'];
545 - });
546 -
547 - wp_send_json_success([
548 - 'hasNewMessages' => !empty($new_messages),
549 - 'new_messages' => array_values($new_messages),
550 - 'latestMessageId' => end($new_messages)['id'] ?? $last_seen_id
551 - ]);
552 - wp_die();
553 -}
554 -
555 -public function mxchat_handle_chat_request() {
556 - global $wpdb;
557 -
558 -
559 - // Check if MX Chat Moderation is active
733 + // Check if MX Chat Moderation is active
560 734 if (class_exists('MX_Chat_Moderation')) {
561 735 // Get user email and IP
562 736 $user_email = '';
563 737 $user_ip = $_SERVER['REMOTE_ADDR'];
@@ -591,14 +765,12 @@
591 765 wp_die();
592 766 }
593 767 }
594 768
595 -
596 - // Reset fallback response at the start of each request
597 769 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
598 770 $this->productCardHtml = '';
599 771
600 - // Get the actual WordPress user ID if logged in
772 + // Get the actual WordPress user ID if logged in
601 773 $is_logged_in = is_user_logged_in();
602 774 if ($is_logged_in) {
603 775 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
604 776 } else {
@@ -608,65 +780,24 @@
608 780
609 781 // Get and sanitize the user identifier
610 782 $user_id = sanitize_key($user_id);
611 783
612 - // Determine if user is logged in
613 - $is_logged_in = is_user_logged_in();
614 - //error_log("User logged in status: " . ($is_logged_in ? 'true' : 'false'));
784 + // Check rate limit using new settings structure
785 + $rate_limit_result = $this->check_rate_limit();
615 786
616 - // Get rate limit based on user status
617 - // Get rate limit based on user status
618 - $rate_limit = $is_logged_in
619 - ? $this->get_user_role_rate_limit($user_id)
620 - : ($this->options['rate_limit_logged_out'] ?? '10');
621 -
622 - //error_log("Selected rate limit: " . $rate_limit);
623 -
624 - // Rest of your code remains the same
625 - // If rate limit is 'unlimited', skip rate limiting checks
626 - if ($rate_limit !== 'unlimited') {
627 - // Convert rate limit to integer
628 - $rate_limit = intval($rate_limit);
629 - // Setup rate limiting
630 - $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
631 - $chat_count = get_transient($rate_limit_transient_key);
632 - if ($chat_count === false) {
633 - // Initialize new counter if none exists
634 - $chat_count = 0;
635 - }
636 - // Check if user has exceeded their rate limit
637 - if ($chat_count >= $rate_limit) {
638 - // Get custom rate limit message or use default
639 - $rate_limit_message = isset($this->options['rate_limit_message'])
640 - ? $this->options['rate_limit_message']
641 - : esc_html__('Rate limit exceeded. Please try again later.', 'mxchat');
642 - // Replace placeholder if it exists in the message
643 - $rate_limit_message = str_replace(
644 - array('{limit}', '{count}', '{remaining}'),
645 - array($rate_limit, $chat_count, max(0, $rate_limit - $chat_count)),
646 - $rate_limit_message
647 - );
648 - wp_send_json([
649 - 'success' => false,
650 - 'message' => $rate_limit_message,
651 - 'status' => 'rate_limit_exceeded',
652 - 'limit' => $rate_limit,
653 - 'count' => $chat_count
654 - ]);
655 - wp_die();
656 - }
657 - // Increment the counter
658 - $chat_count++;
659 - // Store the updated count with 24-hour expiration
660 - set_transient($rate_limit_transient_key, $chat_count, DAY_IN_SECONDS);
787 + if ($rate_limit_result !== true) {
788 + wp_send_json([
789 + 'success' => false,
790 + 'message' => $rate_limit_result['message'],
791 + 'status' => 'rate_limit_exceeded'
792 + ]);
793 + wp_die();
661 794 }
662 795
663 796 // Rest of your existing code...
664 797 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
665 - //error_log("Session ID: $session_id");
666 798
667 799 if (empty($session_id)) {
668 - //error_log("Error: Session ID is missing.");
669 800 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
670 801 wp_die();
671 802 }
672 803
@@ -671,116 +802,296 @@
671 802 }
672 803
673 804 // Validate and sanitize the incoming message
674 805 if (empty($_POST['message'])) {
675 - //error_log("Error: No message received.");
676 806 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
677 807 wp_die();
678 808 }
809 +
810 +
811 + // Track originating page for first message in session
812 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
679 813
814 + // Check if originating page columns exist
815 + $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
680 816
681 -// Modify the message sanitization to preserve PHP tags in code blocks
682 -$allowed_tags = [
683 - 'pre' => [],
684 - 'code' => ['class' => true],
685 - 'span' => ['class' => true],
686 - 'div' => ['class' => true],
687 -];
817 + if ($columns_exist) {
818 + // Check if this session already has messages
819 + $message_count = $wpdb->get_var($wpdb->prepare(
820 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
821 + $session_id
822 + ));
823 +
824 + // If this is the first message in the session
825 + if ($message_count == 0) {
826 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
827 + $originating_url = '';
828 + $originating_title = '';
829 +
830 + // Try to get from POST data first (sent by JavaScript)
831 + if (isset($_POST['current_page_url'])) {
832 + $originating_url = esc_url_raw($_POST['current_page_url']);
833 + $originating_title = isset($_POST['current_page_title'])
834 + ? sanitize_text_field($_POST['current_page_title'])
835 + : '';
836 + }
837 + // Fallback to HTTP_REFERER if not provided by JavaScript
838 + else if (isset($_SERVER['HTTP_REFERER'])) {
839 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
840 + }
841 +
842 + // Generate title if we have URL but no title
843 + if ($originating_url && empty($originating_title)) {
844 + $parsed_url = parse_url($originating_url);
845 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
846 +
847 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
848 + $originating_title = 'Homepage';
849 + } else {
850 + // Clean up the path to make a readable title
851 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
852 + $originating_title = ucwords(trim($originating_title));
853 + }
854 + }
855 +
856 + // Store for later use when saving the message
857 + $this->pending_originating_page = [
858 + 'url' => $originating_url,
859 + 'title' => $originating_title
860 + ];
861 + }
862 + }
863 +
864 +
688 865
689 -// First preserve code blocks
690 -$message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
691 - return htmlspecialchars_decode($matches[0]);
692 -}, $_POST['message']);
866 + // Get page context if provided
867 + $page_context = null;
868 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
869 + $page_context_raw = stripslashes($_POST['page_context']);
870 + $page_context = json_decode($page_context_raw, true);
871 +
872 + // Validate page context structure
873 + if (is_array($page_context) &&
874 + isset($page_context['url']) &&
875 + isset($page_context['title']) &&
876 + isset($page_context['content'])) {
877 +
878 + // Sanitize page context
879 + $page_context['url'] = esc_url_raw($page_context['url']);
880 + $page_context['title'] = sanitize_text_field($page_context['title']);
881 + $page_context['content'] = wp_kses_post($page_context['content']);
882 + } else {
883 + $page_context = null;
884 + }
885 + }
693 886
694 -// Then apply sanitization
695 -$message = wp_kses($message, $allowed_tags);
887 + // Modify the message sanitization to preserve PHP tags in code blocks
888 + $allowed_tags = [
889 + 'pre' => [],
890 + 'code' => ['class' => true],
891 + 'span' => ['class' => true],
892 + 'div' => ['class' => true],
893 + ];
696 894
697 -// Decode code blocks
698 -$message = preg_replace_callback('/(&lt;pre&gt;&lt;code.*?&gt;.*?&lt;\/code&gt;&lt;\/pre&gt;)/s', function($matches) {
699 - return htmlspecialchars_decode($matches[1]);
700 -}, $message);
895 + // First preserve code blocks
896 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
897 + return htmlspecialchars_decode($matches[0]);
898 + }, $_POST['message']);
701 899
702 -$message = trim($message);
900 + // Then apply sanitization
901 + $message = wp_kses($message, $allowed_tags);
703 902
704 -// Preserve code blocks from markdown conversion
705 -$message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
903 + // Preserve code blocks from markdown conversion
904 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
905 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
706 906
707 - // Save the user's message
708 - $this->mxchat_save_chat_message($session_id, 'user', $message);
907 + // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
908 + // Always initialize testing data for admins (no toggle needed)
909 + $testing_data = null;
910 + if (current_user_can('administrator')) {
911 + // For vision messages, use the original user message for the query display
912 + $query_for_testing = $message;
913 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
914 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
915 + }
916 +
917 + $testing_data = [
918 + 'query' => $query_for_testing,
919 + 'timestamp' => time(),
920 + 'top_matches' => [],
921 + 'action_matches' => [], // Initialize action matches array
922 + 'page_context' => $page_context, // Include page context in testing data
923 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
924 + 'bot_id' => $bot_id // Include bot ID in testing data
925 + ];
926 +
927 + // Get similarity threshold from bot options or default options
928 + $similarity_threshold = isset($current_options['similarity_threshold'])
929 + ? ((int) $current_options['similarity_threshold']) / 100
930 + : 0.35;
931 +
932 + $testing_data['similarity_threshold'] = $similarity_threshold;
933 +
934 + // Determine knowledge base type using bot-specific config
935 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
936 + $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
937 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
938 + }
939 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
709 940
710 - // Check if the message is an email address
711 - if (is_email($message)) {
712 - // Add the email to Loops
713 - $this->add_email_to_loops($message);
941 + // Add debug before and after:
942 + //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
943 + $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
944 + //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
714 945
715 - // Send success response
716 - $response_message = $this->options['email_capture_response'] ??
717 - esc_html__('Thank you! Your coupon is on the way!', 'mxchat');
718 946
719 - wp_send_json([
720 - 'success' => true,
721 - 'status' => 'email_captured',
722 - 'message' => $response_message
723 - ]);
724 - wp_die();
725 - }
947 + // If the pre-processing returned a result (not the original message), use it directly
948 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
949 + // Save the AI response
950 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
951 +
952 + // Save HTML content if provided
953 + if (!empty($pre_processed_result['html'])) {
954 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
955 + }
956 +
957 + // Add testing data if admin
958 + $response_data = [
959 + 'text' => $pre_processed_result['text'],
960 + 'html' => $pre_processed_result['html'] ?? '',
961 + 'session_id' => $session_id
962 + ];
963 +
964 + if ($testing_data !== null) {
965 + $response_data['testing_data'] = $testing_data;
966 + }
967 +
968 + wp_send_json($response_data);
969 + wp_die();
970 + }
726 971
727 - $intent_info = '';
972 + // Save the user's message - handle vision processed messages differently
973 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
974 + // For vision messages, save the original user message with image indicator
975 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
976 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
977 + $image_count = intval($_POST['vision_images_count']);
978 + $original_message .= " [{$image_count} image(s)]";
979 + }
980 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
981 + } else {
982 + // Regular message - save as normal
983 + $this->mxchat_save_chat_message($session_id, 'user', $message);
984 + }
728 985
729 - // Check chat mode
730 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
731 - //error_log("Chat Mode: $chat_mode");
986 +
987 + if (is_email($message)) {
988 + // Add the email to Loops
989 + $this->add_email_to_loops($message);
990 +
991 + // Get the user's success message instruction using current_options
992 + $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
993 +
994 + // Set instruction for AI using the user's success message
995 + $this->current_action_instruction = $user_success_message;
996 +
997 + // Clear the email capture transient since we got the email
998 + delete_transient('mxchat_email_capture_' . $user_id);
999 + }
1000 +
1001 + // Check if we're in an email capture flow but user hasn't provided email yet
1002 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1003 + // Check if the message contains an email (not the whole message being an email)
1004 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1005 + $extracted_email = $matches[0];
1006 +
1007 + // Add the extracted email to Loops
1008 + $this->add_email_to_loops($extracted_email);
1009 +
1010 + // Get the user's success message instruction using current_options
1011 + $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1012 +
1013 + // Set instruction for AI using the user's success message
1014 + $this->current_action_instruction = $user_success_message;
1015 +
1016 + // Clear the email capture transient since we got the email
1017 + delete_transient('mxchat_email_capture_' . $user_id);
1018 + }
1019 + // If no email found but we're in capture mode, remind them
1020 + else {
1021 + // Get the original instruction to remind them using current_options
1022 + $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1023 + $this->current_action_instruction = $original_instruction;
1024 + }
1025 + }
732 1026
733 - // Handle agent mode
734 - if ($chat_mode === 'agent') {
735 - // First, check for switch intent before doing anything else
736 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1027 + $intent_info = '';
737 1028
738 - // If we matched an intent and it's the switch intent, handle it
739 - if ($intent_matched && !empty($this->fallbackResponse['text'])) {
740 - //error_log("Switch to chatbot intent detected");
1029 + // Check chat mode
1030 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
741 1031
742 - // Update chat mode first
743 - update_option("mxchat_mode_{$session_id}", 'ai');
1032 + // Handle agent mode
1033 + // Handle agent mode
1034 + if ($chat_mode === 'agent') {
1035 + // First, check for switch intent before doing anything else
1036 + $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
744 1037
745 - // Clear any existing PDF context to start fresh
746 - $this->clear_pdf_transients($session_id);
1038 + // Capture action analysis for testing panel after intent check
1039 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1040 + $testing_data['action_matches'] = $this->last_action_analysis;
1041 + }
1042 +
1043 + // Around line 506, in the agent mode handling section:
1044 + if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1045 + // Update chat mode first
1046 + update_option("mxchat_mode_{$session_id}", 'ai');
1047 +
1048 + // Clear any existing PDF context to start fresh
1049 + $this->clear_pdf_transients($session_id);
1050 +
1051 + // Prepare clean switch response with explicit chat_mode
1052 + $response_data = [
1053 + 'text' => $this->fallbackResponse['text'],
1054 + 'html' => $this->fallbackResponse['html'] ?? '',
1055 + 'session_id' => $session_id,
1056 + 'chat_mode' => 'ai' // EXPLICITLY SET THIS
1057 + ];
1058 +
1059 + if ($testing_data !== null) {
1060 + $response_data['testing_data'] = $testing_data;
1061 + }
1062 +
1063 + // Save the mode switch message
1064 + $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1065 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1066 +
1067 + // Send response and exit
1068 + wp_send_json($response_data);
1069 + wp_die();
1070 + } elseif (!$intent_matched) {
1071 + // No intent matched, handle live agent message
1072 + try {
1073 + $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
747 1074
748 - // Prepare clean switch response
749 - $response_data = [
750 - 'text' => $this->fallbackResponse['text'],
751 - 'html' => '',
752 - 'session_id' => $session_id,
753 - 'chat_mode' => 'ai'
754 - ];
1075 + $agent_response = [
1076 + 'status' => 'waiting_for_agent',
1077 + 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1078 + ];
1079 +
1080 + if ($testing_data !== null) {
1081 + $agent_response['testing_data'] = $testing_data;
1082 + }
755 1083
756 - // Save the mode switch message
757 - $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
758 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
759 -
760 - // Send response and exit
761 - wp_send_json($response_data);
762 - wp_die();
763 - } elseif (!$intent_matched) {
764 - // No intent matched, handle live agent message
765 - try {
766 - $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
767 - //error_log("Message sent to agent.");
768 -
769 - wp_send_json_success([
770 - 'status' => 'waiting_for_agent',
771 - 'message' => esc_html__('Message sent to live agent.', 'mxchat')
772 - ]);
773 - } catch (\Exception $e) {
774 - //error_log("Error sending message to agent: " . $e->getMessage());
775 - wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1084 + wp_send_json_success($agent_response);
1085 + } catch (\Exception $e) {
1086 + wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1087 + }
1088 + wp_die();
776 1089 }
777 - wp_die();
778 1090 }
779 - }
780 1091
781 1092 // Step 1: Check for new PDF URL in the message
782 - if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1093 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
783 1094 $new_pdf_url = $matches[0];
784 1095
785 1096 // Check if this is likely a PDF-related request
786 1097 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
@@ -802,15 +1113,15 @@
802 1113
803 1114 // Clear previous PDF transients
804 1115 $this->clear_pdf_transients($session_id);
805 1116
806 - // Process new PDF
807 - $max_pages = $this->options['pdf_max_pages'] ?? 69;
1117 + // Process new PDF using current_options
1118 + $max_pages = $current_options['pdf_max_pages'] ?? 69;
808 1119 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
809 1120
810 1121 if ($embeddings === 'too_many_pages') {
811 1122 $error_text = sprintf(
812 - $this->options['pdf_intent_error_text'] ??
1123 + $current_options['pdf_intent_error_text'] ??
813 1124 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
814 1125 $max_pages
815 1126 );
816 1127 $this->fallbackResponse['text'] = $error_text;
@@ -815,15 +1126,13 @@
815 1126 );
816 1127 $this->fallbackResponse['text'] = $error_text;
817 1128 } elseif ($embeddings) {
818 1129 // Store new PDF information
819 - // Create a more meaningful filename from URL
820 1130 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
821 1131
822 - // If the filename is generic (like results_download.php), create a more descriptive one
1132 + // If the filename is generic, create a more descriptive one
823 1133 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
824 1134 strpos($pdf_filename, '.php') !== false) {
825 - // Create a timestamp-based name
826 1135 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
827 1136 }
828 1137
829 1138 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
@@ -830,77 +1139,224 @@
830 1139 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
831 1140 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
832 1141 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
833 1142
834 - $success_text = $this->options['pdf_intent_success_text'] ??
1143 + $success_text = $current_options['pdf_intent_success_text'] ??
835 1144 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
836 1145
837 - // Return success with filename for UI update
838 - wp_send_json([
1146 + $pdf_response = [
839 1147 'success' => true,
840 1148 'message' => $success_text,
841 1149 'data' => [
842 1150 'filename' => $pdf_filename
843 1151 ]
844 - ]);
1152 + ];
1153 +
1154 + if ($testing_data !== null) {
1155 + $pdf_response['testing_data'] = $testing_data;
1156 + }
1157 +
1158 + wp_send_json($pdf_response);
845 1159 wp_die();
846 1160 } else {
847 - $error_text = $this->options['pdf_intent_error_text'] ??
1161 + $error_text = $current_options['pdf_intent_error_text'] ??
848 1162 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
849 1163 $this->fallbackResponse['text'] = $error_text;
850 1164 }
851 1165
852 - wp_send_json([
1166 + $pdf_error_response = [
853 1167 'success' => false,
854 1168 'message' => $this->fallbackResponse['text']
855 - ]);
1169 + ];
1170 +
1171 + if ($testing_data !== null) {
1172 + $pdf_error_response['testing_data'] = $testing_data;
1173 + }
1174 +
1175 + wp_send_json($pdf_error_response);
856 1176 wp_die();
857 1177 }
858 1178 }
859 1179 }
860 1180
861 - // Step 2: Detect intent and handle intent-based responses
862 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
863 - //error_log("Intent Matched: " . ($intent_matched ? "Yes" : "No"));
1181 + // Check if there's an active recommendation flow session
1182 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1183 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1184 + // Create a dummy intent object that matches the original intent
1185 + $dummy_intent = new stdClass();
1186 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1187 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1188 +
1189 + // Call the recommendation flow handler directly
1190 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1191 +
1192 + // If the handler returned a response, send it
1193 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1194 + // Save the bot's response to the chat history
1195 + if (!empty($response_data['text'])) {
1196 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
1197 + }
1198 + if (!empty($response_data['html'])) {
1199 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1200 + }
1201 +
1202 + if ($testing_data !== null) {
1203 + $response_data['testing_data'] = $testing_data;
1204 + }
1205 +
1206 + // Send the response
1207 + wp_send_json($response_data);
1208 + wp_die();
1209 + }
1210 + }
864 1211
865 - // Step 3: If intent is matched and handled, respond immediately
866 - if ($intent_matched && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
867 - //error_log("Intent response triggered.");
868 - $response_data = [
869 - 'text' => $this->fallbackResponse['text'],
870 - 'html' => $this->fallbackResponse['html'],
871 - 'session_id' => $session_id
872 - ];
873 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
874 - wp_send_json($response_data);
875 - wp_die();
876 - }
1212 + // Step 2: Detect intent and handle intent-based responses
1213 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
877 1214
878 - // If no intent matched or product not found, proceed with AI response
879 - //error_log("No matching intent or fallback. Generating AI response.");
1215 + // Capture action analysis for testing panel after intent check
1216 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1217 + $testing_data['action_matches'] = $this->last_action_analysis;
1218 + }
880 1219
881 - // Step 4: Generate AI response
882 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
883 - $this->mxchat_increment_chat_count();
1220 + // Step 3: Handle the intent result appropriately
1221 + if ($intent_result !== false) {
1222 + // Intent was matched - ALWAYS send as JSON response, never streaming
1223 +
1224 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1225 + // Intent returned a direct response array
1226 + $response_data = [
1227 + 'text' => $intent_result['text'] ?? '',
1228 + 'html' => $intent_result['html'] ?? '',
1229 + 'session_id' => $session_id
1230 + ];
1231 +
1232 + if ($testing_data !== null) {
1233 + $response_data['testing_data'] = $testing_data;
1234 + }
1235 +
1236 + // Clear streaming headers if they were set
1237 + if ($is_streaming) {
1238 + header_remove('Content-Type');
1239 + header_remove('Cache-Control');
1240 + header_remove('Connection');
1241 + header_remove('X-Accel-Buffering');
1242 + header('Content-Type: application/json');
1243 + }
1244 +
1245 + wp_send_json($response_data);
1246 + wp_die();
1247 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1248 + // Intent returned true and set fallbackResponse
1249 +
1250 + // SAVE TO TRANSCRIPT FIRST
1251 + if (!empty($this->fallbackResponse['text'])) {
1252 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1253 + }
1254 + if (!empty($this->fallbackResponse['html'])) {
1255 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1256 + }
1257 +
1258 + $response_data = [
1259 + 'text' => $this->fallbackResponse['text'] ?? '',
1260 + 'html' => $this->fallbackResponse['html'] ?? '',
1261 + 'session_id' => $session_id
1262 + ];
1263 +
1264 + if (isset($this->fallbackResponse['chat_mode'])) {
1265 + $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1266 + }
1267 +
1268 + if ($testing_data !== null) {
1269 + $response_data['testing_data'] = $testing_data;
1270 + }
1271 +
1272 + // Clear streaming headers if they were set
1273 + if ($is_streaming) {
1274 + header_remove('Content-Type');
1275 + header_remove('Cache-Control');
1276 + header_remove('Connection');
1277 + header_remove('X-Accel-Buffering');
1278 + header('Content-Type: application/json');
1279 + }
1280 +
1281 + wp_send_json($response_data);
1282 + wp_die();
1283 + }
1284 + }
884 1285
885 - // Generate embedding for the user's query
886 - $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
887 - if (!is_array($user_message_embedding)) {
888 - //error_log("Failed to generate message embedding for session $session_id");
889 - wp_send_json_error(esc_html__('Error processing your message.', 'mxchat'));
890 - wp_die();
891 - }
1286 + // If we get here, no intent matched OR the intent didn't provide a usable response
1287 +
1288 + // Step 4: Generate AI response
1289 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1290 + $this->mxchat_increment_chat_count();
1291 +
1292 + // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1293 + $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1294 + $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1295 +
1296 + // Check if the embedding generation returned an error
1297 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1298 + $error_message = $user_message_embedding['error'];
1299 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1300 +
1301 + wp_send_json_error([
1302 + 'error_message' => $error_message,
1303 + 'error_code' => $error_code
1304 + ]);
1305 + wp_die();
1306 + }
1307 +
1308 + // Check if the embedding is valid
1309 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1310 + wp_send_json_error([
1311 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1312 + 'error_code' => 'invalid_embedding'
1313 + ]);
1314 + wp_die();
1315 + }
892 1316
893 - // Build context with both knowledge base and PDF content if available
894 - $context_content = "User asked: '{$message}'\n\n";
1317 + // Build context with both knowledge base and PDF content if available
1318 + $context_content = "User asked: '{$message}'\n\n";
1319 +
1320 + // Add action instruction if present (add this right after the above line)
1321 + if (!empty($this->current_action_instruction)) {
1322 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1323 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1324 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1325 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1326 +
1327 + // Clear the instruction after using it
1328 + $this->current_action_instruction = null;
1329 + }
895 1330
896 - // Get relevant content from knowledge base
897 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
898 - if (!empty($relevant_content)) {
899 - $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
900 - }
901 1331
1332 + // Add page context if available and contextual awareness is enabled using current_options
1333 + if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
1334 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1335 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1336 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1337 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1338 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1339 + }
902 1340
1341 + // Get relevant content from knowledge base - PASS BOT_ID
1342 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id);
1343 +
1344 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1345 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1346 + // Update testing data with the REAL similarity analysis
1347 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1348 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1349 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1350 + }
1351 + // ===== END SIMILARITY DATA CAPTURE =====
1352 +
1353 + if (!empty($relevant_content)) {
1354 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1355 + } else {
1356 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1357 + }
1358 +
903 1359 // Check for and include PDF content
904 1360 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
905 1361 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
906 1362 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
@@ -928,87 +1384,185 @@
928 1384 }
929 1385 $context_content .= "\n";
930 1386 }
931 1387 }
932 - // Generate the response using the full context
933 - $response = $this->mxchat_generate_response(
934 - $context_content,
935 - $this->options['api_key'],
936 - $this->options['xai_api_key'],
937 - $this->options['claude_api_key'],
938 - $this->options['deepseek_api_key'],
939 - $conversation_history
940 - );
1388 +
1389 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
941 1390
942 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1391 + // Extract model from current options for bot-specific model support
1392 + $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-4o';
1393 +
1394 + $response = $this->mxchat_generate_response(
1395 + $context_content,
1396 + $current_options['api_key'] ?? $this->options['api_key'],
1397 + $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1398 + $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1399 + $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1400 + $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1401 + $conversation_history,
1402 + $is_streaming,
1403 + $session_id,
1404 + $testing_data,
1405 + $selected_model // ADD THIS LINE
1406 + );
1407 +
1408 + // Handle streaming vs non-streaming responses
1409 + if ($is_streaming) {
1410 + // Check if streaming actually happened or if it fell back to regular response
1411 + if ($response === true) {
1412 + wp_die();
1413 + }
1414 + // If we get here, streaming fell back to regular response, continue
1415 + }
1416 +
1417 + // Check if the response is an error array
1418 + if (is_array($response) && isset($response['error'])) {
1419 + wp_send_json_error([
1420 + 'error_message' => $response['error'],
1421 + 'error_code' => $response['error_code'] ?? 'api_error'
1422 + ]);
1423 + wp_die();
1424 + }
1425 +
1426 + // If we get here, the response is valid text
1427 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
943 1428
944 - // Step 5: Save additional content if available
945 - if (!empty($this->productCardHtml)) {
946 - $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
947 - }
1429 + // Step 5: Save additional content if available
1430 + if (!empty($this->productCardHtml)) {
1431 + $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1432 + }
948 1433
949 - if (!empty($this->fallbackResponse['html'])) {
950 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
951 - }
1434 + if (!empty($this->fallbackResponse['html'])) {
1435 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1436 + }
952 1437
953 - // Step 6: Return the response
954 - $response_data = [
955 - 'text' => $response,
956 - 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
957 - 'session_id' => $session_id
958 - ];
1438 + // Step 6: Return the response
1439 + $response_data = [
1440 + 'text' => $response,
1441 + 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1442 + 'session_id' => $session_id
1443 + ];
959 1444
960 - wp_send_json($response_data);
961 - wp_die();
1445 + // Always add testing data for admins (no toggle needed)
1446 + if ($testing_data !== null) {
1447 + $response_data['testing_data'] = $testing_data;
1448 + }
1449 +
1450 + wp_send_json($response_data);
1451 + wp_die();
962 1452 }
963 1453
964 1454
965 -// Helper function to clear PDF and Word document related transients
966 -private function clear_pdf_transients($session_id) {
967 - // PDF transients
968 - delete_transient('mxchat_pdf_url_' . $session_id);
969 - delete_transient('mxchat_pdf_embeddings_' . $session_id);
970 - delete_transient('mxchat_include_pdf_in_context_' . $session_id);
971 - delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1455 +/**
1456 + * Get bot-specific options for multi-bot functionality
1457 + * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1458 + */
1459 +// Also debug the bot options retrieval
1460 +private function get_bot_options($bot_id = 'default') {
1461 + error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1462 +
1463 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1464 + error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1465 + return array();
1466 + }
1467 +
1468 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1469 +
1470 + if (!empty($bot_options)) {
1471 + error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1472 + if (isset($bot_options['similarity_threshold'])) {
1473 + error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1474 + }
1475 + }
1476 +
1477 + return is_array($bot_options) ? $bot_options : array();
1478 +}
972 1479
973 - // Word document transients
974 - delete_transient('mxchat_word_url_' . $session_id);
975 - delete_transient('mxchat_word_filename_' . $session_id);
976 - delete_transient('mxchat_word_embeddings_' . $session_id);
977 - delete_transient('mxchat_include_word_in_context_' . $session_id);
978 - delete_transient('mxchat_waiting_for_word_' . $session_id);
1480 +/**
1481 + * Get bot-specific Pinecone configuration
1482 + * Used in the knowledge retrieval functions
1483 + */
1484 +// Also add debugging to your get_bot_pinecone_config function
1485 +private function get_bot_pinecone_config($bot_id = 'default') {
1486 + error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1487 +
1488 + // If default bot or multi-bot add-on not active, use default Pinecone config
1489 + if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1490 + error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1491 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
1492 + $config = array(
1493 + 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1494 + 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1495 + 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1496 + 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1497 + );
1498 + error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1499 + return $config;
1500 + }
1501 +
1502 + error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1503 +
1504 + // Hook for multi-bot add-on to provide bot-specific Pinecone config
1505 + $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1506 +
1507 + if (!empty($bot_pinecone_config)) {
1508 + error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1509 + error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1510 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1511 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1512 + } else {
1513 + error_log("MXCHAT DEBUG: Filter returned empty config!");
1514 + }
1515 +
1516 + return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
979 1517 }
980 1518
981 -// New function to check intents and invoke the callback function
1519 +
1520 +// Updated function to check intents and invoke the callback function
982 1521 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
983 1522 global $wpdb;
984 1523 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
985 1524
986 - //error_log("[MxChat] Checking intents for message: " . $message);
1525 + // NEW: Get the current bot_id
1526 + $current_bot_id = $this->get_current_bot_id($session_id);
987 1527
988 1528 // Generate the user embedding
989 1529 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
990 - if (!is_array($user_embedding)) {
991 - //error_log("[MxChat] Failed to generate user embedding");
992 - return false;
1530 +
1531 + // Check if embedding generation returned an error
1532 + if (is_array($user_embedding) && isset($user_embedding['error'])) {
1533 + $error_message = $user_embedding['error'];
1534 + $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1535 +
1536 + wp_send_json_error([
1537 + 'error_message' => $error_message,
1538 + 'error_code' => $error_code
1539 + ]);
1540 + wp_die();
993 1541 }
994 1542
1543 + // Check if embedding is valid
1544 + if (!is_array($user_embedding) || empty($user_embedding)) {
1545 + wp_send_json_error([
1546 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1547 + 'error_code' => 'invalid_embedding'
1548 + ]);
1549 + wp_die();
1550 + }
1551 +
995 1552 // Fetch intents from the database
996 1553 $table_name = $wpdb->prefix . 'mxchat_intents';
997 1554 if ($chat_mode === 'agent') {
998 1555 $query = $wpdb->prepare(
999 - "SELECT * FROM $table_name WHERE callback_function = %s",
1556 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
1000 1557 'mxchat_handle_switch_to_chatbot_intent'
1001 1558 );
1002 1559 $intents = $wpdb->get_results($query);
1003 1560 } else {
1004 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
1561 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
1005 1562 }
1006 1563
1007 - //error_log("[MxChat] Found " . count($intents) . " intents to check");
1008 -
1009 1564 if (empty($intents)) {
1010 - //error_log("[MxChat] No intents found in database");
1011 1565 return false;
1012 1566 }
1013 1567
1014 1568 $highest_similarity = -INF;
@@ -1013,11 +1567,23 @@
1013 1567
1014 1568 $highest_similarity = -INF;
1015 1569 $matched_intent = null;
1016 1570
1571 + // Array to store action analysis for testing panel
1572 + $action_analysis = [];
1573 +
1017 1574 foreach ($intents as $intent) {
1018 - //error_log("[MxChat] Checking intent: " . $intent->intent_label . " (callback: " . $intent->callback_function . ")");
1575 + // Additional check for enabled state
1576 + $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1577 + if (!$is_enabled) {
1578 + continue;
1579 + }
1019 1580
1581 + // NEW: Check if this action is enabled for the current bot
1582 + if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
1583 + continue;
1584 + }
1585 +
1020 1586 $intent_embedding_serialized = $intent->embedding_vector;
1021 1587 $intent_embedding = $intent_embedding_serialized
1022 1588 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1023 1589 : null;
@@ -1022,9 +1588,8 @@
1022 1588 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1023 1589 : null;
1024 1590
1025 1591 if (!is_array($intent_embedding)) {
1026 - //error_log("[MxChat] Invalid embedding for intent: " . $intent->intent_label);
1027 1592 continue;
1028 1593 }
1029 1594
1030 1595 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
@@ -1029,20 +1594,46 @@
1029 1594
1030 1595 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1031 1596 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1032 1597
1033 - //error_log("[MxChat] Similarity for {$intent->intent_label}: {$similarity} (threshold: {$intent_threshold})");
1598 + // Store action analysis data for testing panel
1599 + $action_analysis[] = [
1600 + 'intent_label' => $intent->intent_label,
1601 + 'callback_function' => $intent->callback_function,
1602 + 'similarity' => round($similarity, 4),
1603 + 'similarity_percentage' => round($similarity * 100, 2),
1604 + 'threshold' => $intent_threshold,
1605 + 'threshold_percentage' => round($intent_threshold * 100, 2),
1606 + 'above_threshold' => $similarity >= $intent_threshold,
1607 + 'triggered' => false // Will be updated below if this intent is triggered
1608 + ];
1034 1609
1035 1610 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1036 1611 $highest_similarity = $similarity;
1037 1612 $matched_intent = $intent;
1038 - //error_log("[MxChat] New best match: {$intent->intent_label} with similarity {$similarity}");
1039 1613 }
1040 1614 }
1041 1615
1616 + // Mark the triggered action if any
1042 1617 if ($matched_intent) {
1043 - //error_log("[MxChat] Invoking callback: " . $matched_intent->callback_function);
1044 -
1618 + foreach ($action_analysis as &$action) {
1619 + if ($action['intent_label'] === $matched_intent->intent_label) {
1620 + $action['triggered'] = true;
1621 + break;
1622 + }
1623 + }
1624 + }
1625 +
1626 + // Sort actions by similarity (highest first) and store for testing panel
1627 + usort($action_analysis, function($a, $b) {
1628 + return $b['similarity'] <=> $a['similarity'];
1629 + });
1630 +
1631 + // Store action analysis for testing panel capture
1632 + $this->last_action_analysis = $action_analysis;
1633 +
1634 + // Around line 715 in your mxchat_check_intent_and_invoke_callback function
1635 + if ($matched_intent) {
1045 1636 // If the callback is a method on this instance (core callback), call it directly
1046 1637 if (method_exists($this, $matched_intent->callback_function)) {
1047 1638 $callback_result = call_user_func(
1048 1639 [$this, $matched_intent->callback_function],
@@ -1048,15 +1639,16 @@
1048 1639 [$this, $matched_intent->callback_function],
1049 1640 $message,
1050 1641 $user_id,
1051 1642 $session_id,
1052 - $matched_intent
1643 + $matched_intent,
1644 + $user_context ?? null
1053 1645 );
1054 1646 } else {
1055 1647 // Otherwise, use apply_filters for add-on callbacks
1056 1648 $callback_result = apply_filters(
1057 1649 $matched_intent->callback_function,
1058 - false, // default return value
1650 + false,
1059 1651 $message,
1060 1652 $user_id,
1061 1653 $session_id,
1062 1654 $matched_intent
@@ -1062,214 +1654,132 @@
1062 1654 $matched_intent
1063 1655 );
1064 1656 }
1065 1657
1066 - //error_log("[MxChat] Callback result: " . print_r($callback_result, true));
1658 + // Handle the callback result properly
1067 1659 if ($callback_result !== false) {
1068 - $this->fallbackResponse = $callback_result;
1069 - return true;
1660 + // If callback returned an array with chat_mode, use it directly
1661 + if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
1662 + $this->fallbackResponse = $callback_result;
1663 + return $callback_result; // Return the full array
1664 + } else {
1665 + $this->fallbackResponse = $callback_result;
1666 + return true;
1667 + }
1070 1668 }
1071 1669 }
1072 1670
1073 - //error_log("[MxChat] No matching intent found");
1074 1671 return false;
1075 1672 }
1076 1673
1077 -
1078 -//verified good
1079 -public function mxchat_handle_order_history($message, $user_id, $session_id) {
1080 - if (!class_exists('WooCommerce')) {
1081 - $this->fallbackResponse['text'] = esc_html__("I can't access order information right now. The order system seems to be unavailable.", 'mxchat');
1674 +/**
1675 + * Check if an action is enabled for a specific bot
1676 + */
1677 +private function is_action_enabled_for_bot($intent, $bot_id) {
1678 + // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
1679 + if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
1082 1680 return true;
1083 1681 }
1084 -
1085 - $orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details('all');
1086 -
1087 - if (empty($orderDetails)) {
1088 - $this->fallbackResponse['text'] = esc_html__("I don't see any orders associated with your account. Are you logged in?", 'mxchat');
1682 +
1683 + $enabled_bots = json_decode($intent->enabled_bots, true);
1684 +
1685 + // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
1686 + if (!is_array($enabled_bots) || empty($enabled_bots)) {
1089 1687 return true;
1090 1688 }
1689 +
1690 + // Check if the current bot is in the enabled bots list
1691 + return in_array($bot_id, $enabled_bots);
1692 +}
1091 1693
1092 - // Generate AI prompt with context
1093 - $prompt = __("User asked about their orders:", 'mxchat') . " '{$message}'\n\n";
1094 - $prompt .= __("Order information:", 'mxchat') . "\n";
1095 - foreach ($orderDetails as $order) {
1096 - $items_list = array_map(function($item) {
1097 - return "{$item['name']} ({$item['quantity']})";
1098 - }, $order['items']);
1694 +// Helper function to clear PDF and Word document related transients
1695 +private function clear_pdf_transients($session_id) {
1696 + // PDF transients
1697 + delete_transient('mxchat_pdf_url_' . $session_id);
1698 + delete_transient('mxchat_pdf_embeddings_' . $session_id);
1699 + delete_transient('mxchat_include_pdf_in_context_' . $session_id);
1700 + delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1099 1701
1100 - $prompt .= __("Order #", 'mxchat') . "{$order['order_id']}: {$order['formatted_total']} " . __("on", 'mxchat') . " {$order['date']}\n";
1101 - $prompt .= __("Items:", 'mxchat') . " " . implode(', ', $items_list) . "\n";
1102 - }
1103 -
1104 - $prompt .= __("\nProvide a natural, conversational response focusing on the specific information the user asked about. ", 'mxchat');
1105 - $prompt .= __("If they ask about a specific order or detail, provide just that information. ", 'mxchat');
1106 - $prompt .= __("If they ask about license keys or sensitive information, inform them to check their email or contact support.", 'mxchat');
1107 -
1108 - // Get AI response
1109 - $ai_response = $this->mxchat_call_ai_api($prompt);
1110 - $this->fallbackResponse['text'] = $ai_response['text'];
1111 -
1112 - return true;
1702 + // Word document transients
1703 + delete_transient('mxchat_word_url_' . $session_id);
1704 + delete_transient('mxchat_word_filename_' . $session_id);
1705 + delete_transient('mxchat_word_embeddings_' . $session_id);
1706 + delete_transient('mxchat_include_word_in_context_' . $session_id);
1707 + delete_transient('mxchat_waiting_for_word_' . $session_id);
1113 1708 }
1114 1709
1115 1710
1116 -public function mxchat_handle_product_inquiry($message, $user_id, $session_id) {
1117 - //error_log("PRODUCT INQUIRY START - Message: $message, User ID: $user_id");
1118 1711
1119 - // Sanitize user ID
1120 - $sanitized_user_id = sanitize_key($user_id);
1121 - //error_log("Sanitized User ID: $sanitized_user_id");
1122 -
1123 - // Find product
1124 - $product_id = $this->find_product_in_message($message);
1125 - //error_log("Initial product ID from message: " . ($product_id ?? 'null'));
1126 -
1127 - // Check transient if no product found
1128 - if (!$product_id) {
1129 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1130 - //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1131 - }
1132 -
1133 - // Handle product inquiries
1134 - if ($product_id && class_exists('WooCommerce')) {
1135 - $product = wc_get_product($product_id);
1136 - if ($product) {
1137 - //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1138 -
1139 - // Prepare product details
1140 - $product_name = esc_html($product->get_name());
1141 - $product_price = $product->get_price_html();
1142 - $product_image_url = esc_url(wp_get_attachment_url($product->get_image_id()));
1143 - $product_url = esc_url(get_permalink($product_id));
1144 - $product_id_attr = esc_attr($product_id);
1145 -
1146 - // Get product description
1147 - $product_description = $product->get_description() ?: $product->get_short_description();
1148 -
1149 - // Log embeddings process
1150 - //error_log("Generating embedding for message: $message");
1151 - $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1152 - //error_log("Finding relevant content for product inquiry");
1153 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1154 -
1155 - // Build AI prompt
1156 - $ai_prompt = esc_html__("You are a knowledgeable product assistant. ", 'mxchat');
1157 - $ai_prompt .= esc_html__("Respond to this user query: '{$message}'\n\n", 'mxchat');
1158 -
1159 - if (!empty($relevant_content)) {
1160 - $ai_prompt .= esc_html__("Relevant information from our knowledge base:\n{$relevant_content}\n\n", 'mxchat');
1161 - }
1162 -
1163 - $ai_prompt .= esc_html__("Product details:\n", 'mxchat');
1164 - $ai_prompt .= esc_html__("Name: {$product_name}\n", 'mxchat');
1165 - $ai_prompt .= esc_html__("Price: ", 'mxchat') . strip_tags($product_price) . "\n";
1166 - if ($product_description) {
1167 - $ai_prompt .= esc_html__("Description: {$product_description}\n", 'mxchat');
1168 - }
1169 -
1170 - $ai_prompt .= esc_html__("\nInstructions:\n", 'mxchat');
1171 - $ai_prompt .= esc_html__("1. Address the user's specific question or concern about the product\n", 'mxchat');
1172 - $ai_prompt .= esc_html__("2. Incorporate relevant information from our knowledge base if provided\n", 'mxchat');
1173 - $ai_prompt .= esc_html__("3. Highlight key product features that relate to their query\n", 'mxchat');
1174 - $ai_prompt .= esc_html__("4. Include a natural suggestion to check out the product\n", 'mxchat');
1175 - $ai_prompt .= esc_html__("5. Keep the response conversational and helpful\n", 'mxchat');
1176 -
1177 - //error_log("Sending AI prompt: " . $ai_prompt);
1178 -
1179 - // Build and log AI response
1180 - $ai_response = $this->mxchat_call_ai_api($ai_prompt);
1181 - //error_log("AI Response received for product inquiry: " . print_r($ai_response, true));
1182 -
1183 - // Generate and log product card
1184 - $product_card_html = <<<HTML
1185 -<div class="mxchat-product-card">
1186 - <a href="{$product_url}" target="_blank">
1187 - <img src="{$product_image_url}" alt="{$product_name}" class="mxchat-product-image" />
1188 - <h3 class="mxchat-product-name">{$product_name}</h3>
1189 - </a>
1190 - <div class="mxchat-product-price">{$product_price}</div>
1191 - <button class="mxchat-add-to-cart-button" data-product-id="{$product_id_attr}">Add to Cart</button>
1192 -</div>
1193 -HTML;
1194 -
1195 - // Save response and set transient
1196 - $this->productCardHtml = $product_card_html;
1197 - $this->fallbackResponse = [
1198 - 'text' => $ai_response['text'],
1199 - 'html' => $this->productCardHtml,
1200 - ];
1201 -
1202 - //error_log("Setting transient for product: $product_id with key: mxchat_last_discussed_product_$sanitized_user_id");
1203 - set_transient('mxchat_last_discussed_product_' . $sanitized_user_id, $product_id, HOUR_IN_SECONDS);
1204 -
1205 - // Verify transient was set
1206 - $verify_transient = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1207 - //error_log("Verification - Retrieved transient value: " . ($verify_transient ?? 'null'));
1208 -
1209 - return true;
1210 - }
1211 - }
1212 -
1213 - //error_log("PRODUCT INQUIRY END - No product found or WooCommerce not available");
1214 - return false;
1215 -}
1216 -
1217 1712 //verified good
1218 1713 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1219 - // Log the message safely
1220 - //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1221 -
1222 - // Initiate email capture flow
1223 - $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1224 -
1714 + // Get the user's original instruction/message
1715 + $user_instruction = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Please provide your email address.", 'mxchat'));
1716 +
1717 + // Set instruction for AI - just pass along what the user wanted to say
1718 + $this->current_action_instruction = $user_instruction;
1719 +
1720 + // Set the transient to track email capture flow
1225 1721 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1226 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1227 -
1228 - // Respond to the user
1229 - wp_send_json(['message' => $response]);
1230 - wp_die();
1722 +
1723 + // Return false to let the AI generate the response
1724 + return false;
1231 1725 }
1232 1726
1233 -//very good
1234 1727 public function mxchat_generate_image($message, $user_id, $session_id) {
1728 + //error_log("Starting image generation for message: " . $message);
1729 +
1235 1730 // Prepare a prompt for DALL-E
1236 1731 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1237 -
1732 +
1238 1733 // Use the existing OpenAI API key
1239 1734 $openai_api_key = sanitize_text_field($this->options['api_key']);
1240 -
1735 +
1241 1736 // Call DALL-E to generate an image
1242 1737 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1243 -
1738 +
1244 1739 // Check if the response contains an image URL
1245 1740 if (isset($image_response['imageUrl'])) {
1246 1741 $image_url = esc_url_raw($image_response['imageUrl']);
1247 -
1742 +
1248 1743 // Construct the HTML with a CSS class instead of inline styles
1249 1744 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1745 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1746 +
1747 + // Save the bot message with both text and HTML
1748 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1749 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1750 +
1751 + // Set the fallback response for the chat handler
1752 + $this->fallbackResponse = [
1753 + 'text' => $response_text,
1754 + 'html' => $response_html,
1755 + 'images' => [$image_url]
1756 + ];
1757 +
1758 + // For debugging/verification - Use json_encode to verify what's being set
1759 + //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1250 1760
1251 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1761 + // Return the response directly instead of relying on the property
1762 + return $this->fallbackResponse;
1252 1763 } else {
1253 1764 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1254 - $response_html = '';
1765 +
1766 + // Save the error message
1767 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1768 +
1769 + // Set the fallback response for the chat handler
1770 + $this->fallbackResponse = [
1771 + 'text' => $response_text,
1772 + 'html' => '',
1773 + 'images' => []
1774 + ];
1775 +
1255 1776 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1777 + //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1778 +
1779 + // Return the response directly instead of relying on the property
1780 + return $this->fallbackResponse;
1256 1781 }
1257 -
1258 - // Save both text and HTML responses
1259 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
1260 -
1261 - // Prepare the response data
1262 - $response_data = [
1263 - 'message' => $response_text,
1264 - 'html' => $response_html,
1265 - 'image_url' => $image_url ?? '',
1266 - ];
1267 -
1268 - // Send the JSON response
1269 - header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1270 - echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1271 - wp_die();
1272 1782 }
1273 1783 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1274 1784 $api_url = 'https://api.openai.com/v1/images/generations';
1275 1785 $body = json_encode([
@@ -1308,44 +1818,43 @@
1308 1818
1309 1819 /**
1310 1820 * Handle web search requests.
1311 1821 *
1312 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1313 - * styled search results. Results are cached for performance.
1822 + * Sends the refined search query to the Brave Search API and uses the
1823 + * results to generate a conversational response with the AI model.
1314 1824 *
1315 1825 * @since 1.0.0
1316 1826 * @param string $message The user's search query.
1317 1827 * @param string $user_id The user identifier.
1318 1828 * @param string $session_id The current session ID.
1319 - * @return void
1829 + * @return array Response array containing text with embedded HTML links
1320 1830 */
1321 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1831 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1322 1832 // Step 1: Interpret and refine the search query
1323 - $refined_search_query = $this->mxchat_interpret_search_query( $message );
1324 -
1325 - if ( empty( $refined_search_query ) ) {
1326 - $this->fallbackResponse = array(
1327 - 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
1833 + $refined_search_query = $this->mxchat_interpret_search_query($message);
1834 + if (empty($refined_search_query)) {
1835 + return array(
1836 + 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1837 + 'html' => ''
1328 1838 );
1329 - return;
1330 1839 }
1331 -
1840 +
1332 1841 // Retrieve and validate API settings
1333 - $options = get_option( 'mxchat_options' );
1334 - $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1335 - $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1336 -
1337 - if ( empty( $api_key ) ) {
1338 - $this->fallbackResponse = array(
1339 - 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
1842 + $options = get_option('mxchat_options');
1843 + $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1844 + $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1845 +
1846 + if (empty($api_key)) {
1847 + return array(
1848 + 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1849 + 'html' => ''
1340 1850 );
1341 - return;
1342 1851 }
1343 -
1852 +
1344 1853 // Build the API request URL
1345 1854 $api_url = add_query_arg(
1346 1855 array(
1347 - 'q' => rawurlencode( $refined_search_query ),
1856 + 'q' => rawurlencode($refined_search_query),
1348 1857 'count' => $results_count,
1349 1858 'text_decorations' => 'true',
1350 1859 'rich_data' => 'true',
1351 1860 ),
@@ -1350,14 +1859,14 @@
1350 1859 'rich_data' => 'true',
1351 1860 ),
1352 1861 'https://api.search.brave.com/res/v1/web/search'
1353 1862 );
1354 -
1863 +
1355 1864 // Attempt to retrieve cached results first
1356 - $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1357 - $results = get_transient( $transient_key );
1358 -
1359 - if ( false === $results ) {
1865 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
1866 + $results = get_transient($transient_key);
1867 +
1868 + if (false === $results) {
1360 1869 // Fetch new results from the Brave Search API
1361 1870 $response = wp_remote_get(
1362 1871 $api_url,
1363 1872 array(
@@ -1368,162 +1877,98 @@
1368 1877 ),
1369 1878 'timeout' => 10,
1370 1879 )
1371 1880 );
1372 -
1373 - if ( is_wp_error( $response ) ) {
1374 - $this->fallbackResponse = array(
1375 - 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
1881 +
1882 + if (is_wp_error($response)) {
1883 + return array(
1884 + 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1885 + 'html' => ''
1376 1886 );
1377 - return;
1378 1887 }
1379 -
1380 - $results = json_decode( wp_remote_retrieve_body( $response ), true );
1381 -
1382 - if ( json_last_error() !== JSON_ERROR_NONE ) {
1383 - $this->fallbackResponse = array(
1384 - 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
1888 +
1889 + $results = json_decode(wp_remote_retrieve_body($response), true);
1890 +
1891 + if (json_last_error() !== JSON_ERROR_NONE) {
1892 + return array(
1893 + 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1894 + 'html' => ''
1385 1895 );
1386 - return;
1387 1896 }
1388 -
1897 +
1389 1898 // Cache results for one hour
1390 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1899 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1391 1900 }
1392 -
1393 - // Process and display results
1394 - if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1395 - $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1396 -
1397 - // Only return HTML (no large text summary)
1398 - $this->fallbackResponse = array(
1399 - 'html' => $html,
1901 +
1902 + // Process results
1903 + if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1904 + // Create a more straightforward summary with HTML links
1905 + $search_results_text = '';
1906 +
1907 + // Add a simple intro
1908 + $search_results_text .= sprintf(
1909 + esc_html__("Here's what I found about '%s':", 'mxchat'),
1910 + esc_html($refined_search_query)
1400 1911 );
1401 -
1912 +
1913 + // Add the top results with HTML links
1914 + foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1915 + $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1916 + $url = isset($result['url']) ? esc_url($result['url']) : '';
1917 + $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1918 +
1919 + // Add a line break after the intro
1920 + $search_results_text .= '<br><br>';
1921 +
1922 + // Add title as a link
1923 + $search_results_text .= sprintf(
1924 + '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1925 + $url,
1926 + $title
1927 + );
1928 +
1929 + // Add a condensed description
1930 + $search_results_text .= sprintf("%s", $description);
1931 + }
1932 +
1402 1933 // Save to chat history
1403 - $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1934 + $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1935 +
1936 + // Return the formatted text with embedded HTML links
1937 + return array(
1938 + 'text' => $search_results_text,
1939 + 'html' => ''
1940 + );
1404 1941 } else {
1405 - $this->fallbackResponse = array(
1942 + return array(
1406 1943 'text' => sprintf(
1407 - esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1408 - esc_html( $refined_search_query )
1944 + esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1945 + esc_html($refined_search_query)
1409 1946 ),
1947 + 'html' => ''
1410 1948 );
1411 1949 }
1412 1950 }
1413 1951
1414 -
1952 +//very good
1415 1953 /**
1416 - * Format search results into a natural text summary.
1954 + * Handle image search requests from the chatbot
1417 1955 *
1418 - * @since 1.0.0
1419 - * @param array $results The search results from the API.
1420 - * @param string $query The original search query.
1421 - * @return string The text summary of the top results.
1956 + * @param string $message The user's search query
1957 + * @param int $user_id The user's ID
1958 + * @param string $session_id The chat session ID
1959 + * @return array Response array with text and HTML content
1422 1960 */
1423 -private function format_search_results( $results, $query ) {
1424 - $summary = sprintf(
1425 - esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1426 - esc_html( $query )
1427 - ) . "\n\n";
1428 -
1429 - $max_results = min( count( $results ), 3 );
1430 - for ( $i = 0; $i < $max_results; $i++ ) {
1431 - $result = $results[ $i ];
1432 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1433 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1434 -
1435 - // Append title and description to the summary
1436 - $summary .= sprintf(
1437 - "%s\n%s\n\n",
1438 - esc_html( $title ),
1439 - esc_html( $description )
1440 - );
1441 - }
1442 -
1443 - return $summary;
1444 -}
1445 -
1446 -/**
1447 - * Generate HTML markup for search results.
1448 - *
1449 - * @since 1.0.0
1450 - * @param array $results The search results from the API.
1451 - * @param string $query The user-refined query.
1452 - * @return string The HTML markup for displaying the results.
1453 - */
1454 -private function generate_search_results_html( $results, $query ) {
1455 - ob_start();
1456 - ?>
1457 - <div class="mxchat-search-results">
1458 - <?php foreach ( $results as $result ) :
1459 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1460 - $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1461 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1462 - $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1463 - $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1464 - $domain = parse_url( $url, PHP_URL_HOST );
1465 - ?>
1466 - <div class="mxchat-search-item">
1467 - <div class="mxchat-search-header">
1468 - <?php if ( $favicon ) : ?>
1469 - <img
1470 - src="<?php echo esc_url( $favicon ); ?>"
1471 - class="mxchat-site-icon"
1472 - alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1473 - width="16"
1474 - height="16"
1475 - />
1476 - <?php endif; ?>
1477 - <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1478 - </div>
1479 -
1480 - <div class="mxchat-search-content">
1481 - <h3 class="mxchat-search-title">
1482 - <a href="<?php echo esc_url( $url ); ?>"
1483 - target="_blank"
1484 - rel="noopener noreferrer"
1485 - >
1486 - <?php echo esc_html( $title ); ?>
1487 - </a>
1488 - </h3>
1489 -
1490 - <?php if ( $thumbnail ) : ?>
1491 - <div class="mxchat-search-thumbnail">
1492 - <img
1493 - src="<?php echo esc_url( $thumbnail ); ?>"
1494 - alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1495 - loading="lazy"
1496 - />
1497 - </div>
1498 - <?php endif; ?>
1499 -
1500 - <div class="mxchat-search-description">
1501 - <?php echo esc_html( $description ); ?>
1502 - </div>
1503 - </div>
1504 - </div>
1505 - <?php endforeach; ?>
1506 - </div>
1507 - <?php
1508 - return ob_get_clean();
1509 -}
1510 -
1511 -
1512 -//very good
1513 1961 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1514 -
1515 - // Step 1: Interpret the search query for better results
1962 + // Step 1: Interpret the search query using the user's selected AI model
1516 1963 $refined_search_query = $this->mxchat_interpret_search_query($message);
1517 1964
1518 -
1519 1965 // If no query was interpreted, return a fallback message
1520 1966 if (empty($refined_search_query)) {
1521 - $this->fallbackResponse = [
1967 + return array(
1522 1968 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1523 1969 'html' => "",
1524 - ];
1525 - return;
1970 + );
1526 1971 }
1527 1972
1528 1973 // Brave API URL
1529 1974 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1532,19 +1977,12 @@
1532 1977 $options = get_option('mxchat_options');
1533 1978 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1534 1979
1535 1980 if (empty($api_key)) {
1536 -/*
1537 - if (defined('WP_DEBUG') && WP_DEBUG) {
1538 - error_log("Brave API key is missing.");
1539 - }
1540 -*/
1541 -
1542 - $this->fallbackResponse = [
1981 + return array(
1543 1982 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1544 1983 'html' => "",
1545 - ];
1546 - return;
1984 + );
1547 1985 }
1548 1986
1549 1987 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1550 1988 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1555,16 +1993,8 @@
1555 1993 'count' => $image_count,
1556 1994 'safesearch' => $safe_search,
1557 1995 ], $api_url);
1558 1996
1559 -/*
1560 - // Log the final API URL for the search
1561 - if (defined('WP_DEBUG') && WP_DEBUG) {
1562 - error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1563 - }
1564 -*/
1565 -
1566 -
1567 1997 // Implement caching
1568 1998 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1569 1999 $body = get_transient($transient_key);
1570 2000
@@ -1580,19 +2010,12 @@
1580 2010
1581 2011 $response = wp_remote_get($api_url, $args);
1582 2012
1583 2013 if (is_wp_error($response)) {
1584 -/*
1585 - if (defined('WP_DEBUG') && WP_DEBUG) {
1586 - error_log("Brave Image API request failed: " . $response->get_error_message());
1587 - }
1588 -*/
1589 -
1590 - $this->fallbackResponse = [
2014 + return array(
1591 2015 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1592 2016 'html' => "",
1593 - ];
1594 - return;
2017 + );
1595 2018 }
1596 2019
1597 2020 $body = json_decode(wp_remote_retrieve_body($response), true);
1598 2021 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1600,10 +2023,16 @@
1600 2023
1601 2024 // Process the API response
1602 2025 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1603 2026 $html_output = '<div class="mxchat-image-gallery">';
1604 -
1605 - foreach ($body['results'] as $image) {
2027 +
2028 + // Get the configured image count (1-6)
2029 + $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
2030 + $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
2031 +
2032 + // Use only the requested number of images
2033 + for ($i = 0; $i < $display_count; $i++) {
2034 + $image = $body['results'][$i];
1606 2035 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1607 2036 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1608 2037 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1609 2038
@@ -1617,47 +2046,95 @@
1617 2046 }
1618 2047
1619 2048 $html_output .= '</div>';
1620 2049
1621 - $this->fallbackResponse = [
1622 - 'text' => "",
1623 - 'html' => $html_output,
1624 - ];
1625 -
1626 - // Save response in chat history
2050 + // Create response text
2051 + $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
2052 +
2053 + // Save both response text and HTML to chat history
2054 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1627 2055 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1628 2056
2057 + // Return the combined response
2058 + return array(
2059 + 'text' => $response_text,
2060 + 'html' => $html_output,
2061 + );
1629 2062 } else {
1630 -/*
1631 - if (defined('WP_DEBUG') && WP_DEBUG) {
1632 - error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1633 - }
1634 -*/
1635 -
1636 - $this->fallbackResponse = [
1637 - 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
2063 + $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
2064 +
2065 + // Save the error message to chat history
2066 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2067 +
2068 + return array(
2069 + 'text' => $response_text,
1638 2070 'html' => "",
1639 - ];
2071 + );
1640 2072 }
1641 2073 }
2074 +
2075 +/**
2076 + * Interpret the search query using the user's selected AI model
2077 + *
2078 + * @param string $user_query The original query from the user
2079 + * @return string The refined search query
2080 + */
1642 2081 public function mxchat_interpret_search_query($user_query) {
1643 2082 $system_prompt = esc_html__("Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.", 'mxchat');
1644 -
1645 - // Retrieve OpenAI API key using 'api_key' as the option key
1646 - $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1647 -
1648 - /*
1649 - // Log the API key check, without exposing the key
1650 - if (defined('WP_DEBUG') && WP_DEBUG) {
1651 - error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
2083 +
2084 + // Get options and determine the selected model
2085 + $options = $this->options ?? get_option('mxchat_options');
2086 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2087 +
2088 + // Extract model prefix to determine the provider
2089 + $model_parts = explode('-', $selected_model);
2090 + $provider = strtolower($model_parts[0]);
2091 +
2092 + // Determine which API key to use based on the provider
2093 + switch ($provider) {
2094 + case 'gemini':
2095 + $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
2096 + if (empty($api_key)) {
2097 + return sanitize_text_field($user_query); // Default to original query if API key missing
2098 + }
2099 + return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
2100 +
2101 + case 'claude':
2102 + $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
2103 + if (empty($api_key)) {
2104 + return sanitize_text_field($user_query);
2105 + }
2106 + return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
2107 +
2108 + case 'grok':
2109 + $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
2110 + if (empty($api_key)) {
2111 + return sanitize_text_field($user_query);
2112 + }
2113 + return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
2114 +
2115 + case 'deepseek':
2116 + $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
2117 + if (empty($api_key)) {
2118 + return sanitize_text_field($user_query);
2119 + }
2120 + return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
2121 +
2122 + case 'gpt':
2123 + default:
2124 + // Default to OpenAI for custom models or unrecognized prefixes
2125 + $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
2126 + if (empty($api_key)) {
2127 + return sanitize_text_field($user_query);
2128 + }
2129 + return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1652 2130 }
1653 - */
2131 +}
1654 2132
1655 - if (empty($api_key)) {
1656 - //error_log("OpenAI API key is missing.");
1657 - return sanitize_text_field($user_query); // Default to the original query if API key is missing
1658 - }
1659 -
2133 +/**
2134 + * Interpret query using OpenAI models
2135 + */
2136 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1660 2137 $url = 'https://api.openai.com/v1/chat/completions';
1661 2138 $args = [
1662 2139 'headers' => [
1663 2140 'Authorization' => 'Bearer ' . $api_key,
@@ -1663,9 +2140,9 @@
1663 2140 'Authorization' => 'Bearer ' . $api_key,
1664 2141 'Content-Type' => 'application/json',
1665 2142 ],
1666 2143 'body' => wp_json_encode([
1667 - 'model' => 'gpt-3.5-turbo',
2144 + 'model' => $model,
1668 2145 'messages' => [
1669 2146 ['role' => 'system', 'content' => $system_prompt],
1670 2147 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1671 2148 ],
@@ -1672,292 +2149,178 @@
1672 2149 'temperature' => 0.2,
1673 2150 'max_tokens' => 20,
1674 2151 ]),
1675 2152 'method' => 'POST',
2153 + 'timeout' => 15,
1676 2154 ];
1677 2155
1678 2156 $response = wp_remote_post($url, $args);
1679 -
1680 2157 if (is_wp_error($response)) {
1681 - //error_log("OpenAI request failed: " . $response->get_error_message());
1682 - return sanitize_text_field($user_query); // Fallback to the original query if there's an error
2158 + return sanitize_text_field($user_query);
1683 2159 }
1684 2160
1685 2161 $body = json_decode(wp_remote_retrieve_body($response), true);
2162 + return isset($body['choices'][0]['message']['content'])
2163 + ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
2164 + : sanitize_text_field($user_query);
2165 +}
1686 2166
1687 - // Check for a valid response and sanitize output
1688 - if (isset($body['choices'][0]['message']['content'])) {
1689 - $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
2167 +/**
2168 + * Interpret query using Claude models
2169 + */
2170 +private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
2171 + $url = 'https://api.anthropic.com/v1/messages';
2172 +
2173 + $args = [
2174 + 'headers' => [
2175 + 'Content-Type' => 'application/json',
2176 + 'x-api-key' => $api_key,
2177 + 'anthropic-version' => '2023-06-01',
2178 + ],
2179 + 'body' => wp_json_encode([
2180 + 'model' => $model,
2181 + 'system' => $system_prompt,
2182 + 'messages' => [
2183 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
2184 + ],
2185 + 'max_tokens' => 20,
2186 + 'temperature' => 0.2,
2187 + ]),
2188 + 'method' => 'POST',
2189 + 'timeout' => 15,
2190 + ];
1690 2191
1691 - /*
1692 - // Log the interpreted query for debugging
1693 - if (defined('WP_DEBUG') && WP_DEBUG) {
1694 - error_log("Interpreted search query: " . $interpreted_query);
1695 - }
1696 - */
1697 -
1698 - return $interpreted_query;
1699 - } else {
1700 - //error_log("Unexpected API response format: " . print_r($body, true));
2192 + $response = wp_remote_post($url, $args);
2193 + if (is_wp_error($response)) {
1701 2194 return sanitize_text_field($user_query);
1702 2195 }
1703 -}
1704 2196
1705 -public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
1706 - //error_log("ADD TO CART START - Message: $message, User ID: $user_id");
1707 -
1708 - if (!class_exists('WooCommerce')) {
1709 - //error_log("WooCommerce not available");
1710 - return $this->generate_intent_response([
1711 - 'intent' => 'add_to_cart',
1712 - 'status' => 'error',
1713 - 'reason' => esc_html__('woocommerce_not_available', 'mxchat')
1714 - ], $session_id);
1715 - }
1716 -
1717 - $sanitized_user_id = sanitize_key($user_id);
1718 - // error_log("Sanitized User ID: $sanitized_user_id");
1719 - $product_id = null;
1720 -
1721 - // If button click, use transient
1722 - if ($message === '!addtocart') {
1723 - //error_log("Button click detected - using transient");
1724 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1725 - //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1726 - } else {
1727 - // For text commands, search message first
1728 - //error_log("Text command detected - searching message first");
1729 - $product_id = $this->find_product_in_message($message);
1730 - //error_log("Product ID from message search: " . ($product_id ?? 'null'));
1731 -
1732 - // Only use transient as fallback if no product found in message
1733 - if (!$product_id) {
1734 - //error_log("No product found in message, checking transient as fallback");
1735 - $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1736 - //error_log("Product ID from transient fallback: " . ($product_id ?? 'null'));
1737 - }
1738 - }
1739 -
1740 - // Handle no product found
1741 - if (!$product_id) {
1742 - //error_log("No product ID found through any method");
1743 - return $this->generate_intent_response([
1744 - 'intent' => 'add_to_cart',
1745 - 'status' => 'error',
1746 - 'reason' => esc_html__('no_product_context', 'mxchat'),
1747 - 'action_needed' => esc_html__('request_product_name', 'mxchat'),
1748 - 'searched_message' => $message
1749 - ], $session_id);
1750 - }
1751 -
1752 - // Get and verify product
1753 - $product = wc_get_product($product_id);
1754 - if (!$product) {
1755 - //error_log("Product not found with ID: $product_id");
1756 - return $this->generate_intent_response([
1757 - 'intent' => 'add_to_cart',
1758 - 'status' => 'error',
1759 - 'reason' => esc_html__('product_not_found', 'mxchat'),
1760 - 'product_id' => $product_id
1761 - ], $session_id);
1762 - }
1763 -
1764 - //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1765 -
1766 - // Add to cart
1767 - $added = WC()->cart->add_to_cart($product_id);
1768 - if ($added) {
1769 - //error_log("Successfully added to cart: " . $product->get_name());
1770 - return $this->generate_intent_response([
1771 - 'intent' => 'add_to_cart',
1772 - 'status' => 'success',
1773 - 'product' => [
1774 - 'name' => $product->get_name(),
1775 - 'id' => $product_id
1776 - ],
1777 - 'cart_url' => wc_get_cart_url(),
1778 - 'available_actions' => [
1779 - esc_html__('view_cart', 'mxchat'),
1780 - esc_html__('checkout', 'mxchat'),
1781 - esc_html__('continue_shopping', 'mxchat')
1782 - ]
1783 - ], $session_id);
1784 - } else {
1785 - //error_log("Failed to add to cart: " . $product->get_name());
1786 - return $this->generate_intent_response([
1787 - 'intent' => 'add_to_cart',
1788 - 'status' => 'error',
1789 - 'reason' => esc_html__('add_to_cart_failed', 'mxchat'),
1790 - 'product' => [
1791 - 'name' => $product->get_name(),
1792 - 'id' => $product_id
1793 - ]
1794 - ], $session_id);
1795 - }
2197 + $body = json_decode(wp_remote_retrieve_body($response), true);
2198 + if (!empty($body['content'][0]['text'])) {
2199 + return sanitize_text_field(trim($body['content'][0]['text']));
2200 + }
2201 +
2202 + return sanitize_text_field($user_query);
1796 2203 }
1797 -private function find_product_in_message($message) {
1798 - global $wpdb;
1799 2204
1800 - // Get embedding for the search query
1801 - $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1802 - if (!is_array($query_embedding)) {
1803 - return null;
2205 +/**
2206 + * Interpret query using Gemini models
2207 + */
2208 +private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2209 + // Strip "gemini-" prefix for the API
2210 + $model_version = str_replace('gemini-', '', $model);
2211 +
2212 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2213 +
2214 + $args = [
2215 + 'headers' => [
2216 + 'Content-Type' => 'application/json',
2217 + ],
2218 + 'body' => wp_json_encode([
2219 + 'contents' => [
2220 + [
2221 + 'role' => 'user',
2222 + 'parts' => [
2223 + ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
2224 + ]
2225 + ]
2226 + ],
2227 + 'generationConfig' => [
2228 + 'temperature' => 0.2,
2229 + 'maxOutputTokens' => 20,
2230 + ],
2231 + ]),
2232 + 'method' => 'POST',
2233 + 'timeout' => 15,
2234 + ];
2235 +
2236 + $response = wp_remote_post($url, $args);
2237 + if (is_wp_error($response)) {
2238 + return sanitize_text_field($user_query);
1804 2239 }
1805 -
1806 - // Get relevant content as string
1807 - $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1808 - if (empty($relevant_content)) {
1809 - // Return null to indicate no results and set fallback response
1810 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Could you please be more specific about the product you're looking for?", 'mxchat');
1811 - return null;
2240 +
2241 + $body = json_decode(wp_remote_retrieve_body($response), true);
2242 + if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
2243 + return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1812 2244 }
2245 +
2246 + return sanitize_text_field($user_query);
2247 +}
1813 2248
1814 - // Extract product URLs from the content
1815 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1816 -
1817 - if (!empty($matches[0])) {
1818 - // Try each URL found
1819 - foreach ($matches[0] as $url) {
1820 - // Clean the URL
1821 - $url = rtrim($url, '/."\']');
1822 -
1823 - // Get the product slug
1824 - $path = parse_url($url, PHP_URL_PATH);
1825 - $slug = basename(rtrim($path, '/'));
1826 -
1827 - // Find product by slug
1828 - $args = array(
1829 - 'post_type' => 'product',
1830 - 'post_status' => 'publish',
1831 - 'name' => $slug,
1832 - 'posts_per_page' => 1
1833 - );
1834 -
1835 - $products = get_posts($args);
1836 -
1837 - if (!empty($products)) {
1838 - $product_id = $products[0]->ID;
1839 - $product = wc_get_product($product_id);
1840 -
1841 - if ($product && $product->is_purchasable()) {
1842 - return $product_id;
1843 - }
1844 - }
1845 - }
2249 +/**
2250 + * Interpret query using X.AI (Grok) models
2251 + */
2252 +private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
2253 + $url = 'https://api.xai.com/v1/chat/completions';
2254 +
2255 + $args = [
2256 + 'headers' => [
2257 + 'Content-Type' => 'application/json',
2258 + 'Authorization' => 'Bearer ' . $api_key,
2259 + ],
2260 + 'body' => wp_json_encode([
2261 + 'model' => $model,
2262 + 'messages' => [
2263 + ['role' => 'system', 'content' => $system_prompt],
2264 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2265 + ],
2266 + 'temperature' => 0.2,
2267 + 'max_tokens' => 20,
2268 + ]),
2269 + 'method' => 'POST',
2270 + 'timeout' => 15,
2271 + ];
2272 +
2273 + $response = wp_remote_post($url, $args);
2274 + if (is_wp_error($response)) {
2275 + return sanitize_text_field($user_query);
1846 2276 }
1847 -
1848 - // Fallback: Look for product names in the content
1849 - $products = wc_get_products([
1850 - 'status' => 'publish',
1851 - 'limit' => -1,
1852 - 'return' => 'all'
1853 - ]);
1854 -
1855 - foreach ($products as $product) {
1856 - $name = $product->get_name();
1857 - if (stripos($relevant_content, $name) !== false) {
1858 - if ($product->is_purchasable()) {
1859 - return $product->get_id();
1860 - }
1861 - }
2277 +
2278 + $body = json_decode(wp_remote_retrieve_body($response), true);
2279 + if (isset($body['choices'][0]['message']['content'])) {
2280 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1862 2281 }
1863 -
1864 - // If no product is found after all checks, set the fallback response
1865 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1866 - return null;
2282 +
2283 + return sanitize_text_field($user_query);
1867 2284 }
1868 2285
1869 -// New method to handle intent responses
1870 -private function generate_intent_response($context_content, $session_id) {
1871 - // Convert the context array to a structured string for the AI
1872 - $context_string = $this->format_intent_context($context_content);
1873 -
1874 - // Generate AI response using the context
1875 - $response = $this->mxchat_generate_response(
1876 - $context_string,
1877 - $this->options['api_key'],
1878 - $this->options['xai_api_key'],
1879 - $this->options['claude_api_key'],
1880 - $this->options['deepseek_api_key'],
1881 - $this->mxchat_fetch_conversation_history_for_ai($session_id)
1882 - );
1883 -
1884 - $this->fallbackResponse['text'] = $response;
1885 - return true;
1886 -}
1887 -// Helper method to format intent context
1888 -private function format_intent_context($context) {
1889 - $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1890 -
1891 - switch ($context['intent']) {
1892 - case 'add_to_cart':
1893 - if ($context['status'] === 'success') {
1894 - $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1895 - $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1896 - $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1897 - $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1898 - $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1899 - } else {
1900 - $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1901 - $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1902 - switch ($context['reason']) {
1903 - case 'woocommerce_not_available':
1904 - $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1905 - break;
1906 - case 'no_product_context':
1907 - $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1908 - break;
1909 - case 'product_not_found':
1910 - $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1911 - break;
1912 - case 'add_to_cart_failed':
1913 - $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1914 - break;
1915 - }
1916 - }
1917 - break;
2286 +/**
2287 + * Interpret query using DeepSeek models
2288 + */
2289 +private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
2290 + $url = 'https://api.deepseek.com/v1/chat/completions';
2291 +
2292 + $args = [
2293 + 'headers' => [
2294 + 'Content-Type' => 'application/json',
2295 + 'Authorization' => 'Bearer ' . $api_key,
2296 + ],
2297 + 'body' => wp_json_encode([
2298 + 'model' => $model,
2299 + 'messages' => [
2300 + ['role' => 'system', 'content' => $system_prompt],
2301 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2302 + ],
2303 + 'temperature' => 0.2,
2304 + 'max_tokens' => 20,
2305 + ]),
2306 + 'method' => 'POST',
2307 + 'timeout' => 15,
2308 + ];
2309 +
2310 + $response = wp_remote_post($url, $args);
2311 + if (is_wp_error($response)) {
2312 + return sanitize_text_field($user_query);
1918 2313 }
1919 -
1920 - return $context_string;
1921 -}
1922 -
1923 -
1924 -public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
1925 - if (!class_exists('WooCommerce')) {
1926 - $this->fallbackResponse['text'] = esc_html__("I apologize, but the checkout feature isn't available at the moment.", 'mxchat');
1927 - return true;
2314 +
2315 + $body = json_decode(wp_remote_retrieve_body($response), true);
2316 + if (isset($body['choices'][0]['message']['content'])) {
2317 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1928 2318 }
1929 -
1930 - // Check if cart has items
1931 - if (WC()->cart->is_empty()) {
1932 - $this->fallbackResponse['text'] = esc_html__("Your cart is empty at the moment. Would you like to see our products?", 'mxchat');
1933 - return true;
1934 - }
1935 -
1936 - // Get cart summary
1937 - $cart_count = WC()->cart->get_cart_contents_count();
1938 - $cart_total = WC()->cart->get_total();
1939 -
1940 - // Get and validate checkout URL
1941 - $checkout_url = wc_get_checkout_url();
1942 - if (!$checkout_url) {
1943 - $this->fallbackResponse['text'] = esc_html__("I'm having trouble accessing the checkout page. Please try again in a moment.", 'mxchat');
1944 - return true;
1945 - }
1946 -
1947 - wp_send_json([
1948 - 'text' => sprintf(
1949 - esc_html__("You have %d item%s in your cart totaling %s. I'll redirect you to checkout now.", 'mxchat'),
1950 - $cart_count,
1951 - $cart_count > 1 ? esc_html__('s', 'mxchat') : '',
1952 - strip_tags($cart_total)
1953 - ),
1954 - 'redirect_url' => esc_url_raw($checkout_url)
1955 - ]);
1956 - wp_die();
2319 +
2320 + return sanitize_text_field($user_query);
1957 2321 }
1958 2322
1959 -
1960 2323 //very good
1961 2324 private function add_email_to_loops($email) {
1962 2325 // Sanitize the email
1963 2326 $email = sanitize_email($email);
@@ -2041,95 +2404,169 @@
2041 2404
2042 2405 // Default to proceeding with conversation if no specific PDF action is needed
2043 2406 $this->fallbackResponse['text'] = '';
2044 2407 }
2408 +
2409 +
2410 +/**
2411 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
2412 + */
2045 2413 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2414 + // CLEAR DEBUG LOGGING
2415 + //error_log("=== MXCHAT PDF PROCESSING START ===");
2416 + //error_log("PDF Source: " . $pdf_source);
2417 + //error_log("Max Pages: " . $max_pages);
2418 + //error_log("Session ID: " . ($this->session_id ?? 'not set'));
2419 +
2420 + // Check if Advanced Claude Toolbar is available and enabled
2421 + $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
2422 + $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
2423 +
2424 + //error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2425 + //error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2426 +
2427 + if ($claude_available && $claude_enabled) {
2428 + //error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2429 +
2430 + // Attempt Claude processing first
2431 + $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
2432 +
2433 + if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
2434 + //error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
2435 + //error_log("Claude returned " . count($claude_result) . " processed pages");
2436 +
2437 + // Log first page details for verification
2438 + if (isset($claude_result[0])) {
2439 + $first_page = $claude_result[0];
2440 + //error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2441 + //error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2442 + //error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2443 + }
2444 +
2445 + //error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2446 + return $claude_result;
2447 + } else {
2448 + //error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
2449 + //error_log("Claude result type: " . gettype($claude_result));
2450 + if (is_array($claude_result)) {
2451 + //error_log("Claude result count: " . count($claude_result));
2452 + }
2453 + }
2454 + }
2455 +
2456 + // Fallback to basic processing
2457 + //error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2458 +
2046 2459 $upload_dir = wp_upload_dir();
2047 2460 $temp_file = null;
2048 -
2461 +
2049 2462 try {
2050 - // Handle URL vs local file
2463 + // Your existing basic processing code here...
2464 + // (I'll include the key parts with debug logging)
2465 +
2051 2466 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2052 - // Validate and download the file from URL
2053 - $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
2054 - $response = wp_remote_get($pdf_source, ['timeout' => 60]);
2055 -
2467 + //error_log("Downloading PDF from URL...");
2468 + $temp_file = wp_tempnam($pdf_source);
2469 + $response = wp_remote_get($pdf_source, [
2470 + 'timeout' => 60,
2471 + 'headers' => ['User-Agent' => 'MxChat PDF Processor']
2472 + ]);
2473 +
2056 2474 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
2057 - //error_log(esc_html__("Failed to download PDF. Error: ", 'mxchat') . print_r($response, true));
2475 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
2476 + //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
2058 2477 return false;
2059 2478 }
2060 -
2479 +
2061 2480 file_put_contents($temp_file, wp_remote_retrieve_body($response));
2062 -
2063 - // Validate that the downloaded file is a PDF
2064 - $mime_type = mime_content_type($temp_file);
2065 - if ($mime_type !== 'application/pdf') {
2066 - //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
2067 - unlink($temp_file);
2068 - return false;
2069 - }
2481 + //error_log("✅ PDF downloaded successfully");
2070 2482 } else {
2071 - // For local files, use the provided path directly
2072 2483 $temp_file = $pdf_source;
2484 + //error_log("Using local PDF file: " . $temp_file);
2073 2485 }
2074 -
2075 - // Parse and process the PDF
2486 +
2487 + // Parse PDF
2488 + //error_log("Parsing PDF with basic parser...");
2076 2489 $parser = new \Smalot\PdfParser\Parser();
2077 2490 $pdf = $parser->parseFile($temp_file);
2078 2491 $pages = $pdf->getPages();
2079 -
2492 +
2493 + //error_log("PDF contains " . count($pages) . " pages");
2494 +
2080 2495 if (count($pages) > $max_pages) {
2081 - //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
2082 - if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2496 + //error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2497 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2083 2498 unlink($temp_file);
2084 2499 }
2085 - return esc_html__('too_many_pages', 'mxchat');
2500 + return 'too_many_pages';
2086 2501 }
2087 -
2502 +
2088 2503 $embeddings = [];
2504 + $processed_pages = 0;
2505 +
2089 2506 foreach ($pages as $page_number => $page) {
2090 2507 $text = $page->getText();
2091 -
2092 - // Ensure text is non-empty before generating embeddings
2508 +
2093 2509 if (empty(trim($text))) {
2094 - //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
2510 + //error_log("Skipping empty page: " . ($page_number + 1));
2095 2511 continue;
2096 2512 }
2097 -
2513 +
2514 + $text = $this->mxchat_clean_text($text);
2515 +
2098 2516 $embedding = $this->mxchat_generate_embedding(
2099 - esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2517 + __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2100 2518 $this->options['api_key']
2101 2519 );
2102 -
2520 +
2103 2521 if ($embedding) {
2104 2522 $embeddings[] = [
2105 2523 'page_number' => $page_number + 1,
2106 2524 'embedding' => $embedding,
2107 2525 'text' => $text,
2526 + 'enhanced' => false, // CLEARLY MARK AS BASIC
2527 + 'processing_method' => 'basic_pdf_parser'
2108 2528 ];
2109 - } else {
2110 - //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
2529 + $processed_pages++;
2111 2530 }
2112 2531 }
2113 -
2114 - // Clean up downloaded file if it was from URL
2115 - if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2532 +
2533 + //error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2534 +
2535 + // Cleanup
2536 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2116 2537 unlink($temp_file);
2117 2538 }
2118 -
2539 +
2540 + //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
2119 2541 return $embeddings;
2120 -
2542 +
2121 2543 } catch (\Exception $e) {
2122 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
2123 -
2124 - // Cleanup in case of exception
2544 + //error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
2125 2545 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2126 2546 unlink($temp_file);
2127 2547 }
2128 -
2548 + //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
2129 2549 return false;
2130 2550 }
2131 2551 }
2552 +
2553 +private function mxchat_clean_text($text) {
2554 + // Remove excessive whitespace
2555 + $text = preg_replace('/\s+/', ' ', $text);
2556 +
2557 + // Remove control characters except newlines and tabs
2558 + $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
2559 +
2560 + // Normalize line endings
2561 + $text = str_replace(["\r\n", "\r"], "\n", $text);
2562 +
2563 + // Trim whitespace
2564 + $text = trim($text);
2565 +
2566 + return $text;
2567 +}
2568 +
2132 2569 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
2133 2570 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
2134 2571
2135 2572 $most_relevant = null;
@@ -2250,408 +2687,10 @@
2250 2687 wp_die();
2251 2688 }
2252 2689
2253 2690
2254 -/**
2255 - * Calls the AI API with the provided prompt.
2256 - *
2257 - * @param string $prompt The prompt to send to the AI.
2258 - * @return array An array containing the AI's response text.
2259 - */
2260 -private function mxchat_call_ai_api( $prompt ) {
2261 - //error_log( esc_html__( 'Calling AI API with the provided prompt.', 'mxchat' ) );
2262 2691
2263 - $api_key = $this->options['api_key'];
2264 - if ( empty( $api_key ) ) {
2265 - //error_log( esc_html__( 'API key is not set.', 'mxchat' ) );
2266 - return [ 'text' => esc_html__( 'API key is not set.', 'mxchat' ) ];
2267 - }
2268 2692
2269 - $url = 'https://api.openai.com/v1/chat/completions';
2270 - $messages = [
2271 - [
2272 - 'role' => 'system',
2273 - 'content' => __( 'You are a helpful assistant that provides concise and personalized product recommendations. Someone has asked you for some recommendations please respond very concisely appropriate for an AI chatbot.', 'mxchat' ),
2274 - ],
2275 - [
2276 - 'role' => 'user',
2277 - 'content' => $prompt,
2278 - ],
2279 - ];
2280 -
2281 - $args = [
2282 - 'headers' => [
2283 - 'Authorization' => 'Bearer ' . $api_key,
2284 - 'Content-Type' => 'application/json',
2285 - ],
2286 - 'body' => wp_json_encode(
2287 - [
2288 - 'model' => 'gpt-4o',
2289 - 'messages' => $messages,
2290 - 'temperature' => 0.7,
2291 - ]
2292 - ),
2293 - 'timeout' => 10,
2294 - 'method' => 'POST',
2295 - ];
2296 -
2297 - $response = wp_remote_post( $url, $args );
2298 -
2299 - if ( is_wp_error( $response ) ) {
2300 - //error_log( esc_html__( 'Error communicating with AI API: ', 'mxchat' ) . $response->get_error_message() );
2301 - return [ 'text' => esc_html__( 'Error communicating with AI API.', 'mxchat' ) ];
2302 - }
2303 -
2304 - $body = wp_remote_retrieve_body( $response );
2305 - //error_log( esc_html__( 'API response body: ', 'mxchat' ) . $body );
2306 -
2307 - $decoded_body = json_decode( $body, true );
2308 - if ( isset( $decoded_body['choices'][0]['message']['content'] ) ) {
2309 - return [ 'text' => $decoded_body['choices'][0]['message']['content'] ];
2310 - } else {
2311 - //error_log( esc_html__( 'Unexpected API response format: ', 'mxchat' ) . wp_json_encode( $decoded_body ) );
2312 - return [ 'text' => esc_html__( 'No response received from AI.', 'mxchat' ) ];
2313 - }
2314 -}
2315 -
2316 -/**
2317 - * Fetches the AI response for the given prompt.
2318 - *
2319 - * @param string $prompt The prompt to send to the AI.
2320 - * @return string|null The AI's response text or null if not available.
2321 - */
2322 -private function mxchat_fetch_ai_response( $prompt ) {
2323 - $response = $this->mxchat_call_ai_api( $prompt );
2324 - return isset( $response['text'] ) ? $response['text'] : null;
2325 -}
2326 -/**
2327 - * Generates the AI prompt for product recommendations.
2328 - *
2329 - * @param array $recommendations An array of product recommendations.
2330 - * @return string The generated AI prompt.
2331 - */
2332 -private function mxchat_generate_ai_recommendation_prompt($recommendations) {
2333 - $recommendation_list = '';
2334 - foreach ($recommendations as $index => $rec) {
2335 - $number = $index + 1;
2336 - $name = $rec['name'];
2337 - $price = $rec['price'];
2338 - $url = $rec['url'];
2339 - $image = $rec['image'];
2340 - $recommendation_list .= "{$number}. " . esc_html__('Product:', 'mxchat') . " {$name} (" . esc_html__('Price:', 'mxchat') . " \\${$price})\n";
2341 - $recommendation_list .= " [Link]({$url})\n";
2342 - $recommendation_list .= " ![Image]({$image})\n\n";
2343 - }
2344 -
2345 - $prompt = esc_html__('Based on the following list of products, generate a unique, friendly, and personalized response to a user. ', 'mxchat');
2346 - $prompt .= esc_html__('If some products aren\'t exactly what the user asked for but share similar styles or patterns, acknowledge this and explain why you\'re suggesting them. ', 'mxchat');
2347 - $prompt .= esc_html__('For each product, provide a brief justification that clearly explains why it\'s relevant, especially if it\'s a different type of product than requested. ', 'mxchat');
2348 - $prompt .= esc_html__('Please number your responses to match the product numbers.', 'mxchat') . "\n\n";
2349 - $prompt .= esc_html__('Products:', 'mxchat') . "\n\n{$recommendation_list}";
2350 - $prompt .= esc_html__('Please ensure that the number of each product matches the order in which the products are listed.', 'mxchat');
2351 -
2352 - return $prompt;
2353 -}
2354 -private function mxchat_generate_recommendations($user_id, $message) {
2355 - // 1. Gather user context
2356 - $user_context = $this->mxchat_get_user_context($user_id);
2357 -
2358 - // Get cart item IDs for filtering
2359 - $cart_item_ids = [];
2360 - if (!empty($user_context['cart_items'])) {
2361 - $cart_item_ids = array_map(function($item) {
2362 - return $item['product_id'];
2363 - }, $user_context['cart_items']);
2364 - }
2365 -
2366 - // 2. Get AI recommendation based on context
2367 - $ai_suggestion = $this->mxchat_get_ai_shopping_suggestion($message, $user_context);
2368 - //error_log("AI Shopping Suggestion: " . $ai_suggestion);
2369 -
2370 - // 3. Generate embedding for the AI suggestion
2371 - $suggestion_embedding = $this->mxchat_generate_embedding($ai_suggestion, $this->options['api_key']);
2372 - if (!$suggestion_embedding) {
2373 - //error_log("Could not generate embedding for AI suggestion");
2374 - return ['recommendations' => [], 'sources' => []];
2375 - }
2376 -
2377 - // 4. Use existing relevant content function to find matches
2378 - $relevant_content = $this->mxchat_find_relevant_content($suggestion_embedding);
2379 -
2380 - // 5. Extract product URLs from the relevant content
2381 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
2382 -
2383 - $found_products = [];
2384 - if (!empty($matches[0])) {
2385 - foreach ($matches[0] as $url) {
2386 - // Clean the URL
2387 - $url = rtrim($url, '/."\']');
2388 -
2389 - // Get the product slug
2390 - $path = parse_url($url, PHP_URL_PATH);
2391 - $slug = basename(rtrim($path, '/'));
2392 -
2393 - // Find product by slug
2394 - $args = array(
2395 - 'post_type' => 'product',
2396 - 'post_status' => 'publish',
2397 - 'name' => $slug,
2398 - 'posts_per_page' => 1
2399 - );
2400 -
2401 - $products = get_posts($args);
2402 -
2403 - if (!empty($products)) {
2404 - $product_id = $products[0]->ID;
2405 -
2406 - // Skip if product is in cart
2407 - if (in_array($product_id, $cart_item_ids)) {
2408 - //error_log("Skipping product ID $product_id - already in cart");
2409 - continue;
2410 - }
2411 -
2412 - $product = wc_get_product($product_id);
2413 -
2414 - if ($product && $product->is_purchasable() && !in_array($product_id, array_map(function($p) {
2415 - return $p->get_id();
2416 - }, $found_products))) {
2417 - $found_products[] = $product;
2418 - }
2419 - }
2420 - }
2421 - }
2422 -
2423 - // If no products found by URLs, look for product names in the content
2424 - if (empty($found_products)) {
2425 - $products = wc_get_products([
2426 - 'status' => 'publish',
2427 - 'limit' => -1,
2428 - 'return' => 'objects'
2429 - ]);
2430 -
2431 - foreach ($products as $product) {
2432 - // Skip if product is in cart
2433 - if (in_array($product->get_id(), $cart_item_ids)) {
2434 - continue;
2435 - }
2436 -
2437 - $name = $product->get_name();
2438 - if (stripos($relevant_content, $name) !== false) {
2439 - if ($product->is_purchasable() && !in_array($product->get_id(), array_map(function($p) {
2440 - return $p->get_id();
2441 - }, $found_products))) {
2442 - $found_products[] = $product;
2443 - }
2444 - }
2445 - }
2446 - }
2447 -
2448 - // Keep searching until we have 4 products or run out of options
2449 - $formatted_recommendations = [];
2450 - foreach ($found_products as $product) {
2451 - if (count($formatted_recommendations) >= 4) break;
2452 -
2453 - $formatted_recommendations[] = [
2454 - 'name' => $product->get_name(),
2455 - 'price' => $product->get_price(),
2456 - 'url' => get_permalink($product->get_id()),
2457 - 'image' => wp_get_attachment_url($product->get_image_id())
2458 - ];
2459 - }
2460 -
2461 - return [
2462 - 'recommendations' => $formatted_recommendations,
2463 - 'sources' => [__('AI-powered personalized recommendations', 'mxchat')]
2464 - ];
2465 -}
2466 -private function mxchat_get_ai_shopping_suggestion($message, $user_context) {
2467 - $prompt = esc_html__("As a shopping assistant, analyze this user's context and generate a specific product search suggestion. ", 'mxchat');
2468 - $prompt .= esc_html__("User's Question: \"{$message}\"\n\n", 'mxchat');
2469 -
2470 - if (!empty($user_context['order_history'])) {
2471 - $prompt .= esc_html__("Their recent orders include:\n", 'mxchat');
2472 - foreach ($user_context['order_history'] as $order) {
2473 - $prompt .= esc_html__("- {$order['name']} ({$order['date']})\n", 'mxchat');
2474 - }
2475 - }
2476 -
2477 - if (!empty($user_context['cart_items'])) {
2478 - $prompt .= esc_html__("\nThey currently have in their cart:\n", 'mxchat');
2479 - foreach ($user_context['cart_items'] as $item) {
2480 - $prompt .= esc_html__("- {$item['name']}\n", 'mxchat');
2481 - }
2482 - }
2483 -
2484 - $prompt .= esc_html__("\nBased on their question and history, suggest a specific search query that would help find the most relevant products. ", 'mxchat');
2485 - $prompt .= esc_html__("Respond with ONLY the search query, nothing else.", 'mxchat');
2486 -
2487 - $response = $this->mxchat_call_ai_api($prompt);
2488 - return isset($response['text']) ? trim($response['text']) : $message;
2489 -}
2490 -public function mxchat_handle_product_recommendations($message, $user_id, $session_id) {
2491 - try {
2492 - //error_log("Starting product recommendations for user: $user_id, session: $session_id");
2493 -
2494 - // Pass the message to get context-aware recommendations
2495 - $recommendation_data = $this->mxchat_generate_recommendations($user_id, $message);
2496 - //error_log('Generated recommendation data: ' . wp_json_encode($recommendation_data));
2497 -
2498 - if (empty($recommendation_data['recommendations'])) {
2499 - //error_log("No recommendations found for user: $user_id");
2500 - $this->fallbackResponse = [
2501 - 'text' => __("I couldn't find any product recommendations for you right now. Please try again later!", 'mxchat'),
2502 - ];
2503 - return;
2504 - }
2505 -
2506 - // Remove duplicates and limit to top 4 recommendations
2507 - $unique_recommendations = [];
2508 - foreach ($recommendation_data['recommendations'] as $rec) {
2509 - $unique_recommendations[$rec['url']] = $rec;
2510 - }
2511 -
2512 - $unique_recommendations = array_slice($unique_recommendations, 0, 4);
2513 - //error_log('Top 4 recommendations: ' . wp_json_encode($unique_recommendations));
2514 -
2515 - $recommendations_summary = [];
2516 - foreach ($unique_recommendations as $rec) {
2517 - $recommendations_summary[] = [
2518 - 'name' => $rec['name'],
2519 - 'price' => strip_tags($rec['price']),
2520 - 'url' => $rec['url'],
2521 - 'image' => $rec['image'],
2522 - ];
2523 - }
2524 -
2525 - // Generate AI prompt and fetch response
2526 - $ai_prompt = $this->mxchat_generate_ai_recommendation_prompt($recommendations_summary);
2527 - $ai_response = $this->mxchat_fetch_ai_response($ai_prompt);
2528 -
2529 - if (empty($ai_response)) {
2530 - $this->fallbackResponse = [
2531 - 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2532 - ];
2533 - return;
2534 - }
2535 -
2536 - // Split the AI's response into lines
2537 - $ai_lines = preg_split('/\r\n|\r|\n/', $ai_response);
2538 -
2539 - // Initialize variables
2540 - $formatted_response = '';
2541 - $justifications = [];
2542 - $current_number = 0;
2543 - $in_introduction = true;
2544 - $introduction = '';
2545 -
2546 - // Parse the AI response to separate the introduction and the justifications
2547 - foreach ($ai_lines as $line) {
2548 - if (preg_match('/^\s*(\d+)\.\s*(.*)$/', $line, $matches)) {
2549 - // This line is a numbered justification
2550 - $current_number = intval($matches[1]) - 1;
2551 - $justifications[$current_number] = $matches[2];
2552 - $in_introduction = false;
2553 - } elseif ($in_introduction) {
2554 - // This line is part of the introduction
2555 - $introduction .= $line . ' ';
2556 - } else {
2557 - // This line is a continuation of the current justification
2558 - if (isset($justifications[$current_number])) {
2559 - $justifications[$current_number] .= ' ' . $line;
2560 - }
2561 - }
2562 - }
2563 -
2564 - // Build the formatted response
2565 - if (!empty($introduction)) {
2566 - $formatted_response .= esc_html(trim($introduction)) . "<br><br>";
2567 - }
2568 -
2569 - foreach ($recommendations_summary as $index => $rec) {
2570 - $name = esc_html($rec['name']);
2571 - $price = number_format((float)$rec['price'], 2, '.', '');
2572 - $url = esc_url($rec['url']);
2573 - $image = esc_url($rec['image']);
2574 -
2575 - $formatted_response .= ($index + 1) . ". <strong>" . $name . "</strong> - <strong>$" . $price . "</strong><br>";
2576 - $formatted_response .= "<img src=\"{$image}\" alt=\"{$name}\" style=\"max-width: 200px; height: auto; display: block; margin: 10px 0;\" /><br>";
2577 -
2578 - if (isset($justifications[$index])) {
2579 - $formatted_response .= "<em>" . esc_html(trim($justifications[$index])) . "</em><br><br>";
2580 - }
2581 - }
2582 -
2583 - $this->fallbackResponse = [
2584 - 'text' => $formatted_response,
2585 - ];
2586 - //error_log('Final formatted response set.');
2587 - } catch (Exception $e) {
2588 - //error_log('Error in mxchat_handle_product_recommendations: ' . $e->getMessage());
2589 - $this->fallbackResponse = [
2590 - 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2591 - ];
2592 - }
2593 -}
2594 -private function mxchat_get_user_context($user_id) {
2595 - $context = [
2596 - 'order_history' => [],
2597 - 'cart_items' => [],
2598 - 'recently_viewed' => [],
2599 - ];
2600 -
2601 - // Get order history
2602 - if (is_user_logged_in() && $user_id) {
2603 - $orders = wc_get_orders([
2604 - 'customer_id' => $user_id,
2605 - 'limit' => 5, // Last 5 orders
2606 - 'orderby' => 'date',
2607 - 'order' => 'DESC',
2608 - ]);
2609 -
2610 - foreach ($orders as $order) {
2611 - foreach ($order->get_items() as $item) {
2612 - $context['order_history'][] = [
2613 - 'name' => $item->get_name(),
2614 - 'product_id' => $item->get_product_id(),
2615 - 'date' => $order->get_date_created()->format('Y-m-d')
2616 - ];
2617 - }
2618 - }
2619 - }
2620 -
2621 - // Get cart items
2622 - if (WC()->cart && WC()->cart->get_cart_contents_count() > 0) {
2623 - foreach (WC()->cart->get_cart() as $cart_item) {
2624 - $product = wc_get_product($cart_item['product_id']);
2625 - if ($product) {
2626 - $context['cart_items'][] = [
2627 - 'name' => $product->get_name(),
2628 - 'product_id' => $product->get_id()
2629 - ];
2630 - }
2631 - }
2632 - }
2633 -
2634 - // Get recently viewed items from session
2635 - $viewed_products = WC()->session->get('recently_viewed_products', []);
2636 - foreach ($viewed_products as $product_id) {
2637 - $product = wc_get_product($product_id);
2638 - if ($product) {
2639 - $context['recently_viewed'][] = [
2640 - 'name' => $product->get_name(),
2641 - 'product_id' => $product->get_id()
2642 - ];
2643 - }
2644 - }
2645 -
2646 - return $context;
2647 -}
2648 -
2649 -
2650 -
2651 -
2652 -
2653 -
2654 2693 function mxchat_fetch_new_messages() {
2655 2694 $session_id = sanitize_text_field($_POST['session_id']);
2656 2695 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2657 2696 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -2685,10 +2724,8 @@
2685 2724 'new_messages' => array_values($new_messages)
2686 2725 ]);
2687 2726 wp_die();
2688 2727 }
2689 -
2690 -
2691 2728 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2692 2729 // First check if live agents are available
2693 2730 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2694 2731 if ($live_agent_available !== 'on') {
@@ -2707,18 +2744,101 @@
2707 2744 ]);
2708 2745 wp_die();
2709 2746 }
2710 2747
2711 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2712 - if (empty($slack_webhook_url)) {
2748 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2749 +
2750 + if (empty($slack_bot_token)) {
2713 2751 return false;
2714 2752 }
2715 2753
2716 - // Get recent chat history (last 5 messages)
2754 + // Check if channel already exists for this session
2755 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2756 +
2757 + if (empty($channel_id)) {
2758 + // Create new channel with session ID as name
2759 + $channel_name = $this->generate_channel_name($session_id);
2760 +
2761 + //error_log("Attempting to create channel: $channel_name");
2762 +
2763 + $response = wp_remote_post('https://slack.com/api/conversations.create', [
2764 + 'headers' => [
2765 + 'Content-Type' => 'application/json',
2766 + 'Authorization' => 'Bearer ' . $slack_bot_token
2767 + ],
2768 + 'body' => json_encode([
2769 + 'name' => $channel_name,
2770 + 'is_private' => false // Public channel - anyone in workspace can join
2771 + ])
2772 + ]);
2773 +
2774 + if (!is_wp_error($response)) {
2775 + $response_body = wp_remote_retrieve_body($response);
2776 + $response_data = json_decode($response_body, true);
2777 +
2778 + //error_log("Channel creation response: " . $response_body);
2779 +
2780 + if (isset($response_data['ok']) && $response_data['ok']) {
2781 + $channel_id = $response_data['channel']['id'];
2782 + $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2783 + //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2784 + update_option("mxchat_channel_{$session_id}", $channel_id);
2785 +
2786 + // Auto-invite agents to the channel
2787 + $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2788 +
2789 + if (!empty($agent_user_ids)) {
2790 + // Parse user IDs (one per line)
2791 + $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2792 +
2793 + foreach ($user_ids as $user_id_to_invite) {
2794 + //error_log("Inviting user to channel: $user_id_to_invite");
2795 +
2796 + $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2797 + 'headers' => [
2798 + 'Content-Type' => 'application/json',
2799 + 'Authorization' => 'Bearer ' . $slack_bot_token
2800 + ],
2801 + 'body' => json_encode([
2802 + 'channel' => $channel_id,
2803 + 'users' => $user_id_to_invite
2804 + ])
2805 + ]);
2806 +
2807 + if (!is_wp_error($invite_response)) {
2808 + $invite_body = wp_remote_retrieve_body($invite_response);
2809 + $invite_data = json_decode($invite_body, true);
2810 + //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2811 +
2812 + if (isset($invite_data['ok']) && $invite_data['ok']) {
2813 + //error_log("Successfully invited user $user_id_to_invite to channel");
2814 + } else {
2815 + //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2816 + }
2817 + } else {
2818 + //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2819 + }
2820 + }
2821 + } else {
2822 + //error_log("No agent user IDs configured for auto-invite");
2823 + }
2824 + } else {
2825 + //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2826 + }
2827 + } else {
2828 + //error_log("WP Error creating channel: " . $response->get_error_message());
2829 + }
2830 +
2831 + if (empty($channel_id)) {
2832 + return false; // Failed to create channel
2833 + }
2834 + }
2835 +
2836 + // Get recent chat history
2717 2837 $history = get_option("mxchat_history_{$session_id}", []);
2718 - $recent_history = array_slice($history, -5); // Get last 5 messages
2838 + $recent_history = array_slice($history, -5);
2719 2839
2720 - // Format conversation history
2840 + // Format conversation context
2721 2841 $conversation_context = "";
2722 2842 if (!empty($recent_history)) {
2723 2843 $conversation_context = "*Recent Conversation:*\n";
2724 2844 foreach ($recent_history as $hist_message) {
@@ -2729,83 +2849,32 @@
2729 2849 }
2730 2850
2731 2851 update_option("mxchat_mode_{$session_id}", 'agent');
2732 2852
2733 - $webhook_data = [
2734 - 'blocks' => [
2735 - [
2736 - 'type' => 'header',
2737 - 'text' => [
2738 - 'type' => 'plain_text',
2739 - 'text' => '🔔 New Live Agent Request',
2740 - 'emoji' => true
2741 - ]
2742 - ],
2743 - [
2744 - 'type' => 'section',
2745 - 'fields' => [
2746 - [
2747 - 'type' => 'mrkdwn',
2748 - 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2749 - ],
2750 - [
2751 - 'type' => 'mrkdwn',
2752 - 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2753 - ]
2754 - ]
2755 - ]
2756 - ]
2757 - ];
2758 -
2759 - // Add conversation history if exists
2853 + // Send message to channel
2854 + $channel_message = "🔔 *New Live Agent Request*\n\n";
2855 + $channel_message .= "*Session ID:* `{$session_id}`\n";
2856 + $channel_message .= "*User ID:* `{$user_id}`\n\n";
2857 +
2760 2858 if (!empty($conversation_context)) {
2761 - $webhook_data['blocks'][] = [
2762 - 'type' => 'section',
2763 - 'text' => [
2764 - 'type' => 'mrkdwn',
2765 - 'text' => $conversation_context
2766 - ]
2767 - ];
2859 + $channel_message .= $conversation_context;
2768 2860 }
2861 +
2862 + $channel_message .= "*Current Message:*\n{$message}\n\n";
2863 + $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2769 2864
2770 - // Add the current message
2771 - $webhook_data['blocks'][] = [
2772 - 'type' => 'section',
2773 - 'text' => [
2774 - 'type' => 'mrkdwn',
2775 - 'text' => sprintf('*Current Message:*\n%s', $message)
2776 - ]
2777 - ];
2778 -
2779 - // Add the reply button
2780 - $webhook_data['blocks'][] = [
2781 - 'type' => 'actions',
2782 - 'elements' => [
2783 - [
2784 - 'type' => 'button',
2785 - 'text' => [
2786 - 'type' => 'plain_text',
2787 - 'text' => '✍️ Reply',
2788 - 'emoji' => true
2789 - ],
2790 - 'value' => $session_id,
2791 - 'action_id' => 'reply_to_user',
2792 - 'style' => 'primary'
2793 - ]
2794 - ]
2795 - ];
2796 -
2797 - $response = wp_remote_post($slack_webhook_url, [
2798 - 'body' => json_encode($webhook_data),
2865 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2799 2866 'headers' => [
2800 2867 'Content-Type' => 'application/json',
2868 + 'Authorization' => 'Bearer ' . $slack_bot_token
2801 2869 ],
2870 + 'body' => json_encode([
2871 + 'channel' => $channel_id,
2872 + 'text' => $channel_message,
2873 + 'mrkdwn' => true
2874 + ])
2802 2875 ]);
2803 2876
2804 - if (is_wp_error($response)) {
2805 - return false;
2806 - }
2807 -
2808 2877 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2809 2878 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2810 2879
2811 2880 $this->fallbackResponse = [
@@ -2824,79 +2893,145 @@
2824 2893 'fallbackResponse' => $this->fallbackResponse
2825 2894 ]);
2826 2895 wp_die();
2827 2896 }
2897 +
2898 +private function generate_channel_name($session_id) {
2899 + $email = null;
2900 + $name = null;
2901 +
2902 + // 1. First priority: Check if user is logged in and get their info
2903 + if (is_user_logged_in()) {
2904 + $current_user = wp_get_current_user();
2905 + if (!empty($current_user->user_email)) {
2906 + $email = $current_user->user_email;
2907 + //error_log("[DEBUG] Using logged-in user email for channel: {$email}");
2908 + }
2909 + if (!empty($current_user->display_name)) {
2910 + $name = $current_user->display_name;
2911 + //error_log("[DEBUG] Using logged-in user name for channel: {$name}");
2912 + }
2913 + }
2914 +
2915 + // 2. Second priority: Check for saved email/name from "require email to chat" option
2916 + if (empty($email)) {
2917 + $email_option_key = "mxchat_email_{$session_id}";
2918 + $saved_email = get_option($email_option_key);
2919 + if (!empty($saved_email)) {
2920 + $email = $saved_email;
2921 + //error_log("[DEBUG] Using saved email from session for channel: {$email}");
2922 + }
2923 + }
2924 +
2925 + if (empty($name)) {
2926 + $name_option_key = "mxchat_name_{$session_id}";
2927 + $saved_name = get_option($name_option_key);
2928 + if (!empty($saved_name)) {
2929 + $name = $saved_name;
2930 + //error_log("[DEBUG] Using saved name from session for channel: {$name}");
2931 + }
2932 + }
2933 +
2934 + // 3. Third priority: Check existing chat transcript for email/name
2935 + if (empty($email) || empty($name)) {
2936 + global $wpdb;
2937 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
2938 + $existing_data = $wpdb->get_row($wpdb->prepare(
2939 + "SELECT user_email, user_name FROM $table_name WHERE session_id = %s AND (user_email IS NOT NULL OR user_name IS NOT NULL) LIMIT 1",
2940 + $session_id
2941 + ));
2942 +
2943 + if ($existing_data) {
2944 + if (empty($email) && !empty($existing_data->user_email)) {
2945 + $email = $existing_data->user_email;
2946 + //error_log("[DEBUG] Using email from chat transcript for channel: {$email}");
2947 + }
2948 + if (empty($name) && !empty($existing_data->user_name)) {
2949 + $name = $existing_data->user_name;
2950 + //error_log("[DEBUG] Using name from chat transcript for channel: {$name}");
2951 + }
2952 + }
2953 + }
2954 +
2955 + // 4. Generate channel name based on priority: Name > Email > Session ID
2956 + $channel_name = '';
2957 +
2958 + if (!empty($name)) {
2959 + // Convert name to valid Slack channel name
2960 + $base_name = strtolower(trim($name));
2961 + // Replace spaces and invalid characters
2962 + $base_name = preg_replace('/[^a-z0-9\s]/', '', $base_name);
2963 + $base_name = preg_replace('/\s+/', '-', $base_name);
2964 + $base_name = trim($base_name, '-');
2965 +
2966 + // Get last 4 characters of session ID for uniqueness
2967 + $session_suffix = substr($session_id, -4);
2968 + $channel_name = 'chat-' . $base_name . '-' . strtolower($session_suffix);
2969 +
2970 + // Slack channel names have a 21 character limit
2971 + if (strlen($channel_name) > 21) {
2972 + // Calculate available space for name (21 - 'chat-' - '-' - session_suffix)
2973 + $available_space = 21 - 5 - 1 - strlen($session_suffix); // 'chat-' = 5, '-' = 1
2974 + $truncated_name = substr($base_name, 0, $available_space);
2975 + $truncated_name = rtrim($truncated_name, '-'); // Remove trailing hyphen
2976 + $channel_name = 'chat-' . $truncated_name . '-' . strtolower($session_suffix);
2977 + }
2978 +
2979 + //error_log("[DEBUG] Using name for channel: {$channel_name} (from name: {$name})");
2980 +
2981 + } elseif (!empty($email)) {
2982 + // Convert email to valid Slack channel name (your existing logic)
2983 + $channel_name = 'chat-' . strtolower(str_replace(['@', '.', '+', '_'], ['-at-', '-', '-plus-', '-'], $email));
2984 + // Remove any remaining invalid characters
2985 + $channel_name = preg_replace('/[^a-z0-9\-]/', '', $channel_name);
2986 + // Ensure it doesn't end with a hyphen
2987 + $channel_name = rtrim($channel_name, '-');
2988 + // Slack channel names have a 21 character limit, so truncate if needed
2989 + if (strlen($channel_name) > 21) {
2990 + $channel_name = substr($channel_name, 0, 21);
2991 + $channel_name = rtrim($channel_name, '-'); // Remove trailing hyphen if truncation created one
2992 + }
2993 +
2994 + //error_log("[DEBUG] Using email for channel: {$channel_name} (from email: {$email})");
2995 +
2996 + } else {
2997 + // Fallback to session ID if no name or email found
2998 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
2999 + //error_log("[DEBUG] No name or email found, using session ID for channel: {$channel_name}");
3000 + }
3001 +
3002 + // Final validation - ensure channel name meets Slack requirements
3003 + if (strlen($channel_name) > 21) {
3004 + $channel_name = substr($channel_name, 0, 21);
3005 + $channel_name = rtrim($channel_name, '-');
3006 + }
3007 +
3008 + //error_log("[DEBUG] Generated channel name: {$channel_name}");
3009 + return $channel_name;
3010 +}
2828 3011 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2829 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
3012 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3013 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2830 3014
2831 - if (empty($slack_webhook_url)) {
2832 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
3015 + if (empty($slack_bot_token) || empty($channel_id)) {
2833 3016 return false;
2834 3017 }
2835 3018
2836 - $webhook_data = [
2837 - 'blocks' => [
2838 - [
2839 - 'type' => 'header',
2840 - 'text' => [
2841 - 'type' => 'plain_text',
2842 - 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2843 - 'emoji' => true
2844 - ]
2845 - ],
2846 - [
2847 - 'type' => 'section',
2848 - 'fields' => [
2849 - [
2850 - 'type' => 'mrkdwn',
2851 - 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2852 - ],
2853 - [
2854 - 'type' => 'mrkdwn',
2855 - 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2856 - ]
2857 - ]
2858 - ],
2859 - [
2860 - 'type' => 'section',
2861 - 'text' => [
2862 - 'type' => 'mrkdwn',
2863 - 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2864 - ]
2865 - ],
2866 - [
2867 - 'type' => 'actions',
2868 - 'elements' => [
2869 - [
2870 - 'type' => 'button',
2871 - 'text' => [
2872 - 'type' => 'plain_text',
2873 - 'text' => esc_html__('✍️ Reply', 'mxchat'),
2874 - 'emoji' => true
2875 - ],
2876 - 'value' => $session_id,
2877 - 'action_id' => 'reply_to_user',
2878 - 'style' => 'primary'
2879 - ]
2880 - ]
2881 - ]
2882 - ]
2883 - ];
3019 + $user_message = "💬 *User:* {$message}";
2884 3020
2885 - $response = wp_remote_post($slack_webhook_url, [
2886 - 'body' => json_encode($webhook_data),
3021 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2887 3022 'headers' => [
2888 3023 'Content-Type' => 'application/json',
3024 + 'Authorization' => 'Bearer ' . $slack_bot_token
2889 3025 ],
3026 + 'body' => json_encode([
3027 + 'channel' => $channel_id,
3028 + 'text' => $user_message,
3029 + 'mrkdwn' => true
3030 + ])
2890 3031 ]);
2891 3032
2892 - if (is_wp_error($response)) {
2893 - //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2894 - return false;
2895 - }
2896 -
2897 - //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2898 - return true;
3033 + return !is_wp_error($response);
2899 3034 }
2900 3035 public function handle_slack_interaction(WP_REST_Request $request) {
2901 3036 //error_log('Received Slack interaction');
2902 3037
@@ -2984,17 +3119,16 @@
2984 3119
2985 3120 // Default acknowledgment
2986 3121 return new WP_REST_Response(['ok' => true]);
2987 3122 }
2988 -
2989 3123 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2990 3124 //error_log('Received agent response request');
2991 3125 //error_log('Request data: ' . print_r($request->get_params(), true));
2992 - // error_log('Raw body: ' . file_get_contents('php://input'));
3126 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2993 3127
2994 3128 // Get the data from Slack's slash command format
2995 3129 $command_text = $request->get_param('text');
2996 - // error_log('Command text: ' . $command_text);
3130 + // //error_log('Command text: ' . $command_text);
2997 3131
2998 3132 if (empty($command_text)) {
2999 3133 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
3000 3134 return new WP_REST_Response([
@@ -3019,9 +3153,9 @@
3019 3153 // Save the message
3020 3154 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
3021 3155
3022 3156 if (!$message_id) {
3023 - // error_log('Failed to save agent message');
3157 + // //error_log('Failed to save agent message');
3024 3158 return new WP_REST_Response([
3025 3159 'error' => esc_html__('Failed to save message', 'mxchat')
3026 3160 ], 500);
3027 3161 }
@@ -3031,29 +3165,141 @@
3031 3165 'response_type' => 'in_channel',
3032 3166 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
3033 3167 ], 200);
3034 3168 }
3035 -
3036 -
3037 3169 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
3038 - //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
3039 -
3040 - // Just update mode to AI
3170 + // Update mode to AI
3041 3171 update_option("mxchat_mode_{$session_id}", 'ai');
3172 +
3173 + // Clear any existing PDF context to start fresh
3174 + $this->clear_pdf_transients($session_id);
3175 +
3176 + // Set the response with explicit chat_mode
3177 + $this->fallbackResponse = [
3178 + 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
3179 + 'html' => '',
3180 + 'images' => [],
3181 + 'chat_mode' => 'ai' // Ensure this is set
3182 + ];
3183 +
3184 + // Return the complete response array instead of just true
3185 + return $this->fallbackResponse;
3186 +}
3042 3187
3043 - // Initialize states
3044 - $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
3045 - $this->productCardHtml = '';
3046 -
3047 - // Set the response message
3048 - $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
3049 -
3050 - return true; // Intent was handled
3188 +public function handle_slack_messages(WP_REST_Request $request) {
3189 + // Log the incoming request for debugging
3190 + //error_log('Slack events request received: ' . $request->get_body());
3191 +
3192 + $body = $request->get_body();
3193 + $data = json_decode($body, true);
3194 +
3195 + // Handle Slack URL verification
3196 + if (isset($data['type']) && $data['type'] === 'url_verification') {
3197 + //error_log('Slack URL verification challenge: ' . $data['challenge']);
3198 + return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
3199 + }
3200 +
3201 + // IMPORTANT: Handle Slack's event deduplication
3202 + if (isset($data['event_id'])) {
3203 + $event_id = $data['event_id'];
3204 + $processed_events = get_transient('mxchat_slack_events') ?: [];
3205 +
3206 + // Check if we've already processed this event
3207 + if (in_array($event_id, $processed_events)) {
3208 + //error_log("Duplicate event detected: $event_id");
3209 + return new WP_REST_Response(['ok' => true]);
3210 + }
3211 +
3212 + // Add this event to processed list
3213 + $processed_events[] = $event_id;
3214 + // Keep only last 100 events to prevent memory issues
3215 + if (count($processed_events) > 100) {
3216 + $processed_events = array_slice($processed_events, -100);
3217 + }
3218 + // Store for 1 hour
3219 + set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
3220 + }
3221 +
3222 + // Handle message events
3223 + if (isset($data['event']) && $data['event']['type'] === 'message') {
3224 + $event = $data['event'];
3225 +
3226 + // Skip bot messages and messages with subtypes (like bot_message)
3227 + if (isset($event['bot_id']) || isset($event['subtype'])) {
3228 + return new WP_REST_Response(['ok' => true]);
3229 + }
3230 +
3231 + // Additional check: Skip if this is a threaded reply to our confirmation
3232 + if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
3233 + return new WP_REST_Response(['ok' => true]);
3234 + }
3235 +
3236 + $channel_id = $event['channel'];
3237 + $message_text = $event['text'] ?? '';
3238 + $message_ts = $event['ts'] ?? '';
3239 +
3240 + // Find session ID by looking for matching channel
3241 + global $wpdb;
3242 + $session_option = $wpdb->get_var(
3243 + $wpdb->prepare(
3244 + "SELECT option_name FROM {$wpdb->options}
3245 + WHERE option_name LIKE 'mxchat_channel_%'
3246 + AND option_value = %s",
3247 + $channel_id
3248 + )
3249 + );
3250 +
3251 + if ($session_option) {
3252 + $session_id = str_replace('mxchat_channel_', '', $session_option);
3253 +
3254 + // Create a unique key for this specific message
3255 + $message_key = md5($session_id . $message_ts . $message_text);
3256 + $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
3257 +
3258 + // Check if we've already processed this exact message
3259 + if (in_array($message_key, $processed_messages)) {
3260 + //error_log("Duplicate message detected for session $session_id");
3261 + return new WP_REST_Response(['ok' => true]);
3262 + }
3263 +
3264 + // Add to processed messages
3265 + $processed_messages[] = $message_key;
3266 + // Keep only last 50 messages per session
3267 + if (count($processed_messages) > 50) {
3268 + $processed_messages = array_slice($processed_messages, -50);
3269 + }
3270 + set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
3271 +
3272 + // Save the agent message
3273 + $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
3274 +
3275 + // Send confirmation back to Slack (only once)
3276 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3277 + if (!empty($slack_bot_token)) {
3278 + // Use a transient to prevent duplicate confirmations
3279 + $confirm_key = 'mxchat_confirm_' . $message_key;
3280 + if (!get_transient($confirm_key)) {
3281 + wp_remote_post('https://slack.com/api/chat.postMessage', [
3282 + 'headers' => [
3283 + 'Content-Type' => 'application/json',
3284 + 'Authorization' => 'Bearer ' . $slack_bot_token
3285 + ],
3286 + 'body' => json_encode([
3287 + 'channel' => $channel_id,
3288 + 'text' => "✅ _Message sent to user_",
3289 + 'thread_ts' => $event['ts'] // Reply in thread
3290 + ])
3291 + ]);
3292 + // Set transient to prevent duplicate confirmations
3293 + set_transient($confirm_key, true, 300); // 5 minutes
3294 + }
3295 + }
3296 + }
3297 + }
3298 +
3299 + return new WP_REST_Response(['ok' => true]);
3051 3300 }
3052 3301
3053 -
3054 -
3055 -
3056 3302 // For the word upload handler
3057 3303 public function mxchat_handle_word_upload() {
3058 3304 // Delegate to word handler
3059 3305 $this->word_handler->mxchat_handle_word_upload();
@@ -3076,21 +3322,102 @@
3076 3322 return MxChat_User::mxchat_get_user_identifier();
3077 3323 }
3078 3324
3079 3325 private function mxchat_generate_embedding($text, $api_key) {
3080 - $endpoint = 'https://api.openai.com/v1/embeddings';
3081 -
3082 - $body = wp_json_encode([
3083 - 'input' => $text,
3084 - 'model' => 'text-embedding-ada-002'
3085 - ]);
3086 -
3326 + try {
3327 + // Get options and selected model
3328 + $options = get_option('mxchat_options');
3329 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3330 +
3331 + // Determine endpoint and API key based on model
3332 + if (strpos($selected_model, 'voyage') === 0) {
3333 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
3334 + $api_key = $options['voyage_api_key'] ?? '';
3335 +
3336 + // Check if Voyage API key is missing
3337 + if (empty($api_key)) {
3338 + //error_log('Voyage API key is missing');
3339 + return [
3340 + 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
3341 + 'error_code' => 'missing_voyage_api_key'
3342 + ];
3343 + }
3344 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3345 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3346 + $api_key = $options['gemini_api_key'] ?? '';
3347 +
3348 + // Check if Gemini API key is missing
3349 + if (empty($api_key)) {
3350 + //error_log('Gemini API key is missing');
3351 + return [
3352 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3353 + 'error_code' => 'missing_gemini_api_key'
3354 + ];
3355 + }
3356 + } else {
3357 + $endpoint = 'https://api.openai.com/v1/embeddings';
3358 + // Use the passed API key for OpenAI
3359 +
3360 + // Check if OpenAI API key is missing
3361 + if (empty($api_key)) {
3362 + //error_log('OpenAI API key is missing');
3363 + return [
3364 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3365 + 'error_code' => 'missing_openai_api_key'
3366 + ];
3367 + }
3368 + }
3369 +
3370 + // Check if text is empty
3371 + if (empty($text)) {
3372 + //error_log('Empty text provided for embedding generation');
3373 + return [
3374 + 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
3375 + 'error_code' => 'empty_embedding_text'
3376 + ];
3377 + }
3378 +
3379 + // Prepare request body based on provider
3380 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3381 + // Gemini API format
3382 + $request_body = [
3383 + 'model' => 'models/' . $selected_model,
3384 + 'content' => [
3385 + 'parts' => [
3386 + ['text' => $text]
3387 + ]
3388 + ],
3389 + 'outputDimensionality' => 1536
3390 + ];
3391 +
3392 + // Prepare headers for Gemini (API key as query parameter)
3393 + $endpoint .= '?key=' . $api_key;
3394 + $headers = [
3395 + 'Content-Type' => 'application/json'
3396 + ];
3397 + } else {
3398 + // OpenAI/Voyage API format
3399 + $request_body = [
3400 + 'input' => $text,
3401 + 'model' => $selected_model
3402 + ];
3403 +
3404 + // Add output_dimension for voyage-3-large
3405 + if ($selected_model === 'voyage-3-large') {
3406 + $request_body['output_dimension'] = 2048;
3407 + }
3408 +
3409 + // Prepare headers for OpenAI/Voyage
3410 + $headers = [
3411 + 'Content-Type' => 'application/json',
3412 + 'Authorization' => 'Bearer ' . $api_key
3413 + ];
3414 + }
3415 +
3416 + // Prepare request arguments
3087 3417 $args = [
3088 - 'body' => $body,
3089 - 'headers' => [
3090 - 'Content-Type' => 'application/json',
3091 - 'Authorization' => 'Bearer ' . $api_key,
3092 - ],
3418 + 'body' => wp_json_encode($request_body),
3419 + 'headers' => $headers,
3093 3420 'timeout' => 60,
3094 3421 'redirection' => 5,
3095 3422 'blocking' => true,
3096 3423 'httpversion' => '1.0',
@@ -3095,63 +3422,177 @@
3095 3422 'blocking' => true,
3096 3423 'httpversion' => '1.0',
3097 3424 'sslverify' => true,
3098 3425 ];
3099 -
3426 +
3427 + // Make the request
3100 3428 $response = wp_remote_post($endpoint, $args);
3101 -
3429 +
3430 + // Handle WordPress errors
3102 3431 if (is_wp_error($response)) {
3103 - return null;
3432 + $error_message = $response->get_error_message();
3433 + //error_log('Embedding Generation Error: ' . $error_message);
3434 + return [
3435 + 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
3436 + 'error_code' => 'embedding_connection_error'
3437 + ];
3104 3438 }
3105 -
3439 +
3440 + // Check HTTP status code
3441 + $status_code = wp_remote_retrieve_response_code($response);
3442 + if ($status_code !== 200) {
3443 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
3444 +
3445 + $error_message = isset($response_body['error']['message'])
3446 + ? $response_body['error']['message']
3447 + : 'HTTP Error ' . $status_code;
3448 +
3449 + $error_type = isset($response_body['error']['type'])
3450 + ? $response_body['error']['type']
3451 + : 'unknown';
3452 +
3453 + //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
3454 +
3455 + // Handle specific error types
3456 + switch ($error_type) {
3457 + case 'invalid_request_error':
3458 + if (strpos($error_message, 'API key') !== false) {
3459 + return [
3460 + 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
3461 + 'error_code' => 'embedding_invalid_api_key'
3462 + ];
3463 + }
3464 + break;
3465 +
3466 + case 'authentication_error':
3467 + return [
3468 + 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
3469 + 'error_code' => 'embedding_auth_error'
3470 + ];
3471 +
3472 + case 'rate_limit_exceeded':
3473 + return [
3474 + 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
3475 + 'error_code' => 'embedding_rate_limit'
3476 + ];
3477 +
3478 + case 'quota_exceeded':
3479 + return [
3480 + 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
3481 + 'error_code' => 'embedding_quota_exceeded'
3482 + ];
3483 + }
3484 +
3485 + // Generic error fallback
3486 + return [
3487 + 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3488 + 'error_code' => 'embedding_api_error',
3489 + 'status_code' => $status_code
3490 + ];
3491 + }
3492 +
3106 3493 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3107 -
3108 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3109 - return $response_body['data'][0]['embedding'];
3494 +
3495 + // Handle different response formats based on provider
3496 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3497 + // Gemini API response format
3498 + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3499 + return $response_body['embedding']['values'];
3500 + } else {
3501 + //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3502 + return [
3503 + 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3504 + 'error_code' => 'invalid_gemini_embedding_response'
3505 + ];
3506 + }
3110 3507 } else {
3111 - return null;
3508 + // OpenAI/Voyage API response format
3509 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3510 + return $response_body['data'][0]['embedding'];
3511 + } else {
3512 + //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3513 + return [
3514 + 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3515 + 'error_code' => 'invalid_embedding_response'
3516 + ];
3517 + }
3112 3518 }
3519 + } catch (Exception $e) {
3520 + //error_log('Embedding Exception: ' . $e->getMessage());
3521 + return [
3522 + 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3523 + 'error_code' => 'embedding_exception'
3524 + ];
3113 3525 }
3526 +}
3114 3527
3115 3528
3116 -private function mxchat_find_relevant_content($user_embedding) {
3117 - //error_log('MXChat Vector Search: Starting content search...');
3529 +private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default') {
3530 + error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
3531 +
3532 + // Get bot-specific Pinecone configuration
3533 + $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3534 +
3535 + // Debug: Log the Pinecone configuration
3536 + error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
3537 + error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
3538 + error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
3539 + error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
3540 + error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
3541 +
3542 + // Determine whether to use Pinecone based on bot configuration
3543 + $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
3544 +
3545 + error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
3118 3546
3119 - // Retrieve the add-on settings from the database.
3120 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
3121 -
3122 - // Determine whether Pinecone is enabled.
3123 - // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
3124 - $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
3125 -
3126 - //error_log('Pinecone enabled flag: ' . $use_pinecone);
3127 -
3128 - if ($use_pinecone === 1) {
3129 - //error_log('MXChat Vector Search: Using Pinecone database');
3130 - return $this->find_relevant_content_pinecone($user_embedding);
3547 + if ($use_pinecone) {
3548 + return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
3131 3549 } else {
3132 - //error_log('MXChat Vector Search: Using WordPress database');
3133 - return $this->find_relevant_content_wordpress($user_embedding);
3550 + return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
3134 3551 }
3135 3552 }
3136 3553
3137 -private function find_relevant_content_wordpress($user_embedding) {
3554 +private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
3138 3555 global $wpdb;
3139 3556 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3140 - $cache_key = 'mxchat_system_prompt_embeddings';
3557 + $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id; // Bot-specific cache key
3141 3558 $batch_size = 500;
3142 3559
3560 + // Initialize similarity analysis storage
3561 + $this->last_similarity_analysis = [
3562 + 'knowledge_base_type' => 'WordPress Database',
3563 + 'bot_id' => $bot_id, // Track which bot is being used
3564 + 'top_matches' => [],
3565 + 'threshold_used' => 0,
3566 + 'total_checked' => 0
3567 + ];
3568 +
3569 + // Get bot-specific options for similarity threshold
3570 + $bot_options = $this->get_bot_options($bot_id);
3571 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
3572 +
3143 3573 // Retrieve embeddings from cache or database
3144 3574 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3145 3575 if ($embeddings === false) {
3576 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
3146 3577 $embeddings = [];
3147 3578 $offset = 0;
3148 3579
3149 - // Load in batches and build cache
3150 3580 do {
3581 + // Add bot_id filter if not default and if bot_metadata column exists
3582 + $bot_filter = '';
3583 + if ($bot_id !== 'default') {
3584 + // Check if bot_metadata column exists
3585 + $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
3586 + if ($column_exists) {
3587 + $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3588 + }
3589 + }
3590 +
3151 3591 $query = $wpdb->prepare(
3152 - "SELECT id, embedding_vector
3592 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
3153 3593 FROM {$system_prompt_table}
3594 + WHERE 1=1 {$bot_filter}
3154 3595 LIMIT %d OFFSET %d",
3155 3596 $batch_size,
3156 3597 $offset
3157 3598 );
@@ -3162,62 +3603,132 @@
3162 3603 }
3163 3604
3164 3605 $embeddings = array_merge($embeddings, $batch);
3165 3606 $offset += $batch_size;
3166 -
3167 - // Free memory
3168 3607 unset($batch);
3169 -
3170 3608 } while (true);
3171 3609
3172 3610 if (empty($embeddings)) {
3173 - return ''; // Return an empty string if no embeddings found
3611 + return '';
3174 3612 }
3613 +
3614 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
3175 3615 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3176 3616 }
3177 3617
3178 - // Initialize array to store relevant results with similarity scores
3618 + // Get knowledge manager instance for role checking
3619 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3620 +
3621 + // Get base similarity threshold from bot options or default options
3622 + $similarity_threshold = isset($current_options['similarity_threshold'])
3623 + ? ((int) $current_options['similarity_threshold']) / 100
3624 + : 0.35;
3625 +
3626 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3627 +
3628 + // Calculate similarities and build results array
3629 + $all_similarities = [];
3179 3630 $relevant_results = [];
3180 - // Iterate through embeddings to calculate similarity
3631 +
3181 3632 foreach ($embeddings as $embedding) {
3182 3633 $database_embedding = $embedding->embedding_vector
3183 3634 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3184 3635 : null;
3636 +
3185 3637 if (is_array($database_embedding) && is_array($user_embedding)) {
3186 3638 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3187 - $relevant_results[] = [
3188 - 'id' => $embedding->id,
3189 - 'similarity' => $similarity
3639 +
3640 + // Check role access
3641 + $role_restriction = $embedding->role_restriction ?? 'public';
3642 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3643 +
3644 + // Store ALL similarities for testing (top 10)
3645 + $source_display = '';
3646 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3647 + $source_display = $embedding->source_url;
3648 + } else {
3649 + $content_preview = strip_tags($embedding->article_content ?? '');
3650 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3651 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3652 + }
3653 +
3654 + $all_similarities[] = [
3655 + 'document_id' => $embedding->id,
3656 + 'similarity' => $similarity,
3657 + 'similarity_percentage' => round($similarity * 100, 2),
3658 + 'above_threshold' => $similarity >= $similarity_threshold,
3659 + 'source_display' => $source_display,
3660 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3661 + 'used_for_context' => false, // Initialize as false, we'll update this later
3662 + 'role_restriction' => $role_restriction, // Include role info for testing
3663 + 'has_access' => $has_access, // Include access info for testing
3664 + 'filtered_out' => !$has_access // Mark if filtered out by role
3190 3665 ];
3666 +
3667 + // Only consider results above threshold AND with access for actual content retrieval
3668 + if ($similarity >= $similarity_threshold && $has_access) {
3669 + $relevant_results[] = [
3670 + 'id' => $embedding->id,
3671 + 'similarity' => $similarity
3672 + ];
3673 + }
3191 3674 }
3192 - // Free memory
3675 +
3193 3676 unset($database_embedding);
3194 3677 }
3195 3678
3196 - // Retrieve the similarity threshold
3197 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
3198 -
3199 - // Filter and sort relevant results by similarity
3200 - $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3201 - return $result['similarity'] >= $similarity_threshold;
3679 + // Sort ALL similarities for testing display (highest first)
3680 + usort($all_similarities, function ($a, $b) {
3681 + return $b['similarity'] <=> $a['similarity'];
3202 3682 });
3683 +
3684 + // Sort relevant results by similarity (highest first)
3203 3685 usort($relevant_results, function ($a, $b) {
3204 3686 return $b['similarity'] <=> $a['similarity'];
3205 3687 });
3206 -
3207 - // Limit to the top 5 results
3688 +
3689 + // Get top 5 results for actual content (standard approach)
3208 3690 $top_results = array_slice($relevant_results, 0, 5);
3209 -
3210 - // Initialize the final content
3691 +
3692 + // NOW mark which documents are actually used for context
3693 + $used_document_ids = [];
3694 + foreach ($top_results as $result) {
3695 + $used_document_ids[] = $result['id'];
3696 + }
3697 +
3698 + // Update the all_similarities array to mark which were actually used
3699 + foreach ($all_similarities as &$similarity_item) {
3700 + $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
3701 + }
3702 +
3703 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
3704 + $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
3705 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3706 +
3707 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3708 +
3709 + // Initialize final content
3211 3710 $content = '';
3212 -
3213 - // Fetch and combine content for the top results
3214 - foreach ($top_results as $result) {
3711 +
3712 + // Track document IDs to avoid duplicates
3713 + $added_document_ids = [];
3714 +
3715 + // Fetch and format content for each selected result
3716 + foreach ($top_results as $index => $result) {
3717 + if (in_array($result['id'], $added_document_ids)) {
3718 + continue;
3719 + }
3720 +
3215 3721 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3216 - // Check if the content is PDF-related and add surrounding pages
3722 + $added_document_ids[] = $result['id'];
3723 +
3724 + $content .= "## Reference " . ($index + 1) . " ##\n";
3725 + $content .= $chunk_content . "\n\n";
3726 +
3727 + // PDF surrounding pages logic (unchanged)
3217 3728 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3218 3729 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3219 - "SELECT article_content FROM {$system_prompt_table}
3730 + "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
3220 3731 WHERE id IN (
3221 3732 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3222 3733 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3223 3734 )",
@@ -3223,52 +3734,116 @@
3223 3734 )",
3224 3735 $result['id'],
3225 3736 $result['id']
3226 3737 ));
3227 - // Add previous content if it exists
3738 +
3739 + // Check role access for surrounding content too
3228 3740 if (!empty($surrounding_content[0])) {
3229 - $content .= $surrounding_content[0]->article_content . "\n\n";
3741 + $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public';
3742 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3743 + $content .= "## Related Content ##\n";
3744 + $content .= $surrounding_content[0]->article_content . "\n\n";
3745 + $added_document_ids[] = $surrounding_content[0]->id;
3746 + }
3230 3747 }
3231 - // Add the main chunk content
3232 - $content .= $chunk_content . "\n\n";
3233 - // Add next content if it exists
3748 +
3234 3749 if (!empty($surrounding_content[1])) {
3235 - $content .= $surrounding_content[1]->article_content . "\n\n";
3750 + $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public';
3751 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3752 + $content .= "## Related Content ##\n";
3753 + $content .= $surrounding_content[1]->article_content . "\n\n";
3754 + $added_document_ids[] = $surrounding_content[1]->id;
3755 + }
3236 3756 }
3237 - } else {
3238 - // For non-PDF content, add directly
3239 - $content .= $chunk_content . "\n\n";
3240 3757 }
3241 3758 }
3759 +
3760 + // Add response guidelines
3761 + if (empty($top_results)) {
3762 + $content = "No reference information was found for this query.\n\n";
3763 + } else {
3764 + $content .= "\n## Response Guidelines ##\n" .
3765 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3766 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3767 + "If you don't have specific information or are uncertain about any details, it's always " .
3768 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3769 + "When information is incomplete, let them know you are unsure.";
3770 + }
3242 3771
3243 3772 return trim($content);
3244 3773 }
3245 -/**
3246 - * Find relevant content in Pinecone vector database
3247 - */
3248 -private function find_relevant_content_pinecone($user_embedding) {
3249 - $options = get_option('mxchat_pinecone_addon_options', array());
3250 - $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3251 - $host = $options['mxchat_pinecone_host'] ?? '';
3252 3774
3775 +private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
3776 + global $wpdb;
3777 +
3778 + error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
3779 + error_log(" - bot_id: " . $bot_id);
3780 + error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
3781 + error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
3782 +
3783 + // Use bot-specific config or fall back to default
3784 + if ($bot_config === null) {
3785 + $bot_config = $this->get_bot_pinecone_config($bot_id);
3786 + }
3787 +
3788 + $api_key = $bot_config['api_key'] ?? '';
3789 + $host = $bot_config['host'] ?? '';
3790 + $namespace = $bot_config['namespace'] ?? '';
3791 +
3792 + error_log("MXCHAT DEBUG: Pinecone query parameters:");
3793 + error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
3794 + error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
3795 + error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
3796 +
3797 + // Initialize similarity analysis storage
3798 + $this->last_similarity_analysis = [
3799 + 'knowledge_base_type' => 'Pinecone',
3800 + 'bot_id' => $bot_id,
3801 + 'namespace' => $namespace,
3802 + 'top_matches' => [],
3803 + 'threshold_used' => 0,
3804 + 'total_checked' => 0
3805 + ];
3806 +
3253 3807 if (empty($host) || empty($api_key)) {
3254 - //error_log('Pinecone credentials not properly configured');
3808 + error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
3809 + error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
3810 + error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
3255 3811 return '';
3256 3812 }
3257 -
3258 - // Get similarity threshold from WordPress settings
3259 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
3260 -
3813 +
3814 + // Get knowledge manager instance for role checking
3815 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3816 +
3817 + // Get the similarity threshold from the bot options or main options
3818 + $bot_options = $this->get_bot_options($bot_id);
3819 + $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
3820 +
3821 + $similarity_threshold = isset($current_options['similarity_threshold'])
3822 + ? ((int) $current_options['similarity_threshold']) / 100
3823 + : 0.35;
3824 +
3825 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3826 +
3261 3827 // Prepare the query request for Pinecone
3262 3828 $api_endpoint = "https://{$host}/query";
3263 -
3829 +
3264 3830 $request_body = array(
3265 3831 'vector' => $user_embedding,
3266 - 'topK' => 5,
3832 + 'topK' => 20, // Request more to get good testing data
3267 3833 'includeMetadata' => true,
3268 3834 'includeValues' => true
3269 3835 );
3270 -
3836 +
3837 + // Add namespace if specified for this bot
3838 + if (!empty($namespace)) {
3839 + $request_body['namespace'] = $namespace;
3840 + }
3841 +
3842 + error_log("MXCHAT DEBUG: About to call Pinecone API");
3843 + error_log(" - Endpoint: " . $api_endpoint);
3844 + error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
3845 +
3271 3846 $response = wp_remote_post($api_endpoint, array(
3272 3847 'headers' => array(
3273 3848 'Api-Key' => $api_key,
3274 3849 'accept' => 'application/json',
@@ -3276,46 +3851,197 @@
3276 3851 ),
3277 3852 'body' => wp_json_encode($request_body),
3278 3853 'timeout' => 30
3279 3854 ));
3280 -
3855 +
3281 3856 if (is_wp_error($response)) {
3282 - //error_log('Pinecone query error: ' . $response->get_error_message());
3857 + error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
3283 3858 return '';
3284 3859 }
3285 -
3860 +
3286 3861 $response_code = wp_remote_retrieve_response_code($response);
3862 + error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
3863 +
3287 3864 if ($response_code !== 200) {
3288 - //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
3865 + $response_body = wp_remote_retrieve_body($response);
3866 + error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
3289 3867 return '';
3290 3868 }
3291 -
3292 - $results = json_decode(wp_remote_retrieve_body($response), true);
3869 +
3870 + // ADD DETAILED DEBUG SECTION HERE
3871 + $response_body = wp_remote_retrieve_body($response);
3872 + error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
3873 +
3874 + $results = json_decode($response_body, true);
3875 +
3876 + if (json_last_error() !== JSON_ERROR_NONE) {
3877 + error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
3878 + error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
3879 + return '';
3880 + }
3881 +
3882 + error_log("MXCHAT DEBUG: Pinecone response structure:");
3883 + error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
3884 + error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
3885 +
3293 3886 if (empty($results['matches'])) {
3887 + error_log("MXCHAT DEBUG: No matches found in Pinecone response");
3888 + error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
3294 3889 return '';
3295 3890 }
3296 -
3891 +
3892 + error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
3893 +
3894 + // Log first match details for debugging
3895 + if (!empty($results['matches'][0])) {
3896 + $first_match = $results['matches'][0];
3897 + error_log("MXCHAT DEBUG: First match details:");
3898 + error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
3899 + error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
3900 + if (isset($first_match['metadata'])) {
3901 + error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
3902 + }
3903 + }
3904 +
3297 3905 // Initialize the final content
3298 3906 $content = '';
3299 -
3300 - // Process each match
3301 - foreach ($results['matches'] as $match) {
3907 + $matches_used = 0;
3908 + $matches_used_for_context = [];
3909 +
3910 + // Process each match for actual content generation (lazy role checking)
3911 + foreach ($results['matches'] as $index => $match) {
3302 3912 // Skip if similarity is below threshold
3303 3913 if ($match['score'] < $similarity_threshold) {
3304 3914 continue;
3305 3915 }
3306 -
3307 - if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
3308 - // Add content with citation
3309 - $content .= $match['metadata']['text'] . "\n";
3310 - $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
3916 +
3917 + // Limit to top 5 matches above threshold
3918 + if ($matches_used >= 5) {
3919 + break;
3311 3920 }
3921 +
3922 + if (!empty($match['metadata']['text'])) {
3923 + // LAZY ROLE CHECK: Only check role for content we're actually considering
3924 + $match_id = $match['id'] ?? '';
3925 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
3926 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3927 +
3928 + // Skip if user doesn't have access
3929 + if (!$has_access) {
3930 + continue;
3931 + }
3932 +
3933 + // User has access - add to content
3934 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3935 + $content .= $match['metadata']['text'] . "\n\n";
3936 +
3937 + if (!empty($match['metadata']['source_url'])) {
3938 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3939 + }
3940 +
3941 + $matches_used_for_context[] = $match['id'] ?? $index;
3942 + $matches_used++;
3943 + }
3312 3944 }
3313 -
3945 +
3946 + // Process ALL matches for testing data (top 10) - with role checking for testing display
3947 + $all_matches = [];
3948 + foreach ($results['matches'] as $index => $match) {
3949 + if ($index >= 10) break; // Limit to top 10 for testing
3950 +
3951 + $match_id = $match['id'] ?? '';
3952 +
3953 + // Check role access for testing display (use cache if available)
3954 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
3955 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3956 +
3957 + $source_display = '';
3958 + if (!empty($match['metadata']['source_url'])) {
3959 + $source_display = $match['metadata']['source_url'];
3960 + } else {
3961 + $content_preview = strip_tags($match['metadata']['text'] ?? '');
3962 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3963 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3964 + }
3965 +
3966 + $match_id_for_display = $match['id'] ?? $index;
3967 +
3968 + $all_matches[] = [
3969 + 'document_id' => $match_id_for_display,
3970 + 'similarity' => $match['score'],
3971 + 'similarity_percentage' => round($match['score'] * 100, 2),
3972 + 'above_threshold' => $match['score'] >= $similarity_threshold,
3973 + 'source_display' => $source_display,
3974 + 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
3975 + 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
3976 + 'role_restriction' => $role_restriction,
3977 + 'has_access' => $has_access,
3978 + 'filtered_out' => !$has_access
3979 + ];
3980 + }
3981 +
3982 + // Store for testing panel
3983 + $this->last_similarity_analysis['top_matches'] = $all_matches;
3984 + $this->last_similarity_analysis['total_checked'] = count($results['matches']);
3985 +
3986 + // Add response guidelines
3987 + if ($matches_used === 0) {
3988 + $content = "No reference information was found for this query.\n\n";
3989 + } else {
3990 + $content .= "\n## Response Guidelines ##\n" .
3991 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3992 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3993 + "If you don't have specific information or are uncertain about any details, it's always " .
3994 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3995 + "When information is incomplete, let them know you are unsure.";
3996 + }
3997 +
3314 3998 return trim($content);
3315 3999 }
3316 4000
3317 4001
4002 +/**
4003 + * Get role restriction for a single vector (with caching)
4004 + */
4005 +private function get_single_vector_role($vector_id, $metadata = array()) {
4006 + global $wpdb;
4007 +
4008 + if (empty($vector_id)) {
4009 + return 'public';
4010 + }
4011 +
4012 + // Check cache first
4013 + $cache_key = 'mxchat_vector_role_' . $vector_id;
4014 + $cached_role = wp_cache_get($cache_key, 'mxchat_vector_roles');
4015 +
4016 + if ($cached_role !== false) {
4017 + return $cached_role;
4018 + }
4019 +
4020 + $role_restriction = 'public';
4021 +
4022 + // First try Pinecone metadata
4023 + if (!empty($metadata['role_restriction'])) {
4024 + $role_restriction = $metadata['role_restriction'];
4025 + } else {
4026 + // Check WordPress table for user-modified roles
4027 + $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4028 + $stored_role = $wpdb->get_var($wpdb->prepare(
4029 + "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
4030 + $vector_id
4031 + ));
4032 +
4033 + if ($stored_role) {
4034 + $role_restriction = $stored_role;
4035 + }
4036 + }
4037 +
4038 + // Cache individual role for 1 hour
4039 + wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
4040 +
4041 + return $role_restriction;
4042 +}
4043 +
3318 4044 private function mxchat_find_relevant_products($user_embedding) {
3319 4045 //error_log('MXChat Vector Search: Starting product search...');
3320 4046
3321 4047 // Retrieve the add-on settings from the database
@@ -3333,9 +4059,8 @@
3333 4059 //error_log('MXChat Vector Search: Using WordPress database for products');
3334 4060 return $this->find_relevant_products_wordpress($user_embedding);
3335 4061 }
3336 4062 }
3337 -
3338 4063 private function find_relevant_products_wordpress($user_embedding) {
3339 4064 global $wpdb;
3340 4065 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3341 4066 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -3410,9 +4135,9 @@
3410 4135
3411 4136 return trim($content);
3412 4137 }
3413 4138
3414 -// Modified search function with correct filter syntax
4139 +
3415 4140 private function find_relevant_products_pinecone($user_embedding) {
3416 4141 //error_log('Starting Pinecone product search...');
3417 4142
3418 4143 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -3509,321 +4234,1169 @@
3509 4234
3510 4235 return null;
3511 4236 }
3512 4237
3513 -// Function definition
3514 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
4238 +/**
4239 + * Get system instructions for a specific bot or default
4240 + * Checks for multi-bot add-on and uses bot-specific instructions if available
4241 + */
4242 +private function get_system_instructions($bot_id = 'default') {
4243 + // Check if multi-bot add-on is active
4244 + if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
4245 + // Get bot-specific options from multi-bot add-on
4246 + $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4247 +
4248 + // If bot has custom system instructions, use those
4249 + if (!empty($bot_options['system_prompt_instructions'])) {
4250 + return $bot_options['system_prompt_instructions'];
4251 + }
4252 + }
4253 +
4254 + // Fall back to default system instructions
4255 + return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4256 +}
4257 +/**
4258 + * Get the current bot ID from session or request context
4259 + */
4260 +private function get_current_bot_id($session_id = '') {
4261 + // First, check if bot_id is passed in the current request
4262 + if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
4263 + return sanitize_key($_POST['bot_id']);
4264 + }
4265 +
4266 + // If not in POST, try to get it from session data
4267 + if (!empty($session_id)) {
4268 + $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
4269 + if (!empty($bot_id)) {
4270 + return $bot_id;
4271 + }
4272 + }
4273 +
4274 + // Fall back to default
4275 + return 'default';
4276 +}
4277 +private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null, $selected_model = 'gpt-4o') {
3515 4278 try {
3516 4279 if (!$relevant_content) {
3517 - return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
4280 + $error_response = [
4281 + 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
4282 + 'error_code' => 'no_relevant_content'
4283 + ];
4284 +
4285 + // Add testing data to error response if available
4286 + if ($testing_data !== null) {
4287 + $error_response['testing_data'] = $testing_data;
4288 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
4289 + }
4290 +
4291 + return $error_response;
3518 4292 }
3519 -
4293 +
3520 4294 // Ensure conversation_history is an array
3521 4295 if (!is_array($conversation_history)) {
3522 4296 $conversation_history = array();
3523 4297 }
4298 +
3524 4299
3525 - // Get selected model with default fallback
3526 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3527 -
3528 4300 // Extract model prefix to determine the provider
3529 4301 $model_parts = explode('-', $selected_model);
3530 4302 $provider = strtolower($model_parts[0]);
3531 -
4303 +
3532 4304 // Handle model selection based on provider prefix
3533 4305 switch ($provider) {
3534 - case 'claude':
3535 - if (empty($claude_api_key)) {
3536 - throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
4306 + case 'gemini':
4307 + if (empty($gemini_api_key)) {
4308 + $error_response = [
4309 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
4310 + 'error_code' => 'missing_gemini_api_key'
4311 + ];
4312 + if ($testing_data !== null) {
4313 + $error_response['testing_data'] = $testing_data;
4314 + }
4315 + return $error_response;
3537 4316 }
3538 - return $this->mxchat_generate_response_claude(
4317 + $response = $this->mxchat_generate_response_gemini(
3539 4318 $selected_model,
3540 - $claude_api_key,
4319 + $gemini_api_key,
3541 4320 $conversation_history,
3542 4321 $relevant_content
3543 4322 );
3544 -
4323 + break;
4324 +
4325 + case 'claude':
4326 + if (empty($claude_api_key)) {
4327 + $error_response = [
4328 + 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
4329 + 'error_code' => 'missing_claude_api_key'
4330 + ];
4331 + if ($testing_data !== null) {
4332 + $error_response['testing_data'] = $testing_data;
4333 + }
4334 + return $error_response;
4335 + }
4336 + if ($streaming) {
4337 + return $this->mxchat_generate_response_claude_stream(
4338 + $selected_model,
4339 + $claude_api_key,
4340 + $conversation_history,
4341 + $relevant_content,
4342 + $session_id,
4343 + $testing_data // Pass testing data
4344 + );
4345 + } else {
4346 + $response = $this->mxchat_generate_response_claude(
4347 + $selected_model,
4348 + $claude_api_key,
4349 + $conversation_history,
4350 + $relevant_content
4351 + );
4352 + }
4353 + break;
4354 +
3545 4355 case 'grok':
3546 4356 if (empty($xai_api_key)) {
3547 - throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
4357 + $error_response = [
4358 + 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
4359 + 'error_code' => 'missing_xai_api_key'
4360 + ];
4361 + if ($testing_data !== null) {
4362 + $error_response['testing_data'] = $testing_data;
4363 + }
4364 + return $error_response;
3548 4365 }
3549 - return $this->mxchat_generate_response_xai(
3550 - $selected_model,
3551 - $xai_api_key,
3552 - $conversation_history,
3553 - $relevant_content
3554 - );
3555 -
4366 + if ($streaming) {
4367 + return $this->mxchat_generate_response_xai_stream(
4368 + $selected_model,
4369 + $xai_api_key,
4370 + $conversation_history,
4371 + $relevant_content,
4372 + $session_id,
4373 + $testing_data // Pass testing data
4374 + );
4375 + } else {
4376 + $response = $this->mxchat_generate_response_xai(
4377 + $selected_model,
4378 + $xai_api_key,
4379 + $conversation_history,
4380 + $relevant_content
4381 + );
4382 + }
4383 + break;
4384 +
3556 4385 case 'deepseek':
3557 4386 if (empty($deepseek_api_key)) {
3558 - throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
4387 + $error_response = [
4388 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
4389 + 'error_code' => 'missing_deepseek_api_key'
4390 + ];
4391 + if ($testing_data !== null) {
4392 + $error_response['testing_data'] = $testing_data;
4393 + }
4394 + return $error_response;
3559 4395 }
3560 - return $this->mxchat_generate_response_deepseek(
3561 - $selected_model,
3562 - $deepseek_api_key,
3563 - $conversation_history,
3564 - $relevant_content
3565 - );
3566 -
4396 + if ($streaming) {
4397 + return $this->mxchat_generate_response_deepseek_stream(
4398 + $selected_model,
4399 + $deepseek_api_key,
4400 + $conversation_history,
4401 + $relevant_content,
4402 + $session_id,
4403 + $testing_data // Pass testing data
4404 + );
4405 + } else {
4406 + $response = $this->mxchat_generate_response_deepseek(
4407 + $selected_model,
4408 + $deepseek_api_key,
4409 + $conversation_history,
4410 + $relevant_content
4411 + );
4412 + }
4413 + break;
4414 +
3567 4415 case 'gpt':
4416 + case 'o1':
3568 4417 if (empty($api_key)) {
3569 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
4418 + $error_response = [
4419 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
4420 + 'error_code' => 'missing_openai_api_key'
4421 + ];
4422 + if ($testing_data !== null) {
4423 + $error_response['testing_data'] = $testing_data;
4424 + }
4425 + return $error_response;
3570 4426 }
3571 - return $this->mxchat_generate_response_openai(
3572 - $selected_model,
3573 - $api_key,
3574 - $conversation_history,
3575 - $relevant_content
3576 - );
3577 -
4427 + if ($streaming) {
4428 + return $this->mxchat_generate_response_openai_stream(
4429 + $selected_model,
4430 + $api_key,
4431 + $conversation_history,
4432 + $relevant_content,
4433 + $session_id,
4434 + $testing_data // Pass testing data
4435 + );
4436 + } else {
4437 + $response = $this->mxchat_generate_response_openai(
4438 + $selected_model,
4439 + $api_key,
4440 + $conversation_history,
4441 + $relevant_content
4442 + );
4443 + }
4444 + break;
4445 +
3578 4446 default:
3579 4447 // Default to OpenAI for custom models or unrecognized prefixes
3580 4448 if (empty($api_key)) {
3581 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
4449 + $error_response = [
4450 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
4451 + 'error_code' => 'missing_openai_api_key'
4452 + ];
4453 + if ($testing_data !== null) {
4454 + $error_response['testing_data'] = $testing_data;
4455 + }
4456 + return $error_response;
3582 4457 }
3583 - return $this->mxchat_generate_response_openai(
3584 - $selected_model,
3585 - $api_key,
3586 - $conversation_history,
3587 - $relevant_content
3588 - );
4458 + if ($streaming) {
4459 + return $this->mxchat_generate_response_openai_stream(
4460 + $selected_model,
4461 + $api_key,
4462 + $conversation_history,
4463 + $relevant_content,
4464 + $session_id,
4465 + $testing_data // Pass testing data
4466 + );
4467 + } else {
4468 + $response = $this->mxchat_generate_response_openai(
4469 + $selected_model,
4470 + $api_key,
4471 + $conversation_history,
4472 + $relevant_content
4473 + );
4474 + }
4475 + break;
3589 4476 }
4477 +
4478 + // Check if the response is an error array from the provider-specific function
4479 + if (is_array($response) && isset($response['error'])) {
4480 + // Add testing data to error response if available
4481 + if ($testing_data !== null) {
4482 + $response['testing_data'] = $testing_data;
4483 + //error_log("MxChat Testing: Added testing data to provider error response");
4484 + }
4485 + return $response; // Pass through the error with testing data
4486 + }
4487 +
4488 + // For successful non-streaming responses, we don't add testing data here
4489 + // because it will be added in the main handler
4490 + return $response;
4491 +
3590 4492 } catch (Exception $e) {
3591 4493 //error_log('MXChat Error: ' . $e->getMessage());
3592 - return sprintf(
3593 - esc_html__('An error occurred: %s', 'mxchat'),
3594 - esc_html($e->getMessage())
3595 - );
4494 + $error_response = [
4495 + 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
4496 + 'error_code' => 'system_exception',
4497 + 'exception_details' => $e->getMessage()
4498 + ];
4499 +
4500 + // Add testing data to exception response if available
4501 + if ($testing_data !== null) {
4502 + $error_response['testing_data'] = $testing_data;
4503 + //error_log("MxChat Testing: Added testing data to exception response");
4504 + }
4505 +
4506 + return $error_response;
3596 4507 }
3597 4508 }
3598 4509
4510 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4511 + try {
4512 + $bot_id = $this->get_current_bot_id($session_id);
4513 +
4514 + // Get system prompt instructions using centralized function
4515 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
4516 +
4517 + // Ensure conversation_history is an array
4518 + if (!is_array($conversation_history)) {
4519 + $conversation_history = array();
4520 + }
3599 4521
3600 -private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3601 - // Ensure conversation_history is an array
3602 - if (!is_array($conversation_history)) {
3603 - $conversation_history = array();
3604 - }
4522 + // Format conversation history for OpenAI
4523 + $formatted_conversation = array();
3605 4524
3606 - // Get system prompt instructions from options
3607 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4525 + $formatted_conversation[] = array(
4526 + 'role' => 'system',
4527 + 'content' => $system_prompt_instructions . " " . $relevant_content
4528 + );
3608 4529
3609 - // Create a new array for the formatted conversation
3610 - $formatted_conversation = array();
4530 + foreach ($conversation_history as $message) {
4531 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4532 + $role = $message['role'];
4533 + if ($role === 'bot' || $role === 'agent') {
4534 + $role = 'assistant';
4535 + }
4536 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4537 + $role = 'user';
4538 + }
4539 + $formatted_conversation[] = array(
4540 + 'role' => $role,
4541 + 'content' => $message['content']
4542 + );
4543 + }
4544 + }
3611 4545
3612 - // Add system message first
3613 - $formatted_conversation[] = array(
3614 - 'role' => 'system',
3615 - 'content' => $system_prompt_instructions . " " . $relevant_content
3616 - );
4546 + // Check if we can actually stream
4547 + if (headers_sent() || !function_exists('curl_init')) {
4548 + // Fallback to regular response with testing data
4549 + $regular_response = $this->mxchat_generate_response_openai(
4550 + $selected_model,
4551 + $api_key,
4552 + $conversation_history,
4553 + $relevant_content
4554 + );
4555 +
4556 + $response_data = [
4557 + 'text' => $regular_response,
4558 + 'html' => '',
4559 + 'session_id' => $session_id
4560 + ];
4561 +
4562 + if ($testing_data !== null) {
4563 + $response_data['testing_data'] = $testing_data;
4564 + }
4565 +
4566 + header('Content-Type: application/json');
4567 + echo json_encode($response_data);
4568 + return true;
4569 + }
3617 4570
3618 - // Add the rest of the conversation history
3619 - foreach ($conversation_history as $message) {
3620 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3621 - $role = $message['role'];
4571 + // Prepare the request body with stream: true
4572 + $body = json_encode([
4573 + 'model' => $selected_model,
4574 + 'messages' => $formatted_conversation,
4575 + 'temperature' => 1,
4576 + 'stream' => true
4577 + ]);
3622 4578
3623 - // Convert roles to supported format
3624 - if ($role === 'bot' || $role === 'agent') {
3625 - $role = 'assistant';
4579 + // Use cURL for streaming support
4580 + $ch = curl_init();
4581 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4582 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4583 + curl_setopt($ch, CURLOPT_POST, true);
4584 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4585 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4586 + 'Content-Type: application/json',
4587 + 'Authorization: Bearer ' . $api_key
4588 + ));
4589 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4590 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4591 +
4592 + $full_response = ''; // Accumulate full response for saving
4593 + $stream_started = false;
4594 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
4595 +
4596 + // Buffer control for real-time streaming
4597 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4598 + // Send testing data as the first event if available
4599 + if (!$stream_started && $testing_data !== null) {
4600 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4601 + flush();
4602 + $stream_started = true;
3626 4603 }
3627 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3628 - $role = 'user';
4604 +
4605 + // CRITICAL FIX: Append new data to buffer
4606 + $buffer .= $data;
4607 +
4608 + // Process complete lines only
4609 + $lines = explode("\n", $buffer);
4610 +
4611 + // CRITICAL FIX: Keep the last incomplete line in the buffer
4612 + // The last element might be incomplete, so keep it in buffer
4613 + $buffer = array_pop($lines);
4614 +
4615 + foreach ($lines as $line) {
4616 + // Skip empty lines
4617 + if (trim($line) === '') {
4618 + continue;
4619 + }
4620 +
4621 + // Only process lines that start with "data: "
4622 + if (strpos($line, 'data: ') !== 0) {
4623 + continue;
4624 + }
4625 +
4626 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4627 +
4628 + if (trim($json_str) === '[DONE]') {
4629 + echo "data: [DONE]\n\n";
4630 + flush();
4631 + continue;
4632 + }
4633 +
4634 + // Try to decode JSON
4635 + $json = json_decode(trim($json_str), true);
4636 + if ($json && isset($json['choices'][0]['delta']['content'])) {
4637 + $content = $json['choices'][0]['delta']['content'];
4638 + $full_response .= $content; // Accumulate the full response
4639 +
4640 + // Send as SSE format
4641 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4642 + flush();
4643 + }
3629 4644 }
3630 -
3631 - $formatted_conversation[] = array(
3632 - 'role' => $role,
3633 - 'content' => $message['content']
4645 +
4646 + return strlen($data);
4647 + });
4648 +
4649 + $response = curl_exec($ch);
4650 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4651 +
4652 + if (curl_errno($ch) || $http_code !== 200) {
4653 + curl_close($ch);
4654 +
4655 + // Fallback to regular response
4656 + $regular_response = $this->mxchat_generate_response_openai(
4657 + $selected_model,
4658 + $api_key,
4659 + $conversation_history,
4660 + $relevant_content
3634 4661 );
4662 +
4663 + $response_data = [
4664 + 'text' => $regular_response,
4665 + 'html' => '',
4666 + 'session_id' => $session_id
4667 + ];
4668 +
4669 + if ($testing_data !== null) {
4670 + $response_data['testing_data'] = $testing_data;
4671 + }
4672 +
4673 + header('Content-Type: application/json');
4674 + echo json_encode($response_data);
4675 + return true;
3635 4676 }
4677 +
4678 + curl_close($ch);
4679 +
4680 + // Save the complete response to maintain chat persistence
4681 + if (!empty($full_response) && !empty($session_id)) {
4682 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4683 + }
4684 +
4685 + return true; // Indicate streaming completed successfully
4686 +
4687 + } catch (Exception $e) {
4688 + // Fallback to regular response
4689 + $regular_response = $this->mxchat_generate_response_openai(
4690 + $selected_model,
4691 + $api_key,
4692 + $conversation_history,
4693 + $relevant_content
4694 + );
4695 +
4696 + $response_data = [
4697 + 'text' => $regular_response,
4698 + 'html' => '',
4699 + 'session_id' => $session_id
4700 + ];
4701 +
4702 + if ($testing_data !== null) {
4703 + $response_data['testing_data'] = $testing_data;
4704 + }
4705 +
4706 + header('Content-Type: application/json');
4707 + echo json_encode($response_data);
4708 + return true;
3636 4709 }
4710 +}
4711 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4712 + try {
4713 + // Get bot ID from session or request
4714 + $bot_id = $this->get_current_bot_id($session_id);
4715 +
4716 + // Get system prompt instructions using centralized function
4717 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
4718 + // Ensure conversation_history is an array
4719 + if (!is_array($conversation_history)) {
4720 + $conversation_history = array();
4721 + }
3637 4722
3638 - $body = json_encode([
3639 - 'model' => $selected_model,
3640 - 'messages' => $formatted_conversation,
3641 - 'temperature' => 0.8,
3642 - 'stream' => false
3643 - ]);
4723 + // Clean and validate conversation history
4724 + foreach ($conversation_history as &$message) {
4725 + // Convert bot and agent roles to assistant
4726 + if ($message['role'] === 'bot' || $message['role'] === 'agent') {
4727 + $message['role'] = 'assistant';
4728 + }
4729 +
4730 + // Remove unsupported roles - Claude only supports 'assistant' and 'user'
4731 + if (!in_array($message['role'], ['assistant', 'user'])) {
4732 + $message['role'] = 'user';
4733 + }
3644 4734
3645 - $args = [
3646 - 'body' => $body,
3647 - 'headers' => [
3648 - 'Content-Type' => 'application/json',
3649 - 'Authorization' => 'Bearer ' . $deepseek_api_key,
3650 - ],
3651 - 'timeout' => 60,
3652 - 'redirection' => 5,
3653 - 'blocking' => true,
3654 - 'httpversion' => '1.0',
3655 - 'sslverify' => true,
3656 - ];
4735 + // Ensure content field exists
4736 + if (!isset($message['content']) || empty($message['content'])) {
4737 + $message['content'] = '';
4738 + }
3657 4739
3658 - $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
4740 + // Remove any unsupported fields
4741 + $message = array_intersect_key($message, array_flip(['role', 'content']));
4742 + }
3659 4743
3660 - if (is_wp_error($response)) {
3661 - //error_log('DeepSeek API Error: ' . $response->get_error_message());
3662 - return "Sorry, there was an error processing your request.";
3663 - }
4744 + // Add relevant content as the latest user message
4745 + $conversation_history[] = [
4746 + 'role' => 'user',
4747 + 'content' => $relevant_content
4748 + ];
3664 4749
3665 - $response_body = wp_remote_retrieve_body($response);
3666 - $decoded_response = json_decode($response_body, true);
4750 + // Prepare the request body with stream: true
4751 + $body = json_encode([
4752 + 'model' => $selected_model,
4753 + 'messages' => $conversation_history,
4754 + 'max_tokens' => 1000,
4755 + 'temperature' => 0.8,
4756 + 'system' => $system_prompt_instructions,
4757 + 'stream' => true
4758 + ]);
3667 4759
3668 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3669 - return trim($decoded_response['choices'][0]['message']['content']);
3670 - } else {
3671 - //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3672 - return "Sorry, I couldn't process that request.";
3673 - }
3674 -}
4760 + // Check if we can actually stream (headers not sent, etc.)
4761 + if (headers_sent() || !function_exists('curl_init')) {
4762 + // Fallback to regular response with testing data
4763 + //error_log("MxChat: Streaming not possible, falling back to regular response");
4764 + $regular_response = $this->mxchat_generate_response_claude(
4765 + $selected_model,
4766 + $claude_api_key,
4767 + array_slice($conversation_history, 0, -1), // Remove the added content
4768 + $relevant_content
4769 + );
4770 +
4771 + // Return as JSON with testing data
4772 + $response_data = [
4773 + 'text' => $regular_response,
4774 + 'html' => '',
4775 + 'session_id' => $session_id
4776 + ];
4777 +
4778 + if ($testing_data !== null) {
4779 + $response_data['testing_data'] = $testing_data;
4780 + //error_log("MxChat Testing: Added testing data to Claude fallback response");
4781 + }
4782 +
4783 + // Clear any streaming headers and send JSON
4784 + if (headers_sent() === false) {
4785 + header('Content-Type: application/json');
4786 + }
4787 + echo json_encode($response_data);
4788 + return true; // Indicate we handled the response
4789 + }
3675 4790
3676 -private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3677 - // Ensure conversation_history is an array
3678 - if (!is_array($conversation_history)) {
3679 - $conversation_history = array();
3680 - }
4791 + // Use cURL for streaming support
4792 + $ch = curl_init();
4793 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
4794 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4795 + curl_setopt($ch, CURLOPT_POST, true);
4796 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4797 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4798 + 'Content-Type: application/json',
4799 + 'x-api-key: ' . $claude_api_key,
4800 + 'anthropic-version: 2023-06-01'
4801 + ));
4802 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4803 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
3681 4804
3682 - // Get system prompt instructions from options
3683 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4805 + $full_response = ''; // Accumulate full response for saving
4806 + $stream_started = false;
4807 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
3684 4808
3685 - // Create a new array for the formatted conversation
3686 - $formatted_conversation = array();
4809 + // Buffer control for real-time streaming
4810 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
4811 + // Send testing data as the first event if available
4812 + if (!$stream_started && $testing_data !== null) {
4813 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4814 + flush();
4815 + $stream_started = true;
4816 + //error_log("MxChat Testing: Sent testing data in Claude stream");
4817 + }
4818 +
4819 + // CRITICAL FIX: Append new data to buffer
4820 + $buffer .= $data;
4821 +
4822 + // Process complete lines only
4823 + $lines = explode("\n", $buffer);
4824 +
4825 + // CRITICAL FIX: Keep the last incomplete line in the buffer
4826 + // The last element might be incomplete, so keep it in buffer
4827 + $buffer = array_pop($lines);
3687 4828
3688 - // Add system message first
3689 - $formatted_conversation[] = array(
3690 - 'role' => 'system',
3691 - 'content' => $system_prompt_instructions . " " . $relevant_content
3692 - );
4829 + foreach ($lines as $line) {
4830 + if (trim($line) === '') {
4831 + continue;
4832 + }
3693 4833
3694 - // Add the rest of the conversation history
3695 - foreach ($conversation_history as $message) {
3696 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3697 - $role = $message['role'];
4834 + // Claude uses event: and data: format
4835 + if (strpos($line, 'event: ') === 0) {
4836 + // Store the event type for the next data line
4837 + continue;
4838 + }
3698 4839
3699 - // Convert roles to supported format
3700 - if ($role === 'bot' || $role === 'agent') {
3701 - $role = 'assistant';
4840 + if (strpos($line, 'data: ') === 0) {
4841 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4842 +
4843 + $json = json_decode(trim($json_str), true);
4844 + if (json_last_error() !== JSON_ERROR_NONE) {
4845 + continue;
4846 + }
4847 +
4848 + // Handle different event types
4849 + if (isset($json['type'])) {
4850 + switch ($json['type']) {
4851 + case 'content_block_delta':
4852 + if (isset($json['delta']['text'])) {
4853 + $content = $json['delta']['text'];
4854 + $full_response .= $content; // Accumulate
4855 + // Send as SSE format compatible with your frontend
4856 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4857 + flush();
4858 + }
4859 + break;
4860 +
4861 + case 'message_stop':
4862 + echo "data: [DONE]\n\n";
4863 + flush();
4864 + break;
4865 +
4866 + case 'error':
4867 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
4868 + flush();
4869 + break;
4870 + }
4871 + }
4872 + }
3702 4873 }
3703 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3704 - $role = 'user';
3705 - }
3706 4874
3707 - $formatted_conversation[] = array(
3708 - 'role' => $role,
3709 - 'content' => $message['content']
3710 - );
4875 + return strlen($data);
4876 + });
4877 +
4878 + $response = curl_exec($ch);
4879 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4880 +
4881 + if (curl_errno($ch)) {
4882 + curl_close($ch);
4883 + throw new Exception('cURL Error: ' . curl_error($ch));
3711 4884 }
3712 - }
3713 4885
3714 - $body = json_encode([
3715 - 'model' => $selected_model,
3716 - 'messages' => $formatted_conversation,
3717 - 'temperature' => 0.8,
3718 - 'stream' => false
3719 - ]);
4886 + curl_close($ch);
3720 4887
3721 - $args = [
3722 - 'body' => $body,
3723 - 'headers' => [
3724 - 'Content-Type' => 'application/json',
3725 - 'Authorization' => 'Bearer ' . $api_key,
3726 - ],
3727 - 'timeout' => 60,
3728 - 'redirection' => 5,
3729 - 'blocking' => true,
3730 - 'httpversion' => '1.0',
3731 - 'sslverify' => true,
3732 - ];
4888 + if ($http_code !== 200) {
4889 + // Fallback to regular response
4890 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
4891 + $regular_response = $this->mxchat_generate_response_claude(
4892 + $selected_model,
4893 + $claude_api_key,
4894 + array_slice($conversation_history, 0, -1), // Remove the added content
4895 + $relevant_content
4896 + );
4897 +
4898 + $response_data = [
4899 + 'text' => $regular_response,
4900 + 'html' => '',
4901 + 'session_id' => $session_id
4902 + ];
4903 +
4904 + if ($testing_data !== null) {
4905 + $response_data['testing_data'] = $testing_data;
4906 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
4907 + }
4908 +
4909 + header('Content-Type: application/json');
4910 + echo json_encode($response_data);
4911 + return true;
4912 + }
3733 4913
3734 - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4914 + // Save the complete response to maintain chat persistence
4915 + if (!empty($full_response) && !empty($session_id)) {
4916 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4917 + }
3735 4918
3736 - if (is_wp_error($response)) {
3737 - //error_log('OpenAI API Error: ' . $response->get_error_message());
3738 - return "Sorry, there was an error processing your request.";
3739 - }
4919 + return true; // Indicate streaming completed successfully
3740 4920
3741 - $response_body = wp_remote_retrieve_body($response);
3742 - $decoded_response = json_decode($response_body, true);
3743 -
3744 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3745 - return trim($decoded_response['choices'][0]['message']['content']);
3746 - } else {
3747 - //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3748 - return "Sorry, I couldn't process that request.";
4921 + } catch (Exception $e) {
4922 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
4923 +
4924 + // Fallback to regular response on exception
4925 + $regular_response = $this->mxchat_generate_response_claude(
4926 + $selected_model,
4927 + $claude_api_key,
4928 + $conversation_history,
4929 + $relevant_content
4930 + );
4931 +
4932 + $response_data = [
4933 + 'text' => $regular_response,
4934 + 'html' => '',
4935 + 'session_id' => $session_id
4936 + ];
4937 +
4938 + if ($testing_data !== null) {
4939 + $response_data['testing_data'] = $testing_data;
4940 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
4941 + }
4942 +
4943 + header('Content-Type: application/json');
4944 + echo json_encode($response_data);
4945 + return true;
3749 4946 }
3750 4947 }
3751 -private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3752 - // Get system prompt instructions from options
3753 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4948 +private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4949 + try {
4950 + // Get bot ID from session or request
4951 + $bot_id = $this->get_current_bot_id($session_id);
4952 +
4953 + // Get system prompt instructions using centralized function
4954 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
4955 +
4956 + // Ensure conversation_history is an array
4957 + if (!is_array($conversation_history)) {
4958 + $conversation_history = array();
4959 + }
3754 4960
3755 - // Add system prompt to relevant content
3756 - $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4961 + // Format conversation history for X.AI (same as OpenAI format)
4962 + $formatted_conversation = array();
3757 4963
3758 - // Prepend system instructions to the conversation history
3759 - array_unshift($conversation_history, [
3760 - 'role' => 'system',
3761 - 'content' => "Here are your instructions: " . $content_with_instructions
3762 - ]);
4964 + $formatted_conversation[] = array(
4965 + 'role' => 'system',
4966 + 'content' => $system_prompt_instructions . " " . $relevant_content
4967 + );
3763 4968
3764 - // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3765 - foreach ($conversation_history as &$message) {
3766 - if ($message['role'] === 'bot') {
3767 - $message['role'] = 'assistant';
3768 - } elseif ($message['role'] === 'agent') {
3769 - // Tag the message as coming from a live agent
3770 - $message['role'] = 'assistant';
3771 - if (!isset($message['metadata'])) {
3772 - $message['metadata'] = ['source' => 'live_agent'];
4969 + foreach ($conversation_history as $message) {
4970 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4971 + $role = $message['role'];
4972 + if ($role === 'bot' || $role === 'agent') {
4973 + $role = 'assistant';
4974 + }
4975 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4976 + $role = 'user';
4977 + }
4978 + $formatted_conversation[] = array(
4979 + 'role' => $role,
4980 + 'content' => $message['content']
4981 + );
3773 4982 }
3774 4983 }
3775 4984
3776 - // Ensure all roles are valid
3777 - if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3778 - $message['role'] = 'user'; // Default to 'user'
4985 + // Check if we can actually stream
4986 + if (headers_sent() || !function_exists('curl_init')) {
4987 + // Fallback to regular response with testing data
4988 + //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
4989 + $regular_response = $this->mxchat_generate_response_xai(
4990 + $selected_model,
4991 + $xai_api_key,
4992 + $conversation_history,
4993 + $relevant_content
4994 + );
4995 +
4996 + $response_data = [
4997 + 'text' => $regular_response,
4998 + 'html' => '',
4999 + 'session_id' => $session_id
5000 + ];
5001 +
5002 + if ($testing_data !== null) {
5003 + $response_data['testing_data'] = $testing_data;
5004 + //error_log("MxChat Testing: Added testing data to X.AI fallback response");
5005 + }
5006 +
5007 + header('Content-Type: application/json');
5008 + echo json_encode($response_data);
5009 + return true;
3779 5010 }
5011 +
5012 + // Prepare the request body with stream: true
5013 + $body = json_encode([
5014 + 'model' => $selected_model,
5015 + 'messages' => $formatted_conversation,
5016 + 'temperature' => 0.8,
5017 + 'stream' => true
5018 + ]);
5019 +
5020 + // Use cURL for streaming support
5021 + $ch = curl_init();
5022 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
5023 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5024 + curl_setopt($ch, CURLOPT_POST, true);
5025 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5026 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5027 + 'Content-Type: application/json',
5028 + 'Authorization: Bearer ' . $xai_api_key
5029 + ));
5030 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5031 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5032 +
5033 + $full_response = ''; // Accumulate full response for saving
5034 + $stream_started = false;
5035 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
5036 +
5037 + // Buffer control for real-time streaming
5038 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5039 + // Send testing data as the first event if available
5040 + if (!$stream_started && $testing_data !== null) {
5041 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5042 + flush();
5043 + $stream_started = true;
5044 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
5045 + }
5046 +
5047 + // CRITICAL FIX: Append new data to buffer
5048 + $buffer .= $data;
5049 +
5050 + // Process complete lines only
5051 + $lines = explode("\n", $buffer);
5052 +
5053 + // CRITICAL FIX: Keep the last incomplete line in the buffer
5054 + // The last element might be incomplete, so keep it in buffer
5055 + $buffer = array_pop($lines);
5056 +
5057 + foreach ($lines as $line) {
5058 + // Skip empty lines
5059 + if (trim($line) === '') {
5060 + continue;
5061 + }
5062 +
5063 + // Only process lines that start with "data: "
5064 + if (strpos($line, 'data: ') !== 0) {
5065 + continue;
5066 + }
5067 +
5068 + $json_str = substr($line, 6); // Remove 'data: ' prefix
5069 +
5070 + if (trim($json_str) === '[DONE]') {
5071 + echo "data: [DONE]\n\n";
5072 + flush();
5073 + continue;
5074 + }
5075 +
5076 + // Try to decode JSON
5077 + $json = json_decode(trim($json_str), true);
5078 + if ($json && isset($json['choices'][0]['delta']['content'])) {
5079 + $content = $json['choices'][0]['delta']['content'];
5080 + $full_response .= $content; // Accumulate
5081 + // Send as SSE format
5082 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
5083 + flush();
5084 + }
5085 + }
5086 +
5087 + return strlen($data);
5088 + });
5089 +
5090 + $response = curl_exec($ch);
5091 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5092 +
5093 + if (curl_errno($ch) || $http_code !== 200) {
5094 + curl_close($ch);
5095 +
5096 + // Fallback to regular response
5097 + //error_log("MxChat: X.AI streaming failed, falling back");
5098 + $regular_response = $this->mxchat_generate_response_xai(
5099 + $selected_model,
5100 + $xai_api_key,
5101 + $conversation_history,
5102 + $relevant_content
5103 + );
5104 +
5105 + $response_data = [
5106 + 'text' => $regular_response,
5107 + 'html' => '',
5108 + 'session_id' => $session_id
5109 + ];
5110 +
5111 + if ($testing_data !== null) {
5112 + $response_data['testing_data'] = $testing_data;
5113 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
5114 + }
5115 +
5116 + header('Content-Type: application/json');
5117 + echo json_encode($response_data);
5118 + return true;
5119 + }
5120 +
5121 + curl_close($ch);
5122 +
5123 + // Save the complete response to maintain chat persistence
5124 + if (!empty($full_response) && !empty($session_id)) {
5125 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
5126 + }
5127 +
5128 + return true; // Indicate streaming completed successfully
5129 +
5130 + } catch (Exception $e) {
5131 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
5132 +
5133 + // Fallback to regular response
5134 + $regular_response = $this->mxchat_generate_response_xai(
5135 + $selected_model,
5136 + $xai_api_key,
5137 + $conversation_history,
5138 + $relevant_content
5139 + );
5140 +
5141 + $response_data = [
5142 + 'text' => $regular_response,
5143 + 'html' => '',
5144 + 'session_id' => $session_id
5145 + ];
5146 +
5147 + if ($testing_data !== null) {
5148 + $response_data['testing_data'] = $testing_data;
5149 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
5150 + }
5151 +
5152 + header('Content-Type: application/json');
5153 + echo json_encode($response_data);
5154 + return true;
3780 5155 }
5156 +}
5157 +private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5158 + try {
5159 + // Get bot ID from session or request
5160 + $bot_id = $this->get_current_bot_id($session_id);
5161 +
5162 + // Get system prompt instructions using centralized function
5163 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5164 +
5165 + // Ensure conversation_history is an array
5166 + if (!is_array($conversation_history)) {
5167 + $conversation_history = array();
5168 + }
3781 5169
5170 + // Format conversation history for DeepSeek
5171 + $formatted_conversation = array();
3782 5172
3783 - // Build the request body
3784 - $body = json_encode([
3785 - 'model' => $selected_model,
3786 - 'messages' => $conversation_history,
3787 - 'temperature' => 0.8,
3788 - 'stream' => false
3789 - ]);
5173 + $formatted_conversation[] = array(
5174 + 'role' => 'system',
5175 + 'content' => $system_prompt_instructions . " " . $relevant_content
5176 + );
3790 5177
3791 - // Set up the API request
3792 - $args = [
3793 - 'body' => $body,
3794 - 'headers' => [
3795 - 'Content-Type' => 'application/json',
3796 - 'Authorization' => 'Bearer ' . $xai_api_key,
3797 - ],
3798 - 'timeout' => 60,
3799 - 'redirection' => 5,
3800 - 'blocking' => true,
3801 - 'httpversion' => '1.0',
3802 - 'sslverify' => true,
3803 - ];
5178 + foreach ($conversation_history as $message) {
5179 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5180 + $role = $message['role'];
5181 + if ($role === 'bot' || $role === 'agent') {
5182 + $role = 'assistant';
5183 + }
5184 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5185 + $role = 'user';
5186 + }
5187 + $formatted_conversation[] = array(
5188 + 'role' => $role,
5189 + 'content' => $message['content']
5190 + );
5191 + }
5192 + }
3804 5193
3805 - // Make the API request
3806 - $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
5194 + // Check if we can actually stream
5195 + if (headers_sent() || !function_exists('curl_init')) {
5196 + // Fallback to regular response with testing data
5197 + //error_log("MxChat: DeepSeek streaming not possible, falling back to regular response");
5198 + $regular_response = $this->mxchat_generate_response_deepseek(
5199 + $selected_model,
5200 + $deepseek_api_key,
5201 + $conversation_history,
5202 + $relevant_content
5203 + );
5204 +
5205 + $response_data = [
5206 + 'text' => $regular_response,
5207 + 'html' => '',
5208 + 'session_id' => $session_id
5209 + ];
5210 +
5211 + if ($testing_data !== null) {
5212 + $response_data['testing_data'] = $testing_data;
5213 + //error_log("MxChat Testing: Added testing data to DeepSeek fallback response");
5214 + }
5215 +
5216 + header('Content-Type: application/json');
5217 + echo json_encode($response_data);
5218 + return true;
5219 + }
3807 5220
3808 - // Process the response
3809 - if (is_wp_error($response)) {
3810 - return "Sorry, there was an error processing your request.";
3811 - }
5221 + // Prepare the request body with stream: true
5222 + $body = json_encode([
5223 + 'model' => $selected_model,
5224 + 'messages' => $formatted_conversation,
5225 + 'temperature' => 0.8,
5226 + 'stream' => true
5227 + ]);
3812 5228
3813 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
3814 -
3815 - if (isset($response_body['choices'][0]['message']['content'])) {
3816 - return trim($response_body['choices'][0]['message']['content']);
3817 - } else {
3818 - return "Sorry, I couldn't process that request.";
5229 + // Use cURL for streaming support
5230 + $ch = curl_init();
5231 + curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
5232 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5233 + curl_setopt($ch, CURLOPT_POST, true);
5234 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5235 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5236 + 'Content-Type: application/json',
5237 + 'Authorization: Bearer ' . $deepseek_api_key
5238 + ));
5239 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5240 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5241 +
5242 + $full_response = ''; // Accumulate full response for saving
5243 + $stream_started = false;
5244 + $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
5245 +
5246 + // Buffer control for real-time streaming
5247 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5248 + // Send testing data as the first event if available
5249 + if (!$stream_started && $testing_data !== null) {
5250 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5251 + flush();
5252 + $stream_started = true;
5253 + //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
5254 + }
5255 +
5256 + // CRITICAL FIX: Append new data to buffer
5257 + $buffer .= $data;
5258 +
5259 + // Process complete lines only
5260 + $lines = explode("\n", $buffer);
5261 +
5262 + // CRITICAL FIX: Keep the last incomplete line in the buffer
5263 + // The last element might be incomplete, so keep it in buffer
5264 + $buffer = array_pop($lines);
5265 +
5266 + foreach ($lines as $line) {
5267 + // Skip empty lines
5268 + if (trim($line) === '') {
5269 + continue;
5270 + }
5271 +
5272 + // Only process lines that start with "data: "
5273 + if (strpos($line, 'data: ') !== 0) {
5274 + continue;
5275 + }
5276 +
5277 + $json_str = substr($line, 6); // Remove 'data: ' prefix
5278 +
5279 + if (trim($json_str) === '[DONE]') {
5280 + echo "data: [DONE]\n\n";
5281 + flush();
5282 + continue;
5283 + }
5284 +
5285 + // Try to decode JSON
5286 + $json = json_decode(trim($json_str), true);
5287 + if ($json && isset($json['choices'][0]['delta']['content'])) {
5288 + $content = $json['choices'][0]['delta']['content'];
5289 + $full_response .= $content; // Accumulate the full response
5290 +
5291 + // Send as SSE format
5292 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
5293 + flush();
5294 + }
5295 + }
5296 +
5297 + return strlen($data);
5298 + });
5299 +
5300 + $response = curl_exec($ch);
5301 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5302 +
5303 + if (curl_errno($ch) || $http_code !== 200) {
5304 + $curl_error = curl_error($ch);
5305 + curl_close($ch);
5306 +
5307 + // Log the specific error for debugging
5308 + //error_log("MxChat: DeepSeek streaming failed - HTTP: $http_code, cURL: $curl_error");
5309 +
5310 + // Fallback to regular response
5311 + $regular_response = $this->mxchat_generate_response_deepseek(
5312 + $selected_model,
5313 + $deepseek_api_key,
5314 + $conversation_history,
5315 + $relevant_content
5316 + );
5317 +
5318 + // Handle error response from regular function
5319 + if (is_array($regular_response) && isset($regular_response['error'])) {
5320 + if ($testing_data !== null) {
5321 + $regular_response['testing_data'] = $testing_data;
5322 + }
5323 + header('Content-Type: application/json');
5324 + echo json_encode($regular_response);
5325 + return true;
5326 + }
5327 +
5328 + $response_data = [
5329 + 'text' => $regular_response,
5330 + 'html' => '',
5331 + 'session_id' => $session_id
5332 + ];
5333 +
5334 + if ($testing_data !== null) {
5335 + $response_data['testing_data'] = $testing_data;
5336 + //error_log("MxChat Testing: Added testing data to DeepSeek error fallback");
5337 + }
5338 +
5339 + header('Content-Type: application/json');
5340 + echo json_encode($response_data);
5341 + return true;
5342 + }
5343 +
5344 + curl_close($ch);
5345 +
5346 + // Save the complete response to maintain chat persistence
5347 + if (!empty($full_response) && !empty($session_id)) {
5348 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
5349 + }
5350 +
5351 + return true; // Indicate streaming completed successfully
5352 +
5353 + } catch (Exception $e) {
5354 + //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
5355 +
5356 + // Fallback to regular response
5357 + $regular_response = $this->mxchat_generate_response_deepseek(
5358 + $selected_model,
5359 + $deepseek_api_key,
5360 + $conversation_history,
5361 + $relevant_content
5362 + );
5363 +
5364 + // Handle error response from regular function
5365 + if (is_array($regular_response) && isset($regular_response['error'])) {
5366 + if ($testing_data !== null) {
5367 + $regular_response['testing_data'] = $testing_data;
5368 + }
5369 + header('Content-Type: application/json');
5370 + echo json_encode($regular_response);
5371 + return true;
5372 + }
5373 +
5374 + $response_data = [
5375 + 'text' => $regular_response,
5376 + 'html' => '',
5377 + 'session_id' => $session_id
5378 + ];
5379 +
5380 + if ($testing_data !== null) {
5381 + $response_data['testing_data'] = $testing_data;
5382 + //error_log("MxChat Testing: Added testing data to DeepSeek exception fallback");
5383 + }
5384 +
5385 + header('Content-Type: application/json');
5386 + echo json_encode($response_data);
5387 + return true;
3819 5388 }
3820 5389 }
3821 5390
3822 5391 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3823 - // Get system prompt instructions from options
3824 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3825 -
5392 +
5393 + // Get bot ID from session or request
5394 + $bot_id = $this->get_current_bot_id($session_id);
5395 +
5396 + // Get system prompt instructions using centralized function
5397 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5398 +
3826 5399 // Clean and validate conversation history
3827 5400 foreach ($conversation_history as &$message) {
3828 5401 // Convert bot and agent roles to assistant
3829 5402 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -3918,8 +5491,796 @@
3918 5491 // Log unexpected response format
3919 5492 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3920 5493 return "Sorry, I received an unexpected response format from the API.";
3921 5494 }
5495 +private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
5496 + try {
5497 + // Ensure conversation_history is an array
5498 + if (!is_array($conversation_history)) {
5499 + $conversation_history = array();
5500 + }
5501 +
5502 + // Get bot ID from session or request
5503 + $bot_id = $this->get_current_bot_id($session_id);
5504 +
5505 + // Get system prompt instructions using centralized function
5506 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5507 +
5508 + // Create a new array for the formatted conversation
5509 + $formatted_conversation = array();
5510 +
5511 + // Add system message first
5512 + $formatted_conversation[] = array(
5513 + 'role' => 'system',
5514 + 'content' => $system_prompt_instructions . " " . $relevant_content
5515 + );
5516 +
5517 + // Add the rest of the conversation history
5518 + foreach ($conversation_history as $message) {
5519 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5520 + $role = $message['role'];
5521 +
5522 + // Convert roles to supported format
5523 + if ($role === 'bot' || $role === 'agent') {
5524 + $role = 'assistant';
5525 + }
5526 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5527 + $role = 'user';
5528 + }
5529 +
5530 + $formatted_conversation[] = array(
5531 + 'role' => $role,
5532 + 'content' => $message['content']
5533 + );
5534 + }
5535 + }
5536 +
5537 + $body = json_encode([
5538 + 'model' => $selected_model,
5539 + 'messages' => $formatted_conversation,
5540 + 'temperature' => 1,
5541 + 'stream' => false
5542 + ]);
5543 +
5544 + $args = [
5545 + 'body' => $body,
5546 + 'headers' => [
5547 + 'Content-Type' => 'application/json',
5548 + 'Authorization' => 'Bearer ' . $api_key,
5549 + ],
5550 + 'timeout' => 60,
5551 + 'redirection' => 5,
5552 + 'blocking' => true,
5553 + 'httpversion' => '1.0',
5554 + 'sslverify' => true,
5555 + ];
5556 +
5557 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
5558 +
5559 + if (is_wp_error($response)) {
5560 + $error_message = $response->get_error_message();
5561 + //error_log('OpenAI API Error: ' . $error_message);
5562 + return [
5563 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
5564 + 'error_code' => 'openai_connection_error',
5565 + 'provider' => 'openai'
5566 + ];
5567 + }
5568 +
5569 + $status_code = wp_remote_retrieve_response_code($response);
5570 + if ($status_code !== 200) {
5571 + $response_body = wp_remote_retrieve_body($response);
5572 + $decoded_response = json_decode($response_body, true);
5573 +
5574 + $error_message = isset($decoded_response['error']['message'])
5575 + ? $decoded_response['error']['message']
5576 + : 'HTTP Error ' . $status_code;
5577 +
5578 + $error_type = isset($decoded_response['error']['type'])
5579 + ? $decoded_response['error']['type']
5580 + : 'unknown';
5581 +
5582 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5583 +
5584 + // Handle specific error types
5585 + switch ($error_type) {
5586 + case 'invalid_request_error':
5587 + if (strpos($error_message, 'API key') !== false) {
5588 + return [
5589 + 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
5590 + 'error_code' => 'openai_invalid_api_key',
5591 + 'provider' => 'openai'
5592 + ];
5593 + }
5594 + break;
5595 +
5596 + case 'authentication_error':
5597 + return [
5598 + 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
5599 + 'error_code' => 'openai_auth_error',
5600 + 'provider' => 'openai'
5601 + ];
5602 +
5603 + case 'rate_limit_exceeded':
5604 + return [
5605 + 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
5606 + 'error_code' => 'openai_rate_limit',
5607 + 'provider' => 'openai'
5608 + ];
5609 +
5610 + case 'quota_exceeded':
5611 + return [
5612 + 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
5613 + 'error_code' => 'openai_quota_exceeded',
5614 + 'provider' => 'openai'
5615 + ];
5616 + }
5617 +
5618 + // Generic error fallback
5619 + return [
5620 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
5621 + 'error_code' => 'openai_api_error',
5622 + 'provider' => 'openai',
5623 + 'status_code' => $status_code
5624 + ];
5625 + }
5626 +
5627 + $response_body = wp_remote_retrieve_body($response);
5628 + $decoded_response = json_decode($response_body, true);
5629 +
5630 + if (isset($decoded_response['choices'][0]['message']['content'])) {
5631 + return trim($decoded_response['choices'][0]['message']['content']);
5632 + } else {
5633 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
5634 + return [
5635 + 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
5636 + 'error_code' => 'openai_response_format_error',
5637 + 'provider' => 'openai'
5638 + ];
5639 + }
5640 + } catch (Exception $e) {
5641 + //error_log('OpenAI Exception: ' . $e->getMessage());
5642 + return [
5643 + 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
5644 + 'error_code' => 'openai_exception',
5645 + 'provider' => 'openai'
5646 + ];
5647 + }
5648 +}
5649 +private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
5650 + try {
5651 + // Get bot ID from session or request
5652 + $bot_id = $this->get_current_bot_id($session_id);
5653 +
5654 + // Get system prompt instructions using centralized function
5655 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5656 +
5657 + // Add system prompt to relevant content
5658 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5659 +
5660 + // Prepend system instructions to the conversation history
5661 + array_unshift($conversation_history, [
5662 + 'role' => 'system',
5663 + 'content' => "Here are your instructions: " . $content_with_instructions
5664 + ]);
5665 +
5666 + // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
5667 + foreach ($conversation_history as &$message) {
5668 + if ($message['role'] === 'bot') {
5669 + $message['role'] = 'assistant';
5670 + } elseif ($message['role'] === 'agent') {
5671 + // Tag the message as coming from a live agent
5672 + $message['role'] = 'assistant';
5673 + if (!isset($message['metadata'])) {
5674 + $message['metadata'] = ['source' => 'live_agent'];
5675 + }
5676 + }
5677 +
5678 + // Ensure all roles are valid
5679 + if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
5680 + $message['role'] = 'user'; // Default to 'user'
5681 + }
5682 + }
5683 +
5684 + // Build the request body
5685 + $body = json_encode([
5686 + 'model' => $selected_model,
5687 + 'messages' => $conversation_history,
5688 + 'temperature' => 0.8,
5689 + 'stream' => false
5690 + ]);
5691 +
5692 + // Set up the API request
5693 + $args = [
5694 + 'body' => $body,
5695 + 'headers' => [
5696 + 'Content-Type' => 'application/json',
5697 + 'Authorization' => 'Bearer ' . $xai_api_key,
5698 + ],
5699 + 'timeout' => 60,
5700 + 'redirection' => 5,
5701 + 'blocking' => true,
5702 + 'httpversion' => '1.0',
5703 + 'sslverify' => true,
5704 + ];
5705 +
5706 + // Make the API request
5707 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
5708 +
5709 + // Process the response
5710 + if (is_wp_error($response)) {
5711 + $error_message = $response->get_error_message();
5712 + //error_log('X.AI API Error: ' . $error_message);
5713 + return [
5714 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
5715 + 'error_code' => 'xai_connection_error',
5716 + 'provider' => 'xai'
5717 + ];
5718 + }
5719 +
5720 + $status_code = wp_remote_retrieve_response_code($response);
5721 + if ($status_code !== 200) {
5722 + $response_body = wp_remote_retrieve_body($response);
5723 + $decoded_response = json_decode($response_body, true);
5724 +
5725 + // Log the full response for debugging
5726 + //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
5727 +
5728 + // Extract error message from X.AI's specific format
5729 + $error_message = '';
5730 +
5731 + // Check for direct error string (as seen in your logs)
5732 + if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
5733 + $error_message = $decoded_response['error'];
5734 + }
5735 + // Check for nested error object (OpenAI style)
5736 + elseif (isset($decoded_response['error']['message'])) {
5737 + $error_message = $decoded_response['error']['message'];
5738 + }
5739 + // Check for top-level message
5740 + elseif (isset($decoded_response['message'])) {
5741 + $error_message = $decoded_response['message'];
5742 + }
5743 + // Fallback
5744 + else {
5745 + $error_message = 'HTTP Error ' . $status_code;
5746 + }
5747 +
5748 + //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5749 +
5750 + // Check for API key errors using string matching
5751 + if (stripos($error_message, 'api key') !== false ||
5752 + stripos($error_message, 'incorrect api key') !== false ||
5753 + stripos($error_message, 'invalid api key') !== false) {
5754 + return [
5755 + 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
5756 + 'error_code' => 'xai_invalid_api_key',
5757 + 'provider' => 'xai'
5758 + ];
5759 + }
5760 +
5761 + // Authentication errors
5762 + if ($status_code === 401 || $status_code === 403 ||
5763 + stripos($error_message, 'auth') !== false) {
5764 + return [
5765 + 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
5766 + 'error_code' => 'xai_auth_error',
5767 + 'provider' => 'xai'
5768 + ];
5769 + }
5770 +
5771 + // Model errors
5772 + if (stripos($error_message, 'model') !== false) {
5773 + return [
5774 + 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
5775 + 'error_code' => 'xai_invalid_model',
5776 + 'provider' => 'xai'
5777 + ];
5778 + }
5779 +
5780 + // Rate limit errors
5781 + if ($status_code === 429 ||
5782 + stripos($error_message, 'rate') !== false ||
5783 + stripos($error_message, 'limit') !== false) {
5784 + return [
5785 + 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
5786 + 'error_code' => 'xai_rate_limit',
5787 + 'provider' => 'xai'
5788 + ];
5789 + }
5790 +
5791 + // Quota errors
5792 + if (stripos($error_message, 'quota') !== false ||
5793 + stripos($error_message, 'billing') !== false) {
5794 + return [
5795 + 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
5796 + 'error_code' => 'xai_quota_exceeded',
5797 + 'provider' => 'xai'
5798 + ];
5799 + }
5800 +
5801 + // Server errors
5802 + if ($status_code >= 500) {
5803 + return [
5804 + 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
5805 + 'error_code' => 'xai_service_unavailable',
5806 + 'provider' => 'xai'
5807 + ];
5808 + }
5809 +
5810 + // Generic error fallback with the actual error message
5811 + return [
5812 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
5813 + 'error_code' => 'xai_api_error',
5814 + 'provider' => 'xai',
5815 + 'status_code' => $status_code
5816 + ];
5817 + }
5818 +
5819 + $response_body = wp_remote_retrieve_body($response);
5820 + $decoded_response = json_decode($response_body, true);
5821 +
5822 + if (isset($decoded_response['choices'][0]['message']['content'])) {
5823 + return trim($decoded_response['choices'][0]['message']['content']);
5824 + } else {
5825 + //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
5826 + return [
5827 + 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
5828 + 'error_code' => 'xai_response_format_error',
5829 + 'provider' => 'xai'
5830 + ];
5831 + }
5832 +} catch (Exception $e) {
5833 + //error_log('X.AI Exception: ' . $e->getMessage());
5834 + return [
5835 + 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
5836 + 'error_code' => 'xai_exception',
5837 + 'provider' => 'xai'
5838 + ];
5839 +}
5840 +
5841 +
5842 +}
5843 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
5844 + try {
5845 + // Ensure conversation_history is an array
5846 + if (!is_array($conversation_history)) {
5847 + $conversation_history = array();
5848 + }
5849 +
5850 + // Get bot ID from session or request
5851 + $bot_id = $this->get_current_bot_id($session_id);
5852 +
5853 + // Get system prompt instructions using centralized function
5854 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
5855 +
5856 + // Create a new array for the formatted conversation
5857 + $formatted_conversation = array();
5858 +
5859 + // Add system message first
5860 + $formatted_conversation[] = array(
5861 + 'role' => 'system',
5862 + 'content' => $system_prompt_instructions . " " . $relevant_content
5863 + );
5864 +
5865 + // Add the rest of the conversation history
5866 + foreach ($conversation_history as $message) {
5867 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5868 + $role = $message['role'];
5869 +
5870 + // Convert roles to supported format
5871 + if ($role === 'bot' || $role === 'agent') {
5872 + $role = 'assistant';
5873 + }
5874 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5875 + $role = 'user';
5876 + }
5877 +
5878 + $formatted_conversation[] = array(
5879 + 'role' => $role,
5880 + 'content' => $message['content']
5881 + );
5882 + }
5883 + }
5884 +
5885 + $body = json_encode([
5886 + 'model' => $selected_model,
5887 + 'messages' => $formatted_conversation,
5888 + 'temperature' => 0.8,
5889 + 'stream' => false
5890 + ]);
5891 +
5892 + $args = [
5893 + 'body' => $body,
5894 + 'headers' => [
5895 + 'Content-Type' => 'application/json',
5896 + 'Authorization' => 'Bearer ' . $deepseek_api_key,
5897 + ],
5898 + 'timeout' => 60,
5899 + 'redirection' => 5,
5900 + 'blocking' => true,
5901 + 'httpversion' => '1.0',
5902 + 'sslverify' => true,
5903 + ];
5904 +
5905 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
5906 +
5907 + if (is_wp_error($response)) {
5908 + $error_message = $response->get_error_message();
5909 + //error_log('DeepSeek API Error: ' . $error_message);
5910 + return [
5911 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
5912 + 'error_code' => 'deepseek_connection_error',
5913 + 'provider' => 'deepseek'
5914 + ];
5915 + }
5916 +
5917 + $status_code = wp_remote_retrieve_response_code($response);
5918 + if ($status_code !== 200) {
5919 + $response_body = wp_remote_retrieve_body($response);
5920 + $decoded_response = json_decode($response_body, true);
5921 +
5922 + $error_message = isset($decoded_response['error']['message'])
5923 + ? $decoded_response['error']['message']
5924 + : 'HTTP Error ' . $status_code;
5925 +
5926 + $error_type = isset($decoded_response['error']['type'])
5927 + ? $decoded_response['error']['type']
5928 + : 'unknown';
5929 +
5930 + //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
5931 +
5932 + // Handle specific error types
5933 + switch ($status_code) {
5934 + case 401:
5935 + return [
5936 + 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
5937 + 'error_code' => 'deepseek_auth_error',
5938 + 'provider' => 'deepseek'
5939 + ];
5940 +
5941 + case 400:
5942 + if (strpos($error_message, 'API key') !== false) {
5943 + return [
5944 + 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
5945 + 'error_code' => 'deepseek_invalid_api_key',
5946 + 'provider' => 'deepseek'
5947 + ];
5948 + }
5949 + break;
5950 +
5951 + case 429:
5952 + if (strpos($error_message, 'quota') !== false) {
5953 + return [
5954 + 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
5955 + 'error_code' => 'deepseek_quota_exceeded',
5956 + 'provider' => 'deepseek'
5957 + ];
5958 + } else {
5959 + return [
5960 + 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
5961 + 'error_code' => 'deepseek_rate_limit',
5962 + 'provider' => 'deepseek'
5963 + ];
5964 + }
5965 +
5966 + case 500:
5967 + case 502:
5968 + case 503:
5969 + case 504:
5970 + return [
5971 + 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
5972 + 'error_code' => 'deepseek_service_unavailable',
5973 + 'provider' => 'deepseek'
5974 + ];
5975 + }
5976 +
5977 + // Generic error fallback
5978 + return [
5979 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
5980 + 'error_code' => 'deepseek_api_error',
5981 + 'provider' => 'deepseek',
5982 + 'status_code' => $status_code
5983 + ];
5984 + }
5985 +
5986 + $response_body = wp_remote_retrieve_body($response);
5987 + $decoded_response = json_decode($response_body, true);
5988 +
5989 + if (isset($decoded_response['choices'][0]['message']['content'])) {
5990 + return trim($decoded_response['choices'][0]['message']['content']);
5991 + } else {
5992 + //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
5993 + return [
5994 + 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
5995 + 'error_code' => 'deepseek_response_format_error',
5996 + 'provider' => 'deepseek'
5997 + ];
5998 + }
5999 + } catch (Exception $e) {
6000 + //error_log('DeepSeek Exception: ' . $e->getMessage());
6001 + return [
6002 + 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
6003 + 'error_code' => 'deepseek_exception',
6004 + 'provider' => 'deepseek'
6005 + ];
6006 + }
6007 +}
6008 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
6009 + // Get bot ID from session or request
6010 + $bot_id = $this->get_current_bot_id($session_id);
6011 +
6012 + // Get system prompt instructions using centralized function
6013 + $system_prompt_instructions = $this->get_system_instructions($bot_id);
6014 +
6015 + // Add system prompt to relevant content
6016 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
6017 +
6018 + // Format messages for Gemini API
6019 + $formatted_messages = [];
6020 +
6021 + // Add system message as the first user message with role prefix
6022 + // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
6023 + $formatted_messages[] = [
6024 + 'role' => 'user',
6025 + 'parts' => [
6026 + ['text' => "[System Instructions] " . $content_with_instructions]
6027 + ]
6028 + ];
6029 +
6030 + // Add model response to acknowledge system instructions
6031 + $formatted_messages[] = [
6032 + 'role' => 'model',
6033 + 'parts' => [
6034 + ['text' => "I understand and will follow these instructions."]
6035 + ]
6036 + ];
6037 +
6038 + // Process the rest of the conversation history
6039 + $current_role = null;
6040 + $current_parts = [];
6041 +
6042 + foreach ($conversation_history as $message) {
6043 + // Skip the first system message as we already handled it
6044 + if ($message['role'] === 'system') {
6045 + continue;
6046 + }
6047 +
6048 + // Map roles to Gemini format
6049 + $gemini_role = '';
6050 + if ($message['role'] === 'user') {
6051 + $gemini_role = 'user';
6052 + } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
6053 + $gemini_role = 'model';
6054 + } else {
6055 + // Skip unsupported roles
6056 + continue;
6057 + }
6058 +
6059 + // If we have a new role, add the previous message
6060 + if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
6061 + $formatted_messages[] = [
6062 + 'role' => $current_role,
6063 + 'parts' => $current_parts
6064 + ];
6065 + $current_parts = [];
6066 + }
6067 +
6068 + // Set current role and add text to parts
6069 + $current_role = $gemini_role;
6070 + $current_parts[] = ['text' => $message['content']];
6071 + }
6072 +
6073 + // Add the last message if there's content
6074 + if ($current_role !== null && !empty($current_parts)) {
6075 + $formatted_messages[] = [
6076 + 'role' => $current_role,
6077 + 'parts' => $current_parts
6078 + ];
6079 + }
6080 +
6081 + // Build the request body
6082 + $body = json_encode([
6083 + 'contents' => $formatted_messages,
6084 + 'generationConfig' => [
6085 + 'temperature' => 0.7,
6086 + 'topP' => 0.95,
6087 + 'topK' => 40,
6088 + 'maxOutputTokens' => 8192,
6089 + ],
6090 + 'safetySettings' => [
6091 + [
6092 + 'category' => 'HARM_CATEGORY_HARASSMENT',
6093 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6094 + ],
6095 + [
6096 + 'category' => 'HARM_CATEGORY_HATE_SPEECH',
6097 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6098 + ],
6099 + [
6100 + 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
6101 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6102 + ],
6103 + [
6104 + 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
6105 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6106 + ]
6107 + ]
6108 + ]);
6109 +
6110 + // Prepare the API endpoint
6111 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
6112 +
6113 + // Set up the API request
6114 + $args = [
6115 + 'body' => $body,
6116 + 'headers' => [
6117 + 'Content-Type' => 'application/json',
6118 + ],
6119 + 'timeout' => 60,
6120 + 'redirection' => 5,
6121 + 'blocking' => true,
6122 + 'httpversion' => '1.0',
6123 + 'sslverify' => true,
6124 + ];
6125 +
6126 + // Make the API request
6127 + $response = wp_remote_post($api_endpoint, $args);
6128 +
6129 + // Process the response
6130 + if (is_wp_error($response)) {
6131 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
6132 + }
6133 +
6134 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
6135 +
6136 + // Handle potential errors in the response
6137 + if (isset($response_body['error'])) {
6138 + //error_log('Gemini API Error: ' . json_encode($response_body['error']));
6139 + return "Sorry, there was an error with the Gemini API: " .
6140 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
6141 + }
6142 +
6143 + // Extract the response text
6144 + if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
6145 + return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
6146 + } else {
6147 + //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
6148 + return "Sorry, I couldn't process that request. The response format was unexpected.";
6149 + }
6150 +}
6151 +
6152 +
6153 +public function test_streaming_request() {
6154 + $options = get_option('mxchat_options', []);
6155 + $model = $options['model'] ?? 'gpt-4o';
6156 +
6157 + // Detect provider from model prefix
6158 + $provider = strtolower(explode('-', $model)[0]);
6159 +
6160 + $sample_prompt = 'Hello! Can you stream this response back to me?';
6161 + $messages = [['role' => 'user', 'content' => $sample_prompt]];
6162 + $headers = [];
6163 + $body = [];
6164 + $url = '';
6165 + $api_key = '';
6166 +
6167 + switch ($provider) {
6168 + case 'gpt':
6169 + case 'o1':
6170 + $api_key = $options['api_key'] ?? '';
6171 + if (empty($api_key)) return '❌ Missing API key for OpenAI';
6172 + $url = 'https://api.openai.com/v1/chat/completions';
6173 + $headers = [
6174 + 'Content-Type: application/json',
6175 + 'Authorization: Bearer ' . $api_key
6176 + ];
6177 + $body = [
6178 + 'model' => $model,
6179 + 'messages' => $messages,
6180 + 'stream' => true
6181 + ];
6182 + break;
6183 +
6184 + case 'claude':
6185 + $api_key = $options['claude_api_key'] ?? '';
6186 + if (empty($api_key)) return '❌ Missing API key for Claude';
6187 + $url = 'https://api.anthropic.com/v1/messages';
6188 + $headers = [
6189 + 'Content-Type: application/json',
6190 + 'x-api-key: ' . $api_key,
6191 + 'anthropic-version: 2023-06-01'
6192 + ];
6193 + $body = [
6194 + 'model' => $model,
6195 + 'messages' => $messages,
6196 + 'max_tokens' => 100,
6197 + 'stream' => true
6198 + ];
6199 + break;
6200 +
6201 + case 'grok':
6202 + $api_key = $options['xai_api_key'] ?? '';
6203 + if (empty($api_key)) return '❌ Missing API key for X.AI';
6204 + $url = 'https://api.x.ai/v1/chat/completions';
6205 + $headers = [
6206 + 'Content-Type: application/json',
6207 + 'Authorization: Bearer ' . $api_key
6208 + ];
6209 + $body = [
6210 + 'model' => $model,
6211 + 'messages' => $messages,
6212 + 'stream' => true
6213 + ];
6214 + break;
6215 +
6216 + case 'deepseek':
6217 + if (empty($deepseek_api_key)) {
6218 + $error_response = [
6219 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
6220 + 'error_code' => 'missing_deepseek_api_key'
6221 + ];
6222 + if ($testing_data !== null) {
6223 + $error_response['testing_data'] = $testing_data;
6224 + }
6225 + return $error_response;
6226 + }
6227 + if ($streaming) {
6228 + return $this->mxchat_generate_response_deepseek_stream(
6229 + $selected_model,
6230 + $deepseek_api_key,
6231 + $conversation_history,
6232 + $relevant_content,
6233 + $session_id,
6234 + $testing_data // Pass testing data
6235 + );
6236 + } else {
6237 + $response = $this->mxchat_generate_response_deepseek(
6238 + $selected_model,
6239 + $deepseek_api_key,
6240 + $conversation_history,
6241 + $relevant_content
6242 + );
6243 + }
6244 + break;
6245 +
6246 + case 'gemini':
6247 + $api_key = $options['gemini_api_key'] ?? '';
6248 + if (empty($api_key)) return '❌ Missing API key for Gemini';
6249 + $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
6250 + $headers = ['Content-Type: application/json'];
6251 + $body = [
6252 + 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
6253 + 'generationConfig' => ['temperature' => 0.7]
6254 + ];
6255 + break;
6256 +
6257 + default:
6258 + return '❌ Unsupported provider: ' . $provider;
6259 + }
6260 +
6261 + // Do the actual streaming test
6262 + $ch = curl_init($url);
6263 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
6264 + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
6265 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
6266 + curl_setopt($ch, CURLOPT_TIMEOUT, 15);
6267 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6268 +
6269 + $response = curl_exec($ch);
6270 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6271 + $error = curl_error($ch);
6272 + curl_close($ch);
6273 +
6274 + if ($error) return "❌ cURL error: $error";
6275 + if ($http_code !== 200) {
6276 + $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
6277 + return "❌ HTTP $http_code: $error_message";
6278 + }
6279 +
6280 + return true;
6281 +}
6282 +
3922 6283 public function mxchat_dismiss_pre_chat_message() {
3923 6284 // Get and sanitize the user identifier
3924 6285 $user_id = $this->mxchat_get_user_identifier();
3925 6286 $user_id = sanitize_key($user_id);
@@ -3973,13 +6334,13 @@
3973 6334
3974 6335 return $dotProduct / ($normA * $normB);
3975 6336 }
3976 6337
6338 +
3977 6339 public function mxchat_enqueue_scripts_styles() {
3978 6340 // Define version numbers for the styles and scripts
3979 - $chat_style_version = '2.0.3'; // Replace with your actual version
3980 - $chat_script_version = '2.0.3'; // Replace with your actual version
3981 -
6341 + $chat_style_version = '2.4.6';
6342 + $chat_script_version = '2.4.6';
3982 6343 // Enqueue the script
3983 6344 wp_enqueue_script(
3984 6345 'mxchat-chat-js',
3985 6346 plugin_dir_url(__FILE__) . '../js/chat-script.js',
@@ -3986,9 +6347,8 @@
3986 6347 array('jquery'),
3987 6348 $chat_script_version,
3988 6349 true
3989 6350 );
3990 -
3991 6351 // Enqueue the CSS
3992 6352 wp_enqueue_style(
3993 6353 'mxchat-chat-css',
3994 6354 plugin_dir_url(__FILE__) . '../css/chat-style.css',
@@ -3994,17 +6354,19 @@
3994 6354 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3995 6355 array(),
3996 6356 $chat_style_version
3997 6357 );
3998 -
3999 6358 // Fetch options from the database
4000 6359 $this->options = get_option('mxchat_options');
4001 6360 $prompts_options = get_option('mxchat_prompts_options', array());
4002 -
6361 +
4003 6362 // Prepare settings for JavaScript
4004 6363 $style_settings = array(
4005 6364 'ajax_url' => admin_url('admin-ajax.php'),
4006 6365 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
6366 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
6367 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
6368 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
4007 6369 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
4008 6370 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
4009 6371 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
4010 6372 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -4018,10 +6380,9 @@
4018 6380 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
4019 6381 'icon_color' => $this->options['icon_color'] ?? '#fff',
4020 6382 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
4021 6383 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
4022 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
4023 -
6384 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
4024 6385 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
4025 6386 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
4026 6387 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
4027 6388 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
@@ -4026,76 +6387,942 @@
4026 6387 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
4027 6388 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
4028 6389 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
4029 6390 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
4030 -
4031 6391 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
6392 + 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
6393 + 'initial_email_state' => null, // Also fixed this undefined variable
6394 + 'skip_email_check' => true,
4032 6395 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
4033 6396 );
4034 -
4035 6397 // Pass the settings to the script
4036 6398 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
4037 6399 }
4038 6400
4039 6401
6402 +/**
6403 + * Setup the cron jobs for rate limits with guard against multiple calls
6404 + */
6405 +public function setup_rate_limit_cron_jobs() {
6406 + // Add a guard to prevent multiple rapid calls
6407 + $last_setup = get_transient('mxchat_cron_setup_guard');
6408 + if ($last_setup && (time() - $last_setup) < 60) {
6409 + // Don't run again if we ran less than 60 seconds ago
6410 + return;
6411 + }
6412 +
6413 + // Set the guard
6414 + set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
6415 +
6416 + try {
6417 + // First, check if WordPress cron is disabled
6418 + if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
6419 + //error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
6420 + $this->setup_fallback_rate_limit_system();
6421 + return;
6422 + }
6423 +
6424 + // Check if cron is already scheduled - if so, don't mess with it
6425 + if (wp_next_scheduled('mxchat_reset_rate_limits')) {
6426 + //error_log('MxChat: Rate limit cron already scheduled, skipping setup');
6427 + return;
6428 + }
6429 +
6430 + // Clear any orphaned hooks (but don't loop indefinitely)
6431 + $hooks_to_clear = [
6432 + 'mxchat_reset_rate_limits',
6433 + 'mxchat_reset_hourly_rate_limits',
6434 + 'mxchat_reset_daily_rate_limits',
6435 + 'mxchat_reset_weekly_rate_limits',
6436 + 'mxchat_reset_monthly_rate_limits'
6437 + ];
6438 +
6439 + foreach ($hooks_to_clear as $hook) {
6440 + // Only clear a maximum of 3 instances to prevent infinite loops
6441 + $cleared = 0;
6442 + while (wp_next_scheduled($hook) && $cleared < 3) {
6443 + wp_clear_scheduled_hook($hook);
6444 + $cleared++;
6445 + }
6446 + }
6447 +
6448 + // Small delay after clearing
6449 + usleep(100000); // 0.1 seconds
6450 +
6451 + // Try to schedule the event
6452 + $initial_time = time() + 300; // Start in 5 minutes
6453 + $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
6454 +
6455 + if ($result === false) {
6456 + //error_log('MxChat: Failed to schedule cron, using fallback system');
6457 + $this->setup_fallback_rate_limit_system();
6458 + } else {
6459 + //error_log('MxChat: Successfully scheduled rate limit reset cron');
6460 + }
6461 +
6462 + } catch (Exception $e) {
6463 + //error_log('MxChat: Cron setup exception: ' . $e->getMessage());
6464 + $this->setup_fallback_rate_limit_system();
6465 + }
6466 +}
6467 +
6468 +/**
6469 + * Try alternative cron scheduling methods
6470 + */
6471 +private function try_alternative_cron_scheduling($initial_time) {
6472 + try {
6473 + // Method 1: Try with current time instead of future time
6474 + $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
6475 + if ($result1 !== false) {
6476 + //error_log('MxChat: Alternative method 1 (current time) succeeded');
6477 + return true;
6478 + }
6479 +
6480 + // Method 2: Try with a different interval
6481 + $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
6482 + if ($result2 !== false) {
6483 + //error_log('MxChat: Alternative method 2 (daily interval) succeeded');
6484 + return true;
6485 + }
6486 +
6487 + // Method 3: Try wp_schedule_single_event first, then recurring
6488 + $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
6489 + if ($result3 !== false) {
6490 + //error_log('MxChat: Alternative method 3 (single event) succeeded');
6491 + // Schedule the next one manually in the handler
6492 + return true;
6493 + }
6494 +
6495 + return false;
6496 +
6497 + } catch (Exception $e) {
6498 + //error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
6499 + return false;
6500 + }
6501 +}
6502 +
6503 +/**
6504 + * Enhanced fallback rate limit system
6505 + */
6506 +private function setup_fallback_rate_limit_system() {
6507 + // Set a flag to use database-based rate limit cleanup
6508 + update_option('mxchat_use_fallback_rate_limits', true);
6509 +
6510 + // Schedule a one-time check to happen on the next plugin load
6511 + update_option('mxchat_next_rate_limit_check', time() + 3600);
6512 +
6513 + // Also set up a more frequent fallback check (every 4 hours)
6514 + update_option('mxchat_fallback_check_interval', 4 * 3600);
6515 +
6516 + //error_log('MxChat: Fallback rate limit system activated');
6517 +}
6518 +
6519 +/**
6520 + * Enhanced fallback check method
6521 + */
6522 +public function check_fallback_rate_limits() {
6523 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
6524 +
6525 + if (!$use_fallback) {
6526 + return; // Regular cron is working
6527 + }
6528 +
6529 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
6530 + $check_interval = get_option('mxchat_fallback_check_interval', 3600);
6531 +
6532 + if (time() >= $next_check) {
6533 + //error_log('MxChat: Running fallback rate limit cleanup');
6534 + $this->mxchat_reset_rate_limits();
6535 +
6536 + // Schedule next check
6537 + update_option('mxchat_next_rate_limit_check', time() + $check_interval);
6538 + }
6539 +}
6540 +/**
6541 + * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
6542 + */
6543 +public function check_rate_limit() {
6544 + // Check if we need to run fallback cleanup
6545 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
6546 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
6547 +
6548 + if ($use_fallback && time() >= $next_check) {
6549 + $this->mxchat_reset_rate_limits();
6550 + update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
6551 + }
6552 +
6553 + // Get bot ID from current request context
6554 + $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
6555 +
6556 + // Get bot-specific options (includes rate limits if overridden)
6557 + $bot_options = $this->get_bot_options($bot_id);
6558 + $current_options = !empty($bot_options) ? $bot_options : $this->options;
6559 +
6560 + // Use bot-specific rate limits if available, otherwise fall back to default
6561 + $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
6562 +
6563 + // Determine user role or if logged out
6564 + if (is_user_logged_in()) {
6565 + $user = wp_get_current_user();
6566 + $user_id = $user->ID;
6567 +
6568 + // Get the user's primary role using reset() to safely get the first element
6569 + $user_roles = $user->roles;
6570 +
6571 + // Safely get the first role regardless of array key structure
6572 + if (!empty($user_roles) && is_array($user_roles)) {
6573 + $role = reset($user_roles); // This safely gets the first element regardless of key
6574 + } else {
6575 + $role = 'subscriber'; // Default to subscriber if no role found
6576 + }
6577 + } else {
6578 + $role = 'logged_out';
6579 + // Use IP address for non-logged-in users
6580 + $user_id = $this->get_client_ip();
6581 + }
6582 +
6583 + // Check if rate limits are configured for this role
6584 + if (!isset($rate_limits_source[$role])) {
6585 + return true; // No limit set for this role
6586 + }
6587 +
6588 + $limit = $rate_limits_source[$role]['limit'];
6589 +
6590 + // If unlimited, return true immediately
6591 + if ($limit === 'unlimited') {
6592 + return true;
6593 + }
6594 +
6595 + // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
6596 + $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
6597 + $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
6598 + $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
6599 +
6600 + // Include bot_id in option name so each bot has separate rate limits
6601 + $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
6602 +
6603 + // Get the counter data
6604 + $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
6605 +
6606 + // If first request or counter reset needed, set the initial timestamp
6607 + if ($limit_data['count'] === 0) {
6608 + $limit_data['timestamp'] = time();
6609 + update_option($option_name, $limit_data);
6610 + }
6611 +
6612 + // Get the timeframe
6613 + $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
6614 + $rate_limits_source[$role]['timeframe'] : 'daily';
6615 +
6616 + // Check if the counter needs to be reset based on timeframe
6617 + $current_time = time();
6618 + $timestamp = $limit_data['timestamp'];
6619 + $should_reset = false;
6620 +
6621 + switch ($timeframe) {
6622 + case 'hourly':
6623 + $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
6624 + break;
6625 + case 'daily':
6626 + $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
6627 + break;
6628 + case 'weekly':
6629 + $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
6630 + break;
6631 + case 'monthly':
6632 + $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
6633 + break;
6634 + }
6635 +
6636 + // Reset the counter if the timeframe has passed
6637 + if ($should_reset) {
6638 + $limit_data = ['count' => 0, 'timestamp' => $current_time];
6639 + update_option($option_name, $limit_data);
6640 + }
6641 +
6642 + // Check if user has exceeded their limit
6643 + if ($limit_data['count'] >= intval($limit)) {
6644 + // Get the custom message for this role
6645 + $message = !empty($rate_limits_source[$role]['message'])
6646 + ? $rate_limits_source[$role]['message']
6647 + : __('Rate limit exceeded. Please try again later.', 'mxchat');
6648 +
6649 + // Add timeframe information to the message if placeholders exist
6650 + $timeframe_label = '';
6651 + switch ($timeframe) {
6652 + case 'hourly':
6653 + $timeframe_label = __('hour', 'mxchat');
6654 + break;
6655 + case 'daily':
6656 + $timeframe_label = __('day', 'mxchat');
6657 + break;
6658 + case 'weekly':
6659 + $timeframe_label = __('week', 'mxchat');
6660 + break;
6661 + case 'monthly':
6662 + $timeframe_label = __('month', 'mxchat');
6663 + break;
6664 + }
6665 +
6666 + // Replace placeholders in the message
6667 + $message = str_replace(
6668 + ['{limit}', '{count}', '{remaining}', '{timeframe}'],
6669 + [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
6670 + $message
6671 + );
6672 +
6673 + // Process HTML links in the message
6674 + $message = $this->process_rate_limit_message_html($message);
6675 +
6676 + // Return error with the processed message
6677 + return [
6678 + 'error' => true,
6679 + 'message' => $message
6680 + ];
6681 + }
6682 +
6683 + // Increment the counter
6684 + $limit_data['count']++;
6685 + update_option($option_name, $limit_data);
6686 +
6687 + return true;
6688 +}
6689 +
6690 +/**
6691 + * Enhanced rate limit reset with better error handling
6692 + */
4040 6693 public function mxchat_reset_rate_limits() {
6694 + try {
4041 6695 global $wpdb;
6696 + $all_options = get_option('mxchat_options', []);
6697 + $current_time = time();
6698 +
6699 + // Get rate limit options with a safer query and limit
6700 + $option_names = $wpdb->get_col(
6701 + $wpdb->prepare(
6702 + "SELECT option_name FROM {$wpdb->options}
6703 + WHERE option_name LIKE %s
6704 + LIMIT 1000",
6705 + 'mxchat_chat_limit_%'
6706 + )
6707 + );
6708 +
6709 + if (empty($option_names)) {
6710 + return;
6711 + }
6712 +
6713 + $processed_count = 0;
6714 + $max_processing_time = 30; // Maximum 30 seconds
6715 + $start_time = time();
6716 +
6717 + foreach ($option_names as $option_name) {
6718 + // Check processing time limit
6719 + if ((time() - $start_time) > $max_processing_time) {
6720 + //error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
6721 + break;
6722 + }
6723 +
6724 + // Parse the option name more safely
6725 + if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
6726 + continue;
6727 + }
6728 +
6729 + $role_and_user = $matches[1] . '_' . $matches[2];
6730 + $parts = explode('_', $role_and_user);
6731 +
6732 + if (count($parts) < 2) {
6733 + continue;
6734 + }
6735 +
6736 + // Extract role (everything except the last part which is user ID)
6737 + $user_id_part = array_pop($parts);
6738 + $role = implode('_', $parts);
6739 +
6740 + // Skip if role doesn't exist in our settings
6741 + if (!isset($all_options['rate_limits'][$role])) {
6742 + // Clean up orphaned entries
6743 + delete_option($option_name);
6744 + continue;
6745 + }
6746 +
6747 + $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
6748 + $limit_data = get_option($option_name);
6749 +
6750 + if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
6751 + // Clean up invalid entries
6752 + delete_option($option_name);
6753 + continue;
6754 + }
6755 +
6756 + $timestamp = $limit_data['timestamp'];
6757 + $should_reset = false;
6758 +
6759 + // Determine if we should reset based on the timeframe
6760 + switch ($timeframe) {
6761 + case 'hourly':
6762 + $should_reset = ($current_time - $timestamp) >= 3600;
6763 + break;
6764 + case 'daily':
6765 + $should_reset = ($current_time - $timestamp) >= 86400;
6766 + break;
6767 + case 'weekly':
6768 + $should_reset = ($current_time - $timestamp) >= 604800;
6769 + break;
6770 + case 'monthly':
6771 + $should_reset = ($current_time - $timestamp) >= 2592000;
6772 + break;
6773 + }
6774 +
6775 + // Reset the counter if the timeframe has passed
6776 + if ($should_reset) {
6777 + delete_option($option_name);
6778 + wp_cache_delete($option_name, 'options');
6779 + $processed_count++;
6780 + }
6781 + }
6782 +
6783 + // Clean up any orphaned cache entries
6784 + wp_cache_delete('mxchat_all_chat_limits', 'options');
6785 +
6786 + //error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
6787 +
6788 + } catch (Exception $e) {
6789 + //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
6790 + }
6791 +}
4042 6792
4043 - // Define a cache key pattern for rate limits
4044 - $cache_key_pattern = 'mxchat_chat_limit_%';
4045 6793
4046 - // Retrieve all option names matching the pattern
4047 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
4048 - $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
6794 +/**
6795 + * Process HTML links in rate limit messages
6796 + *
6797 + * @param string $message The rate limit message
6798 + * @return string The processed message with safe HTML links
6799 + */
6800 +private function process_rate_limit_message_html($message) {
6801 + // Return original message if empty
6802 + if (empty($message)) {
6803 + return $message;
6804 + }
6805 +
6806 + // First, convert markdown links to HTML
6807 + $message = $this->convert_markdown_links($message);
6808 +
6809 + // Then, auto-convert any remaining plain URLs to links
6810 + $message = $this->auto_link_urls($message);
6811 +
6812 + // Allow basic HTML tags for links and formatting
6813 + $allowed_tags = [
6814 + 'a' => [
6815 + 'href' => true,
6816 + 'target' => true,
6817 + 'rel' => true,
6818 + 'title' => true,
6819 + 'class' => true
6820 + ],
6821 + 'strong' => [],
6822 + 'em' => [],
6823 + 'br' => [],
6824 + 'b' => [],
6825 + 'i' => [],
6826 + 'span' => ['class' => true]
6827 + ];
6828 +
6829 + // Sanitize but allow the specified HTML tags
6830 + $processed_message = wp_kses($message, $allowed_tags);
6831 +
6832 + // If wp_kses stripped everything, return the original message as plain text
6833 + if (empty($processed_message) && !empty($message)) {
6834 + // Strip all HTML and return plain text as fallback
6835 + return wp_strip_all_tags($message);
6836 + }
6837 +
6838 + return $processed_message;
6839 +}
4049 6840
4050 - // db call ok; no-cache ok
4051 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
4052 - $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
6841 +/**
6842 + * Convert markdown links to HTML
6843 + *
6844 + * @param string $text The text to process
6845 + * @return string The text with markdown links converted to HTML
6846 + */
6847 +private function convert_markdown_links($text) {
6848 + // Return original text if empty
6849 + if (empty($text)) {
6850 + return $text;
6851 + }
6852 +
6853 + // Pattern to match markdown links: [text](url)
6854 + $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
6855 +
6856 + $processed_text = preg_replace_callback($pattern, function($matches) {
6857 + $link_text = $matches[1];
6858 + $url = $matches[2];
6859 +
6860 + // Clean up any trailing punctuation from the URL
6861 + $url = rtrim($url, '.,;:!?');
6862 +
6863 + // Sanitize the link text and URL
6864 + $safe_text = esc_html($link_text);
6865 + $safe_url = esc_url($url);
6866 +
6867 + // Create the HTML link
6868 + return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
6869 + }, $text);
6870 +
6871 + // If preg_replace_callback failed, return original text
6872 + if ($processed_text === null) {
6873 + return $text;
6874 + }
6875 +
6876 + return $processed_text;
6877 +}
4053 6878
4054 - // Clear the relevant cache entries
4055 - foreach ($option_names as $option_name) {
4056 - wp_cache_delete($option_name, 'options');
4057 - }
6879 +/**
6880 + * Auto-convert plain URLs to clickable links
6881 + *
6882 + * @param string $text The text to process
6883 + * @return string The text with URLs converted to links
6884 + */
6885 +private function auto_link_urls($text) {
6886 + // Return original text if empty
6887 + if (empty($text)) {
6888 + return $text;
6889 + }
6890 +
6891 + // Simple pattern that avoids complex lookbehinds
6892 + // This will match URLs that are not already inside href attributes or markdown links
6893 + $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
6894 +
6895 + $processed_text = preg_replace_callback($pattern, function($matches) {
6896 + $url = $matches[0];
6897 + // Clean up any trailing punctuation that might have been captured
6898 + $url = rtrim($url, '.,;:!?');
6899 +
6900 + // Add target="_blank" and rel="noopener noreferrer" for security
6901 + return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
6902 + }, $text);
6903 +
6904 + // If preg_replace_callback failed, return original text
6905 + if ($processed_text === null) {
6906 + return $text;
6907 + }
6908 +
6909 + return $processed_text;
6910 +}
4058 6911
4059 - // Optionally, clear a general cache if you have one
4060 - wp_cache_delete('mxchat_all_chat_limits', 'options');
6912 +
6913 +// Helper function to get client IP address
6914 +private function get_client_ip() {
6915 + // Check for shared internet/ISP IP
6916 + if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
6917 + return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
4061 6918 }
6919 +
6920 + // Check for IPs passing through proxies
6921 + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
6922 + // Use the first value in the comma-separated list
6923 + $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
6924 + return trim($forwarded_for[0]);
6925 + }
6926 +
6927 + if (!empty($_SERVER['REMOTE_ADDR'])) {
6928 + return sanitize_text_field($_SERVER['REMOTE_ADDR']);
6929 + }
6930 +
6931 + // Fallback
6932 + return 'unknown';
6933 +}
4062 6934
4063 -private function mxchat_fetch_woocommerce_products() {
4064 - // Ensure WooCommerce is active
4065 - if (!class_exists('WooCommerce')) {
4066 - return [];
6935 +/**
6936 + * AJAX handler to get system information for testing panel
6937 + */
6938 +public function mxchat_get_system_info() {
6939 + // Verify nonce for security
6940 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6941 + wp_send_json_error(['message' => 'Invalid nonce']);
6942 + return;
4067 6943 }
6944 +
6945 + // Only allow admin users
6946 + if (!current_user_can('administrator')) {
6947 + wp_send_json_error(['message' => 'Unauthorized']);
6948 + return;
6949 + }
6950 +
6951 + // Get system prompt from options
6952 + $system_prompt = isset($this->options['system_prompt_instructions'])
6953 + ? $this->options['system_prompt_instructions']
6954 + : 'No system prompt configured';
6955 +
6956 + // Get selected model - FIXED: Use $this->options instead of $current_options
6957 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
6958 +
6959 + // Get API key status (just check if they exist, don't expose the keys)
6960 + $api_status = [];
6961 + $api_status['openai'] = !empty($this->options['api_key']);
6962 + $api_status['claude'] = !empty($this->options['claude_api_key']);
6963 + $api_status['gemini'] = !empty($this->options['gemini_api_key']);
6964 + $api_status['xai'] = !empty($this->options['xai_api_key']);
6965 + $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
6966 +
6967 + wp_send_json_success([
6968 + 'system_prompt' => $system_prompt,
6969 + 'selected_model' => $selected_model,
6970 + 'api_status' => $api_status
6971 + ]);
6972 +}
4068 6973
4069 - $args = array(
4070 - 'post_type' => 'product',
4071 - 'post_status' => 'publish',
4072 - 'posts_per_page' => -1,
6974 +/**
6975 + * AJAX handler to get similarity threshold
6976 + */
6977 +public function mxchat_get_similarity_threshold() {
6978 + // Verify nonce for security
6979 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6980 + wp_send_json_error(['message' => 'Invalid nonce']);
6981 + return;
6982 + }
6983 +
6984 + // Only allow admin users
6985 + if (!current_user_can('administrator')) {
6986 + wp_send_json_error(['message' => 'Unauthorized']);
6987 + return;
6988 + }
6989 +
6990 + // Get similarity threshold from main options (default 35%)
6991 + $similarity_threshold = isset($this->options['similarity_threshold'])
6992 + ? ((int) $this->options['similarity_threshold']) / 100
6993 + : 0.35;
6994 +
6995 + wp_send_json_success([
6996 + 'threshold' => $similarity_threshold,
6997 + 'threshold_percentage' => ($similarity_threshold * 100) . '%'
6998 + ]);
6999 +}
7000 +
7001 +/**
7002 + * AJAX handler to get knowledge base status
7003 + */
7004 +public function mxchat_get_kb_status() {
7005 + // Verify nonce for security
7006 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
7007 + wp_send_json_error(['message' => 'Invalid nonce']);
7008 + return;
7009 + }
7010 +
7011 + // Only allow admin users
7012 + if (!current_user_can('administrator')) {
7013 + wp_send_json_error(['message' => 'Unauthorized']);
7014 + return;
7015 + }
7016 +
7017 + // Check Pinecone vs WordPress
7018 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
7019 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
7020 +
7021 + $kb_info = [
7022 + 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
7023 + 'status' => 'Active'
7024 + ];
7025 +
7026 + // Get document count
7027 + if ($use_pinecone) {
7028 + $kb_info['documents'] = 'Connected to Pinecone';
7029 + $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
7030 + } else {
7031 + // Count documents in WordPress database
7032 + global $wpdb;
7033 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
7034 + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
7035 + $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
7036 + }
7037 +
7038 + wp_send_json_success($kb_info);
7039 +}
7040 +
7041 +/**
7042 + * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
7043 + */
7044 +public function mxchat_start_fresh_session() {
7045 + // Verify nonce for security
7046 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
7047 + wp_send_json_error(['message' => 'Invalid nonce']);
7048 + return;
7049 + }
7050 +
7051 + // Only allow admin users
7052 + if (!current_user_can('administrator')) {
7053 + wp_send_json_error(['message' => 'Unauthorized']);
7054 + return;
7055 + }
7056 +
7057 + $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
7058 + $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
7059 +
7060 + if (empty($old_session_id)) {
7061 + wp_send_json_error(['message' => 'Old session ID required']);
7062 + return;
7063 + }
7064 +
7065 + // If no new session ID provided, generate one
7066 + if (empty($new_session_id)) {
7067 + $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
7068 + }
7069 +
7070 + // Clear ALL data associated with the old session
7071 + $this->clear_complete_session_data($old_session_id);
7072 +
7073 + // Initialize the new session
7074 + $this->initialize_fresh_session($new_session_id);
7075 +
7076 + wp_send_json_success([
7077 + 'message' => 'Fresh session started successfully',
7078 + 'new_session_id' => $new_session_id,
7079 + 'old_session_id' => $old_session_id
7080 + ]);
7081 +}
7082 +
7083 +/**
7084 + * Clear ALL data associated with a session (ENHANCED)
7085 + */
7086 +private function clear_complete_session_data($session_id) {
7087 + // Clear chat history
7088 + delete_option("mxchat_history_{$session_id}");
7089 +
7090 + // Clear chat mode
7091 + delete_option("mxchat_mode_{$session_id}");
7092 +
7093 + // Clear any PDF/Word transients
7094 + $this->clear_pdf_transients($session_id);
7095 + if (method_exists($this, 'clear_word_transients')) {
7096 + $this->clear_word_transients($session_id);
7097 + }
7098 +
7099 + // Clear agent-related data
7100 + delete_option("mxchat_channel_{$session_id}");
7101 + delete_option("mxchat_agent_name_{$session_id}");
7102 + delete_option("mxchat_email_{$session_id}");
7103 +
7104 + // Clear any recommendation flow state
7105 + delete_option("mxchat_sr_flow_state_{$session_id}");
7106 +
7107 + // Clear any cached embeddings or context
7108 + delete_transient("mxchat_context_{$session_id}");
7109 + delete_transient("mxchat_last_query_{$session_id}");
7110 +
7111 + // Clear any testing data
7112 + delete_transient("mxchat_testing_data_{$session_id}");
7113 +
7114 + // Clear any rate limiting data for this session
7115 + delete_transient("mxchat_rate_limit_{$session_id}");
7116 +
7117 + // Clear any other session-specific transients
7118 + delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
7119 + delete_transient("mxchat_include_pdf_in_context_{$session_id}");
7120 + delete_transient("mxchat_include_word_in_context_{$session_id}");
7121 +
7122 + //error_log("MxChat: Cleared all data for session: {$session_id}");
7123 +}
7124 +
7125 +/**
7126 + * Initialize a fresh session with default data
7127 + */
7128 +private function initialize_fresh_session($session_id) {
7129 + // Set default chat mode
7130 + update_option("mxchat_mode_{$session_id}", 'ai');
7131 +
7132 + //error_log("MxChat: Initialized fresh session: {$session_id}");
7133 +}
7134 +
7135 +/**
7136 + * Helper method to clear Word document transients (if you have Word support)
7137 + */
7138 +private function clear_word_transients($session_id) {
7139 + delete_transient('mxchat_word_url_' . $session_id);
7140 + delete_transient('mxchat_word_filename_' . $session_id);
7141 + delete_transient('mxchat_word_embeddings_' . $session_id);
7142 + delete_transient('mxchat_include_word_in_context_' . $session_id);
7143 +}
7144 +
7145 +/**
7146 + * Simplified testing data capture method (CLEANED UP)
7147 + */
7148 +private function capture_testing_data($user_embedding, $message, $session_id) {
7149 + // Only capture for admin users
7150 + if (!current_user_can('administrator')) {
7151 + return null;
7152 + }
7153 +
7154 + $testing_data = [
7155 + 'query' => $message,
7156 + 'timestamp' => time(),
7157 + 'top_matches' => [],
7158 + 'action_matches' => [] // Add action matches
7159 + ];
7160 +
7161 + // Get similarity threshold
7162 + $similarity_threshold = isset($this->options['similarity_threshold'])
7163 + ? ((int) $this->options['similarity_threshold']) / 100
7164 + : 0.35;
7165 +
7166 + $testing_data['similarity_threshold'] = $similarity_threshold;
7167 +
7168 + // Use the real similarity analysis if available
7169 + if ($this->last_similarity_analysis !== null) {
7170 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
7171 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
7172 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7173 + } else {
7174 + // Fallback: determine knowledge base type
7175 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
7176 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
7177 +
7178 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
7179 + }
7180 +
7181 + // Include action analysis if available
7182 + if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
7183 + $testing_data['action_matches'] = $this->last_action_analysis;
7184 +
7185 + // Clear it after capturing to avoid stale data
7186 + $this->last_action_analysis = null;
7187 + }
7188 +
7189 + return $testing_data;
7190 +}
7191 +
7192 +
7193 +/**
7194 + * Track URL clicks from chatbot responses
7195 + */
7196 +public function mxchat_track_url_click() {
7197 + // Verify nonce for security
7198 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7199 + wp_send_json_error(['message' => 'Invalid nonce']);
7200 + wp_die();
7201 + }
7202 +
7203 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7204 + $clicked_url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
7205 + $message_context = isset($_POST['message_context']) ? sanitize_textarea_field($_POST['message_context']) : '';
7206 +
7207 + if (empty($session_id) || empty($clicked_url)) {
7208 + wp_send_json_error(['message' => 'Missing required data']);
7209 + wp_die();
7210 + }
7211 +
7212 + global $wpdb;
7213 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
7214 +
7215 + // Insert click tracking record
7216 + $wpdb->insert(
7217 + $table_name,
7218 + [
7219 + 'session_id' => $session_id,
7220 + 'clicked_url' => $clicked_url,
7221 + 'message_context' => $message_context,
7222 + 'click_timestamp' => current_time('mysql', 1),
7223 + 'user_ip' => $_SERVER['REMOTE_ADDR'],
7224 + 'user_agent' => $_SERVER['HTTP_USER_AGENT']
7225 + ]
4073 7226 );
7227 +
7228 + wp_send_json_success(['message' => 'Click tracked']);
7229 + wp_die();
7230 +}
4074 7231
4075 - $products = get_posts($args);
4076 - $product_data = [];
7232 +/**
7233 + * Get URL click analytics for a session
7234 + */
7235 +public function mxchat_get_url_clicks($session_id) {
7236 + global $wpdb;
7237 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
7238 +
7239 + $clicks = $wpdb->get_results($wpdb->prepare(
7240 + "SELECT * FROM $table_name WHERE session_id = %s ORDER BY click_timestamp ASC",
7241 + $session_id
7242 + ));
7243 +
7244 + return $clicks;
7245 +}
7246 +/**
7247 + * Track the originating page where chat was started
7248 + */
7249 +public function mxchat_track_originating_page() {
7250 + // Verify nonce
7251 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7252 + wp_send_json_error(['message' => 'Invalid nonce']);
7253 + wp_die();
7254 + }
7255 +
7256 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7257 + $page_url = isset($_POST['page_url']) ? esc_url_raw($_POST['page_url']) : '';
7258 + $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
7259 +
7260 + if (empty($session_id)) {
7261 + wp_send_json_error(['message' => 'Missing session ID']);
7262 + wp_die();
7263 + }
7264 +
7265 + global $wpdb;
7266 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
7267 +
7268 + // Check if we've already tracked for this session
7269 + $existing = $wpdb->get_var($wpdb->prepare(
7270 + "SELECT COUNT(*) FROM $table_name
7271 + WHERE session_id = %s
7272 + AND originating_page_url IS NOT NULL",
7273 + $session_id
7274 + ));
7275 +
7276 + if ($existing > 0) {
7277 + wp_send_json_success(['message' => 'Already tracked']);
7278 + wp_die();
7279 + }
7280 +
7281 + // Update the first message in this session with originating page info
7282 + $wpdb->query($wpdb->prepare(
7283 + "UPDATE $table_name
7284 + SET originating_page_url = %s,
7285 + originating_page_title = %s
7286 + WHERE session_id = %s
7287 + ORDER BY timestamp ASC
7288 + LIMIT 1",
7289 + $page_url,
7290 + $page_title,
7291 + $session_id
7292 + ));
7293 +
7294 + wp_send_json_success(['message' => 'Originating page tracked']);
7295 + wp_die();
7296 +}
4077 7297
4078 - foreach ($products as $product) {
4079 - $product_id = $product->ID;
4080 - $product_obj = wc_get_product($product_id);
4081 7298
4082 - $product_data[] = array(
4083 - 'id' => $product_id,
4084 - 'name' => $product_obj->get_name(),
4085 - 'description' => $product_obj->get_description(),
4086 - 'short_description' => $product_obj->get_short_description(),
4087 - 'url' => get_permalink($product_id),
4088 - 'price' => $product_obj->get_regular_price(),
4089 - 'sale_price' => $product_obj->get_sale_price(),
4090 - 'stock_status' => $product_obj->get_stock_status(),
4091 - 'sku' => $product_obj->get_sku(),
4092 - 'in_stock' => $product_obj->is_in_stock(),
4093 - 'total_sales' => $product_obj->get_total_sales(),
4094 - );
7299 +/**
7300 + * AJAX handler to get current chat mode for a session
7301 + */
7302 +public function mxchat_get_current_chat_mode() {
7303 + // Verify nonce for security
7304 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7305 + wp_send_json_error(['message' => 'Invalid nonce']);
7306 + wp_die();
4095 7307 }
7308 +
7309 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7310 +
7311 + if (empty($session_id)) {
7312 + wp_send_json_error(['message' => 'Session ID missing']);
7313 + wp_die();
7314 + }
7315 +
7316 + // Get the current chat mode for this session
7317 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
7318 +
7319 + wp_send_json_success([
7320 + 'chat_mode' => $chat_mode
7321 + ]);
7322 + wp_die();
7323 +}
4096 7324
4097 - return $product_data;
4098 -}
7325 +
4099 7326
4100 7327 }
4101 7328 ?>