PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.3
MxChat – AI Chatbot & Content Generation for WordPress v2.2.3
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.3, at includes/class-mxchat-integrator.php

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