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

3,453 lines 128.1 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 $chat_count;
9 private $fallbackResponse;
10 private $productCardHtml;
11 private $word_handler;
12
13 public function __construct() {
14 $this->options = get_option('mxchat_options');
15 $this->chat_count = get_option('mxchat_chat_count', 0);
16 $this->word_handler = new MXChat_Word_Handler($this->options);
17
18 // Add WooCommerce hooks
19 add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3);
20
21 // Ensure embeddings are removed when a product is moved to trash or permanently deleted
22 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
23 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
24
25 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
26 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
27 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
28
29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31 // Add the AJAX actions for checking if the pre-chat message was dismissed
32 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
33 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
34
35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
37
38 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
39 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
40
41 if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
42 wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
43 }
44
45 // Add REST API routes registration
46 add_action('rest_api_init', array($this, 'register_routes'));
47
48 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
49 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
50
51
52 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
53
54 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
55 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
56 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
57 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
58
59 // Add these with your other add_action hooks
60 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
61 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
62 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
63 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
64 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
65 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
66 }
67
68 public function mxchat_handle_product_change($post_id, $post, $update) {
69 // Ensure this is a product post type
70 if ($post->post_type !== 'product') {
71 return;
72 }
73
74 // Only generate embeddings if the product is published
75 if ($post->post_status === 'publish') {
76 // Delay the embedding slightly to ensure all product data is available
77 add_action('shutdown', function() use ($post_id) {
78 $product = wc_get_product($post_id);
79 if ($product && $product->get_price() !== '') {
80 $this->mxchat_store_product_embedding($product);
81 } else {
82 // Optionally, log or handle the case where product data is incomplete
83 // error_log("Product {$post_id} does not have complete data. Embedding not generated.");
84 }
85 });
86 }
87 }
88
89 public function mxchat_handle_product_delete($post_id) {
90 if (get_post_type($post_id) !== 'product') {
91 return;
92 }
93
94 global $wpdb;
95 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
96
97 // Delete the embedding associated with this product
98 $wpdb->delete($table_name, array('source_url' => get_permalink($post_id)), array('%s'));
99 }
100
101 private function mxchat_store_product_embedding($product) {
102 if (isset($this->options['enable_woocommerce_integration']) && $this->options['enable_woocommerce_integration'] === '1') {
103
104 $source_url = get_permalink($product->get_id());
105 $regular_price = $product->get_regular_price();
106 $sale_price = $product->get_sale_price();
107 $price = $sale_price ?: $regular_price;
108
109 $description = $product->get_description() . "\n\n" .
110 "Short Description: " . $product->get_short_description() . "\n" .
111 "Price: " . $regular_price . "\n" .
112 "Sale Price: " . ($sale_price ?: 'N/A') . "\n" .
113 "SKU: " . $product->get_sku();
114
115 global $wpdb;
116 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
117
118 // Delete any existing embedding for this product
119 $wpdb->delete($table_name, array('source_url' => $source_url), array('%s'));
120
121 // Submit the new content and embedding to the database
122 MxChat_Utils::submit_content_to_db($description, $source_url, $this->options['api_key']);
123 }
124 }
125
126
127
128
129
130 private function mxchat_increment_chat_count() {
131 $chat_count = get_option('mxchat_chat_count', 0);
132 $chat_count++;
133 update_option('mxchat_chat_count', $chat_count);
134 }
135
136 function mxchat_fetch_conversation_history() {
137 if (empty($_POST['session_id'])) {
138 wp_send_json_error(['message' => 'Session ID missing.']);
139 wp_die();
140 }
141
142 $session_id = sanitize_text_field($_POST['session_id']);
143 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
144 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
145
146 if (empty($history)) {
147 // Even if history is empty, return the chat mode
148 wp_send_json_success([
149 'conversation' => [],
150 'chat_mode' => $chat_mode
151 ]);
152 wp_die();
153 }
154
155 wp_send_json_success([
156 'conversation' => $history,
157 'chat_mode' => $chat_mode
158 ]);
159 wp_die();
160 }
161 private function mxchat_fetch_conversation_history_for_ajax($session_id) {
162 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
163 $formatted_history = [];
164
165 // Format the history to align with the expected structure for OpenAI
166 foreach ($history as $entry) {
167 $formatted_history[] = [
168 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
169 'content' => $entry['content']
170 ];
171 }
172
173 return $formatted_history;
174 }
175
176
177 private function mxchat_fetch_conversation_history_for_ai($session_id) {
178 $history = get_option("mxchat_history_{$session_id}", []);
179
180 $formatted_history = [];
181 $max_tokens = 120000; // Adjust based on your model's context window
182 $reserved_tokens = 4000; // Reserve tokens for current query and system/context prompt
183 $current_token_count = 0;
184
185 foreach ($history as $entry) {
186 // Skip messages containing HTML
187 if ($entry['content'] !== strip_tags($entry['content'])) {
188 continue;
189 }
190
191 // Calculate tokens for this entry
192 $tokens_in_entry = str_word_count($entry['content']) * 1.5; // Rough estimate: ~1.5 tokens per word
193 $current_token_count += $tokens_in_entry;
194
195 // If adding this entry exceeds the limit, stop
196 if ($current_token_count + $reserved_tokens > $max_tokens) {
197 break;
198 }
199
200 $formatted_history[] = [
201 'role' => $entry['role'],
202 'content' => $entry['content']
203 ];
204 }
205
206 return $formatted_history;
207 }
208
209
210 public function register_routes() {
211 //error_log('Registering MxChat REST routes');
212
213 register_rest_route('mxchat/v1', '/stream', [
214 'methods' => 'GET',
215 'callback' => [$this, 'mxchat_stream_events'],
216 'permission_callback' => [$this, 'verify_chat_session'],
217 ]);
218
219 register_rest_route('mxchat/v1', '/agent-response', [
220 'methods' => 'POST',
221 'callback' => [$this, 'mxchat_handle_agent_response'],
222 'permission_callback' => [$this, 'verify_slack_request'],
223 ]);
224
225 register_rest_route('mxchat/v1', '/slack-interaction', [
226 'methods' => 'POST',
227 'callback' => [$this, 'handle_slack_interaction'],
228 'permission_callback' => [$this, 'verify_slack_request'],
229 ]);
230
231 //error_log('MxChat REST routes registered');
232 }
233
234 /**
235 * Verify valid chat session
236 */
237 public function verify_chat_session($request) {
238 $session_id = $request->get_param('session_id');
239 if (empty($session_id)) {
240 //error_log('Empty session ID in chat request');
241 return false;
242 }
243
244 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
245 return $chat_mode === 'agent';
246 }
247
248 /**
249 * Verify request is coming from Slack.
250 *
251 * @param WP_REST_Request $request
252 * @return bool True if valid, false otherwise.
253 */
254 public function verify_slack_request($request) {
255 // Get the Slack signing secret from your plugin options
256 $valid_key = $this->options['live_agent_secret_key'] ?? '';
257
258 if (empty($valid_key)) {
259 //error_log('Slack signing secret not configured');
260 return false;
261 }
262
263 $timestamp = $request->get_header('X-Slack-Request-Timestamp');
264 $slack_signature = $request->get_header('X-Slack-Signature');
265
266 // Verify timestamp to prevent replay attacks
267 if (abs(time() - intval($timestamp)) > 300) {
268 //error_log('Slack request timestamp too old');
269 return false;
270 }
271
272 // Get raw request body
273 $request_body = file_get_contents('php://input');
274
275 // Create the signature base string
276 $sig_basestring = "v0:{$timestamp}:{$request_body}";
277
278 // Calculate expected signature
279 $my_signature = 'v0=' . hash_hmac('sha256', $sig_basestring, $valid_key);
280
281 // Compare signatures
282 return hash_equals($my_signature, $slack_signature);
283 }
284 public function mxchat_stream_events(WP_REST_Request $request) {
285 header('Content-Type: text/event-stream');
286 header('Cache-Control: no-cache');
287 header('Connection: keep-alive');
288
289 $session_id = sanitize_text_field($request->get_param('session_id'));
290 $last_seen_id = sanitize_text_field($request->get_param('last_seen_id')) ?: '';
291
292 if (empty($session_id)) {
293 echo "event: error\ndata: Missing session_id\n\n";
294 flush();
295 exit;
296 }
297
298 $history = get_option("mxchat_history_{$session_id}", []);
299
300 // Filter only new messages
301 $new_messages = array_filter($history, function ($message) use ($last_seen_id) {
302 return !empty($message['id']) && $message['id'] > $last_seen_id;
303 });
304
305 // Send new messages if available
306 if (!empty($new_messages)) {
307 echo "event: newMessages\ndata: " . json_encode(array_values($new_messages)) . "\n\n";
308 } else {
309 // Keep the connection alive
310 echo "event: keepAlive\ndata: {}\n\n";
311 }
312 flush();
313 exit;
314 }
315
316
317
318 private function mxchat_save_chat_message($session_id, $role, $message) {
319 global $wpdb;
320 // Extract agent name if present in the message
321 $agent_name = '';
322 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
323 $agent_name = $matches[1];
324 $message = str_replace("Agent: $agent_name - ", '', $message);
325 // Store the agent name in session metadata if it's not already set
326 $session_meta_key = "mxchat_agent_name_{$session_id}";
327 if (empty(get_option($session_meta_key))) {
328 update_option($session_meta_key, $agent_name);
329 }
330 }
331
332 // Define the table name for chat transcripts
333 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
334
335 // Generate a unique message ID
336 $message_id = uniqid();
337
338 // Get user ID based on login status
339 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
340
341 // Handle user identification based on whether it's an agent or regular user
342 if ($agent_name) {
343 // If it's an agent message, use agent name
344 $user_identifier = $agent_name;
345 $displayed_name = $agent_name;
346 } else {
347 // For regular users, use email if logged in, otherwise fall back to user identifier
348 $user_email = MxChat_User::mxchat_get_user_email();
349 $user_identifier = MxChat_User::mxchat_get_user_identifier();
350 $displayed_name = $user_email ?: $user_identifier;
351 }
352
353 // Save the message to the session history
354 $history = get_option("mxchat_history_{$session_id}", []);
355 $history[] = [
356 'id' => $message_id,
357 'role' => $role,
358 'content' => $message,
359 'timestamp' => round(microtime(true) * 1000),
360 'agent_name' => $displayed_name, // Use the appropriate name for display
361 ];
362 update_option("mxchat_history_{$session_id}", $history);
363
364 // Save the message to the database
365 $wpdb->insert($table_name, [
366 'user_id' => $user_id,
367 'user_identifier' => $user_identifier,
368 'user_email' => MxChat_User::mxchat_get_user_email(),
369 'session_id' => $session_id,
370 'role' => $role,
371 'message' => $message,
372 'timestamp' => current_time('mysql', 1),
373 ]);
374
375 return $message_id;
376 }
377
378 public function mxchat_handle_chat_request() {
379 global $wpdb;
380
381 // Reset fallback response at the start of each request
382 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
383 $this->productCardHtml = '';
384
385 // Get and sanitize the user identifier
386 $user_id = $this->mxchat_get_user_identifier();
387 $user_id = sanitize_key($user_id);
388
389 // Determine if user is logged in
390 $is_logged_in = is_user_logged_in();
391
392 // Get rate limit based on user status
393 $rate_limit = $is_logged_in
394 ? $this->options['rate_limit_logged_in'] ?? '100'
395 : $this->options['rate_limit_logged_out'] ?? '10';
396
397 // If rate limit is 'unlimited', skip rate limiting checks
398 if ($rate_limit !== 'unlimited') {
399 // Convert rate limit to integer
400 $rate_limit = intval($rate_limit);
401
402 // Setup rate limiting
403 $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
404 $chat_count = get_transient($rate_limit_transient_key);
405
406 if ($chat_count === false) {
407 // Initialize new counter if none exists
408 $chat_count = 0;
409 }
410
411 // Check if user has exceeded their rate limit
412 if ($chat_count >= $rate_limit) {
413 // Get custom rate limit message or use default
414 $rate_limit_message = isset($this->options['rate_limit_message'])
415 ? $this->options['rate_limit_message']
416 : 'Rate limit exceeded. Please try again later.';
417
418 // Replace placeholder if it exists in the message
419 $rate_limit_message = str_replace(
420 array('{limit}', '{count}', '{remaining}'),
421 array($rate_limit, $chat_count, max(0, $rate_limit - $chat_count)),
422 $rate_limit_message
423 );
424
425 wp_send_json([
426 'success' => false,
427 'message' => $rate_limit_message,
428 'status' => 'rate_limit_exceeded',
429 'limit' => $rate_limit,
430 'count' => $chat_count
431 ]);
432 wp_die();
433 }
434
435 // Increment the counter
436 $chat_count++;
437
438 // Store the updated count with 24-hour expiration
439 set_transient($rate_limit_transient_key, $chat_count, DAY_IN_SECONDS);
440 }
441
442 // Rest of your existing code...
443 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
444 //error_log("Session ID: $session_id");
445
446 if (empty($session_id)) {
447 //error_log("Error: Session ID is missing.");
448 wp_send_json_error('Session ID is missing.');
449 wp_die();
450 }
451
452 // Validate and sanitize the incoming message
453 if (empty($_POST['message'])) {
454 //error_log("Error: No message received.");
455 wp_send_json_error('No message received.');
456 wp_die();
457 }
458
459
460 $message = wp_strip_all_tags($_POST['message'], false);
461 $message = trim($message);
462
463 // Save the user's message
464 $this->mxchat_save_chat_message($session_id, 'user', $message);
465
466 // Check if the message is an email address
467 if (is_email($message)) {
468 // Add the email to Loops
469 $this->add_email_to_loops($message);
470
471 // Send success response
472 $response_message = $this->options['email_capture_response'] ??
473 'Thank you! Your coupon is on the way!';
474
475 wp_send_json([
476 'success' => true,
477 'status' => 'email_captured',
478 'message' => $response_message
479 ]);
480 wp_die();
481 }
482
483 $intent_info = '';
484
485 // Check chat mode
486 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
487 //error_log("Chat Mode: $chat_mode");
488
489 // Handle agent mode
490 if ($chat_mode === 'agent') {
491 // First, check for switch intent before doing anything else
492 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
493
494 // If we matched an intent and it's the switch intent, handle it
495 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
496 //error_log("Switch to chatbot intent detected");
497
498 // Update chat mode first
499 update_option("mxchat_mode_{$session_id}", 'ai');
500
501 // Clear any existing PDF context to start fresh
502 $this->clear_pdf_transients($session_id);
503
504 // Prepare clean switch response
505 $response_data = [
506 'text' => $this->fallbackResponse['text'],
507 'html' => '',
508 'session_id' => $session_id,
509 'chat_mode' => 'ai'
510 ];
511
512 // Save the mode switch message
513 $this->mxchat_save_chat_message($session_id, 'system', 'Switched to AI chat mode');
514 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
515
516 // Send response and exit
517 wp_send_json($response_data);
518 wp_die();
519 } elseif (!$intent_matched) {
520 // No intent matched, handle live agent message
521 try {
522 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
523 //error_log("Message sent to agent.");
524
525 wp_send_json_success([
526 'status' => 'waiting_for_agent',
527 'message' => 'Message sent to live agent.'
528 ]);
529 } catch (\Exception $e) {
530 //error_log("Error sending message to agent: " . $e->getMessage());
531 wp_send_json_error('Failed to send message to agent');
532 }
533 wp_die();
534 }
535 }
536
537 // Step 1: Check for new PDF URL in the message
538 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
539 $new_pdf_url = $matches[0];
540
541 // Check if this is likely a PDF-related request
542 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
543 $is_pdf_request = false;
544
545 foreach ($pdf_keywords as $keyword) {
546 if (stripos($message, $keyword) !== false) {
547 $is_pdf_request = true;
548 break;
549 }
550 }
551
552 // If it looks like a PDF request or we're waiting for a PDF URL
553 if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
554 // Validate HTTPS
555 if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
556 // Extract filename from URL
557 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
558
559 // Clear previous PDF transients
560 $this->clear_pdf_transients($session_id);
561
562 // Process new PDF
563 $max_pages = $this->options['pdf_max_pages'] ?? 69;
564 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
565
566 if ($embeddings === 'too_many_pages') {
567 $error_text = sprintf(
568 $this->options['pdf_intent_error_text'] ??
569 "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.",
570 $max_pages
571 );
572 $this->fallbackResponse['text'] = $error_text;
573 } elseif ($embeddings) {
574 // Store new PDF information
575 // Create a more meaningful filename from URL
576 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
577
578 // If the filename is generic (like results_download.php), create a more descriptive one
579 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
580 strpos($pdf_filename, '.php') !== false) {
581 // Create a timestamp-based name
582 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
583 }
584
585 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
586 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
587 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
588 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
589
590 $success_text = $this->options['pdf_intent_success_text'] ??
591 "I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?";
592
593 // Return success with filename for UI update
594 wp_send_json([
595 'success' => true,
596 'message' => $success_text,
597 'data' => [
598 'filename' => $pdf_filename
599 ]
600 ]);
601 wp_die();
602 } else {
603 $error_text = $this->options['pdf_intent_error_text'] ??
604 "Sorry, I couldn't process the PDF. Please ensure it's a valid file.";
605 $this->fallbackResponse['text'] = $error_text;
606 }
607
608 wp_send_json([
609 'success' => false,
610 'message' => $this->fallbackResponse['text']
611 ]);
612 wp_die();
613 }
614 }
615 }
616
617 // Step 2: Detect intent and handle intent-based responses
618 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
619 //error_log("Intent Matched: " . ($intent_matched ? "Yes" : "No"));
620
621 // Step 3: If intent is matched and handled, respond immediately
622 if ($intent_matched && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
623 //error_log("Intent response triggered.");
624 $response_data = [
625 'text' => $this->fallbackResponse['text'],
626 'html' => $this->fallbackResponse['html'],
627 'session_id' => $session_id
628 ];
629 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
630 wp_send_json($response_data);
631 wp_die();
632 }
633
634 // If no intent matched or product not found, proceed with AI response
635 //error_log("No matching intent or fallback. Generating AI response.");
636
637 // Step 4: Generate AI response
638 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
639 $this->mxchat_increment_chat_count();
640
641 // Generate embedding for the user's query
642 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
643 if (!is_array($user_message_embedding)) {
644 //error_log("Failed to generate message embedding for session $session_id");
645 wp_send_json_error('Error processing your message.');
646 wp_die();
647 }
648
649 // Build context with both knowledge base and PDF content if available
650 $context_content = "User asked: '{$message}'\n\n";
651
652 // Get relevant content from knowledge base
653 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
654 if (!empty($relevant_content)) {
655 $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
656 }
657
658
659 // Check for and include PDF content
660 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
661 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
662 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
663 if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
664 $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
665 if (!empty($relevant_pdf_pages)) {
666 $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
667 foreach ($relevant_pdf_pages as $page_data) {
668 $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
669 }
670 $context_content .= "\n";
671 }
672 }
673
674 // Check for and include Word content
675 $word_url = get_transient('mxchat_word_url_' . $session_id);
676 $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
677 $word_filename = get_transient('mxchat_word_filename_' . $session_id);
678 if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
679 $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
680 if (!empty($relevant_word_chunks)) {
681 $context_content .= "Relevant content from Word document '{$word_filename}':\n";
682 foreach ($relevant_word_chunks as $chunk_data) {
683 $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
684 }
685 $context_content .= "\n";
686 }
687 }
688 // Generate the response using the full context
689 $response = $this->mxchat_generate_response(
690 $context_content,
691 $this->options['api_key'],
692 $this->options['xai_api_key'],
693 $this->options['claude_api_key'],
694 $conversation_history
695 );
696
697 $this->mxchat_save_chat_message($session_id, 'bot', $response);
698
699 // Step 5: Save additional content if available
700 if (!empty($this->productCardHtml)) {
701 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
702 }
703
704 if (!empty($this->fallbackResponse['html'])) {
705 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
706 }
707
708 // Step 6: Return the response
709 $response_data = [
710 'text' => $response,
711 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
712 'session_id' => $session_id
713 ];
714
715 wp_send_json($response_data);
716 wp_die();
717 }
718
719
720 // Helper function to clear PDF and Word document related transients
721 private function clear_pdf_transients($session_id) {
722 // PDF transients
723 delete_transient('mxchat_pdf_url_' . $session_id);
724 delete_transient('mxchat_pdf_embeddings_' . $session_id);
725 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
726 delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
727
728 // Word document transients
729 delete_transient('mxchat_word_url_' . $session_id);
730 delete_transient('mxchat_word_filename_' . $session_id);
731 delete_transient('mxchat_word_embeddings_' . $session_id);
732 delete_transient('mxchat_include_word_in_context_' . $session_id);
733 delete_transient('mxchat_waiting_for_word_' . $session_id);
734 }
735
736 // New function to check intents and invoke the callback function
737 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
738 global $wpdb;
739
740 // Check chat mode
741 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
742 //error_log("Checking intents for mode: " . $chat_mode);
743
744 // Generate the user embedding
745 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
746 if (!is_array($user_embedding)) {
747 //error_log("Failed to generate user embedding");
748 return false;
749 }
750
751 // Fetch intents from the database
752 $table_name = $wpdb->prefix . 'mxchat_intents';
753
754 if ($chat_mode === 'agent') {
755 // Only fetch the switch intent when in agent mode
756 $query = $wpdb->prepare(
757 "SELECT * FROM $table_name WHERE callback_function = %s",
758 'mxchat_handle_switch_to_chatbot_intent'
759 );
760 //error_log("Searching for switch intent with query: " . $query);
761 $intents = $wpdb->get_results($query);
762 //error_log("Found " . count($intents) . " switch intents");
763 } else {
764 $intents = $wpdb->get_results("SELECT * FROM $table_name");
765 }
766
767 if (empty($intents)) {
768 //error_log("No intents found in database");
769 return false;
770 }
771
772 $highest_similarity = -INF;
773 $matched_intent = null;
774
775 foreach ($intents as $intent) {
776 //error_log("Checking intent: " . $intent->intent_label);
777 $intent_embedding_serialized = $intent->embedding_vector;
778 $intent_embedding = $intent_embedding_serialized
779 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
780 : null;
781
782 if (!is_array($intent_embedding)) {
783 //error_log("Invalid embedding for intent: " . $intent->intent_label);
784 continue;
785 }
786
787 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
788 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
789 //error_log("Similarity for " . $intent->intent_label . ": " . $similarity . " (threshold: " . $intent_threshold . ")");
790
791 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
792 $highest_similarity = $similarity;
793 $matched_intent = $intent;
794 //error_log("New best match: " . $intent->intent_label . " with similarity " . $similarity);
795 }
796 }
797
798 if ($matched_intent && method_exists($this, $matched_intent->callback_function)) {
799 //error_log("Calling callback function: " . $matched_intent->callback_function);
800 call_user_func([$this, $matched_intent->callback_function], $message, $user_id, $session_id);
801 return true;
802 }
803
804 //error_log("No matching intent found");
805 return false;
806 }
807
808 //verified good
809 public function mxchat_handle_order_history($message, $user_id, $session_id) {
810 if (!class_exists('WooCommerce')) {
811 $this->fallbackResponse['text'] = "I can't access order information right now. The order system seems to be unavailable.";
812 return true;
813 }
814
815 $orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details('all');
816
817 if (empty($orderDetails)) {
818 $this->fallbackResponse['text'] = "I don't see any orders associated with your account. Are you logged in?";
819 return true;
820 }
821
822 // Generate AI prompt with context
823 $prompt = "User asked about their orders: '{$message}'\n\n";
824 $prompt .= "Order information:\n";
825 foreach ($orderDetails as $order) {
826 $items_list = array_map(function($item) {
827 return "{$item['name']} ({$item['quantity']})";
828 }, $order['items']);
829
830 $prompt .= "Order #{$order['order_id']}: {$order['formatted_total']} on {$order['date']}\n";
831 $prompt .= "Items: " . implode(', ', $items_list) . "\n";
832 }
833
834 $prompt .= "\nProvide a natural, conversational response focusing on the specific information the user asked about. ";
835 $prompt .= "If they ask about a specific order or detail, provide just that information. ";
836 $prompt .= "If they ask about license keys or sensitive information, inform them to check their email or contact support.";
837
838 // Get AI response
839 $ai_response = $this->mxchat_call_ai_api($prompt);
840 $this->fallbackResponse['text'] = $ai_response['text'];
841
842 return true;
843 }
844
845
846 public function mxchat_handle_product_inquiry($message, $user_id, $session_id) {
847 //error_log("PRODUCT INQUIRY START - Message: $message, User ID: $user_id");
848
849 // Sanitize user ID
850 $sanitized_user_id = sanitize_key($user_id);
851 //error_log("Sanitized User ID: $sanitized_user_id");
852
853 // Find product
854 $product_id = $this->find_product_in_message($message);
855 //error_log("Initial product ID from message: " . ($product_id ?? 'null'));
856
857 // Check transient if no product found
858 if (!$product_id) {
859 $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
860 //error_log("Product ID from transient: " . ($product_id ?? 'null'));
861 }
862
863 // Handle product inquiries
864 if ($product_id && class_exists('WooCommerce')) {
865 $product = wc_get_product($product_id);
866 if ($product) {
867 //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
868
869 // Prepare product details
870 $product_name = esc_html($product->get_name());
871 $product_price = $product->get_price_html();
872 $product_image_url = esc_url(wp_get_attachment_url($product->get_image_id()));
873 $product_url = esc_url(get_permalink($product_id));
874 $product_id_attr = esc_attr($product_id);
875
876 // Get product description
877 $product_description = $product->get_description() ?: $product->get_short_description();
878
879 // Log embeddings process
880 //error_log("Generating embedding for message: $message");
881 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
882 //error_log("Finding relevant content for product inquiry");
883 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
884
885 // Build AI prompt
886 $ai_prompt = "You are a knowledgeable product assistant. ";
887 $ai_prompt .= "Respond to this user query: '{$message}'\n\n";
888
889 if (!empty($relevant_content)) {
890 $ai_prompt .= "Relevant information from our knowledge base:\n{$relevant_content}\n\n";
891 }
892
893 $ai_prompt .= "Product details:\n";
894 $ai_prompt .= "Name: {$product_name}\n";
895 $ai_prompt .= "Price: " . strip_tags($product_price) . "\n";
896 if ($product_description) {
897 $ai_prompt .= "Description: {$product_description}\n";
898 }
899
900 $ai_prompt .= "\nInstructions:\n";
901 $ai_prompt .= "1. Address the user's specific question or concern about the product\n";
902 $ai_prompt .= "2. Incorporate relevant information from our knowledge base if provided\n";
903 $ai_prompt .= "3. Highlight key product features that relate to their query\n";
904 $ai_prompt .= "4. Include a natural suggestion to check out the product\n";
905 $ai_prompt .= "5. Keep the response conversational and helpful\n";
906
907 //error_log("Sending AI prompt: " . $ai_prompt);
908
909 // Build and log AI response
910 $ai_response = $this->mxchat_call_ai_api($ai_prompt);
911 //error_log("AI Response received for product inquiry: " . print_r($ai_response, true));
912
913 // Generate and log product card
914 $product_card_html = <<<HTML
915 <div class="mxchat-product-card">
916 <a href="{$product_url}" target="_blank">
917 <img src="{$product_image_url}" alt="{$product_name}" class="mxchat-product-image" />
918 <h3 class="mxchat-product-name">{$product_name}</h3>
919 </a>
920 <div class="mxchat-product-price">{$product_price}</div>
921 <button class="mxchat-add-to-cart-button" data-product-id="{$product_id_attr}">Add to Cart</button>
922 </div>
923 HTML;
924
925 // Save response and set transient
926 $this->productCardHtml = $product_card_html;
927 $this->fallbackResponse = [
928 'text' => $ai_response['text'],
929 'html' => $this->productCardHtml,
930 ];
931
932 //error_log("Setting transient for product: $product_id with key: mxchat_last_discussed_product_$sanitized_user_id");
933 set_transient('mxchat_last_discussed_product_' . $sanitized_user_id, $product_id, HOUR_IN_SECONDS);
934
935 // Verify transient was set
936 $verify_transient = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
937 //error_log("Verification - Retrieved transient value: " . ($verify_transient ?? 'null'));
938
939 return true;
940 }
941 }
942
943 //error_log("PRODUCT INQUIRY END - No product found or WooCommerce not available");
944 return false;
945 }
946
947 //verified good
948 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
949 // Log the message safely
950 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
951
952 // Initiate email capture flow
953 $response = esc_html($this->options['triggered_phrase_response'] ?? "Would you like to join our mailing list? Please provide your email below.");
954
955 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
956 $this->mxchat_save_chat_message($session_id, 'bot', $response);
957
958 // Respond to the user
959 wp_send_json(['message' => $response]);
960 wp_die();
961 }
962
963 //very good
964 public function mxchat_generate_image($message, $user_id, $session_id) {
965 // Prepare a prompt for DALL-E
966 $prompt = "Create an image of " . sanitize_text_field($message);
967
968 // Use the existing OpenAI API key
969 $openai_api_key = sanitize_text_field($this->options['api_key']);
970
971 // Call DALL-E to generate an image
972 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
973
974 // Check if the response contains an image URL
975 if (isset($image_response['imageUrl'])) {
976 $image_url = esc_url_raw($image_response['imageUrl']);
977
978 // Construct the HTML with a CSS class instead of inline styles
979 $response_html = '<img src="' . esc_url($image_url) . '" alt="Generated Image" class="mxchat-generated-image" />';
980
981 $response_text = "Here is the image I generated:";
982 } else {
983 $response_text = "I'm sorry, but I couldn't generate an image based on your request.";
984 $response_html = '';
985 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
986 }
987
988 // Save both text and HTML responses
989 $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
990
991 // Prepare the response data
992 $response_data = [
993 'message' => $response_text,
994 'html' => $response_html,
995 'image_url' => $image_url ?? '',
996 ];
997
998 // Send the JSON response
999 header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1000 echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1001 wp_die();
1002 }
1003 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1004 $api_url = 'https://api.openai.com/v1/images/generations';
1005 $body = json_encode([
1006 'prompt' => sanitize_text_field($prompt),
1007 'n' => 1,
1008 'size' => '1024x1024',
1009 'model' => sanitize_text_field($model),
1010 ]);
1011
1012 $args = [
1013 'body' => $body,
1014 'headers' => [
1015 'Content-Type' => 'application/json',
1016 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
1017 ],
1018 'method' => 'POST',
1019 'timeout' => absint($timeout),
1020 ];
1021
1022 $response = wp_remote_post($api_url, $args);
1023
1024 if (is_wp_error($response)) {
1025 //error_log("DALL-E request failed: " . $response->get_error_message());
1026 return ['error' => "Error generating image: " . $response->get_error_message()];
1027 }
1028
1029 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1030
1031 if (isset($response_body['data'][0]['url'])) {
1032 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
1033 } else {
1034 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
1035 return ['error' => "Failed to generate image."];
1036 }
1037 }
1038
1039 /**
1040 * Handle web search requests.
1041 *
1042 * Sends the refined search query to the Brave Search API and displays neatly formatted,
1043 * styled search results. Results are cached for performance.
1044 *
1045 * @since 1.0.0
1046 * @param string $message The user's search query.
1047 * @param string $user_id The user identifier.
1048 * @param string $session_id The current session ID.
1049 * @return void
1050 */
1051 public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1052 // Step 1: Interpret and refine the search query
1053 $refined_search_query = $this->mxchat_interpret_search_query( $message );
1054
1055 if ( empty( $refined_search_query ) ) {
1056 $this->fallbackResponse = array(
1057 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
1058 );
1059 return;
1060 }
1061
1062 // Retrieve and validate API settings
1063 $options = get_option( 'mxchat_options' );
1064 $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1065 $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1066
1067 if ( empty( $api_key ) ) {
1068 $this->fallbackResponse = array(
1069 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
1070 );
1071 return;
1072 }
1073
1074 // Build the API request URL
1075 $api_url = add_query_arg(
1076 array(
1077 'q' => rawurlencode( $refined_search_query ),
1078 'count' => $results_count,
1079 'text_decorations' => 'true',
1080 'rich_data' => 'true',
1081 ),
1082 'https://api.search.brave.com/res/v1/web/search'
1083 );
1084
1085 // Attempt to retrieve cached results first
1086 $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1087 $results = get_transient( $transient_key );
1088
1089 if ( false === $results ) {
1090 // Fetch new results from the Brave Search API
1091 $response = wp_remote_get(
1092 $api_url,
1093 array(
1094 'headers' => array(
1095 'Accept' => 'application/json',
1096 'Accept-Encoding' => 'gzip',
1097 'X-Subscription-Token'=> $api_key,
1098 ),
1099 'timeout' => 10,
1100 )
1101 );
1102
1103 if ( is_wp_error( $response ) ) {
1104 $this->fallbackResponse = array(
1105 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
1106 );
1107 return;
1108 }
1109
1110 $results = json_decode( wp_remote_retrieve_body( $response ), true );
1111
1112 if ( json_last_error() !== JSON_ERROR_NONE ) {
1113 $this->fallbackResponse = array(
1114 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
1115 );
1116 return;
1117 }
1118
1119 // Cache results for one hour
1120 set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1121 }
1122
1123 // Process and display results
1124 if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1125 $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1126
1127 // Only return HTML (no large text summary)
1128 $this->fallbackResponse = array(
1129 'html' => $html,
1130 );
1131
1132 // Save to chat history
1133 $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1134 } else {
1135 $this->fallbackResponse = array(
1136 'text' => sprintf(
1137 esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1138 esc_html( $refined_search_query )
1139 ),
1140 );
1141 }
1142 }
1143
1144
1145 /**
1146 * Format search results into a natural text summary.
1147 *
1148 * @since 1.0.0
1149 * @param array $results The search results from the API.
1150 * @param string $query The original search query.
1151 * @return string The text summary of the top results.
1152 */
1153 private function format_search_results( $results, $query ) {
1154 $summary = sprintf(
1155 esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1156 esc_html( $query )
1157 ) . "\n\n";
1158
1159 $max_results = min( count( $results ), 3 );
1160 for ( $i = 0; $i < $max_results; $i++ ) {
1161 $result = $results[ $i ];
1162 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1163 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1164
1165 // Append title and description to the summary
1166 $summary .= sprintf(
1167 "%s\n%s\n\n",
1168 esc_html( $title ),
1169 esc_html( $description )
1170 );
1171 }
1172
1173 return $summary;
1174 }
1175
1176 /**
1177 * Generate HTML markup for search results.
1178 *
1179 * @since 1.0.0
1180 * @param array $results The search results from the API.
1181 * @param string $query The user-refined query.
1182 * @return string The HTML markup for displaying the results.
1183 */
1184 private function generate_search_results_html( $results, $query ) {
1185 ob_start();
1186 ?>
1187 <div class="mxchat-search-results">
1188 <?php foreach ( $results as $result ) :
1189 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1190 $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1191 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1192 $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1193 $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1194 $domain = parse_url( $url, PHP_URL_HOST );
1195 ?>
1196 <div class="mxchat-search-item">
1197 <div class="mxchat-search-header">
1198 <?php if ( $favicon ) : ?>
1199 <img
1200 src="<?php echo esc_url( $favicon ); ?>"
1201 class="mxchat-site-icon"
1202 alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1203 width="16"
1204 height="16"
1205 />
1206 <?php endif; ?>
1207 <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1208 </div>
1209
1210 <div class="mxchat-search-content">
1211 <h3 class="mxchat-search-title">
1212 <a href="<?php echo esc_url( $url ); ?>"
1213 target="_blank"
1214 rel="noopener noreferrer"
1215 >
1216 <?php echo esc_html( $title ); ?>
1217 </a>
1218 </h3>
1219
1220 <?php if ( $thumbnail ) : ?>
1221 <div class="mxchat-search-thumbnail">
1222 <img
1223 src="<?php echo esc_url( $thumbnail ); ?>"
1224 alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1225 loading="lazy"
1226 />
1227 </div>
1228 <?php endif; ?>
1229
1230 <div class="mxchat-search-description">
1231 <?php echo esc_html( $description ); ?>
1232 </div>
1233 </div>
1234 </div>
1235 <?php endforeach; ?>
1236 </div>
1237 <?php
1238 return ob_get_clean();
1239 }
1240
1241
1242 //very good
1243 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1244
1245 // Step 1: Interpret the search query for better results
1246 $refined_search_query = $this->mxchat_interpret_search_query($message);
1247
1248
1249 // If no query was interpreted, return a fallback message
1250 if (empty($refined_search_query)) {
1251 $this->fallbackResponse = [
1252 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1253 'html' => "",
1254 ];
1255 return;
1256 }
1257
1258 // Brave API URL
1259 $api_url = 'https://api.search.brave.com/res/v1/images/search';
1260
1261 // Retrieve Brave API settings
1262 $options = get_option('mxchat_options');
1263 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1264
1265 if (empty($api_key)) {
1266 /*
1267 if (defined('WP_DEBUG') && WP_DEBUG) {
1268 error_log("Brave API key is missing.");
1269 }
1270 */
1271
1272 $this->fallbackResponse = [
1273 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1274 'html' => "",
1275 ];
1276 return;
1277 }
1278
1279 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1280 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
1281
1282 // Append query parameters based on settings
1283 $api_url = add_query_arg([
1284 'q' => rawurlencode($refined_search_query),
1285 'count' => $image_count,
1286 'safesearch' => $safe_search,
1287 ], $api_url);
1288
1289 /*
1290 // Log the final API URL for the search
1291 if (defined('WP_DEBUG') && WP_DEBUG) {
1292 error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1293 }
1294 */
1295
1296
1297 // Implement caching
1298 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1299 $body = get_transient($transient_key);
1300
1301 if (false === $body) {
1302 $args = [
1303 'headers' => [
1304 'Accept' => 'application/json',
1305 'Accept-Encoding' => 'gzip',
1306 'X-Subscription-Token' => $api_key,
1307 ],
1308 'timeout' => 10,
1309 ];
1310
1311 $response = wp_remote_get($api_url, $args);
1312
1313 if (is_wp_error($response)) {
1314 /*
1315 if (defined('WP_DEBUG') && WP_DEBUG) {
1316 error_log("Brave Image API request failed: " . $response->get_error_message());
1317 }
1318 */
1319
1320 $this->fallbackResponse = [
1321 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1322 'html' => "",
1323 ];
1324 return;
1325 }
1326
1327 $body = json_decode(wp_remote_retrieve_body($response), true);
1328 set_transient($transient_key, $body, HOUR_IN_SECONDS);
1329 }
1330
1331 // Process the API response
1332 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1333 $html_output = '<div class="mxchat-image-gallery">';
1334
1335 foreach ($body['results'] as $image) {
1336 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1337 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1338 $title = isset($image['title']) ? esc_html($image['title']) : __('Image', 'mxchat');
1339
1340 if ($image_url && $thumbnail_url) {
1341 $html_output .= '<div class="mxchat-image-item">';
1342 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
1343 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
1344 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
1345 $html_output .= '</a></div>';
1346 }
1347 }
1348
1349 $html_output .= '</div>';
1350
1351 $this->fallbackResponse = [
1352 'text' => "",
1353 'html' => $html_output,
1354 ];
1355
1356 // Save response in chat history
1357 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1358
1359 } else {
1360 /*
1361 if (defined('WP_DEBUG') && WP_DEBUG) {
1362 error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1363 }
1364 */
1365
1366 $this->fallbackResponse = [
1367 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1368 'html' => "",
1369 ];
1370 }
1371 }
1372 public function mxchat_interpret_search_query($user_query) {
1373 $system_prompt = "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.";
1374
1375 // Retrieve OpenAI API key using 'api_key' as the option key
1376 $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1377
1378 /*
1379 // Log the API key check, without exposing the key
1380 if (defined('WP_DEBUG') && WP_DEBUG) {
1381 error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
1382 }
1383 */
1384
1385
1386 if (empty($api_key)) {
1387 //error_log("OpenAI API key is missing.");
1388 return sanitize_text_field($user_query); // Default to the original query if API key is missing
1389 }
1390
1391 $url = 'https://api.openai.com/v1/chat/completions';
1392 $args = [
1393 'headers' => [
1394 'Authorization' => 'Bearer ' . $api_key,
1395 'Content-Type' => 'application/json',
1396 ],
1397 'body' => wp_json_encode([
1398 'model' => 'gpt-3.5-turbo',
1399 'messages' => [
1400 ['role' => 'system', 'content' => $system_prompt],
1401 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1402 ],
1403 'temperature' => 0.2,
1404 'max_tokens' => 20,
1405 ]),
1406 'method' => 'POST',
1407 ];
1408
1409 $response = wp_remote_post($url, $args);
1410
1411 if (is_wp_error($response)) {
1412 //error_log("OpenAI request failed: " . $response->get_error_message());
1413 return sanitize_text_field($user_query); // Fallback to the original query if there's an error
1414 }
1415
1416 $body = json_decode(wp_remote_retrieve_body($response), true);
1417
1418 // Check for a valid response and sanitize output
1419 if (isset($body['choices'][0]['message']['content'])) {
1420 $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
1421
1422 /*
1423 // Log the interpreted query for debugging
1424 if (defined('WP_DEBUG') && WP_DEBUG) {
1425 error_log("Interpreted search query: " . $interpreted_query);
1426 }
1427 */
1428
1429 return $interpreted_query;
1430 } else {
1431 //error_log("Unexpected API response format: " . print_r($body, true));
1432 return sanitize_text_field($user_query);
1433 }
1434 }
1435
1436 public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
1437 //error_log("ADD TO CART START - Message: $message, User ID: $user_id");
1438
1439 if (!class_exists('WooCommerce')) {
1440 //error_log("WooCommerce not available");
1441 return $this->generate_intent_response([
1442 'intent' => 'add_to_cart',
1443 'status' => 'error',
1444 'reason' => 'woocommerce_not_available'
1445 ], $session_id);
1446 }
1447
1448 $sanitized_user_id = sanitize_key($user_id);
1449 // error_log("Sanitized User ID: $sanitized_user_id");
1450 $product_id = null;
1451
1452 // If button click, use transient
1453 if ($message === '!addtocart') {
1454 //error_log("Button click detected - using transient");
1455 $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1456 //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1457 } else {
1458 // For text commands, search message first
1459 //error_log("Text command detected - searching message first");
1460 $product_id = $this->find_product_in_message($message);
1461 //error_log("Product ID from message search: " . ($product_id ?? 'null'));
1462
1463 // Only use transient as fallback if no product found in message
1464 if (!$product_id) {
1465 //error_log("No product found in message, checking transient as fallback");
1466 $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1467 //error_log("Product ID from transient fallback: " . ($product_id ?? 'null'));
1468 }
1469 }
1470
1471 // Handle no product found
1472 if (!$product_id) {
1473 //error_log("No product ID found through any method");
1474 return $this->generate_intent_response([
1475 'intent' => 'add_to_cart',
1476 'status' => 'error',
1477 'reason' => 'no_product_context',
1478 'action_needed' => 'request_product_name',
1479 'searched_message' => $message
1480 ], $session_id);
1481 }
1482
1483 // Get and verify product
1484 $product = wc_get_product($product_id);
1485 if (!$product) {
1486 //error_log("Product not found with ID: $product_id");
1487 return $this->generate_intent_response([
1488 'intent' => 'add_to_cart',
1489 'status' => 'error',
1490 'reason' => 'product_not_found',
1491 'product_id' => $product_id
1492 ], $session_id);
1493 }
1494
1495 //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1496
1497 // Add to cart
1498 $added = WC()->cart->add_to_cart($product_id);
1499 if ($added) {
1500 //error_log("Successfully added to cart: " . $product->get_name());
1501 return $this->generate_intent_response([
1502 'intent' => 'add_to_cart',
1503 'status' => 'success',
1504 'product' => [
1505 'name' => $product->get_name(),
1506 'id' => $product_id
1507 ],
1508 'cart_url' => wc_get_cart_url(),
1509 'available_actions' => [
1510 'view_cart',
1511 'checkout',
1512 'continue_shopping'
1513 ]
1514 ], $session_id);
1515 } else {
1516 //error_log("Failed to add to cart: " . $product->get_name());
1517 return $this->generate_intent_response([
1518 'intent' => 'add_to_cart',
1519 'status' => 'error',
1520 'reason' => 'add_to_cart_failed',
1521 'product' => [
1522 'name' => $product->get_name(),
1523 'id' => $product_id
1524 ]
1525 ], $session_id);
1526 }
1527 }
1528 private function find_product_in_message($message) {
1529 global $wpdb;
1530
1531 // Get embedding for the search query
1532 $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1533 if (!is_array($query_embedding)) {
1534 return null;
1535 }
1536
1537 // Get relevant content as string
1538 $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1539 if (empty($relevant_content)) {
1540 // Return null to indicate no results and set fallback response
1541 $this->fallbackResponse['text'] = "I couldn't find any relevant products based on your query. Could you please be more specific about the product you're looking for?";
1542 return null;
1543 }
1544
1545 // Extract product URLs from the content
1546 preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1547
1548 if (!empty($matches[0])) {
1549 // Try each URL found
1550 foreach ($matches[0] as $url) {
1551 // Clean the URL
1552 $url = rtrim($url, '/."\']');
1553
1554 // Get the product slug
1555 $path = parse_url($url, PHP_URL_PATH);
1556 $slug = basename(rtrim($path, '/'));
1557
1558 // Find product by slug
1559 $args = array(
1560 'post_type' => 'product',
1561 'post_status' => 'publish',
1562 'name' => $slug,
1563 'posts_per_page' => 1
1564 );
1565
1566 $products = get_posts($args);
1567
1568 if (!empty($products)) {
1569 $product_id = $products[0]->ID;
1570 $product = wc_get_product($product_id);
1571
1572 if ($product && $product->is_purchasable()) {
1573 return $product_id;
1574 }
1575 }
1576 }
1577 }
1578
1579 // Fallback: Look for product names in the content
1580 $products = wc_get_products([
1581 'status' => 'publish',
1582 'limit' => -1,
1583 'return' => 'all'
1584 ]);
1585
1586 foreach ($products as $product) {
1587 $name = $product->get_name();
1588 if (stripos($relevant_content, $name) !== false) {
1589 if ($product->is_purchasable()) {
1590 return $product->get_id();
1591 }
1592 }
1593 }
1594
1595 // If no product is found after all checks, set the fallback response
1596 $this->fallbackResponse['text'] = "I couldn't find any relevant products based on your query. Try to be more specific";
1597 return null;
1598 }
1599
1600 // New method to handle intent responses
1601 private function generate_intent_response($context_content, $session_id) {
1602 // Convert the context array to a structured string for the AI
1603 $context_string = $this->format_intent_context($context_content);
1604
1605 // Generate AI response using the context
1606 $response = $this->mxchat_generate_response(
1607 $context_string,
1608 $this->options['api_key'],
1609 $this->options['xai_api_key'],
1610 $this->options['claude_api_key'],
1611 $this->mxchat_fetch_conversation_history_for_ai($session_id)
1612 );
1613
1614 $this->fallbackResponse['text'] = $response;
1615 return true;
1616 }
1617 // Helper method to format intent context
1618 private function format_intent_context($context) {
1619 $context_string = "INTENT CONTEXT:\n";
1620
1621 switch ($context['intent']) {
1622 case 'add_to_cart':
1623 if ($context['status'] === 'success') {
1624 $context_string .= "Action: Successfully added product to cart\n";
1625 $context_string .= "Product: {$context['product']['name']}\n";
1626 $context_string .= "Available actions: " . implode(', ', $context['available_actions']) . "\n";
1627 $context_string .= "Cart URL: {$context['cart_url']}\n";
1628 $context_string .= "\nPlease inform the user of the successful addition and their available options.";
1629 } else {
1630 $context_string .= "Action: Failed to add product to cart\n";
1631 $context_string .= "Reason: {$context['reason']}\n";
1632 switch ($context['reason']) {
1633 case 'woocommerce_not_available':
1634 $context_string .= "\nPlease inform the user that shopping features are not available.";
1635 break;
1636 case 'no_product_context':
1637 $context_string .= "\nPlease ask the user to specify which product they want to add.";
1638 break;
1639 case 'product_not_found':
1640 $context_string .= "\nPlease inform the user that the product couldn't be found and ask them to try again.";
1641 break;
1642 case 'add_to_cart_failed':
1643 $context_string .= "\nPlease apologize to the user and suggest they try again or ask for assistance.";
1644 break;
1645 }
1646 }
1647 break;
1648 }
1649
1650 return $context_string;
1651 }
1652
1653
1654 public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
1655 if (!class_exists('WooCommerce')) {
1656 $this->fallbackResponse['text'] = "I apologize, but the checkout feature isn't available at the moment.";
1657 return true;
1658 }
1659
1660 // Check if cart has items
1661 if (WC()->cart->is_empty()) {
1662 $this->fallbackResponse['text'] = "Your cart is empty at the moment. Would you like to see our products?";
1663 return true;
1664 }
1665
1666 // Get cart summary
1667 $cart_count = WC()->cart->get_cart_contents_count();
1668 $cart_total = WC()->cart->get_total();
1669
1670 // Get and validate checkout URL
1671 $checkout_url = wc_get_checkout_url();
1672 if (!$checkout_url) {
1673 $this->fallbackResponse['text'] = "I'm having trouble accessing the checkout page. Please try again in a moment.";
1674 return true;
1675 }
1676
1677 wp_send_json([
1678 'text' => sprintf(
1679 "You have %d item%s in your cart totaling %s. I'll redirect you to checkout now.",
1680 $cart_count,
1681 $cart_count > 1 ? 's' : '',
1682 strip_tags($cart_total)
1683 ),
1684 'redirect_url' => esc_url_raw($checkout_url)
1685 ]);
1686 wp_die();
1687 }
1688
1689
1690 //very good
1691 private function add_email_to_loops($email) {
1692 // Sanitize the email
1693 $email = sanitize_email($email);
1694
1695 // Retrieve and sanitize options
1696 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
1697 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
1698
1699 // Check for missing API key or mailing list ID
1700 if (empty($api_key) || empty($mailing_list_id)) {
1701 //error_log('Loops API key or mailing list ID is missing.');
1702 return;
1703 }
1704
1705 $data = array(
1706 'email' => $email,
1707 'subscribed' => true,
1708 'source' => 'MxChat AI Chatbot',
1709 'mailingLists' => array($mailing_list_id => true),
1710 );
1711
1712 $url = 'https://app.loops.so/api/v1/contacts/create';
1713 $args = array(
1714 'body' => wp_json_encode($data),
1715 'headers' => array(
1716 'Authorization' => 'Bearer ' . $api_key,
1717 'Content-Type' => 'application/json',
1718 ),
1719 'method' => 'POST',
1720 'timeout' => 45,
1721 );
1722
1723 $response = wp_remote_post($url, $args);
1724
1725 // Handle errors in the API request
1726 if (is_wp_error($response)) {
1727 //error_log('Error adding email to Loops: ' . $response->get_error_message());
1728 return;
1729 }
1730
1731 // Check for non-200 HTTP responses
1732 $response_code = wp_remote_retrieve_response_code($response);
1733 if ($response_code != 200) {
1734 $response_body = wp_remote_retrieve_body($response);
1735 //error_log('Loops API responded with code ' . $response_code . ': ' . $response_body);
1736 }
1737 }
1738
1739 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
1740 // Get the maximum number of pages allowed from admin settings
1741 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
1742
1743 // Retrieve options for dynamic texts
1744 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? "Please provide the URL to the PDF you'd like to discuss.";
1745 $success_text = $this->options['pdf_intent_success_text'] ?? "I've processed the PDF. What questions do you have about it?";
1746 $error_text = $this->options['pdf_intent_error_text'] ?? "Sorry, I couldn't process the PDF. Please ensure it's a valid file.";
1747
1748 // Check for explicit request for new PDF
1749 $new_pdf_requested = stripos($message, 'new') !== false ||
1750 stripos($message, 'another') !== false ||
1751 stripos($message, 'different') !== false;
1752
1753 // If user mentions adding/reading a PDF, set waiting flag
1754 if (stripos($message, 'pdf') !== false ||
1755 stripos($message, 'document') !== false ||
1756 stripos($message, 'read') !== false) {
1757 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
1758 $this->fallbackResponse['text'] = $trigger_text;
1759 return;
1760 }
1761
1762 // If we're waiting for a URL or user requested new PDF
1763 if ($new_pdf_requested || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1764 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1765 // Process URL... (rest of your existing URL processing code)
1766 } else {
1767 $this->fallbackResponse['text'] = $trigger_text;
1768 }
1769 return;
1770 }
1771
1772 // Default to proceeding with conversation if no specific PDF action is needed
1773 $this->fallbackResponse['text'] = '';
1774 }
1775 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
1776 $upload_dir = wp_upload_dir();
1777 $temp_file = null;
1778
1779 try {
1780 // Handle URL vs local file
1781 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1782 // Validate and download the file from URL
1783 $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
1784 $response = wp_remote_get($pdf_source, ['timeout' => 60]);
1785
1786 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1787 //error_log("Failed to download PDF. Error: " . print_r($response, true));
1788 return false;
1789 }
1790
1791 file_put_contents($temp_file, wp_remote_retrieve_body($response));
1792
1793 // Validate that the downloaded file is a PDF
1794 $mime_type = mime_content_type($temp_file);
1795 if ($mime_type !== 'application/pdf') {
1796 //error_log("Invalid MIME type detected for PDF: $mime_type");
1797 unlink($temp_file);
1798 return false;
1799 }
1800 } else {
1801 // For local files, use the provided path directly
1802 $temp_file = $pdf_source;
1803 }
1804
1805 // Parse and process the PDF
1806 $parser = new \Smalot\PdfParser\Parser();
1807 $pdf = $parser->parseFile($temp_file);
1808 $pages = $pdf->getPages();
1809
1810 if (count($pages) > $max_pages) {
1811 //error_log("PDF exceeds the maximum allowed pages: " . count($pages));
1812 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1813 unlink($temp_file);
1814 }
1815 return 'too_many_pages';
1816 }
1817
1818 $embeddings = [];
1819 foreach ($pages as $page_number => $page) {
1820 $text = $page->getText();
1821
1822 // Ensure text is non-empty before generating embeddings
1823 if (empty(trim($text))) {
1824 //error_log("Skipping empty page: " . ($page_number + 1));
1825 continue;
1826 }
1827
1828 $embedding = $this->mxchat_generate_embedding(
1829 "Page " . ($page_number + 1) . ": " . $text,
1830 $this->options['api_key']
1831 );
1832
1833 if ($embedding) {
1834 $embeddings[] = [
1835 'page_number' => $page_number + 1,
1836 'embedding' => $embedding,
1837 'text' => $text,
1838 ];
1839 } else {
1840 //error_log("Failed to generate embedding for page " . ($page_number + 1));
1841 }
1842 }
1843
1844 // Clean up downloaded file if it was from URL
1845 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
1846 unlink($temp_file);
1847 }
1848
1849 return $embeddings;
1850
1851 } catch (\Exception $e) {
1852 // error_log("Error parsing or processing PDF: " . $e->getMessage());
1853
1854 // Cleanup in case of exception
1855 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1856 unlink($temp_file);
1857 }
1858
1859 return false;
1860 }
1861 }
1862 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
1863 //error_log("find_relevant_pdf_pages called.");
1864
1865 $most_relevant = null;
1866 $highest_similarity = -INF;
1867
1868 foreach ($embeddings as $page_data) {
1869 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
1870
1871 if ($similarity > $highest_similarity) {
1872 $highest_similarity = $similarity;
1873 $most_relevant = $page_data['page_number'];
1874 }
1875 }
1876
1877 if (!is_null($most_relevant)) {
1878 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
1879 return array_filter($embeddings, function ($page) use ($page_numbers) {
1880 return in_array($page['page_number'], $page_numbers);
1881 });
1882 }
1883
1884 return [];
1885 }
1886 // Add this to your class
1887 public function handle_pdf_upload() {
1888 check_ajax_referer('mxchat_chat_nonce', 'nonce');
1889
1890 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
1891 wp_send_json_error('Missing required parameters.');
1892 return;
1893 }
1894
1895 $file = $_FILES['pdf_file'];
1896 $session_id = sanitize_text_field($_POST['session_id']);
1897 $original_filename = sanitize_text_field($file['name']);
1898
1899 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
1900 if ($file_type['type'] !== 'application/pdf') {
1901 wp_send_json_error('Invalid file type. Only PDF files are allowed.');
1902 return;
1903 }
1904
1905 $upload_dir = wp_upload_dir();
1906 $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
1907 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
1908
1909 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
1910 wp_send_json_error('Failed to upload file.');
1911 return;
1912 }
1913
1914 $this->clear_pdf_transients($session_id);
1915
1916 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
1917 $embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
1918
1919 if ($embeddings === 'too_many_pages') {
1920 unlink($pdf_path);
1921 $error_message = sprintf(
1922 $this->options['pdf_intent_error_text'] ??
1923 "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.",
1924 $max_pages
1925 );
1926 wp_send_json_error($error_message);
1927 return;
1928 }
1929
1930 if ($embeddings === false || empty($embeddings)) {
1931 unlink($pdf_path);
1932 $error_message = $this->options['pdf_intent_error_text'] ??
1933 'The uploaded PDF appears to be empty or contains unsupported content.';
1934 wp_send_json_error($error_message);
1935 return;
1936 }
1937
1938 if (!empty($embeddings)) {
1939 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
1940 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
1941 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1942 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1943
1944 $success_message = $this->options['pdf_intent_success_text'] ??
1945 "I've processed the PDF. What questions do you have about it?";
1946
1947 wp_send_json_success([
1948 'message' => $success_message,
1949 'filename' => $original_filename
1950 ]);
1951 return;
1952 }
1953
1954 unlink($pdf_path);
1955 $error_message = $this->options['pdf_intent_error_text'] ??
1956 'Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.';
1957 wp_send_json_error($error_message);
1958 return;
1959 }
1960 public function handle_pdf_remove() {
1961 check_ajax_referer('mxchat_chat_nonce', 'nonce');
1962
1963 if (empty($_POST['session_id'])) {
1964 wp_send_json_error('Session ID missing.');
1965 wp_die();
1966 }
1967
1968 $session_id = sanitize_text_field($_POST['session_id']);
1969 $pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
1970
1971 if ($pdf_path && file_exists($pdf_path)) {
1972 unlink($pdf_path);
1973 }
1974
1975 $this->clear_pdf_transients($session_id);
1976
1977 wp_send_json_success([
1978 'message' => 'PDF removed successfully.'
1979 ]);
1980 wp_die();
1981 }
1982
1983
1984 /**
1985 * Calls the AI API with the provided prompt.
1986 *
1987 * @param string $prompt The prompt to send to the AI.
1988 * @return array An array containing the AI's response text.
1989 */
1990 private function mxchat_call_ai_api( $prompt ) {
1991 //error_log( 'Calling AI API with the provided prompt.' );
1992
1993 $api_key = $this->options['api_key'];
1994 if ( empty( $api_key ) ) {
1995 //error_log( 'API key is not set.' );
1996 return [ 'text' => 'API key is not set.' ];
1997 }
1998
1999 $url = 'https://api.openai.com/v1/chat/completions';
2000 $messages = [
2001 [
2002 'role' => 'system',
2003 'content' => 'You are a helpful assistant that provides concise and personalized product recommendations. Someone has asked you for some recommendations please respond very concisely appropriate for an AI chatbot.',
2004 ],
2005 [
2006 'role' => 'user',
2007 'content' => $prompt,
2008 ],
2009 ];
2010
2011 $args = [
2012 'headers' => [
2013 'Authorization' => 'Bearer ' . $api_key,
2014 'Content-Type' => 'application/json',
2015 ],
2016 'body' => wp_json_encode(
2017 [
2018 'model' => 'gpt-4o',
2019 'messages' => $messages,
2020 'temperature' => 0.7,
2021 ]
2022 ),
2023 'timeout' => 10,
2024 'method' => 'POST',
2025 ];
2026
2027 $response = wp_remote_post( $url, $args );
2028
2029 if ( is_wp_error( $response ) ) {
2030 //error_log( 'Error communicating with AI API: ' . $response->get_error_message() );
2031 return [ 'text' => 'Error communicating with AI API.' ];
2032 }
2033
2034 $body = wp_remote_retrieve_body( $response );
2035 //error_log( 'API response body: ' . $body );
2036
2037 $decoded_body = json_decode( $body, true );
2038 if ( isset( $decoded_body['choices'][0]['message']['content'] ) ) {
2039 return [ 'text' => $decoded_body['choices'][0]['message']['content'] ];
2040 } else {
2041 //error_log( 'Unexpected API response format: ' . wp_json_encode( $decoded_body ) );
2042 return [ 'text' => 'No response received from AI.' ];
2043 }
2044 }
2045 /**
2046 * Fetches the AI response for the given prompt.
2047 *
2048 * @param string $prompt The prompt to send to the AI.
2049 * @return string|null The AI's response text or null if not available.
2050 */
2051 private function mxchat_fetch_ai_response( $prompt ) {
2052 $response = $this->mxchat_call_ai_api( $prompt );
2053 return isset( $response['text'] ) ? $response['text'] : null;
2054 }
2055 /**
2056 * Generates the AI prompt for product recommendations.
2057 *
2058 * @param array $recommendations An array of product recommendations.
2059 * @return string The generated AI prompt.
2060 */
2061 private function mxchat_generate_ai_recommendation_prompt($recommendations) {
2062 $recommendation_list = '';
2063 foreach ($recommendations as $index => $rec) {
2064 $number = $index + 1;
2065 $name = $rec['name'];
2066 $price = $rec['price'];
2067 $url = $rec['url'];
2068 $image = $rec['image'];
2069 $recommendation_list .= "{$number}. Product: {$name} (Price: \\${$price})\n";
2070 $recommendation_list .= " [Link]({$url})\n";
2071 $recommendation_list .= " ![Image]({$image})\n\n";
2072 }
2073
2074 $prompt = "Based on the following list of products, generate a unique, friendly, and personalized response to a user. ";
2075 $prompt .= "If some products aren't exactly what the user asked for but share similar styles or patterns, acknowledge this and explain why you're suggesting them. ";
2076 $prompt .= "For each product, provide a brief justification that clearly explains why it's relevant, especially if it's a different type of product than requested. ";
2077 $prompt .= "Please number your responses to match the product numbers.\n\n";
2078 $prompt .= "Products:\n\n{$recommendation_list}";
2079 $prompt .= "Please ensure that the number of each product matches the order in which the products are listed.";
2080
2081 return $prompt;
2082 }
2083 private function mxchat_generate_recommendations($user_id, $message) {
2084 // 1. Gather user context
2085 $user_context = $this->mxchat_get_user_context($user_id);
2086
2087 // Get cart item IDs for filtering
2088 $cart_item_ids = [];
2089 if (!empty($user_context['cart_items'])) {
2090 $cart_item_ids = array_map(function($item) {
2091 return $item['product_id'];
2092 }, $user_context['cart_items']);
2093 }
2094
2095 // 2. Get AI recommendation based on context
2096 $ai_suggestion = $this->mxchat_get_ai_shopping_suggestion($message, $user_context);
2097 //error_log("AI Shopping Suggestion: " . $ai_suggestion);
2098
2099 // 3. Generate embedding for the AI suggestion
2100 $suggestion_embedding = $this->mxchat_generate_embedding($ai_suggestion, $this->options['api_key']);
2101 if (!$suggestion_embedding) {
2102 //error_log("Could not generate embedding for AI suggestion");
2103 return ['recommendations' => [], 'sources' => []];
2104 }
2105
2106 // 4. Use existing relevant content function to find matches
2107 $relevant_content = $this->mxchat_find_relevant_content($suggestion_embedding);
2108
2109 // 5. Extract product URLs from the relevant content
2110 preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
2111
2112 $found_products = [];
2113 if (!empty($matches[0])) {
2114 foreach ($matches[0] as $url) {
2115 // Clean the URL
2116 $url = rtrim($url, '/."\']');
2117
2118 // Get the product slug
2119 $path = parse_url($url, PHP_URL_PATH);
2120 $slug = basename(rtrim($path, '/'));
2121
2122 // Find product by slug
2123 $args = array(
2124 'post_type' => 'product',
2125 'post_status' => 'publish',
2126 'name' => $slug,
2127 'posts_per_page' => 1
2128 );
2129
2130 $products = get_posts($args);
2131
2132 if (!empty($products)) {
2133 $product_id = $products[0]->ID;
2134
2135 // Skip if product is in cart
2136 if (in_array($product_id, $cart_item_ids)) {
2137 //error_log("Skipping product ID $product_id - already in cart");
2138 continue;
2139 }
2140
2141 $product = wc_get_product($product_id);
2142
2143 if ($product && $product->is_purchasable() && !in_array($product_id, array_map(function($p) {
2144 return $p->get_id();
2145 }, $found_products))) {
2146 $found_products[] = $product;
2147 }
2148 }
2149 }
2150 }
2151
2152 // If no products found by URLs, look for product names in the content
2153 if (empty($found_products)) {
2154 $products = wc_get_products([
2155 'status' => 'publish',
2156 'limit' => -1,
2157 'return' => 'objects'
2158 ]);
2159
2160 foreach ($products as $product) {
2161 // Skip if product is in cart
2162 if (in_array($product->get_id(), $cart_item_ids)) {
2163 continue;
2164 }
2165
2166 $name = $product->get_name();
2167 if (stripos($relevant_content, $name) !== false) {
2168 if ($product->is_purchasable() && !in_array($product->get_id(), array_map(function($p) {
2169 return $p->get_id();
2170 }, $found_products))) {
2171 $found_products[] = $product;
2172 }
2173 }
2174 }
2175 }
2176
2177 // Keep searching until we have 4 products or run out of options
2178 $formatted_recommendations = [];
2179 foreach ($found_products as $product) {
2180 if (count($formatted_recommendations) >= 4) break;
2181
2182 $formatted_recommendations[] = [
2183 'name' => $product->get_name(),
2184 'price' => $product->get_price(),
2185 'url' => get_permalink($product->get_id()),
2186 'image' => wp_get_attachment_url($product->get_image_id())
2187 ];
2188 }
2189
2190 return [
2191 'recommendations' => $formatted_recommendations,
2192 'sources' => ['AI-powered personalized recommendations']
2193 ];
2194 }
2195 private function mxchat_get_ai_shopping_suggestion($message, $user_context) {
2196 $prompt = "As a shopping assistant, analyze this user's context and generate a specific product search suggestion. ";
2197 $prompt .= "User's Question: \"{$message}\"\n\n";
2198
2199 if (!empty($user_context['order_history'])) {
2200 $prompt .= "Their recent orders include:\n";
2201 foreach ($user_context['order_history'] as $order) {
2202 $prompt .= "- {$order['name']} ({$order['date']})\n";
2203 }
2204 }
2205
2206 if (!empty($user_context['cart_items'])) {
2207 $prompt .= "\nThey currently have in their cart:\n";
2208 foreach ($user_context['cart_items'] as $item) {
2209 $prompt .= "- {$item['name']}\n";
2210 }
2211 }
2212
2213 $prompt .= "\nBased on their question and history, suggest a specific search query that would help find the most relevant products. ";
2214 $prompt .= "Respond with ONLY the search query, nothing else.";
2215
2216 $response = $this->mxchat_call_ai_api($prompt);
2217 return isset($response['text']) ? trim($response['text']) : $message;
2218 }
2219 public function mxchat_handle_product_recommendations($message, $user_id, $session_id) {
2220 try {
2221 //error_log("Starting product recommendations for user: $user_id, session: $session_id");
2222
2223 // Pass the message to get context-aware recommendations
2224 $recommendation_data = $this->mxchat_generate_recommendations($user_id, $message);
2225 //error_log('Generated recommendation data: ' . wp_json_encode($recommendation_data));
2226
2227 if (empty($recommendation_data['recommendations'])) {
2228 //error_log("No recommendations found for user: $user_id");
2229 $this->fallbackResponse = [
2230 'text' => __("I couldn't find any product recommendations for you right now. Please try again later!", 'mxchat'),
2231 ];
2232 return;
2233 }
2234
2235 // Remove duplicates and limit to top 4 recommendations
2236 $unique_recommendations = [];
2237 foreach ($recommendation_data['recommendations'] as $rec) {
2238 $unique_recommendations[$rec['url']] = $rec;
2239 }
2240
2241 $unique_recommendations = array_slice($unique_recommendations, 0, 4);
2242 //error_log('Top 4 recommendations: ' . wp_json_encode($unique_recommendations));
2243
2244 $recommendations_summary = [];
2245 foreach ($unique_recommendations as $rec) {
2246 $recommendations_summary[] = [
2247 'name' => $rec['name'],
2248 'price' => strip_tags($rec['price']),
2249 'url' => $rec['url'],
2250 'image' => $rec['image'],
2251 ];
2252 }
2253
2254 // Generate AI prompt and fetch response
2255 $ai_prompt = $this->mxchat_generate_ai_recommendation_prompt($recommendations_summary);
2256 $ai_response = $this->mxchat_fetch_ai_response($ai_prompt);
2257
2258 if (empty($ai_response)) {
2259 $this->fallbackResponse = [
2260 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2261 ];
2262 return;
2263 }
2264
2265 // Split the AI's response into lines
2266 $ai_lines = preg_split('/\r\n|\r|\n/', $ai_response);
2267
2268 // Initialize variables
2269 $formatted_response = '';
2270 $justifications = [];
2271 $current_number = 0;
2272 $in_introduction = true;
2273 $introduction = '';
2274
2275 // Parse the AI response to separate the introduction and the justifications
2276 foreach ($ai_lines as $line) {
2277 if (preg_match('/^\s*(\d+)\.\s*(.*)$/', $line, $matches)) {
2278 // This line is a numbered justification
2279 $current_number = intval($matches[1]) - 1;
2280 $justifications[$current_number] = $matches[2];
2281 $in_introduction = false;
2282 } elseif ($in_introduction) {
2283 // This line is part of the introduction
2284 $introduction .= $line . ' ';
2285 } else {
2286 // This line is a continuation of the current justification
2287 if (isset($justifications[$current_number])) {
2288 $justifications[$current_number] .= ' ' . $line;
2289 }
2290 }
2291 }
2292
2293 // Build the formatted response
2294 if (!empty($introduction)) {
2295 $formatted_response .= esc_html(trim($introduction)) . "<br><br>";
2296 }
2297
2298 foreach ($recommendations_summary as $index => $rec) {
2299 $name = esc_html($rec['name']);
2300 $price = number_format((float)$rec['price'], 2, '.', '');
2301 $url = esc_url($rec['url']);
2302 $image = esc_url($rec['image']);
2303
2304 $formatted_response .= ($index + 1) . ". <strong>" . $name . "</strong> - <strong>$" . $price . "</strong><br>";
2305 $formatted_response .= "<img src=\"{$image}\" alt=\"{$name}\" style=\"max-width: 200px; height: auto; display: block; margin: 10px 0;\" /><br>";
2306
2307 if (isset($justifications[$index])) {
2308 $formatted_response .= "<em>" . esc_html(trim($justifications[$index])) . "</em><br><br>";
2309 }
2310 }
2311
2312 $this->fallbackResponse = [
2313 'text' => $formatted_response,
2314 ];
2315 //error_log('Final formatted response set.');
2316 } catch (Exception $e) {
2317 //error_log('Error in mxchat_handle_product_recommendations: ' . $e->getMessage());
2318 $this->fallbackResponse = [
2319 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2320 ];
2321 }
2322 }
2323 private function mxchat_get_user_context($user_id) {
2324 $context = [
2325 'order_history' => [],
2326 'cart_items' => [],
2327 'recently_viewed' => [],
2328 ];
2329
2330 // Get order history
2331 if (is_user_logged_in() && $user_id) {
2332 $orders = wc_get_orders([
2333 'customer_id' => $user_id,
2334 'limit' => 5, // Last 5 orders
2335 'orderby' => 'date',
2336 'order' => 'DESC',
2337 ]);
2338
2339 foreach ($orders as $order) {
2340 foreach ($order->get_items() as $item) {
2341 $context['order_history'][] = [
2342 'name' => $item->get_name(),
2343 'product_id' => $item->get_product_id(),
2344 'date' => $order->get_date_created()->format('Y-m-d')
2345 ];
2346 }
2347 }
2348 }
2349
2350 // Get cart items
2351 if (WC()->cart && WC()->cart->get_cart_contents_count() > 0) {
2352 foreach (WC()->cart->get_cart() as $cart_item) {
2353 $product = wc_get_product($cart_item['product_id']);
2354 if ($product) {
2355 $context['cart_items'][] = [
2356 'name' => $product->get_name(),
2357 'product_id' => $product->get_id()
2358 ];
2359 }
2360 }
2361 }
2362
2363 // Get recently viewed items from session
2364 $viewed_products = WC()->session->get('recently_viewed_products', []);
2365 foreach ($viewed_products as $product_id) {
2366 $product = wc_get_product($product_id);
2367 if ($product) {
2368 $context['recently_viewed'][] = [
2369 'name' => $product->get_name(),
2370 'product_id' => $product->get_id()
2371 ];
2372 }
2373 }
2374
2375 return $context;
2376 }
2377
2378
2379
2380
2381
2382
2383 function mxchat_fetch_new_messages() {
2384 $session_id = sanitize_text_field($_POST['session_id']);
2385 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2386 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
2387 $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2388
2389 if (empty($session_id)) {
2390 //error_log('Fetch new messages error: Session ID missing.');
2391 wp_send_json_error(['message' => 'Session ID missing.']);
2392 wp_die();
2393 }
2394
2395 $history = get_option("mxchat_history_{$session_id}", []);
2396
2397 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2398 // If persistence is enabled, show all new messages
2399 if ($persistence_enabled) {
2400 return !empty($message['id']) &&
2401 strcmp($message['id'], $last_seen_id) > 0 &&
2402 $message['role'] === 'agent';
2403 }
2404
2405 // If persistence is disabled, only show messages after initial timestamp
2406 return !empty($message['id']) &&
2407 $message['role'] === 'agent' &&
2408 $message['timestamp'] > $initial_timestamp;
2409 });
2410
2411 //error_log("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id");
2412
2413 wp_send_json_success([
2414 'new_messages' => array_values($new_messages)
2415 ]);
2416 wp_die();
2417 }
2418
2419
2420 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2421 // First check if live agents are available
2422 $live_agent_available = $this->options['live_agent_status'] ?? 'offline';
2423
2424 if ($live_agent_available !== 'online') {
2425 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2426
2427 $this->fallbackResponse = [
2428 'text' => $away_message,
2429 'html' => '',
2430 'images' => [],
2431 'chat_mode' => 'ai'
2432 ];
2433
2434 //error_log('Live agent handover attempted but agents are offline');
2435
2436 wp_send_json([
2437 'text' => $away_message,
2438 'html' => '',
2439 'chat_mode' => 'ai',
2440 'session_id' => $session_id
2441 ]);
2442 wp_die();
2443 }
2444
2445 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2446 //error_log('Slack Webhook URL retrieved: ' . $slack_webhook_url);
2447
2448 if (empty($slack_webhook_url)) {
2449 //error_log('Slack Webhook URL is not configured.');
2450 return false;
2451 }
2452
2453 update_option("mxchat_mode_{$session_id}", 'agent');
2454
2455 $webhook_data = [
2456 'blocks' => [
2457 [
2458 'type' => 'header',
2459 'text' => [
2460 'type' => 'plain_text',
2461 'text' => '🔔 New Live Agent Request',
2462 'emoji' => true
2463 ]
2464 ],
2465 [
2466 'type' => 'section',
2467 'fields' => [
2468 [
2469 'type' => 'mrkdwn',
2470 'text' => "*User ID:*\n`$user_id`"
2471 ],
2472 [
2473 'type' => 'mrkdwn',
2474 'text' => "*Session ID:*\n`$session_id`"
2475 ]
2476 ]
2477 ],
2478 [
2479 'type' => 'section',
2480 'text' => [
2481 'type' => 'mrkdwn',
2482 'text' => "*Initial Message:*\n$message"
2483 ]
2484 ],
2485 [
2486 'type' => 'actions',
2487 'elements' => [
2488 [
2489 'type' => 'button',
2490 'text' => [
2491 'type' => 'plain_text',
2492 'text' => '✍️ Reply',
2493 'emoji' => true
2494 ],
2495 'value' => $session_id,
2496 'action_id' => 'reply_to_user',
2497 'style' => 'primary'
2498 ]
2499 ]
2500 ]
2501 ]
2502 ];
2503
2504 $response = wp_remote_post($slack_webhook_url, [
2505 'body' => json_encode($webhook_data),
2506 'headers' => [
2507 'Content-Type' => 'application/json',
2508 ],
2509 ]);
2510
2511 if (is_wp_error($response)) {
2512 //error_log('Error sending live agent handover: ' . $response->get_error_message());
2513 return false;
2514 }
2515
2516 //error_log('Live agent handover triggered successfully.');
2517
2518 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2519
2520 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2521
2522 $this->fallbackResponse = [
2523 'text' => $success_message,
2524 'html' => '',
2525 'images' => [],
2526 'chat_mode' => 'agent'
2527 ];
2528
2529 wp_send_json([
2530 'success' => true,
2531 'text' => $success_message,
2532 'html' => '',
2533 'chat_mode' => 'agent',
2534 'session_id' => $session_id,
2535 'fallbackResponse' => $this->fallbackResponse
2536 ]);
2537 wp_die();
2538 }
2539 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2540 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2541
2542 if (empty($slack_webhook_url)) {
2543 //error_log('Slack Webhook URL is not configured.');
2544 return false;
2545 }
2546
2547 $webhook_data = [
2548 'blocks' => [
2549 [
2550 'type' => 'header',
2551 'text' => [
2552 'type' => 'plain_text',
2553 'text' => '📩 New Chat Message',
2554 'emoji' => true
2555 ]
2556 ],
2557 [
2558 'type' => 'section',
2559 'fields' => [
2560 [
2561 'type' => 'mrkdwn',
2562 'text' => "*User ID:*\n`$user_id`"
2563 ],
2564 [
2565 'type' => 'mrkdwn',
2566 'text' => "*Session ID:*\n`$session_id`"
2567 ]
2568 ]
2569 ],
2570 [
2571 'type' => 'section',
2572 'text' => [
2573 'type' => 'mrkdwn',
2574 'text' => "*Message:*\n$message"
2575 ]
2576 ],
2577 [
2578 'type' => 'actions',
2579 'elements' => [
2580 [
2581 'type' => 'button',
2582 'text' => [
2583 'type' => 'plain_text',
2584 'text' => '✍️ Reply',
2585 'emoji' => true
2586 ],
2587 'value' => $session_id,
2588 'action_id' => 'reply_to_user',
2589 'style' => 'primary'
2590 ]
2591 ]
2592 ]
2593 ]
2594 ];
2595
2596 $response = wp_remote_post($slack_webhook_url, [
2597 'body' => json_encode($webhook_data),
2598 'headers' => [
2599 'Content-Type' => 'application/json',
2600 ],
2601 ]);
2602
2603 if (is_wp_error($response)) {
2604 //error_log('Error sending message to Slack: ' . $response->get_error_message());
2605 return false;
2606 }
2607
2608 //error_log('Message sent to Slack successfully.');
2609 return true;
2610 }
2611 public function handle_slack_interaction(WP_REST_Request $request) {
2612 //error_log('Received Slack interaction');
2613
2614 $payload = json_decode($request->get_param('payload'), true);
2615 //error_log('Payload: ' . print_r($payload, true));
2616
2617 // Handle button click
2618 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
2619 $session_id = $payload['actions'][0]['value'];
2620 $trigger_id = $payload['trigger_id'];
2621
2622 // Get Bot Token from settings
2623 $slack_token = $this->options['live_agent_bot_token'] ?? '';
2624
2625 if (empty($slack_token)) {
2626 //error_log('Slack Bot Token not configured');
2627 return new WP_REST_Response(['error' => 'Bot token not configured'], 400);
2628 }
2629 $response = wp_remote_post('https://slack.com/api/views.open', [
2630 'headers' => [
2631 'Content-Type' => 'application/json',
2632 'Authorization' => 'Bearer ' . $slack_token
2633 ],
2634 'body' => json_encode([
2635 'trigger_id' => $trigger_id,
2636 'view' => [
2637 'type' => 'modal',
2638 'callback_id' => 'reply_modal',
2639 'title' => [
2640 'type' => 'plain_text',
2641 'text' => 'Reply to User'
2642 ],
2643 'submit' => [
2644 'type' => 'plain_text',
2645 'text' => 'Send'
2646 ],
2647 'close' => [
2648 'type' => 'plain_text',
2649 'text' => 'Cancel'
2650 ],
2651 'blocks' => [
2652 [
2653 'type' => 'input',
2654 'block_id' => 'reply_block',
2655 'label' => [
2656 'type' => 'plain_text',
2657 'text' => "Reply to session: $session_id"
2658 ],
2659 'element' => [
2660 'type' => 'plain_text_input',
2661 'action_id' => 'message',
2662 'multiline' => true,
2663 'placeholder' => [
2664 'type' => 'plain_text',
2665 'text' => 'Type your message here...'
2666 ]
2667 ]
2668 ]
2669 ],
2670 'private_metadata' => $session_id
2671 ]
2672 ])
2673 ]);
2674
2675 //error_log('Views.open response: ' . print_r($response, true));
2676
2677 // Return immediate acknowledgment
2678 return new WP_REST_Response(['ok' => true]);
2679 }
2680
2681 // Handle modal submission
2682 if ($payload['type'] === 'view_submission') {
2683 $session_id = $payload['view']['private_metadata'];
2684 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
2685
2686 // Save the message
2687 $this->mxchat_save_chat_message($session_id, 'agent', $message);
2688
2689 return new WP_REST_Response([
2690 'response_action' => 'clear'
2691 ]);
2692 }
2693
2694 // Default acknowledgment
2695 return new WP_REST_Response(['ok' => true]);
2696 }
2697
2698 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2699 //error_log('Received agent response request');
2700 //error_log('Request data: ' . print_r($request->get_params(), true));
2701 // error_log('Raw body: ' . file_get_contents('php://input'));
2702
2703 // Get the data from Slack's slash command format
2704 $command_text = $request->get_param('text');
2705 // error_log('Command text: ' . $command_text);
2706
2707 if (empty($command_text)) {
2708 error_log('Agent response error: No command text received');
2709 return new WP_REST_Response([
2710 'error' => 'Command text is required. Format: /reply session_id message'
2711 ], 400);
2712 }
2713
2714 // Split the command text into session_id and message
2715 $parts = explode(' ', $command_text, 2);
2716 if (count($parts) !== 2) {
2717 //error_log('Agent response error: Invalid command format');
2718 return new WP_REST_Response([
2719 'error' => 'Invalid format. Use: /reply session_id message'
2720 ], 400);
2721 }
2722
2723 $session_id = sanitize_text_field($parts[0]);
2724 $message = sanitize_text_field($parts[1]);
2725
2726 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
2727
2728 // Save the message
2729 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2730
2731 if (!$message_id) {
2732 // error_log('Failed to save agent message');
2733 return new WP_REST_Response([
2734 'error' => 'Failed to save message'
2735 ], 500);
2736 }
2737
2738 // Return success response in Slack's expected format
2739 return new WP_REST_Response([
2740 'response_type' => 'in_channel',
2741 'text' => "Message sent successfully to session $session_id"
2742 ], 200);
2743 }
2744
2745
2746 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2747 //error_log("Switching back to chatbot mode via intent.");
2748
2749 // Just update mode to AI
2750 update_option("mxchat_mode_{$session_id}", 'ai');
2751
2752 // Initialize states
2753 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2754 $this->productCardHtml = '';
2755
2756 // Set the response message
2757 $this->fallbackResponse['text'] = 'You are now chatting with the AI chatbot.';
2758
2759 return true; // Intent was handled
2760 }
2761
2762
2763
2764
2765 // For the word upload handler
2766 public function mxchat_handle_word_upload() {
2767 // Delegate to word handler
2768 $this->word_handler->mxchat_handle_word_upload();
2769 }
2770
2771 // For the word removal handler
2772 public function mxchat_handle_word_remove() {
2773 // Delegate to word handler
2774 $this->word_handler->mxchat_handle_word_remove();
2775 }
2776
2777 // For the word status check
2778 public function mxchat_check_word_status() {
2779 // Delegate to word handler
2780 $this->word_handler->mxchat_check_word_status();
2781 }
2782
2783
2784 private function mxchat_get_user_identifier() {
2785 return MxChat_User::mxchat_get_user_identifier();
2786 }
2787
2788 private function mxchat_generate_embedding($text, $api_key) {
2789 $endpoint = 'https://api.openai.com/v1/embeddings';
2790
2791 $body = wp_json_encode([
2792 'input' => $text,
2793 'model' => 'text-embedding-ada-002'
2794 ]);
2795
2796 $args = [
2797 'body' => $body,
2798 'headers' => [
2799 'Content-Type' => 'application/json',
2800 'Authorization' => 'Bearer ' . $api_key,
2801 ],
2802 'timeout' => 60,
2803 'redirection' => 5,
2804 'blocking' => true,
2805 'httpversion' => '1.0',
2806 'sslverify' => true,
2807 ];
2808
2809 $response = wp_remote_post($endpoint, $args);
2810
2811 if (is_wp_error($response)) {
2812 return null;
2813 }
2814
2815 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2816
2817 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
2818 return $response_body['data'][0]['embedding'];
2819 } else {
2820 return null;
2821 }
2822 }
2823
2824 private function mxchat_find_relevant_content($user_embedding) {
2825 global $wpdb;
2826 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2827 $cache_key = 'mxchat_system_prompt_embeddings';
2828
2829 // Retrieve embeddings from cache or database
2830 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2831 if ($embeddings === false) {
2832 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
2833 $embeddings = $wpdb->get_results($query);
2834 if ($embeddings === null || empty($embeddings)) {
2835 return ''; // Return an empty string if no embeddings found
2836 }
2837 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2838 }
2839
2840 // Initialize array to store relevant results with similarity scores
2841 $relevant_results = [];
2842
2843 // Iterate through embeddings to calculate similarity
2844 foreach ($embeddings as $embedding) {
2845 $database_embedding = $embedding->embedding_vector
2846 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2847 : null;
2848
2849 if (is_array($database_embedding) && is_array($user_embedding)) {
2850 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2851 $relevant_results[] = [
2852 'id' => $embedding->id,
2853 'similarity' => $similarity
2854 ];
2855 }
2856 }
2857
2858 // Retrieve the similarity threshold
2859 $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2860
2861 // Filter and sort relevant results by similarity
2862 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2863 return $result['similarity'] >= $similarity_threshold;
2864 });
2865 usort($relevant_results, function ($a, $b) {
2866 return $b['similarity'] <=> $a['similarity'];
2867 });
2868
2869 // Limit to the top 5 results
2870 $top_results = array_slice($relevant_results, 0, 5);
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
2879 // Check if the content is PDF-related and add surrounding pages
2880 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2881 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2882 "SELECT 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
2891 // Add previous content if it exists
2892 if (!empty($surrounding_content[0])) {
2893 $content .= $surrounding_content[0]->article_content . "\n\n";
2894 }
2895
2896 // Add the main chunk content
2897 $content .= $chunk_content . "\n\n";
2898
2899 // Add next content if it exists
2900 if (!empty($surrounding_content[1])) {
2901 $content .= $surrounding_content[1]->article_content . "\n\n";
2902 }
2903 } else {
2904 // For non-PDF content, add directly
2905 $content .= $chunk_content . "\n\n";
2906 }
2907 }
2908
2909 return trim($content); // Return the combined content
2910 }
2911
2912 private function mxchat_find_relevant_products($user_embedding) {
2913 global $wpdb;
2914 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2915 $cache_key = 'mxchat_system_prompt_embeddings';
2916
2917 // Retrieve embeddings from cache or database
2918 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2919 if ($embeddings === false) {
2920 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
2921 $embeddings = $wpdb->get_results($query);
2922 if ($embeddings === null || empty($embeddings)) {
2923 return ''; // Return an empty string if no embeddings found
2924 }
2925 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2926 }
2927
2928 // Initialize array to store relevant results with similarity scores
2929 $relevant_results = [];
2930
2931 // Iterate through embeddings to calculate similarity
2932 foreach ($embeddings as $embedding) {
2933 $database_embedding = $embedding->embedding_vector
2934 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2935 : null;
2936
2937 if (is_array($database_embedding) && is_array($user_embedding)) {
2938 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2939 $relevant_results[] = [
2940 'id' => $embedding->id,
2941 'similarity' => $similarity
2942 ];
2943 }
2944 }
2945
2946 // Use a fixed similarity threshold of 0.85
2947 $similarity_threshold = 0.85;
2948
2949 // Filter and sort relevant results by similarity
2950 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2951 return $result['similarity'] >= $similarity_threshold;
2952 });
2953 usort($relevant_results, function ($a, $b) {
2954 return $b['similarity'] <=> $a['similarity'];
2955 });
2956
2957 // Limit to the top 5 results
2958 $top_results = array_slice($relevant_results, 0, 5);
2959
2960 // Initialize the final content
2961 $content = '';
2962
2963 // Fetch and combine content for the top results
2964 foreach ($top_results as $result) {
2965 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2966
2967 // Check if the content is PDF-related and add surrounding pages
2968 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2969 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2970 "SELECT article_content FROM {$system_prompt_table}
2971 WHERE id IN (
2972 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
2973 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
2974 )",
2975 $result['id'],
2976 $result['id']
2977 ));
2978
2979 // Add previous content if it exists
2980 if (!empty($surrounding_content[0])) {
2981 $content .= $surrounding_content[0]->article_content . "\n\n";
2982 }
2983
2984 // Add the main chunk content
2985 $content .= $chunk_content . "\n\n";
2986
2987 // Add next content if it exists
2988 if (!empty($surrounding_content[1])) {
2989 $content .= $surrounding_content[1]->article_content . "\n\n";
2990 }
2991 } else {
2992 // For non-PDF content, add directly
2993 $content .= $chunk_content . "\n\n";
2994 }
2995 }
2996
2997 return trim($content); // Return the combined content
2998 }
2999
3000 private function fetch_content_with_product_links($most_relevant_id) {
3001 global $wpdb;
3002 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3003
3004 // Fetch the article content and associated product URL
3005 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
3006 $result = $wpdb->get_row($query);
3007
3008 if ($result) {
3009 // Append the product link to the content if available
3010 $content = $result->article_content;
3011 if (!empty($result->source_url)) {
3012 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
3013 }
3014 return $content;
3015 }
3016
3017 return null;
3018 }
3019
3020 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $conversation_history) {
3021 if (!$relevant_content) {
3022 return "I'm sorry, I couldn't find relevant information on that topic.";
3023 }
3024
3025 // Check the selected model
3026 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-3.5-turbo';
3027
3028 // Call the appropriate function based on the selected model
3029 if (strpos($selected_model, 'claude') !== false) {
3030 return $this->mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content);
3031 } elseif ($selected_model === 'grok-beta') {
3032 return $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content);
3033 } else {
3034 return $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content);
3035 }
3036 }
3037
3038 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3039 // Get system prompt instructions from options
3040 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3041
3042 // Add system prompt to relevant content
3043 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3044
3045 // Prepend system instructions to the conversation history
3046 array_unshift($conversation_history, [
3047 'role' => 'system',
3048 'content' => "Here are your instructions: " . $content_with_instructions
3049 ]);
3050
3051 // Log the system prompt and relevant content
3052 //error_log("System Prompt Instructions: " . $system_prompt_instructions);
3053 //error_log("Relevant Content: " . substr($relevant_content, 0, 500)); // Log first 500 characters for brevity
3054
3055 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3056 foreach ($conversation_history as &$message) {
3057 if ($message['role'] === 'bot') {
3058 $message['role'] = 'assistant';
3059 } elseif ($message['role'] === 'agent') {
3060 // Tag the message as coming from a live agent
3061 $message['role'] = 'assistant';
3062 if (!isset($message['metadata'])) {
3063 $message['metadata'] = ['source' => 'live_agent'];
3064 }
3065 }
3066
3067 // Ensure all roles are valid
3068 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3069 $message['role'] = 'user'; // Default to 'user'
3070 }
3071 }
3072
3073 // Log the formatted conversation history
3074 //error_log("Conversation History: " . json_encode($conversation_history, JSON_PRETTY_PRINT));
3075
3076 // Build the request body
3077 $body = json_encode([
3078 'model' => $selected_model,
3079 'messages' => $conversation_history,
3080 'temperature' => 0.8,
3081 'stream' => false
3082 ]);
3083
3084 // Log the full API request body
3085 //error_log("OpenAI API Request Body: " . $body);
3086
3087 // Set up the API request
3088 $args = [
3089 'body' => $body,
3090 'headers' => [
3091 'Content-Type' => 'application/json',
3092 'Authorization' => 'Bearer ' . $api_key,
3093 ],
3094 'timeout' => 60,
3095 'redirection' => 5,
3096 'blocking' => true,
3097 'httpversion' => '1.0',
3098 'sslverify' => true,
3099 ];
3100
3101 // Make the API request
3102 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3103
3104 // Log the response or error
3105 if (is_wp_error($response)) {
3106 //error_log("OpenAI API Error: " . $response->get_error_message());
3107 return "Sorry, there was an error processing your request.";
3108 }
3109
3110 // Log the raw API response
3111 //error_log("OpenAI API Raw Response: " . print_r($response, true));
3112
3113 $response_body = wp_remote_retrieve_body($response);
3114 $decoded_response = json_decode($response_body, true);
3115
3116 // Log the decoded API response
3117 error_log("OpenAI API Decoded Response: " . json_encode($decoded_response, JSON_PRETTY_PRINT));
3118
3119 if (isset($decoded_response['choices'][0]['message']['content'])) {
3120 $response_text = trim($decoded_response['choices'][0]['message']['content']);
3121 //error_log("OpenAI API Final Response: " . $response_text);
3122 return $response_text;
3123 } else {
3124 // Log an error if the expected response format is missing
3125 //error_log("OpenAI API Response Format Error: Expected 'choices[0][message][content]' not found.");
3126 return "Sorry, I couldn't process that request.";
3127 }
3128 }
3129
3130 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3131 // Get system prompt instructions from options
3132 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3133
3134 // Add system prompt to relevant content
3135 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3136
3137 // Prepend system instructions to the conversation history
3138 array_unshift($conversation_history, [
3139 'role' => 'system',
3140 'content' => "Here are your instructions: " . $content_with_instructions
3141 ]);
3142
3143 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3144 foreach ($conversation_history as &$message) {
3145 if ($message['role'] === 'bot') {
3146 $message['role'] = 'assistant';
3147 } elseif ($message['role'] === 'agent') {
3148 // Tag the message as coming from a live agent
3149 $message['role'] = 'assistant';
3150 if (!isset($message['metadata'])) {
3151 $message['metadata'] = ['source' => 'live_agent'];
3152 }
3153 }
3154
3155 // Ensure all roles are valid
3156 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3157 $message['role'] = 'user'; // Default to 'user'
3158 }
3159 }
3160
3161
3162 // Build the request body
3163 $body = json_encode([
3164 'model' => $selected_model,
3165 'messages' => $conversation_history,
3166 'temperature' => 0.8,
3167 'stream' => false
3168 ]);
3169
3170 // Set up the API request
3171 $args = [
3172 'body' => $body,
3173 'headers' => [
3174 'Content-Type' => 'application/json',
3175 'Authorization' => 'Bearer ' . $xai_api_key,
3176 ],
3177 'timeout' => 60,
3178 'redirection' => 5,
3179 'blocking' => true,
3180 'httpversion' => '1.0',
3181 'sslverify' => true,
3182 ];
3183
3184 // Make the API request
3185 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3186
3187 // Process the response
3188 if (is_wp_error($response)) {
3189 return "Sorry, there was an error processing your request.";
3190 }
3191
3192 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3193
3194 if (isset($response_body['choices'][0]['message']['content'])) {
3195 return trim($response_body['choices'][0]['message']['content']);
3196 } else {
3197 return "Sorry, I couldn't process that request.";
3198 }
3199 }
3200
3201 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3202 // Get system prompt instructions from options for Claude's top-level system parameter
3203 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3204
3205 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3206 foreach ($conversation_history as &$message) {
3207 if ($message['role'] === 'bot') {
3208 $message['role'] = 'assistant';
3209 } elseif ($message['role'] === 'agent') {
3210 // Tag the message as coming from a live agent
3211 $message['role'] = 'assistant';
3212 if (!isset($message['metadata'])) {
3213 $message['metadata'] = ['source' => 'live_agent'];
3214 }
3215 }
3216
3217 // Ensure all roles are valid
3218 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3219 $message['role'] = 'user'; // Default to 'user'
3220 }
3221 }
3222
3223 // Add relevant content as the latest user message in conversation history
3224 $conversation_history[] = [
3225 'role' => 'user',
3226 'content' => $relevant_content
3227 ];
3228
3229 // Build the request body with Claude's expected structure, using system instructions as a top-level parameter
3230 $body = json_encode([
3231 'model' => $selected_model,
3232 'max_tokens' => 1000,
3233 'temperature' => 0.8,
3234 'system' => $system_prompt_instructions, // Set the system prompt at the top level as required
3235 'messages' => $conversation_history
3236 ]);
3237
3238 // Set up the API request with the necessary headers
3239 $args = [
3240 'body' => $body,
3241 'headers' => [
3242 'Content-Type' => 'application/json',
3243 'x-api-key' => $claude_api_key,
3244 'anthropic-version' => '2023-06-01',
3245 ],
3246 'timeout' => 60,
3247 'redirection' => 5,
3248 'blocking' => true,
3249 'httpversion' => '1.0',
3250 'sslverify' => true,
3251 ];
3252
3253 // Make the API request
3254 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
3255
3256 /*
3257 // Check for errors and log the response for debugging
3258 if (is_wp_error($response)) {
3259 error_log("Claude API request error: " . print_r($response->get_error_message(), true));
3260 return "Sorry, there was an error processing your request.";
3261 }
3262 */
3263
3264
3265 // Decode the response and parse according to the expected Claude response structure
3266 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3267 //error_log("Claude API response: " . print_r($response_body, true));
3268
3269 // Check if the response has the expected 'content' array with 'text' blocks
3270 if (isset($response_body['content'][0]['text'])) {
3271 return trim($response_body['content'][0]['text']);
3272 } else {
3273 return "Sorry, I couldn't process that request.";
3274 }
3275 }
3276
3277 public function mxchat_dismiss_pre_chat_message() {
3278 // Get and sanitize the user identifier
3279 $user_id = $this->mxchat_get_user_identifier();
3280 $user_id = sanitize_key($user_id);
3281
3282 // Set a transient to track that the user has dismissed the pre-chat message
3283 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3284 set_transient($transient_key, true, DAY_IN_SECONDS);
3285
3286 wp_send_json_success();
3287 }
3288
3289 public function mxchat_check_pre_chat_message_status() {
3290 // Get and sanitize the user identifier
3291 $user_id = $this->mxchat_get_user_identifier();
3292 $user_id = sanitize_key($user_id);
3293
3294 // Check if the transient exists (i.e., if the message was dismissed)
3295 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3296 $dismissed = get_transient($transient_key);
3297
3298 // Log the result to see if it's being set correctly
3299 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
3300
3301 if ($dismissed) {
3302 wp_send_json_success(['dismissed' => true]);
3303 } else {
3304 wp_send_json_success(['dismissed' => false]);
3305 }
3306
3307 wp_die();
3308 }
3309
3310 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
3311 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
3312 return 0;
3313 }
3314
3315 $dotProduct = array_sum(array_map(function ($a, $b) {
3316 return $a * $b;
3317 }, $vectorA, $vectorB));
3318 $normA = sqrt(array_sum(array_map(function ($a) {
3319 return $a * $a;
3320 }, $vectorA)));
3321 $normB = sqrt(array_sum(array_map(function ($b) {
3322 return $b * $b;
3323 }, $vectorB)));
3324
3325 if ($normA == 0 || $normB == 0) {
3326 return 0;
3327 }
3328
3329 return $dotProduct / ($normA * $normB);
3330 }
3331
3332 public function mxchat_enqueue_scripts_styles() {
3333 // Define version numbers for the styles and scripts
3334 $chat_style_version = '1.5.7'; // Replace with your actual version
3335 $chat_script_version = '1.5.7'; // Replace with your actual version
3336
3337 // Enqueue the script
3338 wp_enqueue_script(
3339 'mxchat-chat-js',
3340 plugin_dir_url(__FILE__) . '../js/chat-script.js',
3341 array('jquery'),
3342 $chat_script_version,
3343 true
3344 );
3345
3346 // Enqueue the CSS
3347 wp_enqueue_style(
3348 'mxchat-chat-css',
3349 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3350 array(),
3351 $chat_style_version
3352 );
3353
3354 // Fetch options from the database
3355 $this->options = get_option('mxchat_options');
3356
3357 // Prepare settings for JavaScript
3358 $style_settings = array(
3359 'ajax_url' => admin_url('admin-ajax.php'),
3360 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
3361 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3362 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3363 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3364 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
3365 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
3366 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
3367 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
3368 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
3369 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
3370 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
3371 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
3372 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3373 'icon_color' => $this->options['icon_color'] ?? '#fff',
3374 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3375 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3376 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3377
3378 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3379 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3380 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3381 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3382 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3383 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3384 );
3385
3386 // Pass the settings to the script
3387 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3388 }
3389
3390
3391 public function mxchat_reset_rate_limits() {
3392 global $wpdb;
3393
3394 // Define a cache key pattern for rate limits
3395 $cache_key_pattern = 'mxchat_chat_limit_%';
3396
3397 // Retrieve all option names matching the pattern
3398 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3399 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3400
3401 // db call ok; no-cache ok
3402 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3403 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3404
3405 // Clear the relevant cache entries
3406 foreach ($option_names as $option_name) {
3407 wp_cache_delete($option_name, 'options');
3408 }
3409
3410 // Optionally, clear a general cache if you have one
3411 wp_cache_delete('mxchat_all_chat_limits', 'options');
3412 }
3413
3414 private function mxchat_fetch_woocommerce_products() {
3415 // Ensure WooCommerce is active
3416 if (!class_exists('WooCommerce')) {
3417 return [];
3418 }
3419
3420 $args = array(
3421 'post_type' => 'product',
3422 'post_status' => 'publish',
3423 'posts_per_page' => -1,
3424 );
3425
3426 $products = get_posts($args);
3427 $product_data = [];
3428
3429 foreach ($products as $product) {
3430 $product_id = $product->ID;
3431 $product_obj = wc_get_product($product_id);
3432
3433 $product_data[] = array(
3434 'id' => $product_id,
3435 'name' => $product_obj->get_name(),
3436 'description' => $product_obj->get_description(),
3437 'short_description' => $product_obj->get_short_description(),
3438 'url' => get_permalink($product_id),
3439 'price' => $product_obj->get_regular_price(),
3440 'sale_price' => $product_obj->get_sale_price(),
3441 'stock_status' => $product_obj->get_stock_status(),
3442 'sku' => $product_obj->get_sku(),
3443 'in_stock' => $product_obj->is_in_stock(),
3444 'total_sales' => $product_obj->get_total_sales(),
3445 );
3446 }
3447
3448 return $product_data;
3449 }
3450
3451 }
3452 ?>
3453