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 +5243 -1369 2.0.42.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,76 +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);
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 + }
1433 +
1434 + if (!empty($this->fallbackResponse['html'])) {
1435 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1436 + }
1437 +
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 + ];
1444 +
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();
1452 +}
1453 +
1454 +
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();
947 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 +}
948 1479
949 - if (!empty($this->fallbackResponse['html'])) {
950 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
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;
951 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();
1517 +}
952 1518
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 - ];
959 1519
960 - wp_send_json($response_data);
961 - wp_die();
962 -}
963 -
964 -// New function to check intents and invoke the callback function
1520 +// Updated function to check intents and invoke the callback function
965 1521 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
966 1522 global $wpdb;
967 1523 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
968 1524
969 - //error_log('🔍 MXCHAT DEBUG: Intent Check Started ==================');
970 - //error_log("🔍 MXCHAT DEBUG: Message: '$message'");
971 - //error_log("🔍 MXCHAT DEBUG: Chat Mode: $chat_mode");
1525 + // NEW: Get the current bot_id
1526 + $current_bot_id = $this->get_current_bot_id($session_id);
972 1527
973 1528 // Generate the user embedding
974 - //error_log('🔄 MXCHAT DEBUG: Generating user embedding');
975 1529 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
976 - if (!is_array($user_embedding)) {
977 - //error_log('❌ MXCHAT DEBUG: Failed to generate user embedding');
978 - 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();
979 1541 }
980 - //error_log('✅ MXCHAT DEBUG: User embedding generated successfully');
981 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 +
982 1552 // Fetch intents from the database
983 1553 $table_name = $wpdb->prefix . 'mxchat_intents';
984 1554 if ($chat_mode === 'agent') {
985 - //error_log('🔍 MXCHAT DEBUG: Agent mode - fetching only chatbot switch intent');
986 1555 $query = $wpdb->prepare(
987 - "SELECT * FROM $table_name WHERE callback_function = %s",
1556 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
988 1557 'mxchat_handle_switch_to_chatbot_intent'
989 1558 );
990 1559 $intents = $wpdb->get_results($query);
991 1560 } else {
992 - //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all intents');
993 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
1561 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
994 1562 }
995 1563
996 - //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' intents to check');
997 -
998 1564 if (empty($intents)) {
999 - //error_log('❌ MXCHAT DEBUG: No intents found in database');
1000 1565 return false;
1001 1566 }
1002 1567
1003 1568 $highest_similarity = -INF;
@@ -1002,12 +1567,23 @@
1002 1567
1003 1568 $highest_similarity = -INF;
1004 1569 $matched_intent = null;
1005 1570
1006 - //error_log('📊 MXCHAT DEBUG: Intent Similarity Scores ==================');
1571 + // Array to store action analysis for testing panel
1572 + $action_analysis = [];
1573 +
1007 1574 foreach ($intents as $intent) {
1008 - //error_log("🔄 MXCHAT DEBUG: 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 + }
1009 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 +
1010 1586 $intent_embedding_serialized = $intent->embedding_vector;
1011 1587 $intent_embedding = $intent_embedding_serialized
1012 1588 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1013 1589 : null;
@@ -1012,9 +1588,8 @@
1012 1588 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1013 1589 : null;
1014 1590
1015 1591 if (!is_array($intent_embedding)) {
1016 - //error_log("❌ MXCHAT DEBUG: Invalid embedding for intent: {$intent->intent_label}");
1017 1592 continue;
1018 1593 }
1019 1594
1020 1595 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
@@ -1019,38 +1594,61 @@
1019 1594
1020 1595 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1021 1596 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1022 1597
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 + ];
1023 1609
1024 1610 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1025 1611 $highest_similarity = $similarity;
1026 1612 $matched_intent = $intent;
1027 - //error_log("✅ MXCHAT DEBUG: New best match: '{$intent->intent_label}' with similarity {$similarity}");
1028 1613 }
1029 1614 }
1030 - //error_log('📊 MXCHAT DEBUG: End Intent Scores ==================');
1031 1615
1616 + // Mark the triggered action if any
1032 1617 if ($matched_intent) {
1033 - //error_log("🎯 MXCHAT DEBUG: Final Intent Match: '{$matched_intent->intent_label}'");
1034 - //error_log("🔄 MXCHAT DEBUG: Invoking callback: {$matched_intent->callback_function}");
1035 -
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) {
1036 1636 // If the callback is a method on this instance (core callback), call it directly
1037 1637 if (method_exists($this, $matched_intent->callback_function)) {
1038 - //error_log('🔍 MXCHAT DEBUG: Using direct method call for core callback');
1039 1638 $callback_result = call_user_func(
1040 - [$this, $matched_intent->callback_function],
1041 - $message,
1042 - $user_id,
1043 - $session_id,
1044 - $matched_intent,
1045 - $user_context // Add user context
1046 - );
1639 + [$this, $matched_intent->callback_function],
1640 + $message,
1641 + $user_id,
1642 + $session_id,
1643 + $matched_intent,
1644 + $user_context ?? null
1645 + );
1047 1646 } else {
1048 - //error_log('🔍 MXCHAT DEBUG: Using apply_filters for add-on callback');
1049 1647 // Otherwise, use apply_filters for add-on callbacks
1050 1648 $callback_result = apply_filters(
1051 1649 $matched_intent->callback_function,
1052 - false, // default return value
1650 + false,
1053 1651 $message,
1054 1652 $user_id,
1055 1653 $session_id,
1056 1654 $matched_intent
@@ -1056,23 +1654,43 @@
1056 1654 $matched_intent
1057 1655 );
1058 1656 }
1059 1657
1060 - //error_log('🔍 MXCHAT DEBUG: Callback result type: ' . gettype($callback_result));
1658 + // Handle the callback result properly
1061 1659 if ($callback_result !== false) {
1062 - //error_log('✅ MXCHAT DEBUG: Intent handled successfully');
1063 - $this->fallbackResponse = $callback_result;
1064 - 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 + }
1065 1668 }
1066 - //error_log('❌ MXCHAT DEBUG: Callback returned false');
1067 - } else {
1068 - //error_log('❌ MXCHAT DEBUG: No matching intent found');
1069 1669 }
1070 1670
1071 - //error_log('🔍 MXCHAT DEBUG: Intent Check Completed ==================');
1072 1671 return false;
1073 1672 }
1074 1673
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)) {
1680 + return true;
1681 + }
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)) {
1687 + return true;
1688 + }
1689 +
1690 + // Check if the current bot is in the enabled bots list
1691 + return in_array($bot_id, $enabled_bots);
1692 +}
1075 1693
1076 1694 // Helper function to clear PDF and Word document related transients
1077 1695 private function clear_pdf_transients($session_id) {
1078 1696 // PDF transients
@@ -1092,61 +1710,76 @@
1092 1710
1093 1711
1094 1712 //verified good
1095 1713 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1096 - // Log the message safely
1097 - //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1098 -
1099 - // Initiate email capture flow
1100 - $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1101 -
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
1102 1721 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1103 - $this->mxchat_save_chat_message($session_id, 'bot', $response);
1104 -
1105 - // Respond to the user
1106 - wp_send_json(['message' => $response]);
1107 - wp_die();
1722 +
1723 + // Return false to let the AI generate the response
1724 + return false;
1108 1725 }
1109 1726
1110 -//very good
1111 1727 public function mxchat_generate_image($message, $user_id, $session_id) {
1728 + //error_log("Starting image generation for message: " . $message);
1729 +
1112 1730 // Prepare a prompt for DALL-E
1113 1731 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1114 -
1732 +
1115 1733 // Use the existing OpenAI API key
1116 1734 $openai_api_key = sanitize_text_field($this->options['api_key']);
1117 -
1735 +
1118 1736 // Call DALL-E to generate an image
1119 1737 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1120 -
1738 +
1121 1739 // Check if the response contains an image URL
1122 1740 if (isset($image_response['imageUrl'])) {
1123 1741 $image_url = esc_url_raw($image_response['imageUrl']);
1124 -
1742 +
1125 1743 // Construct the HTML with a CSS class instead of inline styles
1126 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));
1127 1760
1128 - $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;
1129 1763 } else {
1130 1764 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1131 - $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 +
1132 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;
1133 1781 }
1134 -
1135 - // Save both text and HTML responses
1136 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
1137 -
1138 - // Prepare the response data
1139 - $response_data = [
1140 - 'message' => $response_text,
1141 - 'html' => $response_html,
1142 - 'image_url' => $image_url ?? '',
1143 - ];
1144 -
1145 - // Send the JSON response
1146 - header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1147 - echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1148 - wp_die();
1149 1782 }
1150 1783 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1151 1784 $api_url = 'https://api.openai.com/v1/images/generations';
1152 1785 $body = json_encode([
@@ -1185,44 +1818,43 @@
1185 1818
1186 1819 /**
1187 1820 * Handle web search requests.
1188 1821 *
1189 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1190 - * 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.
1191 1824 *
1192 1825 * @since 1.0.0
1193 1826 * @param string $message The user's search query.
1194 1827 * @param string $user_id The user identifier.
1195 1828 * @param string $session_id The current session ID.
1196 - * @return void
1829 + * @return array Response array containing text with embedded HTML links
1197 1830 */
1198 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1831 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1199 1832 // Step 1: Interpret and refine the search query
1200 - $refined_search_query = $this->mxchat_interpret_search_query( $message );
1201 -
1202 - if ( empty( $refined_search_query ) ) {
1203 - $this->fallbackResponse = array(
1204 - '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' => ''
1205 1838 );
1206 - return;
1207 1839 }
1208 -
1840 +
1209 1841 // Retrieve and validate API settings
1210 - $options = get_option( 'mxchat_options' );
1211 - $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1212 - $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1213 -
1214 - if ( empty( $api_key ) ) {
1215 - $this->fallbackResponse = array(
1216 - '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' => ''
1217 1850 );
1218 - return;
1219 1851 }
1220 -
1852 +
1221 1853 // Build the API request URL
1222 1854 $api_url = add_query_arg(
1223 1855 array(
1224 - 'q' => rawurlencode( $refined_search_query ),
1856 + 'q' => rawurlencode($refined_search_query),
1225 1857 'count' => $results_count,
1226 1858 'text_decorations' => 'true',
1227 1859 'rich_data' => 'true',
1228 1860 ),
@@ -1227,14 +1859,14 @@
1227 1859 'rich_data' => 'true',
1228 1860 ),
1229 1861 'https://api.search.brave.com/res/v1/web/search'
1230 1862 );
1231 -
1863 +
1232 1864 // Attempt to retrieve cached results first
1233 - $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1234 - $results = get_transient( $transient_key );
1235 -
1236 - if ( false === $results ) {
1865 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
1866 + $results = get_transient($transient_key);
1867 +
1868 + if (false === $results) {
1237 1869 // Fetch new results from the Brave Search API
1238 1870 $response = wp_remote_get(
1239 1871 $api_url,
1240 1872 array(
@@ -1245,162 +1877,98 @@
1245 1877 ),
1246 1878 'timeout' => 10,
1247 1879 )
1248 1880 );
1249 -
1250 - if ( is_wp_error( $response ) ) {
1251 - $this->fallbackResponse = array(
1252 - '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' => ''
1253 1886 );
1254 - return;
1255 1887 }
1256 -
1257 - $results = json_decode( wp_remote_retrieve_body( $response ), true );
1258 -
1259 - if ( json_last_error() !== JSON_ERROR_NONE ) {
1260 - $this->fallbackResponse = array(
1261 - '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' => ''
1262 1895 );
1263 - return;
1264 1896 }
1265 -
1897 +
1266 1898 // Cache results for one hour
1267 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1899 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1268 1900 }
1269 -
1270 - // Process and display results
1271 - if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1272 - $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1273 -
1274 - // Only return HTML (no large text summary)
1275 - $this->fallbackResponse = array(
1276 - '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)
1277 1911 );
1278 -
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 +
1279 1933 // Save to chat history
1280 - $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 + );
1281 1941 } else {
1282 - $this->fallbackResponse = array(
1942 + return array(
1283 1943 'text' => sprintf(
1284 - esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1285 - 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)
1286 1946 ),
1947 + 'html' => ''
1287 1948 );
1288 1949 }
1289 1950 }
1290 1951
1291 -
1952 +//very good
1292 1953 /**
1293 - * Format search results into a natural text summary.
1954 + * Handle image search requests from the chatbot
1294 1955 *
1295 - * @since 1.0.0
1296 - * @param array $results The search results from the API.
1297 - * @param string $query The original search query.
1298 - * @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
1299 1960 */
1300 -private function format_search_results( $results, $query ) {
1301 - $summary = sprintf(
1302 - esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1303 - esc_html( $query )
1304 - ) . "\n\n";
1305 -
1306 - $max_results = min( count( $results ), 3 );
1307 - for ( $i = 0; $i < $max_results; $i++ ) {
1308 - $result = $results[ $i ];
1309 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1310 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1311 -
1312 - // Append title and description to the summary
1313 - $summary .= sprintf(
1314 - "%s\n%s\n\n",
1315 - esc_html( $title ),
1316 - esc_html( $description )
1317 - );
1318 - }
1319 -
1320 - return $summary;
1321 -}
1322 -
1323 -/**
1324 - * Generate HTML markup for search results.
1325 - *
1326 - * @since 1.0.0
1327 - * @param array $results The search results from the API.
1328 - * @param string $query The user-refined query.
1329 - * @return string The HTML markup for displaying the results.
1330 - */
1331 -private function generate_search_results_html( $results, $query ) {
1332 - ob_start();
1333 - ?>
1334 - <div class="mxchat-search-results">
1335 - <?php foreach ( $results as $result ) :
1336 - $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1337 - $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1338 - $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1339 - $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1340 - $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1341 - $domain = parse_url( $url, PHP_URL_HOST );
1342 - ?>
1343 - <div class="mxchat-search-item">
1344 - <div class="mxchat-search-header">
1345 - <?php if ( $favicon ) : ?>
1346 - <img
1347 - src="<?php echo esc_url( $favicon ); ?>"
1348 - class="mxchat-site-icon"
1349 - alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1350 - width="16"
1351 - height="16"
1352 - />
1353 - <?php endif; ?>
1354 - <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1355 - </div>
1356 -
1357 - <div class="mxchat-search-content">
1358 - <h3 class="mxchat-search-title">
1359 - <a href="<?php echo esc_url( $url ); ?>"
1360 - target="_blank"
1361 - rel="noopener noreferrer"
1362 - >
1363 - <?php echo esc_html( $title ); ?>
1364 - </a>
1365 - </h3>
1366 -
1367 - <?php if ( $thumbnail ) : ?>
1368 - <div class="mxchat-search-thumbnail">
1369 - <img
1370 - src="<?php echo esc_url( $thumbnail ); ?>"
1371 - alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1372 - loading="lazy"
1373 - />
1374 - </div>
1375 - <?php endif; ?>
1376 -
1377 - <div class="mxchat-search-description">
1378 - <?php echo esc_html( $description ); ?>
1379 - </div>
1380 - </div>
1381 - </div>
1382 - <?php endforeach; ?>
1383 - </div>
1384 - <?php
1385 - return ob_get_clean();
1386 -}
1387 -
1388 -
1389 -//very good
1390 1961 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1391 -
1392 - // Step 1: Interpret the search query for better results
1962 + // Step 1: Interpret the search query using the user's selected AI model
1393 1963 $refined_search_query = $this->mxchat_interpret_search_query($message);
1394 1964
1395 -
1396 1965 // If no query was interpreted, return a fallback message
1397 1966 if (empty($refined_search_query)) {
1398 - $this->fallbackResponse = [
1967 + return array(
1399 1968 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1400 1969 'html' => "",
1401 - ];
1402 - return;
1970 + );
1403 1971 }
1404 1972
1405 1973 // Brave API URL
1406 1974 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1409,19 +1977,12 @@
1409 1977 $options = get_option('mxchat_options');
1410 1978 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1411 1979
1412 1980 if (empty($api_key)) {
1413 -/*
1414 - if (defined('WP_DEBUG') && WP_DEBUG) {
1415 - error_log("Brave API key is missing.");
1416 - }
1417 -*/
1418 -
1419 - $this->fallbackResponse = [
1981 + return array(
1420 1982 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1421 1983 'html' => "",
1422 - ];
1423 - return;
1984 + );
1424 1985 }
1425 1986
1426 1987 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1427 1988 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1432,16 +1993,8 @@
1432 1993 'count' => $image_count,
1433 1994 'safesearch' => $safe_search,
1434 1995 ], $api_url);
1435 1996
1436 -/*
1437 - // Log the final API URL for the search
1438 - if (defined('WP_DEBUG') && WP_DEBUG) {
1439 - error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1440 - }
1441 -*/
1442 -
1443 -
1444 1997 // Implement caching
1445 1998 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1446 1999 $body = get_transient($transient_key);
1447 2000
@@ -1457,19 +2010,12 @@
1457 2010
1458 2011 $response = wp_remote_get($api_url, $args);
1459 2012
1460 2013 if (is_wp_error($response)) {
1461 -/*
1462 - if (defined('WP_DEBUG') && WP_DEBUG) {
1463 - error_log("Brave Image API request failed: " . $response->get_error_message());
1464 - }
1465 -*/
1466 -
1467 - $this->fallbackResponse = [
2014 + return array(
1468 2015 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1469 2016 'html' => "",
1470 - ];
1471 - return;
2017 + );
1472 2018 }
1473 2019
1474 2020 $body = json_decode(wp_remote_retrieve_body($response), true);
1475 2021 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1477,10 +2023,16 @@
1477 2023
1478 2024 // Process the API response
1479 2025 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1480 2026 $html_output = '<div class="mxchat-image-gallery">';
1481 -
1482 - 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];
1483 2035 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1484 2036 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1485 2037 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1486 2038
@@ -1494,47 +2046,95 @@
1494 2046 }
1495 2047
1496 2048 $html_output .= '</div>';
1497 2049
1498 - $this->fallbackResponse = [
1499 - 'text' => "",
1500 - 'html' => $html_output,
1501 - ];
1502 -
1503 - // 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);
1504 2055 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1505 2056
2057 + // Return the combined response
2058 + return array(
2059 + 'text' => $response_text,
2060 + 'html' => $html_output,
2061 + );
1506 2062 } else {
1507 -/*
1508 - if (defined('WP_DEBUG') && WP_DEBUG) {
1509 - error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1510 - }
1511 -*/
1512 -
1513 - $this->fallbackResponse = [
1514 - '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,
1515 2070 'html' => "",
1516 - ];
2071 + );
1517 2072 }
1518 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 + */
1519 2081 public function mxchat_interpret_search_query($user_query) {
1520 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');
1521 -
1522 - // Retrieve OpenAI API key using 'api_key' as the option key
1523 - $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1524 -
1525 - /*
1526 - // Log the API key check, without exposing the key
1527 - if (defined('WP_DEBUG') && WP_DEBUG) {
1528 - 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);
1529 2130 }
1530 - */
2131 +}
1531 2132
1532 - if (empty($api_key)) {
1533 - //error_log("OpenAI API key is missing.");
1534 - return sanitize_text_field($user_query); // Default to the original query if API key is missing
1535 - }
1536 -
2133 +/**
2134 + * Interpret query using OpenAI models
2135 + */
2136 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1537 2137 $url = 'https://api.openai.com/v1/chat/completions';
1538 2138 $args = [
1539 2139 'headers' => [
1540 2140 'Authorization' => 'Bearer ' . $api_key,
@@ -1540,9 +2140,9 @@
1540 2140 'Authorization' => 'Bearer ' . $api_key,
1541 2141 'Content-Type' => 'application/json',
1542 2142 ],
1543 2143 'body' => wp_json_encode([
1544 - 'model' => 'gpt-3.5-turbo',
2144 + 'model' => $model,
1545 2145 'messages' => [
1546 2146 ['role' => 'system', 'content' => $system_prompt],
1547 2147 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1548 2148 ],
@@ -1549,166 +2149,178 @@
1549 2149 'temperature' => 0.2,
1550 2150 'max_tokens' => 20,
1551 2151 ]),
1552 2152 'method' => 'POST',
2153 + 'timeout' => 15,
1553 2154 ];
1554 2155
1555 2156 $response = wp_remote_post($url, $args);
1556 -
1557 2157 if (is_wp_error($response)) {
1558 - //error_log("OpenAI request failed: " . $response->get_error_message());
1559 - return sanitize_text_field($user_query); // Fallback to the original query if there's an error
2158 + return sanitize_text_field($user_query);
1560 2159 }
1561 2160
1562 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 +}
1563 2166
1564 - // Check for a valid response and sanitize output
1565 - if (isset($body['choices'][0]['message']['content'])) {
1566 - $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 + ];
1567 2191
1568 - /*
1569 - // Log the interpreted query for debugging
1570 - if (defined('WP_DEBUG') && WP_DEBUG) {
1571 - error_log("Interpreted search query: " . $interpreted_query);
1572 - }
1573 - */
2192 + $response = wp_remote_post($url, $args);
2193 + if (is_wp_error($response)) {
2194 + return sanitize_text_field($user_query);
2195 + }
1574 2196
1575 - return $interpreted_query;
1576 - } else {
1577 - //error_log("Unexpected API response format: " . print_r($body, true));
1578 - return sanitize_text_field($user_query);
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']));
1579 2200 }
2201 +
2202 + return sanitize_text_field($user_query);
1580 2203 }
1581 2204
1582 -
1583 -
1584 -private function find_product_in_message($message) {
1585 - global $wpdb;
1586 -
1587 - // Get embedding for the search query
1588 - $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1589 - if (!is_array($query_embedding)) {
1590 - 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);
1591 2239 }
1592 -
1593 - // Get relevant content as string
1594 - $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1595 - if (empty($relevant_content)) {
1596 - // Return null to indicate no results and set fallback response
1597 - $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');
1598 - 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']));
1599 2244 }
2245 +
2246 + return sanitize_text_field($user_query);
2247 +}
1600 2248
1601 - // Extract product URLs from the content
1602 - preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1603 -
1604 - if (!empty($matches[0])) {
1605 - // Try each URL found
1606 - foreach ($matches[0] as $url) {
1607 - // Clean the URL
1608 - $url = rtrim($url, '/."\']');
1609 -
1610 - // Get the product slug
1611 - $path = parse_url($url, PHP_URL_PATH);
1612 - $slug = basename(rtrim($path, '/'));
1613 -
1614 - // Find product by slug
1615 - $args = array(
1616 - 'post_type' => 'product',
1617 - 'post_status' => 'publish',
1618 - 'name' => $slug,
1619 - 'posts_per_page' => 1
1620 - );
1621 -
1622 - $products = get_posts($args);
1623 -
1624 - if (!empty($products)) {
1625 - $product_id = $products[0]->ID;
1626 - $product = wc_get_product($product_id);
1627 -
1628 - if ($product && $product->is_purchasable()) {
1629 - return $product_id;
1630 - }
1631 - }
1632 - }
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);
1633 2276 }
1634 -
1635 - // Fallback: Look for product names in the content
1636 - $products = wc_get_products([
1637 - 'status' => 'publish',
1638 - 'limit' => -1,
1639 - 'return' => 'all'
1640 - ]);
1641 -
1642 - foreach ($products as $product) {
1643 - $name = $product->get_name();
1644 - if (stripos($relevant_content, $name) !== false) {
1645 - if ($product->is_purchasable()) {
1646 - return $product->get_id();
1647 - }
1648 - }
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']));
1649 2281 }
1650 -
1651 - // If no product is found after all checks, set the fallback response
1652 - $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1653 - return null;
2282 +
2283 + return sanitize_text_field($user_query);
1654 2284 }
1655 2285
1656 -// New method to handle intent responses
1657 -private function generate_intent_response($context_content, $session_id) {
1658 - // Convert the context array to a structured string for the AI
1659 - $context_string = $this->format_intent_context($context_content);
1660 -
1661 - // Generate AI response using the context
1662 - $response = $this->mxchat_generate_response(
1663 - $context_string,
1664 - $this->options['api_key'],
1665 - $this->options['xai_api_key'],
1666 - $this->options['claude_api_key'],
1667 - $this->options['deepseek_api_key'],
1668 - $this->mxchat_fetch_conversation_history_for_ai($session_id)
1669 - );
1670 -
1671 - $this->fallbackResponse['text'] = $response;
1672 - return true;
1673 -}
1674 -// Helper method to format intent context
1675 -private function format_intent_context($context) {
1676 - $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1677 -
1678 - switch ($context['intent']) {
1679 - case 'add_to_cart':
1680 - if ($context['status'] === 'success') {
1681 - $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1682 - $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1683 - $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1684 - $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1685 - $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1686 - } else {
1687 - $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1688 - $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1689 - switch ($context['reason']) {
1690 - case 'woocommerce_not_available':
1691 - $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1692 - break;
1693 - case 'no_product_context':
1694 - $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1695 - break;
1696 - case 'product_not_found':
1697 - $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1698 - break;
1699 - case 'add_to_cart_failed':
1700 - $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1701 - break;
1702 - }
1703 - }
1704 - 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);
1705 2313 }
1706 -
1707 - return $context_string;
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']));
2318 + }
2319 +
2320 + return sanitize_text_field($user_query);
1708 2321 }
1709 2322
1710 -
1711 2323 //very good
1712 2324 private function add_email_to_loops($email) {
1713 2325 // Sanitize the email
1714 2326 $email = sanitize_email($email);
@@ -1792,95 +2404,169 @@
1792 2404
1793 2405 // Default to proceeding with conversation if no specific PDF action is needed
1794 2406 $this->fallbackResponse['text'] = '';
1795 2407 }
2408 +
2409 +
2410 +/**
2411 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
2412 + */
1796 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 +
1797 2459 $upload_dir = wp_upload_dir();
1798 2460 $temp_file = null;
1799 -
2461 +
1800 2462 try {
1801 - // Handle URL vs local file
2463 + // Your existing basic processing code here...
2464 + // (I'll include the key parts with debug logging)
2465 +
1802 2466 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1803 - // Validate and download the file from URL
1804 - $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
1805 - $response = wp_remote_get($pdf_source, ['timeout' => 60]);
1806 -
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 +
1807 2474 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1808 - //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);
1809 2477 return false;
1810 2478 }
1811 -
2479 +
1812 2480 file_put_contents($temp_file, wp_remote_retrieve_body($response));
1813 -
1814 - // Validate that the downloaded file is a PDF
1815 - $mime_type = mime_content_type($temp_file);
1816 - if ($mime_type !== 'application/pdf') {
1817 - //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
1818 - unlink($temp_file);
1819 - return false;
1820 - }
2481 + //error_log("✅ PDF downloaded successfully");
1821 2482 } else {
1822 - // For local files, use the provided path directly
1823 2483 $temp_file = $pdf_source;
2484 + //error_log("Using local PDF file: " . $temp_file);
1824 2485 }
1825 -
1826 - // Parse and process the PDF
2486 +
2487 + // Parse PDF
2488 + //error_log("Parsing PDF with basic parser...");
1827 2489 $parser = new \Smalot\PdfParser\Parser();
1828 2490 $pdf = $parser->parseFile($temp_file);
1829 2491 $pages = $pdf->getPages();
1830 -
2492 +
2493 + //error_log("PDF contains " . count($pages) . " pages");
2494 +
1831 2495 if (count($pages) > $max_pages) {
1832 - //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
1833 - 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) {
1834 2498 unlink($temp_file);
1835 2499 }
1836 - return esc_html__('too_many_pages', 'mxchat');
2500 + return 'too_many_pages';
1837 2501 }
1838 -
2502 +
1839 2503 $embeddings = [];
2504 + $processed_pages = 0;
2505 +
1840 2506 foreach ($pages as $page_number => $page) {
1841 2507 $text = $page->getText();
1842 -
1843 - // Ensure text is non-empty before generating embeddings
2508 +
1844 2509 if (empty(trim($text))) {
1845 - //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
2510 + //error_log("Skipping empty page: " . ($page_number + 1));
1846 2511 continue;
1847 2512 }
1848 -
2513 +
2514 + $text = $this->mxchat_clean_text($text);
2515 +
1849 2516 $embedding = $this->mxchat_generate_embedding(
1850 - esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2517 + __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
1851 2518 $this->options['api_key']
1852 2519 );
1853 -
2520 +
1854 2521 if ($embedding) {
1855 2522 $embeddings[] = [
1856 2523 'page_number' => $page_number + 1,
1857 2524 'embedding' => $embedding,
1858 2525 'text' => $text,
2526 + 'enhanced' => false, // CLEARLY MARK AS BASIC
2527 + 'processing_method' => 'basic_pdf_parser'
1859 2528 ];
1860 - } else {
1861 - //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
2529 + $processed_pages++;
1862 2530 }
1863 2531 }
1864 -
1865 - // Clean up downloaded file if it was from URL
1866 - 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)) {
1867 2537 unlink($temp_file);
1868 2538 }
1869 -
2539 +
2540 + //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
1870 2541 return $embeddings;
1871 -
2542 +
1872 2543 } catch (\Exception $e) {
1873 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
1874 -
1875 - // Cleanup in case of exception
2544 + //error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
1876 2545 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1877 2546 unlink($temp_file);
1878 2547 }
1879 -
2548 + //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
1880 2549 return false;
1881 2550 }
1882 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 +
1883 2569 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
1884 2570 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
1885 2571
1886 2572 $most_relevant = null;
@@ -2038,10 +2724,8 @@
2038 2724 'new_messages' => array_values($new_messages)
2039 2725 ]);
2040 2726 wp_die();
2041 2727 }
2042 -
2043 -
2044 2728 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2045 2729 // First check if live agents are available
2046 2730 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2047 2731 if ($live_agent_available !== 'on') {
@@ -2060,18 +2744,101 @@
2060 2744 ]);
2061 2745 wp_die();
2062 2746 }
2063 2747
2064 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2065 - if (empty($slack_webhook_url)) {
2748 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2749 +
2750 + if (empty($slack_bot_token)) {
2066 2751 return false;
2067 2752 }
2068 2753
2069 - // 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
2070 2837 $history = get_option("mxchat_history_{$session_id}", []);
2071 - $recent_history = array_slice($history, -5); // Get last 5 messages
2838 + $recent_history = array_slice($history, -5);
2072 2839
2073 - // Format conversation history
2840 + // Format conversation context
2074 2841 $conversation_context = "";
2075 2842 if (!empty($recent_history)) {
2076 2843 $conversation_context = "*Recent Conversation:*\n";
2077 2844 foreach ($recent_history as $hist_message) {
@@ -2082,83 +2849,32 @@
2082 2849 }
2083 2850
2084 2851 update_option("mxchat_mode_{$session_id}", 'agent');
2085 2852
2086 - $webhook_data = [
2087 - 'blocks' => [
2088 - [
2089 - 'type' => 'header',
2090 - 'text' => [
2091 - 'type' => 'plain_text',
2092 - 'text' => '🔔 New Live Agent Request',
2093 - 'emoji' => true
2094 - ]
2095 - ],
2096 - [
2097 - 'type' => 'section',
2098 - 'fields' => [
2099 - [
2100 - 'type' => 'mrkdwn',
2101 - 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2102 - ],
2103 - [
2104 - 'type' => 'mrkdwn',
2105 - 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2106 - ]
2107 - ]
2108 - ]
2109 - ]
2110 - ];
2111 -
2112 - // 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 +
2113 2858 if (!empty($conversation_context)) {
2114 - $webhook_data['blocks'][] = [
2115 - 'type' => 'section',
2116 - 'text' => [
2117 - 'type' => 'mrkdwn',
2118 - 'text' => $conversation_context
2119 - ]
2120 - ];
2859 + $channel_message .= $conversation_context;
2121 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_";
2122 2864
2123 - // Add the current message
2124 - $webhook_data['blocks'][] = [
2125 - 'type' => 'section',
2126 - 'text' => [
2127 - 'type' => 'mrkdwn',
2128 - 'text' => sprintf('*Current Message:*\n%s', $message)
2129 - ]
2130 - ];
2131 -
2132 - // Add the reply button
2133 - $webhook_data['blocks'][] = [
2134 - 'type' => 'actions',
2135 - 'elements' => [
2136 - [
2137 - 'type' => 'button',
2138 - 'text' => [
2139 - 'type' => 'plain_text',
2140 - 'text' => '✍️ Reply',
2141 - 'emoji' => true
2142 - ],
2143 - 'value' => $session_id,
2144 - 'action_id' => 'reply_to_user',
2145 - 'style' => 'primary'
2146 - ]
2147 - ]
2148 - ];
2149 -
2150 - $response = wp_remote_post($slack_webhook_url, [
2151 - 'body' => json_encode($webhook_data),
2865 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2152 2866 'headers' => [
2153 2867 'Content-Type' => 'application/json',
2868 + 'Authorization' => 'Bearer ' . $slack_bot_token
2154 2869 ],
2870 + 'body' => json_encode([
2871 + 'channel' => $channel_id,
2872 + 'text' => $channel_message,
2873 + 'mrkdwn' => true
2874 + ])
2155 2875 ]);
2156 2876
2157 - if (is_wp_error($response)) {
2158 - return false;
2159 - }
2160 -
2161 2877 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2162 2878 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2163 2879
2164 2880 $this->fallbackResponse = [
@@ -2177,79 +2893,145 @@
2177 2893 'fallbackResponse' => $this->fallbackResponse
2178 2894 ]);
2179 2895 wp_die();
2180 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 +}
2181 3011 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2182 - $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}", '');
2183 3014
2184 - if (empty($slack_webhook_url)) {
2185 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
3015 + if (empty($slack_bot_token) || empty($channel_id)) {
2186 3016 return false;
2187 3017 }
2188 3018
2189 - $webhook_data = [
2190 - 'blocks' => [
2191 - [
2192 - 'type' => 'header',
2193 - 'text' => [
2194 - 'type' => 'plain_text',
2195 - 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2196 - 'emoji' => true
2197 - ]
2198 - ],
2199 - [
2200 - 'type' => 'section',
2201 - 'fields' => [
2202 - [
2203 - 'type' => 'mrkdwn',
2204 - 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2205 - ],
2206 - [
2207 - 'type' => 'mrkdwn',
2208 - 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2209 - ]
2210 - ]
2211 - ],
2212 - [
2213 - 'type' => 'section',
2214 - 'text' => [
2215 - 'type' => 'mrkdwn',
2216 - 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2217 - ]
2218 - ],
2219 - [
2220 - 'type' => 'actions',
2221 - 'elements' => [
2222 - [
2223 - 'type' => 'button',
2224 - 'text' => [
2225 - 'type' => 'plain_text',
2226 - 'text' => esc_html__('✍️ Reply', 'mxchat'),
2227 - 'emoji' => true
2228 - ],
2229 - 'value' => $session_id,
2230 - 'action_id' => 'reply_to_user',
2231 - 'style' => 'primary'
2232 - ]
2233 - ]
2234 - ]
2235 - ]
2236 - ];
3019 + $user_message = "💬 *User:* {$message}";
2237 3020
2238 - $response = wp_remote_post($slack_webhook_url, [
2239 - 'body' => json_encode($webhook_data),
3021 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2240 3022 'headers' => [
2241 3023 'Content-Type' => 'application/json',
3024 + 'Authorization' => 'Bearer ' . $slack_bot_token
2242 3025 ],
3026 + 'body' => json_encode([
3027 + 'channel' => $channel_id,
3028 + 'text' => $user_message,
3029 + 'mrkdwn' => true
3030 + ])
2243 3031 ]);
2244 3032
2245 - if (is_wp_error($response)) {
2246 - //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2247 - return false;
2248 - }
2249 -
2250 - //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2251 - return true;
3033 + return !is_wp_error($response);
2252 3034 }
2253 3035 public function handle_slack_interaction(WP_REST_Request $request) {
2254 3036 //error_log('Received Slack interaction');
2255 3037
@@ -2337,17 +3119,16 @@
2337 3119
2338 3120 // Default acknowledgment
2339 3121 return new WP_REST_Response(['ok' => true]);
2340 3122 }
2341 -
2342 3123 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2343 3124 //error_log('Received agent response request');
2344 3125 //error_log('Request data: ' . print_r($request->get_params(), true));
2345 - // error_log('Raw body: ' . file_get_contents('php://input'));
3126 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2346 3127
2347 3128 // Get the data from Slack's slash command format
2348 3129 $command_text = $request->get_param('text');
2349 - // error_log('Command text: ' . $command_text);
3130 + // //error_log('Command text: ' . $command_text);
2350 3131
2351 3132 if (empty($command_text)) {
2352 3133 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2353 3134 return new WP_REST_Response([
@@ -2372,9 +3153,9 @@
2372 3153 // Save the message
2373 3154 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2374 3155
2375 3156 if (!$message_id) {
2376 - // error_log('Failed to save agent message');
3157 + // //error_log('Failed to save agent message');
2377 3158 return new WP_REST_Response([
2378 3159 'error' => esc_html__('Failed to save message', 'mxchat')
2379 3160 ], 500);
2380 3161 }
@@ -2384,29 +3165,141 @@
2384 3165 'response_type' => 'in_channel',
2385 3166 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2386 3167 ], 200);
2387 3168 }
2388 -
2389 -
2390 3169 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2391 - //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2392 -
2393 - // Just update mode to AI
3170 + // Update mode to AI
2394 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 +}
2395 3187
2396 - // Initialize states
2397 - $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2398 - $this->productCardHtml = '';
2399 -
2400 - // Set the response message
2401 - $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2402 -
2403 - 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]);
2404 3300 }
2405 3301
2406 -
2407 -
2408 -
2409 3302 // For the word upload handler
2410 3303 public function mxchat_handle_word_upload() {
2411 3304 // Delegate to word handler
2412 3305 $this->word_handler->mxchat_handle_word_upload();
@@ -2429,21 +3322,102 @@
2429 3322 return MxChat_User::mxchat_get_user_identifier();
2430 3323 }
2431 3324
2432 3325 private function mxchat_generate_embedding($text, $api_key) {
2433 - $endpoint = 'https://api.openai.com/v1/embeddings';
2434 -
2435 - $body = wp_json_encode([
2436 - 'input' => $text,
2437 - 'model' => 'text-embedding-ada-002'
2438 - ]);
2439 -
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
2440 3417 $args = [
2441 - 'body' => $body,
2442 - 'headers' => [
2443 - 'Content-Type' => 'application/json',
2444 - 'Authorization' => 'Bearer ' . $api_key,
2445 - ],
3418 + 'body' => wp_json_encode($request_body),
3419 + 'headers' => $headers,
2446 3420 'timeout' => 60,
2447 3421 'redirection' => 5,
2448 3422 'blocking' => true,
2449 3423 'httpversion' => '1.0',
@@ -2448,63 +3422,177 @@
2448 3422 'blocking' => true,
2449 3423 'httpversion' => '1.0',
2450 3424 'sslverify' => true,
2451 3425 ];
2452 -
3426 +
3427 + // Make the request
2453 3428 $response = wp_remote_post($endpoint, $args);
2454 -
3429 +
3430 + // Handle WordPress errors
2455 3431 if (is_wp_error($response)) {
2456 - 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 + ];
2457 3438 }
2458 -
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 +
2459 3493 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2460 -
2461 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
2462 - 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 + }
2463 3507 } else {
2464 - 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 + }
2465 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 + ];
2466 3525 }
3526 +}
2467 3527
2468 3528
2469 -private function mxchat_find_relevant_content($user_embedding) {
2470 - //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");
2471 3546
2472 - // Retrieve the add-on settings from the database.
2473 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
2474 -
2475 - // Determine whether Pinecone is enabled.
2476 - // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
2477 - $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
2478 -
2479 - //error_log('Pinecone enabled flag: ' . $use_pinecone);
2480 -
2481 - if ($use_pinecone === 1) {
2482 - //error_log('MXChat Vector Search: Using Pinecone database');
2483 - 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);
2484 3549 } else {
2485 - //error_log('MXChat Vector Search: Using WordPress database');
2486 - return $this->find_relevant_content_wordpress($user_embedding);
3550 + return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
2487 3551 }
2488 3552 }
2489 3553
2490 -private function find_relevant_content_wordpress($user_embedding) {
3554 +private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
2491 3555 global $wpdb;
2492 3556 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2493 - $cache_key = 'mxchat_system_prompt_embeddings';
3557 + $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id; // Bot-specific cache key
2494 3558 $batch_size = 500;
2495 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 +
2496 3573 // Retrieve embeddings from cache or database
2497 3574 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2498 3575 if ($embeddings === false) {
3576 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
2499 3577 $embeddings = [];
2500 3578 $offset = 0;
2501 3579
2502 - // Load in batches and build cache
2503 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 +
2504 3591 $query = $wpdb->prepare(
2505 - "SELECT id, embedding_vector
3592 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
2506 3593 FROM {$system_prompt_table}
3594 + WHERE 1=1 {$bot_filter}
2507 3595 LIMIT %d OFFSET %d",
2508 3596 $batch_size,
2509 3597 $offset
2510 3598 );
@@ -2515,62 +3603,132 @@
2515 3603 }
2516 3604
2517 3605 $embeddings = array_merge($embeddings, $batch);
2518 3606 $offset += $batch_size;
2519 -
2520 - // Free memory
2521 3607 unset($batch);
2522 -
2523 3608 } while (true);
2524 3609
2525 3610 if (empty($embeddings)) {
2526 - return ''; // Return an empty string if no embeddings found
3611 + return '';
2527 3612 }
3613 +
3614 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
2528 3615 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2529 3616 }
2530 3617
2531 - // 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 = [];
2532 3630 $relevant_results = [];
2533 - // Iterate through embeddings to calculate similarity
3631 +
2534 3632 foreach ($embeddings as $embedding) {
2535 3633 $database_embedding = $embedding->embedding_vector
2536 3634 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2537 3635 : null;
3636 +
2538 3637 if (is_array($database_embedding) && is_array($user_embedding)) {
2539 3638 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2540 - $relevant_results[] = [
2541 - 'id' => $embedding->id,
2542 - '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
2543 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 + }
2544 3674 }
2545 - // Free memory
3675 +
2546 3676 unset($database_embedding);
2547 3677 }
2548 3678
2549 - // Retrieve the similarity threshold
2550 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2551 -
2552 - // Filter and sort relevant results by similarity
2553 - $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2554 - 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'];
2555 3682 });
3683 +
3684 + // Sort relevant results by similarity (highest first)
2556 3685 usort($relevant_results, function ($a, $b) {
2557 3686 return $b['similarity'] <=> $a['similarity'];
2558 3687 });
2559 -
2560 - // Limit to the top 5 results
3688 +
3689 + // Get top 5 results for actual content (standard approach)
2561 3690 $top_results = array_slice($relevant_results, 0, 5);
2562 -
2563 - // 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
2564 3710 $content = '';
2565 -
2566 - // Fetch and combine content for the top results
2567 - 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 +
2568 3721 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2569 - // 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)
2570 3728 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2571 3729 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2572 - "SELECT article_content FROM {$system_prompt_table}
3730 + "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
2573 3731 WHERE id IN (
2574 3732 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
2575 3733 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
2576 3734 )",
@@ -2576,52 +3734,116 @@
2576 3734 )",
2577 3735 $result['id'],
2578 3736 $result['id']
2579 3737 ));
2580 - // Add previous content if it exists
3738 +
3739 + // Check role access for surrounding content too
2581 3740 if (!empty($surrounding_content[0])) {
2582 - $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 + }
2583 3747 }
2584 - // Add the main chunk content
2585 - $content .= $chunk_content . "\n\n";
2586 - // Add next content if it exists
3748 +
2587 3749 if (!empty($surrounding_content[1])) {
2588 - $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 + }
2589 3756 }
2590 - } else {
2591 - // For non-PDF content, add directly
2592 - $content .= $chunk_content . "\n\n";
2593 3757 }
2594 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 + }
2595 3771
2596 3772 return trim($content);
2597 3773 }
2598 -/**
2599 - * Find relevant content in Pinecone vector database
2600 - */
2601 -private function find_relevant_content_pinecone($user_embedding) {
2602 - $options = get_option('mxchat_pinecone_addon_options', array());
2603 - $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2604 - $host = $options['mxchat_pinecone_host'] ?? '';
2605 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 +
2606 3807 if (empty($host) || empty($api_key)) {
2607 - //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'));
2608 3811 return '';
2609 3812 }
2610 -
2611 - // Get similarity threshold from WordPress settings
2612 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2613 -
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 +
2614 3827 // Prepare the query request for Pinecone
2615 3828 $api_endpoint = "https://{$host}/query";
2616 -
3829 +
2617 3830 $request_body = array(
2618 3831 'vector' => $user_embedding,
2619 - 'topK' => 5,
3832 + 'topK' => 20, // Request more to get good testing data
2620 3833 'includeMetadata' => true,
2621 3834 'includeValues' => true
2622 3835 );
2623 -
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 +
2624 3846 $response = wp_remote_post($api_endpoint, array(
2625 3847 'headers' => array(
2626 3848 'Api-Key' => $api_key,
2627 3849 'accept' => 'application/json',
@@ -2629,46 +3851,197 @@
2629 3851 ),
2630 3852 'body' => wp_json_encode($request_body),
2631 3853 'timeout' => 30
2632 3854 ));
2633 -
3855 +
2634 3856 if (is_wp_error($response)) {
2635 - //error_log('Pinecone query error: ' . $response->get_error_message());
3857 + error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
2636 3858 return '';
2637 3859 }
2638 -
3860 +
2639 3861 $response_code = wp_remote_retrieve_response_code($response);
3862 + error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
3863 +
2640 3864 if ($response_code !== 200) {
2641 - //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));
2642 3867 return '';
2643 3868 }
2644 -
2645 - $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 +
2646 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)));
2647 3889 return '';
2648 3890 }
2649 -
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 +
2650 3905 // Initialize the final content
2651 3906 $content = '';
2652 -
2653 - // Process each match
2654 - 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) {
2655 3912 // Skip if similarity is below threshold
2656 3913 if ($match['score'] < $similarity_threshold) {
2657 3914 continue;
2658 3915 }
2659 -
2660 - if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
2661 - // Add content with citation
2662 - $content .= $match['metadata']['text'] . "\n";
2663 - $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
3916 +
3917 + // Limit to top 5 matches above threshold
3918 + if ($matches_used >= 5) {
3919 + break;
2664 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 + }
2665 3944 }
2666 -
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 +
2667 3998 return trim($content);
2668 3999 }
2669 4000
2670 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 +
2671 4044 private function mxchat_find_relevant_products($user_embedding) {
2672 4045 //error_log('MXChat Vector Search: Starting product search...');
2673 4046
2674 4047 // Retrieve the add-on settings from the database
@@ -2686,9 +4059,8 @@
2686 4059 //error_log('MXChat Vector Search: Using WordPress database for products');
2687 4060 return $this->find_relevant_products_wordpress($user_embedding);
2688 4061 }
2689 4062 }
2690 -
2691 4063 private function find_relevant_products_wordpress($user_embedding) {
2692 4064 global $wpdb;
2693 4065 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2694 4066 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -2763,9 +4135,9 @@
2763 4135
2764 4136 return trim($content);
2765 4137 }
2766 4138
2767 -// Modified search function with correct filter syntax
4139 +
2768 4140 private function find_relevant_products_pinecone($user_embedding) {
2769 4141 //error_log('Starting Pinecone product search...');
2770 4142
2771 4143 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -2862,321 +4234,1169 @@
2862 4234
2863 4235 return null;
2864 4236 }
2865 4237
2866 -// Function definition
2867 -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') {
2868 4278 try {
2869 4279 if (!$relevant_content) {
2870 - 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;
2871 4292 }
2872 -
4293 +
2873 4294 // Ensure conversation_history is an array
2874 4295 if (!is_array($conversation_history)) {
2875 4296 $conversation_history = array();
2876 4297 }
4298 +
2877 4299
2878 - // Get selected model with default fallback
2879 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
2880 -
2881 4300 // Extract model prefix to determine the provider
2882 4301 $model_parts = explode('-', $selected_model);
2883 4302 $provider = strtolower($model_parts[0]);
2884 -
4303 +
2885 4304 // Handle model selection based on provider prefix
2886 4305 switch ($provider) {
2887 - case 'claude':
2888 - if (empty($claude_api_key)) {
2889 - 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;
2890 4316 }
2891 - return $this->mxchat_generate_response_claude(
4317 + $response = $this->mxchat_generate_response_gemini(
2892 4318 $selected_model,
2893 - $claude_api_key,
4319 + $gemini_api_key,
2894 4320 $conversation_history,
2895 4321 $relevant_content
2896 4322 );
2897 -
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 +
2898 4355 case 'grok':
2899 4356 if (empty($xai_api_key)) {
2900 - 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;
2901 4365 }
2902 - return $this->mxchat_generate_response_xai(
2903 - $selected_model,
2904 - $xai_api_key,
2905 - $conversation_history,
2906 - $relevant_content
2907 - );
2908 -
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 +
2909 4385 case 'deepseek':
2910 4386 if (empty($deepseek_api_key)) {
2911 - 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;
2912 4395 }
2913 - return $this->mxchat_generate_response_deepseek(
2914 - $selected_model,
2915 - $deepseek_api_key,
2916 - $conversation_history,
2917 - $relevant_content
2918 - );
2919 -
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 +
2920 4415 case 'gpt':
4416 + case 'o1':
2921 4417 if (empty($api_key)) {
2922 - 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;
2923 4426 }
2924 - return $this->mxchat_generate_response_openai(
2925 - $selected_model,
2926 - $api_key,
2927 - $conversation_history,
2928 - $relevant_content
2929 - );
2930 -
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 +
2931 4446 default:
2932 4447 // Default to OpenAI for custom models or unrecognized prefixes
2933 4448 if (empty($api_key)) {
2934 - 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;
2935 4457 }
2936 - return $this->mxchat_generate_response_openai(
2937 - $selected_model,
2938 - $api_key,
2939 - $conversation_history,
2940 - $relevant_content
2941 - );
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;
2942 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 +
2943 4492 } catch (Exception $e) {
2944 4493 //error_log('MXChat Error: ' . $e->getMessage());
2945 - return sprintf(
2946 - esc_html__('An error occurred: %s', 'mxchat'),
2947 - esc_html($e->getMessage())
2948 - );
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;
2949 4507 }
2950 4508 }
2951 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 + }
2952 4521
2953 -private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
2954 - // Ensure conversation_history is an array
2955 - if (!is_array($conversation_history)) {
2956 - $conversation_history = array();
2957 - }
4522 + // Format conversation history for OpenAI
4523 + $formatted_conversation = array();
2958 4524
2959 - // Get system prompt instructions from options
2960 - $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 + );
2961 4529
2962 - // Create a new array for the formatted conversation
2963 - $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 + }
2964 4545
2965 - // Add system message first
2966 - $formatted_conversation[] = array(
2967 - 'role' => 'system',
2968 - 'content' => $system_prompt_instructions . " " . $relevant_content
2969 - );
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 + }
2970 4570
2971 - // Add the rest of the conversation history
2972 - foreach ($conversation_history as $message) {
2973 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
2974 - $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 + ]);
2975 4578
2976 - // Convert roles to supported format
2977 - if ($role === 'bot' || $role === 'agent') {
2978 - $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;
2979 4603 }
2980 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
2981 - $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 + }
2982 4644 }
2983 -
2984 - $formatted_conversation[] = array(
2985 - 'role' => $role,
2986 - '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
2987 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;
2988 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;
2989 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 + }
2990 4722
2991 - $body = json_encode([
2992 - 'model' => $selected_model,
2993 - 'messages' => $formatted_conversation,
2994 - 'temperature' => 0.8,
2995 - 'stream' => false
2996 - ]);
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 + }
2997 4734
2998 - $args = [
2999 - 'body' => $body,
3000 - 'headers' => [
3001 - 'Content-Type' => 'application/json',
3002 - 'Authorization' => 'Bearer ' . $deepseek_api_key,
3003 - ],
3004 - 'timeout' => 60,
3005 - 'redirection' => 5,
3006 - 'blocking' => true,
3007 - 'httpversion' => '1.0',
3008 - 'sslverify' => true,
3009 - ];
4735 + // Ensure content field exists
4736 + if (!isset($message['content']) || empty($message['content'])) {
4737 + $message['content'] = '';
4738 + }
3010 4739
3011 - $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 + }
3012 4743
3013 - if (is_wp_error($response)) {
3014 - //error_log('DeepSeek API Error: ' . $response->get_error_message());
3015 - return "Sorry, there was an error processing your request.";
3016 - }
4744 + // Add relevant content as the latest user message
4745 + $conversation_history[] = [
4746 + 'role' => 'user',
4747 + 'content' => $relevant_content
4748 + ];
3017 4749
3018 - $response_body = wp_remote_retrieve_body($response);
3019 - $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 + ]);
3020 4759
3021 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3022 - return trim($decoded_response['choices'][0]['message']['content']);
3023 - } else {
3024 - //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3025 - return "Sorry, I couldn't process that request.";
3026 - }
3027 -}
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 + }
3028 4790
3029 -private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3030 - // Ensure conversation_history is an array
3031 - if (!is_array($conversation_history)) {
3032 - $conversation_history = array();
3033 - }
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);
3034 4804
3035 - // Get system prompt instructions from options
3036 - $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
3037 4808
3038 - // Create a new array for the formatted conversation
3039 - $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);
3040 4828
3041 - // Add system message first
3042 - $formatted_conversation[] = array(
3043 - 'role' => 'system',
3044 - 'content' => $system_prompt_instructions . " " . $relevant_content
3045 - );
4829 + foreach ($lines as $line) {
4830 + if (trim($line) === '') {
4831 + continue;
4832 + }
3046 4833
3047 - // Add the rest of the conversation history
3048 - foreach ($conversation_history as $message) {
3049 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3050 - $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 + }
3051 4839
3052 - // Convert roles to supported format
3053 - if ($role === 'bot' || $role === 'agent') {
3054 - $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 + }
3055 4873 }
3056 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3057 - $role = 'user';
3058 - }
3059 4874
3060 - $formatted_conversation[] = array(
3061 - 'role' => $role,
3062 - 'content' => $message['content']
3063 - );
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));
3064 4884 }
3065 - }
3066 4885
3067 - $body = json_encode([
3068 - 'model' => $selected_model,
3069 - 'messages' => $formatted_conversation,
3070 - 'temperature' => 0.8,
3071 - 'stream' => false
3072 - ]);
4886 + curl_close($ch);
3073 4887
3074 - $args = [
3075 - 'body' => $body,
3076 - 'headers' => [
3077 - 'Content-Type' => 'application/json',
3078 - 'Authorization' => 'Bearer ' . $api_key,
3079 - ],
3080 - 'timeout' => 60,
3081 - 'redirection' => 5,
3082 - 'blocking' => true,
3083 - 'httpversion' => '1.0',
3084 - 'sslverify' => true,
3085 - ];
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 + }
3086 4913
3087 - $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 + }
3088 4918
3089 - if (is_wp_error($response)) {
3090 - //error_log('OpenAI API Error: ' . $response->get_error_message());
3091 - return "Sorry, there was an error processing your request.";
3092 - }
4919 + return true; // Indicate streaming completed successfully
3093 4920
3094 - $response_body = wp_remote_retrieve_body($response);
3095 - $decoded_response = json_decode($response_body, true);
3096 -
3097 - if (isset($decoded_response['choices'][0]['message']['content'])) {
3098 - return trim($decoded_response['choices'][0]['message']['content']);
3099 - } else {
3100 - //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3101 - 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;
3102 4946 }
3103 4947 }
3104 -private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3105 - // Get system prompt instructions from options
3106 - $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 + }
3107 4960
3108 - // Add system prompt to relevant content
3109 - $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4961 + // Format conversation history for X.AI (same as OpenAI format)
4962 + $formatted_conversation = array();
3110 4963
3111 - // Prepend system instructions to the conversation history
3112 - array_unshift($conversation_history, [
3113 - 'role' => 'system',
3114 - 'content' => "Here are your instructions: " . $content_with_instructions
3115 - ]);
4964 + $formatted_conversation[] = array(
4965 + 'role' => 'system',
4966 + 'content' => $system_prompt_instructions . " " . $relevant_content
4967 + );
3116 4968
3117 - // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3118 - foreach ($conversation_history as &$message) {
3119 - if ($message['role'] === 'bot') {
3120 - $message['role'] = 'assistant';
3121 - } elseif ($message['role'] === 'agent') {
3122 - // Tag the message as coming from a live agent
3123 - $message['role'] = 'assistant';
3124 - if (!isset($message['metadata'])) {
3125 - $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 + );
3126 4982 }
3127 4983 }
3128 4984
3129 - // Ensure all roles are valid
3130 - if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3131 - $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;
3132 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;
3133 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 + }
3134 5169
5170 + // Format conversation history for DeepSeek
5171 + $formatted_conversation = array();
3135 5172
3136 - // Build the request body
3137 - $body = json_encode([
3138 - 'model' => $selected_model,
3139 - 'messages' => $conversation_history,
3140 - 'temperature' => 0.8,
3141 - 'stream' => false
3142 - ]);
5173 + $formatted_conversation[] = array(
5174 + 'role' => 'system',
5175 + 'content' => $system_prompt_instructions . " " . $relevant_content
5176 + );
3143 5177
3144 - // Set up the API request
3145 - $args = [
3146 - 'body' => $body,
3147 - 'headers' => [
3148 - 'Content-Type' => 'application/json',
3149 - 'Authorization' => 'Bearer ' . $xai_api_key,
3150 - ],
3151 - 'timeout' => 60,
3152 - 'redirection' => 5,
3153 - 'blocking' => true,
3154 - 'httpversion' => '1.0',
3155 - 'sslverify' => true,
3156 - ];
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 + }
3157 5193
3158 - // Make the API request
3159 - $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 + }
3160 5220
3161 - // Process the response
3162 - if (is_wp_error($response)) {
3163 - return "Sorry, there was an error processing your request.";
3164 - }
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 + ]);
3165 5228
3166 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
3167 -
3168 - if (isset($response_body['choices'][0]['message']['content'])) {
3169 - return trim($response_body['choices'][0]['message']['content']);
3170 - } else {
3171 - 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;
3172 5388 }
3173 5389 }
3174 5390
3175 5391 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3176 - // Get system prompt instructions from options
3177 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3178 -
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 +
3179 5399 // Clean and validate conversation history
3180 5400 foreach ($conversation_history as &$message) {
3181 5401 // Convert bot and agent roles to assistant
3182 5402 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -3271,8 +5491,796 @@
3271 5491 // Log unexpected response format
3272 5492 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3273 5493 return "Sorry, I received an unexpected response format from the API.";
3274 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 +
3275 6283 public function mxchat_dismiss_pre_chat_message() {
3276 6284 // Get and sanitize the user identifier
3277 6285 $user_id = $this->mxchat_get_user_identifier();
3278 6286 $user_id = sanitize_key($user_id);
@@ -3326,13 +6334,13 @@
3326 6334
3327 6335 return $dotProduct / ($normA * $normB);
3328 6336 }
3329 6337
6338 +
3330 6339 public function mxchat_enqueue_scripts_styles() {
3331 6340 // Define version numbers for the styles and scripts
3332 - $chat_style_version = '2.0.4'; // Replace with your actual version
3333 - $chat_script_version = '2.0.4'; // Replace with your actual version
3334 -
6341 + $chat_style_version = '2.4.6';
6342 + $chat_script_version = '2.4.6';
3335 6343 // Enqueue the script
3336 6344 wp_enqueue_script(
3337 6345 'mxchat-chat-js',
3338 6346 plugin_dir_url(__FILE__) . '../js/chat-script.js',
@@ -3339,9 +6347,8 @@
3339 6347 array('jquery'),
3340 6348 $chat_script_version,
3341 6349 true
3342 6350 );
3343 -
3344 6351 // Enqueue the CSS
3345 6352 wp_enqueue_style(
3346 6353 'mxchat-chat-css',
3347 6354 plugin_dir_url(__FILE__) . '../css/chat-style.css',
@@ -3347,17 +6354,19 @@
3347 6354 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3348 6355 array(),
3349 6356 $chat_style_version
3350 6357 );
3351 -
3352 6358 // Fetch options from the database
3353 6359 $this->options = get_option('mxchat_options');
3354 6360 $prompts_options = get_option('mxchat_prompts_options', array());
3355 -
6361 +
3356 6362 // Prepare settings for JavaScript
3357 6363 $style_settings = array(
3358 6364 'ajax_url' => admin_url('admin-ajax.php'),
3359 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',
3360 6369 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3361 6370 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3362 6371 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3363 6372 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -3371,10 +6380,9 @@
3371 6380 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3372 6381 'icon_color' => $this->options['icon_color'] ?? '#fff',
3373 6382 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3374 6383 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3375 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3376 -
6384 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
3377 6385 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3378 6386 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3379 6387 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 6388 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
@@ -3379,76 +6387,942 @@
3379 6387 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 6388 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3381 6389 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3382 6390 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3383 -
3384 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,
3385 6395 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
3386 6396 );
3387 -
3388 6397 // Pass the settings to the script
3389 6398 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3390 6399 }
3391 6400
3392 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 + */
3393 6693 public function mxchat_reset_rate_limits() {
6694 + try {
3394 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 +}
3395 6792
3396 - // Define a cache key pattern for rate limits
3397 - $cache_key_pattern = 'mxchat_chat_limit_%';
3398 6793
3399 - // Retrieve all option names matching the pattern
3400 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3401 - $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 +}
3402 6840
3403 - // db call ok; no-cache ok
3404 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3405 - $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 +}
3406 6878
3407 - // Clear the relevant cache entries
3408 - foreach ($option_names as $option_name) {
3409 - wp_cache_delete($option_name, 'options');
3410 - }
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 +}
3411 6911
3412 - // Optionally, clear a general cache if you have one
3413 - 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']);
3414 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 +}
3415 6934
3416 -private function mxchat_fetch_woocommerce_products() {
3417 - // Ensure WooCommerce is active
3418 - if (!class_exists('WooCommerce')) {
3419 - 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;
3420 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 +}
3421 6973
3422 - $args = array(
3423 - 'post_type' => 'product',
3424 - 'post_status' => 'publish',
3425 - '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 + ]
3426 7226 );
7227 +
7228 + wp_send_json_success(['message' => 'Click tracked']);
7229 + wp_die();
7230 +}
3427 7231
3428 - $products = get_posts($args);
3429 - $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 +}
3430 7297
3431 - foreach ($products as $product) {
3432 - $product_id = $product->ID;
3433 - $product_obj = wc_get_product($product_id);
3434 7298
3435 - $product_data[] = array(
3436 - 'id' => $product_id,
3437 - 'name' => $product_obj->get_name(),
3438 - 'description' => $product_obj->get_description(),
3439 - 'short_description' => $product_obj->get_short_description(),
3440 - 'url' => get_permalink($product_id),
3441 - 'price' => $product_obj->get_regular_price(),
3442 - 'sale_price' => $product_obj->get_sale_price(),
3443 - 'stock_status' => $product_obj->get_stock_status(),
3444 - 'sku' => $product_obj->get_sku(),
3445 - 'in_stock' => $product_obj->is_in_stock(),
3446 - 'total_sales' => $product_obj->get_total_sales(),
3447 - );
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();
3448 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 +}
3449 7324
3450 - return $product_data;
3451 -}
7325 +
3452 7326
3453 7327 }
3454 7328 ?>