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

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