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