PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.1
MxChat – AI Chatbot & Content Generation for WordPress v2.2.1
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
mxchat-basic / includes / class-mxchat-integrator.php

class-mxchat-integrator.php in MxChat – AI Chatbot & Content Generation for WordPress 2.2.1, at includes/class-mxchat-integrator.php

4,744 lines 181.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 class MxChat_Integrator {
7 private $options;
8 private $prompts_options;
9 private $chat_count;
10 private $fallbackResponse;
11 private $productCardHtml;
12 private $word_handler;
13
14 /**
15 * Setup the cron jobs for rate limits
16 */
17 public function setup_rate_limit_cron_jobs() {
18 // Clear previous schedules
19 wp_clear_scheduled_hook('mxchat_reset_rate_limits');
20 wp_clear_scheduled_hook('mxchat_reset_hourly_rate_limits');
21 wp_clear_scheduled_hook('mxchat_reset_daily_rate_limits');
22 wp_clear_scheduled_hook('mxchat_reset_weekly_rate_limits');
23 wp_clear_scheduled_hook('mxchat_reset_monthly_rate_limits');
24
25 // Schedule the main rate limit reset check (runs hourly)
26 if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
27 wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
28 }
29 }
30
31 /**
32 * Class constructor
33 */
34 public function __construct() {
35 $this->options = get_option('mxchat_options');
36 $this->prompts_options = get_option('mxchat_prompts_options', array());
37 $this->chat_count = get_option('mxchat_chat_count', 0);
38 $this->word_handler = new MXChat_Word_Handler($this->options);
39
40 // Setup the cron jobs for rate limits
41 $this->setup_rate_limit_cron_jobs();
42
43 // Add all action hooks
44 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
45 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
46 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
47 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
48 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
49
50 // Add the AJAX actions for checking if the pre-chat message was dismissed
51 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
52 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
53 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
54 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
55 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
56 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
57
58 // Add REST API routes registration
59 add_action('rest_api_init', array($this, 'register_routes'));
60 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
61 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
62
63 // Rate limit action - notice we removed the old schedule setup
64 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
65
66 // File upload and handling actions
67 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
68 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
69 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
70 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
71
72 // Word document handling actions
73 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
74 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
75 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
76 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
77 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
78 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
79
80 // Email handling actions
81 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
82 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
83 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
84 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
85 }
86
87
88 private function mxchat_increment_chat_count() {
89 $chat_count = get_option('mxchat_chat_count', 0);
90 $chat_count++;
91 update_option('mxchat_chat_count', $chat_count);
92 }
93
94 function mxchat_fetch_conversation_history() {
95 if (empty($_POST['session_id'])) {
96 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
97 wp_die();
98 }
99
100 $session_id = sanitize_text_field($_POST['session_id']);
101 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
102 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
103
104 if (empty($history)) {
105 // Even if history is empty, return the chat mode
106 wp_send_json_success([
107 'conversation' => [],
108 'chat_mode' => $chat_mode
109 ]);
110 wp_die();
111 }
112
113 wp_send_json_success([
114 'conversation' => $history,
115 'chat_mode' => $chat_mode
116 ]);
117 wp_die();
118 }
119 private function mxchat_fetch_conversation_history_for_ajax($session_id) {
120 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
121 $formatted_history = [];
122
123 // Format the history to align with the expected structure for OpenAI
124 foreach ($history as $entry) {
125 $formatted_history[] = [
126 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
127 'content' => $entry['content']
128 ];
129 }
130
131 return $formatted_history;
132 }
133
134
135 private function mxchat_fetch_conversation_history_for_ai($session_id) {
136 $history = get_option("mxchat_history_{$session_id}", []);
137 $formatted_history = [];
138
139 // Adjusted for code-heavy conversations
140 $max_tokens = 120000; // Context window size
141 $reserved_tokens = 5000; // Space for system prompts + current query
142 $current_token_count = 0;
143
144 // Allowed HTML tags for content sanitization
145 $allowed_tags = [
146 'pre' => ['class' => true],
147 'code' => ['class' => true],
148 'span' => ['class' => true],
149 'div' => ['class' => true],
150 'strong' => [],
151 'em' => []
152 ];
153
154 foreach (array_reverse($history) as $entry) {
155 // Preserve code blocks while sanitizing other HTML
156 $clean_content = wp_kses($entry['content'], $allowed_tags);
157
158 // Detect code blocks in content
159 $has_code = false;
160 // Replace the HTML check with:
161 // Allow messages that contain code blocks or are plain text
162 if (strpos($clean_content, '<pre') === false &&
163 strpos($clean_content, '<code') === false &&
164 $clean_content !== strip_tags($entry['content'])) {
165 continue;
166 }
167
168 // Skip entries that lost significant content during sanitization
169 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
170 continue;
171 }
172
173 // More accurate token estimation (1 token ≈ 4 characters)
174 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
175
176 // Check token budget with the new estimate
177 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
178 // Try to fit partial content if it's the first entry
179 if (empty($formatted_history)) {
180 $clean_content = mb_substr($clean_content, 0, ($max_tokens - $reserved_tokens) * 4);
181 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
182 } else {
183 break;
184 }
185 }
186
187 // Add to formatted history
188 $formatted_history[] = [
189 'role' => $entry['role'],
190 'content' => $clean_content
191 ];
192
193 $current_token_count += $token_estimate;
194 }
195
196 // Reverse back to maintain chronological order
197 $formatted_history = array_reverse($formatted_history);
198
199 // Add system message about code context
200 array_unshift($formatted_history, [
201 'role' => 'system',
202 'content' => 'Preserved code blocks are marked with [CODE BLOCK PRESERVED]. '
203 . 'Maintain formatting and syntax highlighting when referencing code.'
204 ]);
205
206 return $formatted_history;
207 }
208
209 public function register_routes() {
210 //error_log(esc_html__('Registering MxChat REST routes', 'mxchat'));
211
212 register_rest_route('mxchat/v1', '/stream', [
213 'methods' => 'GET',
214 'callback' => [$this, 'mxchat_stream_events'],
215 'permission_callback' => [$this, 'verify_chat_session'],
216 ]);
217
218 register_rest_route('mxchat/v1', '/agent-response', [
219 'methods' => 'POST',
220 'callback' => [$this, 'mxchat_handle_agent_response'],
221 'permission_callback' => [$this, 'verify_slack_request'],
222 ]);
223
224 register_rest_route('mxchat/v1', '/slack-interaction', [
225 'methods' => 'POST',
226 'callback' => [$this, 'handle_slack_interaction'],
227 'permission_callback' => [$this, 'verify_slack_request'],
228 ]);
229
230 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
231 }
232
233 /**
234 * Verify valid chat session
235 */
236 public function verify_chat_session($request) {
237 $session_id = $request->get_param('session_id');
238 if (empty($session_id)) {
239 //error_log(esc_html__('Empty session ID in chat request', 'mxchat'));
240 return false;
241 }
242
243 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
244 return $chat_mode === 'agent';
245 }
246
247 /**
248 * Verify request is coming from Slack.
249 *
250 * @param WP_REST_Request $request
251 * @return bool True if valid, false otherwise.
252 */
253 public function verify_slack_request($request) {
254 // Get the Slack signing secret from your plugin options
255 $valid_key = $this->options['live_agent_secret_key'] ?? '';
256
257 if (empty($valid_key)) {
258 //error_log(esc_html__('Slack signing secret not configured', 'mxchat'));
259 return false;
260 }
261
262 $timestamp = $request->get_header('X-Slack-Request-Timestamp');
263 $slack_signature = $request->get_header('X-Slack-Signature');
264
265 // Verify timestamp to prevent replay attacks
266 if (abs(time() - intval($timestamp)) > 300) {
267 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
268 return false;
269 }
270
271 // Get raw request body
272 $request_body = file_get_contents('php://input');
273
274 // Create the signature base string
275 $sig_basestring = "v0:{$timestamp}:{$request_body}";
276
277 // Calculate expected signature
278 $my_signature = 'v0=' . hash_hmac('sha256', $sig_basestring, $valid_key);
279
280 // Compare signatures
281 return hash_equals($my_signature, $slack_signature);
282 }
283
284 public function mxchat_stream_events(WP_REST_Request $request) {
285 header('Content-Type: text/event-stream');
286 header('Cache-Control: no-cache');
287 header('Connection: keep-alive');
288
289 $session_id = sanitize_text_field($request->get_param('session_id'));
290 $last_seen_id = sanitize_text_field($request->get_param('last_seen_id')) ?: '';
291
292 if (empty($session_id)) {
293 echo esc_html__("event: error\ndata: ", 'mxchat') . esc_html__('Missing session_id', 'mxchat') . "\n\n";
294 flush();
295 exit;
296 }
297
298 $history = get_option("mxchat_history_{$session_id}", []);
299
300 // Filter only new messages
301 $new_messages = array_filter($history, function ($message) use ($last_seen_id) {
302 return !empty($message['id']) && $message['id'] > $last_seen_id;
303 });
304
305 // Send new messages if available
306 if (!empty($new_messages)) {
307 echo esc_html__("event: newMessages\ndata: ", 'mxchat') . json_encode(array_values($new_messages)) . "\n\n";
308 } else {
309 // Keep the connection alive
310 echo esc_html__("event: keepAlive\ndata: ", 'mxchat') . "{}\n\n";
311 }
312 flush();
313 exit;
314 }
315
316
317
318
319 private function mxchat_save_chat_message($session_id, $role, $message) {
320 global $wpdb;
321 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
322 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
323
324 // Check if this is the first message in a new session (before any other database operations)
325 $is_new_session = false;
326 if ($role === 'user') { // Only check for user messages, not bot responses
327 $existing_messages = $wpdb->get_var($wpdb->prepare(
328 "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
329 $session_id
330 ));
331 $is_new_session = ($existing_messages == 0);
332 }
333
334 // 1) Extract agent name if present
335 $agent_name = '';
336 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
337 $agent_name = $matches[1];
338 $message = str_replace("Agent: $agent_name - ", '', $message);
339 $session_meta_key = "mxchat_agent_name_{$session_id}";
340 if (empty(get_option($session_meta_key))) {
341 update_option($session_meta_key, $agent_name);
342 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
343 }
344 }
345 // 2) Generate unique message_id
346 $message_id = uniqid();
347 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
348 // 3) Determine user_id
349 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
350 // 4) Determine user_identifier
351 $user_identifier = $agent_name
352 ? $agent_name
353 : MxChat_User::mxchat_get_user_identifier();
354 // 5) Determine displayed_name
355 $user_email = MxChat_User::mxchat_get_user_email();
356 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
357 // 6) Check for a saved email in wp_options
358 $email_option_key = "mxchat_email_{$session_id}";
359 $saved_email = get_option($email_option_key);
360 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
361 // If found, update DB user_email
362 if ($saved_email) {
363 $update_res = $wpdb->update(
364 $table_name,
365 ['user_email' => $saved_email],
366 ['session_id' => $session_id],
367 ['%s'],
368 ['%s']
369 );
370 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
371 }
372 // 7) Save to session history in wp_options
373 $history_key = "mxchat_history_{$session_id}";
374 $history = get_option($history_key, []);
375 $history[] = [
376 'id' => $message_id,
377 'role' => $role,
378 'content' => $message,
379 'timestamp' => round(microtime(true) * 1000),
380 'agent_name' => $displayed_name,
381 ];
382 update_option($history_key, $history, 'no');
383 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
384 // 8) Save the message to DB (INSERT)
385 $insert_data = [
386 'user_id' => $user_id,
387 'user_identifier'=> $user_identifier,
388 'user_email' => $saved_email ?: $user_email,
389 'session_id' => $session_id,
390 'role' => $role,
391 'message' => $message,
392 'timestamp' => current_time('mysql', 1),
393 ];
394 $wpdb->insert($table_name, $insert_data);
395 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
396
397 // 9) Send notification email if this is the first user message in a new session
398 if ($wpdb->insert_id && $is_new_session && $role === 'user') {
399 $this->send_new_chat_notification($session_id, array(
400 'identifier' => $user_identifier,
401 'email' => $saved_email ?: $user_email,
402 'ip' => $_SERVER['REMOTE_ADDR']
403 ));
404 }
405
406 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
407 return $message_id;
408 }
409 private function send_new_chat_notification($session_id, $user_info = array()) {
410 $options = get_option('mxchat_transcripts_options');
411
412 // Check if notifications are enabled
413 if (empty($options['mxchat_enable_notifications'])) {
414 return false;
415 }
416
417 // Get notification email
418 $to = !empty($options['mxchat_notification_email']) ?
419 $options['mxchat_notification_email'] :
420 get_option('admin_email');
421
422 if (!is_email($to)) {
423 return false;
424 }
425
426 // Prepare email content
427 $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
428
429 $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
430 $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
431 $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
432
433 $message = sprintf(
434 "A new chat session has started on your website.\n\n" .
435 "Session ID: %s\n" .
436 "User: %s\n" .
437 "Email: %s\n" .
438 "IP Address: %s\n" .
439 "Time: %s\n\n" .
440 "View transcripts: %s",
441 $session_id,
442 $user_identifier,
443 $user_email,
444 $user_ip,
445 current_time('mysql'),
446 admin_url('admin.php?page=mxchat-transcripts')
447 );
448
449 // Send email
450 return wp_mail($to, $subject, $message);
451 }
452
453 public function mxchat_handle_save_email_and_response() {
454 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
455
456 // Validate nonce
457 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
458 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
459 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
460 wp_die();
461 }
462
463 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
464 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
465
466 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
467
468 if (empty($session_id) || empty($email)) {
469 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
470 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
471 wp_die();
472 }
473
474 // 1) Always store in wp_options
475 $option_key = "mxchat_email_{$session_id}";
476 update_option($option_key, $email);
477 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$option_key} => {$email}");
478
479 // 2) (Optional) Also store in DB if a row already exists
480 global $wpdb;
481 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
482
483 // Make sure we have a valid placeholder in prepare
484 $sql = $wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE session_id = %s", $session_id);
485 $session_count = $wpdb->get_var($sql);
486
487 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
488
489 if ($session_count) {
490 // Update user_email if row(s) exist
491 $update_sql = $wpdb->prepare(
492 "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
493 $email,
494 $session_id
495 );
496 $wpdb->query($update_sql);
497 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
498 } else {
499 //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
500 }
501
502 // Provide success response
503 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
504 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
505 wp_send_json_success(['message' => $bot_message]);
506 wp_die();
507 }
508
509 public function mxchat_check_email_provided() {
510 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
511
512 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
513 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
514 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
515 }
516
517 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
518 if (empty($session_id)) {
519 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
520 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
521 }
522
523 // Check if the user is logged in
524 if (is_user_logged_in()) {
525 $current_user = wp_get_current_user();
526 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
527 wp_send_json_success(['logged_in' => true, 'email' => $current_user->user_email]);
528 }
529
530 $option_key = "mxchat_email_{$session_id}";
531 $stored_email = get_option($option_key, '');
532
533 //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
534
535 if (!empty($stored_email)) {
536 //error_log("[DEBUG] mxchat_check_email_provided -> Email found, returning success");
537 wp_send_json_success(['email' => $stored_email]);
538 } else {
539 //error_log("[DEBUG] mxchat_check_email_provided -> No email found, returning error");
540 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
541 }
542 }
543
544 // Add this to your plugin's main PHP file
545 public function mxchat_check_new_messages() {
546 if (!isset($_POST['session_id']) || !isset($_POST['last_seen_id'])) {
547 wp_send_json_error(['message' => 'Missing required parameters']);
548 wp_die();
549 }
550
551 $session_id = sanitize_text_field($_POST['session_id']);
552 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
553
554 // Get chat history
555 $history = get_option("mxchat_history_{$session_id}", []);
556
557 if (empty($history)) {
558 wp_send_json_success([
559 'hasNewMessages' => false,
560 'new_messages' => []
561 ]);
562 wp_die();
563 }
564
565 // Filter new messages
566 $new_messages = array_filter($history, function($message) use ($last_seen_id) {
567 return isset($message['id']) && $message['id'] > $last_seen_id;
568 });
569
570 // Sort by ID to ensure proper order
571 usort($new_messages, function($a, $b) {
572 return $a['id'] <=> $b['id'];
573 });
574
575 wp_send_json_success([
576 'hasNewMessages' => !empty($new_messages),
577 'new_messages' => array_values($new_messages),
578 'latestMessageId' => end($new_messages)['id'] ?? $last_seen_id
579 ]);
580 wp_die();
581 }
582
583 public function mxchat_handle_chat_request() {
584 global $wpdb;
585
586
587 // Check if MX Chat Moderation is active
588 if (class_exists('MX_Chat_Moderation')) {
589 // Get user email and IP
590 $user_email = '';
591 $user_ip = $_SERVER['REMOTE_ADDR'];
592
593 // If user is logged in, get their email
594 if (is_user_logged_in()) {
595 $current_user = wp_get_current_user();
596 $user_email = $current_user->user_email;
597 }
598
599 // Create ban handler instance
600 $ban_handler = new MX_Chat_Ban_Handler();
601
602 // Check if user is banned by IP
603 if ($ban_handler->check_ban($user_ip, 'ip')) {
604 wp_send_json([
605 'success' => false,
606 'message' => esc_html__('Access denied. Your IP address has been banned.', 'mxchat'),
607 'status' => 'banned'
608 ]);
609 wp_die();
610 }
611
612 // If user is logged in, also check email
613 if (!empty($user_email) && $ban_handler->check_ban($user_email, 'email')) {
614 wp_send_json([
615 'success' => false,
616 'message' => esc_html__('Access denied. Your email address has been banned.', 'mxchat'),
617 'status' => 'banned'
618 ]);
619 wp_die();
620 }
621 }
622
623 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
624 $this->productCardHtml = '';
625
626 // Get the actual WordPress user ID if logged in
627 $is_logged_in = is_user_logged_in();
628 if ($is_logged_in) {
629 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
630 } else {
631 // For logged-out users, use your existing identifier method
632 $user_id = $this->mxchat_get_user_identifier();
633 }
634
635 // Get and sanitize the user identifier
636 $user_id = sanitize_key($user_id);
637
638 // Check rate limit using new settings structure
639 $rate_limit_result = $this->check_rate_limit();
640
641 // Add this at the start of your rate limit checking in mxchat_handle_chat_request()
642 //error_log('MXChat Rate Limit: Starting rate limit check in handle_chat_request()');
643
644 // Then right after checking the result:
645 if ($rate_limit_result !== true) {
646 //error_log('MXChat Rate Limit: Rate limit exceeded, returning error');
647 wp_send_json([
648 'success' => false,
649 'message' => $rate_limit_result['message'],
650 'status' => 'rate_limit_exceeded'
651 ]);
652 wp_die();
653 } else {
654 //error_log('MXChat Rate Limit: Check passed successfully');
655 }
656
657 // Rest of your existing code...
658 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
659 //error_log("Session ID: $session_id");
660
661 if (empty($session_id)) {
662 //error_log("Error: Session ID is missing.");
663 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
664 wp_die();
665 }
666
667 // Validate and sanitize the incoming message
668 if (empty($_POST['message'])) {
669 //error_log("Error: No message received.");
670 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
671 wp_die();
672 }
673
674
675 // Modify the message sanitization to preserve PHP tags in code blocks
676 $allowed_tags = [
677 'pre' => [],
678 'code' => ['class' => true],
679 'span' => ['class' => true],
680 'div' => ['class' => true],
681 ];
682
683 // First preserve code blocks
684 $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
685 return htmlspecialchars_decode($matches[0]);
686 }, $_POST['message']);
687
688 // Then apply sanitization
689 $message = wp_kses($message, $allowed_tags);
690
691 // Decode code blocks
692 $message = preg_replace_callback('/(&lt;pre&gt;&lt;code.*?&gt;.*?&lt;\/code&gt;&lt;\/pre&gt;)/s', function($matches) {
693 return htmlspecialchars_decode($matches[1]);
694 }, $message);
695
696 $message = trim($message);
697
698 // Preserve code blocks from markdown conversion
699 $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
700
701 // Check if any add-ons want to pre-process this message (for web search etc.)
702 $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
703
704 // If the pre-processing returned a result (not the original message), use it directly
705 if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
706 // Save the AI response
707 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
708
709 // Save HTML content if provided
710 if (!empty($pre_processed_result['html'])) {
711 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
712 }
713
714 // Return the response
715 wp_send_json([
716 'text' => $pre_processed_result['text'],
717 'html' => $pre_processed_result['html'] ?? '',
718 'session_id' => $session_id
719 ]);
720 wp_die();
721 }
722
723 // Save the user's message
724 $this->mxchat_save_chat_message($session_id, 'user', $message);
725
726 // Check if the message is an email address
727 if (is_email($message)) {
728 // Add the email to Loops
729 $this->add_email_to_loops($message);
730
731 // Send success response
732 $response_message = $this->options['email_capture_response'] ??
733 esc_html__('Thank you! Your coupon is on the way!', 'mxchat');
734
735 wp_send_json([
736 'success' => true,
737 'status' => 'email_captured',
738 'message' => $response_message
739 ]);
740 wp_die();
741 }
742
743 $intent_info = '';
744
745 // Check chat mode
746 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
747 //error_log("Chat Mode: $chat_mode");
748
749 // Handle agent mode
750 if ($chat_mode === 'agent') {
751 // First, check for switch intent before doing anything else
752 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
753
754 // If we matched an intent and it's the switch intent, handle it
755 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
756 //error_log("Switch to chatbot intent detected");
757
758 // Update chat mode first
759 update_option("mxchat_mode_{$session_id}", 'ai');
760
761 // Clear any existing PDF context to start fresh
762 $this->clear_pdf_transients($session_id);
763
764 // Prepare clean switch response
765 $response_data = [
766 'text' => $this->fallbackResponse['text'],
767 'html' => '',
768 'session_id' => $session_id,
769 'chat_mode' => 'ai'
770 ];
771
772 // Save the mode switch message
773 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
774 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
775
776 // Send response and exit
777 wp_send_json($response_data);
778 wp_die();
779 } elseif (!$intent_matched) {
780 // No intent matched, handle live agent message
781 try {
782 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
783 //error_log("Message sent to agent.");
784
785 wp_send_json_success([
786 'status' => 'waiting_for_agent',
787 'message' => esc_html__('Message sent to live agent.', 'mxchat')
788 ]);
789 } catch (\Exception $e) {
790 //error_log("Error sending message to agent: " . $e->getMessage());
791 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
792 }
793 wp_die();
794 }
795 }
796
797 // Step 1: Check for new PDF URL in the message
798 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
799 $new_pdf_url = $matches[0];
800
801 // Check if this is likely a PDF-related request
802 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
803 $is_pdf_request = false;
804
805 foreach ($pdf_keywords as $keyword) {
806 if (stripos($message, $keyword) !== false) {
807 $is_pdf_request = true;
808 break;
809 }
810 }
811
812 // If it looks like a PDF request or we're waiting for a PDF URL
813 if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
814 // Validate HTTPS
815 if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
816 // Extract filename from URL
817 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
818
819 // Clear previous PDF transients
820 $this->clear_pdf_transients($session_id);
821
822 // Process new PDF
823 $max_pages = $this->options['pdf_max_pages'] ?? 69;
824 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
825
826 if ($embeddings === 'too_many_pages') {
827 $error_text = sprintf(
828 $this->options['pdf_intent_error_text'] ??
829 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
830 $max_pages
831 );
832 $this->fallbackResponse['text'] = $error_text;
833 } elseif ($embeddings) {
834 // Store new PDF information
835 // Create a more meaningful filename from URL
836 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
837
838 // If the filename is generic (like results_download.php), create a more descriptive one
839 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
840 strpos($pdf_filename, '.php') !== false) {
841 // Create a timestamp-based name
842 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
843 }
844
845 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
846 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
847 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
848 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
849
850 $success_text = $this->options['pdf_intent_success_text'] ??
851 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
852
853 // Return success with filename for UI update
854 wp_send_json([
855 'success' => true,
856 'message' => $success_text,
857 'data' => [
858 'filename' => $pdf_filename
859 ]
860 ]);
861 wp_die();
862 } else {
863 $error_text = $this->options['pdf_intent_error_text'] ??
864 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
865 $this->fallbackResponse['text'] = $error_text;
866 }
867
868 wp_send_json([
869 'success' => false,
870 'message' => $this->fallbackResponse['text']
871 ]);
872 wp_die();
873 }
874 }
875 }
876
877
878 // Add this before the intent check section (before Step 2) in mxchat_handle_chat_request
879 // Check if there's an active recommendation flow session
880 $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
881 if (!empty($flow_state) && isset($flow_state['flow_id'])) {
882 //error_log('MXCHAT DEBUG: Detected active recommendation flow, routing directly');
883
884 // Create a dummy intent object that matches the original intent
885 $dummy_intent = new stdClass();
886 $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
887 $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
888
889 // Call the recommendation flow handler directly
890 $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
891
892 // If the handler returned a response, send it
893 if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
894 // Save the bot's response to the chat history
895 if (!empty($response_data['text'])) {
896 $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
897 }
898 if (!empty($response_data['html'])) {
899 $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
900 }
901
902 // Send the response
903 wp_send_json($response_data);
904 wp_die();
905 }
906
907 // If we reach here, the flow handler didn't provide a usable response
908 // We'll continue with regular processing
909 //error_log('MXCHAT DEBUG: Recommendation flow handler did not provide a usable response');
910 }
911
912 // Step 2: Detect intent and handle intent-based responses
913 $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
914 //error_log("Intent Result Type: " . gettype($intent_result));
915
916 // Step 3: Handle the intent result appropriately
917 if ($intent_result !== false) {
918 // The intent was matched and handled
919 //error_log("Intent was matched and handled.");
920
921 if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
922 // Intent returned a direct response array
923 //error_log("Intent returned a direct response.");
924 $response_data = [
925 'text' => $intent_result['text'] ?? '',
926 'html' => $intent_result['html'] ?? '',
927 'session_id' => $session_id
928 ];
929
930 wp_send_json($response_data);
931 wp_die();
932 }
933 else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
934 // Intent returned true and set fallbackResponse
935 //error_log("Intent returned true with fallbackResponse set.");
936 $response_data = [
937 'text' => $this->fallbackResponse['text'] ?? '',
938 'html' => $this->fallbackResponse['html'] ?? '',
939 'session_id' => $session_id
940 ];
941
942 wp_send_json($response_data);
943 wp_die();
944 }
945
946 // Intent was matched but no usable response was provided
947 // This shouldn't happen with proper intent implementation
948 //error_log("Warning: Intent matched but no response provided.");
949 }
950
951 // If we get here, no intent matched OR the intent didn't provide a usable response
952 //error_log("No matching intent or usable response. Generating AI response.");
953
954 // Step 4: Generate AI response
955 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
956 $this->mxchat_increment_chat_count();
957
958 // Generate embedding for the user's query
959 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
960
961 // Check if the embedding generation returned an error
962 if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
963 $error_message = $user_message_embedding['error'];
964 $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
965
966 //error_log("Embedding error for session $session_id: $error_message (Code: $error_code)");
967
968 // Important: Structure the error data correctly for wp_send_json_error
969 wp_send_json_error([
970 'error_message' => $error_message,
971 'error_code' => $error_code
972 ]);
973 wp_die();
974 }
975
976 // Check if the embedding is valid
977 if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
978 //error_log("Failed to generate message embedding for session $session_id");
979 wp_send_json_error([
980 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
981 'error_code' => 'invalid_embedding'
982 ]);
983 wp_die();
984 }
985
986 // Build context with both knowledge base and PDF content if available
987 $context_content = "User asked: '{$message}'\n\n";
988
989
990 // Get relevant content from knowledge base
991 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
992 if (!empty($relevant_content)) {
993 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
994 } else {
995 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
996 }
997
998 // Check for and include PDF content
999 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1000 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1001 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1002 if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1003 $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1004 if (!empty($relevant_pdf_pages)) {
1005 $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1006 foreach ($relevant_pdf_pages as $page_data) {
1007 $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1008 }
1009 $context_content .= "\n";
1010 }
1011 }
1012
1013 // Check for and include Word content
1014 $word_url = get_transient('mxchat_word_url_' . $session_id);
1015 $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1016 $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1017 if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1018 $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1019 if (!empty($relevant_word_chunks)) {
1020 $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1021 foreach ($relevant_word_chunks as $chunk_data) {
1022 $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1023 }
1024 $context_content .= "\n";
1025 }
1026 }
1027
1028 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1029
1030 // Generate the response using the full context
1031 $response = $this->mxchat_generate_response(
1032 $context_content,
1033 $this->options['api_key'],
1034 $this->options['xai_api_key'],
1035 $this->options['claude_api_key'],
1036 $this->options['deepseek_api_key'],
1037 $this->options['gemini_api_key'],
1038 $conversation_history
1039 );
1040
1041 // Check if the response is an error array
1042 if (is_array($response) && isset($response['error'])) {
1043 //error_log("AI Response Error: " . $response['error'] . " (Code: " . ($response['error_code'] ?? 'unknown') . ")");
1044
1045 // Send a user-friendly error message
1046 wp_send_json_error([
1047 'error_message' => $response['error'],
1048 'error_code' => $response['error_code'] ?? 'api_error'
1049 ]);
1050 wp_die();
1051 }
1052
1053 // If we get here, the response is valid text
1054 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1055
1056 // Step 5: Save additional content if available
1057 if (!empty($this->productCardHtml)) {
1058 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1059 }
1060
1061 if (!empty($this->fallbackResponse['html'])) {
1062 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1063 }
1064
1065 // Step 6: Return the response
1066 $response_data = [
1067 'text' => $response,
1068 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1069 'session_id' => $session_id
1070 ];
1071
1072 wp_send_json($response_data);
1073 wp_die();
1074 }
1075
1076 // Updated function to check intents and invoke the callback function
1077 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1078 global $wpdb;
1079 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1080
1081 //error_log('🔍 MXCHAT DEBUG: Intent Check Started ==================');
1082 //error_log("🔍 MXCHAT DEBUG: Message: '$message'");
1083 //error_log("🔍 MXCHAT DEBUG: Chat Mode: $chat_mode");
1084
1085 // Generate the user embedding
1086 //error_log('🔄 MXCHAT DEBUG: Generating user embedding');
1087 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1088
1089 // Check if embedding generation returned an error
1090 if (is_array($user_embedding) && isset($user_embedding['error'])) {
1091 $error_message = $user_embedding['error'];
1092 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1093
1094 //error_log("❌ MXCHAT DEBUG: Embedding error: $error_message (Code: $error_code)");
1095
1096 // Send the error to the frontend
1097 wp_send_json_error([
1098 'error_message' => $error_message,
1099 'error_code' => $error_code
1100 ]);
1101 wp_die();
1102 }
1103
1104 // Check if embedding is valid (not an error and is an array)
1105 if (!is_array($user_embedding) || empty($user_embedding)) {
1106 //error_log('❌ MXCHAT DEBUG: Failed to generate user embedding');
1107
1108 // Send a generic error to the frontend
1109 wp_send_json_error([
1110 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1111 'error_code' => 'invalid_embedding'
1112 ]);
1113 wp_die();
1114 }
1115
1116 //error_log('�
1117 MXCHAT DEBUG: User embedding generated successfully');
1118
1119 // Fetch intents from the database
1120 $table_name = $wpdb->prefix . 'mxchat_intents';
1121 if ($chat_mode === 'agent') {
1122 //error_log('🔍 MXCHAT DEBUG: Agent mode - fetching only chatbot switch intent');
1123 $query = $wpdb->prepare(
1124 "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
1125 'mxchat_handle_switch_to_chatbot_intent'
1126 );
1127 $intents = $wpdb->get_results($query);
1128 } else {
1129 //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all enabled intents');
1130 // Only fetch enabled intents (either explicitly enabled with 1 or implicitly enabled with NULL for backward compatibility)
1131 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
1132 }
1133
1134 //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' enabled intents to check');
1135
1136 if (empty($intents)) {
1137 //error_log('❌ MXCHAT DEBUG: No enabled intents found in database');
1138 return false;
1139 }
1140
1141 $highest_similarity = -INF;
1142 $matched_intent = null;
1143
1144 //error_log('📊 MXCHAT DEBUG: Intent Similarity Scores ==================');
1145 foreach ($intents as $intent) {
1146 //error_log("🔄 MXCHAT DEBUG: Checking intent: '{$intent->intent_label}' (callback: {$intent->callback_function})");
1147
1148 // Additional check for enabled state in case database structure was modified
1149 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1150 if (!$is_enabled) {
1151 //error_log("⚠️ MXCHAT DEBUG: Skipping disabled intent: {$intent->intent_label}");
1152 continue;
1153 }
1154
1155 $intent_embedding_serialized = $intent->embedding_vector;
1156 $intent_embedding = $intent_embedding_serialized
1157 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1158 : null;
1159
1160 if (!is_array($intent_embedding)) {
1161 //error_log("❌ MXCHAT DEBUG: Invalid embedding for intent: {$intent->intent_label}");
1162 continue;
1163 }
1164
1165 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1166 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1167
1168 //error_log("📊 MXCHAT DEBUG: Intent '{$intent->intent_label}' similarity: {$similarity}, threshold: {$intent_threshold}");
1169
1170 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1171 $highest_similarity = $similarity;
1172 $matched_intent = $intent;
1173 //error_log("�
1174 MXCHAT DEBUG: New best match: '{$intent->intent_label}' with similarity {$similarity}");
1175 }
1176 }
1177 //error_log('📊 MXCHAT DEBUG: End Intent Scores ==================');
1178
1179 if ($matched_intent) {
1180 //error_log("🎯 MXCHAT DEBUG: Final Intent Match: '{$matched_intent->intent_label}'");
1181 //error_log("🔄 MXCHAT DEBUG: Invoking callback: {$matched_intent->callback_function}");
1182
1183 // If the callback is a method on this instance (core callback), call it directly
1184 if (method_exists($this, $matched_intent->callback_function)) {
1185 //error_log('🔍 MXCHAT DEBUG: Using direct method call for core callback');
1186 $callback_result = call_user_func(
1187 [$this, $matched_intent->callback_function],
1188 $message,
1189 $user_id,
1190 $session_id,
1191 $matched_intent,
1192 $user_context
1193 );
1194 } else {
1195 //error_log('🔍 MXCHAT DEBUG: Using apply_filters for add-on callback');
1196 // Otherwise, use apply_filters for add-on callbacks
1197 $callback_result = apply_filters(
1198 $matched_intent->callback_function,
1199 false, // default return value
1200 $message,
1201 $user_id,
1202 $session_id,
1203 $matched_intent
1204 );
1205 }
1206
1207 //error_log('🔍 MXCHAT DEBUG: Callback result type: ' . gettype($callback_result));
1208 if ($callback_result !== false) {
1209 //error_log('�
1210 MXCHAT DEBUG: Intent handled successfully');
1211 $this->fallbackResponse = $callback_result;
1212 return true;
1213 }
1214 //error_log('❌ MXCHAT DEBUG: Callback returned false');
1215 } else {
1216 //error_log('❌ MXCHAT DEBUG: No matching intent found');
1217 }
1218
1219 //error_log('🔍 MXCHAT DEBUG: Intent Check Completed ==================');
1220 return false;
1221 }
1222
1223 // Helper function to clear PDF and Word document related transients
1224 private function clear_pdf_transients($session_id) {
1225 // PDF transients
1226 delete_transient('mxchat_pdf_url_' . $session_id);
1227 delete_transient('mxchat_pdf_embeddings_' . $session_id);
1228 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
1229 delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1230
1231 // Word document transients
1232 delete_transient('mxchat_word_url_' . $session_id);
1233 delete_transient('mxchat_word_filename_' . $session_id);
1234 delete_transient('mxchat_word_embeddings_' . $session_id);
1235 delete_transient('mxchat_include_word_in_context_' . $session_id);
1236 delete_transient('mxchat_waiting_for_word_' . $session_id);
1237 }
1238
1239
1240
1241 //verified good
1242 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1243 // Log the message safely
1244 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1245
1246 // Initiate email capture flow
1247 $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1248
1249 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1250 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1251
1252 // Respond to the user
1253 wp_send_json(['message' => $response]);
1254 wp_die();
1255 }
1256
1257 public function mxchat_generate_image($message, $user_id, $session_id) {
1258 //error_log("Starting image generation for message: " . $message);
1259
1260 // Prepare a prompt for DALL-E
1261 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1262
1263 // Use the existing OpenAI API key
1264 $openai_api_key = sanitize_text_field($this->options['api_key']);
1265
1266 // Call DALL-E to generate an image
1267 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1268
1269 // Check if the response contains an image URL
1270 if (isset($image_response['imageUrl'])) {
1271 $image_url = esc_url_raw($image_response['imageUrl']);
1272
1273 // Construct the HTML with a CSS class instead of inline styles
1274 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1275 $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1276
1277 // Save the bot message with both text and HTML
1278 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1279 $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1280
1281 // Set the fallback response for the chat handler
1282 $this->fallbackResponse = [
1283 'text' => $response_text,
1284 'html' => $response_html,
1285 'images' => [$image_url]
1286 ];
1287
1288 // For debugging/verification - Use json_encode to verify what's being set
1289 //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1290
1291 // Return the response directly instead of relying on the property
1292 return $this->fallbackResponse;
1293 } else {
1294 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1295
1296 // Save the error message
1297 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1298
1299 // Set the fallback response for the chat handler
1300 $this->fallbackResponse = [
1301 'text' => $response_text,
1302 'html' => '',
1303 'images' => []
1304 ];
1305
1306 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1307 //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1308
1309 // Return the response directly instead of relying on the property
1310 return $this->fallbackResponse;
1311 }
1312 }
1313 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1314 $api_url = 'https://api.openai.com/v1/images/generations';
1315 $body = json_encode([
1316 'prompt' => sanitize_text_field($prompt),
1317 'n' => 1,
1318 'size' => '1024x1024',
1319 'model' => sanitize_text_field($model),
1320 ]);
1321
1322 $args = [
1323 'body' => $body,
1324 'headers' => [
1325 'Content-Type' => 'application/json',
1326 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
1327 ],
1328 'method' => 'POST',
1329 'timeout' => absint($timeout),
1330 ];
1331
1332 $response = wp_remote_post($api_url, $args);
1333
1334 if (is_wp_error($response)) {
1335 //error_log("DALL-E request failed: " . $response->get_error_message());
1336 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
1337 }
1338
1339 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1340
1341 if (isset($response_body['data'][0]['url'])) {
1342 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
1343 } else {
1344 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
1345 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
1346 }
1347 }
1348
1349 /**
1350 * Handle web search requests.
1351 *
1352 * Sends the refined search query to the Brave Search API and uses the
1353 * results to generate a conversational response with the AI model.
1354 *
1355 * @since 1.0.0
1356 * @param string $message The user's search query.
1357 * @param string $user_id The user identifier.
1358 * @param string $session_id The current session ID.
1359 * @return array Response array containing text with embedded HTML links
1360 */
1361 public function mxchat_handle_search_request($message, $user_id, $session_id) {
1362 // Step 1: Interpret and refine the search query
1363 $refined_search_query = $this->mxchat_interpret_search_query($message);
1364 if (empty($refined_search_query)) {
1365 return array(
1366 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1367 'html' => ''
1368 );
1369 }
1370
1371 // Retrieve and validate API settings
1372 $options = get_option('mxchat_options');
1373 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1374 $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1375
1376 if (empty($api_key)) {
1377 return array(
1378 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1379 'html' => ''
1380 );
1381 }
1382
1383 // Build the API request URL
1384 $api_url = add_query_arg(
1385 array(
1386 'q' => rawurlencode($refined_search_query),
1387 'count' => $results_count,
1388 'text_decorations' => 'true',
1389 'rich_data' => 'true',
1390 ),
1391 'https://api.search.brave.com/res/v1/web/search'
1392 );
1393
1394 // Attempt to retrieve cached results first
1395 $transient_key = 'mxchat_search_' . md5($refined_search_query);
1396 $results = get_transient($transient_key);
1397
1398 if (false === $results) {
1399 // Fetch new results from the Brave Search API
1400 $response = wp_remote_get(
1401 $api_url,
1402 array(
1403 'headers' => array(
1404 'Accept' => 'application/json',
1405 'Accept-Encoding' => 'gzip',
1406 'X-Subscription-Token'=> $api_key,
1407 ),
1408 'timeout' => 10,
1409 )
1410 );
1411
1412 if (is_wp_error($response)) {
1413 return array(
1414 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1415 'html' => ''
1416 );
1417 }
1418
1419 $results = json_decode(wp_remote_retrieve_body($response), true);
1420
1421 if (json_last_error() !== JSON_ERROR_NONE) {
1422 return array(
1423 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1424 'html' => ''
1425 );
1426 }
1427
1428 // Cache results for one hour
1429 set_transient($transient_key, $results, HOUR_IN_SECONDS);
1430 }
1431
1432 // Process results
1433 if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1434 // Create a more straightforward summary with HTML links
1435 $search_results_text = '';
1436
1437 // Add a simple intro
1438 $search_results_text .= sprintf(
1439 esc_html__("Here's what I found about '%s':", 'mxchat'),
1440 esc_html($refined_search_query)
1441 );
1442
1443 // Add the top results with HTML links
1444 foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1445 $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1446 $url = isset($result['url']) ? esc_url($result['url']) : '';
1447 $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1448
1449 // Add a line break after the intro
1450 $search_results_text .= '<br><br>';
1451
1452 // Add title as a link
1453 $search_results_text .= sprintf(
1454 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1455 $url,
1456 $title
1457 );
1458
1459 // Add a condensed description
1460 $search_results_text .= sprintf("%s", $description);
1461 }
1462
1463 // Save to chat history
1464 $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1465
1466 // Return the formatted text with embedded HTML links
1467 return array(
1468 'text' => $search_results_text,
1469 'html' => ''
1470 );
1471 } else {
1472 return array(
1473 'text' => sprintf(
1474 esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1475 esc_html($refined_search_query)
1476 ),
1477 'html' => ''
1478 );
1479 }
1480 }
1481 /**
1482 * Format search results into a natural text summary.
1483 *
1484 * @since 1.0.0
1485 * @param array $results The search results from the API.
1486 * @param string $query The original search query.
1487 * @return string The text summary of the top results.
1488 */
1489 private function format_search_results( $results, $query ) {
1490 $summary = sprintf(
1491 esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1492 esc_html( $query )
1493 ) . "\n\n";
1494
1495 $max_results = min( count( $results ), 3 );
1496 for ( $i = 0; $i < $max_results; $i++ ) {
1497 $result = $results[ $i ];
1498 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1499 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1500
1501 // Append title and description to the summary
1502 $summary .= sprintf(
1503 "%s\n%s\n\n",
1504 esc_html( $title ),
1505 esc_html( $description )
1506 );
1507 }
1508
1509 return $summary;
1510 }
1511
1512 /**
1513 * Generate HTML markup for search results.
1514 *
1515 * @since 1.0.0
1516 * @param array $results The search results from the API.
1517 * @param string $query The user-refined query.
1518 * @return string The HTML markup for displaying the results.
1519 */
1520 private function generate_search_results_html( $results, $query ) {
1521 ob_start();
1522 ?>
1523 <div class="mxchat-search-results">
1524 <?php foreach ( $results as $result ) :
1525 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1526 $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1527 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1528 $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1529 $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1530 $domain = parse_url( $url, PHP_URL_HOST );
1531 ?>
1532 <div class="mxchat-search-item">
1533 <div class="mxchat-search-header">
1534 <?php if ( $favicon ) : ?>
1535 <img
1536 src="<?php echo esc_url( $favicon ); ?>"
1537 class="mxchat-site-icon"
1538 alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1539 width="16"
1540 height="16"
1541 />
1542 <?php endif; ?>
1543 <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1544 </div>
1545
1546 <div class="mxchat-search-content">
1547 <h3 class="mxchat-search-title">
1548 <a href="<?php echo esc_url( $url ); ?>"
1549 target="_blank"
1550 rel="noopener noreferrer"
1551 >
1552 <?php echo esc_html( $title ); ?>
1553 </a>
1554 </h3>
1555
1556 <?php if ( $thumbnail ) : ?>
1557 <div class="mxchat-search-thumbnail">
1558 <img
1559 src="<?php echo esc_url( $thumbnail ); ?>"
1560 alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1561 loading="lazy"
1562 />
1563 </div>
1564 <?php endif; ?>
1565
1566 <div class="mxchat-search-description">
1567 <?php echo esc_html( $description ); ?>
1568 </div>
1569 </div>
1570 </div>
1571 <?php endforeach; ?>
1572 </div>
1573 <?php
1574 return ob_get_clean();
1575 }
1576
1577
1578 //very good
1579 /**
1580 * Handle image search requests from the chatbot
1581 *
1582 * @param string $message The user's search query
1583 * @param int $user_id The user's ID
1584 * @param string $session_id The chat session ID
1585 * @return array Response array with text and HTML content
1586 */
1587 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1588 // Step 1: Interpret the search query using the user's selected AI model
1589 $refined_search_query = $this->mxchat_interpret_search_query($message);
1590
1591 // If no query was interpreted, return a fallback message
1592 if (empty($refined_search_query)) {
1593 return array(
1594 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1595 'html' => "",
1596 );
1597 }
1598
1599 // Brave API URL
1600 $api_url = 'https://api.search.brave.com/res/v1/images/search';
1601
1602 // Retrieve Brave API settings
1603 $options = get_option('mxchat_options');
1604 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1605
1606 if (empty($api_key)) {
1607 return array(
1608 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1609 'html' => "",
1610 );
1611 }
1612
1613 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1614 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
1615
1616 // Append query parameters based on settings
1617 $api_url = add_query_arg([
1618 'q' => rawurlencode($refined_search_query),
1619 'count' => $image_count,
1620 'safesearch' => $safe_search,
1621 ], $api_url);
1622
1623 // Implement caching
1624 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1625 $body = get_transient($transient_key);
1626
1627 if (false === $body) {
1628 $args = [
1629 'headers' => [
1630 'Accept' => 'application/json',
1631 'Accept-Encoding' => 'gzip',
1632 'X-Subscription-Token' => $api_key,
1633 ],
1634 'timeout' => 10,
1635 ];
1636
1637 $response = wp_remote_get($api_url, $args);
1638
1639 if (is_wp_error($response)) {
1640 return array(
1641 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1642 'html' => "",
1643 );
1644 }
1645
1646 $body = json_decode(wp_remote_retrieve_body($response), true);
1647 set_transient($transient_key, $body, HOUR_IN_SECONDS);
1648 }
1649
1650 // Process the API response
1651 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1652 $html_output = '<div class="mxchat-image-gallery">';
1653
1654 // Get the configured image count (1-6)
1655 $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1656 $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
1657
1658 // Use only the requested number of images
1659 for ($i = 0; $i < $display_count; $i++) {
1660 $image = $body['results'][$i];
1661 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1662 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1663 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1664
1665 if ($image_url && $thumbnail_url) {
1666 $html_output .= '<div class="mxchat-image-item">';
1667 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
1668 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
1669 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
1670 $html_output .= '</a></div>';
1671 }
1672 }
1673
1674 $html_output .= '</div>';
1675
1676 // Create response text
1677 $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
1678
1679 // Save both response text and HTML to chat history
1680 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1681 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1682
1683 // Return the combined response
1684 return array(
1685 'text' => $response_text,
1686 'html' => $html_output,
1687 );
1688 } else {
1689 $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
1690
1691 // Save the error message to chat history
1692 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1693
1694 return array(
1695 'text' => $response_text,
1696 'html' => "",
1697 );
1698 }
1699 }
1700
1701 /**
1702 * Interpret the search query using the user's selected AI model
1703 *
1704 * @param string $user_query The original query from the user
1705 * @return string The refined search query
1706 */
1707 public function mxchat_interpret_search_query($user_query) {
1708 $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');
1709
1710 // Get options and determine the selected model
1711 $options = $this->options ?? get_option('mxchat_options');
1712 $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1713
1714 // Extract model prefix to determine the provider
1715 $model_parts = explode('-', $selected_model);
1716 $provider = strtolower($model_parts[0]);
1717
1718 // Determine which API key to use based on the provider
1719 switch ($provider) {
1720 case 'gemini':
1721 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
1722 if (empty($api_key)) {
1723 return sanitize_text_field($user_query); // Default to original query if API key missing
1724 }
1725 return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
1726
1727 case 'claude':
1728 $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
1729 if (empty($api_key)) {
1730 return sanitize_text_field($user_query);
1731 }
1732 return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
1733
1734 case 'grok':
1735 $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
1736 if (empty($api_key)) {
1737 return sanitize_text_field($user_query);
1738 }
1739 return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
1740
1741 case 'deepseek':
1742 $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
1743 if (empty($api_key)) {
1744 return sanitize_text_field($user_query);
1745 }
1746 return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
1747
1748 case 'gpt':
1749 default:
1750 // Default to OpenAI for custom models or unrecognized prefixes
1751 $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
1752 if (empty($api_key)) {
1753 return sanitize_text_field($user_query);
1754 }
1755 return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1756 }
1757 }
1758
1759 /**
1760 * Interpret query using OpenAI models
1761 */
1762 private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1763 $url = 'https://api.openai.com/v1/chat/completions';
1764 $args = [
1765 'headers' => [
1766 'Authorization' => 'Bearer ' . $api_key,
1767 'Content-Type' => 'application/json',
1768 ],
1769 'body' => wp_json_encode([
1770 'model' => $model,
1771 'messages' => [
1772 ['role' => 'system', 'content' => $system_prompt],
1773 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1774 ],
1775 'temperature' => 0.2,
1776 'max_tokens' => 20,
1777 ]),
1778 'method' => 'POST',
1779 'timeout' => 15,
1780 ];
1781
1782 $response = wp_remote_post($url, $args);
1783 if (is_wp_error($response)) {
1784 return sanitize_text_field($user_query);
1785 }
1786
1787 $body = json_decode(wp_remote_retrieve_body($response), true);
1788 return isset($body['choices'][0]['message']['content'])
1789 ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
1790 : sanitize_text_field($user_query);
1791 }
1792
1793 /**
1794 * Interpret query using Claude models
1795 */
1796 private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
1797 $url = 'https://api.anthropic.com/v1/messages';
1798
1799 $args = [
1800 'headers' => [
1801 'Content-Type' => 'application/json',
1802 'x-api-key' => $api_key,
1803 'anthropic-version' => '2023-06-01',
1804 ],
1805 'body' => wp_json_encode([
1806 'model' => $model,
1807 'system' => $system_prompt,
1808 'messages' => [
1809 ['role' => 'user', 'content' => sanitize_text_field($user_query)]
1810 ],
1811 'max_tokens' => 20,
1812 'temperature' => 0.2,
1813 ]),
1814 'method' => 'POST',
1815 'timeout' => 15,
1816 ];
1817
1818 $response = wp_remote_post($url, $args);
1819 if (is_wp_error($response)) {
1820 return sanitize_text_field($user_query);
1821 }
1822
1823 $body = json_decode(wp_remote_retrieve_body($response), true);
1824 if (!empty($body['content'][0]['text'])) {
1825 return sanitize_text_field(trim($body['content'][0]['text']));
1826 }
1827
1828 return sanitize_text_field($user_query);
1829 }
1830
1831 /**
1832 * Interpret query using Gemini models
1833 */
1834 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
1835 // Strip "gemini-" prefix for the API
1836 $model_version = str_replace('gemini-', '', $model);
1837
1838 $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
1839
1840 $args = [
1841 'headers' => [
1842 'Content-Type' => 'application/json',
1843 ],
1844 'body' => wp_json_encode([
1845 'contents' => [
1846 [
1847 'role' => 'user',
1848 'parts' => [
1849 ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
1850 ]
1851 ]
1852 ],
1853 'generationConfig' => [
1854 'temperature' => 0.2,
1855 'maxOutputTokens' => 20,
1856 ],
1857 ]),
1858 'method' => 'POST',
1859 'timeout' => 15,
1860 ];
1861
1862 $response = wp_remote_post($url, $args);
1863 if (is_wp_error($response)) {
1864 return sanitize_text_field($user_query);
1865 }
1866
1867 $body = json_decode(wp_remote_retrieve_body($response), true);
1868 if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
1869 return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1870 }
1871
1872 return sanitize_text_field($user_query);
1873 }
1874
1875 /**
1876 * Interpret query using X.AI (Grok) models
1877 */
1878 private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
1879 $url = 'https://api.xai.com/v1/chat/completions';
1880
1881 $args = [
1882 'headers' => [
1883 'Content-Type' => 'application/json',
1884 'Authorization' => 'Bearer ' . $api_key,
1885 ],
1886 'body' => wp_json_encode([
1887 'model' => $model,
1888 'messages' => [
1889 ['role' => 'system', 'content' => $system_prompt],
1890 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1891 ],
1892 'temperature' => 0.2,
1893 'max_tokens' => 20,
1894 ]),
1895 'method' => 'POST',
1896 'timeout' => 15,
1897 ];
1898
1899 $response = wp_remote_post($url, $args);
1900 if (is_wp_error($response)) {
1901 return sanitize_text_field($user_query);
1902 }
1903
1904 $body = json_decode(wp_remote_retrieve_body($response), true);
1905 if (isset($body['choices'][0]['message']['content'])) {
1906 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1907 }
1908
1909 return sanitize_text_field($user_query);
1910 }
1911
1912 /**
1913 * Interpret query using DeepSeek models
1914 */
1915 private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
1916 $url = 'https://api.deepseek.com/v1/chat/completions';
1917
1918 $args = [
1919 'headers' => [
1920 'Content-Type' => 'application/json',
1921 'Authorization' => 'Bearer ' . $api_key,
1922 ],
1923 'body' => wp_json_encode([
1924 'model' => $model,
1925 'messages' => [
1926 ['role' => 'system', 'content' => $system_prompt],
1927 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1928 ],
1929 'temperature' => 0.2,
1930 'max_tokens' => 20,
1931 ]),
1932 'method' => 'POST',
1933 'timeout' => 15,
1934 ];
1935
1936 $response = wp_remote_post($url, $args);
1937 if (is_wp_error($response)) {
1938 return sanitize_text_field($user_query);
1939 }
1940
1941 $body = json_decode(wp_remote_retrieve_body($response), true);
1942 if (isset($body['choices'][0]['message']['content'])) {
1943 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1944 }
1945
1946 return sanitize_text_field($user_query);
1947 }
1948
1949
1950
1951 private function find_product_in_message($message) {
1952 global $wpdb;
1953
1954 // Get embedding for the search query
1955 $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1956
1957 // Check if embedding generation returned an error
1958 if (is_array($query_embedding) && isset($query_embedding['error'])) {
1959 $error_message = $query_embedding['error'];
1960 $error_code = $query_embedding['error_code'] ?? 'embedding_error';
1961
1962 //error_log("Product search embedding error: $error_message (Code: $error_code)");
1963
1964 // Set a user-friendly fallback response
1965 $this->fallbackResponse['text'] = esc_html__("I'm having trouble processing your product search. Please try again later or contact support if this persists.", 'mxchat');
1966
1967 // Also store the technical error for admin users
1968 $this->fallbackResponse['admin_error'] = $error_message;
1969 $this->fallbackResponse['error_code'] = $error_code;
1970
1971 return null;
1972 }
1973
1974 // Check if embedding is valid
1975 if (!is_array($query_embedding) || empty($query_embedding)) {
1976 //error_log("Failed to generate embedding for product search");
1977 $this->fallbackResponse['text'] = esc_html__("I couldn't process your product search. Please try again with different wording.", 'mxchat');
1978 return null;
1979 }
1980
1981 // Get relevant content as string
1982 $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1983 if (empty($relevant_content)) {
1984 // Return null to indicate no results and set fallback response
1985 $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');
1986 return null;
1987 }
1988
1989
1990 // Extract product URLs from the content
1991 preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1992
1993 if (!empty($matches[0])) {
1994 // Try each URL found
1995 foreach ($matches[0] as $url) {
1996 // Clean the URL
1997 $url = rtrim($url, '/."\']');
1998
1999 // Get the product slug
2000 $path = parse_url($url, PHP_URL_PATH);
2001 $slug = basename(rtrim($path, '/'));
2002
2003 // Find product by slug
2004 $args = array(
2005 'post_type' => 'product',
2006 'post_status' => 'publish',
2007 'name' => $slug,
2008 'posts_per_page' => 1
2009 );
2010
2011 $products = get_posts($args);
2012
2013 if (!empty($products)) {
2014 $product_id = $products[0]->ID;
2015 $product = wc_get_product($product_id);
2016
2017 if ($product && $product->is_purchasable()) {
2018 return $product_id;
2019 }
2020 }
2021 }
2022 }
2023
2024 // Fallback: Look for product names in the content
2025 $products = wc_get_products([
2026 'status' => 'publish',
2027 'limit' => -1,
2028 'return' => 'all'
2029 ]);
2030
2031 foreach ($products as $product) {
2032 $name = $product->get_name();
2033 if (stripos($relevant_content, $name) !== false) {
2034 if ($product->is_purchasable()) {
2035 return $product->get_id();
2036 }
2037 }
2038 }
2039
2040 // If no product is found after all checks, set the fallback response
2041 $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
2042 return null;
2043 }
2044
2045 // New method to handle intent responses
2046 private function generate_intent_response($context_content, $session_id) {
2047 // Convert the context array to a structured string for the AI
2048 $context_string = $this->format_intent_context($context_content);
2049 // Generate AI response using the context
2050 $response = $this->mxchat_generate_response(
2051 $context_string,
2052 $this->options['api_key'],
2053 $this->options['xai_api_key'],
2054 $this->options['claude_api_key'],
2055 $this->options['deepseek_api_key'],
2056 $this->options['gemini_api_key'], // Added Gemini API key
2057 $this->mxchat_fetch_conversation_history_for_ai($session_id)
2058 );
2059 $this->fallbackResponse['text'] = $response;
2060 return true;
2061 }
2062
2063 // Helper method to format intent context
2064 private function format_intent_context($context) {
2065 $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
2066
2067 switch ($context['intent']) {
2068 case 'add_to_cart':
2069 if ($context['status'] === 'success') {
2070 $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
2071 $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
2072 $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
2073 $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
2074 $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
2075 } else {
2076 $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
2077 $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
2078 switch ($context['reason']) {
2079 case 'woocommerce_not_available':
2080 $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
2081 break;
2082 case 'no_product_context':
2083 $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
2084 break;
2085 case 'product_not_found':
2086 $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
2087 break;
2088 case 'add_to_cart_failed':
2089 $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
2090 break;
2091 }
2092 }
2093 break;
2094 }
2095
2096 return $context_string;
2097 }
2098
2099
2100 //very good
2101 private function add_email_to_loops($email) {
2102 // Sanitize the email
2103 $email = sanitize_email($email);
2104
2105 // Retrieve and sanitize options
2106 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
2107 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
2108
2109 // Check for missing API key or mailing list ID
2110 if (empty($api_key) || empty($mailing_list_id)) {
2111 //error_log(esc_html__('Loops API key or mailing list ID is missing.', 'mxchat'));
2112 return;
2113 }
2114
2115 $data = array(
2116 'email' => $email,
2117 'subscribed' => true,
2118 'source' => __('MxChat AI Chatbot', 'mxchat'),
2119 'mailingLists' => array($mailing_list_id => true),
2120 );
2121
2122 $url = 'https://app.loops.so/api/v1/contacts/create';
2123 $args = array(
2124 'body' => wp_json_encode($data),
2125 'headers' => array(
2126 'Authorization' => 'Bearer ' . $api_key,
2127 'Content-Type' => 'application/json',
2128 ),
2129 'method' => 'POST',
2130 'timeout' => 45,
2131 );
2132
2133 $response = wp_remote_post($url, $args);
2134
2135 // Handle errors in the API request
2136 if (is_wp_error($response)) {
2137 //error_log(esc_html__('Error adding email to Loops: ', 'mxchat') . $response->get_error_message());
2138 return;
2139 }
2140
2141 // Check for non-200 HTTP responses
2142 $response_code = wp_remote_retrieve_response_code($response);
2143 if ($response_code != 200) {
2144 $response_body = wp_remote_retrieve_body($response);
2145 //error_log(esc_html__('Loops API responded with code ', 'mxchat') . $response_code . ': ' . $response_body);
2146 }
2147 }
2148
2149 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
2150 // Get the maximum number of pages allowed from admin settings
2151 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2152
2153 // Retrieve options for dynamic texts
2154 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
2155 $success_text = $this->options['pdf_intent_success_text'] ?? __("I've processed the PDF. What questions do you have about it?", 'mxchat');
2156 $error_text = $this->options['pdf_intent_error_text'] ?? __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
2157
2158 // Check for explicit request for new PDF
2159 $new_pdf_requested = stripos($message, 'new') !== false ||
2160 stripos($message, 'another') !== false ||
2161 stripos($message, 'different') !== false;
2162
2163 // If user mentions adding/reading a PDF, set waiting flag
2164 if (stripos($message, 'pdf') !== false ||
2165 stripos($message, 'document') !== false ||
2166 stripos($message, 'read') !== false) {
2167 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
2168 $this->fallbackResponse['text'] = $trigger_text;
2169 return;
2170 }
2171
2172 // If we're waiting for a URL or user requested new PDF
2173 if ($new_pdf_requested || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
2174 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
2175 // Process URL... (rest of your existing URL processing code)
2176 } else {
2177 $this->fallbackResponse['text'] = $trigger_text;
2178 }
2179 return;
2180 }
2181
2182 // Default to proceeding with conversation if no specific PDF action is needed
2183 $this->fallbackResponse['text'] = '';
2184 }
2185
2186
2187 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2188 $upload_dir = wp_upload_dir();
2189 $temp_file = null;
2190
2191 try {
2192 // Handle URL vs local file
2193 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2194 // Validate and download the file from URL
2195 $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
2196 $response = wp_remote_get($pdf_source, ['timeout' => 60]);
2197
2198 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
2199 //error_log(esc_html__("Failed to download PDF. Error: ", 'mxchat') . print_r($response, true));
2200 return false;
2201 }
2202
2203 file_put_contents($temp_file, wp_remote_retrieve_body($response));
2204
2205 // Validate that the downloaded file is a PDF
2206 $mime_type = mime_content_type($temp_file);
2207 if ($mime_type !== 'application/pdf') {
2208 //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
2209 unlink($temp_file);
2210 return false;
2211 }
2212 } else {
2213 // For local files, use the provided path directly
2214 $temp_file = $pdf_source;
2215 }
2216
2217 // Parse and process the PDF
2218 $parser = new \Smalot\PdfParser\Parser();
2219 $pdf = $parser->parseFile($temp_file);
2220 $pages = $pdf->getPages();
2221
2222 if (count($pages) > $max_pages) {
2223 //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
2224 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2225 unlink($temp_file);
2226 }
2227 return esc_html__('too_many_pages', 'mxchat');
2228 }
2229
2230 $embeddings = [];
2231 foreach ($pages as $page_number => $page) {
2232 $text = $page->getText();
2233
2234 // Ensure text is non-empty before generating embeddings
2235 if (empty(trim($text))) {
2236 //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
2237 continue;
2238 }
2239
2240 $embedding = $this->mxchat_generate_embedding(
2241 esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2242 $this->options['api_key']
2243 );
2244
2245 if ($embedding) {
2246 $embeddings[] = [
2247 'page_number' => $page_number + 1,
2248 'embedding' => $embedding,
2249 'text' => $text,
2250 ];
2251 } else {
2252 //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
2253 }
2254 }
2255
2256 // Clean up downloaded file if it was from URL
2257 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2258 unlink($temp_file);
2259 }
2260
2261 return $embeddings;
2262
2263 } catch (\Exception $e) {
2264 // //error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
2265
2266 // Cleanup in case of exception
2267 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2268 unlink($temp_file);
2269 }
2270
2271 return false;
2272 }
2273 }
2274 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
2275 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
2276
2277 $most_relevant = null;
2278 $highest_similarity = -INF;
2279
2280 foreach ($embeddings as $page_data) {
2281 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
2282
2283 if ($similarity > $highest_similarity) {
2284 $highest_similarity = $similarity;
2285 $most_relevant = $page_data['page_number'];
2286 }
2287 }
2288
2289 if (!is_null($most_relevant)) {
2290 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
2291 return array_filter($embeddings, function ($page) use ($page_numbers) {
2292 return in_array($page['page_number'], $page_numbers);
2293 });
2294 }
2295
2296 return [];
2297 }
2298 // Add this to your class
2299 public function handle_pdf_upload() {
2300 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2301
2302 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
2303 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
2304 return;
2305 }
2306
2307 $file = $_FILES['pdf_file'];
2308 $session_id = sanitize_text_field($_POST['session_id']);
2309 $original_filename = sanitize_text_field($file['name']);
2310
2311 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
2312 if ($file_type['type'] !== 'application/pdf') {
2313 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
2314 return;
2315 }
2316
2317 $upload_dir = wp_upload_dir();
2318 $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
2319 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
2320
2321 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
2322 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
2323 return;
2324 }
2325
2326 $this->clear_pdf_transients($session_id);
2327
2328 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2329 $embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
2330
2331 if ($embeddings === 'too_many_pages') {
2332 unlink($pdf_path);
2333 $error_message = sprintf(
2334 $this->options['pdf_intent_error_text'] ??
2335 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
2336 $max_pages
2337 );
2338 wp_send_json_error($error_message);
2339 return;
2340 }
2341
2342 if ($embeddings === false || empty($embeddings)) {
2343 unlink($pdf_path);
2344 $error_message = $this->options['pdf_intent_error_text'] ??
2345 esc_html__('The uploaded PDF appears to be empty or contains unsupported content.', 'mxchat');
2346 wp_send_json_error($error_message);
2347 return;
2348 }
2349
2350 if (!empty($embeddings)) {
2351 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
2352 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
2353 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
2354 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
2355
2356 $success_message = $this->options['pdf_intent_success_text'] ??
2357 esc_html__("I've processed the PDF. What questions do you have about it?", 'mxchat');
2358
2359 wp_send_json_success([
2360 'message' => $success_message,
2361 'filename' => $original_filename
2362 ]);
2363 return;
2364 }
2365
2366 unlink($pdf_path);
2367 $error_message = $this->options['pdf_intent_error_text'] ??
2368 esc_html__('Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.', 'mxchat');
2369 wp_send_json_error($error_message);
2370 return;
2371 }
2372 public function handle_pdf_remove() {
2373 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2374
2375 if (empty($_POST['session_id'])) {
2376 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
2377 wp_die();
2378 }
2379
2380 $session_id = sanitize_text_field($_POST['session_id']);
2381 $pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
2382
2383 if ($pdf_path && file_exists($pdf_path)) {
2384 unlink($pdf_path);
2385 }
2386
2387 $this->clear_pdf_transients($session_id);
2388
2389 wp_send_json_success([
2390 'message' => esc_html__('PDF removed successfully.', 'mxchat')
2391 ]);
2392 wp_die();
2393 }
2394
2395
2396
2397
2398 function mxchat_fetch_new_messages() {
2399 $session_id = sanitize_text_field($_POST['session_id']);
2400 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2401 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
2402 $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2403
2404 if (empty($session_id)) {
2405 //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
2406 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
2407 wp_die();
2408 }
2409
2410 $history = get_option("mxchat_history_{$session_id}", []);
2411
2412 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2413 // If persistence is enabled, show all new messages
2414 if ($persistence_enabled) {
2415 return !empty($message['id']) &&
2416 strcmp($message['id'], $last_seen_id) > 0 &&
2417 $message['role'] === 'agent';
2418 }
2419
2420 // If persistence is disabled, only show messages after initial timestamp
2421 return !empty($message['id']) &&
2422 $message['role'] === 'agent' &&
2423 $message['timestamp'] > $initial_timestamp;
2424 });
2425
2426 //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
2427
2428 wp_send_json_success([
2429 'new_messages' => array_values($new_messages)
2430 ]);
2431 wp_die();
2432 }
2433
2434
2435 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2436 // First check if live agents are available
2437 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2438 if ($live_agent_available !== 'on') {
2439 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2440 $this->fallbackResponse = [
2441 'text' => $away_message,
2442 'html' => '',
2443 'images' => [],
2444 'chat_mode' => 'ai'
2445 ];
2446 wp_send_json([
2447 'text' => $away_message,
2448 'html' => '',
2449 'chat_mode' => 'ai',
2450 'session_id' => $session_id
2451 ]);
2452 wp_die();
2453 }
2454
2455 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2456 if (empty($slack_webhook_url)) {
2457 return false;
2458 }
2459
2460 // Get recent chat history (last 5 messages)
2461 $history = get_option("mxchat_history_{$session_id}", []);
2462 $recent_history = array_slice($history, -5); // Get last 5 messages
2463
2464 // Format conversation history
2465 $conversation_context = "";
2466 if (!empty($recent_history)) {
2467 $conversation_context = "*Recent Conversation:*\n";
2468 foreach ($recent_history as $hist_message) {
2469 $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
2470 $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
2471 }
2472 $conversation_context .= "\n";
2473 }
2474
2475 update_option("mxchat_mode_{$session_id}", 'agent');
2476
2477 $webhook_data = [
2478 'blocks' => [
2479 [
2480 'type' => 'header',
2481 'text' => [
2482 'type' => 'plain_text',
2483 'text' => '🔔 New Live Agent Request',
2484 'emoji' => true
2485 ]
2486 ],
2487 [
2488 'type' => 'section',
2489 'fields' => [
2490 [
2491 'type' => 'mrkdwn',
2492 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2493 ],
2494 [
2495 'type' => 'mrkdwn',
2496 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2497 ]
2498 ]
2499 ]
2500 ]
2501 ];
2502
2503 // Add conversation history if exists
2504 if (!empty($conversation_context)) {
2505 $webhook_data['blocks'][] = [
2506 'type' => 'section',
2507 'text' => [
2508 'type' => 'mrkdwn',
2509 'text' => $conversation_context
2510 ]
2511 ];
2512 }
2513
2514 // Add the current message
2515 $webhook_data['blocks'][] = [
2516 'type' => 'section',
2517 'text' => [
2518 'type' => 'mrkdwn',
2519 'text' => sprintf('*Current Message:*\n%s', $message)
2520 ]
2521 ];
2522
2523 // Add the reply button
2524 $webhook_data['blocks'][] = [
2525 'type' => 'actions',
2526 'elements' => [
2527 [
2528 'type' => 'button',
2529 'text' => [
2530 'type' => 'plain_text',
2531 'text' => '✍️ Reply',
2532 'emoji' => true
2533 ],
2534 'value' => $session_id,
2535 'action_id' => 'reply_to_user',
2536 'style' => 'primary'
2537 ]
2538 ]
2539 ];
2540
2541 $response = wp_remote_post($slack_webhook_url, [
2542 'body' => json_encode($webhook_data),
2543 'headers' => [
2544 'Content-Type' => 'application/json',
2545 ],
2546 ]);
2547
2548 if (is_wp_error($response)) {
2549 return false;
2550 }
2551
2552 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2553 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2554
2555 $this->fallbackResponse = [
2556 'text' => $success_message,
2557 'html' => '',
2558 'images' => [],
2559 'chat_mode' => 'agent'
2560 ];
2561
2562 wp_send_json([
2563 'success' => true,
2564 'text' => $success_message,
2565 'html' => '',
2566 'chat_mode' => 'agent',
2567 'session_id' => $session_id,
2568 'fallbackResponse' => $this->fallbackResponse
2569 ]);
2570 wp_die();
2571 }
2572 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2573 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2574
2575 if (empty($slack_webhook_url)) {
2576 //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
2577 return false;
2578 }
2579
2580 $webhook_data = [
2581 'blocks' => [
2582 [
2583 'type' => 'header',
2584 'text' => [
2585 'type' => 'plain_text',
2586 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2587 'emoji' => true
2588 ]
2589 ],
2590 [
2591 'type' => 'section',
2592 'fields' => [
2593 [
2594 'type' => 'mrkdwn',
2595 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2596 ],
2597 [
2598 'type' => 'mrkdwn',
2599 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2600 ]
2601 ]
2602 ],
2603 [
2604 'type' => 'section',
2605 'text' => [
2606 'type' => 'mrkdwn',
2607 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2608 ]
2609 ],
2610 [
2611 'type' => 'actions',
2612 'elements' => [
2613 [
2614 'type' => 'button',
2615 'text' => [
2616 'type' => 'plain_text',
2617 'text' => esc_html__('✍️ Reply', 'mxchat'),
2618 'emoji' => true
2619 ],
2620 'value' => $session_id,
2621 'action_id' => 'reply_to_user',
2622 'style' => 'primary'
2623 ]
2624 ]
2625 ]
2626 ]
2627 ];
2628
2629 $response = wp_remote_post($slack_webhook_url, [
2630 'body' => json_encode($webhook_data),
2631 'headers' => [
2632 'Content-Type' => 'application/json',
2633 ],
2634 ]);
2635
2636 if (is_wp_error($response)) {
2637 //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2638 return false;
2639 }
2640
2641 //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2642 return true;
2643 }
2644 public function handle_slack_interaction(WP_REST_Request $request) {
2645 //error_log('Received Slack interaction');
2646
2647 $payload = json_decode($request->get_param('payload'), true);
2648 //error_log('Payload: ' . print_r($payload, true));
2649
2650 // Handle button click
2651 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
2652 $session_id = $payload['actions'][0]['value'];
2653 $trigger_id = $payload['trigger_id'];
2654
2655 // Get Bot Token from settings
2656 $slack_token = $this->options['live_agent_bot_token'] ?? '';
2657
2658 if (empty($slack_token)) {
2659 //error_log('Slack Bot Token not configured');
2660 return new WP_REST_Response(['error' => esc_html__('Bot token not configured', 'mxchat')], 400);
2661 }
2662 $response = wp_remote_post('https://slack.com/api/views.open', [
2663 'headers' => [
2664 'Content-Type' => 'application/json',
2665 'Authorization' => 'Bearer ' . $slack_token
2666 ],
2667 'body' => json_encode([
2668 'trigger_id' => $trigger_id,
2669 'view' => [
2670 'type' => 'modal',
2671 'callback_id' => 'reply_modal',
2672 'title' => [
2673 'type' => 'plain_text',
2674 'text' => __('Reply to User', 'mxchat')
2675 ],
2676 'submit' => [
2677 'type' => 'plain_text',
2678 'text' => __('Send', 'mxchat')
2679 ],
2680 'close' => [
2681 'type' => 'plain_text',
2682 'text' => __('Cancel', 'mxchat')
2683 ],
2684 'blocks' => [
2685 [
2686 'type' => 'input',
2687 'block_id' => 'reply_block',
2688 'label' => [
2689 'type' => 'plain_text',
2690 'text' => sprintf(__('Reply to session: %s', 'mxchat'), $session_id)
2691 ],
2692 'element' => [
2693 'type' => 'plain_text_input',
2694 'action_id' => 'message',
2695 'multiline' => true,
2696 'placeholder' => [
2697 'type' => 'plain_text',
2698 'text' => __('Type your message here...', 'mxchat')
2699 ]
2700 ]
2701 ]
2702 ],
2703 'private_metadata' => $session_id
2704 ]
2705 ])
2706 ]);
2707
2708 //error_log('Views.open response: ' . print_r($response, true));
2709
2710 // Return immediate acknowledgment
2711 return new WP_REST_Response(['ok' => true]);
2712 }
2713
2714 // Handle modal submission
2715 // Handle modal submission
2716 if ($payload['type'] === 'view_submission') {
2717 $session_id = $payload['view']['private_metadata'];
2718 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
2719
2720 // Save the message (keep the message_id but don't include in response)
2721 $this->mxchat_save_chat_message($session_id, 'agent', $message);
2722
2723 // Keep the original response format for Slack
2724 return new WP_REST_Response([
2725 'response_action' => 'clear'
2726 ]);
2727 }
2728
2729 // Default acknowledgment
2730 return new WP_REST_Response(['ok' => true]);
2731 }
2732
2733 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2734 //error_log('Received agent response request');
2735 //error_log('Request data: ' . print_r($request->get_params(), true));
2736 // //error_log('Raw body: ' . file_get_contents('php://input'));
2737
2738 // Get the data from Slack's slash command format
2739 $command_text = $request->get_param('text');
2740 // //error_log('Command text: ' . $command_text);
2741
2742 if (empty($command_text)) {
2743 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2744 return new WP_REST_Response([
2745 'error' => esc_html__('Command text is required. Format: /reply session_id message', 'mxchat')
2746 ], 400);
2747 }
2748
2749 // Split the command text into session_id and message
2750 $parts = explode(' ', $command_text, 2);
2751 if (count($parts) !== 2) {
2752 //error_log('Agent response error: Invalid command format');
2753 return new WP_REST_Response([
2754 'error' => esc_html__('Invalid format. Use: /reply session_id message', 'mxchat')
2755 ], 400);
2756 }
2757
2758 $session_id = sanitize_text_field($parts[0]);
2759 $message = sanitize_text_field($parts[1]);
2760
2761 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
2762
2763 // Save the message
2764 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2765
2766 if (!$message_id) {
2767 // //error_log('Failed to save agent message');
2768 return new WP_REST_Response([
2769 'error' => esc_html__('Failed to save message', 'mxchat')
2770 ], 500);
2771 }
2772
2773 // Return success response in Slack's expected format
2774 return new WP_REST_Response([
2775 'response_type' => 'in_channel',
2776 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2777 ], 200);
2778 }
2779
2780
2781 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2782 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2783
2784 // Just update mode to AI
2785 update_option("mxchat_mode_{$session_id}", 'ai');
2786
2787 // Initialize states
2788 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2789 $this->productCardHtml = '';
2790
2791 // Set the response message
2792 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2793
2794 return true; // Intent was handled
2795 }
2796
2797
2798
2799
2800 // For the word upload handler
2801 public function mxchat_handle_word_upload() {
2802 // Delegate to word handler
2803 $this->word_handler->mxchat_handle_word_upload();
2804 }
2805
2806 // For the word removal handler
2807 public function mxchat_handle_word_remove() {
2808 // Delegate to word handler
2809 $this->word_handler->mxchat_handle_word_remove();
2810 }
2811
2812 // For the word status check
2813 public function mxchat_check_word_status() {
2814 // Delegate to word handler
2815 $this->word_handler->mxchat_check_word_status();
2816 }
2817
2818
2819 private function mxchat_get_user_identifier() {
2820 return MxChat_User::mxchat_get_user_identifier();
2821 }
2822
2823 private function mxchat_generate_embedding($text, $api_key) {
2824 try {
2825 // Get options and selected model
2826 $options = get_option('mxchat_options');
2827 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2828
2829 // Determine endpoint and API key based on model
2830 if (strpos($selected_model, 'voyage') === 0) {
2831 $endpoint = 'https://api.voyageai.com/v1/embeddings';
2832 $api_key = $options['voyage_api_key'] ?? '';
2833
2834 // Check if Voyage API key is missing
2835 if (empty($api_key)) {
2836 //error_log('Voyage API key is missing');
2837 return [
2838 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
2839 'error_code' => 'missing_voyage_api_key'
2840 ];
2841 }
2842 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2843 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2844 $api_key = $options['gemini_api_key'] ?? '';
2845
2846 // Check if Gemini API key is missing
2847 if (empty($api_key)) {
2848 //error_log('Gemini API key is missing');
2849 return [
2850 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
2851 'error_code' => 'missing_gemini_api_key'
2852 ];
2853 }
2854 } else {
2855 $endpoint = 'https://api.openai.com/v1/embeddings';
2856 // Use the passed API key for OpenAI
2857
2858 // Check if OpenAI API key is missing
2859 if (empty($api_key)) {
2860 //error_log('OpenAI API key is missing');
2861 return [
2862 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
2863 'error_code' => 'missing_openai_api_key'
2864 ];
2865 }
2866 }
2867
2868 // Check if text is empty
2869 if (empty($text)) {
2870 //error_log('Empty text provided for embedding generation');
2871 return [
2872 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
2873 'error_code' => 'empty_embedding_text'
2874 ];
2875 }
2876
2877 // Prepare request body based on provider
2878 if (strpos($selected_model, 'gemini-embedding') === 0) {
2879 // Gemini API format
2880 $request_body = [
2881 'model' => 'models/' . $selected_model,
2882 'content' => [
2883 'parts' => [
2884 ['text' => $text]
2885 ]
2886 ],
2887 'outputDimensionality' => 1536
2888 ];
2889
2890 // Prepare headers for Gemini (API key as query parameter)
2891 $endpoint .= '?key=' . $api_key;
2892 $headers = [
2893 'Content-Type' => 'application/json'
2894 ];
2895 } else {
2896 // OpenAI/Voyage API format
2897 $request_body = [
2898 'input' => $text,
2899 'model' => $selected_model
2900 ];
2901
2902 // Add output_dimension for voyage-3-large
2903 if ($selected_model === 'voyage-3-large') {
2904 $request_body['output_dimension'] = 2048;
2905 }
2906
2907 // Prepare headers for OpenAI/Voyage
2908 $headers = [
2909 'Content-Type' => 'application/json',
2910 'Authorization' => 'Bearer ' . $api_key
2911 ];
2912 }
2913
2914 // Prepare request arguments
2915 $args = [
2916 'body' => wp_json_encode($request_body),
2917 'headers' => $headers,
2918 'timeout' => 60,
2919 'redirection' => 5,
2920 'blocking' => true,
2921 'httpversion' => '1.0',
2922 'sslverify' => true,
2923 ];
2924
2925 // Make the request
2926 $response = wp_remote_post($endpoint, $args);
2927
2928 // Handle WordPress errors
2929 if (is_wp_error($response)) {
2930 $error_message = $response->get_error_message();
2931 //error_log('Embedding Generation Error: ' . $error_message);
2932 return [
2933 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
2934 'error_code' => 'embedding_connection_error'
2935 ];
2936 }
2937
2938 // Check HTTP status code
2939 $status_code = wp_remote_retrieve_response_code($response);
2940 if ($status_code !== 200) {
2941 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2942
2943 $error_message = isset($response_body['error']['message'])
2944 ? $response_body['error']['message']
2945 : 'HTTP Error ' . $status_code;
2946
2947 $error_type = isset($response_body['error']['type'])
2948 ? $response_body['error']['type']
2949 : 'unknown';
2950
2951 //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
2952
2953 // Handle specific error types
2954 switch ($error_type) {
2955 case 'invalid_request_error':
2956 if (strpos($error_message, 'API key') !== false) {
2957 return [
2958 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
2959 'error_code' => 'embedding_invalid_api_key'
2960 ];
2961 }
2962 break;
2963
2964 case 'authentication_error':
2965 return [
2966 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
2967 'error_code' => 'embedding_auth_error'
2968 ];
2969
2970 case 'rate_limit_exceeded':
2971 return [
2972 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
2973 'error_code' => 'embedding_rate_limit'
2974 ];
2975
2976 case 'quota_exceeded':
2977 return [
2978 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
2979 'error_code' => 'embedding_quota_exceeded'
2980 ];
2981 }
2982
2983 // Generic error fallback
2984 return [
2985 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
2986 'error_code' => 'embedding_api_error',
2987 'status_code' => $status_code
2988 ];
2989 }
2990
2991 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2992
2993 // Handle different response formats based on provider
2994 if (strpos($selected_model, 'gemini-embedding') === 0) {
2995 // Gemini API response format
2996 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
2997 return $response_body['embedding']['values'];
2998 } else {
2999 //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3000 return [
3001 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3002 'error_code' => 'invalid_gemini_embedding_response'
3003 ];
3004 }
3005 } else {
3006 // OpenAI/Voyage API response format
3007 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3008 return $response_body['data'][0]['embedding'];
3009 } else {
3010 //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3011 return [
3012 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3013 'error_code' => 'invalid_embedding_response'
3014 ];
3015 }
3016 }
3017 } catch (Exception $e) {
3018 //error_log('Embedding Exception: ' . $e->getMessage());
3019 return [
3020 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3021 'error_code' => 'embedding_exception'
3022 ];
3023 }
3024 }
3025 private function mxchat_find_relevant_content($user_embedding) {
3026 //error_log('MXChat Vector Search: Starting content search...');
3027
3028 // Retrieve the add-on settings from the database.
3029 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3030
3031 // Determine whether Pinecone is enabled.
3032 // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
3033 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
3034
3035 //error_log('Pinecone enabled flag: ' . $use_pinecone);
3036
3037 if ($use_pinecone === 1) {
3038 //error_log('MXChat Vector Search: Using Pinecone database');
3039 return $this->find_relevant_content_pinecone($user_embedding);
3040 } else {
3041 //error_log('MXChat Vector Search: Using WordPress database');
3042 return $this->find_relevant_content_wordpress($user_embedding);
3043 }
3044 }
3045 private function find_relevant_content_wordpress($user_embedding) {
3046 global $wpdb;
3047 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3048 $cache_key = 'mxchat_system_prompt_embeddings';
3049 $batch_size = 500;
3050
3051 // Retrieve embeddings from cache or database
3052 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3053 if ($embeddings === false) {
3054 // Cache miss - load embeddings from database
3055 $embeddings = [];
3056 $offset = 0;
3057
3058 // Load in batches and build cache
3059 do {
3060 $query = $wpdb->prepare(
3061 "SELECT id, embedding_vector
3062 FROM {$system_prompt_table}
3063 LIMIT %d OFFSET %d",
3064 $batch_size,
3065 $offset
3066 );
3067
3068 $batch = $wpdb->get_results($query);
3069 if (empty($batch)) {
3070 break;
3071 }
3072
3073 $embeddings = array_merge($embeddings, $batch);
3074 $offset += $batch_size;
3075 unset($batch); // Free memory
3076 } while (true);
3077
3078 if (empty($embeddings)) {
3079 return ''; // Return empty string if no embeddings found
3080 }
3081
3082 // Cache embeddings for future use
3083 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3084 }
3085
3086 // Get configuration options
3087 $main_options = get_option('mxchat_options', []);
3088
3089 // Get base similarity threshold (default 75%)
3090 $similarity_threshold = isset($main_options['similarity_threshold'])
3091 ? ((int) $main_options['similarity_threshold']) / 100
3092 : 0.75;
3093
3094 // Calculate similarities and build results array
3095 $relevant_results = [];
3096 foreach ($embeddings as $embedding) {
3097 $database_embedding = $embedding->embedding_vector
3098 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3099 : null;
3100
3101 if (is_array($database_embedding) && is_array($user_embedding)) {
3102 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3103
3104 // Only consider results above threshold
3105 if ($similarity >= $similarity_threshold) {
3106 $relevant_results[] = [
3107 'id' => $embedding->id,
3108 'similarity' => $similarity
3109 ];
3110 }
3111 }
3112
3113 unset($database_embedding); // Free memory
3114 }
3115
3116 // Sort by similarity (highest first)
3117 usort($relevant_results, function ($a, $b) {
3118 return $b['similarity'] <=> $a['similarity'];
3119 });
3120
3121 // Get top 5 results (standard approach)
3122 $top_results = array_slice($relevant_results, 0, 5);
3123
3124 // Initialize final content
3125 $content = '';
3126
3127 // Track document IDs to avoid duplicates
3128 $added_document_ids = [];
3129
3130 // Fetch and format content for each selected result
3131 foreach ($top_results as $index => $result) {
3132 // Skip if already added (for PDF surrounding pages)
3133 if (in_array($result['id'], $added_document_ids)) {
3134 continue;
3135 }
3136
3137 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3138 $added_document_ids[] = $result['id'];
3139
3140 // Add content with minimal markup
3141 $content .= "## Reference " . ($index + 1) . " ##\n";
3142 $content .= $chunk_content . "\n\n";
3143
3144 // Check if content is PDF-related
3145 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3146 // Get surrounding pages
3147 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3148 "SELECT id, article_content FROM {$system_prompt_table}
3149 WHERE id IN (
3150 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3151 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3152 )",
3153 $result['id'],
3154 $result['id']
3155 ));
3156
3157 // Add previous page content if it exists
3158 if (!empty($surrounding_content[0])) {
3159 $content .= "## Related Content ##\n";
3160 $content .= $surrounding_content[0]->article_content . "\n\n";
3161 $added_document_ids[] = $surrounding_content[0]->id;
3162 }
3163
3164 // Add next page content if it exists
3165 if (!empty($surrounding_content[1])) {
3166 $content .= "## Related Content ##\n";
3167 $content .= $surrounding_content[1]->article_content . "\n\n";
3168 $added_document_ids[] = $surrounding_content[1]->id;
3169 }
3170 }
3171 }
3172
3173 // Add response guidelines that encourage natural conversation but strict adherence to facts
3174 if (empty($top_results)) {
3175 $content = "No reference information was found for this query.\n\n" .
3176 "Respond naturally but inform the user that you don't have specific information " .
3177 "about their query and suggest they check the documentation, speak to a live agent, " .
3178 "or create a support ticket.";
3179 } else {
3180 $content .= "\n## Response Guidelines ##\n" .
3181 "Answer naturally as a helpful product representative would, without phrases like " .
3182 "'based on my knowledge base' or 'according to the information.' Only use facts " .
3183 "contained in the references above. If essential information is missing, politely " .
3184 "guide the user to documentation or support.";
3185 }
3186
3187 return trim($content);
3188 }
3189 private function find_relevant_content_pinecone($user_embedding) {
3190 $options = get_option('mxchat_pinecone_addon_options', array());
3191 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3192 $host = $options['mxchat_pinecone_host'] ?? '';
3193
3194 if (empty($host) || empty($api_key)) {
3195 return '';
3196 }
3197
3198 // Get the similarity threshold from the main options
3199 $main_options = get_option('mxchat_options', []);
3200 $similarity_threshold = isset($main_options['similarity_threshold'])
3201 ? ((int) $main_options['similarity_threshold']) / 100
3202 : 0.75; // Default to 75%
3203
3204 // Prepare the query request for Pinecone
3205 $api_endpoint = "https://{$host}/query";
3206
3207 $request_body = array(
3208 'vector' => $user_embedding,
3209 'topK' => 8, // Request more to ensure we have enough after filtering
3210 'includeMetadata' => true,
3211 'includeValues' => true
3212 );
3213
3214 $response = wp_remote_post($api_endpoint, array(
3215 'headers' => array(
3216 'Api-Key' => $api_key,
3217 'accept' => 'application/json',
3218 'content-type' => 'application/json'
3219 ),
3220 'body' => wp_json_encode($request_body),
3221 'timeout' => 30
3222 ));
3223
3224 if (is_wp_error($response)) {
3225 return '';
3226 }
3227
3228 $response_code = wp_remote_retrieve_response_code($response);
3229 if ($response_code !== 200) {
3230 return '';
3231 }
3232
3233 $results = json_decode(wp_remote_retrieve_body($response), true);
3234 if (empty($results['matches'])) {
3235 return '';
3236 }
3237
3238 // Initialize the final content
3239 $content = '';
3240 $matches_used = 0;
3241
3242 // Process each match
3243 foreach ($results['matches'] as $index => $match) {
3244 // Skip if similarity is below threshold
3245 if ($match['score'] < $similarity_threshold) {
3246 continue;
3247 }
3248
3249 // Limit to top 5 matches above threshold
3250 if ($matches_used >= 5) {
3251 break;
3252 }
3253
3254 if (!empty($match['metadata']['text'])) {
3255 // Add content with minimal markup
3256 $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3257 $content .= $match['metadata']['text'] . "\n\n";
3258
3259 // Add source URL if available
3260 if (!empty($match['metadata']['source_url'])) {
3261 $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3262 }
3263
3264 $matches_used++;
3265 }
3266 }
3267
3268 // Add response guidelines that encourage natural conversation but strict adherence to facts
3269 if ($matches_used === 0) {
3270 $content = "No reference information was found for this query.\n\n" .
3271 "Respond naturally but inform the user that you don't have specific information " .
3272 "about their query and suggest they check the documentation, speak to a live agent, " .
3273 "or create a support ticket.";
3274 } else {
3275 $content .= "\n## Response Guidelines ##\n" .
3276 "Answer naturally as a helpful product representative would, without phrases like " .
3277 "'based on my knowledge base' or 'according to the information.' Only use facts " .
3278 "contained in the references above. If essential information is missing, politely " .
3279 "guide the user to documentation or support.";
3280 }
3281
3282 return trim($content);
3283 }
3284
3285 private function mxchat_find_relevant_products($user_embedding) {
3286 //error_log('MXChat Vector Search: Starting product search...');
3287
3288 // Retrieve the add-on settings from the database
3289 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3290
3291 // Determine whether Pinecone is enabled
3292 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
3293
3294 //error_log('Pinecone enabled flag: ' . $use_pinecone);
3295
3296 if ($use_pinecone === 1) {
3297 //error_log('MXChat Vector Search: Using Pinecone database for products');
3298 return $this->find_relevant_products_pinecone($user_embedding);
3299 } else {
3300 //error_log('MXChat Vector Search: Using WordPress database for products');
3301 return $this->find_relevant_products_wordpress($user_embedding);
3302 }
3303 }
3304 private function find_relevant_products_wordpress($user_embedding) {
3305 global $wpdb;
3306 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3307 $cache_key = 'mxchat_system_prompt_embeddings';
3308 $batch_size = 500;
3309
3310 // Original WordPress database search logic
3311 // [Previous implementation remains the same]
3312 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3313 if ($embeddings === false) {
3314 $embeddings = [];
3315 $offset = 0;
3316
3317 do {
3318 $query = $wpdb->prepare(
3319 "SELECT id, embedding_vector
3320 FROM {$system_prompt_table}
3321 LIMIT %d OFFSET %d",
3322 $batch_size,
3323 $offset
3324 );
3325
3326 $batch = $wpdb->get_results($query);
3327 if (empty($batch)) {
3328 break;
3329 }
3330
3331 $embeddings = array_merge($embeddings, $batch);
3332 $offset += $batch_size;
3333
3334 unset($batch);
3335
3336 } while (true);
3337
3338 if (empty($embeddings)) {
3339 return '';
3340 }
3341 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3342 }
3343
3344 $relevant_results = [];
3345 foreach ($embeddings as $embedding) {
3346 $database_embedding = $embedding->embedding_vector
3347 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3348 : null;
3349 if (is_array($database_embedding) && is_array($user_embedding)) {
3350 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3351 $relevant_results[] = [
3352 'id' => $embedding->id,
3353 'similarity' => $similarity
3354 ];
3355 }
3356 unset($database_embedding);
3357 }
3358
3359 // Use fixed threshold for products
3360 $similarity_threshold = 0.85;
3361
3362 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3363 return $result['similarity'] >= $similarity_threshold;
3364 });
3365 usort($relevant_results, function ($a, $b) {
3366 return $b['similarity'] <=> $a['similarity'];
3367 });
3368
3369 $top_results = array_slice($relevant_results, 0, 5);
3370 $content = '';
3371
3372 foreach ($top_results as $result) {
3373 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3374 $content .= $chunk_content . "\n\n";
3375 }
3376
3377 return trim($content);
3378 }
3379 private function find_relevant_products_pinecone($user_embedding) {
3380 //error_log('Starting Pinecone product search...');
3381
3382 $options = get_option('mxchat_pinecone_addon_options', array());
3383 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3384 $host = $options['mxchat_pinecone_host'] ?? '';
3385
3386 if (empty($host) || empty($api_key)) {
3387 //error_log('Pinecone credentials not properly configured for product search');
3388 return '';
3389 }
3390
3391 $similarity_threshold = 0.85;
3392 $api_endpoint = "https://{$host}/query";
3393
3394 $request_body = array(
3395 'vector' => $user_embedding,
3396 'topK' => 5,
3397 'includeMetadata' => true,
3398 'includeValues' => true,
3399 'filter' => array(
3400 'type' => 'product'
3401 )
3402 );
3403
3404 //error_log('Sending request to Pinecone with body: ' . wp_json_encode($request_body));
3405
3406 $response = wp_remote_post($api_endpoint, array(
3407 'headers' => array(
3408 'Api-Key' => $api_key,
3409 'accept' => 'application/json',
3410 'content-type' => 'application/json'
3411 ),
3412 'body' => wp_json_encode($request_body),
3413 'timeout' => 30
3414 ));
3415
3416 if (is_wp_error($response)) {
3417 //error_log('Pinecone product query error: ' . $response->get_error_message());
3418 return '';
3419 }
3420
3421 $response_code = wp_remote_retrieve_response_code($response);
3422 //error_log('Pinecone response code: ' . $response_code);
3423
3424 if ($response_code !== 200) {
3425 //error_log('Pinecone API error during product search: ' . wp_remote_retrieve_body($response));
3426 return '';
3427 }
3428
3429 $results = json_decode(wp_remote_retrieve_body($response), true);
3430 //error_log('Pinecone raw response: ' . wp_remote_retrieve_body($response));
3431
3432 if (empty($results['matches'])) {
3433 //error_log('No matches found in Pinecone response');
3434 return '';
3435 }
3436
3437 $content = '';
3438 foreach ($results['matches'] as $match) {
3439 if ($match['score'] < $similarity_threshold) {
3440 //error_log("Match below threshold: " . $match['score']);
3441 continue;
3442 }
3443
3444 if (!empty($match['metadata']['text'])) {
3445 $content .= $match['metadata']['text'];
3446 if (!empty($match['metadata']['source_url'])) {
3447 $content .= "\n\nFor more details, check out this product: " . esc_url($match['metadata']['source_url']);
3448 }
3449 $content .= "\n\n";
3450 }
3451 }
3452
3453 return trim($content);
3454 }
3455 private function fetch_content_with_product_links($most_relevant_id) {
3456 global $wpdb;
3457 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3458
3459 // Fetch the article content and associated product URL
3460 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
3461 $result = $wpdb->get_row($query);
3462
3463 if ($result) {
3464 // Append the product link to the content if available
3465 $content = $result->article_content;
3466 if (!empty($result->source_url)) {
3467 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
3468 }
3469 return $content;
3470 }
3471
3472 return null;
3473 }
3474
3475 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history) {
3476 try {
3477 if (!$relevant_content) {
3478 return [
3479 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
3480 'error_code' => 'no_relevant_content'
3481 ];
3482 }
3483
3484 // Ensure conversation_history is an array
3485 if (!is_array($conversation_history)) {
3486 $conversation_history = array();
3487 }
3488
3489 // Get selected model with default fallback
3490 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3491
3492 // Extract model prefix to determine the provider
3493 $model_parts = explode('-', $selected_model);
3494 $provider = strtolower($model_parts[0]);
3495
3496 // Handle model selection based on provider prefix
3497 switch ($provider) {
3498 case 'gemini':
3499 if (empty($gemini_api_key)) {
3500 return [
3501 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3502 'error_code' => 'missing_gemini_api_key'
3503 ];
3504 }
3505 $response = $this->mxchat_generate_response_gemini(
3506 $selected_model,
3507 $gemini_api_key,
3508 $conversation_history,
3509 $relevant_content
3510 );
3511 break;
3512
3513 case 'claude':
3514 if (empty($claude_api_key)) {
3515 return [
3516 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
3517 'error_code' => 'missing_claude_api_key'
3518 ];
3519 }
3520 $response = $this->mxchat_generate_response_claude(
3521 $selected_model,
3522 $claude_api_key,
3523 $conversation_history,
3524 $relevant_content
3525 );
3526 break;
3527
3528 case 'grok':
3529 if (empty($xai_api_key)) {
3530 return [
3531 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
3532 'error_code' => 'missing_xai_api_key'
3533 ];
3534 }
3535 $response = $this->mxchat_generate_response_xai(
3536 $selected_model,
3537 $xai_api_key,
3538 $conversation_history,
3539 $relevant_content
3540 );
3541 break;
3542
3543 case 'deepseek':
3544 if (empty($deepseek_api_key)) {
3545 return [
3546 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
3547 'error_code' => 'missing_deepseek_api_key'
3548 ];
3549 }
3550 $response = $this->mxchat_generate_response_deepseek(
3551 $selected_model,
3552 $deepseek_api_key,
3553 $conversation_history,
3554 $relevant_content
3555 );
3556 break;
3557
3558 case 'gpt':
3559 if (empty($api_key)) {
3560 return [
3561 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3562 'error_code' => 'missing_openai_api_key'
3563 ];
3564 }
3565 $response = $this->mxchat_generate_response_openai(
3566 $selected_model,
3567 $api_key,
3568 $conversation_history,
3569 $relevant_content
3570 );
3571 break;
3572
3573 default:
3574 // Default to OpenAI for custom models or unrecognized prefixes
3575 if (empty($api_key)) {
3576 return [
3577 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3578 'error_code' => 'missing_openai_api_key'
3579 ];
3580 }
3581 $response = $this->mxchat_generate_response_openai(
3582 $selected_model,
3583 $api_key,
3584 $conversation_history,
3585 $relevant_content
3586 );
3587 break;
3588 }
3589
3590 // Check if the response is an error array from the provider-specific function
3591 if (is_array($response) && isset($response['error'])) {
3592 return $response; // Pass through the error
3593 }
3594
3595 return $response;
3596
3597 } catch (Exception $e) {
3598 //error_log('MXChat Error: ' . $e->getMessage());
3599 return [
3600 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
3601 'error_code' => 'system_exception',
3602 'exception_details' => $e->getMessage()
3603 ];
3604 }
3605 }
3606
3607 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3608 try {
3609 // Ensure conversation_history is an array
3610 if (!is_array($conversation_history)) {
3611 $conversation_history = array();
3612 }
3613
3614 // Get system prompt instructions from options
3615 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3616
3617 // Create a new array for the formatted conversation
3618 $formatted_conversation = array();
3619
3620 // Add system message first
3621 $formatted_conversation[] = array(
3622 'role' => 'system',
3623 'content' => $system_prompt_instructions . " " . $relevant_content
3624 );
3625
3626 // Add the rest of the conversation history
3627 foreach ($conversation_history as $message) {
3628 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3629 $role = $message['role'];
3630
3631 // Convert roles to supported format
3632 if ($role === 'bot' || $role === 'agent') {
3633 $role = 'assistant';
3634 }
3635 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3636 $role = 'user';
3637 }
3638
3639 $formatted_conversation[] = array(
3640 'role' => $role,
3641 'content' => $message['content']
3642 );
3643 }
3644 }
3645
3646 $body = json_encode([
3647 'model' => $selected_model,
3648 'messages' => $formatted_conversation,
3649 'temperature' => 0.8,
3650 'stream' => false
3651 ]);
3652
3653 $args = [
3654 'body' => $body,
3655 'headers' => [
3656 'Content-Type' => 'application/json',
3657 'Authorization' => 'Bearer ' . $deepseek_api_key,
3658 ],
3659 'timeout' => 60,
3660 'redirection' => 5,
3661 'blocking' => true,
3662 'httpversion' => '1.0',
3663 'sslverify' => true,
3664 ];
3665
3666 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3667
3668 if (is_wp_error($response)) {
3669 $error_message = $response->get_error_message();
3670 //error_log('DeepSeek API Error: ' . $error_message);
3671 return [
3672 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
3673 'error_code' => 'deepseek_connection_error',
3674 'provider' => 'deepseek'
3675 ];
3676 }
3677
3678 $status_code = wp_remote_retrieve_response_code($response);
3679 if ($status_code !== 200) {
3680 $response_body = wp_remote_retrieve_body($response);
3681 $decoded_response = json_decode($response_body, true);
3682
3683 $error_message = isset($decoded_response['error']['message'])
3684 ? $decoded_response['error']['message']
3685 : 'HTTP Error ' . $status_code;
3686
3687 $error_type = isset($decoded_response['error']['type'])
3688 ? $decoded_response['error']['type']
3689 : 'unknown';
3690
3691 //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
3692
3693 // Handle specific error types
3694 switch ($status_code) {
3695 case 401:
3696 return [
3697 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
3698 'error_code' => 'deepseek_auth_error',
3699 'provider' => 'deepseek'
3700 ];
3701
3702 case 400:
3703 if (strpos($error_message, 'API key') !== false) {
3704 return [
3705 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
3706 'error_code' => 'deepseek_invalid_api_key',
3707 'provider' => 'deepseek'
3708 ];
3709 }
3710 break;
3711
3712 case 429:
3713 if (strpos($error_message, 'quota') !== false) {
3714 return [
3715 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
3716 'error_code' => 'deepseek_quota_exceeded',
3717 'provider' => 'deepseek'
3718 ];
3719 } else {
3720 return [
3721 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
3722 'error_code' => 'deepseek_rate_limit',
3723 'provider' => 'deepseek'
3724 ];
3725 }
3726
3727 case 500:
3728 case 502:
3729 case 503:
3730 case 504:
3731 return [
3732 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
3733 'error_code' => 'deepseek_service_unavailable',
3734 'provider' => 'deepseek'
3735 ];
3736 }
3737
3738 // Generic error fallback
3739 return [
3740 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
3741 'error_code' => 'deepseek_api_error',
3742 'provider' => 'deepseek',
3743 'status_code' => $status_code
3744 ];
3745 }
3746
3747 $response_body = wp_remote_retrieve_body($response);
3748 $decoded_response = json_decode($response_body, true);
3749
3750 if (isset($decoded_response['choices'][0]['message']['content'])) {
3751 return trim($decoded_response['choices'][0]['message']['content']);
3752 } else {
3753 //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3754 return [
3755 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
3756 'error_code' => 'deepseek_response_format_error',
3757 'provider' => 'deepseek'
3758 ];
3759 }
3760 } catch (Exception $e) {
3761 //error_log('DeepSeek Exception: ' . $e->getMessage());
3762 return [
3763 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
3764 'error_code' => 'deepseek_exception',
3765 'provider' => 'deepseek'
3766 ];
3767 }
3768 }
3769
3770 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3771 try {
3772 // Ensure conversation_history is an array
3773 if (!is_array($conversation_history)) {
3774 $conversation_history = array();
3775 }
3776
3777 // Get system prompt instructions from options
3778 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3779
3780 // Create a new array for the formatted conversation
3781 $formatted_conversation = array();
3782
3783 // Add system message first
3784 $formatted_conversation[] = array(
3785 'role' => 'system',
3786 'content' => $system_prompt_instructions . " " . $relevant_content
3787 );
3788
3789 // Add the rest of the conversation history
3790 foreach ($conversation_history as $message) {
3791 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3792 $role = $message['role'];
3793
3794 // Convert roles to supported format
3795 if ($role === 'bot' || $role === 'agent') {
3796 $role = 'assistant';
3797 }
3798 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3799 $role = 'user';
3800 }
3801
3802 $formatted_conversation[] = array(
3803 'role' => $role,
3804 'content' => $message['content']
3805 );
3806 }
3807 }
3808
3809 $body = json_encode([
3810 'model' => $selected_model,
3811 'messages' => $formatted_conversation,
3812 'temperature' => 0.8,
3813 'stream' => false
3814 ]);
3815
3816 $args = [
3817 'body' => $body,
3818 'headers' => [
3819 'Content-Type' => 'application/json',
3820 'Authorization' => 'Bearer ' . $api_key,
3821 ],
3822 'timeout' => 60,
3823 'redirection' => 5,
3824 'blocking' => true,
3825 'httpversion' => '1.0',
3826 'sslverify' => true,
3827 ];
3828
3829 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3830
3831 if (is_wp_error($response)) {
3832 $error_message = $response->get_error_message();
3833 //error_log('OpenAI API Error: ' . $error_message);
3834 return [
3835 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
3836 'error_code' => 'openai_connection_error',
3837 'provider' => 'openai'
3838 ];
3839 }
3840
3841 $status_code = wp_remote_retrieve_response_code($response);
3842 if ($status_code !== 200) {
3843 $response_body = wp_remote_retrieve_body($response);
3844 $decoded_response = json_decode($response_body, true);
3845
3846 $error_message = isset($decoded_response['error']['message'])
3847 ? $decoded_response['error']['message']
3848 : 'HTTP Error ' . $status_code;
3849
3850 $error_type = isset($decoded_response['error']['type'])
3851 ? $decoded_response['error']['type']
3852 : 'unknown';
3853
3854 //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
3855
3856 // Handle specific error types
3857 switch ($error_type) {
3858 case 'invalid_request_error':
3859 if (strpos($error_message, 'API key') !== false) {
3860 return [
3861 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
3862 'error_code' => 'openai_invalid_api_key',
3863 'provider' => 'openai'
3864 ];
3865 }
3866 break;
3867
3868 case 'authentication_error':
3869 return [
3870 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
3871 'error_code' => 'openai_auth_error',
3872 'provider' => 'openai'
3873 ];
3874
3875 case 'rate_limit_exceeded':
3876 return [
3877 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
3878 'error_code' => 'openai_rate_limit',
3879 'provider' => 'openai'
3880 ];
3881
3882 case 'quota_exceeded':
3883 return [
3884 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
3885 'error_code' => 'openai_quota_exceeded',
3886 'provider' => 'openai'
3887 ];
3888 }
3889
3890 // Generic error fallback
3891 return [
3892 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
3893 'error_code' => 'openai_api_error',
3894 'provider' => 'openai',
3895 'status_code' => $status_code
3896 ];
3897 }
3898
3899 $response_body = wp_remote_retrieve_body($response);
3900 $decoded_response = json_decode($response_body, true);
3901
3902 if (isset($decoded_response['choices'][0]['message']['content'])) {
3903 return trim($decoded_response['choices'][0]['message']['content']);
3904 } else {
3905 //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3906 return [
3907 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
3908 'error_code' => 'openai_response_format_error',
3909 'provider' => 'openai'
3910 ];
3911 }
3912 } catch (Exception $e) {
3913 //error_log('OpenAI Exception: ' . $e->getMessage());
3914 return [
3915 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
3916 'error_code' => 'openai_exception',
3917 'provider' => 'openai'
3918 ];
3919 }
3920 }
3921 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3922 try {
3923 // Get system prompt instructions from options
3924 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3925
3926 // Add system prompt to relevant content
3927 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3928
3929 // Prepend system instructions to the conversation history
3930 array_unshift($conversation_history, [
3931 'role' => 'system',
3932 'content' => "Here are your instructions: " . $content_with_instructions
3933 ]);
3934
3935 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3936 foreach ($conversation_history as &$message) {
3937 if ($message['role'] === 'bot') {
3938 $message['role'] = 'assistant';
3939 } elseif ($message['role'] === 'agent') {
3940 // Tag the message as coming from a live agent
3941 $message['role'] = 'assistant';
3942 if (!isset($message['metadata'])) {
3943 $message['metadata'] = ['source' => 'live_agent'];
3944 }
3945 }
3946
3947 // Ensure all roles are valid
3948 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3949 $message['role'] = 'user'; // Default to 'user'
3950 }
3951 }
3952
3953 // Build the request body
3954 $body = json_encode([
3955 'model' => $selected_model,
3956 'messages' => $conversation_history,
3957 'temperature' => 0.8,
3958 'stream' => false
3959 ]);
3960
3961 // Set up the API request
3962 $args = [
3963 'body' => $body,
3964 'headers' => [
3965 'Content-Type' => 'application/json',
3966 'Authorization' => 'Bearer ' . $xai_api_key,
3967 ],
3968 'timeout' => 60,
3969 'redirection' => 5,
3970 'blocking' => true,
3971 'httpversion' => '1.0',
3972 'sslverify' => true,
3973 ];
3974
3975 // Make the API request
3976 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3977
3978 // Process the response
3979 if (is_wp_error($response)) {
3980 $error_message = $response->get_error_message();
3981 //error_log('X.AI API Error: ' . $error_message);
3982 return [
3983 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
3984 'error_code' => 'xai_connection_error',
3985 'provider' => 'xai'
3986 ];
3987 }
3988
3989 $status_code = wp_remote_retrieve_response_code($response);
3990 if ($status_code !== 200) {
3991 $response_body = wp_remote_retrieve_body($response);
3992 $decoded_response = json_decode($response_body, true);
3993
3994 // Log the full response for debugging
3995 //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
3996
3997 // Extract error message from X.AI's specific format
3998 $error_message = '';
3999
4000 // Check for direct error string (as seen in your logs)
4001 if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
4002 $error_message = $decoded_response['error'];
4003 }
4004 // Check for nested error object (OpenAI style)
4005 elseif (isset($decoded_response['error']['message'])) {
4006 $error_message = $decoded_response['error']['message'];
4007 }
4008 // Check for top-level message
4009 elseif (isset($decoded_response['message'])) {
4010 $error_message = $decoded_response['message'];
4011 }
4012 // Fallback
4013 else {
4014 $error_message = 'HTTP Error ' . $status_code;
4015 }
4016
4017 //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4018
4019 // Check for API key errors using string matching
4020 if (stripos($error_message, 'api key') !== false ||
4021 stripos($error_message, 'incorrect api key') !== false ||
4022 stripos($error_message, 'invalid api key') !== false) {
4023 return [
4024 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
4025 'error_code' => 'xai_invalid_api_key',
4026 'provider' => 'xai'
4027 ];
4028 }
4029
4030 // Authentication errors
4031 if ($status_code === 401 || $status_code === 403 ||
4032 stripos($error_message, 'auth') !== false) {
4033 return [
4034 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
4035 'error_code' => 'xai_auth_error',
4036 'provider' => 'xai'
4037 ];
4038 }
4039
4040 // Model errors
4041 if (stripos($error_message, 'model') !== false) {
4042 return [
4043 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
4044 'error_code' => 'xai_invalid_model',
4045 'provider' => 'xai'
4046 ];
4047 }
4048
4049 // Rate limit errors
4050 if ($status_code === 429 ||
4051 stripos($error_message, 'rate') !== false ||
4052 stripos($error_message, 'limit') !== false) {
4053 return [
4054 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
4055 'error_code' => 'xai_rate_limit',
4056 'provider' => 'xai'
4057 ];
4058 }
4059
4060 // Quota errors
4061 if (stripos($error_message, 'quota') !== false ||
4062 stripos($error_message, 'billing') !== false) {
4063 return [
4064 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
4065 'error_code' => 'xai_quota_exceeded',
4066 'provider' => 'xai'
4067 ];
4068 }
4069
4070 // Server errors
4071 if ($status_code >= 500) {
4072 return [
4073 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
4074 'error_code' => 'xai_service_unavailable',
4075 'provider' => 'xai'
4076 ];
4077 }
4078
4079 // Generic error fallback with the actual error message
4080 return [
4081 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
4082 'error_code' => 'xai_api_error',
4083 'provider' => 'xai',
4084 'status_code' => $status_code
4085 ];
4086 }
4087
4088 $response_body = wp_remote_retrieve_body($response);
4089 $decoded_response = json_decode($response_body, true);
4090
4091 if (isset($decoded_response['choices'][0]['message']['content'])) {
4092 return trim($decoded_response['choices'][0]['message']['content']);
4093 } else {
4094 //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
4095 return [
4096 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
4097 'error_code' => 'xai_response_format_error',
4098 'provider' => 'xai'
4099 ];
4100 }
4101 } catch (Exception $e) {
4102 //error_log('X.AI Exception: ' . $e->getMessage());
4103 return [
4104 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
4105 'error_code' => 'xai_exception',
4106 'provider' => 'xai'
4107 ];
4108 }
4109 }
4110 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
4111 // Get system prompt instructions from options
4112 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4113
4114 // Clean and validate conversation history
4115 foreach ($conversation_history as &$message) {
4116 // Convert bot and agent roles to assistant
4117 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
4118 $message['role'] = 'assistant';
4119 }
4120
4121 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
4122 if (!in_array($message['role'], ['assistant', 'user'])) {
4123 $message['role'] = 'user';
4124 }
4125
4126 // Ensure content field exists
4127 if (!isset($message['content']) || empty($message['content'])) {
4128 $message['content'] = '';
4129 }
4130
4131 // Remove any unsupported fields
4132 $message = array_intersect_key($message, array_flip(['role', 'content']));
4133 }
4134
4135 // Add relevant content as the latest user message
4136 $conversation_history[] = [
4137 'role' => 'user',
4138 'content' => $relevant_content
4139 ];
4140
4141 // Build request body
4142 $body = json_encode([
4143 'model' => $selected_model,
4144 'max_tokens' => 1000,
4145 'temperature' => 0.8,
4146 'messages' => $conversation_history,
4147 'system' => $system_prompt_instructions
4148 ]);
4149
4150 // Set up API request
4151 $args = [
4152 'body' => $body,
4153 'headers' => [
4154 'Content-Type' => 'application/json',
4155 'x-api-key' => $claude_api_key,
4156 'anthropic-version' => '2023-06-01'
4157 ],
4158 'timeout' => 60,
4159 'redirection' => 5,
4160 'blocking' => true,
4161 'httpversion' => '1.0',
4162 'sslverify' => true,
4163 ];
4164
4165 // Make API request
4166 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
4167
4168 // Check for WordPress errors
4169 if (is_wp_error($response)) {
4170 //error_log("Claude API request error: " . $response->get_error_message());
4171 return "Sorry, there was an error connecting to the API.";
4172 }
4173
4174 // Check HTTP response code
4175 $http_code = wp_remote_retrieve_response_code($response);
4176 if ($http_code !== 200) {
4177 $error_body = wp_remote_retrieve_body($response);
4178 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
4179
4180 // Try to extract error message from response
4181 $error_data = json_decode($error_body, true);
4182 $error_message = isset($error_data['error']['message']) ?
4183 $error_data['error']['message'] :
4184 "HTTP error " . $http_code;
4185
4186 return "Sorry, the API returned an error: " . $error_message;
4187 }
4188
4189 // Parse response
4190 $response_body = json_decode(wp_remote_retrieve_body($response), true);
4191
4192 // Check for JSON decode errors
4193 if (json_last_error() !== JSON_ERROR_NONE) {
4194 //error_log("Claude API JSON decode error: " . json_last_error_msg());
4195 return "Sorry, there was an error processing the API response.";
4196 }
4197
4198 // Extract and validate response content
4199 if (isset($response_body['content']) &&
4200 is_array($response_body['content']) &&
4201 !empty($response_body['content']) &&
4202 isset($response_body['content'][0]['text'])) {
4203 return trim($response_body['content'][0]['text']);
4204 }
4205
4206 // Log unexpected response format
4207 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
4208 return "Sorry, I received an unexpected response format from the API.";
4209 }
4210 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
4211 // Get system prompt instructions from options
4212 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4213
4214 // Add system prompt to relevant content
4215 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4216
4217 // Format messages for Gemini API
4218 $formatted_messages = [];
4219
4220 // Add system message as the first user message with role prefix
4221 // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
4222 $formatted_messages[] = [
4223 'role' => 'user',
4224 'parts' => [
4225 ['text' => "[System Instructions] " . $content_with_instructions]
4226 ]
4227 ];
4228
4229 // Add model response to acknowledge system instructions
4230 $formatted_messages[] = [
4231 'role' => 'model',
4232 'parts' => [
4233 ['text' => "I understand and will follow these instructions."]
4234 ]
4235 ];
4236
4237 // Process the rest of the conversation history
4238 $current_role = null;
4239 $current_parts = [];
4240
4241 foreach ($conversation_history as $message) {
4242 // Skip the first system message as we already handled it
4243 if ($message['role'] === 'system') {
4244 continue;
4245 }
4246
4247 // Map roles to Gemini format
4248 $gemini_role = '';
4249 if ($message['role'] === 'user') {
4250 $gemini_role = 'user';
4251 } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
4252 $gemini_role = 'model';
4253 } else {
4254 // Skip unsupported roles
4255 continue;
4256 }
4257
4258 // If we have a new role, add the previous message
4259 if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
4260 $formatted_messages[] = [
4261 'role' => $current_role,
4262 'parts' => $current_parts
4263 ];
4264 $current_parts = [];
4265 }
4266
4267 // Set current role and add text to parts
4268 $current_role = $gemini_role;
4269 $current_parts[] = ['text' => $message['content']];
4270 }
4271
4272 // Add the last message if there's content
4273 if ($current_role !== null && !empty($current_parts)) {
4274 $formatted_messages[] = [
4275 'role' => $current_role,
4276 'parts' => $current_parts
4277 ];
4278 }
4279
4280 // Build the request body
4281 $body = json_encode([
4282 'contents' => $formatted_messages,
4283 'generationConfig' => [
4284 'temperature' => 0.7,
4285 'topP' => 0.95,
4286 'topK' => 40,
4287 'maxOutputTokens' => 8192,
4288 ],
4289 'safetySettings' => [
4290 [
4291 'category' => 'HARM_CATEGORY_HARASSMENT',
4292 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4293 ],
4294 [
4295 'category' => 'HARM_CATEGORY_HATE_SPEECH',
4296 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4297 ],
4298 [
4299 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
4300 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4301 ],
4302 [
4303 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
4304 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
4305 ]
4306 ]
4307 ]);
4308
4309 // Prepare the API endpoint
4310 $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
4311
4312 // Set up the API request
4313 $args = [
4314 'body' => $body,
4315 'headers' => [
4316 'Content-Type' => 'application/json',
4317 ],
4318 'timeout' => 60,
4319 'redirection' => 5,
4320 'blocking' => true,
4321 'httpversion' => '1.0',
4322 'sslverify' => true,
4323 ];
4324
4325 // Make the API request
4326 $response = wp_remote_post($api_endpoint, $args);
4327
4328 // Process the response
4329 if (is_wp_error($response)) {
4330 return "Sorry, there was an error processing your request: " . $response->get_error_message();
4331 }
4332
4333 $response_body = json_decode(wp_remote_retrieve_body($response), true);
4334
4335 // Handle potential errors in the response
4336 if (isset($response_body['error'])) {
4337 //error_log('Gemini API Error: ' . json_encode($response_body['error']));
4338 return "Sorry, there was an error with the Gemini API: " .
4339 (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
4340 }
4341
4342 // Extract the response text
4343 if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
4344 return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
4345 } else {
4346 //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
4347 return "Sorry, I couldn't process that request. The response format was unexpected.";
4348 }
4349 }
4350
4351
4352
4353 public function mxchat_dismiss_pre_chat_message() {
4354 // Get and sanitize the user identifier
4355 $user_id = $this->mxchat_get_user_identifier();
4356 $user_id = sanitize_key($user_id);
4357
4358 // Set a transient to track that the user has dismissed the pre-chat message
4359 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
4360 set_transient($transient_key, true, DAY_IN_SECONDS);
4361
4362 wp_send_json_success();
4363 }
4364
4365 public function mxchat_check_pre_chat_message_status() {
4366 // Get and sanitize the user identifier
4367 $user_id = $this->mxchat_get_user_identifier();
4368 $user_id = sanitize_key($user_id);
4369
4370 // Check if the transient exists (i.e., if the message was dismissed)
4371 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
4372 $dismissed = get_transient($transient_key);
4373
4374 // Log the result to see if it's being set correctly
4375 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
4376
4377 if ($dismissed) {
4378 wp_send_json_success(['dismissed' => true]);
4379 } else {
4380 wp_send_json_success(['dismissed' => false]);
4381 }
4382
4383 wp_die();
4384 }
4385
4386 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
4387 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
4388 return 0;
4389 }
4390
4391 $dotProduct = array_sum(array_map(function ($a, $b) {
4392 return $a * $b;
4393 }, $vectorA, $vectorB));
4394 $normA = sqrt(array_sum(array_map(function ($a) {
4395 return $a * $a;
4396 }, $vectorA)));
4397 $normB = sqrt(array_sum(array_map(function ($b) {
4398 return $b * $b;
4399 }, $vectorB)));
4400
4401 if ($normA == 0 || $normB == 0) {
4402 return 0;
4403 }
4404
4405 return $dotProduct / ($normA * $normB);
4406 }
4407
4408 public function mxchat_enqueue_scripts_styles() {
4409 // Define version numbers for the styles and scripts
4410 $chat_style_version = '2.2.1';
4411 $chat_script_version = '2.2.1';
4412
4413 // Enqueue the script
4414 wp_enqueue_script(
4415 'mxchat-chat-js',
4416 plugin_dir_url(__FILE__) . '../js/chat-script.js',
4417 array('jquery'),
4418 $chat_script_version,
4419 true
4420 );
4421
4422 // Enqueue the CSS
4423 wp_enqueue_style(
4424 'mxchat-chat-css',
4425 plugin_dir_url(__FILE__) . '../css/chat-style.css',
4426 array(),
4427 $chat_style_version
4428 );
4429
4430 // Fetch options from the database
4431 $this->options = get_option('mxchat_options');
4432 $prompts_options = get_option('mxchat_prompts_options', array());
4433
4434 // Prepare settings for JavaScript
4435 $style_settings = array(
4436 'ajax_url' => admin_url('admin-ajax.php'),
4437 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
4438 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
4439 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
4440 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
4441 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
4442 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
4443 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
4444 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
4445 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
4446 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
4447 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
4448 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
4449 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
4450 'icon_color' => $this->options['icon_color'] ?? '#fff',
4451 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
4452 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
4453 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
4454
4455 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
4456 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
4457 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
4458 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
4459 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
4460 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
4461
4462 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
4463 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
4464 );
4465
4466 // Pass the settings to the script
4467 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
4468 }
4469
4470
4471 // Modify the mxchat_reset_rate_limits function to handle different timeframes
4472 public function mxchat_reset_rate_limits() {
4473 global $wpdb;
4474 $all_options = get_option('mxchat_options', []);
4475 $current_time = time();
4476
4477 // Get all rate limit options
4478 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
4479
4480 foreach ($option_names as $option_name) {
4481 // Parse the option name to extract role and user ID
4482 // Format: mxchat_chat_limit_ROLE_USERID or mxchat_chat_limit_logged_out_IP
4483 $parts = explode('_', $option_name);
4484
4485 // Skip if the option name doesn't match our expected format
4486 if (count($parts) < 4) {
4487 continue;
4488 }
4489
4490 // Extract role (may be multiple parts like 'shop_manager')
4491 $role_parts = array_slice($parts, 3, -1); // Get all parts between 'mxchat_chat_limit_' and the last part (user ID)
4492 $role = implode('_', $role_parts);
4493
4494 // Skip if role doesn't exist in our settings
4495 if (!isset($all_options['rate_limits'][$role])) {
4496 continue;
4497 }
4498
4499 $timeframe = $all_options['rate_limits'][$role]['timeframe'];
4500 $limit_data = get_option($option_name);
4501
4502 if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
4503 continue;
4504 }
4505
4506 $timestamp = $limit_data['timestamp'];
4507 $should_reset = false;
4508
4509 // Determine if we should reset based on the timeframe
4510 switch ($timeframe) {
4511 case 'hourly':
4512 $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
4513 break;
4514 case 'daily':
4515 $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
4516 break;
4517 case 'weekly':
4518 $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
4519 break;
4520 case 'monthly':
4521 $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
4522 break;
4523 }
4524
4525 // Reset the counter if the timeframe has passed
4526 if ($should_reset) {
4527 delete_option($option_name);
4528 wp_cache_delete($option_name, 'options');
4529 }
4530 }
4531
4532 // Clean up any orphaned entries
4533 wp_cache_delete('mxchat_all_chat_limits', 'options');
4534 }
4535
4536 private function mxchat_fetch_woocommerce_products() {
4537 // Ensure WooCommerce is active
4538 if (!class_exists('WooCommerce')) {
4539 return [];
4540 }
4541
4542 $args = array(
4543 'post_type' => 'product',
4544 'post_status' => 'publish',
4545 'posts_per_page' => -1,
4546 );
4547
4548 $products = get_posts($args);
4549 $product_data = [];
4550
4551 foreach ($products as $product) {
4552 $product_id = $product->ID;
4553 $product_obj = wc_get_product($product_id);
4554
4555 $product_data[] = array(
4556 'id' => $product_id,
4557 'name' => $product_obj->get_name(),
4558 'description' => $product_obj->get_description(),
4559 'short_description' => $product_obj->get_short_description(),
4560 'url' => get_permalink($product_id),
4561 'price' => $product_obj->get_regular_price(),
4562 'sale_price' => $product_obj->get_sale_price(),
4563 'stock_status' => $product_obj->get_stock_status(),
4564 'sku' => $product_obj->get_sku(),
4565 'in_stock' => $product_obj->is_in_stock(),
4566 'total_sales' => $product_obj->get_total_sales(),
4567 );
4568 }
4569
4570 return $product_data;
4571 }
4572
4573 /**
4574 * Check if the current user has exceeded their rate limit based on role
4575 *
4576 * @return true|array True if limit not exceeded, or array with error message if exceeded
4577 */
4578 public function check_rate_limit() {
4579 $all_options = get_option('mxchat_options', []);
4580 //error_log('MXChat Rate Limit: Starting check');
4581 //error_log('MXChat Rate Limit: Options: ' . print_r($all_options, true));
4582
4583 // Determine user role or if logged out
4584 if (is_user_logged_in()) {
4585 $user = wp_get_current_user();
4586 $user_id = $user->ID;
4587
4588 // Get the user's primary role using reset() to safely get the first element
4589 $user_roles = $user->roles;
4590
4591 // Safely get the first role regardless of array key structure
4592 if (!empty($user_roles) && is_array($user_roles)) {
4593 $role = reset($user_roles); // This safely gets the first element regardless of key
4594 } else {
4595 $role = 'subscriber'; // Default to subscriber if no role found
4596 }
4597
4598 //error_log('MXChat Rate Limit: User ID: ' . $user_id . ', Role: ' . $role);
4599 } else {
4600 $role = 'logged_out';
4601 // Use IP address for non-logged-in users
4602 $user_id = $this->get_client_ip();
4603 //error_log('MXChat Rate Limit: Logged out user IP: ' . $user_id);
4604 }
4605
4606 // Check if rate limits are configured for this role
4607 if (!isset($all_options['rate_limits'][$role])) {
4608 //error_log('MXChat Rate Limit: No rate limit configured for role: ' . $role);
4609 return true; // No limit set for this role
4610 }
4611
4612 $limit = $all_options['rate_limits'][$role]['limit'];
4613 //error_log('MXChat Rate Limit: Limit for role ' . $role . ': ' . $limit);
4614
4615 // If unlimited, return true immediately
4616 if ($limit === 'unlimited') {
4617 //error_log('MXChat Rate Limit: Unlimited setting, no limit applied');
4618 return true;
4619 }
4620
4621 // Get the option name for this user/role
4622 $option_name = 'mxchat_chat_limit_' . $role . '_' . $user_id;
4623 //error_log('MXChat Rate Limit: Option name: ' . $option_name);
4624
4625 // Get the counter data
4626 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
4627 //error_log('MXChat Rate Limit: Current limit data: ' . print_r($limit_data, true));
4628
4629 // If first request or counter reset needed, set the initial timestamp
4630 if ($limit_data['count'] === 0) {
4631 $limit_data['timestamp'] = time();
4632 update_option($option_name, $limit_data);
4633 //error_log('MXChat Rate Limit: First request, initialized timestamp');
4634 }
4635
4636 // Get the timeframe
4637 $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
4638 $all_options['rate_limits'][$role]['timeframe'] : 'daily';
4639 //error_log('MXChat Rate Limit: Timeframe: ' . $timeframe);
4640
4641 // Check if the counter needs to be reset based on timeframe
4642 $current_time = time();
4643 $timestamp = $limit_data['timestamp'];
4644 $should_reset = false;
4645
4646 switch ($timeframe) {
4647 case 'hourly':
4648 $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
4649 break;
4650 case 'daily':
4651 $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
4652 break;
4653 case 'weekly':
4654 $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
4655 break;
4656 case 'monthly':
4657 $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
4658 break;
4659 }
4660
4661 //error_log('MXChat Rate Limit: Current time: ' . $current_time . ', Last timestamp: ' . $timestamp);
4662 //error_log('MXChat Rate Limit: Time elapsed: ' . ($current_time - $timestamp) . ' seconds');
4663 //error_log('MXChat Rate Limit: Should reset: ' . ($should_reset ? 'Yes' : 'No'));
4664
4665 // Reset the counter if the timeframe has passed
4666 if ($should_reset) {
4667 $limit_data = ['count' => 0, 'timestamp' => $current_time];
4668 update_option($option_name, $limit_data);
4669 //error_log('MXChat Rate Limit: Reset counter to 0');
4670 }
4671
4672 // Check if user has exceeded their limit
4673 if ($limit_data['count'] >= intval($limit)) {
4674 // Get the custom message for this role
4675 $message = !empty($all_options['rate_limits'][$role]['message'])
4676 ? $all_options['rate_limits'][$role]['message']
4677 : __('Rate limit exceeded. Please try again later.', 'mxchat');
4678
4679 // Add timeframe information to the message if placeholders exist
4680 $timeframe_label = '';
4681 switch ($timeframe) {
4682 case 'hourly':
4683 $timeframe_label = __('hour', 'mxchat');
4684 break;
4685 case 'daily':
4686 $timeframe_label = __('day', 'mxchat');
4687 break;
4688 case 'weekly':
4689 $timeframe_label = __('week', 'mxchat');
4690 break;
4691 case 'monthly':
4692 $timeframe_label = __('month', 'mxchat');
4693 break;
4694 }
4695
4696 // Replace placeholders in the message
4697 $message = str_replace(
4698 ['{limit}', '{count}', '{remaining}', '{timeframe}'],
4699 [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
4700 $message
4701 );
4702
4703 //error_log('MXChat Rate Limit: Limit exceeded. Message: ' . $message);
4704
4705 // Return error with the custom message
4706 return [
4707 'error' => true,
4708 'message' => $message
4709 ];
4710 }
4711
4712 // Increment the counter
4713 $limit_data['count']++;
4714 update_option($option_name, $limit_data);
4715 //error_log('MXChat Rate Limit: Incremented counter to ' . $limit_data['count']);
4716
4717 return true;
4718 }
4719
4720 // Helper function to get client IP address
4721 private function get_client_ip() {
4722 // Check for shared internet/ISP IP
4723 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
4724 return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
4725 }
4726
4727 // Check for IPs passing through proxies
4728 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
4729 // Use the first value in the comma-separated list
4730 $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
4731 return trim($forwarded_for[0]);
4732 }
4733
4734 if (!empty($_SERVER['REMOTE_ADDR'])) {
4735 return sanitize_text_field($_SERVER['REMOTE_ADDR']);
4736 }
4737
4738 // Fallback
4739 return 'unknown';
4740 }
4741
4742 }
4743 ?>
4744