PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.6
MxChat – AI Chatbot & Content Generation for WordPress v2.3.6
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-integrator.php +4324 -1301 2.0.42.3.6 View file →
@@ -9,50 +9,50 @@
9 9 private $chat_count;
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 + private $last_similarity_analysis = null;
13 14
15 +
16 +/**
17 + * Class constructor
18 + */
14 19 public function __construct() {
15 20 $this->options = get_option('mxchat_options');
16 21 $this->prompts_options = get_option('mxchat_prompts_options', array());
17 -
18 22 $this->chat_count = get_option('mxchat_chat_count', 0);
19 23 $this->word_handler = new MXChat_Word_Handler($this->options);
20 -
24 +
25 + // Add all action hooks
21 26 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 27 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 28 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24 -
25 29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31 +
27 32 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 33 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 34 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30 -
31 35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33 -
34 37 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
35 38 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
36 -
37 - if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
38 - wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
39 - }
40 -
39 +
41 40 // Add REST API routes registration
42 41 add_action('rest_api_init', array($this, 'register_routes'));
43 -
44 42 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
45 43 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
46 -
44 +
45 + // Rate limit action - notice we removed the old schedule setup
47 46 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
48 -
47 +
48 + // File upload and handling actions
49 49 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 50 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 51 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 52 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53 -
54 - // Add these with your other add_action hooks
53 +
54 + // Word document handling actions
55 55 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 56 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
@@ -57,15 +57,45 @@
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 +
82 +
83 + add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
84 +
85 +
66 86 }
67 87
88 +// In your core plugin's check_actions_for_addons method:
89 +public function check_actions_for_addons($default, $message, $user_id, $session_id) {
90 + error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
91 +
92 + $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
93 +
94 + error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
95 +
96 + return $result;
97 +}
68 98
69 99 private function mxchat_increment_chat_count() {
70 100 $chat_count = get_option('mxchat_chat_count', 0);
71 101 $chat_count++;
@@ -96,24 +126,9 @@
96 126 'chat_mode' => $chat_mode
97 127 ]);
98 128 wp_die();
99 129 }
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 130
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 131 private function mxchat_fetch_conversation_history_for_ai($session_id) {
117 132 $history = get_option("mxchat_history_{$session_id}", []);
118 133 $formatted_history = [];
119 134
@@ -150,9 +165,9 @@
150 165 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
151 166 continue;
152 167 }
153 168
154 - // More accurate token estimation (1 token ≈ 4 characters)
169 + // More accurate token estimation (1 token ≈ 4 characters)
155 170 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
156 171
157 172 // Check token budget with the new estimate
158 173 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
@@ -206,8 +221,14 @@
206 221 'methods' => 'POST',
207 222 'callback' => [$this, 'handle_slack_interaction'],
208 223 'permission_callback' => [$this, 'verify_slack_request'],
209 224 ]);
225 +
226 + register_rest_route('mxchat/v1', '/slack-messages', [
227 + 'methods' => 'POST',
228 + 'callback' => [$this, 'handle_slack_messages'],
229 + 'permission_callback' => [$this, 'verify_slack_request'],
230 + ]);
210 231
211 232 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
212 233 }
213 234
@@ -260,8 +281,9 @@
260 281
261 282 // Compare signatures
262 283 return hash_equals($my_signature, $slack_signature);
263 284 }
285 +
264 286 public function mxchat_stream_events(WP_REST_Request $request) {
265 287 header('Content-Type: text/event-stream');
266 288 header('Cache-Control: no-cache');
267 289 header('Connection: keep-alive');
@@ -295,20 +317,33 @@
295 317
296 318
297 319
298 320
299 -private function mxchat_save_chat_message($session_id, $role, $message) {
321 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
300 322 global $wpdb;
301 -
302 323 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
303 324 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
304 -
325 +
326 + // Check if this is the first message in a new session (before any other database operations)
327 + $is_new_session = false;
328 + if ($role === 'user') { // Only check for user messages, not bot responses
329 + $existing_messages = $wpdb->get_var($wpdb->prepare(
330 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
331 + $session_id
332 + ));
333 + $is_new_session = ($existing_messages == 0);
334 +
335 + // NEW: Log for debugging
336 + if ($is_new_session) {
337 + error_log("[DEBUG] This is a NEW session - first message");
338 + }
339 + }
340 +
305 341 // 1) Extract agent name if present
306 342 $agent_name = '';
307 343 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
308 344 $agent_name = $matches[1];
309 345 $message = str_replace("Agent: $agent_name - ", '', $message);
310 -
311 346 $session_meta_key = "mxchat_agent_name_{$session_id}";
312 347 if (empty(get_option($session_meta_key))) {
313 348 update_option($session_meta_key, $agent_name);
314 349 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
@@ -313,30 +348,30 @@
313 348 update_option($session_meta_key, $agent_name);
314 349 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
315 350 }
316 351 }
317 -
352 +
318 353 // 2) Generate unique message_id
319 354 $message_id = uniqid();
320 355 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
321 -
356 +
322 357 // 3) Determine user_id
323 358 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
324 -
359 +
325 360 // 4) Determine user_identifier
326 361 $user_identifier = $agent_name
327 362 ? $agent_name
328 363 : MxChat_User::mxchat_get_user_identifier();
329 -
364 +
330 365 // 5) Determine displayed_name
331 366 $user_email = MxChat_User::mxchat_get_user_email();
332 367 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
333 -
368 +
334 369 // 6) Check for a saved email in wp_options
335 370 $email_option_key = "mxchat_email_{$session_id}";
336 371 $saved_email = get_option($email_option_key);
337 372 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
338 -
373 +
339 374 // If found, update DB user_email
340 375 if ($saved_email) {
341 376 $update_res = $wpdb->update(
342 377 $table_name,
@@ -346,9 +381,9 @@
346 381 ['%s']
347 382 );
348 383 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
349 384 }
350 -
385 +
351 386 // 7) Save to session history in wp_options
352 387 $history_key = "mxchat_history_{$session_id}";
353 388 $history = get_option($history_key, []);
354 389 $history[] = [
@@ -357,11 +392,11 @@
357 392 'content' => $message,
358 393 'timestamp' => round(microtime(true) * 1000),
359 394 'agent_name' => $displayed_name,
360 395 ];
361 - update_option($history_key, $history);
396 + update_option($history_key, $history, 'no');
362 397 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
363 -
398 +
364 399 // 8) Save the message to DB (INSERT)
365 400 $insert_data = [
366 401 'user_id' => $user_id,
367 402 'user_identifier'=> $user_identifier,
@@ -370,21 +405,135 @@
370 405 'role' => $role,
371 406 'message' => $message,
372 407 'timestamp' => current_time('mysql', 1),
373 408 ];
409 +
410 + // IMPROVED: Handle originating page data
411 + $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
412 +
413 + if ($columns_exist) {
414 + if ($is_new_session && $role === 'user') {
415 + // For the first user message, set originating page data
416 +
417 + // First check if we have it from the parameter
418 + if ($originating_page && !empty($originating_page['url'])) {
419 + $insert_data['originating_page_url'] = $originating_page['url'];
420 + $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
421 +
422 + error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
423 + }
424 + // Otherwise check if it's stored in the instance property
425 + else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
426 + $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
427 + $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
428 +
429 + error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
430 +
431 + // Clear after using
432 + unset($this->pending_originating_page);
433 + }
434 + // Fallback to HTTP_REFERER if nothing else is available
435 + else if (isset($_SERVER['HTTP_REFERER'])) {
436 + $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
437 + $insert_data['originating_page_url'] = $referer_url;
438 +
439 + // Generate title from URL
440 + $parsed_url = parse_url($referer_url);
441 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
442 +
443 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
444 + $insert_data['originating_page_title'] = 'Homepage';
445 + } else {
446 + $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
447 + $insert_data['originating_page_title'] = ucwords(trim($title));
448 + }
449 +
450 + error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
451 + }
452 +
453 + // Store for this session so all messages have the same originating page
454 + if (!empty($insert_data['originating_page_url'])) {
455 + update_option("mxchat_originating_page_{$session_id}", [
456 + 'url' => $insert_data['originating_page_url'],
457 + 'title' => $insert_data['originating_page_title']
458 + ], 'no');
459 + }
460 + } else {
461 + // For subsequent messages in the session, use the stored originating page
462 + $stored_originating = get_option("mxchat_originating_page_{$session_id}");
463 + if ($stored_originating && !empty($stored_originating['url'])) {
464 + $insert_data['originating_page_url'] = $stored_originating['url'];
465 + $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
466 + }
467 + }
468 + }
469 +
374 470 $wpdb->insert($table_name, $insert_data);
375 471 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
376 -
472 +
473 + // 9) Send notification email if this is the first user message in a new session
474 + if ($wpdb->insert_id && $is_new_session && $role === 'user') {
475 + $this->send_new_chat_notification($session_id, array(
476 + 'identifier' => $user_identifier,
477 + 'email' => $saved_email ?: $user_email,
478 + 'ip' => $_SERVER['REMOTE_ADDR']
479 + ));
480 + }
481 +
377 482 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
378 483 return $message_id;
379 484 }
380 485
486 +private function send_new_chat_notification($session_id, $user_info = array()) {
487 + $options = get_option('mxchat_transcripts_options');
488 +
489 + // Check if notifications are enabled
490 + if (empty($options['mxchat_enable_notifications'])) {
491 + return false;
492 + }
493 +
494 + // Get notification email
495 + $to = !empty($options['mxchat_notification_email']) ?
496 + $options['mxchat_notification_email'] :
497 + get_option('admin_email');
498 +
499 + if (!is_email($to)) {
500 + return false;
501 + }
502 +
503 + // Prepare email content
504 + $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
505 +
506 + $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
507 + $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
508 + $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
509 +
510 + $message = sprintf(
511 + "A new chat session has started on your website.\n\n" .
512 + "Session ID: %s\n" .
513 + "User: %s\n" .
514 + "Email: %s\n" .
515 + "IP Address: %s\n" .
516 + "Time: %s\n\n" .
517 + "View transcripts: %s",
518 + $session_id,
519 + $user_identifier,
520 + $user_email,
521 + $user_ip,
522 + current_time('mysql'),
523 + admin_url('admin.php?page=mxchat-transcripts')
524 + );
525 +
526 + // Send email
527 + return wp_mail($to, $subject, $message);
528 +}
529 +
381 530 public function mxchat_handle_save_email_and_response() {
382 531 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
383 532
384 533 // Validate nonce
385 534 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'));
535 + //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
387 536 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
388 537 wp_die();
389 538 }
390 539
@@ -468,96 +617,33 @@
468 617 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
469 618 }
470 619 }
471 620
621 +public function mxchat_handle_chat_request() {
622 + global $wpdb;
472 623
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';
480 - }
481 -
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);
624 + // NEW: Check if this is a streaming request
625 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
626 +
627 + // NEW: Set streaming headers if needed
628 + if ($is_streaming) {
629 + // Disable output buffering
630 + while (ob_get_level()) {
631 + ob_end_flush(); // Changed from ob_end_clean()
507 632 }
633 +
634 + // Set headers for SSE
635 + header('Content-Type: text/event-stream');
636 + header('Cache-Control: no-cache');
637 + header('Connection: keep-alive');
638 + header('X-Accel-Buffering: no');
639 +
640 + // Add these new lines:
641 + ob_implicit_flush(true);
642 + flush();
508 643 }
509 644
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 -}
514 -
515 -
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 - }
522 -
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();
535 - }
536 -
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
645 + // Check if MX Chat Moderation is active
560 646 if (class_exists('MX_Chat_Moderation')) {
561 647 // Get user email and IP
562 648 $user_email = '';
563 649 $user_ip = $_SERVER['REMOTE_ADDR'];
@@ -591,14 +677,12 @@
591 677 wp_die();
592 678 }
593 679 }
594 680
595 -
596 - // Reset fallback response at the start of each request
597 681 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
598 682 $this->productCardHtml = '';
599 683
600 - // Get the actual WordPress user ID if logged in
684 + // Get the actual WordPress user ID if logged in
601 685 $is_logged_in = is_user_logged_in();
602 686 if ($is_logged_in) {
603 687 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
604 688 } else {
@@ -608,65 +692,24 @@
608 692
609 693 // Get and sanitize the user identifier
610 694 $user_id = sanitize_key($user_id);
611 695
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'));
696 + // Check rate limit using new settings structure
697 + $rate_limit_result = $this->check_rate_limit();
615 698
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);
699 + if ($rate_limit_result !== true) {
700 + wp_send_json([
701 + 'success' => false,
702 + 'message' => $rate_limit_result['message'],
703 + 'status' => 'rate_limit_exceeded'
704 + ]);
705 + wp_die();
661 706 }
662 707
663 708 // Rest of your existing code...
664 709 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
665 - //error_log("Session ID: $session_id");
666 710
667 711 if (empty($session_id)) {
668 - //error_log("Error: Session ID is missing.");
669 712 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
670 713 wp_die();
671 714 }
672 715
@@ -671,75 +714,246 @@
671 714 }
672 715
673 716 // Validate and sanitize the incoming message
674 717 if (empty($_POST['message'])) {
675 - //error_log("Error: No message received.");
676 718 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
677 719 wp_die();
678 720 }
721 +
722 +
723 + // NEW: Track originating page for first message in session
724 +$table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
679 725
726 +// Check if originating page columns exist
727 +$columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
680 728
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 -];
729 +if ($columns_exist) {
730 + // Check if this session already has messages
731 + $message_count = $wpdb->get_var($wpdb->prepare(
732 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
733 + $session_id
734 + ));
735 +
736 + // If this is the first message in the session
737 + if ($message_count == 0) {
738 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
739 + $originating_url = '';
740 + $originating_title = '';
741 +
742 + // Try to get from POST data first (sent by JavaScript)
743 + if (isset($_POST['current_page_url'])) {
744 + $originating_url = esc_url_raw($_POST['current_page_url']);
745 + $originating_title = isset($_POST['current_page_title'])
746 + ? sanitize_text_field($_POST['current_page_title'])
747 + : '';
748 + }
749 + // Fallback to HTTP_REFERER if not provided by JavaScript
750 + else if (isset($_SERVER['HTTP_REFERER'])) {
751 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
752 + }
753 +
754 + // Generate title if we have URL but no title
755 + if ($originating_url && empty($originating_title)) {
756 + $parsed_url = parse_url($originating_url);
757 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
758 +
759 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
760 + $originating_title = 'Homepage';
761 + } else {
762 + // Clean up the path to make a readable title
763 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
764 + $originating_title = ucwords(trim($originating_title));
765 + }
766 + }
767 +
768 + // Store for later use when saving the message
769 + $this->pending_originating_page = [
770 + 'url' => $originating_url,
771 + 'title' => $originating_title
772 + ];
773 + }
774 +}
775 +
776 +
688 777
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']);
778 + // NEW: Get page context if provided
779 + $page_context = null;
780 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
781 + $page_context_raw = stripslashes($_POST['page_context']);
782 + $page_context = json_decode($page_context_raw, true);
783 +
784 + // Validate page context structure
785 + if (is_array($page_context) &&
786 + isset($page_context['url']) &&
787 + isset($page_context['title']) &&
788 + isset($page_context['content'])) {
789 +
790 + // Sanitize page context
791 + $page_context['url'] = esc_url_raw($page_context['url']);
792 + $page_context['title'] = sanitize_text_field($page_context['title']);
793 + $page_context['content'] = wp_kses_post($page_context['content']);
794 + } else {
795 + $page_context = null;
796 + }
797 + }
693 798
694 -// Then apply sanitization
695 -$message = wp_kses($message, $allowed_tags);
799 + // Modify the message sanitization to preserve PHP tags in code blocks
800 + $allowed_tags = [
801 + 'pre' => [],
802 + 'code' => ['class' => true],
803 + 'span' => ['class' => true],
804 + 'div' => ['class' => true],
805 + ];
696 806
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);
807 + // First preserve code blocks
808 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
809 + return htmlspecialchars_decode($matches[0]);
810 + }, $_POST['message']);
701 811
702 -$message = trim($message);
812 + // Then apply sanitization
813 + $message = wp_kses($message, $allowed_tags);
703 814
704 -// Preserve code blocks from markdown conversion
705 -$message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
815 + // Preserve code blocks from markdown conversion
816 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
817 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
706 818
707 - // Save the user's message
708 - $this->mxchat_save_chat_message($session_id, 'user', $message);
819 +// ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
820 + // Always initialize testing data for admins (no toggle needed)
821 + $testing_data = null;
822 + if (current_user_can('administrator')) {
823 + // For vision messages, use the original user message for the query display
824 + $query_for_testing = $message;
825 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
826 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
827 + }
828 +
829 + $testing_data = [
830 + 'query' => $query_for_testing,
831 + 'timestamp' => time(),
832 + 'top_matches' => [],
833 + 'action_matches' => [], // NEW: Initialize action matches array
834 + 'page_context' => $page_context, // NEW: Include page context in testing data
835 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed']
836 + ];
837 +
838 + // Get similarity threshold
839 + $similarity_threshold = isset($this->options['similarity_threshold'])
840 + ? ((int) $this->options['similarity_threshold']) / 100
841 + : 0.75;
842 +
843 + $testing_data['similarity_threshold'] = $similarity_threshold;
844 +
845 + // Determine knowledge base type
846 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
847 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
848 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
849 + }
850 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
709 851
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);
852 +// Add debug before and after:
853 +error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
854 +$pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
855 +error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
714 856
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 857
719 - wp_send_json([
720 - 'success' => true,
721 - 'status' => 'email_captured',
722 - 'message' => $response_message
723 - ]);
858 + // If the pre-processing returned a result (not the original message), use it directly
859 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
860 + // Save the AI response
861 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
862 +
863 + // Save HTML content if provided
864 + if (!empty($pre_processed_result['html'])) {
865 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
866 + }
867 +
868 + // Add testing data if admin
869 + $response_data = [
870 + 'text' => $pre_processed_result['text'],
871 + 'html' => $pre_processed_result['html'] ?? '',
872 + 'session_id' => $session_id
873 + ];
874 +
875 + if ($testing_data !== null) {
876 + $response_data['testing_data'] = $testing_data;
877 + }
878 +
879 + wp_send_json($response_data);
724 880 wp_die();
725 881 }
726 882
883 + // Save the user's message - handle vision processed messages differently
884 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
885 + // For vision messages, save the original user message with image indicator
886 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
887 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
888 + $image_count = intval($_POST['vision_images_count']);
889 + $original_message .= " [{$image_count} image(s)]";
890 + }
891 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
892 + } else {
893 + // Regular message - save as normal
894 + $this->mxchat_save_chat_message($session_id, 'user', $message);
895 + }
896 +
897 +
898 +if (is_email($message)) {
899 + // Add the email to Loops
900 + $this->add_email_to_loops($message);
901 +
902 + // Get the user's success message instruction
903 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
904 +
905 + // Set instruction for AI using the user's success message
906 + $this->current_action_instruction = $user_success_message;
907 +
908 + // Clear the email capture transient since we got the email
909 + delete_transient('mxchat_email_capture_' . $user_id);
910 + }
911 +
912 + // NEW: Check if we're in an email capture flow but user hasn't provided email yet
913 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
914 + // Check if the message contains an email (not the whole message being an email)
915 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
916 + $extracted_email = $matches[0];
917 +
918 + // Add the extracted email to Loops
919 + $this->add_email_to_loops($extracted_email);
920 +
921 + // Get the user's success message instruction
922 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
923 +
924 + // Set instruction for AI using the user's success message
925 + $this->current_action_instruction = $user_success_message;
926 +
927 + // Clear the email capture transient since we got the email
928 + delete_transient('mxchat_email_capture_' . $user_id);
929 + }
930 + // If no email found but we're in capture mode, remind them
931 + else {
932 + // Get the original instruction to remind them
933 + $original_instruction = $this->options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
934 + $this->current_action_instruction = $original_instruction;
935 + }
936 + }
937 +
727 938 $intent_info = '';
728 939
729 940 // Check chat mode
730 941 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
731 - //error_log("Chat Mode: $chat_mode");
732 942
733 943 // Handle agent mode
944 +// Handle agent mode
734 945 if ($chat_mode === 'agent') {
735 946 // First, check for switch intent before doing anything else
736 947 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
737 948
949 + // NEW: Capture action analysis for testing panel after intent check
950 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
951 + $testing_data['action_matches'] = $this->last_action_analysis;
952 + }
953 +
738 954 // If we matched an intent and it's the switch intent, handle it
739 955 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
740 - //error_log("Switch to chatbot intent detected");
741 -
742 956 // Update chat mode first
743 957 update_option("mxchat_mode_{$session_id}", 'ai');
744 958
745 959 // Clear any existing PDF context to start fresh
@@ -752,8 +966,12 @@
752 966 'session_id' => $session_id,
753 967 'chat_mode' => 'ai'
754 968 ];
755 969
970 + if ($testing_data !== null) {
971 + $response_data['testing_data'] = $testing_data;
972 + }
973 +
756 974 // Save the mode switch message
757 975 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
758 976 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
759 977
@@ -763,16 +981,20 @@
763 981 } elseif (!$intent_matched) {
764 982 // No intent matched, handle live agent message
765 983 try {
766 984 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
767 - //error_log("Message sent to agent.");
768 985
769 - wp_send_json_success([
986 + $agent_response = [
770 987 'status' => 'waiting_for_agent',
771 988 'message' => esc_html__('Message sent to live agent.', 'mxchat')
772 - ]);
989 + ];
990 +
991 + if ($testing_data !== null) {
992 + $agent_response['testing_data'] = $testing_data;
993 + }
994 +
995 + wp_send_json_success($agent_response);
773 996 } catch (\Exception $e) {
774 - //error_log("Error sending message to agent: " . $e->getMessage());
775 997 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
776 998 }
777 999 wp_die();
778 1000 }
@@ -777,160 +999,303 @@
777 999 wp_die();
778 1000 }
779 1001 }
780 1002
781 - // Step 1: Check for new PDF URL in the message
782 - if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
783 - $new_pdf_url = $matches[0];
1003 + // Step 1: Check for new PDF URL in the message
1004 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1005 + $new_pdf_url = $matches[0];
784 1006
785 - // Check if this is likely a PDF-related request
786 - $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
787 - $is_pdf_request = false;
1007 + // Check if this is likely a PDF-related request
1008 + $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1009 + $is_pdf_request = false;
788 1010
789 - foreach ($pdf_keywords as $keyword) {
790 - if (stripos($message, $keyword) !== false) {
791 - $is_pdf_request = true;
792 - break;
793 - }
1011 + foreach ($pdf_keywords as $keyword) {
1012 + if (stripos($message, $keyword) !== false) {
1013 + $is_pdf_request = true;
1014 + break;
794 1015 }
1016 + }
795 1017
796 - // If it looks like a PDF request or we're waiting for a PDF URL
797 - if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
798 - // Validate HTTPS
799 - if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
800 - // Extract filename from URL
801 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1018 + // If it looks like a PDF request or we're waiting for a PDF URL
1019 + if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1020 + // Validate HTTPS
1021 + if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1022 + // Extract filename from URL
1023 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
802 1024
803 - // Clear previous PDF transients
804 - $this->clear_pdf_transients($session_id);
1025 + // Clear previous PDF transients
1026 + $this->clear_pdf_transients($session_id);
805 1027
806 - // Process new PDF
807 - $max_pages = $this->options['pdf_max_pages'] ?? 69;
808 - $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1028 + // Process new PDF
1029 + $max_pages = $this->options['pdf_max_pages'] ?? 69;
1030 + $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
809 1031
810 - if ($embeddings === 'too_many_pages') {
811 - $error_text = sprintf(
812 - $this->options['pdf_intent_error_text'] ??
813 - esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
814 - $max_pages
815 - );
816 - $this->fallbackResponse['text'] = $error_text;
817 - } elseif ($embeddings) {
818 - // Store new PDF information
819 - // Create a more meaningful filename from URL
820 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1032 + if ($embeddings === 'too_many_pages') {
1033 + $error_text = sprintf(
1034 + $this->options['pdf_intent_error_text'] ??
1035 + esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1036 + $max_pages
1037 + );
1038 + $this->fallbackResponse['text'] = $error_text;
1039 + } elseif ($embeddings) {
1040 + // Store new PDF information
1041 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
821 1042
822 - // If the filename is generic (like results_download.php), create a more descriptive one
823 - if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
824 - strpos($pdf_filename, '.php') !== false) {
825 - // Create a timestamp-based name
826 - $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
827 - }
1043 + // If the filename is generic, create a more descriptive one
1044 + if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1045 + strpos($pdf_filename, '.php') !== false) {
1046 + $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1047 + }
828 1048
829 - set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
830 - set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
831 - set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
832 - set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1049 + set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1050 + set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1051 + set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1052 + set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
833 1053
834 - $success_text = $this->options['pdf_intent_success_text'] ??
835 - esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1054 + $success_text = $this->options['pdf_intent_success_text'] ??
1055 + esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
836 1056
837 - // Return success with filename for UI update
838 - wp_send_json([
839 - 'success' => true,
840 - 'message' => $success_text,
841 - 'data' => [
842 - 'filename' => $pdf_filename
843 - ]
844 - ]);
845 - wp_die();
846 - } else {
847 - $error_text = $this->options['pdf_intent_error_text'] ??
848 - esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
849 - $this->fallbackResponse['text'] = $error_text;
1057 + $pdf_response = [
1058 + 'success' => true,
1059 + 'message' => $success_text,
1060 + 'data' => [
1061 + 'filename' => $pdf_filename
1062 + ]
1063 + ];
1064 +
1065 + if ($testing_data !== null) {
1066 + $pdf_response['testing_data'] = $testing_data;
850 1067 }
851 1068
852 - wp_send_json([
853 - 'success' => false,
854 - 'message' => $this->fallbackResponse['text']
855 - ]);
1069 + wp_send_json($pdf_response);
856 1070 wp_die();
1071 + } else {
1072 + $error_text = $this->options['pdf_intent_error_text'] ??
1073 + esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1074 + $this->fallbackResponse['text'] = $error_text;
857 1075 }
1076 +
1077 + $pdf_error_response = [
1078 + 'success' => false,
1079 + 'message' => $this->fallbackResponse['text']
1080 + ];
1081 +
1082 + if ($testing_data !== null) {
1083 + $pdf_error_response['testing_data'] = $testing_data;
1084 + }
1085 +
1086 + wp_send_json($pdf_error_response);
1087 + wp_die();
858 1088 }
859 1089 }
1090 + }
860 1091
1092 + // Check if there's an active recommendation flow session
1093 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1094 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1095 + // Create a dummy intent object that matches the original intent
1096 + $dummy_intent = new stdClass();
1097 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1098 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1099 +
1100 + // Call the recommendation flow handler directly
1101 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1102 +
1103 + // If the handler returned a response, send it
1104 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1105 + // Save the bot's response to the chat history
1106 + if (!empty($response_data['text'])) {
1107 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
1108 + }
1109 + if (!empty($response_data['html'])) {
1110 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1111 + }
1112 +
1113 + if ($testing_data !== null) {
1114 + $response_data['testing_data'] = $testing_data;
1115 + }
1116 +
1117 + // Send the response
1118 + wp_send_json($response_data);
1119 + wp_die();
1120 + }
1121 + }
1122 +
861 1123 // 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"));
1124 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
864 1125
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();
1126 + // NEW: Capture action analysis for testing panel after intent check
1127 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1128 + $testing_data['action_matches'] = $this->last_action_analysis;
876 1129 }
877 1130
878 - // If no intent matched or product not found, proceed with AI response
879 - //error_log("No matching intent or fallback. Generating AI response.");
1131 + // Step 3: Handle the intent result appropriately
1132 + if ($intent_result !== false) {
1133 + // Intent was matched - ALWAYS send as JSON response, never streaming
1134 +
1135 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1136 + // Intent returned a direct response array
1137 + $response_data = [
1138 + 'text' => $intent_result['text'] ?? '',
1139 + 'html' => $intent_result['html'] ?? '',
1140 + 'session_id' => $session_id
1141 + ];
1142 +
1143 + if ($testing_data !== null) {
1144 + $response_data['testing_data'] = $testing_data;
1145 + }
1146 +
1147 + // Clear streaming headers if they were set
1148 + if ($is_streaming) {
1149 + header_remove('Content-Type');
1150 + header_remove('Cache-Control');
1151 + header_remove('Connection');
1152 + header_remove('X-Accel-Buffering');
1153 + header('Content-Type: application/json');
1154 + }
1155 +
1156 + wp_send_json($response_data);
1157 + wp_die();
1158 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1159 + // Intent returned true and set fallbackResponse
1160 +
1161 + // SAVE TO TRANSCRIPT FIRST
1162 + if (!empty($this->fallbackResponse['text'])) {
1163 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1164 + }
1165 + if (!empty($this->fallbackResponse['html'])) {
1166 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1167 + }
1168 +
1169 + $response_data = [
1170 + 'text' => $this->fallbackResponse['text'] ?? '',
1171 + 'html' => $this->fallbackResponse['html'] ?? '',
1172 + 'session_id' => $session_id
1173 + ];
1174 +
1175 + if ($testing_data !== null) {
1176 + $response_data['testing_data'] = $testing_data;
1177 + }
1178 +
1179 + // Clear streaming headers if they were set
1180 + if ($is_streaming) {
1181 + header_remove('Content-Type');
1182 + header_remove('Cache-Control');
1183 + header_remove('Connection');
1184 + header_remove('X-Accel-Buffering');
1185 + header('Content-Type: application/json');
1186 + }
1187 +
1188 + wp_send_json($response_data);
1189 + wp_die();
1190 + }
1191 + }
880 1192
1193 + // If we get here, no intent matched OR the intent didn't provide a usable response
1194 +
881 1195 // Step 4: Generate AI response
882 1196 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
883 1197 $this->mxchat_increment_chat_count();
884 -
1198 +
885 1199 // Generate embedding for the user's query
886 1200 $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'));
1201 +
1202 + // Check if the embedding generation returned an error
1203 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1204 + $error_message = $user_message_embedding['error'];
1205 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1206 +
1207 + wp_send_json_error([
1208 + 'error_message' => $error_message,
1209 + 'error_code' => $error_code
1210 + ]);
890 1211 wp_die();
891 1212 }
1213 +
1214 + // Check if the embedding is valid
1215 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1216 + wp_send_json_error([
1217 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1218 + 'error_code' => 'invalid_embedding'
1219 + ]);
1220 + wp_die();
1221 + }
892 1222
893 1223 // Build context with both knowledge base and PDF content if available
894 1224 $context_content = "User asked: '{$message}'\n\n";
1225 +
1226 + // NEW: Add action instruction if present (add this right after the above line)
1227 + if (!empty($this->current_action_instruction)) {
1228 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1229 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1230 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1231 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1232 +
1233 + // Clear the instruction after using it
1234 + $this->current_action_instruction = null;
1235 + }
895 1236
896 - // Get relevant content from knowledge base
1237 +
1238 + // NEW: Add page context if available and contextual awareness is enabled
1239 + if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1240 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1241 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1242 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1243 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1244 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1245 + }
1246 +
1247 + // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
897 1248 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1249 +
1250 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1251 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1252 + // Update testing data with the REAL similarity analysis
1253 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1254 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1255 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1256 + }
1257 + // ===== END SIMILARITY DATA CAPTURE =====
1258 +
898 1259 if (!empty($relevant_content)) {
899 - $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
1260 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1261 + } else {
1262 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
900 1263 }
901 1264
902 -
903 - // Check for and include PDF content
904 - $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
905 - $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
906 - $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
907 - if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
908 - $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
909 - if (!empty($relevant_pdf_pages)) {
910 - $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
911 - foreach ($relevant_pdf_pages as $page_data) {
912 - $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
913 - }
914 - $context_content .= "\n";
1265 + // Check for and include PDF content
1266 + $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1267 + $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1268 + $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1269 + if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1270 + $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1271 + if (!empty($relevant_pdf_pages)) {
1272 + $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1273 + foreach ($relevant_pdf_pages as $page_data) {
1274 + $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
915 1275 }
1276 + $context_content .= "\n";
916 1277 }
1278 + }
917 1279
918 - // Check for and include Word content
919 - $word_url = get_transient('mxchat_word_url_' . $session_id);
920 - $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
921 - $word_filename = get_transient('mxchat_word_filename_' . $session_id);
922 - if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
923 - $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
924 - if (!empty($relevant_word_chunks)) {
925 - $context_content .= "Relevant content from Word document '{$word_filename}':\n";
926 - foreach ($relevant_word_chunks as $chunk_data) {
927 - $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
928 - }
929 - $context_content .= "\n";
1280 + // Check for and include Word content
1281 + $word_url = get_transient('mxchat_word_url_' . $session_id);
1282 + $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1283 + $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1284 + if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1285 + $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1286 + if (!empty($relevant_word_chunks)) {
1287 + $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1288 + foreach ($relevant_word_chunks as $chunk_data) {
1289 + $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
930 1290 }
1291 + $context_content .= "\n";
931 1292 }
932 - // Generate the response using the full context
1293 + }
1294 +
1295 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1296 +
1297 + // Generate response
933 1298 $response = $this->mxchat_generate_response(
934 1299 $context_content,
935 1300 $this->options['api_key'],
936 1301 $this->options['xai_api_key'],
@@ -935,11 +1300,34 @@
935 1300 $this->options['api_key'],
936 1301 $this->options['xai_api_key'],
937 1302 $this->options['claude_api_key'],
938 1303 $this->options['deepseek_api_key'],
939 - $conversation_history
1304 + $this->options['gemini_api_key'],
1305 + $conversation_history,
1306 + $is_streaming,
1307 + $session_id,
1308 + $testing_data
940 1309 );
941 -
1310 +
1311 + // Handle streaming vs non-streaming responses
1312 + if ($is_streaming) {
1313 + // Check if streaming actually happened or if it fell back to regular response
1314 + if ($response === true) {
1315 + wp_die();
1316 + }
1317 + // If we get here, streaming fell back to regular response, continue
1318 + }
1319 +
1320 + // Check if the response is an error array
1321 + if (is_array($response) && isset($response['error'])) {
1322 + wp_send_json_error([
1323 + 'error_message' => $response['error'],
1324 + 'error_code' => $response['error_code'] ?? 'api_error'
1325 + ]);
1326 + wp_die();
1327 + }
1328 +
1329 + // If we get here, the response is valid text
942 1330 $this->mxchat_save_chat_message($session_id, 'bot', $response);
943 1331
944 1332 // Step 5: Save additional content if available
945 1333 if (!empty($this->productCardHtml)) {
@@ -956,48 +1344,59 @@
956 1344 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
957 1345 'session_id' => $session_id
958 1346 ];
959 1347
1348 + // Always add testing data for admins (no toggle needed)
1349 + if ($testing_data !== null) {
1350 + $response_data['testing_data'] = $testing_data;
1351 + }
1352 +
960 1353 wp_send_json($response_data);
961 1354 wp_die();
962 1355 }
963 1356
964 -// New function to check intents and invoke the callback function
1357 +// Updated function to check intents and invoke the callback function
965 1358 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
966 1359 global $wpdb;
967 1360 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
968 1361
969 - //error_log('🔍 MXCHAT DEBUG: Intent Check Started ==================');
970 - //error_log("🔍 MXCHAT DEBUG: Message: '$message'");
971 - //error_log("🔍 MXCHAT DEBUG: Chat Mode: $chat_mode");
972 -
973 1362 // Generate the user embedding
974 - //error_log('🔄 MXCHAT DEBUG: Generating user embedding');
975 1363 $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;
1364 +
1365 + // Check if embedding generation returned an error
1366 + if (is_array($user_embedding) && isset($user_embedding['error'])) {
1367 + $error_message = $user_embedding['error'];
1368 + $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1369 +
1370 + wp_send_json_error([
1371 + 'error_message' => $error_message,
1372 + 'error_code' => $error_code
1373 + ]);
1374 + wp_die();
979 1375 }
980 - //error_log('✅ MXCHAT DEBUG: User embedding generated successfully');
981 1376
1377 + // Check if embedding is valid
1378 + if (!is_array($user_embedding) || empty($user_embedding)) {
1379 + wp_send_json_error([
1380 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1381 + 'error_code' => 'invalid_embedding'
1382 + ]);
1383 + wp_die();
1384 + }
1385 +
982 1386 // Fetch intents from the database
983 1387 $table_name = $wpdb->prefix . 'mxchat_intents';
984 1388 if ($chat_mode === 'agent') {
985 - //error_log('🔍 MXCHAT DEBUG: Agent mode - fetching only chatbot switch intent');
986 1389 $query = $wpdb->prepare(
987 - "SELECT * FROM $table_name WHERE callback_function = %s",
1390 + "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
988 1391 'mxchat_handle_switch_to_chatbot_intent'
989 1392 );
990 1393 $intents = $wpdb->get_results($query);
991 1394 } else {
992 - //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all intents');
993 - $intents = $wpdb->get_results("SELECT * FROM $table_name");
1395 + $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
994 1396 }
995 1397
996 - //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' intents to check');
997 -
998 1398 if (empty($intents)) {
999 - //error_log('❌ MXCHAT DEBUG: No intents found in database');
1000 1399 return false;
1001 1400 }
1002 1401
1003 1402 $highest_similarity = -INF;
@@ -1002,11 +1401,17 @@
1002 1401
1003 1402 $highest_similarity = -INF;
1004 1403 $matched_intent = null;
1005 1404
1006 - //error_log('📊 MXCHAT DEBUG: Intent Similarity Scores ==================');
1405 + // NEW: Array to store action analysis for testing panel
1406 + $action_analysis = [];
1407 +
1007 1408 foreach ($intents as $intent) {
1008 - //error_log("🔄 MXCHAT DEBUG: Checking intent: '{$intent->intent_label}' (callback: {$intent->callback_function})");
1409 + // Additional check for enabled state
1410 + $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1411 + if (!$is_enabled) {
1412 + continue;
1413 + }
1009 1414
1010 1415 $intent_embedding_serialized = $intent->embedding_vector;
1011 1416 $intent_embedding = $intent_embedding_serialized
1012 1417 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
@@ -1012,9 +1417,8 @@
1012 1417 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1013 1418 : null;
1014 1419
1015 1420 if (!is_array($intent_embedding)) {
1016 - //error_log("❌ MXCHAT DEBUG: Invalid embedding for intent: {$intent->intent_label}");
1017 1421 continue;
1018 1422 }
1019 1423
1020 1424 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
@@ -1019,34 +1423,56 @@
1019 1423
1020 1424 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1021 1425 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1022 1426
1427 + // NEW: Store action analysis data for testing panel
1428 + $action_analysis[] = [
1429 + 'intent_label' => $intent->intent_label,
1430 + 'callback_function' => $intent->callback_function,
1431 + 'similarity' => round($similarity, 4),
1432 + 'similarity_percentage' => round($similarity * 100, 2),
1433 + 'threshold' => $intent_threshold,
1434 + 'threshold_percentage' => round($intent_threshold * 100, 2),
1435 + 'above_threshold' => $similarity >= $intent_threshold,
1436 + 'triggered' => false // Will be updated below if this intent is triggered
1437 + ];
1023 1438
1024 1439 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1025 1440 $highest_similarity = $similarity;
1026 1441 $matched_intent = $intent;
1027 - //error_log("✅ MXCHAT DEBUG: New best match: '{$intent->intent_label}' with similarity {$similarity}");
1028 1442 }
1029 1443 }
1030 - //error_log('📊 MXCHAT DEBUG: End Intent Scores ==================');
1031 1444
1445 + // NEW: Mark the triggered action if any
1032 1446 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 -
1447 + foreach ($action_analysis as &$action) {
1448 + if ($action['intent_label'] === $matched_intent->intent_label) {
1449 + $action['triggered'] = true;
1450 + break;
1451 + }
1452 + }
1453 + }
1454 +
1455 + // NEW: Sort actions by similarity (highest first) and store for testing panel
1456 + usort($action_analysis, function($a, $b) {
1457 + return $b['similarity'] <=> $a['similarity'];
1458 + });
1459 +
1460 + // Store action analysis for testing panel capture
1461 + $this->last_action_analysis = $action_analysis;
1462 +
1463 + if ($matched_intent) {
1036 1464 // If the callback is a method on this instance (core callback), call it directly
1037 1465 if (method_exists($this, $matched_intent->callback_function)) {
1038 - //error_log('🔍 MXCHAT DEBUG: Using direct method call for core callback');
1039 1466 $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 - );
1467 + [$this, $matched_intent->callback_function],
1468 + $message,
1469 + $user_id,
1470 + $session_id,
1471 + $matched_intent,
1472 + $user_context ?? null
1473 + );
1047 1474 } else {
1048 - //error_log('🔍 MXCHAT DEBUG: Using apply_filters for add-on callback');
1049 1475 // Otherwise, use apply_filters for add-on callbacks
1050 1476 $callback_result = apply_filters(
1051 1477 $matched_intent->callback_function,
1052 1478 false, // default return value
@@ -1056,24 +1482,17 @@
1056 1482 $matched_intent
1057 1483 );
1058 1484 }
1059 1485
1060 - //error_log('🔍 MXCHAT DEBUG: Callback result type: ' . gettype($callback_result));
1061 1486 if ($callback_result !== false) {
1062 - //error_log('✅ MXCHAT DEBUG: Intent handled successfully');
1063 1487 $this->fallbackResponse = $callback_result;
1064 1488 return true;
1065 1489 }
1066 - //error_log('❌ MXCHAT DEBUG: Callback returned false');
1067 - } else {
1068 - //error_log('❌ MXCHAT DEBUG: No matching intent found');
1069 1490 }
1070 1491
1071 - //error_log('🔍 MXCHAT DEBUG: Intent Check Completed ==================');
1072 1492 return false;
1073 1493 }
1074 1494
1075 -
1076 1495 // Helper function to clear PDF and Word document related transients
1077 1496 private function clear_pdf_transients($session_id) {
1078 1497 // PDF transients
1079 1498 delete_transient('mxchat_pdf_url_' . $session_id);
@@ -1092,61 +1511,76 @@
1092 1511
1093 1512
1094 1513 //verified good
1095 1514 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 -
1515 + // Get the user's original instruction/message
1516 + $user_instruction = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Please provide your email address.", 'mxchat'));
1517 +
1518 + // Set instruction for AI - just pass along what the user wanted to say
1519 + $this->current_action_instruction = $user_instruction;
1520 +
1521 + // Set the transient to track email capture flow
1102 1522 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();
1523 +
1524 + // Return false to let the AI generate the response
1525 + return false;
1108 1526 }
1109 1527
1110 -//very good
1111 1528 public function mxchat_generate_image($message, $user_id, $session_id) {
1529 + //error_log("Starting image generation for message: " . $message);
1530 +
1112 1531 // Prepare a prompt for DALL-E
1113 1532 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1114 -
1533 +
1115 1534 // Use the existing OpenAI API key
1116 1535 $openai_api_key = sanitize_text_field($this->options['api_key']);
1117 -
1536 +
1118 1537 // Call DALL-E to generate an image
1119 1538 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1120 -
1539 +
1121 1540 // Check if the response contains an image URL
1122 1541 if (isset($image_response['imageUrl'])) {
1123 1542 $image_url = esc_url_raw($image_response['imageUrl']);
1124 -
1543 +
1125 1544 // Construct the HTML with a CSS class instead of inline styles
1126 1545 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1546 + $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1547 +
1548 + // Save the bot message with both text and HTML
1549 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1550 + $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1551 +
1552 + // Set the fallback response for the chat handler
1553 + $this->fallbackResponse = [
1554 + 'text' => $response_text,
1555 + 'html' => $response_html,
1556 + 'images' => [$image_url]
1557 + ];
1558 +
1559 + // For debugging/verification - Use json_encode to verify what's being set
1560 + //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1127 1561
1128 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1562 + // Return the response directly instead of relying on the property
1563 + return $this->fallbackResponse;
1129 1564 } else {
1130 1565 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1131 - $response_html = '';
1566 +
1567 + // Save the error message
1568 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1569 +
1570 + // Set the fallback response for the chat handler
1571 + $this->fallbackResponse = [
1572 + 'text' => $response_text,
1573 + 'html' => '',
1574 + 'images' => []
1575 + ];
1576 +
1132 1577 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1578 + //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1579 +
1580 + // Return the response directly instead of relying on the property
1581 + return $this->fallbackResponse;
1133 1582 }
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 1583 }
1150 1584 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1151 1585 $api_url = 'https://api.openai.com/v1/images/generations';
1152 1586 $body = json_encode([
@@ -1185,44 +1619,43 @@
1185 1619
1186 1620 /**
1187 1621 * Handle web search requests.
1188 1622 *
1189 - * Sends the refined search query to the Brave Search API and displays neatly formatted,
1190 - * styled search results. Results are cached for performance.
1623 + * Sends the refined search query to the Brave Search API and uses the
1624 + * results to generate a conversational response with the AI model.
1191 1625 *
1192 1626 * @since 1.0.0
1193 1627 * @param string $message The user's search query.
1194 1628 * @param string $user_id The user identifier.
1195 1629 * @param string $session_id The current session ID.
1196 - * @return void
1630 + * @return array Response array containing text with embedded HTML links
1197 1631 */
1198 -public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1632 +public function mxchat_handle_search_request($message, $user_id, $session_id) {
1199 1633 // 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' ),
1634 + $refined_search_query = $this->mxchat_interpret_search_query($message);
1635 + if (empty($refined_search_query)) {
1636 + return array(
1637 + 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1638 + 'html' => ''
1205 1639 );
1206 - return;
1207 1640 }
1208 -
1641 +
1209 1642 // 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' ),
1643 + $options = get_option('mxchat_options');
1644 + $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1645 + $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1646 +
1647 + if (empty($api_key)) {
1648 + return array(
1649 + 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1650 + 'html' => ''
1217 1651 );
1218 - return;
1219 1652 }
1220 -
1653 +
1221 1654 // Build the API request URL
1222 1655 $api_url = add_query_arg(
1223 1656 array(
1224 - 'q' => rawurlencode( $refined_search_query ),
1657 + 'q' => rawurlencode($refined_search_query),
1225 1658 'count' => $results_count,
1226 1659 'text_decorations' => 'true',
1227 1660 'rich_data' => 'true',
1228 1661 ),
@@ -1227,14 +1660,14 @@
1227 1660 'rich_data' => 'true',
1228 1661 ),
1229 1662 'https://api.search.brave.com/res/v1/web/search'
1230 1663 );
1231 -
1664 +
1232 1665 // 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 ) {
1666 + $transient_key = 'mxchat_search_' . md5($refined_search_query);
1667 + $results = get_transient($transient_key);
1668 +
1669 + if (false === $results) {
1237 1670 // Fetch new results from the Brave Search API
1238 1671 $response = wp_remote_get(
1239 1672 $api_url,
1240 1673 array(
@@ -1245,162 +1678,98 @@
1245 1678 ),
1246 1679 'timeout' => 10,
1247 1680 )
1248 1681 );
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' ),
1682 +
1683 + if (is_wp_error($response)) {
1684 + return array(
1685 + 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1686 + 'html' => ''
1253 1687 );
1254 - return;
1255 1688 }
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' ),
1689 +
1690 + $results = json_decode(wp_remote_retrieve_body($response), true);
1691 +
1692 + if (json_last_error() !== JSON_ERROR_NONE) {
1693 + return array(
1694 + 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1695 + 'html' => ''
1262 1696 );
1263 - return;
1264 1697 }
1265 -
1698 +
1266 1699 // Cache results for one hour
1267 - set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1700 + set_transient($transient_key, $results, HOUR_IN_SECONDS);
1268 1701 }
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,
1702 +
1703 + // Process results
1704 + if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1705 + // Create a more straightforward summary with HTML links
1706 + $search_results_text = '';
1707 +
1708 + // Add a simple intro
1709 + $search_results_text .= sprintf(
1710 + esc_html__("Here's what I found about '%s':", 'mxchat'),
1711 + esc_html($refined_search_query)
1277 1712 );
1278 -
1713 +
1714 + // Add the top results with HTML links
1715 + foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1716 + $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1717 + $url = isset($result['url']) ? esc_url($result['url']) : '';
1718 + $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1719 +
1720 + // Add a line break after the intro
1721 + $search_results_text .= '<br><br>';
1722 +
1723 + // Add title as a link
1724 + $search_results_text .= sprintf(
1725 + '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1726 + $url,
1727 + $title
1728 + );
1729 +
1730 + // Add a condensed description
1731 + $search_results_text .= sprintf("%s", $description);
1732 + }
1733 +
1279 1734 // Save to chat history
1280 - $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1735 + $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1736 +
1737 + // Return the formatted text with embedded HTML links
1738 + return array(
1739 + 'text' => $search_results_text,
1740 + 'html' => ''
1741 + );
1281 1742 } else {
1282 - $this->fallbackResponse = array(
1743 + return array(
1283 1744 '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 )
1745 + esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1746 + esc_html($refined_search_query)
1286 1747 ),
1748 + 'html' => ''
1287 1749 );
1288 1750 }
1289 1751 }
1290 1752
1291 -
1753 +//very good
1292 1754 /**
1293 - * Format search results into a natural text summary.
1755 + * Handle image search requests from the chatbot
1294 1756 *
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.
1757 + * @param string $message The user's search query
1758 + * @param int $user_id The user's ID
1759 + * @param string $session_id The chat session ID
1760 + * @return array Response array with text and HTML content
1299 1761 */
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 1762 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1391 -
1392 - // Step 1: Interpret the search query for better results
1763 + // Step 1: Interpret the search query using the user's selected AI model
1393 1764 $refined_search_query = $this->mxchat_interpret_search_query($message);
1394 1765
1395 -
1396 1766 // If no query was interpreted, return a fallback message
1397 1767 if (empty($refined_search_query)) {
1398 - $this->fallbackResponse = [
1768 + return array(
1399 1769 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1400 1770 'html' => "",
1401 - ];
1402 - return;
1771 + );
1403 1772 }
1404 1773
1405 1774 // Brave API URL
1406 1775 $api_url = 'https://api.search.brave.com/res/v1/images/search';
@@ -1409,19 +1778,12 @@
1409 1778 $options = get_option('mxchat_options');
1410 1779 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1411 1780
1412 1781 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 = [
1782 + return array(
1420 1783 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1421 1784 'html' => "",
1422 - ];
1423 - return;
1785 + );
1424 1786 }
1425 1787
1426 1788 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1427 1789 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
@@ -1432,16 +1794,8 @@
1432 1794 'count' => $image_count,
1433 1795 'safesearch' => $safe_search,
1434 1796 ], $api_url);
1435 1797
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 1798 // Implement caching
1445 1799 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1446 1800 $body = get_transient($transient_key);
1447 1801
@@ -1457,19 +1811,12 @@
1457 1811
1458 1812 $response = wp_remote_get($api_url, $args);
1459 1813
1460 1814 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 = [
1815 + return array(
1468 1816 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1469 1817 'html' => "",
1470 - ];
1471 - return;
1818 + );
1472 1819 }
1473 1820
1474 1821 $body = json_decode(wp_remote_retrieve_body($response), true);
1475 1822 set_transient($transient_key, $body, HOUR_IN_SECONDS);
@@ -1477,10 +1824,16 @@
1477 1824
1478 1825 // Process the API response
1479 1826 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1480 1827 $html_output = '<div class="mxchat-image-gallery">';
1481 -
1482 - foreach ($body['results'] as $image) {
1828 +
1829 + // Get the configured image count (1-6)
1830 + $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1831 + $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
1832 +
1833 + // Use only the requested number of images
1834 + for ($i = 0; $i < $display_count; $i++) {
1835 + $image = $body['results'][$i];
1483 1836 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1484 1837 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1485 1838 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1486 1839
@@ -1494,47 +1847,95 @@
1494 1847 }
1495 1848
1496 1849 $html_output .= '</div>';
1497 1850
1498 - $this->fallbackResponse = [
1499 - 'text' => "",
1500 - 'html' => $html_output,
1501 - ];
1502 -
1503 - // Save response in chat history
1851 + // Create response text
1852 + $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
1853 +
1854 + // Save both response text and HTML to chat history
1855 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1504 1856 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1505 1857
1858 + // Return the combined response
1859 + return array(
1860 + 'text' => $response_text,
1861 + 'html' => $html_output,
1862 + );
1506 1863 } 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'),
1864 + $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
1865 +
1866 + // Save the error message to chat history
1867 + $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1868 +
1869 + return array(
1870 + 'text' => $response_text,
1515 1871 'html' => "",
1516 - ];
1872 + );
1517 1873 }
1518 1874 }
1875 +
1876 +/**
1877 + * Interpret the search query using the user's selected AI model
1878 + *
1879 + * @param string $user_query The original query from the user
1880 + * @return string The refined search query
1881 + */
1519 1882 public function mxchat_interpret_search_query($user_query) {
1520 1883 $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"));
1884 +
1885 + // Get options and determine the selected model
1886 + $options = $this->options ?? get_option('mxchat_options');
1887 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1888 +
1889 + // Extract model prefix to determine the provider
1890 + $model_parts = explode('-', $selected_model);
1891 + $provider = strtolower($model_parts[0]);
1892 +
1893 + // Determine which API key to use based on the provider
1894 + switch ($provider) {
1895 + case 'gemini':
1896 + $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
1897 + if (empty($api_key)) {
1898 + return sanitize_text_field($user_query); // Default to original query if API key missing
1899 + }
1900 + return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
1901 +
1902 + case 'claude':
1903 + $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
1904 + if (empty($api_key)) {
1905 + return sanitize_text_field($user_query);
1906 + }
1907 + return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
1908 +
1909 + case 'grok':
1910 + $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
1911 + if (empty($api_key)) {
1912 + return sanitize_text_field($user_query);
1913 + }
1914 + return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
1915 +
1916 + case 'deepseek':
1917 + $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
1918 + if (empty($api_key)) {
1919 + return sanitize_text_field($user_query);
1920 + }
1921 + return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
1922 +
1923 + case 'gpt':
1924 + default:
1925 + // Default to OpenAI for custom models or unrecognized prefixes
1926 + $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
1927 + if (empty($api_key)) {
1928 + return sanitize_text_field($user_query);
1929 + }
1930 + return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1529 1931 }
1530 - */
1932 +}
1531 1933
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 -
1934 +/**
1935 + * Interpret query using OpenAI models
1936 + */
1937 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1537 1938 $url = 'https://api.openai.com/v1/chat/completions';
1538 1939 $args = [
1539 1940 'headers' => [
1540 1941 'Authorization' => 'Bearer ' . $api_key,
@@ -1540,9 +1941,9 @@
1540 1941 'Authorization' => 'Bearer ' . $api_key,
1541 1942 'Content-Type' => 'application/json',
1542 1943 ],
1543 1944 'body' => wp_json_encode([
1544 - 'model' => 'gpt-3.5-turbo',
1945 + 'model' => $model,
1545 1946 'messages' => [
1546 1947 ['role' => 'system', 'content' => $system_prompt],
1547 1948 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1548 1949 ],
@@ -1549,166 +1950,178 @@
1549 1950 'temperature' => 0.2,
1550 1951 'max_tokens' => 20,
1551 1952 ]),
1552 1953 'method' => 'POST',
1954 + 'timeout' => 15,
1553 1955 ];
1554 1956
1555 1957 $response = wp_remote_post($url, $args);
1556 -
1557 1958 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
1959 + return sanitize_text_field($user_query);
1560 1960 }
1561 1961
1562 1962 $body = json_decode(wp_remote_retrieve_body($response), true);
1963 + return isset($body['choices'][0]['message']['content'])
1964 + ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
1965 + : sanitize_text_field($user_query);
1966 +}
1563 1967
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']));
1968 +/**
1969 + * Interpret query using Claude models
1970 + */
1971 +private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
1972 + $url = 'https://api.anthropic.com/v1/messages';
1973 +
1974 + $args = [
1975 + 'headers' => [
1976 + 'Content-Type' => 'application/json',
1977 + 'x-api-key' => $api_key,
1978 + 'anthropic-version' => '2023-06-01',
1979 + ],
1980 + 'body' => wp_json_encode([
1981 + 'model' => $model,
1982 + 'system' => $system_prompt,
1983 + 'messages' => [
1984 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
1985 + ],
1986 + 'max_tokens' => 20,
1987 + 'temperature' => 0.2,
1988 + ]),
1989 + 'method' => 'POST',
1990 + 'timeout' => 15,
1991 + ];
1567 1992
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 - */
1993 + $response = wp_remote_post($url, $args);
1994 + if (is_wp_error($response)) {
1995 + return sanitize_text_field($user_query);
1996 + }
1574 1997
1575 - return $interpreted_query;
1576 - } else {
1577 - //error_log("Unexpected API response format: " . print_r($body, true));
1578 - return sanitize_text_field($user_query);
1998 + $body = json_decode(wp_remote_retrieve_body($response), true);
1999 + if (!empty($body['content'][0]['text'])) {
2000 + return sanitize_text_field(trim($body['content'][0]['text']));
1579 2001 }
2002 +
2003 + return sanitize_text_field($user_query);
1580 2004 }
1581 2005
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;
2006 +/**
2007 + * Interpret query using Gemini models
2008 + */
2009 +private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2010 + // Strip "gemini-" prefix for the API
2011 + $model_version = str_replace('gemini-', '', $model);
2012 +
2013 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2014 +
2015 + $args = [
2016 + 'headers' => [
2017 + 'Content-Type' => 'application/json',
2018 + ],
2019 + 'body' => wp_json_encode([
2020 + 'contents' => [
2021 + [
2022 + 'role' => 'user',
2023 + 'parts' => [
2024 + ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
2025 + ]
2026 + ]
2027 + ],
2028 + 'generationConfig' => [
2029 + 'temperature' => 0.2,
2030 + 'maxOutputTokens' => 20,
2031 + ],
2032 + ]),
2033 + 'method' => 'POST',
2034 + 'timeout' => 15,
2035 + ];
2036 +
2037 + $response = wp_remote_post($url, $args);
2038 + if (is_wp_error($response)) {
2039 + return sanitize_text_field($user_query);
1591 2040 }
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;
2041 +
2042 + $body = json_decode(wp_remote_retrieve_body($response), true);
2043 + if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
2044 + return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1599 2045 }
2046 +
2047 + return sanitize_text_field($user_query);
2048 +}
1600 2049
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 - }
2050 +/**
2051 + * Interpret query using X.AI (Grok) models
2052 + */
2053 +private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
2054 + $url = 'https://api.xai.com/v1/chat/completions';
2055 +
2056 + $args = [
2057 + 'headers' => [
2058 + 'Content-Type' => 'application/json',
2059 + 'Authorization' => 'Bearer ' . $api_key,
2060 + ],
2061 + 'body' => wp_json_encode([
2062 + 'model' => $model,
2063 + 'messages' => [
2064 + ['role' => 'system', 'content' => $system_prompt],
2065 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2066 + ],
2067 + 'temperature' => 0.2,
2068 + 'max_tokens' => 20,
2069 + ]),
2070 + 'method' => 'POST',
2071 + 'timeout' => 15,
2072 + ];
2073 +
2074 + $response = wp_remote_post($url, $args);
2075 + if (is_wp_error($response)) {
2076 + return sanitize_text_field($user_query);
1633 2077 }
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 - }
2078 +
2079 + $body = json_decode(wp_remote_retrieve_body($response), true);
2080 + if (isset($body['choices'][0]['message']['content'])) {
2081 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1649 2082 }
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;
2083 +
2084 + return sanitize_text_field($user_query);
1654 2085 }
1655 2086
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;
2087 +/**
2088 + * Interpret query using DeepSeek models
2089 + */
2090 +private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
2091 + $url = 'https://api.deepseek.com/v1/chat/completions';
2092 +
2093 + $args = [
2094 + 'headers' => [
2095 + 'Content-Type' => 'application/json',
2096 + 'Authorization' => 'Bearer ' . $api_key,
2097 + ],
2098 + 'body' => wp_json_encode([
2099 + 'model' => $model,
2100 + 'messages' => [
2101 + ['role' => 'system', 'content' => $system_prompt],
2102 + ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2103 + ],
2104 + 'temperature' => 0.2,
2105 + 'max_tokens' => 20,
2106 + ]),
2107 + 'method' => 'POST',
2108 + 'timeout' => 15,
2109 + ];
2110 +
2111 + $response = wp_remote_post($url, $args);
2112 + if (is_wp_error($response)) {
2113 + return sanitize_text_field($user_query);
1705 2114 }
1706 -
1707 - return $context_string;
2115 +
2116 + $body = json_decode(wp_remote_retrieve_body($response), true);
2117 + if (isset($body['choices'][0]['message']['content'])) {
2118 + return sanitize_text_field(trim($body['choices'][0]['message']['content']));
2119 + }
2120 +
2121 + return sanitize_text_field($user_query);
1708 2122 }
1709 2123
1710 -
1711 2124 //very good
1712 2125 private function add_email_to_loops($email) {
1713 2126 // Sanitize the email
1714 2127 $email = sanitize_email($email);
@@ -1792,95 +2205,169 @@
1792 2205
1793 2206 // Default to proceeding with conversation if no specific PDF action is needed
1794 2207 $this->fallbackResponse['text'] = '';
1795 2208 }
2209 +
2210 +
2211 +/**
2212 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
2213 + */
1796 2214 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2215 + // CLEAR DEBUG LOGGING
2216 + error_log("=== MXCHAT PDF PROCESSING START ===");
2217 + error_log("PDF Source: " . $pdf_source);
2218 + error_log("Max Pages: " . $max_pages);
2219 + error_log("Session ID: " . ($this->session_id ?? 'not set'));
2220 +
2221 + // Check if Advanced Claude Toolbar is available and enabled
2222 + $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
2223 + $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
2224 +
2225 + error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2226 + error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2227 +
2228 + if ($claude_available && $claude_enabled) {
2229 + error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2230 +
2231 + // Attempt Claude processing first
2232 + $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
2233 +
2234 + if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
2235 + error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
2236 + error_log("Claude returned " . count($claude_result) . " processed pages");
2237 +
2238 + // Log first page details for verification
2239 + if (isset($claude_result[0])) {
2240 + $first_page = $claude_result[0];
2241 + error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2242 + error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2243 + error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2244 + }
2245 +
2246 + error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2247 + return $claude_result;
2248 + } else {
2249 + error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
2250 + error_log("Claude result type: " . gettype($claude_result));
2251 + if (is_array($claude_result)) {
2252 + error_log("Claude result count: " . count($claude_result));
2253 + }
2254 + }
2255 + }
2256 +
2257 + // Fallback to basic processing
2258 + error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2259 +
1797 2260 $upload_dir = wp_upload_dir();
1798 2261 $temp_file = null;
1799 -
2262 +
1800 2263 try {
1801 - // Handle URL vs local file
2264 + // Your existing basic processing code here...
2265 + // (I'll include the key parts with debug logging)
2266 +
1802 2267 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 -
2268 + error_log("Downloading PDF from URL...");
2269 + $temp_file = wp_tempnam($pdf_source);
2270 + $response = wp_remote_get($pdf_source, [
2271 + 'timeout' => 60,
2272 + 'headers' => ['User-Agent' => 'MxChat PDF Processor']
2273 + ]);
2274 +
1807 2275 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));
2276 + $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
2277 + error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
1809 2278 return false;
1810 2279 }
1811 -
2280 +
1812 2281 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 - }
2282 + error_log("✅ PDF downloaded successfully");
1821 2283 } else {
1822 - // For local files, use the provided path directly
1823 2284 $temp_file = $pdf_source;
2285 + error_log("Using local PDF file: " . $temp_file);
1824 2286 }
1825 -
1826 - // Parse and process the PDF
2287 +
2288 + // Parse PDF
2289 + error_log("Parsing PDF with basic parser...");
1827 2290 $parser = new \Smalot\PdfParser\Parser();
1828 2291 $pdf = $parser->parseFile($temp_file);
1829 2292 $pages = $pdf->getPages();
1830 -
2293 +
2294 + error_log("PDF contains " . count($pages) . " pages");
2295 +
1831 2296 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)) {
2297 + error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2298 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
1834 2299 unlink($temp_file);
1835 2300 }
1836 - return esc_html__('too_many_pages', 'mxchat');
2301 + return 'too_many_pages';
1837 2302 }
1838 -
2303 +
1839 2304 $embeddings = [];
2305 + $processed_pages = 0;
2306 +
1840 2307 foreach ($pages as $page_number => $page) {
1841 2308 $text = $page->getText();
1842 -
1843 - // Ensure text is non-empty before generating embeddings
2309 +
1844 2310 if (empty(trim($text))) {
1845 - //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
2311 + error_log("Skipping empty page: " . ($page_number + 1));
1846 2312 continue;
1847 2313 }
1848 -
2314 +
2315 + $text = $this->mxchat_clean_text($text);
2316 +
1849 2317 $embedding = $this->mxchat_generate_embedding(
1850 - esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2318 + __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
1851 2319 $this->options['api_key']
1852 2320 );
1853 -
2321 +
1854 2322 if ($embedding) {
1855 2323 $embeddings[] = [
1856 2324 'page_number' => $page_number + 1,
1857 2325 'embedding' => $embedding,
1858 2326 'text' => $text,
2327 + 'enhanced' => false, // CLEARLY MARK AS BASIC
2328 + 'processing_method' => 'basic_pdf_parser'
1859 2329 ];
1860 - } else {
1861 - //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
2330 + $processed_pages++;
1862 2331 }
1863 2332 }
1864 -
1865 - // Clean up downloaded file if it was from URL
1866 - if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2333 +
2334 + error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2335 +
2336 + // Cleanup
2337 + if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1867 2338 unlink($temp_file);
1868 2339 }
1869 -
2340 +
2341 + error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
1870 2342 return $embeddings;
1871 -
2343 +
1872 2344 } catch (\Exception $e) {
1873 - // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
1874 -
1875 - // Cleanup in case of exception
2345 + error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
1876 2346 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1877 2347 unlink($temp_file);
1878 2348 }
1879 -
2349 + error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
1880 2350 return false;
1881 2351 }
1882 2352 }
2353 +
2354 +private function mxchat_clean_text($text) {
2355 + // Remove excessive whitespace
2356 + $text = preg_replace('/\s+/', ' ', $text);
2357 +
2358 + // Remove control characters except newlines and tabs
2359 + $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
2360 +
2361 + // Normalize line endings
2362 + $text = str_replace(["\r\n", "\r"], "\n", $text);
2363 +
2364 + // Trim whitespace
2365 + $text = trim($text);
2366 +
2367 + return $text;
2368 +}
2369 +
1883 2370 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
1884 2371 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
1885 2372
1886 2373 $most_relevant = null;
@@ -2038,10 +2525,8 @@
2038 2525 'new_messages' => array_values($new_messages)
2039 2526 ]);
2040 2527 wp_die();
2041 2528 }
2042 -
2043 -
2044 2529 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2045 2530 // First check if live agents are available
2046 2531 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2047 2532 if ($live_agent_available !== 'on') {
@@ -2060,18 +2545,101 @@
2060 2545 ]);
2061 2546 wp_die();
2062 2547 }
2063 2548
2064 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2065 - if (empty($slack_webhook_url)) {
2549 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2550 +
2551 + if (empty($slack_bot_token)) {
2066 2552 return false;
2067 2553 }
2068 2554
2069 - // Get recent chat history (last 5 messages)
2555 + // Check if channel already exists for this session
2556 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2557 +
2558 + if (empty($channel_id)) {
2559 + // Create new channel with session ID as name
2560 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
2561 +
2562 + //error_log("Attempting to create channel: $channel_name");
2563 +
2564 + $response = wp_remote_post('https://slack.com/api/conversations.create', [
2565 + 'headers' => [
2566 + 'Content-Type' => 'application/json',
2567 + 'Authorization' => 'Bearer ' . $slack_bot_token
2568 + ],
2569 + 'body' => json_encode([
2570 + 'name' => $channel_name,
2571 + 'is_private' => false // Public channel - anyone in workspace can join
2572 + ])
2573 + ]);
2574 +
2575 + if (!is_wp_error($response)) {
2576 + $response_body = wp_remote_retrieve_body($response);
2577 + $response_data = json_decode($response_body, true);
2578 +
2579 + //error_log("Channel creation response: " . $response_body);
2580 +
2581 + if (isset($response_data['ok']) && $response_data['ok']) {
2582 + $channel_id = $response_data['channel']['id'];
2583 + $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2584 + //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2585 + update_option("mxchat_channel_{$session_id}", $channel_id);
2586 +
2587 + // Auto-invite agents to the channel
2588 + $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2589 +
2590 + if (!empty($agent_user_ids)) {
2591 + // Parse user IDs (one per line)
2592 + $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2593 +
2594 + foreach ($user_ids as $user_id_to_invite) {
2595 + //error_log("Inviting user to channel: $user_id_to_invite");
2596 +
2597 + $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2598 + 'headers' => [
2599 + 'Content-Type' => 'application/json',
2600 + 'Authorization' => 'Bearer ' . $slack_bot_token
2601 + ],
2602 + 'body' => json_encode([
2603 + 'channel' => $channel_id,
2604 + 'users' => $user_id_to_invite
2605 + ])
2606 + ]);
2607 +
2608 + if (!is_wp_error($invite_response)) {
2609 + $invite_body = wp_remote_retrieve_body($invite_response);
2610 + $invite_data = json_decode($invite_body, true);
2611 + //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2612 +
2613 + if (isset($invite_data['ok']) && $invite_data['ok']) {
2614 + //error_log("Successfully invited user $user_id_to_invite to channel");
2615 + } else {
2616 + //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2617 + }
2618 + } else {
2619 + //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2620 + }
2621 + }
2622 + } else {
2623 + //error_log("No agent user IDs configured for auto-invite");
2624 + }
2625 + } else {
2626 + //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2627 + }
2628 + } else {
2629 + //error_log("WP Error creating channel: " . $response->get_error_message());
2630 + }
2631 +
2632 + if (empty($channel_id)) {
2633 + return false; // Failed to create channel
2634 + }
2635 + }
2636 +
2637 + // Get recent chat history
2070 2638 $history = get_option("mxchat_history_{$session_id}", []);
2071 - $recent_history = array_slice($history, -5); // Get last 5 messages
2639 + $recent_history = array_slice($history, -5);
2072 2640
2073 - // Format conversation history
2641 + // Format conversation context
2074 2642 $conversation_context = "";
2075 2643 if (!empty($recent_history)) {
2076 2644 $conversation_context = "*Recent Conversation:*\n";
2077 2645 foreach ($recent_history as $hist_message) {
@@ -2082,83 +2650,32 @@
2082 2650 }
2083 2651
2084 2652 update_option("mxchat_mode_{$session_id}", 'agent');
2085 2653
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
2654 + // Send message to channel
2655 + $channel_message = "🔔 *New Live Agent Request*\n\n";
2656 + $channel_message .= "*Session ID:* `{$session_id}`\n";
2657 + $channel_message .= "*User ID:* `{$user_id}`\n\n";
2658 +
2113 2659 if (!empty($conversation_context)) {
2114 - $webhook_data['blocks'][] = [
2115 - 'type' => 'section',
2116 - 'text' => [
2117 - 'type' => 'mrkdwn',
2118 - 'text' => $conversation_context
2119 - ]
2120 - ];
2660 + $channel_message .= $conversation_context;
2121 2661 }
2662 +
2663 + $channel_message .= "*Current Message:*\n{$message}\n\n";
2664 + $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2122 2665
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),
2666 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2152 2667 'headers' => [
2153 2668 'Content-Type' => 'application/json',
2669 + 'Authorization' => 'Bearer ' . $slack_bot_token
2154 2670 ],
2671 + 'body' => json_encode([
2672 + 'channel' => $channel_id,
2673 + 'text' => $channel_message,
2674 + 'mrkdwn' => true
2675 + ])
2155 2676 ]);
2156 2677
2157 - if (is_wp_error($response)) {
2158 - return false;
2159 - }
2160 -
2161 2678 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2162 2679 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2163 2680
2164 2681 $this->fallbackResponse = [
@@ -2178,78 +2695,30 @@
2178 2695 ]);
2179 2696 wp_die();
2180 2697 }
2181 2698 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2182 - $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2699 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2700 + $channel_id = get_option("mxchat_channel_{$session_id}", '');
2183 2701
2184 - if (empty($slack_webhook_url)) {
2185 - //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
2702 + if (empty($slack_bot_token) || empty($channel_id)) {
2186 2703 return false;
2187 2704 }
2188 2705
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 - ];
2706 + $user_message = "💬 *User:* {$message}";
2237 2707
2238 - $response = wp_remote_post($slack_webhook_url, [
2239 - 'body' => json_encode($webhook_data),
2708 + $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2240 2709 'headers' => [
2241 2710 'Content-Type' => 'application/json',
2711 + 'Authorization' => 'Bearer ' . $slack_bot_token
2242 2712 ],
2713 + 'body' => json_encode([
2714 + 'channel' => $channel_id,
2715 + 'text' => $user_message,
2716 + 'mrkdwn' => true
2717 + ])
2243 2718 ]);
2244 2719
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;
2720 + return !is_wp_error($response);
2252 2721 }
2253 2722 public function handle_slack_interaction(WP_REST_Request $request) {
2254 2723 //error_log('Received Slack interaction');
2255 2724
@@ -2337,17 +2806,16 @@
2337 2806
2338 2807 // Default acknowledgment
2339 2808 return new WP_REST_Response(['ok' => true]);
2340 2809 }
2341 -
2342 2810 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2343 2811 //error_log('Received agent response request');
2344 2812 //error_log('Request data: ' . print_r($request->get_params(), true));
2345 - // error_log('Raw body: ' . file_get_contents('php://input'));
2813 + // //error_log('Raw body: ' . file_get_contents('php://input'));
2346 2814
2347 2815 // Get the data from Slack's slash command format
2348 2816 $command_text = $request->get_param('text');
2349 - // error_log('Command text: ' . $command_text);
2817 + // //error_log('Command text: ' . $command_text);
2350 2818
2351 2819 if (empty($command_text)) {
2352 2820 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2353 2821 return new WP_REST_Response([
@@ -2372,9 +2840,9 @@
2372 2840 // Save the message
2373 2841 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2374 2842
2375 2843 if (!$message_id) {
2376 - // error_log('Failed to save agent message');
2844 + // //error_log('Failed to save agent message');
2377 2845 return new WP_REST_Response([
2378 2846 'error' => esc_html__('Failed to save message', 'mxchat')
2379 2847 ], 500);
2380 2848 }
@@ -2384,10 +2852,8 @@
2384 2852 'response_type' => 'in_channel',
2385 2853 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2386 2854 ], 200);
2387 2855 }
2388 -
2389 -
2390 2856 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2391 2857 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2392 2858
2393 2859 // Just update mode to AI
@@ -2401,12 +2867,122 @@
2401 2867 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2402 2868
2403 2869 return true; // Intent was handled
2404 2870 }
2871 +public function handle_slack_messages(WP_REST_Request $request) {
2872 + // Log the incoming request for debugging
2873 + //error_log('Slack events request received: ' . $request->get_body());
2874 +
2875 + $body = $request->get_body();
2876 + $data = json_decode($body, true);
2877 +
2878 + // Handle Slack URL verification
2879 + if (isset($data['type']) && $data['type'] === 'url_verification') {
2880 + //error_log('Slack URL verification challenge: ' . $data['challenge']);
2881 + return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
2882 + }
2883 +
2884 + // IMPORTANT: Handle Slack's event deduplication
2885 + if (isset($data['event_id'])) {
2886 + $event_id = $data['event_id'];
2887 + $processed_events = get_transient('mxchat_slack_events') ?: [];
2888 +
2889 + // Check if we've already processed this event
2890 + if (in_array($event_id, $processed_events)) {
2891 + //error_log("Duplicate event detected: $event_id");
2892 + return new WP_REST_Response(['ok' => true]);
2893 + }
2894 +
2895 + // Add this event to processed list
2896 + $processed_events[] = $event_id;
2897 + // Keep only last 100 events to prevent memory issues
2898 + if (count($processed_events) > 100) {
2899 + $processed_events = array_slice($processed_events, -100);
2900 + }
2901 + // Store for 1 hour
2902 + set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
2903 + }
2904 +
2905 + // Handle message events
2906 + if (isset($data['event']) && $data['event']['type'] === 'message') {
2907 + $event = $data['event'];
2908 +
2909 + // Skip bot messages and messages with subtypes (like bot_message)
2910 + if (isset($event['bot_id']) || isset($event['subtype'])) {
2911 + return new WP_REST_Response(['ok' => true]);
2912 + }
2913 +
2914 + // Additional check: Skip if this is a threaded reply to our confirmation
2915 + if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
2916 + return new WP_REST_Response(['ok' => true]);
2917 + }
2918 +
2919 + $channel_id = $event['channel'];
2920 + $message_text = $event['text'] ?? '';
2921 + $message_ts = $event['ts'] ?? '';
2922 +
2923 + // Find session ID by looking for matching channel
2924 + global $wpdb;
2925 + $session_option = $wpdb->get_var(
2926 + $wpdb->prepare(
2927 + "SELECT option_name FROM {$wpdb->options}
2928 + WHERE option_name LIKE 'mxchat_channel_%'
2929 + AND option_value = %s",
2930 + $channel_id
2931 + )
2932 + );
2933 +
2934 + if ($session_option) {
2935 + $session_id = str_replace('mxchat_channel_', '', $session_option);
2936 +
2937 + // Create a unique key for this specific message
2938 + $message_key = md5($session_id . $message_ts . $message_text);
2939 + $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
2940 +
2941 + // Check if we've already processed this exact message
2942 + if (in_array($message_key, $processed_messages)) {
2943 + //error_log("Duplicate message detected for session $session_id");
2944 + return new WP_REST_Response(['ok' => true]);
2945 + }
2946 +
2947 + // Add to processed messages
2948 + $processed_messages[] = $message_key;
2949 + // Keep only last 50 messages per session
2950 + if (count($processed_messages) > 50) {
2951 + $processed_messages = array_slice($processed_messages, -50);
2952 + }
2953 + set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
2954 +
2955 + // Save the agent message
2956 + $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
2957 +
2958 + // Send confirmation back to Slack (only once)
2959 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2960 + if (!empty($slack_bot_token)) {
2961 + // Use a transient to prevent duplicate confirmations
2962 + $confirm_key = 'mxchat_confirm_' . $message_key;
2963 + if (!get_transient($confirm_key)) {
2964 + wp_remote_post('https://slack.com/api/chat.postMessage', [
2965 + 'headers' => [
2966 + 'Content-Type' => 'application/json',
2967 + 'Authorization' => 'Bearer ' . $slack_bot_token
2968 + ],
2969 + 'body' => json_encode([
2970 + 'channel' => $channel_id,
2971 + 'text' => "✅ _Message sent to user_",
2972 + 'thread_ts' => $event['ts'] // Reply in thread
2973 + ])
2974 + ]);
2975 + // Set transient to prevent duplicate confirmations
2976 + set_transient($confirm_key, true, 300); // 5 minutes
2977 + }
2978 + }
2979 + }
2980 + }
2981 +
2982 + return new WP_REST_Response(['ok' => true]);
2983 +}
2405 2984
2406 -
2407 -
2408 -
2409 2985 // For the word upload handler
2410 2986 public function mxchat_handle_word_upload() {
2411 2987 // Delegate to word handler
2412 2988 $this->word_handler->mxchat_handle_word_upload();
@@ -2429,21 +3005,102 @@
2429 3005 return MxChat_User::mxchat_get_user_identifier();
2430 3006 }
2431 3007
2432 3008 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 -
3009 + try {
3010 + // Get options and selected model
3011 + $options = get_option('mxchat_options');
3012 + $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3013 +
3014 + // Determine endpoint and API key based on model
3015 + if (strpos($selected_model, 'voyage') === 0) {
3016 + $endpoint = 'https://api.voyageai.com/v1/embeddings';
3017 + $api_key = $options['voyage_api_key'] ?? '';
3018 +
3019 + // Check if Voyage API key is missing
3020 + if (empty($api_key)) {
3021 + //error_log('Voyage API key is missing');
3022 + return [
3023 + 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
3024 + 'error_code' => 'missing_voyage_api_key'
3025 + ];
3026 + }
3027 + } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3028 + $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3029 + $api_key = $options['gemini_api_key'] ?? '';
3030 +
3031 + // Check if Gemini API key is missing
3032 + if (empty($api_key)) {
3033 + //error_log('Gemini API key is missing');
3034 + return [
3035 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3036 + 'error_code' => 'missing_gemini_api_key'
3037 + ];
3038 + }
3039 + } else {
3040 + $endpoint = 'https://api.openai.com/v1/embeddings';
3041 + // Use the passed API key for OpenAI
3042 +
3043 + // Check if OpenAI API key is missing
3044 + if (empty($api_key)) {
3045 + //error_log('OpenAI API key is missing');
3046 + return [
3047 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3048 + 'error_code' => 'missing_openai_api_key'
3049 + ];
3050 + }
3051 + }
3052 +
3053 + // Check if text is empty
3054 + if (empty($text)) {
3055 + //error_log('Empty text provided for embedding generation');
3056 + return [
3057 + 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
3058 + 'error_code' => 'empty_embedding_text'
3059 + ];
3060 + }
3061 +
3062 + // Prepare request body based on provider
3063 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3064 + // Gemini API format
3065 + $request_body = [
3066 + 'model' => 'models/' . $selected_model,
3067 + 'content' => [
3068 + 'parts' => [
3069 + ['text' => $text]
3070 + ]
3071 + ],
3072 + 'outputDimensionality' => 1536
3073 + ];
3074 +
3075 + // Prepare headers for Gemini (API key as query parameter)
3076 + $endpoint .= '?key=' . $api_key;
3077 + $headers = [
3078 + 'Content-Type' => 'application/json'
3079 + ];
3080 + } else {
3081 + // OpenAI/Voyage API format
3082 + $request_body = [
3083 + 'input' => $text,
3084 + 'model' => $selected_model
3085 + ];
3086 +
3087 + // Add output_dimension for voyage-3-large
3088 + if ($selected_model === 'voyage-3-large') {
3089 + $request_body['output_dimension'] = 2048;
3090 + }
3091 +
3092 + // Prepare headers for OpenAI/Voyage
3093 + $headers = [
3094 + 'Content-Type' => 'application/json',
3095 + 'Authorization' => 'Bearer ' . $api_key
3096 + ];
3097 + }
3098 +
3099 + // Prepare request arguments
2440 3100 $args = [
2441 - 'body' => $body,
2442 - 'headers' => [
2443 - 'Content-Type' => 'application/json',
2444 - 'Authorization' => 'Bearer ' . $api_key,
2445 - ],
3101 + 'body' => wp_json_encode($request_body),
3102 + 'headers' => $headers,
2446 3103 'timeout' => 60,
2447 3104 'redirection' => 5,
2448 3105 'blocking' => true,
2449 3106 'httpversion' => '1.0',
@@ -2448,25 +3105,109 @@
2448 3105 'blocking' => true,
2449 3106 'httpversion' => '1.0',
2450 3107 'sslverify' => true,
2451 3108 ];
2452 -
3109 +
3110 + // Make the request
2453 3111 $response = wp_remote_post($endpoint, $args);
2454 -
3112 +
3113 + // Handle WordPress errors
2455 3114 if (is_wp_error($response)) {
2456 - return null;
3115 + $error_message = $response->get_error_message();
3116 + //error_log('Embedding Generation Error: ' . $error_message);
3117 + return [
3118 + 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
3119 + 'error_code' => 'embedding_connection_error'
3120 + ];
2457 3121 }
2458 -
3122 +
3123 + // Check HTTP status code
3124 + $status_code = wp_remote_retrieve_response_code($response);
3125 + if ($status_code !== 200) {
3126 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
3127 +
3128 + $error_message = isset($response_body['error']['message'])
3129 + ? $response_body['error']['message']
3130 + : 'HTTP Error ' . $status_code;
3131 +
3132 + $error_type = isset($response_body['error']['type'])
3133 + ? $response_body['error']['type']
3134 + : 'unknown';
3135 +
3136 + //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
3137 +
3138 + // Handle specific error types
3139 + switch ($error_type) {
3140 + case 'invalid_request_error':
3141 + if (strpos($error_message, 'API key') !== false) {
3142 + return [
3143 + 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
3144 + 'error_code' => 'embedding_invalid_api_key'
3145 + ];
3146 + }
3147 + break;
3148 +
3149 + case 'authentication_error':
3150 + return [
3151 + 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
3152 + 'error_code' => 'embedding_auth_error'
3153 + ];
3154 +
3155 + case 'rate_limit_exceeded':
3156 + return [
3157 + 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
3158 + 'error_code' => 'embedding_rate_limit'
3159 + ];
3160 +
3161 + case 'quota_exceeded':
3162 + return [
3163 + 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
3164 + 'error_code' => 'embedding_quota_exceeded'
3165 + ];
3166 + }
3167 +
3168 + // Generic error fallback
3169 + return [
3170 + 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3171 + 'error_code' => 'embedding_api_error',
3172 + 'status_code' => $status_code
3173 + ];
3174 + }
3175 +
2459 3176 $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'];
3177 +
3178 + // Handle different response formats based on provider
3179 + if (strpos($selected_model, 'gemini-embedding') === 0) {
3180 + // Gemini API response format
3181 + if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3182 + return $response_body['embedding']['values'];
3183 + } else {
3184 + //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3185 + return [
3186 + 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3187 + 'error_code' => 'invalid_gemini_embedding_response'
3188 + ];
3189 + }
2463 3190 } else {
2464 - return null;
3191 + // OpenAI/Voyage API response format
3192 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3193 + return $response_body['data'][0]['embedding'];
3194 + } else {
3195 + //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3196 + return [
3197 + 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3198 + 'error_code' => 'invalid_embedding_response'
3199 + ];
3200 + }
2465 3201 }
3202 + } catch (Exception $e) {
3203 + //error_log('Embedding Exception: ' . $e->getMessage());
3204 + return [
3205 + 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3206 + 'error_code' => 'embedding_exception'
3207 + ];
2466 3208 }
2467 -
2468 -
3209 +}
2469 3210 private function mxchat_find_relevant_content($user_embedding) {
2470 3211 //error_log('MXChat Vector Search: Starting content search...');
2471 3212
2472 3213 // Retrieve the add-on settings from the database.
@@ -2472,9 +3213,8 @@
2472 3213 // Retrieve the add-on settings from the database.
2473 3214 $addon_options = get_option('mxchat_pinecone_addon_options', array());
2474 3215
2475 3216 // Determine whether Pinecone is enabled.
2476 - // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
2477 3217 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
2478 3218
2479 3219 //error_log('Pinecone enabled flag: ' . $use_pinecone);
2480 3220
@@ -2492,18 +3232,26 @@
2492 3232 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2493 3233 $cache_key = 'mxchat_system_prompt_embeddings';
2494 3234 $batch_size = 500;
2495 3235
3236 + // Initialize similarity analysis storage
3237 + $this->last_similarity_analysis = [
3238 + 'knowledge_base_type' => 'WordPress Database',
3239 + 'top_matches' => [],
3240 + 'threshold_used' => 0,
3241 + 'total_checked' => 0
3242 + ];
3243 +
2496 3244 // Retrieve embeddings from cache or database
2497 3245 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2498 3246 if ($embeddings === false) {
3247 + // Cache miss - load embeddings from database WITH CONTENT for testing
2499 3248 $embeddings = [];
2500 3249 $offset = 0;
2501 3250
2502 - // Load in batches and build cache
2503 3251 do {
2504 3252 $query = $wpdb->prepare(
2505 - "SELECT id, embedding_vector
3253 + "SELECT id, embedding_vector, article_content, source_url
2506 3254 FROM {$system_prompt_table}
2507 3255 LIMIT %d OFFSET %d",
2508 3256 $batch_size,
2509 3257 $offset
@@ -2515,62 +3263,125 @@
2515 3263 }
2516 3264
2517 3265 $embeddings = array_merge($embeddings, $batch);
2518 3266 $offset += $batch_size;
2519 -
2520 - // Free memory
2521 3267 unset($batch);
2522 -
2523 3268 } while (true);
2524 3269
2525 3270 if (empty($embeddings)) {
2526 - return ''; // Return an empty string if no embeddings found
3271 + return '';
2527 3272 }
3273 +
3274 + // Cache embeddings for future use (but note: this now includes content)
2528 3275 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2529 3276 }
2530 3277
2531 - // Initialize array to store relevant results with similarity scores
3278 + // Get configuration options
3279 + $main_options = get_option('mxchat_options', []);
3280 +
3281 + // Get base similarity threshold (default 75%)
3282 + $similarity_threshold = isset($main_options['similarity_threshold'])
3283 + ? ((int) $main_options['similarity_threshold']) / 100
3284 + : 0.75;
3285 +
3286 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3287 +
3288 + // Calculate similarities and build results array
3289 + $all_similarities = [];
2532 3290 $relevant_results = [];
2533 - // Iterate through embeddings to calculate similarity
3291 +
2534 3292 foreach ($embeddings as $embedding) {
2535 3293 $database_embedding = $embedding->embedding_vector
2536 3294 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2537 3295 : null;
3296 +
2538 3297 if (is_array($database_embedding) && is_array($user_embedding)) {
2539 3298 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2540 - $relevant_results[] = [
2541 - 'id' => $embedding->id,
2542 - 'similarity' => $similarity
3299 +
3300 + // Store ALL similarities for testing (top 10)
3301 + $source_display = '';
3302 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3303 + $source_display = $embedding->source_url;
3304 + } else {
3305 + $content_preview = strip_tags($embedding->article_content ?? '');
3306 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3307 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3308 + }
3309 +
3310 + $all_similarities[] = [
3311 + 'document_id' => $embedding->id,
3312 + 'similarity' => $similarity,
3313 + 'similarity_percentage' => round($similarity * 100, 2),
3314 + 'above_threshold' => $similarity >= $similarity_threshold,
3315 + 'source_display' => $source_display,
3316 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3317 + 'used_for_context' => false // Initialize as false, we'll update this later
2543 3318 ];
3319 +
3320 + // Only consider results above threshold for actual content retrieval
3321 + if ($similarity >= $similarity_threshold) {
3322 + $relevant_results[] = [
3323 + 'id' => $embedding->id,
3324 + 'similarity' => $similarity
3325 + ];
3326 + }
2544 3327 }
2545 - // Free memory
3328 +
2546 3329 unset($database_embedding);
2547 3330 }
2548 3331
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;
3332 + // Sort ALL similarities for testing display (highest first)
3333 + usort($all_similarities, function ($a, $b) {
3334 + return $b['similarity'] <=> $a['similarity'];
2555 3335 });
3336 +
3337 + // Sort relevant results by similarity (highest first)
2556 3338 usort($relevant_results, function ($a, $b) {
2557 3339 return $b['similarity'] <=> $a['similarity'];
2558 3340 });
2559 -
2560 - // Limit to the top 5 results
3341 +
3342 + // Get top 5 results for actual content (standard approach)
2561 3343 $top_results = array_slice($relevant_results, 0, 5);
2562 -
2563 - // Initialize the final content
3344 +
3345 + // NOW mark which documents are actually used for context
3346 + $used_document_ids = [];
3347 + foreach ($top_results as $result) {
3348 + $used_document_ids[] = $result['id'];
3349 + }
3350 +
3351 + // Update the all_similarities array to mark which were actually used
3352 + foreach ($all_similarities as &$similarity_item) {
3353 + $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
3354 + }
3355 +
3356 + // Store top 10 for testing panel (now with correct used_for_context flags)
3357 + $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
3358 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3359 +
3360 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3361 +
3362 + // Initialize final content
2564 3363 $content = '';
2565 -
2566 - // Fetch and combine content for the top results
2567 - foreach ($top_results as $result) {
3364 +
3365 + // Track document IDs to avoid duplicates
3366 + $added_document_ids = [];
3367 +
3368 + // Fetch and format content for each selected result
3369 + foreach ($top_results as $index => $result) {
3370 + if (in_array($result['id'], $added_document_ids)) {
3371 + continue;
3372 + }
3373 +
2568 3374 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2569 - // Check if the content is PDF-related and add surrounding pages
3375 + $added_document_ids[] = $result['id'];
3376 +
3377 + $content .= "## Reference " . ($index + 1) . " ##\n";
3378 + $content .= $chunk_content . "\n\n";
3379 +
3380 + // PDF surrounding pages logic (unchanged)
2570 3381 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2571 3382 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2572 - "SELECT article_content FROM {$system_prompt_table}
3383 + "SELECT id, article_content FROM {$system_prompt_table}
2573 3384 WHERE id IN (
2574 3385 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
2575 3386 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
2576 3387 )",
@@ -2576,52 +3387,73 @@
2576 3387 )",
2577 3388 $result['id'],
2578 3389 $result['id']
2579 3390 ));
2580 - // Add previous content if it exists
3391 +
2581 3392 if (!empty($surrounding_content[0])) {
3393 + $content .= "## Related Content ##\n";
2582 3394 $content .= $surrounding_content[0]->article_content . "\n\n";
3395 + $added_document_ids[] = $surrounding_content[0]->id;
2583 3396 }
2584 - // Add the main chunk content
2585 - $content .= $chunk_content . "\n\n";
2586 - // Add next content if it exists
3397 +
2587 3398 if (!empty($surrounding_content[1])) {
3399 + $content .= "## Related Content ##\n";
2588 3400 $content .= $surrounding_content[1]->article_content . "\n\n";
3401 + $added_document_ids[] = $surrounding_content[1]->id;
2589 3402 }
3403 + }
3404 + }
3405 +
3406 + // Add response guidelines
3407 + if (empty($top_results)) {
3408 + $content = "No reference information was found for this query.\n\n";
2590 3409 } else {
2591 - // For non-PDF content, add directly
2592 - $content .= $chunk_content . "\n\n";
3410 + $content .= "\n## Response Guidelines ##\n" .
3411 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3412 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3413 + "If you don't have specific information or are uncertain about any details, it's always " .
3414 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3415 + "When information is incomplete, let them know you are unsure.";
2593 3416 }
2594 - }
2595 -
3417 +
2596 3418 return trim($content);
2597 3419 }
2598 -/**
2599 - * Find relevant content in Pinecone vector database
2600 - */
3420 +
2601 3421 private function find_relevant_content_pinecone($user_embedding) {
2602 3422 $options = get_option('mxchat_pinecone_addon_options', array());
2603 3423 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2604 3424 $host = $options['mxchat_pinecone_host'] ?? '';
2605 -
3425 +
3426 + // Initialize similarity analysis storage
3427 + $this->last_similarity_analysis = [
3428 + 'knowledge_base_type' => 'Pinecone',
3429 + 'top_matches' => [],
3430 + 'threshold_used' => 0,
3431 + 'total_checked' => 0
3432 + ];
3433 +
2606 3434 if (empty($host) || empty($api_key)) {
2607 - //error_log('Pinecone credentials not properly configured');
2608 3435 return '';
2609 3436 }
2610 -
2611 - // Get similarity threshold from WordPress settings
2612 - $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2613 -
2614 - // Prepare the query request for Pinecone
3437 +
3438 + // Get the similarity threshold from the main options
3439 + $main_options = get_option('mxchat_options', []);
3440 + $similarity_threshold = isset($main_options['similarity_threshold'])
3441 + ? ((int) $main_options['similarity_threshold']) / 100
3442 + : 0.75;
3443 +
3444 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3445 +
3446 + // Prepare the query request for Pinecone (request more for testing)
2615 3447 $api_endpoint = "https://{$host}/query";
2616 -
3448 +
2617 3449 $request_body = array(
2618 3450 'vector' => $user_embedding,
2619 - 'topK' => 5,
3451 + 'topK' => 20, // Request more to get good testing data
2620 3452 'includeMetadata' => true,
2621 3453 'includeValues' => true
2622 3454 );
2623 -
3455 +
2624 3456 $response = wp_remote_post($api_endpoint, array(
2625 3457 'headers' => array(
2626 3458 'Api-Key' => $api_key,
2627 3459 'accept' => 'application/json',
@@ -2629,46 +3461,120 @@
2629 3461 ),
2630 3462 'body' => wp_json_encode($request_body),
2631 3463 'timeout' => 30
2632 3464 ));
2633 -
3465 +
2634 3466 if (is_wp_error($response)) {
2635 - //error_log('Pinecone query error: ' . $response->get_error_message());
2636 3467 return '';
2637 3468 }
2638 -
3469 +
2639 3470 $response_code = wp_remote_retrieve_response_code($response);
2640 3471 if ($response_code !== 200) {
2641 - //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
2642 3472 return '';
2643 3473 }
2644 -
3474 +
2645 3475 $results = json_decode(wp_remote_retrieve_body($response), true);
2646 3476 if (empty($results['matches'])) {
2647 3477 return '';
2648 3478 }
2649 -
3479 +
3480 + // First, determine which matches will actually be used for content
3481 + $matches_used_for_context = [];
3482 + $matches_used = 0;
3483 +
3484 + foreach ($results['matches'] as $index => $match) {
3485 + // Skip if similarity is below threshold
3486 + if ($match['score'] < $similarity_threshold) {
3487 + continue;
3488 + }
3489 +
3490 + // Limit to top 5 matches above threshold
3491 + if ($matches_used >= 5) {
3492 + break;
3493 + }
3494 +
3495 + if (!empty($match['metadata']['text'])) {
3496 + $matches_used_for_context[] = $match['id'] ?? $index;
3497 + $matches_used++;
3498 + }
3499 + }
3500 +
3501 + // Process ALL matches for testing data (top 10)
3502 + $all_matches = [];
3503 + foreach ($results['matches'] as $index => $match) {
3504 + if ($index >= 10) break; // Limit to top 10 for testing
3505 +
3506 + $source_display = '';
3507 + if (!empty($match['metadata']['source_url'])) {
3508 + $source_display = $match['metadata']['source_url'];
3509 + } else {
3510 + $content_preview = strip_tags($match['metadata']['text'] ?? '');
3511 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3512 + $source_display = substr(trim($content_preview), 0, 50) . '...';
3513 + }
3514 +
3515 + $match_id = $match['id'] ?? $index;
3516 +
3517 + $all_matches[] = [
3518 + 'document_id' => $match_id,
3519 + 'similarity' => $match['score'],
3520 + 'similarity_percentage' => round($match['score'] * 100, 2),
3521 + 'above_threshold' => $match['score'] >= $similarity_threshold,
3522 + 'source_display' => $source_display,
3523 + 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
3524 + 'used_for_context' => in_array($match_id, $matches_used_for_context) // Correct usage flag
3525 + ];
3526 + }
3527 +
3528 + // Store for testing panel
3529 + $this->last_similarity_analysis['top_matches'] = $all_matches;
3530 + $this->last_similarity_analysis['total_checked'] = count($results['matches']);
3531 +
3532 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
3533 +
2650 3534 // Initialize the final content
2651 3535 $content = '';
2652 -
2653 - // Process each match
2654 - foreach ($results['matches'] as $match) {
3536 + $matches_used = 0;
3537 +
3538 + // Process each match for actual content (this is the real content generation)
3539 + foreach ($results['matches'] as $index => $match) {
2655 3540 // Skip if similarity is below threshold
2656 3541 if ($match['score'] < $similarity_threshold) {
2657 3542 continue;
2658 3543 }
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";
3544 +
3545 + // Limit to top 5 matches above threshold
3546 + if ($matches_used >= 5) {
3547 + break;
2664 3548 }
3549 +
3550 + if (!empty($match['metadata']['text'])) {
3551 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3552 + $content .= $match['metadata']['text'] . "\n\n";
3553 +
3554 + if (!empty($match['metadata']['source_url'])) {
3555 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3556 + }
3557 +
3558 + $matches_used++;
3559 + }
2665 3560 }
2666 -
3561 +
3562 + // Add response guidelines
3563 + if ($matches_used === 0) {
3564 + $content = "No reference information was found for this query.\n\n";
3565 + } else {
3566 + $content .= "\n## Response Guidelines ##\n" .
3567 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3568 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3569 + "If you don't have specific information or are uncertain about any details, it's always " .
3570 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3571 + "When information is incomplete, let them know you are unsure.";
3572 + }
3573 +
2667 3574 return trim($content);
2668 3575 }
2669 3576
2670 -
2671 3577 private function mxchat_find_relevant_products($user_embedding) {
2672 3578 //error_log('MXChat Vector Search: Starting product search...');
2673 3579
2674 3580 // Retrieve the add-on settings from the database
@@ -2686,9 +3592,8 @@
2686 3592 //error_log('MXChat Vector Search: Using WordPress database for products');
2687 3593 return $this->find_relevant_products_wordpress($user_embedding);
2688 3594 }
2689 3595 }
2690 -
2691 3596 private function find_relevant_products_wordpress($user_embedding) {
2692 3597 global $wpdb;
2693 3598 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2694 3599 $cache_key = 'mxchat_system_prompt_embeddings';
@@ -2762,10 +3667,8 @@
2762 3667 }
2763 3668
2764 3669 return trim($content);
2765 3670 }
2766 -
2767 -// Modified search function with correct filter syntax
2768 3671 private function find_relevant_products_pinecone($user_embedding) {
2769 3672 //error_log('Starting Pinecone product search...');
2770 3673
2771 3674 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -2840,10 +3743,8 @@
2840 3743 }
2841 3744
2842 3745 return trim($content);
2843 3746 }
2844 -
2845 -
2846 3747 private function fetch_content_with_product_links($most_relevant_id) {
2847 3748 global $wpdb;
2848 3749 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2849 3750
@@ -2862,315 +3763,962 @@
2862 3763
2863 3764 return null;
2864 3765 }
2865 3766
2866 -// Function definition
2867 -private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
3767 +/**
3768 + * Modified streaming functions to include testing data
3769 + */
3770 +
3771 +// 1. Update the main handler to pass testing data to streaming functions
3772 +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 3773 try {
2869 3774 if (!$relevant_content) {
2870 - return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
3775 + $error_response = [
3776 + 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
3777 + 'error_code' => 'no_relevant_content'
3778 + ];
3779 +
3780 + // Add testing data to error response if available
3781 + if ($testing_data !== null) {
3782 + $error_response['testing_data'] = $testing_data;
3783 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
3784 + }
3785 +
3786 + return $error_response;
2871 3787 }
2872 -
3788 +
2873 3789 // Ensure conversation_history is an array
2874 3790 if (!is_array($conversation_history)) {
2875 3791 $conversation_history = array();
2876 3792 }
2877 -
3793 +
2878 3794 // Get selected model with default fallback
2879 3795 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
2880 -
3796 +
2881 3797 // Extract model prefix to determine the provider
2882 3798 $model_parts = explode('-', $selected_model);
2883 3799 $provider = strtolower($model_parts[0]);
2884 -
3800 +
2885 3801 // Handle model selection based on provider prefix
2886 3802 switch ($provider) {
2887 - case 'claude':
2888 - if (empty($claude_api_key)) {
2889 - throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
3803 + case 'gemini':
3804 + if (empty($gemini_api_key)) {
3805 + $error_response = [
3806 + 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3807 + 'error_code' => 'missing_gemini_api_key'
3808 + ];
3809 + if ($testing_data !== null) {
3810 + $error_response['testing_data'] = $testing_data;
3811 + }
3812 + return $error_response;
2890 3813 }
2891 - return $this->mxchat_generate_response_claude(
3814 + $response = $this->mxchat_generate_response_gemini(
2892 3815 $selected_model,
2893 - $claude_api_key,
3816 + $gemini_api_key,
2894 3817 $conversation_history,
2895 3818 $relevant_content
2896 3819 );
2897 -
3820 + break;
3821 +
3822 + case 'claude':
3823 + if (empty($claude_api_key)) {
3824 + $error_response = [
3825 + 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
3826 + 'error_code' => 'missing_claude_api_key'
3827 + ];
3828 + if ($testing_data !== null) {
3829 + $error_response['testing_data'] = $testing_data;
3830 + }
3831 + return $error_response;
3832 + }
3833 + if ($streaming) {
3834 + return $this->mxchat_generate_response_claude_stream(
3835 + $selected_model,
3836 + $claude_api_key,
3837 + $conversation_history,
3838 + $relevant_content,
3839 + $session_id,
3840 + $testing_data // Pass testing data
3841 + );
3842 + } else {
3843 + $response = $this->mxchat_generate_response_claude(
3844 + $selected_model,
3845 + $claude_api_key,
3846 + $conversation_history,
3847 + $relevant_content
3848 + );
3849 + }
3850 + break;
3851 +
2898 3852 case 'grok':
2899 3853 if (empty($xai_api_key)) {
2900 - throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
3854 + $error_response = [
3855 + 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
3856 + 'error_code' => 'missing_xai_api_key'
3857 + ];
3858 + if ($testing_data !== null) {
3859 + $error_response['testing_data'] = $testing_data;
3860 + }
3861 + return $error_response;
2901 3862 }
2902 - return $this->mxchat_generate_response_xai(
2903 - $selected_model,
2904 - $xai_api_key,
2905 - $conversation_history,
2906 - $relevant_content
2907 - );
2908 -
3863 + if ($streaming) {
3864 + return $this->mxchat_generate_response_xai_stream(
3865 + $selected_model,
3866 + $xai_api_key,
3867 + $conversation_history,
3868 + $relevant_content,
3869 + $session_id,
3870 + $testing_data // Pass testing data
3871 + );
3872 + } else {
3873 + $response = $this->mxchat_generate_response_xai(
3874 + $selected_model,
3875 + $xai_api_key,
3876 + $conversation_history,
3877 + $relevant_content
3878 + );
3879 + }
3880 + break;
3881 +
2909 3882 case 'deepseek':
2910 3883 if (empty($deepseek_api_key)) {
2911 - throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
3884 + $error_response = [
3885 + 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
3886 + 'error_code' => 'missing_deepseek_api_key'
3887 + ];
3888 + if ($testing_data !== null) {
3889 + $error_response['testing_data'] = $testing_data;
3890 + }
3891 + return $error_response;
2912 3892 }
2913 - return $this->mxchat_generate_response_deepseek(
3893 + $response = $this->mxchat_generate_response_deepseek(
2914 3894 $selected_model,
2915 3895 $deepseek_api_key,
2916 3896 $conversation_history,
2917 3897 $relevant_content
2918 3898 );
2919 -
3899 + break;
3900 +
2920 3901 case 'gpt':
3902 + case 'o1':
2921 3903 if (empty($api_key)) {
2922 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3904 + $error_response = [
3905 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3906 + 'error_code' => 'missing_openai_api_key'
3907 + ];
3908 + if ($testing_data !== null) {
3909 + $error_response['testing_data'] = $testing_data;
3910 + }
3911 + return $error_response;
2923 3912 }
2924 - return $this->mxchat_generate_response_openai(
2925 - $selected_model,
2926 - $api_key,
2927 - $conversation_history,
2928 - $relevant_content
2929 - );
2930 -
3913 + if ($streaming) {
3914 + return $this->mxchat_generate_response_openai_stream(
3915 + $selected_model,
3916 + $api_key,
3917 + $conversation_history,
3918 + $relevant_content,
3919 + $session_id,
3920 + $testing_data // Pass testing data
3921 + );
3922 + } else {
3923 + $response = $this->mxchat_generate_response_openai(
3924 + $selected_model,
3925 + $api_key,
3926 + $conversation_history,
3927 + $relevant_content
3928 + );
3929 + }
3930 + break;
3931 +
2931 3932 default:
2932 3933 // Default to OpenAI for custom models or unrecognized prefixes
2933 3934 if (empty($api_key)) {
2934 - throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3935 + $error_response = [
3936 + 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3937 + 'error_code' => 'missing_openai_api_key'
3938 + ];
3939 + if ($testing_data !== null) {
3940 + $error_response['testing_data'] = $testing_data;
3941 + }
3942 + return $error_response;
2935 3943 }
2936 - return $this->mxchat_generate_response_openai(
2937 - $selected_model,
2938 - $api_key,
2939 - $conversation_history,
2940 - $relevant_content
2941 - );
3944 + if ($streaming) {
3945 + return $this->mxchat_generate_response_openai_stream(
3946 + $selected_model,
3947 + $api_key,
3948 + $conversation_history,
3949 + $relevant_content,
3950 + $session_id,
3951 + $testing_data // Pass testing data
3952 + );
3953 + } else {
3954 + $response = $this->mxchat_generate_response_openai(
3955 + $selected_model,
3956 + $api_key,
3957 + $conversation_history,
3958 + $relevant_content
3959 + );
3960 + }
3961 + break;
2942 3962 }
3963 +
3964 + // Check if the response is an error array from the provider-specific function
3965 + if (is_array($response) && isset($response['error'])) {
3966 + // Add testing data to error response if available
3967 + if ($testing_data !== null) {
3968 + $response['testing_data'] = $testing_data;
3969 + //error_log("MxChat Testing: Added testing data to provider error response");
3970 + }
3971 + return $response; // Pass through the error with testing data
3972 + }
3973 +
3974 + // For successful non-streaming responses, we don't add testing data here
3975 + // because it will be added in the main handler
3976 + return $response;
3977 +
2943 3978 } catch (Exception $e) {
2944 3979 //error_log('MXChat Error: ' . $e->getMessage());
2945 - return sprintf(
2946 - esc_html__('An error occurred: %s', 'mxchat'),
2947 - esc_html($e->getMessage())
2948 - );
3980 + $error_response = [
3981 + 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
3982 + 'error_code' => 'system_exception',
3983 + 'exception_details' => $e->getMessage()
3984 + ];
3985 +
3986 + // Add testing data to exception response if available
3987 + if ($testing_data !== null) {
3988 + $error_response['testing_data'] = $testing_data;
3989 + //error_log("MxChat Testing: Added testing data to exception response");
3990 + }
3991 +
3992 + return $error_response;
2949 3993 }
2950 3994 }
2951 3995
3996 +// 2. streaming function
3997 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
3998 + try {
3999 + // Get system prompt instructions from options
4000 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
2952 4001
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 - }
4002 + // Ensure conversation_history is an array
4003 + if (!is_array($conversation_history)) {
4004 + $conversation_history = array();
4005 + }
2958 4006
2959 - // Get system prompt instructions from options
2960 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4007 + // Clean and validate conversation history
4008 + foreach ($conversation_history as &$message) {
4009 + // Convert bot and agent roles to assistant
4010 + if ($message['role'] === 'bot' || $message['role'] === 'agent') {
4011 + $message['role'] = 'assistant';
4012 + }
4013 +
4014 + // Remove unsupported roles - Claude only supports 'assistant' and 'user'
4015 + if (!in_array($message['role'], ['assistant', 'user'])) {
4016 + $message['role'] = 'user';
4017 + }
2961 4018
2962 - // Create a new array for the formatted conversation
2963 - $formatted_conversation = array();
4019 + // Ensure content field exists
4020 + if (!isset($message['content']) || empty($message['content'])) {
4021 + $message['content'] = '';
4022 + }
2964 4023
2965 - // Add system message first
2966 - $formatted_conversation[] = array(
2967 - 'role' => 'system',
2968 - 'content' => $system_prompt_instructions . " " . $relevant_content
2969 - );
4024 + // Remove any unsupported fields
4025 + $message = array_intersect_key($message, array_flip(['role', 'content']));
4026 + }
2970 4027
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'];
4028 + // Add relevant content as the latest user message
4029 + $conversation_history[] = [
4030 + 'role' => 'user',
4031 + 'content' => $relevant_content
4032 + ];
2975 4033
2976 - // Convert roles to supported format
2977 - if ($role === 'bot' || $role === 'agent') {
2978 - $role = 'assistant';
4034 + // Prepare the request body with stream: true
4035 + $body = json_encode([
4036 + 'model' => $selected_model,
4037 + 'messages' => $conversation_history,
4038 + 'max_tokens' => 1000,
4039 + 'temperature' => 0.8,
4040 + 'system' => $system_prompt_instructions,
4041 + 'stream' => true
4042 + ]);
4043 +
4044 + // Check if we can actually stream (headers not sent, etc.)
4045 + if (headers_sent() || !function_exists('curl_init')) {
4046 + // Fallback to regular response with testing data
4047 + //error_log("MxChat: Streaming not possible, falling back to regular response");
4048 + $regular_response = $this->mxchat_generate_response_claude(
4049 + $selected_model,
4050 + $claude_api_key,
4051 + array_slice($conversation_history, 0, -1), // Remove the added content
4052 + $relevant_content
4053 + );
4054 +
4055 + // Return as JSON with testing data
4056 + $response_data = [
4057 + 'text' => $regular_response,
4058 + 'html' => '',
4059 + 'session_id' => $session_id
4060 + ];
4061 +
4062 + if ($testing_data !== null) {
4063 + $response_data['testing_data'] = $testing_data;
4064 + //error_log("MxChat Testing: Added testing data to Claude fallback response");
2979 4065 }
2980 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
2981 - $role = 'user';
4066 +
4067 + // Clear any streaming headers and send JSON
4068 + if (headers_sent() === false) {
4069 + header('Content-Type: application/json');
2982 4070 }
4071 + echo json_encode($response_data);
4072 + return true; // Indicate we handled the response
4073 + }
2983 4074
2984 - $formatted_conversation[] = array(
2985 - 'role' => $role,
2986 - 'content' => $message['content']
2987 - );
2988 - }
2989 - }
4075 + // Use cURL for streaming support
4076 + $ch = curl_init();
4077 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
4078 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4079 + curl_setopt($ch, CURLOPT_POST, true);
4080 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4081 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4082 + 'Content-Type: application/json',
4083 + 'x-api-key: ' . $claude_api_key,
4084 + 'anthropic-version: 2023-06-01'
4085 + ));
4086 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4087 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
2990 4088
2991 - $body = json_encode([
2992 - 'model' => $selected_model,
2993 - 'messages' => $formatted_conversation,
2994 - 'temperature' => 0.8,
2995 - 'stream' => false
2996 - ]);
4089 + $full_response = ''; // Accumulate full response for saving
4090 + $stream_started = false;
2997 4091
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 - ];
4092 + // Buffer control for real-time streaming
4093 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4094 + // Send testing data as the first event if available
4095 + if (!$stream_started && $testing_data !== null) {
4096 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4097 + flush();
4098 + $stream_started = true;
4099 + //error_log("MxChat Testing: Sent testing data in Claude stream");
4100 + }
4101 +
4102 + // Process each chunk of data
4103 + $lines = explode("\n", $data);
3010 4104
3011 - $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
4105 + foreach ($lines as $line) {
4106 + if (trim($line) === '') {
4107 + continue;
4108 + }
3012 4109
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 - }
4110 + // Claude uses event: and data: format
4111 + if (strpos($line, 'event: ') === 0) {
4112 + // Store the event type for the next data line
4113 + continue;
4114 + }
3017 4115
3018 - $response_body = wp_remote_retrieve_body($response);
3019 - $decoded_response = json_decode($response_body, true);
4116 + if (strpos($line, 'data: ') === 0) {
4117 + $json_str = substr($line, 6); // Remove 'data: ' prefix
3020 4118
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 -}
4119 + $json = json_decode($json_str, true);
4120 + if (json_last_error() !== JSON_ERROR_NONE) {
4121 + continue;
4122 + }
3028 4123
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 - }
4124 + // Handle different event types
4125 + if (isset($json['type'])) {
4126 + switch ($json['type']) {
4127 + case 'content_block_delta':
4128 + if (isset($json['delta']['text'])) {
4129 + $content = $json['delta']['text'];
4130 + $full_response .= $content; // Accumulate
4131 + // Send as SSE format compatible with your frontend
4132 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4133 + flush();
4134 + }
4135 + break;
3034 4136
3035 - // Get system prompt instructions from options
3036 - $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4137 + case 'message_stop':
4138 + echo "data: [DONE]\n\n";
4139 + flush();
4140 + break;
3037 4141
3038 - // Create a new array for the formatted conversation
3039 - $formatted_conversation = array();
4142 + case 'error':
4143 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
4144 + flush();
4145 + break;
4146 + }
4147 + }
4148 + }
4149 + }
3040 4150
3041 - // Add system message first
3042 - $formatted_conversation[] = array(
3043 - 'role' => 'system',
3044 - 'content' => $system_prompt_instructions . " " . $relevant_content
3045 - );
4151 + return strlen($data);
4152 + });
3046 4153
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'];
4154 + $response = curl_exec($ch);
4155 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
3051 4156
3052 - // Convert roles to supported format
3053 - if ($role === 'bot' || $role === 'agent') {
3054 - $role = 'assistant';
4157 + if (curl_errno($ch)) {
4158 + curl_close($ch);
4159 + throw new Exception('cURL Error: ' . curl_error($ch));
4160 + }
4161 +
4162 + curl_close($ch);
4163 +
4164 + if ($http_code !== 200) {
4165 + // Fallback to regular response
4166 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
4167 + $regular_response = $this->mxchat_generate_response_claude(
4168 + $selected_model,
4169 + $claude_api_key,
4170 + array_slice($conversation_history, 0, -1), // Remove the added content
4171 + $relevant_content
4172 + );
4173 +
4174 + $response_data = [
4175 + 'text' => $regular_response,
4176 + 'html' => '',
4177 + 'session_id' => $session_id
4178 + ];
4179 +
4180 + if ($testing_data !== null) {
4181 + $response_data['testing_data'] = $testing_data;
4182 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
3055 4183 }
3056 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3057 - $role = 'user';
3058 - }
4184 +
4185 + header('Content-Type: application/json');
4186 + echo json_encode($response_data);
4187 + return true;
4188 + }
3059 4189
3060 - $formatted_conversation[] = array(
3061 - 'role' => $role,
3062 - 'content' => $message['content']
3063 - );
4190 + // Save the complete response to maintain chat persistence
4191 + if (!empty($full_response) && !empty($session_id)) {
4192 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
3064 4193 }
4194 +
4195 + return true; // Indicate streaming completed successfully
4196 +
4197 + } catch (Exception $e) {
4198 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
4199 +
4200 + // Fallback to regular response on exception
4201 + $regular_response = $this->mxchat_generate_response_claude(
4202 + $selected_model,
4203 + $claude_api_key,
4204 + $conversation_history,
4205 + $relevant_content
4206 + );
4207 +
4208 + $response_data = [
4209 + 'text' => $regular_response,
4210 + 'html' => '',
4211 + 'session_id' => $session_id
4212 + ];
4213 +
4214 + if ($testing_data !== null) {
4215 + $response_data['testing_data'] = $testing_data;
4216 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
4217 + }
4218 +
4219 + header('Content-Type: application/json');
4220 + echo json_encode($response_data);
4221 + return true;
3065 4222 }
4223 +}
4224 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4225 + try {
4226 + // Get system prompt instructions from options
4227 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4228 +
4229 + // Ensure conversation_history is an array
4230 + if (!is_array($conversation_history)) {
4231 + $conversation_history = array();
4232 + }
3066 4233
3067 - $body = json_encode([
3068 - 'model' => $selected_model,
3069 - 'messages' => $formatted_conversation,
3070 - 'temperature' => 0.8,
3071 - 'stream' => false
3072 - ]);
4234 + // Format conversation history for OpenAI
4235 + $formatted_conversation = array();
3073 4236
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 - ];
4237 + $formatted_conversation[] = array(
4238 + 'role' => 'system',
4239 + 'content' => $system_prompt_instructions . " " . $relevant_content
4240 + );
3086 4241
3087 - $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4242 + foreach ($conversation_history as $message) {
4243 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4244 + $role = $message['role'];
4245 + if ($role === 'bot' || $role === 'agent') {
4246 + $role = 'assistant';
4247 + }
4248 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4249 + $role = 'user';
4250 + }
4251 + $formatted_conversation[] = array(
4252 + 'role' => $role,
4253 + 'content' => $message['content']
4254 + );
4255 + }
4256 + }
3088 4257
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 - }
4258 + // Check if we can actually stream
4259 + if (headers_sent() || !function_exists('curl_init')) {
4260 + // Fallback to regular response with testing data
4261 + //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4262 + $regular_response = $this->mxchat_generate_response_openai(
4263 + $selected_model,
4264 + $api_key,
4265 + $conversation_history,
4266 + $relevant_content
4267 + );
4268 +
4269 + $response_data = [
4270 + 'text' => $regular_response,
4271 + 'html' => '',
4272 + 'session_id' => $session_id
4273 + ];
4274 +
4275 + if ($testing_data !== null) {
4276 + $response_data['testing_data'] = $testing_data;
4277 + //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
4278 + }
4279 +
4280 + header('Content-Type: application/json');
4281 + echo json_encode($response_data);
4282 + return true;
4283 + }
3093 4284
3094 - $response_body = wp_remote_retrieve_body($response);
3095 - $decoded_response = json_decode($response_body, true);
4285 + // Prepare the request body with stream: true
4286 + $body = json_encode([
4287 + 'model' => $selected_model,
4288 + 'messages' => $formatted_conversation,
4289 + 'temperature' => 0.8,
4290 + 'stream' => true
4291 + ]);
3096 4292
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.";
4293 + // Use cURL for streaming support
4294 + $ch = curl_init();
4295 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4296 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4297 + curl_setopt($ch, CURLOPT_POST, true);
4298 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4299 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4300 + 'Content-Type: application/json',
4301 + 'Authorization: Bearer ' . $api_key
4302 + ));
4303 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4304 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4305 +
4306 + $full_response = ''; // Accumulate full response for saving
4307 + $stream_started = false;
4308 +
4309 + // Buffer control for real-time streaming
4310 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4311 + // Send testing data as the first event if available
4312 + if (!$stream_started && $testing_data !== null) {
4313 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4314 + flush();
4315 + $stream_started = true;
4316 + //error_log("MxChat Testing: Sent testing data in OpenAI stream");
4317 + }
4318 +
4319 + // Process each chunk of data
4320 + $lines = explode("\n", $data);
4321 +
4322 + foreach ($lines as $line) {
4323 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4324 + continue;
4325 + }
4326 +
4327 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4328 +
4329 + if ($json_str === '[DONE]') {
4330 + echo "data: [DONE]\n\n";
4331 + flush();
4332 + continue;
4333 + }
4334 +
4335 + $json = json_decode($json_str, true);
4336 + if (isset($json['choices'][0]['delta']['content'])) {
4337 + $content = $json['choices'][0]['delta']['content'];
4338 + $full_response .= $content; // Accumulate
4339 + // Send as SSE format
4340 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4341 + flush();
4342 + }
4343 + }
4344 +
4345 + return strlen($data);
4346 + });
4347 +
4348 + $response = curl_exec($ch);
4349 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4350 +
4351 + if (curl_errno($ch) || $http_code !== 200) {
4352 + curl_close($ch);
4353 +
4354 + // Fallback to regular response
4355 + //error_log("MxChat: OpenAI streaming failed, falling back");
4356 + $regular_response = $this->mxchat_generate_response_openai(
4357 + $selected_model,
4358 + $api_key,
4359 + $conversation_history,
4360 + $relevant_content
4361 + );
4362 +
4363 + $response_data = [
4364 + 'text' => $regular_response,
4365 + 'html' => '',
4366 + 'session_id' => $session_id
4367 + ];
4368 +
4369 + if ($testing_data !== null) {
4370 + $response_data['testing_data'] = $testing_data;
4371 + //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
4372 + }
4373 +
4374 + header('Content-Type: application/json');
4375 + echo json_encode($response_data);
4376 + return true;
4377 + }
4378 +
4379 + curl_close($ch);
4380 +
4381 + // Save the complete response to maintain chat persistence
4382 + if (!empty($full_response) && !empty($session_id)) {
4383 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4384 + }
4385 +
4386 + return true; // Indicate streaming completed successfully
4387 +
4388 + } catch (Exception $e) {
4389 + //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4390 +
4391 + // Fallback to regular response
4392 + $regular_response = $this->mxchat_generate_response_openai(
4393 + $selected_model,
4394 + $api_key,
4395 + $conversation_history,
4396 + $relevant_content
4397 + );
4398 +
4399 + $response_data = [
4400 + 'text' => $regular_response,
4401 + 'html' => '',
4402 + 'session_id' => $session_id
4403 + ];
4404 +
4405 + if ($testing_data !== null) {
4406 + $response_data['testing_data'] = $testing_data;
4407 + //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
4408 + }
4409 +
4410 + header('Content-Type: application/json');
4411 + echo json_encode($response_data);
4412 + return true;
3102 4413 }
3103 4414 }
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'] : '';
4415 +private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4416 + try {
4417 + // Get system prompt instructions from options
4418 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4419 +
4420 + // Ensure conversation_history is an array
4421 + if (!is_array($conversation_history)) {
4422 + $conversation_history = array();
4423 + }
3107 4424
3108 - // Add system prompt to relevant content
3109 - $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4425 + // Format conversation history for X.AI (same as OpenAI format)
4426 + $formatted_conversation = array();
3110 4427
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 - ]);
4428 + $formatted_conversation[] = array(
4429 + 'role' => 'system',
4430 + 'content' => $system_prompt_instructions . " " . $relevant_content
4431 + );
3116 4432
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'];
4433 + foreach ($conversation_history as $message) {
4434 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4435 + $role = $message['role'];
4436 + if ($role === 'bot' || $role === 'agent') {
4437 + $role = 'assistant';
4438 + }
4439 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4440 + $role = 'user';
4441 + }
4442 + $formatted_conversation[] = array(
4443 + 'role' => $role,
4444 + 'content' => $message['content']
4445 + );
3126 4446 }
3127 4447 }
3128 4448
3129 - // Ensure all roles are valid
3130 - if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3131 - $message['role'] = 'user'; // Default to 'user'
4449 + // Check if we can actually stream
4450 + if (headers_sent() || !function_exists('curl_init')) {
4451 + // Fallback to regular response with testing data
4452 + //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
4453 + $regular_response = $this->mxchat_generate_response_xai(
4454 + $selected_model,
4455 + $xai_api_key,
4456 + $conversation_history,
4457 + $relevant_content
4458 + );
4459 +
4460 + $response_data = [
4461 + 'text' => $regular_response,
4462 + 'html' => '',
4463 + 'session_id' => $session_id
4464 + ];
4465 +
4466 + if ($testing_data !== null) {
4467 + $response_data['testing_data'] = $testing_data;
4468 + //error_log("MxChat Testing: Added testing data to X.AI fallback response");
4469 + }
4470 +
4471 + header('Content-Type: application/json');
4472 + echo json_encode($response_data);
4473 + return true;
3132 4474 }
4475 +
4476 + // Prepare the request body with stream: true
4477 + $body = json_encode([
4478 + 'model' => $selected_model,
4479 + 'messages' => $formatted_conversation,
4480 + 'temperature' => 0.8,
4481 + 'stream' => true
4482 + ]);
4483 +
4484 + // Use cURL for streaming support
4485 + $ch = curl_init();
4486 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
4487 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4488 + curl_setopt($ch, CURLOPT_POST, true);
4489 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4490 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4491 + 'Content-Type: application/json',
4492 + 'Authorization: Bearer ' . $xai_api_key
4493 + ));
4494 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4495 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4496 +
4497 + $full_response = ''; // Accumulate full response for saving
4498 + $stream_started = false;
4499 +
4500 + // Buffer control for real-time streaming
4501 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4502 + // Send testing data as the first event if available
4503 + if (!$stream_started && $testing_data !== null) {
4504 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4505 + flush();
4506 + $stream_started = true;
4507 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
4508 + }
4509 +
4510 + // Process each chunk of data
4511 + $lines = explode("\n", $data);
4512 +
4513 + foreach ($lines as $line) {
4514 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4515 + continue;
4516 + }
4517 +
4518 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4519 +
4520 + if ($json_str === '[DONE]') {
4521 + echo "data: [DONE]\n\n";
4522 + flush();
4523 + continue;
4524 + }
4525 +
4526 + $json = json_decode($json_str, true);
4527 + if (isset($json['choices'][0]['delta']['content'])) {
4528 + $content = $json['choices'][0]['delta']['content'];
4529 + $full_response .= $content; // Accumulate
4530 + // Send as SSE format
4531 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4532 + flush();
4533 + }
4534 + }
4535 +
4536 + return strlen($data);
4537 + });
4538 +
4539 + $response = curl_exec($ch);
4540 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4541 +
4542 + if (curl_errno($ch) || $http_code !== 200) {
4543 + curl_close($ch);
4544 +
4545 + // Fallback to regular response
4546 + //error_log("MxChat: X.AI streaming failed, falling back");
4547 + $regular_response = $this->mxchat_generate_response_xai(
4548 + $selected_model,
4549 + $xai_api_key,
4550 + $conversation_history,
4551 + $relevant_content
4552 + );
4553 +
4554 + $response_data = [
4555 + 'text' => $regular_response,
4556 + 'html' => '',
4557 + 'session_id' => $session_id
4558 + ];
4559 +
4560 + if ($testing_data !== null) {
4561 + $response_data['testing_data'] = $testing_data;
4562 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
4563 + }
4564 +
4565 + header('Content-Type: application/json');
4566 + echo json_encode($response_data);
4567 + return true;
4568 + }
4569 +
4570 + curl_close($ch);
4571 +
4572 + // Save the complete response to maintain chat persistence
4573 + if (!empty($full_response) && !empty($session_id)) {
4574 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4575 + }
4576 +
4577 + return true; // Indicate streaming completed successfully
4578 +
4579 + } catch (Exception $e) {
4580 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
4581 +
4582 + // Fallback to regular response
4583 + $regular_response = $this->mxchat_generate_response_xai(
4584 + $selected_model,
4585 + $xai_api_key,
4586 + $conversation_history,
4587 + $relevant_content
4588 + );
4589 +
4590 + $response_data = [
4591 + 'text' => $regular_response,
4592 + 'html' => '',
4593 + 'session_id' => $session_id
4594 + ];
4595 +
4596 + if ($testing_data !== null) {
4597 + $response_data['testing_data'] = $testing_data;
4598 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
4599 + }
4600 +
4601 + header('Content-Type: application/json');
4602 + echo json_encode($response_data);
4603 + return true;
3133 4604 }
4605 +}
3134 4606
3135 4607
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 - ]);
4608 +public function test_streaming_request() {
4609 + $options = get_option('mxchat_options', []);
4610 + $model = $options['model'] ?? 'gpt-4o';
3143 4611
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 - ];
4612 + // Detect provider from model prefix
4613 + $provider = strtolower(explode('-', $model)[0]);
3157 4614
3158 - // Make the API request
3159 - $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
4615 + $sample_prompt = 'Hello! Can you stream this response back to me?';
4616 + $messages = [['role' => 'user', 'content' => $sample_prompt]];
4617 + $headers = [];
4618 + $body = [];
4619 + $url = '';
4620 + $api_key = '';
3160 4621
3161 - // Process the response
3162 - if (is_wp_error($response)) {
3163 - return "Sorry, there was an error processing your request.";
4622 + switch ($provider) {
4623 + case 'gpt':
4624 + case 'o1':
4625 + $api_key = $options['api_key'] ?? '';
4626 + if (empty($api_key)) return '❌ Missing API key for OpenAI';
4627 + $url = 'https://api.openai.com/v1/chat/completions';
4628 + $headers = [
4629 + 'Content-Type: application/json',
4630 + 'Authorization: Bearer ' . $api_key
4631 + ];
4632 + $body = [
4633 + 'model' => $model,
4634 + 'messages' => $messages,
4635 + 'stream' => true
4636 + ];
4637 + break;
4638 +
4639 + case 'claude':
4640 + $api_key = $options['claude_api_key'] ?? '';
4641 + if (empty($api_key)) return '❌ Missing API key for Claude';
4642 + $url = 'https://api.anthropic.com/v1/messages';
4643 + $headers = [
4644 + 'Content-Type: application/json',
4645 + 'x-api-key: ' . $api_key,
4646 + 'anthropic-version: 2023-06-01'
4647 + ];
4648 + $body = [
4649 + 'model' => $model,
4650 + 'messages' => $messages,
4651 + 'max_tokens' => 100,
4652 + 'stream' => true
4653 + ];
4654 + break;
4655 +
4656 + case 'grok':
4657 + $api_key = $options['xai_api_key'] ?? '';
4658 + if (empty($api_key)) return '❌ Missing API key for X.AI';
4659 + $url = 'https://api.x.ai/v1/chat/completions';
4660 + $headers = [
4661 + 'Content-Type: application/json',
4662 + 'Authorization: Bearer ' . $api_key
4663 + ];
4664 + $body = [
4665 + 'model' => $model,
4666 + 'messages' => $messages,
4667 + 'stream' => true
4668 + ];
4669 + break;
4670 +
4671 + case 'deepseek':
4672 + $api_key = $options['deepseek_api_key'] ?? '';
4673 + if (empty($api_key)) return '❌ Missing API key for DeepSeek';
4674 + $url = 'https://api.deepseek.com/v1/chat/completions';
4675 + $headers = [
4676 + 'Content-Type: application/json',
4677 + 'Authorization: Bearer ' . $api_key
4678 + ];
4679 + $body = [
4680 + 'model' => $model,
4681 + 'messages' => $messages,
4682 + 'stream' => true
4683 + ];
4684 + break;
4685 +
4686 + case 'gemini':
4687 + $api_key = $options['gemini_api_key'] ?? '';
4688 + if (empty($api_key)) return '❌ Missing API key for Gemini';
4689 + $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
4690 + $headers = ['Content-Type: application/json'];
4691 + $body = [
4692 + 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
4693 + 'generationConfig' => ['temperature' => 0.7]
4694 + ];
4695 + break;
4696 +
4697 + default:
4698 + return '❌ Unsupported provider: ' . $provider;
3164 4699 }
3165 4700
3166 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
4701 + // Do the actual streaming test
4702 + $ch = curl_init($url);
4703 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
4704 + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
4705 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
4706 + curl_setopt($ch, CURLOPT_TIMEOUT, 15);
4707 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
3167 4708
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.";
4709 + $response = curl_exec($ch);
4710 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4711 + $error = curl_error($ch);
4712 + curl_close($ch);
4713 +
4714 + if ($error) return "❌ cURL error: $error";
4715 + if ($http_code !== 200) {
4716 + $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
4717 + return "❌ HTTP $http_code: $error_message";
3172 4718 }
4719 +
4720 + return true;
3173 4721 }
3174 4722
3175 4723 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3176 4724 // Get system prompt instructions from options
@@ -3271,8 +4819,657 @@
3271 4819 // Log unexpected response format
3272 4820 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3273 4821 return "Sorry, I received an unexpected response format from the API.";
3274 4822 }
4823 +private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
4824 + try {
4825 + // Ensure conversation_history is an array
4826 + if (!is_array($conversation_history)) {
4827 + $conversation_history = array();
4828 + }
4829 +
4830 + // Get system prompt instructions from options
4831 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4832 +
4833 + // Create a new array for the formatted conversation
4834 + $formatted_conversation = array();
4835 +
4836 + // Add system message first
4837 + $formatted_conversation[] = array(
4838 + 'role' => 'system',
4839 + 'content' => $system_prompt_instructions . " " . $relevant_content
4840 + );
4841 +
4842 + // Add the rest of the conversation history
4843 + foreach ($conversation_history as $message) {
4844 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4845 + $role = $message['role'];
4846 +
4847 + // Convert roles to supported format
4848 + if ($role === 'bot' || $role === 'agent') {
4849 + $role = 'assistant';
4850 + }
4851 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4852 + $role = 'user';
4853 + }
4854 +
4855 + $formatted_conversation[] = array(
4856 + 'role' => $role,
4857 + 'content' => $message['content']
4858 + );
4859 + }
4860 + }
4861 +
4862 + $body = json_encode([
4863 + 'model' => $selected_model,
4864 + 'messages' => $formatted_conversation,
4865 + 'temperature' => 0.8,
4866 + 'stream' => false
4867 + ]);
4868 +
4869 + $args = [
4870 + 'body' => $body,
4871 + 'headers' => [
4872 + 'Content-Type' => 'application/json',
4873 + 'Authorization' => 'Bearer ' . $api_key,
4874 + ],
4875 + 'timeout' => 60,
4876 + 'redirection' => 5,
4877 + 'blocking' => true,
4878 + 'httpversion' => '1.0',
4879 + 'sslverify' => true,
4880 + ];
4881 +
4882 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4883 +
4884 + if (is_wp_error($response)) {
4885 + $error_message = $response->get_error_message();
4886 + //error_log('OpenAI API Error: ' . $error_message);
4887 + return [
4888 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
4889 + 'error_code' => 'openai_connection_error',
4890 + 'provider' => 'openai'
4891 + ];
4892 + }
4893 +
4894 + $status_code = wp_remote_retrieve_response_code($response);
4895 + if ($status_code !== 200) {
4896 + $response_body = wp_remote_retrieve_body($response);
4897 + $decoded_response = json_decode($response_body, true);
4898 +
4899 + $error_message = isset($decoded_response['error']['message'])
4900 + ? $decoded_response['error']['message']
4901 + : 'HTTP Error ' . $status_code;
4902 +
4903 + $error_type = isset($decoded_response['error']['type'])
4904 + ? $decoded_response['error']['type']
4905 + : 'unknown';
4906 +
4907 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4908 +
4909 + // Handle specific error types
4910 + switch ($error_type) {
4911 + case 'invalid_request_error':
4912 + if (strpos($error_message, 'API key') !== false) {
4913 + return [
4914 + 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
4915 + 'error_code' => 'openai_invalid_api_key',
4916 + 'provider' => 'openai'
4917 + ];
4918 + }
4919 + break;
4920 +
4921 + case 'authentication_error':
4922 + return [
4923 + 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
4924 + 'error_code' => 'openai_auth_error',
4925 + 'provider' => 'openai'
4926 + ];
4927 +
4928 + case 'rate_limit_exceeded':
4929 + return [
4930 + 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
4931 + 'error_code' => 'openai_rate_limit',
4932 + 'provider' => 'openai'
4933 + ];
4934 +
4935 + case 'quota_exceeded':
4936 + return [
4937 + 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
4938 + 'error_code' => 'openai_quota_exceeded',
4939 + 'provider' => 'openai'
4940 + ];
4941 + }
4942 +
4943 + // Generic error fallback
4944 + return [
4945 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
4946 + 'error_code' => 'openai_api_error',
4947 + 'provider' => 'openai',
4948 + 'status_code' => $status_code
4949 + ];
4950 + }
4951 +
4952 + $response_body = wp_remote_retrieve_body($response);
4953 + $decoded_response = json_decode($response_body, true);
4954 +
4955 + if (isset($decoded_response['choices'][0]['message']['content'])) {
4956 + return trim($decoded_response['choices'][0]['message']['content']);
4957 + } else {
4958 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
4959 + return [
4960 + 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
4961 + 'error_code' => 'openai_response_format_error',
4962 + 'provider' => 'openai'
4963 + ];
4964 + }
4965 + } catch (Exception $e) {
4966 + //error_log('OpenAI Exception: ' . $e->getMessage());
4967 + return [
4968 + 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
4969 + 'error_code' => 'openai_exception',
4970 + 'provider' => 'openai'
4971 + ];
4972 + }
4973 +}
4974 +private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
4975 + try {
4976 + // Get system prompt instructions from options
4977 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4978 +
4979 + // Add system prompt to relevant content
4980 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4981 +
4982 + // Prepend system instructions to the conversation history
4983 + array_unshift($conversation_history, [
4984 + 'role' => 'system',
4985 + 'content' => "Here are your instructions: " . $content_with_instructions
4986 + ]);
4987 +
4988 + // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
4989 + foreach ($conversation_history as &$message) {
4990 + if ($message['role'] === 'bot') {
4991 + $message['role'] = 'assistant';
4992 + } elseif ($message['role'] === 'agent') {
4993 + // Tag the message as coming from a live agent
4994 + $message['role'] = 'assistant';
4995 + if (!isset($message['metadata'])) {
4996 + $message['metadata'] = ['source' => 'live_agent'];
4997 + }
4998 + }
4999 +
5000 + // Ensure all roles are valid
5001 + if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
5002 + $message['role'] = 'user'; // Default to 'user'
5003 + }
5004 + }
5005 +
5006 + // Build the request body
5007 + $body = json_encode([
5008 + 'model' => $selected_model,
5009 + 'messages' => $conversation_history,
5010 + 'temperature' => 0.8,
5011 + 'stream' => false
5012 + ]);
5013 +
5014 + // Set up the API request
5015 + $args = [
5016 + 'body' => $body,
5017 + 'headers' => [
5018 + 'Content-Type' => 'application/json',
5019 + 'Authorization' => 'Bearer ' . $xai_api_key,
5020 + ],
5021 + 'timeout' => 60,
5022 + 'redirection' => 5,
5023 + 'blocking' => true,
5024 + 'httpversion' => '1.0',
5025 + 'sslverify' => true,
5026 + ];
5027 +
5028 + // Make the API request
5029 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
5030 +
5031 + // Process the response
5032 + if (is_wp_error($response)) {
5033 + $error_message = $response->get_error_message();
5034 + //error_log('X.AI API Error: ' . $error_message);
5035 + return [
5036 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
5037 + 'error_code' => 'xai_connection_error',
5038 + 'provider' => 'xai'
5039 + ];
5040 + }
5041 +
5042 + $status_code = wp_remote_retrieve_response_code($response);
5043 + if ($status_code !== 200) {
5044 + $response_body = wp_remote_retrieve_body($response);
5045 + $decoded_response = json_decode($response_body, true);
5046 +
5047 + // Log the full response for debugging
5048 + //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
5049 +
5050 + // Extract error message from X.AI's specific format
5051 + $error_message = '';
5052 +
5053 + // Check for direct error string (as seen in your logs)
5054 + if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
5055 + $error_message = $decoded_response['error'];
5056 + }
5057 + // Check for nested error object (OpenAI style)
5058 + elseif (isset($decoded_response['error']['message'])) {
5059 + $error_message = $decoded_response['error']['message'];
5060 + }
5061 + // Check for top-level message
5062 + elseif (isset($decoded_response['message'])) {
5063 + $error_message = $decoded_response['message'];
5064 + }
5065 + // Fallback
5066 + else {
5067 + $error_message = 'HTTP Error ' . $status_code;
5068 + }
5069 +
5070 + //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5071 +
5072 + // Check for API key errors using string matching
5073 + if (stripos($error_message, 'api key') !== false ||
5074 + stripos($error_message, 'incorrect api key') !== false ||
5075 + stripos($error_message, 'invalid api key') !== false) {
5076 + return [
5077 + 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
5078 + 'error_code' => 'xai_invalid_api_key',
5079 + 'provider' => 'xai'
5080 + ];
5081 + }
5082 +
5083 + // Authentication errors
5084 + if ($status_code === 401 || $status_code === 403 ||
5085 + stripos($error_message, 'auth') !== false) {
5086 + return [
5087 + 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
5088 + 'error_code' => 'xai_auth_error',
5089 + 'provider' => 'xai'
5090 + ];
5091 + }
5092 +
5093 + // Model errors
5094 + if (stripos($error_message, 'model') !== false) {
5095 + return [
5096 + 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
5097 + 'error_code' => 'xai_invalid_model',
5098 + 'provider' => 'xai'
5099 + ];
5100 + }
5101 +
5102 + // Rate limit errors
5103 + if ($status_code === 429 ||
5104 + stripos($error_message, 'rate') !== false ||
5105 + stripos($error_message, 'limit') !== false) {
5106 + return [
5107 + 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
5108 + 'error_code' => 'xai_rate_limit',
5109 + 'provider' => 'xai'
5110 + ];
5111 + }
5112 +
5113 + // Quota errors
5114 + if (stripos($error_message, 'quota') !== false ||
5115 + stripos($error_message, 'billing') !== false) {
5116 + return [
5117 + 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
5118 + 'error_code' => 'xai_quota_exceeded',
5119 + 'provider' => 'xai'
5120 + ];
5121 + }
5122 +
5123 + // Server errors
5124 + if ($status_code >= 500) {
5125 + return [
5126 + 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
5127 + 'error_code' => 'xai_service_unavailable',
5128 + 'provider' => 'xai'
5129 + ];
5130 + }
5131 +
5132 + // Generic error fallback with the actual error message
5133 + return [
5134 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
5135 + 'error_code' => 'xai_api_error',
5136 + 'provider' => 'xai',
5137 + 'status_code' => $status_code
5138 + ];
5139 + }
5140 +
5141 + $response_body = wp_remote_retrieve_body($response);
5142 + $decoded_response = json_decode($response_body, true);
5143 +
5144 + if (isset($decoded_response['choices'][0]['message']['content'])) {
5145 + return trim($decoded_response['choices'][0]['message']['content']);
5146 + } else {
5147 + //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
5148 + return [
5149 + 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
5150 + 'error_code' => 'xai_response_format_error',
5151 + 'provider' => 'xai'
5152 + ];
5153 + }
5154 +} catch (Exception $e) {
5155 + //error_log('X.AI Exception: ' . $e->getMessage());
5156 + return [
5157 + 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
5158 + 'error_code' => 'xai_exception',
5159 + 'provider' => 'xai'
5160 + ];
5161 +}
5162 +
5163 +
5164 +}
5165 +
5166 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
5167 + try {
5168 + // Ensure conversation_history is an array
5169 + if (!is_array($conversation_history)) {
5170 + $conversation_history = array();
5171 + }
5172 +
5173 + // Get system prompt instructions from options
5174 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5175 +
5176 + // Create a new array for the formatted conversation
5177 + $formatted_conversation = array();
5178 +
5179 + // Add system message first
5180 + $formatted_conversation[] = array(
5181 + 'role' => 'system',
5182 + 'content' => $system_prompt_instructions . " " . $relevant_content
5183 + );
5184 +
5185 + // Add the rest of the conversation history
5186 + foreach ($conversation_history as $message) {
5187 + if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5188 + $role = $message['role'];
5189 +
5190 + // Convert roles to supported format
5191 + if ($role === 'bot' || $role === 'agent') {
5192 + $role = 'assistant';
5193 + }
5194 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5195 + $role = 'user';
5196 + }
5197 +
5198 + $formatted_conversation[] = array(
5199 + 'role' => $role,
5200 + 'content' => $message['content']
5201 + );
5202 + }
5203 + }
5204 +
5205 + $body = json_encode([
5206 + 'model' => $selected_model,
5207 + 'messages' => $formatted_conversation,
5208 + 'temperature' => 0.8,
5209 + 'stream' => false
5210 + ]);
5211 +
5212 + $args = [
5213 + 'body' => $body,
5214 + 'headers' => [
5215 + 'Content-Type' => 'application/json',
5216 + 'Authorization' => 'Bearer ' . $deepseek_api_key,
5217 + ],
5218 + 'timeout' => 60,
5219 + 'redirection' => 5,
5220 + 'blocking' => true,
5221 + 'httpversion' => '1.0',
5222 + 'sslverify' => true,
5223 + ];
5224 +
5225 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
5226 +
5227 + if (is_wp_error($response)) {
5228 + $error_message = $response->get_error_message();
5229 + //error_log('DeepSeek API Error: ' . $error_message);
5230 + return [
5231 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
5232 + 'error_code' => 'deepseek_connection_error',
5233 + 'provider' => 'deepseek'
5234 + ];
5235 + }
5236 +
5237 + $status_code = wp_remote_retrieve_response_code($response);
5238 + if ($status_code !== 200) {
5239 + $response_body = wp_remote_retrieve_body($response);
5240 + $decoded_response = json_decode($response_body, true);
5241 +
5242 + $error_message = isset($decoded_response['error']['message'])
5243 + ? $decoded_response['error']['message']
5244 + : 'HTTP Error ' . $status_code;
5245 +
5246 + $error_type = isset($decoded_response['error']['type'])
5247 + ? $decoded_response['error']['type']
5248 + : 'unknown';
5249 +
5250 + //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
5251 +
5252 + // Handle specific error types
5253 + switch ($status_code) {
5254 + case 401:
5255 + return [
5256 + 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
5257 + 'error_code' => 'deepseek_auth_error',
5258 + 'provider' => 'deepseek'
5259 + ];
5260 +
5261 + case 400:
5262 + if (strpos($error_message, 'API key') !== false) {
5263 + return [
5264 + 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
5265 + 'error_code' => 'deepseek_invalid_api_key',
5266 + 'provider' => 'deepseek'
5267 + ];
5268 + }
5269 + break;
5270 +
5271 + case 429:
5272 + if (strpos($error_message, 'quota') !== false) {
5273 + return [
5274 + 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
5275 + 'error_code' => 'deepseek_quota_exceeded',
5276 + 'provider' => 'deepseek'
5277 + ];
5278 + } else {
5279 + return [
5280 + 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
5281 + 'error_code' => 'deepseek_rate_limit',
5282 + 'provider' => 'deepseek'
5283 + ];
5284 + }
5285 +
5286 + case 500:
5287 + case 502:
5288 + case 503:
5289 + case 504:
5290 + return [
5291 + 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
5292 + 'error_code' => 'deepseek_service_unavailable',
5293 + 'provider' => 'deepseek'
5294 + ];
5295 + }
5296 +
5297 + // Generic error fallback
5298 + return [
5299 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
5300 + 'error_code' => 'deepseek_api_error',
5301 + 'provider' => 'deepseek',
5302 + 'status_code' => $status_code
5303 + ];
5304 + }
5305 +
5306 + $response_body = wp_remote_retrieve_body($response);
5307 + $decoded_response = json_decode($response_body, true);
5308 +
5309 + if (isset($decoded_response['choices'][0]['message']['content'])) {
5310 + return trim($decoded_response['choices'][0]['message']['content']);
5311 + } else {
5312 + //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
5313 + return [
5314 + 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
5315 + 'error_code' => 'deepseek_response_format_error',
5316 + 'provider' => 'deepseek'
5317 + ];
5318 + }
5319 + } catch (Exception $e) {
5320 + //error_log('DeepSeek Exception: ' . $e->getMessage());
5321 + return [
5322 + 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
5323 + 'error_code' => 'deepseek_exception',
5324 + 'provider' => 'deepseek'
5325 + ];
5326 + }
5327 +}
5328 +
5329 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
5330 + // Get system prompt instructions from options
5331 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5332 +
5333 + // Add system prompt to relevant content
5334 + $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5335 +
5336 + // Format messages for Gemini API
5337 + $formatted_messages = [];
5338 +
5339 + // Add system message as the first user message with role prefix
5340 + // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
5341 + $formatted_messages[] = [
5342 + 'role' => 'user',
5343 + 'parts' => [
5344 + ['text' => "[System Instructions] " . $content_with_instructions]
5345 + ]
5346 + ];
5347 +
5348 + // Add model response to acknowledge system instructions
5349 + $formatted_messages[] = [
5350 + 'role' => 'model',
5351 + 'parts' => [
5352 + ['text' => "I understand and will follow these instructions."]
5353 + ]
5354 + ];
5355 +
5356 + // Process the rest of the conversation history
5357 + $current_role = null;
5358 + $current_parts = [];
5359 +
5360 + foreach ($conversation_history as $message) {
5361 + // Skip the first system message as we already handled it
5362 + if ($message['role'] === 'system') {
5363 + continue;
5364 + }
5365 +
5366 + // Map roles to Gemini format
5367 + $gemini_role = '';
5368 + if ($message['role'] === 'user') {
5369 + $gemini_role = 'user';
5370 + } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
5371 + $gemini_role = 'model';
5372 + } else {
5373 + // Skip unsupported roles
5374 + continue;
5375 + }
5376 +
5377 + // If we have a new role, add the previous message
5378 + if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
5379 + $formatted_messages[] = [
5380 + 'role' => $current_role,
5381 + 'parts' => $current_parts
5382 + ];
5383 + $current_parts = [];
5384 + }
5385 +
5386 + // Set current role and add text to parts
5387 + $current_role = $gemini_role;
5388 + $current_parts[] = ['text' => $message['content']];
5389 + }
5390 +
5391 + // Add the last message if there's content
5392 + if ($current_role !== null && !empty($current_parts)) {
5393 + $formatted_messages[] = [
5394 + 'role' => $current_role,
5395 + 'parts' => $current_parts
5396 + ];
5397 + }
5398 +
5399 + // Build the request body
5400 + $body = json_encode([
5401 + 'contents' => $formatted_messages,
5402 + 'generationConfig' => [
5403 + 'temperature' => 0.7,
5404 + 'topP' => 0.95,
5405 + 'topK' => 40,
5406 + 'maxOutputTokens' => 8192,
5407 + ],
5408 + 'safetySettings' => [
5409 + [
5410 + 'category' => 'HARM_CATEGORY_HARASSMENT',
5411 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5412 + ],
5413 + [
5414 + 'category' => 'HARM_CATEGORY_HATE_SPEECH',
5415 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5416 + ],
5417 + [
5418 + 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
5419 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5420 + ],
5421 + [
5422 + 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
5423 + 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5424 + ]
5425 + ]
5426 + ]);
5427 +
5428 + // Prepare the API endpoint
5429 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5430 +
5431 + // Set up the API request
5432 + $args = [
5433 + 'body' => $body,
5434 + 'headers' => [
5435 + 'Content-Type' => 'application/json',
5436 + ],
5437 + 'timeout' => 60,
5438 + 'redirection' => 5,
5439 + 'blocking' => true,
5440 + 'httpversion' => '1.0',
5441 + 'sslverify' => true,
5442 + ];
5443 +
5444 + // Make the API request
5445 + $response = wp_remote_post($api_endpoint, $args);
5446 +
5447 + // Process the response
5448 + if (is_wp_error($response)) {
5449 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
5450 + }
5451 +
5452 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
5453 +
5454 + // Handle potential errors in the response
5455 + if (isset($response_body['error'])) {
5456 + //error_log('Gemini API Error: ' . json_encode($response_body['error']));
5457 + return "Sorry, there was an error with the Gemini API: " .
5458 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
5459 + }
5460 +
5461 + // Extract the response text
5462 + if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
5463 + return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
5464 + } else {
5465 + //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
5466 + return "Sorry, I couldn't process that request. The response format was unexpected.";
5467 + }
5468 +}
5469 +
5470 +
5471 +
3275 5472 public function mxchat_dismiss_pre_chat_message() {
3276 5473 // Get and sanitize the user identifier
3277 5474 $user_id = $this->mxchat_get_user_identifier();
3278 5475 $user_id = sanitize_key($user_id);
@@ -3328,11 +5525,10 @@
3328 5525 }
3329 5526
3330 5527 public function mxchat_enqueue_scripts_styles() {
3331 5528 // 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 -
5529 + $chat_style_version = '2.3.6';
5530 + $chat_script_version = '2.3.6';
3335 5531 // Enqueue the script
3336 5532 wp_enqueue_script(
3337 5533 'mxchat-chat-js',
3338 5534 plugin_dir_url(__FILE__) . '../js/chat-script.js',
@@ -3339,9 +5535,8 @@
3339 5535 array('jquery'),
3340 5536 $chat_script_version,
3341 5537 true
3342 5538 );
3343 -
3344 5539 // Enqueue the CSS
3345 5540 wp_enqueue_style(
3346 5541 'mxchat-chat-css',
3347 5542 plugin_dir_url(__FILE__) . '../css/chat-style.css',
@@ -3347,17 +5542,19 @@
3347 5542 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3348 5543 array(),
3349 5544 $chat_style_version
3350 5545 );
3351 -
3352 5546 // Fetch options from the database
3353 5547 $this->options = get_option('mxchat_options');
3354 5548 $prompts_options = get_option('mxchat_prompts_options', array());
3355 -
5549 +
3356 5550 // Prepare settings for JavaScript
3357 5551 $style_settings = array(
3358 5552 'ajax_url' => admin_url('admin-ajax.php'),
3359 5553 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
5554 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
5555 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
5556 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off', // ADD THIS LINE
3360 5557 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3361 5558 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3362 5559 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3363 5560 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
@@ -3371,10 +5568,9 @@
3371 5568 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3372 5569 'icon_color' => $this->options['icon_color'] ?? '#fff',
3373 5570 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3374 5571 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3375 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3376 -
5572 + 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
3377 5573 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3378 5574 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3379 5575 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 5576 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
@@ -3379,76 +5575,903 @@
3379 5575 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3380 5576 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3381 5577 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3382 5578 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3383 -
3384 5579 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
3385 5580 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
3386 5581 );
3387 -
3388 5582 // Pass the settings to the script
3389 5583 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3390 5584 }
3391 5585
3392 5586
5587 +/**
5588 + * Setup the cron jobs for rate limits with guard against multiple calls
5589 + */
5590 +public function setup_rate_limit_cron_jobs() {
5591 + // Add a guard to prevent multiple rapid calls
5592 + $last_setup = get_transient('mxchat_cron_setup_guard');
5593 + if ($last_setup && (time() - $last_setup) < 60) {
5594 + // Don't run again if we ran less than 60 seconds ago
5595 + return;
5596 + }
5597 +
5598 + // Set the guard
5599 + set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
5600 +
5601 + try {
5602 + // First, check if WordPress cron is disabled
5603 + if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
5604 + error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
5605 + $this->setup_fallback_rate_limit_system();
5606 + return;
5607 + }
5608 +
5609 + // Check if cron is already scheduled - if so, don't mess with it
5610 + if (wp_next_scheduled('mxchat_reset_rate_limits')) {
5611 + error_log('MxChat: Rate limit cron already scheduled, skipping setup');
5612 + return;
5613 + }
5614 +
5615 + // Clear any orphaned hooks (but don't loop indefinitely)
5616 + $hooks_to_clear = [
5617 + 'mxchat_reset_rate_limits',
5618 + 'mxchat_reset_hourly_rate_limits',
5619 + 'mxchat_reset_daily_rate_limits',
5620 + 'mxchat_reset_weekly_rate_limits',
5621 + 'mxchat_reset_monthly_rate_limits'
5622 + ];
5623 +
5624 + foreach ($hooks_to_clear as $hook) {
5625 + // Only clear a maximum of 3 instances to prevent infinite loops
5626 + $cleared = 0;
5627 + while (wp_next_scheduled($hook) && $cleared < 3) {
5628 + wp_clear_scheduled_hook($hook);
5629 + $cleared++;
5630 + }
5631 + }
5632 +
5633 + // Small delay after clearing
5634 + usleep(100000); // 0.1 seconds
5635 +
5636 + // Try to schedule the event
5637 + $initial_time = time() + 300; // Start in 5 minutes
5638 + $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
5639 +
5640 + if ($result === false) {
5641 + error_log('MxChat: Failed to schedule cron, using fallback system');
5642 + $this->setup_fallback_rate_limit_system();
5643 + } else {
5644 + error_log('MxChat: Successfully scheduled rate limit reset cron');
5645 + }
5646 +
5647 + } catch (Exception $e) {
5648 + error_log('MxChat: Cron setup exception: ' . $e->getMessage());
5649 + $this->setup_fallback_rate_limit_system();
5650 + }
5651 +}
5652 +
5653 +/**
5654 + * Try alternative cron scheduling methods
5655 + */
5656 +private function try_alternative_cron_scheduling($initial_time) {
5657 + try {
5658 + // Method 1: Try with current time instead of future time
5659 + $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
5660 + if ($result1 !== false) {
5661 + error_log('MxChat: Alternative method 1 (current time) succeeded');
5662 + return true;
5663 + }
5664 +
5665 + // Method 2: Try with a different interval
5666 + $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
5667 + if ($result2 !== false) {
5668 + error_log('MxChat: Alternative method 2 (daily interval) succeeded');
5669 + return true;
5670 + }
5671 +
5672 + // Method 3: Try wp_schedule_single_event first, then recurring
5673 + $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
5674 + if ($result3 !== false) {
5675 + error_log('MxChat: Alternative method 3 (single event) succeeded');
5676 + // Schedule the next one manually in the handler
5677 + return true;
5678 + }
5679 +
5680 + return false;
5681 +
5682 + } catch (Exception $e) {
5683 + error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
5684 + return false;
5685 + }
5686 +}
5687 +
5688 +/**
5689 + * Enhanced fallback rate limit system
5690 + */
5691 +private function setup_fallback_rate_limit_system() {
5692 + // Set a flag to use database-based rate limit cleanup
5693 + update_option('mxchat_use_fallback_rate_limits', true);
5694 +
5695 + // Schedule a one-time check to happen on the next plugin load
5696 + update_option('mxchat_next_rate_limit_check', time() + 3600);
5697 +
5698 + // Also set up a more frequent fallback check (every 4 hours)
5699 + update_option('mxchat_fallback_check_interval', 4 * 3600);
5700 +
5701 + error_log('MxChat: Fallback rate limit system activated');
5702 +}
5703 +
5704 +/**
5705 + * Enhanced fallback check method
5706 + */
5707 +public function check_fallback_rate_limits() {
5708 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5709 +
5710 + if (!$use_fallback) {
5711 + return; // Regular cron is working
5712 + }
5713 +
5714 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
5715 + $check_interval = get_option('mxchat_fallback_check_interval', 3600);
5716 +
5717 + if (time() >= $next_check) {
5718 + error_log('MxChat: Running fallback rate limit cleanup');
5719 + $this->mxchat_reset_rate_limits();
5720 +
5721 + // Schedule next check
5722 + update_option('mxchat_next_rate_limit_check', time() + $check_interval);
5723 + }
5724 +}
5725 +/**
5726 + * Enhanced rate limit check that includes fallback cleanup
5727 + */
5728 +public function check_rate_limit() {
5729 + // Check if we need to run fallback cleanup
5730 + $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5731 + $next_check = get_option('mxchat_next_rate_limit_check', 0);
5732 +
5733 + if ($use_fallback && time() >= $next_check) {
5734 + $this->mxchat_reset_rate_limits();
5735 + update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
5736 + }
5737 +
5738 + // Continue with your existing rate limit logic...
5739 + $all_options = get_option('mxchat_options', []);
5740 +
5741 + // Determine user role or if logged out
5742 + if (is_user_logged_in()) {
5743 + $user = wp_get_current_user();
5744 + $user_id = $user->ID;
5745 +
5746 + // Get the user's primary role using reset() to safely get the first element
5747 + $user_roles = $user->roles;
5748 +
5749 + // Safely get the first role regardless of array key structure
5750 + if (!empty($user_roles) && is_array($user_roles)) {
5751 + $role = reset($user_roles); // This safely gets the first element regardless of key
5752 + } else {
5753 + $role = 'subscriber'; // Default to subscriber if no role found
5754 + }
5755 + } else {
5756 + $role = 'logged_out';
5757 + // Use IP address for non-logged-in users
5758 + $user_id = $this->get_client_ip();
5759 + }
5760 +
5761 + // Check if rate limits are configured for this role
5762 + if (!isset($all_options['rate_limits'][$role])) {
5763 + return true; // No limit set for this role
5764 + }
5765 +
5766 + $limit = $all_options['rate_limits'][$role]['limit'];
5767 +
5768 + // If unlimited, return true immediately
5769 + if ($limit === 'unlimited') {
5770 + return true;
5771 + }
5772 +
5773 + // Get the option name for this user/role with safer naming
5774 + $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
5775 + $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
5776 + $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
5777 +
5778 + // Get the counter data
5779 + $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
5780 +
5781 + // If first request or counter reset needed, set the initial timestamp
5782 + if ($limit_data['count'] === 0) {
5783 + $limit_data['timestamp'] = time();
5784 + update_option($option_name, $limit_data);
5785 + }
5786 +
5787 + // Get the timeframe
5788 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
5789 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
5790 +
5791 + // Check if the counter needs to be reset based on timeframe
5792 + $current_time = time();
5793 + $timestamp = $limit_data['timestamp'];
5794 + $should_reset = false;
5795 +
5796 + switch ($timeframe) {
5797 + case 'hourly':
5798 + $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
5799 + break;
5800 + case 'daily':
5801 + $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
5802 + break;
5803 + case 'weekly':
5804 + $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
5805 + break;
5806 + case 'monthly':
5807 + $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
5808 + break;
5809 + }
5810 +
5811 + // Reset the counter if the timeframe has passed
5812 + if ($should_reset) {
5813 + $limit_data = ['count' => 0, 'timestamp' => $current_time];
5814 + update_option($option_name, $limit_data);
5815 + }
5816 +
5817 + // Check if user has exceeded their limit
5818 + if ($limit_data['count'] >= intval($limit)) {
5819 + // Get the custom message for this role
5820 + $message = !empty($all_options['rate_limits'][$role]['message'])
5821 + ? $all_options['rate_limits'][$role]['message']
5822 + : __('Rate limit exceeded. Please try again later.', 'mxchat');
5823 +
5824 + // Add timeframe information to the message if placeholders exist
5825 + $timeframe_label = '';
5826 + switch ($timeframe) {
5827 + case 'hourly':
5828 + $timeframe_label = __('hour', 'mxchat');
5829 + break;
5830 + case 'daily':
5831 + $timeframe_label = __('day', 'mxchat');
5832 + break;
5833 + case 'weekly':
5834 + $timeframe_label = __('week', 'mxchat');
5835 + break;
5836 + case 'monthly':
5837 + $timeframe_label = __('month', 'mxchat');
5838 + break;
5839 + }
5840 +
5841 + // Replace placeholders in the message
5842 + $message = str_replace(
5843 + ['{limit}', '{count}', '{remaining}', '{timeframe}'],
5844 + [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
5845 + $message
5846 + );
5847 +
5848 + // Process HTML links in the message
5849 + $message = $this->process_rate_limit_message_html($message);
5850 +
5851 + // Return error with the processed message
5852 + return [
5853 + 'error' => true,
5854 + 'message' => $message
5855 + ];
5856 + }
5857 +
5858 + // Increment the counter
5859 + $limit_data['count']++;
5860 + update_option($option_name, $limit_data);
5861 +
5862 + return true;
5863 +}
5864 +
5865 +/**
5866 + * Enhanced rate limit reset with better error handling
5867 + */
3393 5868 public function mxchat_reset_rate_limits() {
5869 + try {
3394 5870 global $wpdb;
5871 + $all_options = get_option('mxchat_options', []);
5872 + $current_time = time();
5873 +
5874 + // Get rate limit options with a safer query and limit
5875 + $option_names = $wpdb->get_col(
5876 + $wpdb->prepare(
5877 + "SELECT option_name FROM {$wpdb->options}
5878 + WHERE option_name LIKE %s
5879 + LIMIT 1000",
5880 + 'mxchat_chat_limit_%'
5881 + )
5882 + );
5883 +
5884 + if (empty($option_names)) {
5885 + return;
5886 + }
5887 +
5888 + $processed_count = 0;
5889 + $max_processing_time = 30; // Maximum 30 seconds
5890 + $start_time = time();
5891 +
5892 + foreach ($option_names as $option_name) {
5893 + // Check processing time limit
5894 + if ((time() - $start_time) > $max_processing_time) {
5895 + error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
5896 + break;
5897 + }
5898 +
5899 + // Parse the option name more safely
5900 + if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
5901 + continue;
5902 + }
5903 +
5904 + $role_and_user = $matches[1] . '_' . $matches[2];
5905 + $parts = explode('_', $role_and_user);
5906 +
5907 + if (count($parts) < 2) {
5908 + continue;
5909 + }
5910 +
5911 + // Extract role (everything except the last part which is user ID)
5912 + $user_id_part = array_pop($parts);
5913 + $role = implode('_', $parts);
5914 +
5915 + // Skip if role doesn't exist in our settings
5916 + if (!isset($all_options['rate_limits'][$role])) {
5917 + // Clean up orphaned entries
5918 + delete_option($option_name);
5919 + continue;
5920 + }
5921 +
5922 + $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
5923 + $limit_data = get_option($option_name);
5924 +
5925 + if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
5926 + // Clean up invalid entries
5927 + delete_option($option_name);
5928 + continue;
5929 + }
5930 +
5931 + $timestamp = $limit_data['timestamp'];
5932 + $should_reset = false;
5933 +
5934 + // Determine if we should reset based on the timeframe
5935 + switch ($timeframe) {
5936 + case 'hourly':
5937 + $should_reset = ($current_time - $timestamp) >= 3600;
5938 + break;
5939 + case 'daily':
5940 + $should_reset = ($current_time - $timestamp) >= 86400;
5941 + break;
5942 + case 'weekly':
5943 + $should_reset = ($current_time - $timestamp) >= 604800;
5944 + break;
5945 + case 'monthly':
5946 + $should_reset = ($current_time - $timestamp) >= 2592000;
5947 + break;
5948 + }
5949 +
5950 + // Reset the counter if the timeframe has passed
5951 + if ($should_reset) {
5952 + delete_option($option_name);
5953 + wp_cache_delete($option_name, 'options');
5954 + $processed_count++;
5955 + }
5956 + }
5957 +
5958 + // Clean up any orphaned cache entries
5959 + wp_cache_delete('mxchat_all_chat_limits', 'options');
5960 +
5961 + error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
5962 +
5963 + } catch (Exception $e) {
5964 + error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
5965 + }
5966 +}
3395 5967
3396 - // Define a cache key pattern for rate limits
3397 - $cache_key_pattern = 'mxchat_chat_limit_%';
3398 5968
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_%'");
5969 +/**
5970 + * Process HTML links in rate limit messages
5971 + *
5972 + * @param string $message The rate limit message
5973 + * @return string The processed message with safe HTML links
5974 + */
5975 +private function process_rate_limit_message_html($message) {
5976 + // Return original message if empty
5977 + if (empty($message)) {
5978 + return $message;
5979 + }
5980 +
5981 + // First, convert markdown links to HTML
5982 + $message = $this->convert_markdown_links($message);
5983 +
5984 + // Then, auto-convert any remaining plain URLs to links
5985 + $message = $this->auto_link_urls($message);
5986 +
5987 + // Allow basic HTML tags for links and formatting
5988 + $allowed_tags = [
5989 + 'a' => [
5990 + 'href' => true,
5991 + 'target' => true,
5992 + 'rel' => true,
5993 + 'title' => true,
5994 + 'class' => true
5995 + ],
5996 + 'strong' => [],
5997 + 'em' => [],
5998 + 'br' => [],
5999 + 'b' => [],
6000 + 'i' => [],
6001 + 'span' => ['class' => true]
6002 + ];
6003 +
6004 + // Sanitize but allow the specified HTML tags
6005 + $processed_message = wp_kses($message, $allowed_tags);
6006 +
6007 + // If wp_kses stripped everything, return the original message as plain text
6008 + if (empty($processed_message) && !empty($message)) {
6009 + // Strip all HTML and return plain text as fallback
6010 + return wp_strip_all_tags($message);
6011 + }
6012 +
6013 + return $processed_message;
6014 +}
3402 6015
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_%'");
6016 +/**
6017 + * Convert markdown links to HTML
6018 + *
6019 + * @param string $text The text to process
6020 + * @return string The text with markdown links converted to HTML
6021 + */
6022 +private function convert_markdown_links($text) {
6023 + // Return original text if empty
6024 + if (empty($text)) {
6025 + return $text;
6026 + }
6027 +
6028 + // Pattern to match markdown links: [text](url)
6029 + $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
6030 +
6031 + $processed_text = preg_replace_callback($pattern, function($matches) {
6032 + $link_text = $matches[1];
6033 + $url = $matches[2];
6034 +
6035 + // Clean up any trailing punctuation from the URL
6036 + $url = rtrim($url, '.,;:!?');
6037 +
6038 + // Sanitize the link text and URL
6039 + $safe_text = esc_html($link_text);
6040 + $safe_url = esc_url($url);
6041 +
6042 + // Create the HTML link
6043 + return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
6044 + }, $text);
6045 +
6046 + // If preg_replace_callback failed, return original text
6047 + if ($processed_text === null) {
6048 + return $text;
6049 + }
6050 +
6051 + return $processed_text;
6052 +}
3406 6053
3407 - // Clear the relevant cache entries
3408 - foreach ($option_names as $option_name) {
3409 - wp_cache_delete($option_name, 'options');
3410 - }
6054 +/**
6055 + * Auto-convert plain URLs to clickable links
6056 + *
6057 + * @param string $text The text to process
6058 + * @return string The text with URLs converted to links
6059 + */
6060 +private function auto_link_urls($text) {
6061 + // Return original text if empty
6062 + if (empty($text)) {
6063 + return $text;
6064 + }
6065 +
6066 + // Simple pattern that avoids complex lookbehinds
6067 + // This will match URLs that are not already inside href attributes or markdown links
6068 + $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
6069 +
6070 + $processed_text = preg_replace_callback($pattern, function($matches) {
6071 + $url = $matches[0];
6072 + // Clean up any trailing punctuation that might have been captured
6073 + $url = rtrim($url, '.,;:!?');
6074 +
6075 + // Add target="_blank" and rel="noopener noreferrer" for security
6076 + return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
6077 + }, $text);
6078 +
6079 + // If preg_replace_callback failed, return original text
6080 + if ($processed_text === null) {
6081 + return $text;
6082 + }
6083 +
6084 + return $processed_text;
6085 +}
3411 6086
3412 - // Optionally, clear a general cache if you have one
3413 - wp_cache_delete('mxchat_all_chat_limits', 'options');
6087 +
6088 +// Helper function to get client IP address
6089 +private function get_client_ip() {
6090 + // Check for shared internet/ISP IP
6091 + if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
6092 + return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
3414 6093 }
6094 +
6095 + // Check for IPs passing through proxies
6096 + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
6097 + // Use the first value in the comma-separated list
6098 + $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
6099 + return trim($forwarded_for[0]);
6100 + }
6101 +
6102 + if (!empty($_SERVER['REMOTE_ADDR'])) {
6103 + return sanitize_text_field($_SERVER['REMOTE_ADDR']);
6104 + }
6105 +
6106 + // Fallback
6107 + return 'unknown';
6108 +}
3415 6109
3416 -private function mxchat_fetch_woocommerce_products() {
3417 - // Ensure WooCommerce is active
3418 - if (!class_exists('WooCommerce')) {
3419 - return [];
6110 +/**
6111 + * AJAX handler to get system information for testing panel
6112 + */
6113 +public function mxchat_get_system_info() {
6114 + // Verify nonce for security
6115 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6116 + wp_send_json_error(['message' => 'Invalid nonce']);
6117 + return;
3420 6118 }
6119 +
6120 + // Only allow admin users
6121 + if (!current_user_can('administrator')) {
6122 + wp_send_json_error(['message' => 'Unauthorized']);
6123 + return;
6124 + }
6125 +
6126 + // Get system prompt from options
6127 + $system_prompt = isset($this->options['system_prompt_instructions'])
6128 + ? $this->options['system_prompt_instructions']
6129 + : 'No system prompt configured';
6130 +
6131 + // Get selected model
6132 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
6133 +
6134 + // Get API key status (just check if they exist, don't expose the keys)
6135 + $api_status = [];
6136 + $api_status['openai'] = !empty($this->options['api_key']);
6137 + $api_status['claude'] = !empty($this->options['claude_api_key']);
6138 + $api_status['gemini'] = !empty($this->options['gemini_api_key']);
6139 + $api_status['xai'] = !empty($this->options['xai_api_key']);
6140 + $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
6141 +
6142 + wp_send_json_success([
6143 + 'system_prompt' => $system_prompt,
6144 + 'selected_model' => $selected_model,
6145 + 'api_status' => $api_status
6146 + ]);
6147 +}
3421 6148
3422 - $args = array(
3423 - 'post_type' => 'product',
3424 - 'post_status' => 'publish',
3425 - 'posts_per_page' => -1,
3426 - );
6149 +/**
6150 + * AJAX handler to get similarity threshold
6151 + */
6152 +public function mxchat_get_similarity_threshold() {
6153 + // Verify nonce for security
6154 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6155 + wp_send_json_error(['message' => 'Invalid nonce']);
6156 + return;
6157 + }
6158 +
6159 + // Only allow admin users
6160 + if (!current_user_can('administrator')) {
6161 + wp_send_json_error(['message' => 'Unauthorized']);
6162 + return;
6163 + }
6164 +
6165 + // Get similarity threshold from main options (default 75%)
6166 + $similarity_threshold = isset($this->options['similarity_threshold'])
6167 + ? ((int) $this->options['similarity_threshold']) / 100
6168 + : 0.75;
6169 +
6170 + wp_send_json_success([
6171 + 'threshold' => $similarity_threshold,
6172 + 'threshold_percentage' => ($similarity_threshold * 100) . '%'
6173 + ]);
6174 +}
3427 6175
3428 - $products = get_posts($args);
3429 - $product_data = [];
6176 +/**
6177 + * AJAX handler to get knowledge base status
6178 + */
6179 +public function mxchat_get_kb_status() {
6180 + // Verify nonce for security
6181 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6182 + wp_send_json_error(['message' => 'Invalid nonce']);
6183 + return;
6184 + }
6185 +
6186 + // Only allow admin users
6187 + if (!current_user_can('administrator')) {
6188 + wp_send_json_error(['message' => 'Unauthorized']);
6189 + return;
6190 + }
6191 +
6192 + // Check Pinecone vs WordPress
6193 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
6194 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
6195 +
6196 + $kb_info = [
6197 + 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
6198 + 'status' => 'Active'
6199 + ];
6200 +
6201 + // Get document count
6202 + if ($use_pinecone) {
6203 + $kb_info['documents'] = 'Connected to Pinecone';
6204 + $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
6205 + } else {
6206 + // Count documents in WordPress database
6207 + global $wpdb;
6208 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6209 + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
6210 + $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
6211 + }
6212 +
6213 + wp_send_json_success($kb_info);
6214 +}
3430 6215
3431 - foreach ($products as $product) {
3432 - $product_id = $product->ID;
3433 - $product_obj = wc_get_product($product_id);
6216 +/**
6217 + * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
6218 + */
6219 +public function mxchat_start_fresh_session() {
6220 + // Verify nonce for security
6221 + if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6222 + wp_send_json_error(['message' => 'Invalid nonce']);
6223 + return;
6224 + }
6225 +
6226 + // Only allow admin users
6227 + if (!current_user_can('administrator')) {
6228 + wp_send_json_error(['message' => 'Unauthorized']);
6229 + return;
6230 + }
6231 +
6232 + $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
6233 + $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
6234 +
6235 + if (empty($old_session_id)) {
6236 + wp_send_json_error(['message' => 'Old session ID required']);
6237 + return;
6238 + }
6239 +
6240 + // If no new session ID provided, generate one
6241 + if (empty($new_session_id)) {
6242 + $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
6243 + }
6244 +
6245 + // Clear ALL data associated with the old session
6246 + $this->clear_complete_session_data($old_session_id);
6247 +
6248 + // Initialize the new session
6249 + $this->initialize_fresh_session($new_session_id);
6250 +
6251 + wp_send_json_success([
6252 + 'message' => 'Fresh session started successfully',
6253 + 'new_session_id' => $new_session_id,
6254 + 'old_session_id' => $old_session_id
6255 + ]);
6256 +}
3434 6257
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 - );
6258 +/**
6259 + * Clear ALL data associated with a session (ENHANCED)
6260 + */
6261 +private function clear_complete_session_data($session_id) {
6262 + // Clear chat history
6263 + delete_option("mxchat_history_{$session_id}");
6264 +
6265 + // Clear chat mode
6266 + delete_option("mxchat_mode_{$session_id}");
6267 +
6268 + // Clear any PDF/Word transients
6269 + $this->clear_pdf_transients($session_id);
6270 + if (method_exists($this, 'clear_word_transients')) {
6271 + $this->clear_word_transients($session_id);
3448 6272 }
6273 +
6274 + // Clear agent-related data
6275 + delete_option("mxchat_channel_{$session_id}");
6276 + delete_option("mxchat_agent_name_{$session_id}");
6277 + delete_option("mxchat_email_{$session_id}");
6278 +
6279 + // Clear any recommendation flow state
6280 + delete_option("mxchat_sr_flow_state_{$session_id}");
6281 +
6282 + // Clear any cached embeddings or context
6283 + delete_transient("mxchat_context_{$session_id}");
6284 + delete_transient("mxchat_last_query_{$session_id}");
6285 +
6286 + // Clear any testing data
6287 + delete_transient("mxchat_testing_data_{$session_id}");
6288 +
6289 + // Clear any rate limiting data for this session
6290 + delete_transient("mxchat_rate_limit_{$session_id}");
6291 +
6292 + // Clear any other session-specific transients
6293 + delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
6294 + delete_transient("mxchat_include_pdf_in_context_{$session_id}");
6295 + delete_transient("mxchat_include_word_in_context_{$session_id}");
6296 +
6297 + //error_log("MxChat: Cleared all data for session: {$session_id}");
6298 +}
3449 6299
3450 - return $product_data;
6300 +/**
6301 + * Initialize a fresh session with default data
6302 + */
6303 +private function initialize_fresh_session($session_id) {
6304 + // Set default chat mode
6305 + update_option("mxchat_mode_{$session_id}", 'ai');
6306 +
6307 + //error_log("MxChat: Initialized fresh session: {$session_id}");
3451 6308 }
6309 +
6310 +/**
6311 + * Helper method to clear Word document transients (if you have Word support)
6312 + */
6313 +private function clear_word_transients($session_id) {
6314 + delete_transient('mxchat_word_url_' . $session_id);
6315 + delete_transient('mxchat_word_filename_' . $session_id);
6316 + delete_transient('mxchat_word_embeddings_' . $session_id);
6317 + delete_transient('mxchat_include_word_in_context_' . $session_id);
6318 +}
6319 +
6320 +/**
6321 + * Simplified testing data capture method (CLEANED UP)
6322 + */
6323 +private function capture_testing_data($user_embedding, $message, $session_id) {
6324 + // Only capture for admin users
6325 + if (!current_user_can('administrator')) {
6326 + return null;
6327 + }
6328 +
6329 + $testing_data = [
6330 + 'query' => $message,
6331 + 'timestamp' => time(),
6332 + 'top_matches' => [],
6333 + 'action_matches' => [] // NEW: Add action matches
6334 + ];
6335 +
6336 + // Get similarity threshold
6337 + $similarity_threshold = isset($this->options['similarity_threshold'])
6338 + ? ((int) $this->options['similarity_threshold']) / 100
6339 + : 0.75;
6340 +
6341 + $testing_data['similarity_threshold'] = $similarity_threshold;
6342 +
6343 + // Use the real similarity analysis if available
6344 + if ($this->last_similarity_analysis !== null) {
6345 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
6346 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
6347 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6348 + } else {
6349 + // Fallback: determine knowledge base type
6350 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
6351 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
6352 +
6353 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
6354 + }
6355 +
6356 + // NEW: Include action analysis if available
6357 + if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
6358 + $testing_data['action_matches'] = $this->last_action_analysis;
6359 +
6360 + // Clear it after capturing to avoid stale data
6361 + $this->last_action_analysis = null;
6362 + }
6363 +
6364 + return $testing_data;
6365 +}
6366 +
6367 +
6368 +/**
6369 + * NEW: Track URL clicks from chatbot responses
6370 + */
6371 +public function mxchat_track_url_click() {
6372 + // Verify nonce for security
6373 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
6374 + wp_send_json_error(['message' => 'Invalid nonce']);
6375 + wp_die();
6376 + }
6377 +
6378 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
6379 + $clicked_url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
6380 + $message_context = isset($_POST['message_context']) ? sanitize_textarea_field($_POST['message_context']) : '';
6381 +
6382 + if (empty($session_id) || empty($clicked_url)) {
6383 + wp_send_json_error(['message' => 'Missing required data']);
6384 + wp_die();
6385 + }
6386 +
6387 + global $wpdb;
6388 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
6389 +
6390 + // Insert click tracking record
6391 + $wpdb->insert(
6392 + $table_name,
6393 + [
6394 + 'session_id' => $session_id,
6395 + 'clicked_url' => $clicked_url,
6396 + 'message_context' => $message_context,
6397 + 'click_timestamp' => current_time('mysql', 1),
6398 + 'user_ip' => $_SERVER['REMOTE_ADDR'],
6399 + 'user_agent' => $_SERVER['HTTP_USER_AGENT']
6400 + ]
6401 + );
6402 +
6403 + wp_send_json_success(['message' => 'Click tracked']);
6404 + wp_die();
6405 +}
6406 +
6407 +/**
6408 + * NEW: Get URL click analytics for a session
6409 + */
6410 +public function mxchat_get_url_clicks($session_id) {
6411 + global $wpdb;
6412 + $table_name = $wpdb->prefix . 'mxchat_url_clicks';
6413 +
6414 + $clicks = $wpdb->get_results($wpdb->prepare(
6415 + "SELECT * FROM $table_name WHERE session_id = %s ORDER BY click_timestamp ASC",
6416 + $session_id
6417 + ));
6418 +
6419 + return $clicks;
6420 +}
6421 +/**
6422 + * NEW: Track the originating page where chat was started
6423 + */
6424 +public function mxchat_track_originating_page() {
6425 + // Verify nonce
6426 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
6427 + wp_send_json_error(['message' => 'Invalid nonce']);
6428 + wp_die();
6429 + }
6430 +
6431 + $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
6432 + $page_url = isset($_POST['page_url']) ? esc_url_raw($_POST['page_url']) : '';
6433 + $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
6434 +
6435 + if (empty($session_id)) {
6436 + wp_send_json_error(['message' => 'Missing session ID']);
6437 + wp_die();
6438 + }
6439 +
6440 + global $wpdb;
6441 + $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
6442 +
6443 + // Check if we've already tracked for this session
6444 + $existing = $wpdb->get_var($wpdb->prepare(
6445 + "SELECT COUNT(*) FROM $table_name
6446 + WHERE session_id = %s
6447 + AND originating_page_url IS NOT NULL",
6448 + $session_id
6449 + ));
6450 +
6451 + if ($existing > 0) {
6452 + wp_send_json_success(['message' => 'Already tracked']);
6453 + wp_die();
6454 + }
6455 +
6456 + // Update the first message in this session with originating page info
6457 + $wpdb->query($wpdb->prepare(
6458 + "UPDATE $table_name
6459 + SET originating_page_url = %s,
6460 + originating_page_title = %s
6461 + WHERE session_id = %s
6462 + ORDER BY timestamp ASC
6463 + LIMIT 1",
6464 + $page_url,
6465 + $page_title,
6466 + $session_id
6467 + ));
6468 +
6469 + wp_send_json_success(['message' => 'Originating page tracked']);
6470 + wp_die();
6471 +}
6472 +
6473 +
6474 +
3452 6475
3453 6476 }
3454 6477 ?>