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

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