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