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

1,776 lines 70.7 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
10 public function __construct() {
11 $this->options = get_option('mxchat_options');
12 $this->chat_count = get_option('mxchat_chat_count', 0);
13
14 // Add WooCommerce hooks
15 add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3);
16
17 // Ensure embeddings are removed when a product is moved to trash or permanently deleted
18 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
19 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
20
21 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
22 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
23 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
24
25 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
26 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
27 // Add the AJAX actions for checking if the pre-chat message was dismissed
28 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
29 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
30
31 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
32 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
33
34
35 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
36 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
37
38 if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
39 wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
40 }
41
42 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
43 }
44
45 public function mxchat_handle_product_change($post_id, $post, $update) {
46 // Ensure this is a product post type
47 if ($post->post_type !== 'product') {
48 return;
49 }
50
51 // Only generate embeddings if the product is published
52 if ($post->post_status === 'publish') {
53 // Delay the embedding slightly to ensure all product data is available
54 add_action('shutdown', function() use ($post_id) {
55 $product = wc_get_product($post_id);
56 if ($product && $product->get_price() !== '') {
57 $this->mxchat_store_product_embedding($product);
58 } else {
59 // Optionally, log or handle the case where product data is incomplete
60 // error_log("Product {$post_id} does not have complete data. Embedding not generated.");
61 }
62 });
63 }
64 }
65
66 public function mxchat_handle_product_delete($post_id) {
67 if (get_post_type($post_id) !== 'product') {
68 return;
69 }
70
71 global $wpdb;
72 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
73
74 // Delete the embedding associated with this product
75 $wpdb->delete($table_name, array('source_url' => get_permalink($post_id)), array('%s'));
76 }
77
78 private function mxchat_store_product_embedding($product) {
79 if (isset($this->options['enable_woocommerce_integration']) && $this->options['enable_woocommerce_integration'] === '1') {
80
81 $source_url = get_permalink($product->get_id());
82 $regular_price = $product->get_regular_price();
83 $sale_price = $product->get_sale_price();
84 $price = $sale_price ?: $regular_price;
85
86 $description = $product->get_description() . "\n\n" .
87 "Short Description: " . $product->get_short_description() . "\n" .
88 "Price: " . $regular_price . "\n" .
89 "Sale Price: " . ($sale_price ?: 'N/A') . "\n" .
90 "SKU: " . $product->get_sku();
91
92 global $wpdb;
93 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
94
95 // Delete any existing embedding for this product
96 $wpdb->delete($table_name, array('source_url' => $source_url), array('%s'));
97
98 // Submit the new content and embedding to the database
99 MxChat_Utils::submit_content_to_db($description, $source_url, $this->options['api_key']);
100 }
101 }
102
103
104
105
106
107 private function mxchat_increment_chat_count() {
108 $chat_count = get_option('mxchat_chat_count', 0);
109 $chat_count++;
110 update_option('mxchat_chat_count', $chat_count);
111 }
112
113 function mxchat_fetch_conversation_history() {
114 if (empty($_POST['session_id'])) {
115 wp_send_json_error(['message' => 'Session ID missing.']);
116 wp_die();
117 }
118
119 $session_id = sanitize_text_field($_POST['session_id']);
120 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
121
122 if (empty($history)) {
123 wp_send_json_error(['message' => 'No history found.']);
124 wp_die();
125 }
126
127 wp_send_json_success(['conversation' => $history]);
128 wp_die();
129 }
130 private function mxchat_fetch_conversation_history_for_ajax($session_id) {
131 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
132 $formatted_history = [];
133
134 // Format the history to align with the expected structure for OpenAI
135 foreach ($history as $entry) {
136 $formatted_history[] = [
137 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
138 'content' => $entry['content']
139 ];
140 }
141
142 return $formatted_history;
143 }
144 private function mxchat_fetch_conversation_history_for_ai($session_id) {
145 $history = get_option("mxchat_history_{$session_id}", []);
146
147 $formatted_history = [];
148
149 foreach ($history as $entry) {
150 // Skip messages containing HTML
151 if ($entry['content'] !== strip_tags($entry['content'])) {
152 continue;
153 }
154
155 $formatted_history[] = [
156 'role' => $entry['role'],
157 'content' => $entry['content']
158 ];
159 }
160
161 return $formatted_history;
162 }
163
164
165 private function mxchat_save_chat_message($session_id, $role, $message) {
166 global $wpdb;
167 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
168
169 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
170 $user_identifier = MxChat_User::mxchat_get_user_identifier();
171 $user_email = MxChat_User::mxchat_get_user_email();
172
173 $history = get_option("mxchat_history_{$session_id}", []);
174 $history[] = ['role' => $role, 'content' => $message];
175 update_option("mxchat_history_{$session_id}", $history);
176
177 $wpdb->insert($table_name, [
178 'user_id' => $user_id,
179 'user_identifier' => $user_identifier,
180 'user_email' => $user_email,
181 'session_id' => $session_id,
182 'role' => $role,
183 'message' => $message,
184 'timestamp' => current_time('mysql', 1)
185 ]);
186 }
187
188
189 public function mxchat_handle_chat_request() {
190 global $wpdb;
191
192 // Get and sanitize the user identifier
193 $user_id = $this->mxchat_get_user_identifier();
194 $user_id = sanitize_key($user_id);
195
196 // Setup rate limiting
197 $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
198 $chat_count = get_transient($rate_limit_transient_key) ?: 0;
199
200 // Retrieve the session ID from the client's POST data
201 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
202
203 if (empty($session_id)) {
204 wp_send_json_error('Session ID is missing.');
205 wp_die();
206 }
207
208 // Determine the rate limit based on login status
209 $rate_limit_option = is_user_logged_in()
210 ? ($this->options['rate_limit_logged_in'] ?? '100')
211 : ($this->options['rate_limit_logged_out'] ?? '10');
212
213 if ($rate_limit_option !== 'unlimited' && $chat_count >= intval($rate_limit_option)) {
214 $rate_limit_message = $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.';
215 wp_send_json_error(['message' => $rate_limit_message]);
216 wp_die();
217 }
218
219 // Increment chat count only if limit is not 'unlimited'
220 if ($rate_limit_option !== 'unlimited') {
221 set_transient($rate_limit_transient_key, $chat_count + 1, DAY_IN_SECONDS);
222 }
223
224 // Validate and sanitize the incoming message
225 if (empty($_POST['message'])) {
226 wp_send_json_error('No message received.');
227 wp_die();
228 }
229
230 $message = wp_strip_all_tags($_POST['message'], false);
231 $message = trim($message);
232 $this->mxchat_save_chat_message($session_id, 'user', $message);
233
234 // Initialize response variables
235 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
236 $this->productCardHtml = '';
237 $intent_info = '';
238
239 // Step 1: Check for new PDF URL in the message
240 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
241 $new_pdf_url = $matches[0];
242
243 // Validate HTTPS
244 if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
245 $existing_pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
246
247 // If a new URL is detected, reset all PDF-related transients
248 if ($existing_pdf_url !== $new_pdf_url) {
249 delete_transient('mxchat_pdf_url_' . $session_id);
250 delete_transient('mxchat_pdf_embeddings_' . $session_id);
251 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
252
253 // Store the new PDF URL
254 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
255
256 // Get the maximum number of pages allowed from admin settings
257 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Default to 69
258
259
260 // Fetch and process the new PDF
261 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages); // Pass both arguments
262
263 if ($embeddings === 'too_many_pages') {
264 // Handle PDF exceeding page limit
265 $error_text = sprintf(
266 $this->options['pdf_intent_error_text'] ?? "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.",
267 $max_pages
268 );
269 $this->fallbackResponse['text'] = $error_text;
270 delete_transient('mxchat_pdf_url_' . $session_id);
271 } elseif ($embeddings) {
272 // Handle successful processing
273 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
274 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
275
276 $success_text = $this->options['pdf_intent_success_text'] ?? "I've processed the new PDF. What questions do you have about it?";
277 $this->fallbackResponse['text'] = $success_text;
278 } else {
279 // Handle general processing error
280 $error_text = $this->options['pdf_intent_error_text'] ?? "Sorry, I couldn't process the new PDF. Please ensure it's a valid file.";
281 $this->fallbackResponse['text'] = $error_text;
282 delete_transient('mxchat_pdf_url_' . $session_id);
283 }
284
285
286 wp_send_json(['message' => $this->fallbackResponse['text']]);
287 wp_die();
288 }
289 }
290 }
291
292
293
294 // Step 2: Detect intent and handle intent-based responses
295 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
296
297 // Step 3: If intent is matched and handled, respond immediately
298 if (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html'])) {
299 $response_data = [
300 'text' => $this->fallbackResponse['text'],
301 'html' => $this->fallbackResponse['html'],
302 'session_id' => $session_id
303 ];
304 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
305 wp_send_json($response_data);
306 wp_die();
307 }
308
309 // Step 4: Handle PDF-related content if applicable
310 $include_pdf_in_context = get_transient('mxchat_include_pdf_in_context_' . $session_id);
311 $pdf_vectors = get_transient('mxchat_pdf_embeddings_' . $session_id);
312
313 // Generate embedding for the user's query
314 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
315 if (!is_array($user_message_embedding)) {
316 wp_send_json_error('Error processing your message.');
317 wp_die();
318 }
319
320 // Prepare context for the response
321 $context_content = "User asked: '{$message}'\n\n";
322
323 // Step 5: Fetch relevant content from the knowledge database
324 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
325 if (!empty($relevant_content)) {
326 $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
327 }
328
329 // Step 6: Check PDF content if available
330 if ($include_pdf_in_context && is_array($pdf_vectors)) {
331 //error_log("Searching relevant PDF pages for query embedding.");
332 $relevant_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_vectors);
333
334 if (!empty($relevant_pages)) {
335 error_log("Relevant PDF pages found: " . implode(', ', array_map(function($page) {
336 return $page['page_number'];
337 }, $relevant_pages)));
338
339 foreach ($relevant_pages as $page_data) {
340 $context_content .= "Content from page {$page_data['page_number']}:\n" . $page_data['text'] . "\n\n";
341 }
342 } else {
343 //error_log("No relevant PDF pages found for query embedding.");
344 }
345 }
346
347 // Step 7: Generate AI response based on context and conversation history
348 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
349 $this->mxchat_increment_chat_count();
350
351 $response = $this->mxchat_generate_response(
352 $context_content,
353 $this->options['api_key'],
354 $this->options['xai_api_key'],
355 $this->options['claude_api_key'],
356 $conversation_history
357 );
358
359 $this->mxchat_save_chat_message($session_id, 'bot', $response);
360
361 // Step 8: Save and include additional content
362 if (!empty($this->productCardHtml)) {
363 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
364 }
365
366 if (!empty($this->fallbackResponse['html'])) {
367 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
368 }
369
370 // Step 9: Return the response
371 $response_data = [
372 'text' => $response,
373 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
374 'session_id' => $session_id
375 ];
376
377 wp_send_json($response_data);
378 wp_die();
379 }
380
381 // New function to check intents and invoke the callback function
382 private function mxchat_check_intent_and_invoke_callback( $message, $user_id, $session_id ) {
383 global $wpdb;
384
385 // Generate the user embedding
386 $user_embedding = $this->mxchat_generate_embedding( $message, $this->options['api_key'] );
387 if ( ! is_array( $user_embedding ) ) {
388 //error_log( 'mxchat: Error generating embedding for the message.' );
389 wp_send_json_error( 'Error generating embedding for the message.' );
390 return false;
391 }
392
393 // Fetch all intents from the database
394 $table_name = $wpdb->prefix . 'mxchat_intents';
395 $intents = $wpdb->get_results( "SELECT * FROM $table_name" );
396
397 if ( empty( $intents ) ) {
398 //error_log( 'mxchat: No intents found in the database.' );
399 return false; // No intents to process
400 }
401
402 $highest_similarity = -INF;
403 $matched_intent = null;
404
405 // Calculate similarity for each intent, using each intent's individual threshold if set
406 foreach ( $intents as $intent ) {
407 $intent_embedding_serialized = $intent->embedding_vector;
408
409 // Securely unserialize the embedding vector
410 $intent_embedding = $intent_embedding_serialized
411 ? unserialize( $intent_embedding_serialized, [ 'allowed_classes' => false ] )
412 : null;
413
414 if ( ! is_array( $intent_embedding ) ) {
415 //error_log( "mxchat: Invalid embedding for intent '{$intent->intent_label}'. Skipping." );
416 continue; // Skip if the embedding is invalid
417 }
418
419 // Calculate cosine similarity between user message embedding and intent embedding
420 $similarity = $this->mxchat_calculate_cosine_similarity( $user_embedding, $intent_embedding );
421 //error_log( "mxchat: Similarity for intent '{$intent->intent_label}': {$similarity}" );
422
423 // Determine the similarity threshold for this intent
424 $intent_threshold = isset( $intent->similarity_threshold ) ? $intent->similarity_threshold : 0.85; // Default to 0.85 if not set
425 //error_log( "mxchat: Using threshold {$intent_threshold} for intent '{$intent->intent_label}'" );
426
427 // Only consider this intent as a potential match if it meets or exceeds the intent's threshold
428 if ( $similarity >= $intent_threshold && $similarity > $highest_similarity ) {
429 $highest_similarity = $similarity;
430 $matched_intent = $intent;
431 }
432 }
433
434 // Log the highest similarity and matched intent if found
435 if ( $matched_intent ) {
436 //error_log( "mxchat: Highest similarity is {$highest_similarity} for intent '{$matched_intent->intent_label}'" );
437 } else {
438 //error_log( 'mxchat: No intent matched.' );
439 }
440
441 // If a match is found above the threshold, call the associated callback
442 if ( $matched_intent ) {
443 //error_log( "mxchat: Intent '{$matched_intent->intent_label}' matches the threshold. Invoking callback '{$matched_intent->callback_function}'" );
444
445 // Ensure the callback function exists before calling it
446 if ( method_exists( $this, $matched_intent->callback_function ) ) {
447 // Call the intent handler function
448 call_user_func( [ $this, $matched_intent->callback_function ], $message, $user_id, $session_id );
449 return true; // Indicate that an intent was matched and handled
450 } else {
451 //error_log( "mxchat: Callback function '{$matched_intent->callback_function}' not found." );
452 }
453 } else {
454 //error_log( 'mxchat: No intent matched above its respective threshold.' );
455 }
456
457 return false; // No intent was matched above the threshold
458 }
459
460 //verified good
461 public function mxchat_handle_order_history($message, $user_id, $session_id) {
462 // Check if WooCommerce is active
463 if (!class_exists('WooCommerce')) {
464 $this->fallbackResponse['text'] = "Order functionality is currently unavailable.";
465 return; // No need to proceed further
466 }
467
468 // Determine if the user is asking for the "last order" or "all orders"
469 $queryType = (stripos($message, 'last order') !== false || stripos($message, 'recent order') !== false) ? 'last' : 'all';
470
471 // Fetch the relevant order details
472 $orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details($queryType);
473
474 // Set the fallback response to order details, which will be used if needed
475 $this->fallbackResponse['text'] = $orderDetails ?: "No previous orders found.";
476 }
477
478 //verified good
479 public function mxchat_handle_product_inquiry( $message, $user_id, $session_id ) {
480 // Attempt to retrieve the product ID from the message
481 $product_id = MxChat_WooCommerce::mxchat_extract_product_id_from_message( $message );
482
483 // Check if there's a previously discussed product for follow-up queries
484 if ( ! $product_id ) {
485 $product_id = get_transient( 'mxchat_last_discussed_product_' . $user_id );
486 }
487
488 if ( $product_id && class_exists( 'WooCommerce' ) ) {
489 $product = wc_get_product( $product_id );
490 if ( $product ) {
491 // Escape and prepare product data
492 $product_name = esc_html( $product->get_name() );
493 $product_price = $product->get_price_html(); // Assuming this is safe HTML
494 $product_image_url = esc_url( wp_get_attachment_url( $product->get_image_id() ) );
495 $product_url = esc_url( get_permalink( $product_id ) );
496 $product_id_attr = esc_attr( $product_id );
497
498 // Generate the product card HTML using HEREDOC syntax
499 $product_card_html = <<<HTML
500 <div class="mxchat-product-card">
501 <a href="{$product_url}" target="_blank">
502 <img src="{$product_image_url}" alt="{$product_name}" class="mxchat-product-image" />
503 <h3 class="mxchat-product-name">{$product_name}</h3>
504 </a>
505 <div class="mxchat-product-price">{$product_price}</div>
506 <button class="mxchat-add-to-cart-button" data-product-id="{$product_id_attr}">Add to Cart</button>
507 </div>
508 HTML;
509
510 // Save the last discussed product
511 set_transient( 'mxchat_last_discussed_product_' . $user_id, $product_id, HOUR_IN_SECONDS );
512
513 // Save the product card HTML to be appended later
514 $this->productCardHtml = $product_card_html;
515
516 // Do not send a response here; allow execution to continue
517 return;
518 }
519 }
520
521 // If no product found, set a fallback response
522 $this->fallbackResponse = __( "I couldn't find details for that product. Could you specify the product name?", 'mxchat' );
523 return;
524 }
525
526 //verified good
527 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
528 // Log the message safely
529 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
530
531 // Initiate email capture flow
532 $response = esc_html($this->options['triggered_phrase_response'] ?? "Would you like to join our mailing list? Please provide your email below.");
533
534 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
535 $this->mxchat_save_chat_message($session_id, 'bot', $response);
536
537 // Respond to the user
538 wp_send_json(['message' => $response]);
539 wp_die();
540 }
541
542 //very good
543 public function mxchat_generate_image($message, $user_id, $session_id) {
544 // Prepare a prompt for DALL-E
545 $prompt = "Create an image of " . sanitize_text_field($message);
546
547 // Use the existing OpenAI API key
548 $openai_api_key = sanitize_text_field($this->options['api_key']);
549
550 // Call DALL-E to generate an image
551 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
552
553 // Check if the response contains an image URL
554 if (isset($image_response['imageUrl'])) {
555 $image_url = esc_url_raw($image_response['imageUrl']);
556
557 // Construct the HTML with a CSS class instead of inline styles
558 $response_html = '<img src="' . esc_url($image_url) . '" alt="Generated Image" class="mxchat-generated-image" />';
559
560 $response_text = "Here is the image I generated:";
561 } else {
562 $response_text = "I'm sorry, but I couldn't generate an image based on your request.";
563 $response_html = '';
564 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
565 }
566
567 // Save both text and HTML responses
568 $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
569
570 // Prepare the response data
571 $response_data = [
572 'message' => $response_text,
573 'html' => $response_html,
574 'image_url' => $image_url ?? '',
575 ];
576
577 // Send the JSON response
578 header('Content-Type: application/json; charset=' . get_option('blog_charset'));
579 echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
580 wp_die();
581 }
582 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
583 $api_url = 'https://api.openai.com/v1/images/generations';
584 $body = json_encode([
585 'prompt' => sanitize_text_field($prompt),
586 'n' => 1,
587 'size' => '1024x1024',
588 'model' => sanitize_text_field($model),
589 ]);
590
591 $args = [
592 'body' => $body,
593 'headers' => [
594 'Content-Type' => 'application/json',
595 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
596 ],
597 'method' => 'POST',
598 'timeout' => absint($timeout),
599 ];
600
601 $response = wp_remote_post($api_url, $args);
602
603 if (is_wp_error($response)) {
604 //error_log("DALL-E request failed: " . $response->get_error_message());
605 return ['error' => "Error generating image: " . $response->get_error_message()];
606 }
607
608 $response_body = json_decode(wp_remote_retrieve_body($response), true);
609
610 if (isset($response_body['data'][0]['url'])) {
611 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
612 } else {
613 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
614 return ['error' => "Failed to generate image."];
615 }
616 }
617
618 //very good
619 public function mxchat_handle_search_request($message, $user_id, $session_id) {
620 // Sanitize user input
621 $search_query = preg_replace('/^search the web for\s+/i', '', $message);
622 $search_query = preg_replace('/^show me the latest news about\s+/i', '', $message);
623 $search_query = preg_replace('/^what\'?s the news in\s+/i', '', $message);
624 $search_query = preg_replace('/^news about\s+/i', '', $message);
625 $search_query = trim(sanitize_text_field($search_query));
626
627 if (empty($search_query)) {
628 $this->fallbackResponse = [
629 'text' => __("Please provide a valid search query.", 'mxchat'),
630 ];
631 return;
632 }
633
634 // Check if the query is likely related to news
635 $is_news_search = preg_match("/\bnews\b/i", $message);
636
637 // Retrieve settings
638 $options = get_option('mxchat_options');
639
640 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
641 $news_count = isset($options['brave_news_count']) ? intval($options['brave_news_count']) : 3;
642 $country = isset($options['brave_country']) ? sanitize_text_field($options['brave_country']) : 'us';
643 $language = isset($options['brave_language']) ? sanitize_text_field($options['brave_language']) : 'en';
644
645 if (empty($api_key)) {
646
647 $this->fallbackResponse = [
648 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
649 ];
650 return;
651 }
652
653 // Set API endpoint and parameters based on search type
654 $api_url = $is_news_search
655 ? 'https://api.search.brave.com/res/v1/news/search'
656 : 'https://api.search.brave.com/res/v1/web/search';
657
658 $query_args = [
659 'q' => urlencode($search_query),
660 ];
661
662 if ($is_news_search) {
663 $query_args['count'] = $news_count;
664 $query_args['country'] = $country;
665 $query_args['search_lang'] = $language;
666 }
667
668 $api_url = add_query_arg($query_args, $api_url);
669
670 // Implement caching
671 $transient_key = 'mxchat_search_' . md5($api_url);
672 $body = get_transient($transient_key);
673
674 if (false === $body) {
675 $args = [
676 'headers' => [
677 'Accept' => 'application/json',
678 'Accept-Encoding' => 'gzip',
679 'Authorization' => 'Bearer ' . $api_key,
680 'x-subscription-token' => $api_key,
681 ],
682 'timeout' => 10,
683 ];
684
685 $response = wp_remote_get($api_url, $args);
686
687 if (is_wp_error($response)) {
688
689 $this->fallbackResponse = [
690 'text' => __("I'm sorry, I couldn't retrieve any information based on your request.", 'mxchat'),
691 ];
692 return;
693 }
694
695 $body = json_decode(wp_remote_retrieve_body($response), true);
696 set_transient($transient_key, $body, HOUR_IN_SECONDS);
697 }
698
699 // Process the API response and build a more informative summary
700 $text_summary = "";
701
702 if ($is_news_search && isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
703 $text_summary = "Here are some recent news articles:\n\n";
704
705 foreach ($body['results'] as $news) {
706 $title = isset($news['title']) ? esc_html($news['title']) : __('No Title', 'mxchat');
707 $url = isset($news['url']) ? esc_url($news['url']) : '#';
708 $description = isset($news['description']) ? esc_html(strip_tags($news['description'])) : __('No description available.', 'mxchat');
709 $age = isset($news['age']) ? esc_html($news['age']) : '';
710 $hostname = isset($news['meta_url']['hostname']) ? esc_html($news['meta_url']['hostname']) : '';
711 $thumbnail = isset($news['thumbnail']['src']) ? esc_url($news['thumbnail']['src']) : '';
712
713 // Build text-only news item summary
714 $text_summary .= "Title: {$title}\nURL: {$url}\nDescription: {$description}\nPublished: {$age}\n";
715 if (!empty($hostname)) {
716 $text_summary .= "Source: {$hostname}\n";
717 }
718 if (!empty($thumbnail)) {
719 $text_summary .= "Thumbnail: ![Image]({$thumbnail})\n";
720 }
721 if (!empty($news['extra_snippets'])) {
722 $extra_snippet_text = implode(" ", $news['extra_snippets']);
723 $text_summary .= "Additional Info: {$extra_snippet_text}\n";
724 }
725 $text_summary .= "\n"; // Separate entries
726 }
727 } elseif (!$is_news_search && isset($body['web']['results']) && is_array($body['web']['results']) && count($body['web']['results']) > 0) {
728 $text_summary = "Here are some relevant articles based on your query:\n\n";
729
730 foreach ($body['web']['results'] as $result) {
731 $title = isset($result['title']) ? esc_html($result['title']) : __('No Title', 'mxchat');
732 $url = isset($result['url']) ? esc_url($result['url']) : '#';
733 $description = isset($result['description']) ? esc_html(strip_tags($result['description'])) : __('No description available.', 'mxchat');
734 $hostname = isset($result['meta_url']['hostname']) ? esc_html($result['meta_url']['hostname']) : '';
735 $thumbnail = isset($result['thumbnail']['src']) ? esc_url($result['thumbnail']['src']) : '';
736
737 // Build text-only web item summary
738 $text_summary .= "Title: {$title}\nURL: {$url}\nDescription: {$description}\n";
739 if (!empty($hostname)) {
740 $text_summary .= "Source: {$hostname}\n";
741 }
742 if (!empty($thumbnail)) {
743 $text_summary .= "Thumbnail: ![Image]({$thumbnail})\n";
744 }
745 if (!empty($result['extra_snippets'])) {
746 $extra_snippet_text = implode(" ", $result['extra_snippets']);
747 $text_summary .= "Additional Info: {$extra_snippet_text}\n";
748 }
749 $text_summary .= "\n"; // Separate entries
750 }
751 } else {
752
753 $this->fallbackResponse = [
754 'text' => __("I'm sorry, I couldn't retrieve any information based on your request.", 'mxchat'),
755 ];
756 return;
757 }
758
759 $this->fallbackResponse = [
760 'text' => $text_summary,
761 ];
762
763
764 }
765
766 //very good
767 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
768
769 // Step 1: Interpret the search query for better results
770 $refined_search_query = $this->mxchat_interpret_search_query($message);
771
772
773 // If no query was interpreted, return a fallback message
774 if (empty($refined_search_query)) {
775 $this->fallbackResponse = [
776 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
777 'html' => "",
778 ];
779 return;
780 }
781
782 // Brave API URL
783 $api_url = 'https://api.search.brave.com/res/v1/images/search';
784
785 // Retrieve Brave API settings
786 $options = get_option('mxchat_options');
787 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
788
789 if (empty($api_key)) {
790 /*
791 if (defined('WP_DEBUG') && WP_DEBUG) {
792 error_log("Brave API key is missing.");
793 }
794 */
795
796 $this->fallbackResponse = [
797 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
798 'html' => "",
799 ];
800 return;
801 }
802
803 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
804 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
805
806 // Append query parameters based on settings
807 $api_url = add_query_arg([
808 'q' => rawurlencode($refined_search_query),
809 'count' => $image_count,
810 'safesearch' => $safe_search,
811 ], $api_url);
812
813 /*
814 // Log the final API URL for the search
815 if (defined('WP_DEBUG') && WP_DEBUG) {
816 error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
817 }
818 */
819
820
821 // Implement caching
822 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
823 $body = get_transient($transient_key);
824
825 if (false === $body) {
826 $args = [
827 'headers' => [
828 'Accept' => 'application/json',
829 'Accept-Encoding' => 'gzip',
830 'X-Subscription-Token' => $api_key,
831 ],
832 'timeout' => 10,
833 ];
834
835 $response = wp_remote_get($api_url, $args);
836
837 if (is_wp_error($response)) {
838 /*
839 if (defined('WP_DEBUG') && WP_DEBUG) {
840 error_log("Brave Image API request failed: " . $response->get_error_message());
841 }
842 */
843
844 $this->fallbackResponse = [
845 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
846 'html' => "",
847 ];
848 return;
849 }
850
851 $body = json_decode(wp_remote_retrieve_body($response), true);
852 set_transient($transient_key, $body, HOUR_IN_SECONDS);
853 }
854
855 // Process the API response
856 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
857 $html_output = '<div class="mxchat-image-gallery">';
858
859 foreach ($body['results'] as $image) {
860 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
861 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
862 $title = isset($image['title']) ? esc_html($image['title']) : __('Image', 'mxchat');
863
864 if ($image_url && $thumbnail_url) {
865 $html_output .= '<div class="mxchat-image-item">';
866 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
867 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
868 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
869 $html_output .= '</a></div>';
870 }
871 }
872
873 $html_output .= '</div>';
874
875 $this->fallbackResponse = [
876 'text' => "",
877 'html' => $html_output,
878 ];
879
880 // Save response in chat history
881 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
882
883 } else {
884 /*
885 if (defined('WP_DEBUG') && WP_DEBUG) {
886 error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
887 }
888 */
889
890 $this->fallbackResponse = [
891 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
892 'html' => "",
893 ];
894 }
895 }
896 public function mxchat_interpret_search_query($user_query) {
897 $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.";
898
899 // Retrieve OpenAI API key using 'api_key' as the option key
900 $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
901
902 /*
903 // Log the API key check, without exposing the key
904 if (defined('WP_DEBUG') && WP_DEBUG) {
905 error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
906 }
907 */
908
909
910 if (empty($api_key)) {
911 //error_log("OpenAI API key is missing.");
912 return sanitize_text_field($user_query); // Default to the original query if API key is missing
913 }
914
915 $url = 'https://api.openai.com/v1/chat/completions';
916 $args = [
917 'headers' => [
918 'Authorization' => 'Bearer ' . $api_key,
919 'Content-Type' => 'application/json',
920 ],
921 'body' => wp_json_encode([
922 'model' => 'gpt-3.5-turbo',
923 'messages' => [
924 ['role' => 'system', 'content' => $system_prompt],
925 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
926 ],
927 'temperature' => 0.2,
928 'max_tokens' => 20,
929 ]),
930 'method' => 'POST',
931 ];
932
933 $response = wp_remote_post($url, $args);
934
935 if (is_wp_error($response)) {
936 //error_log("OpenAI request failed: " . $response->get_error_message());
937 return sanitize_text_field($user_query); // Fallback to the original query if there's an error
938 }
939
940 $body = json_decode(wp_remote_retrieve_body($response), true);
941
942 // Check for a valid response and sanitize output
943 if (isset($body['choices'][0]['message']['content'])) {
944 $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
945
946 /*
947 // Log the interpreted query for debugging
948 if (defined('WP_DEBUG') && WP_DEBUG) {
949 error_log("Interpreted search query: " . $interpreted_query);
950 }
951 */
952
953 return $interpreted_query;
954 } else {
955 //error_log("Unexpected API response format: " . print_r($body, true));
956 return sanitize_text_field($user_query);
957 }
958 }
959
960 //very good
961 public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
962 // Check if WooCommerce is active before proceeding
963 if (!class_exists('WooCommerce')) {
964 // error_log("WooCommerce is not active. Add-to-cart intent cannot be processed.");
965 return false;
966 }
967
968 // Sanitize user ID for transient key
969 $sanitized_user_id = sanitize_key($user_id);
970
971 // Retrieve the last discussed product ID from the transient
972 $last_product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
973 if ($last_product_id) {
974 // Attempt to add the product to the WooCommerce cart
975 $added = WC()->cart->add_to_cart($last_product_id);
976 $product = wc_get_product($last_product_id);
977
978 if ($added && $product) {
979 // Escape product name for safety
980 $product_name = esc_html($product->get_name());
981
982 // Success response if the product is added to the cart
983 $response = "The product '{$product_name}' has been added to your cart. To proceed to checkout, please type 'checkout'.";
984 set_transient('mxchat_checkout_prompt_' . $sanitized_user_id, true, 5 * MINUTE_IN_SECONDS);
985
986 // Save the response in chat history and send a JSON response
987 $this->mxchat_save_chat_message($session_id, 'bot', $response);
988 wp_send_json(['message' => $response]);
989 wp_die();
990 } else {
991 // Failure response if the product could not be added
992 $response = "Sorry, I couldn't add the product to your cart. Please try again.";
993 $this->mxchat_save_chat_message($session_id, 'bot', $response);
994 wp_send_json(['message' => $response]);
995 wp_die();
996 }
997 } else {
998 // Prompt the user to specify the product name if no product was found
999 $response = "I couldn't find the product to add. Please mention the product name again.";
1000 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1001 wp_send_json(['message' => $response]);
1002 wp_die();
1003 }
1004 }
1005 public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
1006 // Ensure WooCommerce is available and perform checkout actions
1007 if (class_exists('WooCommerce')) {
1008 // Sanitize user ID for transient key
1009 $sanitized_user_id = sanitize_key($user_id);
1010
1011 $checkout_prompt = get_transient('mxchat_checkout_prompt_' . $sanitized_user_id);
1012
1013 // Check if there is an active checkout prompt and if the cart has items
1014 if ($checkout_prompt && WC()->cart->get_cart_contents_count() > 0) {
1015 // Get and escape the checkout URL
1016 $checkout_url = wc_get_checkout_url();
1017 $checkout_url = esc_url_raw($checkout_url);
1018
1019 $response = "Great! Redirecting you to the checkout page...";
1020
1021 // Clear the checkout prompt transient to avoid duplicate responses
1022 delete_transient('mxchat_checkout_prompt_' . $sanitized_user_id);
1023
1024 // Save and send the response with the checkout URL for redirection
1025 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1026 wp_send_json(['message' => $response, 'redirect_url' => $checkout_url]);
1027 wp_die();
1028 } else {
1029 // If no active checkout prompt or empty cart, prompt user to add items first
1030 $response = "It seems like there is no active checkout prompt or no items in your cart. Please add a product to the cart first.";
1031 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1032 wp_send_json(['message' => $response]);
1033 wp_die();
1034 }
1035 } else {
1036 // WooCommerce is not available, send an error message
1037 $response = "WooCommerce is not active on this site, so checkout is not possible.";
1038 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1039 wp_send_json(['message' => $response]);
1040 wp_die();
1041 }
1042 }
1043
1044 //very good
1045 public static function mxchat_extract_product_id_from_message($message) {
1046 // Sanitize the message input
1047 $message = sanitize_text_field($message);
1048
1049 // Convert Markdown links to plain URLs
1050 $message = preg_replace('/\[(.*?)\]\((.*?)\)/', '$2', $message);
1051
1052 // Match all URLs in the message
1053 preg_match_all('/https?:\/\/[^\s]+/', $message, $matches);
1054
1055 if (!empty($matches[0])) {
1056 foreach ($matches[0] as $url) {
1057 // Sanitize the URL
1058 $url = esc_url_raw($url);
1059
1060 // Try to get the post ID from the URL
1061 $post_id = url_to_postid($url);
1062
1063 if ($post_id) {
1064 // Check if the post type is 'product'
1065 if (get_post_type($post_id) === 'product') {
1066 return $post_id;
1067 }
1068 }
1069 }
1070 }
1071 return false;
1072 }
1073
1074 //very good
1075 private function add_email_to_loops($email) {
1076 // Sanitize the email
1077 $email = sanitize_email($email);
1078
1079 // Retrieve and sanitize options
1080 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
1081 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
1082
1083 // Check for missing API key or mailing list ID
1084 if (empty($api_key) || empty($mailing_list_id)) {
1085 //error_log('Loops API key or mailing list ID is missing.');
1086 return;
1087 }
1088
1089 $data = array(
1090 'email' => $email,
1091 'subscribed' => true,
1092 'source' => 'MxChat AI Chatbot',
1093 'mailingLists' => array($mailing_list_id => true),
1094 );
1095
1096 $url = 'https://app.loops.so/api/v1/contacts/create';
1097 $args = array(
1098 'body' => wp_json_encode($data),
1099 'headers' => array(
1100 'Authorization' => 'Bearer ' . $api_key,
1101 'Content-Type' => 'application/json',
1102 ),
1103 'method' => 'POST',
1104 'timeout' => 45,
1105 );
1106
1107 $response = wp_remote_post($url, $args);
1108
1109 // Handle errors in the API request
1110 if (is_wp_error($response)) {
1111 //error_log('Error adding email to Loops: ' . $response->get_error_message());
1112 return;
1113 }
1114
1115 // Check for non-200 HTTP responses
1116 $response_code = wp_remote_retrieve_response_code($response);
1117 if ($response_code != 200) {
1118 $response_body = wp_remote_retrieve_body($response);
1119 //error_log('Loops API responded with code ' . $response_code . ': ' . $response_body);
1120 }
1121 }
1122
1123
1124
1125 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
1126 // Get the maximum number of pages allowed from admin settings
1127 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Default to 69
1128
1129
1130 // Retrieve options for dynamic texts
1131 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? "Please provide the URL to the PDF you'd like to discuss.";
1132 $success_text = $this->options['pdf_intent_success_text'] ?? "I've processed the PDF. What questions do you have about it?";
1133 $error_text = $this->options['pdf_intent_error_text'] ?? "Sorry, I couldn't process the PDF. Please ensure it's a valid file.";
1134
1135 // Check if we're waiting for a URL
1136 $waiting_for_url = get_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1137
1138 // Check if we already have a PDF URL stored for this session
1139 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1140
1141 // Always process a new URL if detected in the current message
1142 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1143 $new_pdf_url = $matches[0];
1144
1145 // Validate HTTPS
1146 if (!wp_http_validate_url($new_pdf_url) || parse_url($new_pdf_url, PHP_URL_SCHEME) !== 'https') {
1147 //error_log("Invalid PDF URL: $new_pdf_url");
1148 $this->fallbackResponse['text'] = $trigger_text;
1149 return;
1150 }
1151
1152 // Reset previous transients if a new URL is provided
1153 if ($pdf_url !== $new_pdf_url) {
1154 //error_log("New PDF URL detected. Resetting previous transients for session $session_id.");
1155 delete_transient('mxchat_pdf_url_' . $session_id);
1156 delete_transient('mxchat_pdf_embeddings_' . $session_id);
1157 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
1158
1159 // Store the new PDF URL
1160 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1161
1162 // Fetch and process the new PDF
1163 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages); // Ensure both arguments are passed
1164
1165 if ($embeddings === 'too_many_pages') {
1166 //error_log("PDF exceeds maximum allowed pages ($max_pages) for URL: $new_pdf_url");
1167 $error_text = sprintf(
1168 $this->options['pdf_intent_error_text'] ?? "The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.",
1169 $max_pages
1170 );
1171 $this->fallbackResponse['text'] = $error_text;
1172 delete_transient('mxchat_pdf_url_' . $session_id);
1173 } elseif ($embeddings) {
1174 //error_log("PDF processed successfully for URL: $new_pdf_url");
1175 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1176 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1177
1178 $this->fallbackResponse['text'] = $success_text;
1179 } else {
1180 //error_log("Failed to process PDF for URL: $new_pdf_url");
1181 $this->fallbackResponse['text'] = $error_text;
1182 delete_transient('mxchat_pdf_url_' . $session_id);
1183 }
1184
1185 return;
1186 }
1187 }
1188
1189 if (!$pdf_url) {
1190 //error_log("No PDF URL provided or stored for session $session_id.");
1191 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
1192 $this->fallbackResponse['text'] = $trigger_text;
1193 return;
1194 }
1195
1196 // Retrieve stored embeddings or fetch if missing
1197 $embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1198
1199 if (!$embeddings) {
1200 //error_log("No embeddings found for PDF URL: $pdf_url. Attempting to process again.");
1201 $embeddings = $this->fetch_and_split_pdf_pages($pdf_url, $max_pages); // Ensure both arguments are passed
1202
1203 if ($embeddings === 'too_many_pages') {
1204 //error_log("PDF exceeds maximum allowed pages ($max_pages) for URL: $pdf_url");
1205 $this->fallbackResponse['text'] = "The provided PDF exceeds the maximum allowed limit of {$max_pages} pages. Please provide a smaller document.";
1206 delete_transient('mxchat_pdf_url_' . $session_id);
1207 } elseif ($embeddings) {
1208 //error_log("PDF processed successfully for URL: $pdf_url");
1209 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1210 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1211
1212 $this->fallbackResponse['text'] = $success_text;
1213 } else {
1214 //error_log("Failed to process PDF for URL: $pdf_url");
1215 $this->fallbackResponse['text'] = $error_text;
1216 delete_transient('mxchat_pdf_url_' . $session_id);
1217 }
1218 } else {
1219 //error_log("Using stored embeddings for session $session_id.");
1220 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1221 $this->fallbackResponse['text'] = ''; // Proceed without additional message
1222 }
1223 }
1224 private function fetch_and_split_pdf_pages($pdf_url, $max_pages) {
1225 $upload_dir = wp_upload_dir();
1226 $local_pdf_path = $upload_dir['path'] . '/' . basename(parse_url($pdf_url, PHP_URL_PATH));
1227
1228 $response = wp_remote_get($pdf_url, ['timeout' => 60]);
1229 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1230 //error_log("Failed to download PDF. Error: " . print_r($response, true));
1231 return false;
1232 }
1233
1234 file_put_contents($local_pdf_path, wp_remote_retrieve_body($response));
1235
1236 try {
1237 $parser = new \Smalot\PdfParser\Parser();
1238 $pdf = $parser->parseFile($local_pdf_path);
1239 $pages = $pdf->getPages();
1240
1241 if (count($pages) > $max_pages) {
1242 unlink($local_pdf_path);
1243 return 'too_many_pages';
1244 }
1245
1246 $embeddings = [];
1247 foreach ($pages as $page_number => $page) {
1248 $text = $page->getText();
1249 $embedding = $this->mxchat_generate_embedding("Page " . ($page_number + 1) . ": " . $text, $this->options['api_key']);
1250 if ($embedding) {
1251 $embeddings[] = [
1252 'page_number' => $page_number + 1,
1253 'embedding' => $embedding,
1254 'text' => $text,
1255 ];
1256 } else {
1257 //error_log("Failed to generate embedding for page " . ($page_number + 1));
1258 }
1259 }
1260
1261 unlink($local_pdf_path);
1262 return $embeddings;
1263 } catch (\Exception $e) {
1264 unlink($local_pdf_path);
1265 //error_log("Error parsing or processing PDF: " . $e->getMessage());
1266 return false;
1267 }
1268 }
1269 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
1270 //error_log("find_relevant_pdf_pages called.");
1271
1272 $most_relevant = null;
1273 $highest_similarity = -INF;
1274
1275 foreach ($embeddings as $page_data) {
1276 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
1277
1278 if ($similarity > $highest_similarity) {
1279 $highest_similarity = $similarity;
1280 $most_relevant = $page_data['page_number'];
1281 }
1282 }
1283
1284 if (!is_null($most_relevant)) {
1285 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
1286 return array_filter($embeddings, function ($page) use ($page_numbers) {
1287 return in_array($page['page_number'], $page_numbers);
1288 });
1289 }
1290
1291 return [];
1292 }
1293
1294
1295
1296
1297 private function mxchat_get_user_identifier() {
1298 return MxChat_User::mxchat_get_user_identifier();
1299 }
1300
1301 private function mxchat_generate_embedding($text, $api_key) {
1302 $endpoint = 'https://api.openai.com/v1/embeddings';
1303
1304 $body = wp_json_encode([
1305 'input' => $text,
1306 'model' => 'text-embedding-ada-002'
1307 ]);
1308
1309 $args = [
1310 'body' => $body,
1311 'headers' => [
1312 'Content-Type' => 'application/json',
1313 'Authorization' => 'Bearer ' . $api_key,
1314 ],
1315 'timeout' => 60,
1316 'redirection' => 5,
1317 'blocking' => true,
1318 'httpversion' => '1.0',
1319 'sslverify' => true,
1320 ];
1321
1322 $response = wp_remote_post($endpoint, $args);
1323
1324 if (is_wp_error($response)) {
1325 return null;
1326 }
1327
1328 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1329
1330 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
1331 return $response_body['data'][0]['embedding'];
1332 } else {
1333 return null;
1334 }
1335 }
1336
1337 private function mxchat_find_relevant_content($user_embedding) {
1338 global $wpdb;
1339 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
1340
1341 // Define a cache key for embeddings
1342 $cache_key = 'mxchat_system_prompt_embeddings';
1343
1344 // Attempt to get the embeddings from the cache
1345 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
1346
1347 if ($embeddings === false) {
1348 // Cache miss, query the database and cache the results
1349 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
1350 $embeddings = $wpdb->get_results($query);
1351
1352 if ($embeddings === null || empty($embeddings)) {
1353 return null; // Return null to handle no embeddings gracefully
1354 }
1355
1356 // Cache the results if successful
1357 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600); // Cache for 1 hour
1358 }
1359
1360 $most_relevant_id = null;
1361 $highest_similarity = -INF;
1362
1363 foreach ($embeddings as $embedding) {
1364 $database_embedding = $embedding->embedding_vector
1365 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
1366 : null;
1367
1368 if (is_array($database_embedding) && is_array($user_embedding)) {
1369 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
1370
1371 if ($similarity > $highest_similarity) {
1372 $highest_similarity = $similarity;
1373 $most_relevant_id = $embedding->id;
1374 }
1375 }
1376 }
1377
1378 if ($most_relevant_id !== null) {
1379 // Fetch content with product links
1380 return $this->fetch_content_with_product_links($most_relevant_id);
1381 }
1382
1383 return null; // Return null if no relevant content is found
1384 }
1385
1386 private function fetch_content_with_product_links($most_relevant_id) {
1387 global $wpdb;
1388 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
1389
1390 // Fetch the article content and associated product URL
1391 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
1392 $result = $wpdb->get_row($query);
1393
1394 if ($result) {
1395 // Append the product link to the content if available
1396 $content = $result->article_content;
1397 if (!empty($result->source_url)) {
1398 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
1399 }
1400 return $content;
1401 }
1402
1403 return null;
1404 }
1405
1406 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $conversation_history) {
1407 if (!$relevant_content) {
1408 return "I'm sorry, I couldn't find relevant information on that topic.";
1409 }
1410
1411 // Check the selected model
1412 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-3.5-turbo';
1413
1414 // Call the appropriate function based on the selected model
1415 if (strpos($selected_model, 'claude') !== false) {
1416 return $this->mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content);
1417 } elseif ($selected_model === 'grok-beta') {
1418 return $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content);
1419 } else {
1420 return $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content);
1421 }
1422 }
1423
1424 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
1425 // Get system prompt instructions from options
1426 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
1427
1428 // Add system prompt to relevant content
1429 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
1430
1431 // Prepend system instructions to the conversation history
1432 array_unshift($conversation_history, [
1433 'role' => 'system',
1434 'content' => "Here are your instructions: " . $content_with_instructions
1435 ]);
1436
1437 // Ensure consistency: Replace 'bot' role with 'assistant' in conversation history
1438 foreach ($conversation_history as &$message) {
1439 if ($message['role'] === 'bot') {
1440 $message['role'] = 'assistant';
1441 }
1442 }
1443
1444 // Build the request body
1445 $body = json_encode([
1446 'model' => $selected_model,
1447 'messages' => $conversation_history,
1448 'temperature' => 0.8,
1449 'stream' => false
1450 ]);
1451
1452 // Set up the API request
1453 $args = [
1454 'body' => $body,
1455 'headers' => [
1456 'Content-Type' => 'application/json',
1457 'Authorization' => 'Bearer ' . $api_key,
1458 ],
1459 'timeout' => 60,
1460 'redirection' => 5,
1461 'blocking' => true,
1462 'httpversion' => '1.0',
1463 'sslverify' => true,
1464 ];
1465
1466 // Make the API request
1467 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
1468
1469 // Process the response
1470 if (is_wp_error($response)) {
1471 return "Sorry, there was an error processing your request.";
1472 }
1473
1474 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1475
1476 if (isset($response_body['choices'][0]['message']['content'])) {
1477 return trim($response_body['choices'][0]['message']['content']);
1478 } else {
1479 return "Sorry, I couldn't process that request.";
1480 }
1481 }
1482
1483 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
1484 // Get system prompt instructions from options
1485 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
1486
1487 // Add system prompt to relevant content
1488 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
1489
1490 // Prepend system instructions to the conversation history
1491 array_unshift($conversation_history, [
1492 'role' => 'system',
1493 'content' => "Here are your instructions: " . $content_with_instructions
1494 ]);
1495
1496 // Ensure consistency: Replace 'bot' role with 'assistant' in conversation history
1497 foreach ($conversation_history as &$message) {
1498 if ($message['role'] === 'bot') {
1499 $message['role'] = 'assistant';
1500 }
1501 }
1502
1503 // Build the request body
1504 $body = json_encode([
1505 'model' => $selected_model,
1506 'messages' => $conversation_history,
1507 'temperature' => 0.8,
1508 'stream' => false
1509 ]);
1510
1511 // Set up the API request
1512 $args = [
1513 'body' => $body,
1514 'headers' => [
1515 'Content-Type' => 'application/json',
1516 'Authorization' => 'Bearer ' . $xai_api_key,
1517 ],
1518 'timeout' => 60,
1519 'redirection' => 5,
1520 'blocking' => true,
1521 'httpversion' => '1.0',
1522 'sslverify' => true,
1523 ];
1524
1525 // Make the API request
1526 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
1527
1528 // Process the response
1529 if (is_wp_error($response)) {
1530 return "Sorry, there was an error processing your request.";
1531 }
1532
1533 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1534
1535 if (isset($response_body['choices'][0]['message']['content'])) {
1536 return trim($response_body['choices'][0]['message']['content']);
1537 } else {
1538 return "Sorry, I couldn't process that request.";
1539 }
1540 }
1541
1542 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
1543 // Get system prompt instructions from options for Claude's top-level system parameter
1544 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
1545
1546 // Ensure consistency: Replace 'bot' role with 'assistant' in conversation history
1547 foreach ($conversation_history as &$message) {
1548 if ($message['role'] === 'bot') {
1549 $message['role'] = 'assistant';
1550 }
1551 }
1552
1553 // Add relevant content as the latest user message in conversation history
1554 $conversation_history[] = [
1555 'role' => 'user',
1556 'content' => $relevant_content
1557 ];
1558
1559 // Build the request body with Claude's expected structure, using system instructions as a top-level parameter
1560 $body = json_encode([
1561 'model' => $selected_model,
1562 'max_tokens' => 1000,
1563 'temperature' => 0.8,
1564 'system' => $system_prompt_instructions, // Set the system prompt at the top level as required
1565 'messages' => $conversation_history
1566 ]);
1567
1568 // Set up the API request with the necessary headers
1569 $args = [
1570 'body' => $body,
1571 'headers' => [
1572 'Content-Type' => 'application/json',
1573 'x-api-key' => $claude_api_key,
1574 'anthropic-version' => '2023-06-01',
1575 ],
1576 'timeout' => 60,
1577 'redirection' => 5,
1578 'blocking' => true,
1579 'httpversion' => '1.0',
1580 'sslverify' => true,
1581 ];
1582
1583 // Make the API request
1584 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
1585
1586 /*
1587 // Check for errors and log the response for debugging
1588 if (is_wp_error($response)) {
1589 error_log("Claude API request error: " . print_r($response->get_error_message(), true));
1590 return "Sorry, there was an error processing your request.";
1591 }
1592 */
1593
1594
1595 // Decode the response and parse according to the expected Claude response structure
1596 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1597 //error_log("Claude API response: " . print_r($response_body, true));
1598
1599 // Check if the response has the expected 'content' array with 'text' blocks
1600 if (isset($response_body['content'][0]['text'])) {
1601 return trim($response_body['content'][0]['text']);
1602 } else {
1603 return "Sorry, I couldn't process that request.";
1604 }
1605 }
1606
1607 public function mxchat_dismiss_pre_chat_message() {
1608 // Get and sanitize the user identifier
1609 $user_id = $this->mxchat_get_user_identifier();
1610 $user_id = sanitize_key($user_id);
1611
1612 // Set a transient to track that the user has dismissed the pre-chat message
1613 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
1614 set_transient($transient_key, true, DAY_IN_SECONDS);
1615
1616 wp_send_json_success();
1617 }
1618
1619 public function mxchat_check_pre_chat_message_status() {
1620 // Get and sanitize the user identifier
1621 $user_id = $this->mxchat_get_user_identifier();
1622 $user_id = sanitize_key($user_id);
1623
1624 // Check if the transient exists (i.e., if the message was dismissed)
1625 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
1626 $dismissed = get_transient($transient_key);
1627
1628 // Log the result to see if it's being set correctly
1629 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
1630
1631 if ($dismissed) {
1632 wp_send_json_success(['dismissed' => true]);
1633 } else {
1634 wp_send_json_success(['dismissed' => false]);
1635 }
1636
1637 wp_die();
1638 }
1639
1640 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
1641 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
1642 return 0;
1643 }
1644
1645 $dotProduct = array_sum(array_map(function ($a, $b) {
1646 return $a * $b;
1647 }, $vectorA, $vectorB));
1648 $normA = sqrt(array_sum(array_map(function ($a) {
1649 return $a * $a;
1650 }, $vectorA)));
1651 $normB = sqrt(array_sum(array_map(function ($b) {
1652 return $b * $b;
1653 }, $vectorB)));
1654
1655 if ($normA == 0 || $normB == 0) {
1656 return 0;
1657 }
1658
1659 return $dotProduct / ($normA * $normB);
1660 }
1661
1662 public function mxchat_enqueue_scripts_styles() {
1663 // Define version numbers for the styles and scripts
1664 $chat_style_version = '1.4.5'; // Replace with your actual version
1665 $chat_script_version = '1.4.5'; // Replace with your actual version
1666
1667 // Enqueue the script
1668 wp_enqueue_script(
1669 'mxchat-chat-js',
1670 plugin_dir_url(__FILE__) . '../js/chat-script.js',
1671 array('jquery'),
1672 $chat_script_version,
1673 true
1674 );
1675
1676 // Enqueue the CSS
1677 wp_enqueue_style(
1678 'mxchat-chat-css',
1679 plugin_dir_url(__FILE__) . '../css/chat-style.css',
1680 array(),
1681 $chat_style_version
1682 );
1683
1684 // Fetch options from the database
1685 $this->options = get_option('mxchat_options');
1686
1687 // Prepare settings for JavaScript
1688 $style_settings = array(
1689 'ajax_url' => admin_url('admin-ajax.php'),
1690 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
1691 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
1692 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
1693 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
1694 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
1695 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
1696 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
1697 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
1698 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
1699 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
1700 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
1701 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
1702 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
1703 'icon_color' => $this->options['icon_color'] ?? '#fff',
1704 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
1705 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
1706 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
1707 );
1708
1709 // Pass the settings to the script
1710 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
1711 }
1712
1713
1714 public function mxchat_reset_rate_limits() {
1715 global $wpdb;
1716
1717 // Define a cache key pattern for rate limits
1718 $cache_key_pattern = 'mxchat_chat_limit_%';
1719
1720 // Retrieve all option names matching the pattern
1721 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
1722 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
1723
1724 // db call ok; no-cache ok
1725 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
1726 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
1727
1728 // Clear the relevant cache entries
1729 foreach ($option_names as $option_name) {
1730 wp_cache_delete($option_name, 'options');
1731 }
1732
1733 // Optionally, clear a general cache if you have one
1734 wp_cache_delete('mxchat_all_chat_limits', 'options');
1735 }
1736
1737 private function mxchat_fetch_woocommerce_products() {
1738 // Ensure WooCommerce is active
1739 if (!class_exists('WooCommerce')) {
1740 return [];
1741 }
1742
1743 $args = array(
1744 'post_type' => 'product',
1745 'post_status' => 'publish',
1746 'posts_per_page' => -1,
1747 );
1748
1749 $products = get_posts($args);
1750 $product_data = [];
1751
1752 foreach ($products as $product) {
1753 $product_id = $product->ID;
1754 $product_obj = wc_get_product($product_id);
1755
1756 $product_data[] = array(
1757 'id' => $product_id,
1758 'name' => $product_obj->get_name(),
1759 'description' => $product_obj->get_description(),
1760 'short_description' => $product_obj->get_short_description(),
1761 'url' => get_permalink($product_id),
1762 'price' => $product_obj->get_regular_price(),
1763 'sale_price' => $product_obj->get_sale_price(),
1764 'stock_status' => $product_obj->get_stock_status(),
1765 'sku' => $product_obj->get_sku(),
1766 'in_stock' => $product_obj->is_in_stock(),
1767 'total_sales' => $product_obj->get_total_sales(),
1768 );
1769 }
1770
1771 return $product_data;
1772 }
1773
1774 }
1775 ?>
1776