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