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

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