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

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