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

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