PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 1.6.4
MxChat – AI Chatbot & Content Generation for WordPress v1.6.4
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / includes / class-mxchat-integrator.php

class-mxchat-integrator.php in MxChat – AI Chatbot & Content Generation for WordPress 1.6.4, at includes/class-mxchat-integrator.php

3,810 lines 149.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 class MxChat_Integrator {
7 private $options;
8 private $chat_count;
9 private $fallbackResponse;
10 private $productCardHtml;
11 private $word_handler;
12
13 public function __construct() {
14 $this->options = get_option('mxchat_options');
15 $this->chat_count = get_option('mxchat_chat_count', 0);
16 $this->word_handler = new MXChat_Word_Handler($this->options);
17
18 // Add WooCommerce hooks
19 add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3);
20
21 // Ensure embeddings are removed when a product is moved to trash or permanently deleted
22 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
23 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
24
25 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
26 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
27 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
28
29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31 // Add the AJAX actions for checking if the pre-chat message was dismissed
32 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
33 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
34
35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
37
38 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
39 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
40
41 if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
42 wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
43 }
44
45 // Add REST API routes registration
46 add_action('rest_api_init', array($this, 'register_routes'));
47
48 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
49 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
50
51 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
52
53 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
54 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
55 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
56 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
57
58 // Add these with your other add_action hooks
59 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
60 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
61 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
62 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
63 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
64 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
65
66 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
67 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
68 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
69 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
70 }
71
72 public function mxchat_handle_product_change($post_id, $post, $update) {
73 // Ensure this is a product post type
74 if ($post->post_type !== 'product') {
75 return;
76 }
77
78 // Only generate embeddings if the product is published
79 if ($post->post_status === 'publish') {
80 // Delay the embedding slightly to ensure all product data is available
81 add_action('shutdown', function() use ($post_id) {
82 $product = wc_get_product($post_id);
83 if ($product && $product->get_price() !== '') {
84 $this->mxchat_store_product_embedding($product);
85 } else {
86 // Optionally, log or handle the case where product data is incomplete
87 // error_log(esc_html__("Product {$post_id} does not have complete data. Embedding not generated.", 'mxchat'));
88 }
89 });
90 }
91 }
92
93 public function mxchat_handle_product_delete($post_id) {
94 if (get_post_type($post_id) !== 'product') {
95 return;
96 }
97
98 global $wpdb;
99 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
100
101 // Delete the embedding associated with this product
102 $wpdb->delete($table_name, array('source_url' => get_permalink($post_id)), array('%s'));
103 }
104
105 private function mxchat_store_product_embedding($product) {
106 if (isset($this->options['enable_woocommerce_integration']) &&
107 ($this->options['enable_woocommerce_integration'] === '1' ||
108 $this->options['enable_woocommerce_integration'] === 'on')) {
109
110 $source_url = get_permalink($product->get_id());
111 $regular_price = $product->get_regular_price();
112 $sale_price = $product->get_sale_price();
113 $price = $sale_price ?: $regular_price;
114
115 $description = $product->get_description() . "\n\n" .
116 esc_html__('Short Description:', 'mxchat') . " " . $product->get_short_description() . "\n" .
117 esc_html__('Price:', 'mxchat') . " " . $regular_price . "\n" .
118 esc_html__('Sale Price:', 'mxchat') . " " . ($sale_price ?: esc_html__('N/A', 'mxchat')) . "\n" .
119 esc_html__('SKU:', 'mxchat') . " " . $product->get_sku();
120
121 global $wpdb;
122 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
123
124 // Delete any existing embedding for this product
125 $wpdb->delete($table_name, array('source_url' => $source_url), array('%s'));
126
127 // Submit the new content and embedding to the database
128 MxChat_Utils::submit_content_to_db($description, $source_url, $this->options['api_key']);
129 }
130 }
131
132
133
134
135 private function mxchat_increment_chat_count() {
136 $chat_count = get_option('mxchat_chat_count', 0);
137 $chat_count++;
138 update_option('mxchat_chat_count', $chat_count);
139 }
140
141 function mxchat_fetch_conversation_history() {
142 if (empty($_POST['session_id'])) {
143 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
144 wp_die();
145 }
146
147 $session_id = sanitize_text_field($_POST['session_id']);
148 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
149 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
150
151 if (empty($history)) {
152 // Even if history is empty, return the chat mode
153 wp_send_json_success([
154 'conversation' => [],
155 'chat_mode' => $chat_mode
156 ]);
157 wp_die();
158 }
159
160 wp_send_json_success([
161 'conversation' => $history,
162 'chat_mode' => $chat_mode
163 ]);
164 wp_die();
165 }
166 private function mxchat_fetch_conversation_history_for_ajax($session_id) {
167 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history based on session ID
168 $formatted_history = [];
169
170 // Format the history to align with the expected structure for OpenAI
171 foreach ($history as $entry) {
172 $formatted_history[] = [
173 'role' => $entry['role'], // Ensure this matches 'user' or 'assistant'
174 'content' => $entry['content']
175 ];
176 }
177
178 return $formatted_history;
179 }
180
181
182 private function mxchat_fetch_conversation_history_for_ai($session_id) {
183 $history = get_option("mxchat_history_{$session_id}", []);
184
185 $formatted_history = [];
186 $max_tokens = 120000; // Adjust based on your model's context window
187 $reserved_tokens = 4000; // Reserve tokens for current query and system/context prompt
188 $current_token_count = 0;
189
190 foreach ($history as $entry) {
191 // Skip messages containing HTML
192 if ($entry['content'] !== strip_tags($entry['content'])) {
193 continue;
194 }
195
196 // Calculate tokens for this entry
197 $tokens_in_entry = str_word_count($entry['content']) * 1.5; // Rough estimate: ~1.5 tokens per word
198 $current_token_count += $tokens_in_entry;
199
200 // If adding this entry exceeds the limit, stop
201 if ($current_token_count + $reserved_tokens > $max_tokens) {
202 break;
203 }
204
205 $formatted_history[] = [
206 'role' => $entry['role'],
207 'content' => $entry['content']
208 ];
209 }
210
211 return $formatted_history;
212 }
213
214
215 public function register_routes() {
216 //error_log(esc_html__('Registering MxChat REST routes', 'mxchat'));
217
218 register_rest_route('mxchat/v1', '/stream', [
219 'methods' => 'GET',
220 'callback' => [$this, 'mxchat_stream_events'],
221 'permission_callback' => [$this, 'verify_chat_session'],
222 ]);
223
224 register_rest_route('mxchat/v1', '/agent-response', [
225 'methods' => 'POST',
226 'callback' => [$this, 'mxchat_handle_agent_response'],
227 'permission_callback' => [$this, 'verify_slack_request'],
228 ]);
229
230 register_rest_route('mxchat/v1', '/slack-interaction', [
231 'methods' => 'POST',
232 'callback' => [$this, 'handle_slack_interaction'],
233 'permission_callback' => [$this, 'verify_slack_request'],
234 ]);
235
236 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
237 }
238
239 /**
240 * Verify valid chat session
241 */
242 public function verify_chat_session($request) {
243 $session_id = $request->get_param('session_id');
244 if (empty($session_id)) {
245 //error_log(esc_html__('Empty session ID in chat request', 'mxchat'));
246 return false;
247 }
248
249 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
250 return $chat_mode === 'agent';
251 }
252
253 /**
254 * Verify request is coming from Slack.
255 *
256 * @param WP_REST_Request $request
257 * @return bool True if valid, false otherwise.
258 */
259 public function verify_slack_request($request) {
260 // Get the Slack signing secret from your plugin options
261 $valid_key = $this->options['live_agent_secret_key'] ?? '';
262
263 if (empty($valid_key)) {
264 //error_log(esc_html__('Slack signing secret not configured', 'mxchat'));
265 return false;
266 }
267
268 $timestamp = $request->get_header('X-Slack-Request-Timestamp');
269 $slack_signature = $request->get_header('X-Slack-Signature');
270
271 // Verify timestamp to prevent replay attacks
272 if (abs(time() - intval($timestamp)) > 300) {
273 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
274 return false;
275 }
276
277 // Get raw request body
278 $request_body = file_get_contents('php://input');
279
280 // Create the signature base string
281 $sig_basestring = "v0:{$timestamp}:{$request_body}";
282
283 // Calculate expected signature
284 $my_signature = 'v0=' . hash_hmac('sha256', $sig_basestring, $valid_key);
285
286 // Compare signatures
287 return hash_equals($my_signature, $slack_signature);
288 }
289 public function mxchat_stream_events(WP_REST_Request $request) {
290 header('Content-Type: text/event-stream');
291 header('Cache-Control: no-cache');
292 header('Connection: keep-alive');
293
294 $session_id = sanitize_text_field($request->get_param('session_id'));
295 $last_seen_id = sanitize_text_field($request->get_param('last_seen_id')) ?: '';
296
297 if (empty($session_id)) {
298 echo esc_html__("event: error\ndata: ", 'mxchat') . esc_html__('Missing session_id', 'mxchat') . "\n\n";
299 flush();
300 exit;
301 }
302
303 $history = get_option("mxchat_history_{$session_id}", []);
304
305 // Filter only new messages
306 $new_messages = array_filter($history, function ($message) use ($last_seen_id) {
307 return !empty($message['id']) && $message['id'] > $last_seen_id;
308 });
309
310 // Send new messages if available
311 if (!empty($new_messages)) {
312 echo esc_html__("event: newMessages\ndata: ", 'mxchat') . json_encode(array_values($new_messages)) . "\n\n";
313 } else {
314 // Keep the connection alive
315 echo esc_html__("event: keepAlive\ndata: ", 'mxchat') . "{}\n\n";
316 }
317 flush();
318 exit;
319 }
320
321
322
323
324 private function mxchat_save_chat_message($session_id, $role, $message) {
325 global $wpdb;
326
327 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
328 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
329
330 // 1) Extract agent name if present
331 $agent_name = '';
332 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
333 $agent_name = $matches[1];
334 $message = str_replace("Agent: $agent_name - ", '', $message);
335
336 $session_meta_key = "mxchat_agent_name_{$session_id}";
337 if (empty(get_option($session_meta_key))) {
338 update_option($session_meta_key, $agent_name);
339 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
340 }
341 }
342
343 // 2) Generate unique message_id
344 $message_id = uniqid();
345 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
346
347 // 3) Determine user_id
348 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
349
350 // 4) Determine user_identifier
351 $user_identifier = $agent_name
352 ? $agent_name
353 : MxChat_User::mxchat_get_user_identifier();
354
355 // 5) Determine displayed_name
356 $user_email = MxChat_User::mxchat_get_user_email();
357 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
358
359 // 6) Check for a saved email in wp_options
360 $email_option_key = "mxchat_email_{$session_id}";
361 $saved_email = get_option($email_option_key);
362 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
363
364 // If found, update DB user_email
365 if ($saved_email) {
366 $update_res = $wpdb->update(
367 $table_name,
368 ['user_email' => $saved_email],
369 ['session_id' => $session_id],
370 ['%s'],
371 ['%s']
372 );
373 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
374 }
375
376 // 7) Save to session history in wp_options
377 $history_key = "mxchat_history_{$session_id}";
378 $history = get_option($history_key, []);
379 $history[] = [
380 'id' => $message_id,
381 'role' => $role,
382 'content' => $message,
383 'timestamp' => round(microtime(true) * 1000),
384 'agent_name' => $displayed_name,
385 ];
386 update_option($history_key, $history);
387 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
388
389 // 8) Save the message to DB (INSERT)
390 $insert_data = [
391 'user_id' => $user_id,
392 'user_identifier'=> $user_identifier,
393 'user_email' => $saved_email ?: $user_email,
394 'session_id' => $session_id,
395 'role' => $role,
396 'message' => $message,
397 'timestamp' => current_time('mysql', 1),
398 ];
399 $wpdb->insert($table_name, $insert_data);
400 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
401
402 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
403 return $message_id;
404 }
405
406 public function mxchat_handle_save_email_and_response() {
407 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
408
409 // Validate nonce
410 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
411 error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
412 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
413 wp_die();
414 }
415
416 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
417 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
418
419 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
420
421 if (empty($session_id) || empty($email)) {
422 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
423 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
424 wp_die();
425 }
426
427 // 1) Always store in wp_options
428 $option_key = "mxchat_email_{$session_id}";
429 update_option($option_key, $email);
430 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$option_key} => {$email}");
431
432 // 2) (Optional) Also store in DB if a row already exists
433 global $wpdb;
434 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
435
436 // Make sure we have a valid placeholder in prepare
437 $sql = $wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE session_id = %s", $session_id);
438 $session_count = $wpdb->get_var($sql);
439
440 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
441
442 if ($session_count) {
443 // Update user_email if row(s) exist
444 $update_sql = $wpdb->prepare(
445 "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
446 $email,
447 $session_id
448 );
449 $wpdb->query($update_sql);
450 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
451 } else {
452 //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
453 }
454
455 // Provide success response
456 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
457 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
458 wp_send_json_success(['message' => $bot_message]);
459 wp_die();
460 }
461
462 public function mxchat_check_email_provided() {
463 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
464
465 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
466 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
467 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
468 }
469
470 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
471 if (empty($session_id)) {
472 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
473 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
474 }
475
476 // Check if the user is logged in
477 if (is_user_logged_in()) {
478 $current_user = wp_get_current_user();
479 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
480 wp_send_json_success(['logged_in' => true, 'email' => $current_user->user_email]);
481 }
482
483 $option_key = "mxchat_email_{$session_id}";
484 $stored_email = get_option($option_key, '');
485
486 //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
487
488 if (!empty($stored_email)) {
489 //error_log("[DEBUG] mxchat_check_email_provided -> Email found, returning success");
490 wp_send_json_success(['email' => $stored_email]);
491 } else {
492 //error_log("[DEBUG] mxchat_check_email_provided -> No email found, returning error");
493 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
494 }
495 }
496
497
498 // First, add this helper function to get the highest rate limit for a user's roles
499 private function get_user_role_rate_limit($user_id) {
500 error_log(esc_html__("Checking rate limit for user ID: ", 'mxchat') . $user_id);
501
502 if (!$user_id) {
503 error_log(esc_html__("No user ID provided, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
504 return $this->options['rate_limit_logged_out'] ?? '10';
505 }
506
507 $user = get_userdata($user_id);
508 if (!$user || !$user->roles) {
509 error_log(esc_html__("No user data or roles found, using logged-out limit: ", 'mxchat') . ($this->options['rate_limit_logged_out'] ?? '10'));
510 return $this->options['rate_limit_logged_out'] ?? '10';
511 }
512
513 error_log(esc_html__("User roles: ", 'mxchat') . print_r($user->roles, true));
514 error_log(esc_html__("Available role rate limits: ", 'mxchat') . print_r($this->options['role_rate_limits'] ?? [], true));
515
516 $max_limit = 0;
517 foreach ($user->roles as $role) {
518 error_log(esc_html__("Checking limit for role: ", 'mxchat') . $role);
519 if (isset($this->options['role_rate_limits'][$role])) {
520 $role_limit = $this->options['role_rate_limits'][$role];
521 error_log(esc_html__("Found limit for role ", 'mxchat') . $role . esc_html__(": ", 'mxchat') . $role_limit);
522
523 if ($role_limit === 'unlimited') {
524 error_log(esc_html__("Returning unlimited for role: ", 'mxchat') . $role);
525 return 'unlimited';
526 }
527
528 $max_limit = max($max_limit, (int)$role_limit);
529 error_log(esc_html__("Current max limit: ", 'mxchat') . $max_limit);
530 } else {
531 error_log(esc_html__("No limit found for role: ", 'mxchat') . $role);
532 }
533 }
534
535 $final_limit = $max_limit > 0 ? (string)$max_limit : '100';
536 error_log(esc_html__("Final rate limit: ", 'mxchat') . $final_limit);
537 return $final_limit;
538 }
539
540
541 // Add this to your plugin's main PHP file
542 public function mxchat_check_new_messages() {
543 if (!isset($_POST['session_id']) || !isset($_POST['last_seen_id'])) {
544 wp_send_json_error(['message' => 'Missing required parameters']);
545 wp_die();
546 }
547
548 $session_id = sanitize_text_field($_POST['session_id']);
549 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
550
551 // Get chat history
552 $history = get_option("mxchat_history_{$session_id}", []);
553
554 if (empty($history)) {
555 wp_send_json_success([
556 'hasNewMessages' => false,
557 'new_messages' => []
558 ]);
559 wp_die();
560 }
561
562 // Filter new messages
563 $new_messages = array_filter($history, function($message) use ($last_seen_id) {
564 return isset($message['id']) && $message['id'] > $last_seen_id;
565 });
566
567 // Sort by ID to ensure proper order
568 usort($new_messages, function($a, $b) {
569 return $a['id'] <=> $b['id'];
570 });
571
572 wp_send_json_success([
573 'hasNewMessages' => !empty($new_messages),
574 'new_messages' => array_values($new_messages),
575 'latestMessageId' => end($new_messages)['id'] ?? $last_seen_id
576 ]);
577 wp_die();
578 }
579 public function mxchat_handle_chat_request() {
580 global $wpdb;
581
582 // Reset fallback response at the start of each request
583 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
584 $this->productCardHtml = '';
585
586 // Get the actual WordPress user ID if logged in
587 $is_logged_in = is_user_logged_in();
588 if ($is_logged_in) {
589 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
590 } else {
591 // For logged-out users, use your existing identifier method
592 $user_id = $this->mxchat_get_user_identifier();
593 }
594
595 // Get and sanitize the user identifier
596 $user_id = sanitize_key($user_id);
597
598 // Determine if user is logged in
599 $is_logged_in = is_user_logged_in();
600 error_log("User logged in status: " . ($is_logged_in ? 'true' : 'false'));
601
602 // Get rate limit based on user status
603 // Get rate limit based on user status
604 $rate_limit = $is_logged_in
605 ? $this->get_user_role_rate_limit($user_id)
606 : ($this->options['rate_limit_logged_out'] ?? '10');
607
608 error_log("Selected rate limit: " . $rate_limit);
609
610 // Rest of your code remains the same
611 // If rate limit is 'unlimited', skip rate limiting checks
612 if ($rate_limit !== 'unlimited') {
613 // Convert rate limit to integer
614 $rate_limit = intval($rate_limit);
615 // Setup rate limiting
616 $rate_limit_transient_key = 'mxchat_chat_limit_' . $user_id;
617 $chat_count = get_transient($rate_limit_transient_key);
618 if ($chat_count === false) {
619 // Initialize new counter if none exists
620 $chat_count = 0;
621 }
622 // Check if user has exceeded their rate limit
623 if ($chat_count >= $rate_limit) {
624 // Get custom rate limit message or use default
625 $rate_limit_message = isset($this->options['rate_limit_message'])
626 ? $this->options['rate_limit_message']
627 : esc_html__('Rate limit exceeded. Please try again later.', 'mxchat');
628 // Replace placeholder if it exists in the message
629 $rate_limit_message = str_replace(
630 array('{limit}', '{count}', '{remaining}'),
631 array($rate_limit, $chat_count, max(0, $rate_limit - $chat_count)),
632 $rate_limit_message
633 );
634 wp_send_json([
635 'success' => false,
636 'message' => $rate_limit_message,
637 'status' => 'rate_limit_exceeded',
638 'limit' => $rate_limit,
639 'count' => $chat_count
640 ]);
641 wp_die();
642 }
643 // Increment the counter
644 $chat_count++;
645 // Store the updated count with 24-hour expiration
646 set_transient($rate_limit_transient_key, $chat_count, DAY_IN_SECONDS);
647 }
648
649 // Rest of your existing code...
650 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
651 //error_log("Session ID: $session_id");
652
653 if (empty($session_id)) {
654 //error_log("Error: Session ID is missing.");
655 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
656 wp_die();
657 }
658
659 // Validate and sanitize the incoming message
660 if (empty($_POST['message'])) {
661 //error_log("Error: No message received.");
662 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
663 wp_die();
664 }
665
666
667 $message = wp_strip_all_tags($_POST['message'], false);
668 $message = trim($message);
669
670 // Save the user's message
671 $this->mxchat_save_chat_message($session_id, 'user', $message);
672
673 // Check if the message is an email address
674 if (is_email($message)) {
675 // Add the email to Loops
676 $this->add_email_to_loops($message);
677
678 // Send success response
679 $response_message = $this->options['email_capture_response'] ??
680 esc_html__('Thank you! Your coupon is on the way!', 'mxchat');
681
682 wp_send_json([
683 'success' => true,
684 'status' => 'email_captured',
685 'message' => $response_message
686 ]);
687 wp_die();
688 }
689
690 $intent_info = '';
691
692 // Check chat mode
693 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
694 //error_log("Chat Mode: $chat_mode");
695
696 // Handle agent mode
697 if ($chat_mode === 'agent') {
698 // First, check for switch intent before doing anything else
699 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
700
701 // If we matched an intent and it's the switch intent, handle it
702 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
703 //error_log("Switch to chatbot intent detected");
704
705 // Update chat mode first
706 update_option("mxchat_mode_{$session_id}", 'ai');
707
708 // Clear any existing PDF context to start fresh
709 $this->clear_pdf_transients($session_id);
710
711 // Prepare clean switch response
712 $response_data = [
713 'text' => $this->fallbackResponse['text'],
714 'html' => '',
715 'session_id' => $session_id,
716 'chat_mode' => 'ai'
717 ];
718
719 // Save the mode switch message
720 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
721 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
722
723 // Send response and exit
724 wp_send_json($response_data);
725 wp_die();
726 } elseif (!$intent_matched) {
727 // No intent matched, handle live agent message
728 try {
729 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
730 //error_log("Message sent to agent.");
731
732 wp_send_json_success([
733 'status' => 'waiting_for_agent',
734 'message' => esc_html__('Message sent to live agent.', 'mxchat')
735 ]);
736 } catch (\Exception $e) {
737 //error_log("Error sending message to agent: " . $e->getMessage());
738 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
739 }
740 wp_die();
741 }
742 }
743
744 // Step 1: Check for new PDF URL in the message
745 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
746 $new_pdf_url = $matches[0];
747
748 // Check if this is likely a PDF-related request
749 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
750 $is_pdf_request = false;
751
752 foreach ($pdf_keywords as $keyword) {
753 if (stripos($message, $keyword) !== false) {
754 $is_pdf_request = true;
755 break;
756 }
757 }
758
759 // If it looks like a PDF request or we're waiting for a PDF URL
760 if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
761 // Validate HTTPS
762 if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
763 // Extract filename from URL
764 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
765
766 // Clear previous PDF transients
767 $this->clear_pdf_transients($session_id);
768
769 // Process new PDF
770 $max_pages = $this->options['pdf_max_pages'] ?? 69;
771 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
772
773 if ($embeddings === 'too_many_pages') {
774 $error_text = sprintf(
775 $this->options['pdf_intent_error_text'] ??
776 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
777 $max_pages
778 );
779 $this->fallbackResponse['text'] = $error_text;
780 } elseif ($embeddings) {
781 // Store new PDF information
782 // Create a more meaningful filename from URL
783 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
784
785 // If the filename is generic (like results_download.php), create a more descriptive one
786 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
787 strpos($pdf_filename, '.php') !== false) {
788 // Create a timestamp-based name
789 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
790 }
791
792 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
793 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
794 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
795 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
796
797 $success_text = $this->options['pdf_intent_success_text'] ??
798 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
799
800 // Return success with filename for UI update
801 wp_send_json([
802 'success' => true,
803 'message' => $success_text,
804 'data' => [
805 'filename' => $pdf_filename
806 ]
807 ]);
808 wp_die();
809 } else {
810 $error_text = $this->options['pdf_intent_error_text'] ??
811 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
812 $this->fallbackResponse['text'] = $error_text;
813 }
814
815 wp_send_json([
816 'success' => false,
817 'message' => $this->fallbackResponse['text']
818 ]);
819 wp_die();
820 }
821 }
822 }
823
824 // Step 2: Detect intent and handle intent-based responses
825 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
826 //error_log("Intent Matched: " . ($intent_matched ? "Yes" : "No"));
827
828 // Step 3: If intent is matched and handled, respond immediately
829 if ($intent_matched && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
830 //error_log("Intent response triggered.");
831 $response_data = [
832 'text' => $this->fallbackResponse['text'],
833 'html' => $this->fallbackResponse['html'],
834 'session_id' => $session_id
835 ];
836 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text'] . $this->fallbackResponse['html']);
837 wp_send_json($response_data);
838 wp_die();
839 }
840
841 // If no intent matched or product not found, proceed with AI response
842 //error_log("No matching intent or fallback. Generating AI response.");
843
844 // Step 4: Generate AI response
845 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
846 $this->mxchat_increment_chat_count();
847
848 // Generate embedding for the user's query
849 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
850 if (!is_array($user_message_embedding)) {
851 //error_log("Failed to generate message embedding for session $session_id");
852 wp_send_json_error(esc_html__('Error processing your message.', 'mxchat'));
853 wp_die();
854 }
855
856 // Build context with both knowledge base and PDF content if available
857 $context_content = "User asked: '{$message}'\n\n";
858
859 // Get relevant content from knowledge base
860 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
861 if (!empty($relevant_content)) {
862 $context_content .= "Relevant content from knowledge database:\n" . $relevant_content . "\n\n";
863 }
864
865
866 // Check for and include PDF content
867 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
868 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
869 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
870 if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
871 $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
872 if (!empty($relevant_pdf_pages)) {
873 $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
874 foreach ($relevant_pdf_pages as $page_data) {
875 $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
876 }
877 $context_content .= "\n";
878 }
879 }
880
881 // Check for and include Word content
882 $word_url = get_transient('mxchat_word_url_' . $session_id);
883 $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
884 $word_filename = get_transient('mxchat_word_filename_' . $session_id);
885 if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
886 $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
887 if (!empty($relevant_word_chunks)) {
888 $context_content .= "Relevant content from Word document '{$word_filename}':\n";
889 foreach ($relevant_word_chunks as $chunk_data) {
890 $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
891 }
892 $context_content .= "\n";
893 }
894 }
895 // Generate the response using the full context
896 $response = $this->mxchat_generate_response(
897 $context_content,
898 $this->options['api_key'],
899 $this->options['xai_api_key'],
900 $this->options['claude_api_key'],
901 $this->options['deepseek_api_key'],
902 $conversation_history
903 );
904
905 $this->mxchat_save_chat_message($session_id, 'bot', $response);
906
907 // Step 5: Save additional content if available
908 if (!empty($this->productCardHtml)) {
909 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
910 }
911
912 if (!empty($this->fallbackResponse['html'])) {
913 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
914 }
915
916 // Step 6: Return the response
917 $response_data = [
918 'text' => $response,
919 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
920 'session_id' => $session_id
921 ];
922
923 wp_send_json($response_data);
924 wp_die();
925 }
926
927
928 // Helper function to clear PDF and Word document related transients
929 private function clear_pdf_transients($session_id) {
930 // PDF transients
931 delete_transient('mxchat_pdf_url_' . $session_id);
932 delete_transient('mxchat_pdf_embeddings_' . $session_id);
933 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
934 delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
935
936 // Word document transients
937 delete_transient('mxchat_word_url_' . $session_id);
938 delete_transient('mxchat_word_filename_' . $session_id);
939 delete_transient('mxchat_word_embeddings_' . $session_id);
940 delete_transient('mxchat_include_word_in_context_' . $session_id);
941 delete_transient('mxchat_waiting_for_word_' . $session_id);
942 }
943
944 // New function to check intents and invoke the callback function
945 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
946 global $wpdb;
947
948 // Check chat mode
949 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
950 //error_log(esc_html__("Checking intents for mode: ", 'mxchat') . $chat_mode);
951
952 // Generate the user embedding
953 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
954 if (!is_array($user_embedding)) {
955 //error_log(esc_html__("Failed to generate user embedding", 'mxchat'));
956 return false;
957 }
958
959 // Fetch intents from the database
960 $table_name = $wpdb->prefix . 'mxchat_intents';
961
962 if ($chat_mode === 'agent') {
963 // Only fetch the switch intent when in agent mode
964 $query = $wpdb->prepare(
965 "SELECT * FROM $table_name WHERE callback_function = %s",
966 'mxchat_handle_switch_to_chatbot_intent'
967 );
968 //error_log(esc_html__("Searching for switch intent with query: ", 'mxchat') . $query);
969 $intents = $wpdb->get_results($query);
970 //error_log(esc_html__("Found ", 'mxchat') . count($intents) . esc_html__(" switch intents", 'mxchat'));
971 } else {
972 $intents = $wpdb->get_results("SELECT * FROM $table_name");
973 }
974
975 if (empty($intents)) {
976 //error_log(esc_html__("No intents found in database", 'mxchat'));
977 return false;
978 }
979
980 $highest_similarity = -INF;
981 $matched_intent = null;
982
983 foreach ($intents as $intent) {
984 //error_log(esc_html__("Checking intent: ", 'mxchat') . $intent->intent_label);
985 $intent_embedding_serialized = $intent->embedding_vector;
986 $intent_embedding = $intent_embedding_serialized
987 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
988 : null;
989
990 if (!is_array($intent_embedding)) {
991 //error_log(esc_html__("Invalid embedding for intent: ", 'mxchat') . $intent->intent_label);
992 continue;
993 }
994
995 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
996 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
997 //error_log(esc_html__("Similarity for ", 'mxchat') . $intent->intent_label . esc_html__(": ", 'mxchat') . $similarity . esc_html__(" (threshold: ", 'mxchat') . $intent_threshold . esc_html__(")", 'mxchat'));
998
999 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1000 $highest_similarity = $similarity;
1001 $matched_intent = $intent;
1002 //error_log(esc_html__("New best match: ", 'mxchat') . $intent->intent_label . esc_html__(" with similarity ", 'mxchat') . $similarity);
1003 }
1004 }
1005
1006 if ($matched_intent && method_exists($this, $matched_intent->callback_function)) {
1007 //error_log(esc_html__("Calling callback function: ", 'mxchat') . $matched_intent->callback_function);
1008 call_user_func([$this, $matched_intent->callback_function], $message, $user_id, $session_id);
1009 return true;
1010 }
1011
1012 //error_log(esc_html__("No matching intent found", 'mxchat'));
1013 return false;
1014 }
1015
1016 //verified good
1017 public function mxchat_handle_order_history($message, $user_id, $session_id) {
1018 if (!class_exists('WooCommerce')) {
1019 $this->fallbackResponse['text'] = esc_html__("I can't access order information right now. The order system seems to be unavailable.", 'mxchat');
1020 return true;
1021 }
1022
1023 $orderDetails = MxChat_WooCommerce::mxchat_fetch_user_orders_details('all');
1024
1025 if (empty($orderDetails)) {
1026 $this->fallbackResponse['text'] = esc_html__("I don't see any orders associated with your account. Are you logged in?", 'mxchat');
1027 return true;
1028 }
1029
1030 // Generate AI prompt with context
1031 $prompt = __("User asked about their orders:", 'mxchat') . " '{$message}'\n\n";
1032 $prompt .= __("Order information:", 'mxchat') . "\n";
1033 foreach ($orderDetails as $order) {
1034 $items_list = array_map(function($item) {
1035 return "{$item['name']} ({$item['quantity']})";
1036 }, $order['items']);
1037
1038 $prompt .= __("Order #", 'mxchat') . "{$order['order_id']}: {$order['formatted_total']} " . __("on", 'mxchat') . " {$order['date']}\n";
1039 $prompt .= __("Items:", 'mxchat') . " " . implode(', ', $items_list) . "\n";
1040 }
1041
1042 $prompt .= __("\nProvide a natural, conversational response focusing on the specific information the user asked about. ", 'mxchat');
1043 $prompt .= __("If they ask about a specific order or detail, provide just that information. ", 'mxchat');
1044 $prompt .= __("If they ask about license keys or sensitive information, inform them to check their email or contact support.", 'mxchat');
1045
1046 // Get AI response
1047 $ai_response = $this->mxchat_call_ai_api($prompt);
1048 $this->fallbackResponse['text'] = $ai_response['text'];
1049
1050 return true;
1051 }
1052
1053
1054 public function mxchat_handle_product_inquiry($message, $user_id, $session_id) {
1055 //error_log("PRODUCT INQUIRY START - Message: $message, User ID: $user_id");
1056
1057 // Sanitize user ID
1058 $sanitized_user_id = sanitize_key($user_id);
1059 //error_log("Sanitized User ID: $sanitized_user_id");
1060
1061 // Find product
1062 $product_id = $this->find_product_in_message($message);
1063 //error_log("Initial product ID from message: " . ($product_id ?? 'null'));
1064
1065 // Check transient if no product found
1066 if (!$product_id) {
1067 $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1068 //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1069 }
1070
1071 // Handle product inquiries
1072 if ($product_id && class_exists('WooCommerce')) {
1073 $product = wc_get_product($product_id);
1074 if ($product) {
1075 //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1076
1077 // Prepare product details
1078 $product_name = esc_html($product->get_name());
1079 $product_price = $product->get_price_html();
1080 $product_image_url = esc_url(wp_get_attachment_url($product->get_image_id()));
1081 $product_url = esc_url(get_permalink($product_id));
1082 $product_id_attr = esc_attr($product_id);
1083
1084 // Get product description
1085 $product_description = $product->get_description() ?: $product->get_short_description();
1086
1087 // Log embeddings process
1088 //error_log("Generating embedding for message: $message");
1089 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1090 //error_log("Finding relevant content for product inquiry");
1091 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1092
1093 // Build AI prompt
1094 $ai_prompt = esc_html__("You are a knowledgeable product assistant. ", 'mxchat');
1095 $ai_prompt .= esc_html__("Respond to this user query: '{$message}'\n\n", 'mxchat');
1096
1097 if (!empty($relevant_content)) {
1098 $ai_prompt .= esc_html__("Relevant information from our knowledge base:\n{$relevant_content}\n\n", 'mxchat');
1099 }
1100
1101 $ai_prompt .= esc_html__("Product details:\n", 'mxchat');
1102 $ai_prompt .= esc_html__("Name: {$product_name}\n", 'mxchat');
1103 $ai_prompt .= esc_html__("Price: ", 'mxchat') . strip_tags($product_price) . "\n";
1104 if ($product_description) {
1105 $ai_prompt .= esc_html__("Description: {$product_description}\n", 'mxchat');
1106 }
1107
1108 $ai_prompt .= esc_html__("\nInstructions:\n", 'mxchat');
1109 $ai_prompt .= esc_html__("1. Address the user's specific question or concern about the product\n", 'mxchat');
1110 $ai_prompt .= esc_html__("2. Incorporate relevant information from our knowledge base if provided\n", 'mxchat');
1111 $ai_prompt .= esc_html__("3. Highlight key product features that relate to their query\n", 'mxchat');
1112 $ai_prompt .= esc_html__("4. Include a natural suggestion to check out the product\n", 'mxchat');
1113 $ai_prompt .= esc_html__("5. Keep the response conversational and helpful\n", 'mxchat');
1114
1115 //error_log("Sending AI prompt: " . $ai_prompt);
1116
1117 // Build and log AI response
1118 $ai_response = $this->mxchat_call_ai_api($ai_prompt);
1119 //error_log("AI Response received for product inquiry: " . print_r($ai_response, true));
1120
1121 // Generate and log product card
1122 $product_card_html = <<<HTML
1123 <div class="mxchat-product-card">
1124 <a href="{$product_url}" target="_blank">
1125 <img src="{$product_image_url}" alt="{$product_name}" class="mxchat-product-image" />
1126 <h3 class="mxchat-product-name">{$product_name}</h3>
1127 </a>
1128 <div class="mxchat-product-price">{$product_price}</div>
1129 <button class="mxchat-add-to-cart-button" data-product-id="{$product_id_attr}">Add to Cart</button>
1130 </div>
1131 HTML;
1132
1133 // Save response and set transient
1134 $this->productCardHtml = $product_card_html;
1135 $this->fallbackResponse = [
1136 'text' => $ai_response['text'],
1137 'html' => $this->productCardHtml,
1138 ];
1139
1140 //error_log("Setting transient for product: $product_id with key: mxchat_last_discussed_product_$sanitized_user_id");
1141 set_transient('mxchat_last_discussed_product_' . $sanitized_user_id, $product_id, HOUR_IN_SECONDS);
1142
1143 // Verify transient was set
1144 $verify_transient = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1145 //error_log("Verification - Retrieved transient value: " . ($verify_transient ?? 'null'));
1146
1147 return true;
1148 }
1149 }
1150
1151 //error_log("PRODUCT INQUIRY END - No product found or WooCommerce not available");
1152 return false;
1153 }
1154
1155 //verified good
1156 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1157 // Log the message safely
1158 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1159
1160 // Initiate email capture flow
1161 $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1162
1163 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1164 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1165
1166 // Respond to the user
1167 wp_send_json(['message' => $response]);
1168 wp_die();
1169 }
1170
1171 //very good
1172 public function mxchat_generate_image($message, $user_id, $session_id) {
1173 // Prepare a prompt for DALL-E
1174 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1175
1176 // Use the existing OpenAI API key
1177 $openai_api_key = sanitize_text_field($this->options['api_key']);
1178
1179 // Call DALL-E to generate an image
1180 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1181
1182 // Check if the response contains an image URL
1183 if (isset($image_response['imageUrl'])) {
1184 $image_url = esc_url_raw($image_response['imageUrl']);
1185
1186 // Construct the HTML with a CSS class instead of inline styles
1187 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1188
1189 $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1190 } else {
1191 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1192 $response_html = '';
1193 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1194 }
1195
1196 // Save both text and HTML responses
1197 $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
1198
1199 // Prepare the response data
1200 $response_data = [
1201 'message' => $response_text,
1202 'html' => $response_html,
1203 'image_url' => $image_url ?? '',
1204 ];
1205
1206 // Send the JSON response
1207 header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1208 echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1209 wp_die();
1210 }
1211 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1212 $api_url = 'https://api.openai.com/v1/images/generations';
1213 $body = json_encode([
1214 'prompt' => sanitize_text_field($prompt),
1215 'n' => 1,
1216 'size' => '1024x1024',
1217 'model' => sanitize_text_field($model),
1218 ]);
1219
1220 $args = [
1221 'body' => $body,
1222 'headers' => [
1223 'Content-Type' => 'application/json',
1224 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
1225 ],
1226 'method' => 'POST',
1227 'timeout' => absint($timeout),
1228 ];
1229
1230 $response = wp_remote_post($api_url, $args);
1231
1232 if (is_wp_error($response)) {
1233 //error_log("DALL-E request failed: " . $response->get_error_message());
1234 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
1235 }
1236
1237 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1238
1239 if (isset($response_body['data'][0]['url'])) {
1240 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
1241 } else {
1242 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
1243 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
1244 }
1245 }
1246
1247 /**
1248 * Handle web search requests.
1249 *
1250 * Sends the refined search query to the Brave Search API and displays neatly formatted,
1251 * styled search results. Results are cached for performance.
1252 *
1253 * @since 1.0.0
1254 * @param string $message The user's search query.
1255 * @param string $user_id The user identifier.
1256 * @param string $session_id The current session ID.
1257 * @return void
1258 */
1259 public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1260 // Step 1: Interpret and refine the search query
1261 $refined_search_query = $this->mxchat_interpret_search_query( $message );
1262
1263 if ( empty( $refined_search_query ) ) {
1264 $this->fallbackResponse = array(
1265 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
1266 );
1267 return;
1268 }
1269
1270 // Retrieve and validate API settings
1271 $options = get_option( 'mxchat_options' );
1272 $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1273 $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1274
1275 if ( empty( $api_key ) ) {
1276 $this->fallbackResponse = array(
1277 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
1278 );
1279 return;
1280 }
1281
1282 // Build the API request URL
1283 $api_url = add_query_arg(
1284 array(
1285 'q' => rawurlencode( $refined_search_query ),
1286 'count' => $results_count,
1287 'text_decorations' => 'true',
1288 'rich_data' => 'true',
1289 ),
1290 'https://api.search.brave.com/res/v1/web/search'
1291 );
1292
1293 // Attempt to retrieve cached results first
1294 $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1295 $results = get_transient( $transient_key );
1296
1297 if ( false === $results ) {
1298 // Fetch new results from the Brave Search API
1299 $response = wp_remote_get(
1300 $api_url,
1301 array(
1302 'headers' => array(
1303 'Accept' => 'application/json',
1304 'Accept-Encoding' => 'gzip',
1305 'X-Subscription-Token'=> $api_key,
1306 ),
1307 'timeout' => 10,
1308 )
1309 );
1310
1311 if ( is_wp_error( $response ) ) {
1312 $this->fallbackResponse = array(
1313 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
1314 );
1315 return;
1316 }
1317
1318 $results = json_decode( wp_remote_retrieve_body( $response ), true );
1319
1320 if ( json_last_error() !== JSON_ERROR_NONE ) {
1321 $this->fallbackResponse = array(
1322 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
1323 );
1324 return;
1325 }
1326
1327 // Cache results for one hour
1328 set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1329 }
1330
1331 // Process and display results
1332 if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1333 $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1334
1335 // Only return HTML (no large text summary)
1336 $this->fallbackResponse = array(
1337 'html' => $html,
1338 );
1339
1340 // Save to chat history
1341 $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1342 } else {
1343 $this->fallbackResponse = array(
1344 'text' => sprintf(
1345 esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1346 esc_html( $refined_search_query )
1347 ),
1348 );
1349 }
1350 }
1351
1352
1353 /**
1354 * Format search results into a natural text summary.
1355 *
1356 * @since 1.0.0
1357 * @param array $results The search results from the API.
1358 * @param string $query The original search query.
1359 * @return string The text summary of the top results.
1360 */
1361 private function format_search_results( $results, $query ) {
1362 $summary = sprintf(
1363 esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1364 esc_html( $query )
1365 ) . "\n\n";
1366
1367 $max_results = min( count( $results ), 3 );
1368 for ( $i = 0; $i < $max_results; $i++ ) {
1369 $result = $results[ $i ];
1370 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1371 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1372
1373 // Append title and description to the summary
1374 $summary .= sprintf(
1375 "%s\n%s\n\n",
1376 esc_html( $title ),
1377 esc_html( $description )
1378 );
1379 }
1380
1381 return $summary;
1382 }
1383
1384 /**
1385 * Generate HTML markup for search results.
1386 *
1387 * @since 1.0.0
1388 * @param array $results The search results from the API.
1389 * @param string $query The user-refined query.
1390 * @return string The HTML markup for displaying the results.
1391 */
1392 private function generate_search_results_html( $results, $query ) {
1393 ob_start();
1394 ?>
1395 <div class="mxchat-search-results">
1396 <?php foreach ( $results as $result ) :
1397 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1398 $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1399 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1400 $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1401 $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1402 $domain = parse_url( $url, PHP_URL_HOST );
1403 ?>
1404 <div class="mxchat-search-item">
1405 <div class="mxchat-search-header">
1406 <?php if ( $favicon ) : ?>
1407 <img
1408 src="<?php echo esc_url( $favicon ); ?>"
1409 class="mxchat-site-icon"
1410 alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1411 width="16"
1412 height="16"
1413 />
1414 <?php endif; ?>
1415 <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1416 </div>
1417
1418 <div class="mxchat-search-content">
1419 <h3 class="mxchat-search-title">
1420 <a href="<?php echo esc_url( $url ); ?>"
1421 target="_blank"
1422 rel="noopener noreferrer"
1423 >
1424 <?php echo esc_html( $title ); ?>
1425 </a>
1426 </h3>
1427
1428 <?php if ( $thumbnail ) : ?>
1429 <div class="mxchat-search-thumbnail">
1430 <img
1431 src="<?php echo esc_url( $thumbnail ); ?>"
1432 alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1433 loading="lazy"
1434 />
1435 </div>
1436 <?php endif; ?>
1437
1438 <div class="mxchat-search-description">
1439 <?php echo esc_html( $description ); ?>
1440 </div>
1441 </div>
1442 </div>
1443 <?php endforeach; ?>
1444 </div>
1445 <?php
1446 return ob_get_clean();
1447 }
1448
1449
1450 //very good
1451 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1452
1453 // Step 1: Interpret the search query for better results
1454 $refined_search_query = $this->mxchat_interpret_search_query($message);
1455
1456
1457 // If no query was interpreted, return a fallback message
1458 if (empty($refined_search_query)) {
1459 $this->fallbackResponse = [
1460 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1461 'html' => "",
1462 ];
1463 return;
1464 }
1465
1466 // Brave API URL
1467 $api_url = 'https://api.search.brave.com/res/v1/images/search';
1468
1469 // Retrieve Brave API settings
1470 $options = get_option('mxchat_options');
1471 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1472
1473 if (empty($api_key)) {
1474 /*
1475 if (defined('WP_DEBUG') && WP_DEBUG) {
1476 error_log("Brave API key is missing.");
1477 }
1478 */
1479
1480 $this->fallbackResponse = [
1481 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1482 'html' => "",
1483 ];
1484 return;
1485 }
1486
1487 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1488 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
1489
1490 // Append query parameters based on settings
1491 $api_url = add_query_arg([
1492 'q' => rawurlencode($refined_search_query),
1493 'count' => $image_count,
1494 'safesearch' => $safe_search,
1495 ], $api_url);
1496
1497 /*
1498 // Log the final API URL for the search
1499 if (defined('WP_DEBUG') && WP_DEBUG) {
1500 error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1501 }
1502 */
1503
1504
1505 // Implement caching
1506 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1507 $body = get_transient($transient_key);
1508
1509 if (false === $body) {
1510 $args = [
1511 'headers' => [
1512 'Accept' => 'application/json',
1513 'Accept-Encoding' => 'gzip',
1514 'X-Subscription-Token' => $api_key,
1515 ],
1516 'timeout' => 10,
1517 ];
1518
1519 $response = wp_remote_get($api_url, $args);
1520
1521 if (is_wp_error($response)) {
1522 /*
1523 if (defined('WP_DEBUG') && WP_DEBUG) {
1524 error_log("Brave Image API request failed: " . $response->get_error_message());
1525 }
1526 */
1527
1528 $this->fallbackResponse = [
1529 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1530 'html' => "",
1531 ];
1532 return;
1533 }
1534
1535 $body = json_decode(wp_remote_retrieve_body($response), true);
1536 set_transient($transient_key, $body, HOUR_IN_SECONDS);
1537 }
1538
1539 // Process the API response
1540 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1541 $html_output = '<div class="mxchat-image-gallery">';
1542
1543 foreach ($body['results'] as $image) {
1544 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1545 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1546 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1547
1548 if ($image_url && $thumbnail_url) {
1549 $html_output .= '<div class="mxchat-image-item">';
1550 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
1551 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
1552 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
1553 $html_output .= '</a></div>';
1554 }
1555 }
1556
1557 $html_output .= '</div>';
1558
1559 $this->fallbackResponse = [
1560 'text' => "",
1561 'html' => $html_output,
1562 ];
1563
1564 // Save response in chat history
1565 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1566
1567 } else {
1568 /*
1569 if (defined('WP_DEBUG') && WP_DEBUG) {
1570 error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1571 }
1572 */
1573
1574 $this->fallbackResponse = [
1575 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1576 'html' => "",
1577 ];
1578 }
1579 }
1580 public function mxchat_interpret_search_query($user_query) {
1581 $system_prompt = esc_html__("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.", 'mxchat');
1582
1583 // Retrieve OpenAI API key using 'api_key' as the option key
1584 $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1585
1586 /*
1587 // Log the API key check, without exposing the key
1588 if (defined('WP_DEBUG') && WP_DEBUG) {
1589 error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
1590 }
1591 */
1592
1593 if (empty($api_key)) {
1594 //error_log("OpenAI API key is missing.");
1595 return sanitize_text_field($user_query); // Default to the original query if API key is missing
1596 }
1597
1598 $url = 'https://api.openai.com/v1/chat/completions';
1599 $args = [
1600 'headers' => [
1601 'Authorization' => 'Bearer ' . $api_key,
1602 'Content-Type' => 'application/json',
1603 ],
1604 'body' => wp_json_encode([
1605 'model' => 'gpt-3.5-turbo',
1606 'messages' => [
1607 ['role' => 'system', 'content' => $system_prompt],
1608 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1609 ],
1610 'temperature' => 0.2,
1611 'max_tokens' => 20,
1612 ]),
1613 'method' => 'POST',
1614 ];
1615
1616 $response = wp_remote_post($url, $args);
1617
1618 if (is_wp_error($response)) {
1619 //error_log("OpenAI request failed: " . $response->get_error_message());
1620 return sanitize_text_field($user_query); // Fallback to the original query if there's an error
1621 }
1622
1623 $body = json_decode(wp_remote_retrieve_body($response), true);
1624
1625 // Check for a valid response and sanitize output
1626 if (isset($body['choices'][0]['message']['content'])) {
1627 $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
1628
1629 /*
1630 // Log the interpreted query for debugging
1631 if (defined('WP_DEBUG') && WP_DEBUG) {
1632 error_log("Interpreted search query: " . $interpreted_query);
1633 }
1634 */
1635
1636 return $interpreted_query;
1637 } else {
1638 //error_log("Unexpected API response format: " . print_r($body, true));
1639 return sanitize_text_field($user_query);
1640 }
1641 }
1642
1643 public function mxchat_handle_add_to_cart_intent($message, $user_id, $session_id) {
1644 //error_log("ADD TO CART START - Message: $message, User ID: $user_id");
1645
1646 if (!class_exists('WooCommerce')) {
1647 //error_log("WooCommerce not available");
1648 return $this->generate_intent_response([
1649 'intent' => 'add_to_cart',
1650 'status' => 'error',
1651 'reason' => esc_html__('woocommerce_not_available', 'mxchat')
1652 ], $session_id);
1653 }
1654
1655 $sanitized_user_id = sanitize_key($user_id);
1656 // error_log("Sanitized User ID: $sanitized_user_id");
1657 $product_id = null;
1658
1659 // If button click, use transient
1660 if ($message === '!addtocart') {
1661 //error_log("Button click detected - using transient");
1662 $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1663 //error_log("Product ID from transient: " . ($product_id ?? 'null'));
1664 } else {
1665 // For text commands, search message first
1666 //error_log("Text command detected - searching message first");
1667 $product_id = $this->find_product_in_message($message);
1668 //error_log("Product ID from message search: " . ($product_id ?? 'null'));
1669
1670 // Only use transient as fallback if no product found in message
1671 if (!$product_id) {
1672 //error_log("No product found in message, checking transient as fallback");
1673 $product_id = get_transient('mxchat_last_discussed_product_' . $sanitized_user_id);
1674 //error_log("Product ID from transient fallback: " . ($product_id ?? 'null'));
1675 }
1676 }
1677
1678 // Handle no product found
1679 if (!$product_id) {
1680 //error_log("No product ID found through any method");
1681 return $this->generate_intent_response([
1682 'intent' => 'add_to_cart',
1683 'status' => 'error',
1684 'reason' => esc_html__('no_product_context', 'mxchat'),
1685 'action_needed' => esc_html__('request_product_name', 'mxchat'),
1686 'searched_message' => $message
1687 ], $session_id);
1688 }
1689
1690 // Get and verify product
1691 $product = wc_get_product($product_id);
1692 if (!$product) {
1693 //error_log("Product not found with ID: $product_id");
1694 return $this->generate_intent_response([
1695 'intent' => 'add_to_cart',
1696 'status' => 'error',
1697 'reason' => esc_html__('product_not_found', 'mxchat'),
1698 'product_id' => $product_id
1699 ], $session_id);
1700 }
1701
1702 //error_log("Found product: " . $product->get_name() . " (ID: $product_id)");
1703
1704 // Add to cart
1705 $added = WC()->cart->add_to_cart($product_id);
1706 if ($added) {
1707 //error_log("Successfully added to cart: " . $product->get_name());
1708 return $this->generate_intent_response([
1709 'intent' => 'add_to_cart',
1710 'status' => 'success',
1711 'product' => [
1712 'name' => $product->get_name(),
1713 'id' => $product_id
1714 ],
1715 'cart_url' => wc_get_cart_url(),
1716 'available_actions' => [
1717 esc_html__('view_cart', 'mxchat'),
1718 esc_html__('checkout', 'mxchat'),
1719 esc_html__('continue_shopping', 'mxchat')
1720 ]
1721 ], $session_id);
1722 } else {
1723 //error_log("Failed to add to cart: " . $product->get_name());
1724 return $this->generate_intent_response([
1725 'intent' => 'add_to_cart',
1726 'status' => 'error',
1727 'reason' => esc_html__('add_to_cart_failed', 'mxchat'),
1728 'product' => [
1729 'name' => $product->get_name(),
1730 'id' => $product_id
1731 ]
1732 ], $session_id);
1733 }
1734 }
1735 private function find_product_in_message($message) {
1736 global $wpdb;
1737
1738 // Get embedding for the search query
1739 $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1740 if (!is_array($query_embedding)) {
1741 return null;
1742 }
1743
1744 // Get relevant content as string
1745 $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1746 if (empty($relevant_content)) {
1747 // Return null to indicate no results and set fallback response
1748 $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Could you please be more specific about the product you're looking for?", 'mxchat');
1749 return null;
1750 }
1751
1752 // Extract product URLs from the content
1753 preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1754
1755 if (!empty($matches[0])) {
1756 // Try each URL found
1757 foreach ($matches[0] as $url) {
1758 // Clean the URL
1759 $url = rtrim($url, '/."\']');
1760
1761 // Get the product slug
1762 $path = parse_url($url, PHP_URL_PATH);
1763 $slug = basename(rtrim($path, '/'));
1764
1765 // Find product by slug
1766 $args = array(
1767 'post_type' => 'product',
1768 'post_status' => 'publish',
1769 'name' => $slug,
1770 'posts_per_page' => 1
1771 );
1772
1773 $products = get_posts($args);
1774
1775 if (!empty($products)) {
1776 $product_id = $products[0]->ID;
1777 $product = wc_get_product($product_id);
1778
1779 if ($product && $product->is_purchasable()) {
1780 return $product_id;
1781 }
1782 }
1783 }
1784 }
1785
1786 // Fallback: Look for product names in the content
1787 $products = wc_get_products([
1788 'status' => 'publish',
1789 'limit' => -1,
1790 'return' => 'all'
1791 ]);
1792
1793 foreach ($products as $product) {
1794 $name = $product->get_name();
1795 if (stripos($relevant_content, $name) !== false) {
1796 if ($product->is_purchasable()) {
1797 return $product->get_id();
1798 }
1799 }
1800 }
1801
1802 // If no product is found after all checks, set the fallback response
1803 $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1804 return null;
1805 }
1806
1807 // New method to handle intent responses
1808 private function generate_intent_response($context_content, $session_id) {
1809 // Convert the context array to a structured string for the AI
1810 $context_string = $this->format_intent_context($context_content);
1811
1812 // Generate AI response using the context
1813 $response = $this->mxchat_generate_response(
1814 $context_string,
1815 $this->options['api_key'],
1816 $this->options['xai_api_key'],
1817 $this->options['claude_api_key'],
1818 $this->options['deepseek_api_key'],
1819 $this->mxchat_fetch_conversation_history_for_ai($session_id)
1820 );
1821
1822 $this->fallbackResponse['text'] = $response;
1823 return true;
1824 }
1825 // Helper method to format intent context
1826 private function format_intent_context($context) {
1827 $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1828
1829 switch ($context['intent']) {
1830 case 'add_to_cart':
1831 if ($context['status'] === 'success') {
1832 $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1833 $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1834 $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1835 $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1836 $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1837 } else {
1838 $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1839 $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1840 switch ($context['reason']) {
1841 case 'woocommerce_not_available':
1842 $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1843 break;
1844 case 'no_product_context':
1845 $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1846 break;
1847 case 'product_not_found':
1848 $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1849 break;
1850 case 'add_to_cart_failed':
1851 $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1852 break;
1853 }
1854 }
1855 break;
1856 }
1857
1858 return $context_string;
1859 }
1860
1861
1862 public function mxchat_handle_checkout_intent($message, $user_id, $session_id) {
1863 if (!class_exists('WooCommerce')) {
1864 $this->fallbackResponse['text'] = esc_html__("I apologize, but the checkout feature isn't available at the moment.", 'mxchat');
1865 return true;
1866 }
1867
1868 // Check if cart has items
1869 if (WC()->cart->is_empty()) {
1870 $this->fallbackResponse['text'] = esc_html__("Your cart is empty at the moment. Would you like to see our products?", 'mxchat');
1871 return true;
1872 }
1873
1874 // Get cart summary
1875 $cart_count = WC()->cart->get_cart_contents_count();
1876 $cart_total = WC()->cart->get_total();
1877
1878 // Get and validate checkout URL
1879 $checkout_url = wc_get_checkout_url();
1880 if (!$checkout_url) {
1881 $this->fallbackResponse['text'] = esc_html__("I'm having trouble accessing the checkout page. Please try again in a moment.", 'mxchat');
1882 return true;
1883 }
1884
1885 wp_send_json([
1886 'text' => sprintf(
1887 esc_html__("You have %d item%s in your cart totaling %s. I'll redirect you to checkout now.", 'mxchat'),
1888 $cart_count,
1889 $cart_count > 1 ? esc_html__('s', 'mxchat') : '',
1890 strip_tags($cart_total)
1891 ),
1892 'redirect_url' => esc_url_raw($checkout_url)
1893 ]);
1894 wp_die();
1895 }
1896
1897
1898 //very good
1899 private function add_email_to_loops($email) {
1900 // Sanitize the email
1901 $email = sanitize_email($email);
1902
1903 // Retrieve and sanitize options
1904 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
1905 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
1906
1907 // Check for missing API key or mailing list ID
1908 if (empty($api_key) || empty($mailing_list_id)) {
1909 //error_log(esc_html__('Loops API key or mailing list ID is missing.', 'mxchat'));
1910 return;
1911 }
1912
1913 $data = array(
1914 'email' => $email,
1915 'subscribed' => true,
1916 'source' => __('MxChat AI Chatbot', 'mxchat'),
1917 'mailingLists' => array($mailing_list_id => true),
1918 );
1919
1920 $url = 'https://app.loops.so/api/v1/contacts/create';
1921 $args = array(
1922 'body' => wp_json_encode($data),
1923 'headers' => array(
1924 'Authorization' => 'Bearer ' . $api_key,
1925 'Content-Type' => 'application/json',
1926 ),
1927 'method' => 'POST',
1928 'timeout' => 45,
1929 );
1930
1931 $response = wp_remote_post($url, $args);
1932
1933 // Handle errors in the API request
1934 if (is_wp_error($response)) {
1935 //error_log(esc_html__('Error adding email to Loops: ', 'mxchat') . $response->get_error_message());
1936 return;
1937 }
1938
1939 // Check for non-200 HTTP responses
1940 $response_code = wp_remote_retrieve_response_code($response);
1941 if ($response_code != 200) {
1942 $response_body = wp_remote_retrieve_body($response);
1943 //error_log(esc_html__('Loops API responded with code ', 'mxchat') . $response_code . ': ' . $response_body);
1944 }
1945 }
1946
1947 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
1948 // Get the maximum number of pages allowed from admin settings
1949 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
1950
1951 // Retrieve options for dynamic texts
1952 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
1953 $success_text = $this->options['pdf_intent_success_text'] ?? __("I've processed the PDF. What questions do you have about it?", 'mxchat');
1954 $error_text = $this->options['pdf_intent_error_text'] ?? __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1955
1956 // Check for explicit request for new PDF
1957 $new_pdf_requested = stripos($message, 'new') !== false ||
1958 stripos($message, 'another') !== false ||
1959 stripos($message, 'different') !== false;
1960
1961 // If user mentions adding/reading a PDF, set waiting flag
1962 if (stripos($message, 'pdf') !== false ||
1963 stripos($message, 'document') !== false ||
1964 stripos($message, 'read') !== false) {
1965 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
1966 $this->fallbackResponse['text'] = $trigger_text;
1967 return;
1968 }
1969
1970 // If we're waiting for a URL or user requested new PDF
1971 if ($new_pdf_requested || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1972 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1973 // Process URL... (rest of your existing URL processing code)
1974 } else {
1975 $this->fallbackResponse['text'] = $trigger_text;
1976 }
1977 return;
1978 }
1979
1980 // Default to proceeding with conversation if no specific PDF action is needed
1981 $this->fallbackResponse['text'] = '';
1982 }
1983 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
1984 $upload_dir = wp_upload_dir();
1985 $temp_file = null;
1986
1987 try {
1988 // Handle URL vs local file
1989 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1990 // Validate and download the file from URL
1991 $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
1992 $response = wp_remote_get($pdf_source, ['timeout' => 60]);
1993
1994 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1995 //error_log(esc_html__("Failed to download PDF. Error: ", 'mxchat') . print_r($response, true));
1996 return false;
1997 }
1998
1999 file_put_contents($temp_file, wp_remote_retrieve_body($response));
2000
2001 // Validate that the downloaded file is a PDF
2002 $mime_type = mime_content_type($temp_file);
2003 if ($mime_type !== 'application/pdf') {
2004 //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
2005 unlink($temp_file);
2006 return false;
2007 }
2008 } else {
2009 // For local files, use the provided path directly
2010 $temp_file = $pdf_source;
2011 }
2012
2013 // Parse and process the PDF
2014 $parser = new \Smalot\PdfParser\Parser();
2015 $pdf = $parser->parseFile($temp_file);
2016 $pages = $pdf->getPages();
2017
2018 if (count($pages) > $max_pages) {
2019 //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
2020 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2021 unlink($temp_file);
2022 }
2023 return esc_html__('too_many_pages', 'mxchat');
2024 }
2025
2026 $embeddings = [];
2027 foreach ($pages as $page_number => $page) {
2028 $text = $page->getText();
2029
2030 // Ensure text is non-empty before generating embeddings
2031 if (empty(trim($text))) {
2032 //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
2033 continue;
2034 }
2035
2036 $embedding = $this->mxchat_generate_embedding(
2037 esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2038 $this->options['api_key']
2039 );
2040
2041 if ($embedding) {
2042 $embeddings[] = [
2043 'page_number' => $page_number + 1,
2044 'embedding' => $embedding,
2045 'text' => $text,
2046 ];
2047 } else {
2048 //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
2049 }
2050 }
2051
2052 // Clean up downloaded file if it was from URL
2053 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2054 unlink($temp_file);
2055 }
2056
2057 return $embeddings;
2058
2059 } catch (\Exception $e) {
2060 // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
2061
2062 // Cleanup in case of exception
2063 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2064 unlink($temp_file);
2065 }
2066
2067 return false;
2068 }
2069 }
2070 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
2071 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
2072
2073 $most_relevant = null;
2074 $highest_similarity = -INF;
2075
2076 foreach ($embeddings as $page_data) {
2077 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
2078
2079 if ($similarity > $highest_similarity) {
2080 $highest_similarity = $similarity;
2081 $most_relevant = $page_data['page_number'];
2082 }
2083 }
2084
2085 if (!is_null($most_relevant)) {
2086 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
2087 return array_filter($embeddings, function ($page) use ($page_numbers) {
2088 return in_array($page['page_number'], $page_numbers);
2089 });
2090 }
2091
2092 return [];
2093 }
2094 // Add this to your class
2095 public function handle_pdf_upload() {
2096 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2097
2098 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
2099 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
2100 return;
2101 }
2102
2103 $file = $_FILES['pdf_file'];
2104 $session_id = sanitize_text_field($_POST['session_id']);
2105 $original_filename = sanitize_text_field($file['name']);
2106
2107 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
2108 if ($file_type['type'] !== 'application/pdf') {
2109 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
2110 return;
2111 }
2112
2113 $upload_dir = wp_upload_dir();
2114 $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
2115 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
2116
2117 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
2118 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
2119 return;
2120 }
2121
2122 $this->clear_pdf_transients($session_id);
2123
2124 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2125 $embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
2126
2127 if ($embeddings === 'too_many_pages') {
2128 unlink($pdf_path);
2129 $error_message = sprintf(
2130 $this->options['pdf_intent_error_text'] ??
2131 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
2132 $max_pages
2133 );
2134 wp_send_json_error($error_message);
2135 return;
2136 }
2137
2138 if ($embeddings === false || empty($embeddings)) {
2139 unlink($pdf_path);
2140 $error_message = $this->options['pdf_intent_error_text'] ??
2141 esc_html__('The uploaded PDF appears to be empty or contains unsupported content.', 'mxchat');
2142 wp_send_json_error($error_message);
2143 return;
2144 }
2145
2146 if (!empty($embeddings)) {
2147 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
2148 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
2149 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
2150 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
2151
2152 $success_message = $this->options['pdf_intent_success_text'] ??
2153 esc_html__("I've processed the PDF. What questions do you have about it?", 'mxchat');
2154
2155 wp_send_json_success([
2156 'message' => $success_message,
2157 'filename' => $original_filename
2158 ]);
2159 return;
2160 }
2161
2162 unlink($pdf_path);
2163 $error_message = $this->options['pdf_intent_error_text'] ??
2164 esc_html__('Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.', 'mxchat');
2165 wp_send_json_error($error_message);
2166 return;
2167 }
2168 public function handle_pdf_remove() {
2169 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2170
2171 if (empty($_POST['session_id'])) {
2172 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
2173 wp_die();
2174 }
2175
2176 $session_id = sanitize_text_field($_POST['session_id']);
2177 $pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
2178
2179 if ($pdf_path && file_exists($pdf_path)) {
2180 unlink($pdf_path);
2181 }
2182
2183 $this->clear_pdf_transients($session_id);
2184
2185 wp_send_json_success([
2186 'message' => esc_html__('PDF removed successfully.', 'mxchat')
2187 ]);
2188 wp_die();
2189 }
2190
2191
2192 /**
2193 * Calls the AI API with the provided prompt.
2194 *
2195 * @param string $prompt The prompt to send to the AI.
2196 * @return array An array containing the AI's response text.
2197 */
2198 private function mxchat_call_ai_api( $prompt ) {
2199 //error_log( esc_html__( 'Calling AI API with the provided prompt.', 'mxchat' ) );
2200
2201 $api_key = $this->options['api_key'];
2202 if ( empty( $api_key ) ) {
2203 //error_log( esc_html__( 'API key is not set.', 'mxchat' ) );
2204 return [ 'text' => esc_html__( 'API key is not set.', 'mxchat' ) ];
2205 }
2206
2207 $url = 'https://api.openai.com/v1/chat/completions';
2208 $messages = [
2209 [
2210 'role' => 'system',
2211 '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.', 'mxchat' ),
2212 ],
2213 [
2214 'role' => 'user',
2215 'content' => $prompt,
2216 ],
2217 ];
2218
2219 $args = [
2220 'headers' => [
2221 'Authorization' => 'Bearer ' . $api_key,
2222 'Content-Type' => 'application/json',
2223 ],
2224 'body' => wp_json_encode(
2225 [
2226 'model' => 'gpt-4o',
2227 'messages' => $messages,
2228 'temperature' => 0.7,
2229 ]
2230 ),
2231 'timeout' => 10,
2232 'method' => 'POST',
2233 ];
2234
2235 $response = wp_remote_post( $url, $args );
2236
2237 if ( is_wp_error( $response ) ) {
2238 //error_log( esc_html__( 'Error communicating with AI API: ', 'mxchat' ) . $response->get_error_message() );
2239 return [ 'text' => esc_html__( 'Error communicating with AI API.', 'mxchat' ) ];
2240 }
2241
2242 $body = wp_remote_retrieve_body( $response );
2243 //error_log( esc_html__( 'API response body: ', 'mxchat' ) . $body );
2244
2245 $decoded_body = json_decode( $body, true );
2246 if ( isset( $decoded_body['choices'][0]['message']['content'] ) ) {
2247 return [ 'text' => $decoded_body['choices'][0]['message']['content'] ];
2248 } else {
2249 //error_log( esc_html__( 'Unexpected API response format: ', 'mxchat' ) . wp_json_encode( $decoded_body ) );
2250 return [ 'text' => esc_html__( 'No response received from AI.', 'mxchat' ) ];
2251 }
2252 }
2253
2254 /**
2255 * Fetches the AI response for the given prompt.
2256 *
2257 * @param string $prompt The prompt to send to the AI.
2258 * @return string|null The AI's response text or null if not available.
2259 */
2260 private function mxchat_fetch_ai_response( $prompt ) {
2261 $response = $this->mxchat_call_ai_api( $prompt );
2262 return isset( $response['text'] ) ? $response['text'] : null;
2263 }
2264 /**
2265 * Generates the AI prompt for product recommendations.
2266 *
2267 * @param array $recommendations An array of product recommendations.
2268 * @return string The generated AI prompt.
2269 */
2270 private function mxchat_generate_ai_recommendation_prompt($recommendations) {
2271 $recommendation_list = '';
2272 foreach ($recommendations as $index => $rec) {
2273 $number = $index + 1;
2274 $name = $rec['name'];
2275 $price = $rec['price'];
2276 $url = $rec['url'];
2277 $image = $rec['image'];
2278 $recommendation_list .= "{$number}. " . esc_html__('Product:', 'mxchat') . " {$name} (" . esc_html__('Price:', 'mxchat') . " \\${$price})\n";
2279 $recommendation_list .= " [Link]({$url})\n";
2280 $recommendation_list .= " ![Image]({$image})\n\n";
2281 }
2282
2283 $prompt = esc_html__('Based on the following list of products, generate a unique, friendly, and personalized response to a user. ', 'mxchat');
2284 $prompt .= esc_html__('If some products aren\'t exactly what the user asked for but share similar styles or patterns, acknowledge this and explain why you\'re suggesting them. ', 'mxchat');
2285 $prompt .= esc_html__('For each product, provide a brief justification that clearly explains why it\'s relevant, especially if it\'s a different type of product than requested. ', 'mxchat');
2286 $prompt .= esc_html__('Please number your responses to match the product numbers.', 'mxchat') . "\n\n";
2287 $prompt .= esc_html__('Products:', 'mxchat') . "\n\n{$recommendation_list}";
2288 $prompt .= esc_html__('Please ensure that the number of each product matches the order in which the products are listed.', 'mxchat');
2289
2290 return $prompt;
2291 }
2292 private function mxchat_generate_recommendations($user_id, $message) {
2293 // 1. Gather user context
2294 $user_context = $this->mxchat_get_user_context($user_id);
2295
2296 // Get cart item IDs for filtering
2297 $cart_item_ids = [];
2298 if (!empty($user_context['cart_items'])) {
2299 $cart_item_ids = array_map(function($item) {
2300 return $item['product_id'];
2301 }, $user_context['cart_items']);
2302 }
2303
2304 // 2. Get AI recommendation based on context
2305 $ai_suggestion = $this->mxchat_get_ai_shopping_suggestion($message, $user_context);
2306 //error_log("AI Shopping Suggestion: " . $ai_suggestion);
2307
2308 // 3. Generate embedding for the AI suggestion
2309 $suggestion_embedding = $this->mxchat_generate_embedding($ai_suggestion, $this->options['api_key']);
2310 if (!$suggestion_embedding) {
2311 //error_log("Could not generate embedding for AI suggestion");
2312 return ['recommendations' => [], 'sources' => []];
2313 }
2314
2315 // 4. Use existing relevant content function to find matches
2316 $relevant_content = $this->mxchat_find_relevant_content($suggestion_embedding);
2317
2318 // 5. Extract product URLs from the relevant content
2319 preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
2320
2321 $found_products = [];
2322 if (!empty($matches[0])) {
2323 foreach ($matches[0] as $url) {
2324 // Clean the URL
2325 $url = rtrim($url, '/."\']');
2326
2327 // Get the product slug
2328 $path = parse_url($url, PHP_URL_PATH);
2329 $slug = basename(rtrim($path, '/'));
2330
2331 // Find product by slug
2332 $args = array(
2333 'post_type' => 'product',
2334 'post_status' => 'publish',
2335 'name' => $slug,
2336 'posts_per_page' => 1
2337 );
2338
2339 $products = get_posts($args);
2340
2341 if (!empty($products)) {
2342 $product_id = $products[0]->ID;
2343
2344 // Skip if product is in cart
2345 if (in_array($product_id, $cart_item_ids)) {
2346 //error_log("Skipping product ID $product_id - already in cart");
2347 continue;
2348 }
2349
2350 $product = wc_get_product($product_id);
2351
2352 if ($product && $product->is_purchasable() && !in_array($product_id, array_map(function($p) {
2353 return $p->get_id();
2354 }, $found_products))) {
2355 $found_products[] = $product;
2356 }
2357 }
2358 }
2359 }
2360
2361 // If no products found by URLs, look for product names in the content
2362 if (empty($found_products)) {
2363 $products = wc_get_products([
2364 'status' => 'publish',
2365 'limit' => -1,
2366 'return' => 'objects'
2367 ]);
2368
2369 foreach ($products as $product) {
2370 // Skip if product is in cart
2371 if (in_array($product->get_id(), $cart_item_ids)) {
2372 continue;
2373 }
2374
2375 $name = $product->get_name();
2376 if (stripos($relevant_content, $name) !== false) {
2377 if ($product->is_purchasable() && !in_array($product->get_id(), array_map(function($p) {
2378 return $p->get_id();
2379 }, $found_products))) {
2380 $found_products[] = $product;
2381 }
2382 }
2383 }
2384 }
2385
2386 // Keep searching until we have 4 products or run out of options
2387 $formatted_recommendations = [];
2388 foreach ($found_products as $product) {
2389 if (count($formatted_recommendations) >= 4) break;
2390
2391 $formatted_recommendations[] = [
2392 'name' => $product->get_name(),
2393 'price' => $product->get_price(),
2394 'url' => get_permalink($product->get_id()),
2395 'image' => wp_get_attachment_url($product->get_image_id())
2396 ];
2397 }
2398
2399 return [
2400 'recommendations' => $formatted_recommendations,
2401 'sources' => [__('AI-powered personalized recommendations', 'mxchat')]
2402 ];
2403 }
2404 private function mxchat_get_ai_shopping_suggestion($message, $user_context) {
2405 $prompt = esc_html__("As a shopping assistant, analyze this user's context and generate a specific product search suggestion. ", 'mxchat');
2406 $prompt .= esc_html__("User's Question: \"{$message}\"\n\n", 'mxchat');
2407
2408 if (!empty($user_context['order_history'])) {
2409 $prompt .= esc_html__("Their recent orders include:\n", 'mxchat');
2410 foreach ($user_context['order_history'] as $order) {
2411 $prompt .= esc_html__("- {$order['name']} ({$order['date']})\n", 'mxchat');
2412 }
2413 }
2414
2415 if (!empty($user_context['cart_items'])) {
2416 $prompt .= esc_html__("\nThey currently have in their cart:\n", 'mxchat');
2417 foreach ($user_context['cart_items'] as $item) {
2418 $prompt .= esc_html__("- {$item['name']}\n", 'mxchat');
2419 }
2420 }
2421
2422 $prompt .= esc_html__("\nBased on their question and history, suggest a specific search query that would help find the most relevant products. ", 'mxchat');
2423 $prompt .= esc_html__("Respond with ONLY the search query, nothing else.", 'mxchat');
2424
2425 $response = $this->mxchat_call_ai_api($prompt);
2426 return isset($response['text']) ? trim($response['text']) : $message;
2427 }
2428 public function mxchat_handle_product_recommendations($message, $user_id, $session_id) {
2429 try {
2430 //error_log("Starting product recommendations for user: $user_id, session: $session_id");
2431
2432 // Pass the message to get context-aware recommendations
2433 $recommendation_data = $this->mxchat_generate_recommendations($user_id, $message);
2434 //error_log('Generated recommendation data: ' . wp_json_encode($recommendation_data));
2435
2436 if (empty($recommendation_data['recommendations'])) {
2437 //error_log("No recommendations found for user: $user_id");
2438 $this->fallbackResponse = [
2439 'text' => __("I couldn't find any product recommendations for you right now. Please try again later!", 'mxchat'),
2440 ];
2441 return;
2442 }
2443
2444 // Remove duplicates and limit to top 4 recommendations
2445 $unique_recommendations = [];
2446 foreach ($recommendation_data['recommendations'] as $rec) {
2447 $unique_recommendations[$rec['url']] = $rec;
2448 }
2449
2450 $unique_recommendations = array_slice($unique_recommendations, 0, 4);
2451 //error_log('Top 4 recommendations: ' . wp_json_encode($unique_recommendations));
2452
2453 $recommendations_summary = [];
2454 foreach ($unique_recommendations as $rec) {
2455 $recommendations_summary[] = [
2456 'name' => $rec['name'],
2457 'price' => strip_tags($rec['price']),
2458 'url' => $rec['url'],
2459 'image' => $rec['image'],
2460 ];
2461 }
2462
2463 // Generate AI prompt and fetch response
2464 $ai_prompt = $this->mxchat_generate_ai_recommendation_prompt($recommendations_summary);
2465 $ai_response = $this->mxchat_fetch_ai_response($ai_prompt);
2466
2467 if (empty($ai_response)) {
2468 $this->fallbackResponse = [
2469 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2470 ];
2471 return;
2472 }
2473
2474 // Split the AI's response into lines
2475 $ai_lines = preg_split('/\r\n|\r|\n/', $ai_response);
2476
2477 // Initialize variables
2478 $formatted_response = '';
2479 $justifications = [];
2480 $current_number = 0;
2481 $in_introduction = true;
2482 $introduction = '';
2483
2484 // Parse the AI response to separate the introduction and the justifications
2485 foreach ($ai_lines as $line) {
2486 if (preg_match('/^\s*(\d+)\.\s*(.*)$/', $line, $matches)) {
2487 // This line is a numbered justification
2488 $current_number = intval($matches[1]) - 1;
2489 $justifications[$current_number] = $matches[2];
2490 $in_introduction = false;
2491 } elseif ($in_introduction) {
2492 // This line is part of the introduction
2493 $introduction .= $line . ' ';
2494 } else {
2495 // This line is a continuation of the current justification
2496 if (isset($justifications[$current_number])) {
2497 $justifications[$current_number] .= ' ' . $line;
2498 }
2499 }
2500 }
2501
2502 // Build the formatted response
2503 if (!empty($introduction)) {
2504 $formatted_response .= esc_html(trim($introduction)) . "<br><br>";
2505 }
2506
2507 foreach ($recommendations_summary as $index => $rec) {
2508 $name = esc_html($rec['name']);
2509 $price = number_format((float)$rec['price'], 2, '.', '');
2510 $url = esc_url($rec['url']);
2511 $image = esc_url($rec['image']);
2512
2513 $formatted_response .= ($index + 1) . ". <strong>" . $name . "</strong> - <strong>$" . $price . "</strong><br>";
2514 $formatted_response .= "<img src=\"{$image}\" alt=\"{$name}\" style=\"max-width: 200px; height: auto; display: block; margin: 10px 0;\" /><br>";
2515
2516 if (isset($justifications[$index])) {
2517 $formatted_response .= "<em>" . esc_html(trim($justifications[$index])) . "</em><br><br>";
2518 }
2519 }
2520
2521 $this->fallbackResponse = [
2522 'text' => $formatted_response,
2523 ];
2524 //error_log('Final formatted response set.');
2525 } catch (Exception $e) {
2526 //error_log('Error in mxchat_handle_product_recommendations: ' . $e->getMessage());
2527 $this->fallbackResponse = [
2528 'text' => __('An unexpected error occurred while generating recommendations. Please try again later.', 'mxchat'),
2529 ];
2530 }
2531 }
2532 private function mxchat_get_user_context($user_id) {
2533 $context = [
2534 'order_history' => [],
2535 'cart_items' => [],
2536 'recently_viewed' => [],
2537 ];
2538
2539 // Get order history
2540 if (is_user_logged_in() && $user_id) {
2541 $orders = wc_get_orders([
2542 'customer_id' => $user_id,
2543 'limit' => 5, // Last 5 orders
2544 'orderby' => 'date',
2545 'order' => 'DESC',
2546 ]);
2547
2548 foreach ($orders as $order) {
2549 foreach ($order->get_items() as $item) {
2550 $context['order_history'][] = [
2551 'name' => $item->get_name(),
2552 'product_id' => $item->get_product_id(),
2553 'date' => $order->get_date_created()->format('Y-m-d')
2554 ];
2555 }
2556 }
2557 }
2558
2559 // Get cart items
2560 if (WC()->cart && WC()->cart->get_cart_contents_count() > 0) {
2561 foreach (WC()->cart->get_cart() as $cart_item) {
2562 $product = wc_get_product($cart_item['product_id']);
2563 if ($product) {
2564 $context['cart_items'][] = [
2565 'name' => $product->get_name(),
2566 'product_id' => $product->get_id()
2567 ];
2568 }
2569 }
2570 }
2571
2572 // Get recently viewed items from session
2573 $viewed_products = WC()->session->get('recently_viewed_products', []);
2574 foreach ($viewed_products as $product_id) {
2575 $product = wc_get_product($product_id);
2576 if ($product) {
2577 $context['recently_viewed'][] = [
2578 'name' => $product->get_name(),
2579 'product_id' => $product->get_id()
2580 ];
2581 }
2582 }
2583
2584 return $context;
2585 }
2586
2587
2588
2589
2590
2591
2592 function mxchat_fetch_new_messages() {
2593 $session_id = sanitize_text_field($_POST['session_id']);
2594 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2595 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
2596 $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2597
2598 if (empty($session_id)) {
2599 //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
2600 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
2601 wp_die();
2602 }
2603
2604 $history = get_option("mxchat_history_{$session_id}", []);
2605
2606 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2607 // If persistence is enabled, show all new messages
2608 if ($persistence_enabled) {
2609 return !empty($message['id']) &&
2610 strcmp($message['id'], $last_seen_id) > 0 &&
2611 $message['role'] === 'agent';
2612 }
2613
2614 // If persistence is disabled, only show messages after initial timestamp
2615 return !empty($message['id']) &&
2616 $message['role'] === 'agent' &&
2617 $message['timestamp'] > $initial_timestamp;
2618 });
2619
2620 //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
2621
2622 wp_send_json_success([
2623 'new_messages' => array_values($new_messages)
2624 ]);
2625 wp_die();
2626 }
2627
2628
2629 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2630 // First check if live agents are available
2631 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2632 if ($live_agent_available !== 'on') {
2633 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2634 $this->fallbackResponse = [
2635 'text' => $away_message,
2636 'html' => '',
2637 'images' => [],
2638 'chat_mode' => 'ai'
2639 ];
2640 wp_send_json([
2641 'text' => $away_message,
2642 'html' => '',
2643 'chat_mode' => 'ai',
2644 'session_id' => $session_id
2645 ]);
2646 wp_die();
2647 }
2648
2649 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2650 if (empty($slack_webhook_url)) {
2651 return false;
2652 }
2653
2654 // Get recent chat history (last 5 messages)
2655 $history = get_option("mxchat_history_{$session_id}", []);
2656 $recent_history = array_slice($history, -5); // Get last 5 messages
2657
2658 // Format conversation history
2659 $conversation_context = "";
2660 if (!empty($recent_history)) {
2661 $conversation_context = "*Recent Conversation:*\n";
2662 foreach ($recent_history as $hist_message) {
2663 $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
2664 $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
2665 }
2666 $conversation_context .= "\n";
2667 }
2668
2669 update_option("mxchat_mode_{$session_id}", 'agent');
2670
2671 $webhook_data = [
2672 'blocks' => [
2673 [
2674 'type' => 'header',
2675 'text' => [
2676 'type' => 'plain_text',
2677 'text' => '🔔 New Live Agent Request',
2678 'emoji' => true
2679 ]
2680 ],
2681 [
2682 'type' => 'section',
2683 'fields' => [
2684 [
2685 'type' => 'mrkdwn',
2686 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2687 ],
2688 [
2689 'type' => 'mrkdwn',
2690 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2691 ]
2692 ]
2693 ]
2694 ]
2695 ];
2696
2697 // Add conversation history if exists
2698 if (!empty($conversation_context)) {
2699 $webhook_data['blocks'][] = [
2700 'type' => 'section',
2701 'text' => [
2702 'type' => 'mrkdwn',
2703 'text' => $conversation_context
2704 ]
2705 ];
2706 }
2707
2708 // Add the current message
2709 $webhook_data['blocks'][] = [
2710 'type' => 'section',
2711 'text' => [
2712 'type' => 'mrkdwn',
2713 'text' => sprintf('*Current Message:*\n%s', $message)
2714 ]
2715 ];
2716
2717 // Add the reply button
2718 $webhook_data['blocks'][] = [
2719 'type' => 'actions',
2720 'elements' => [
2721 [
2722 'type' => 'button',
2723 'text' => [
2724 'type' => 'plain_text',
2725 'text' => '✍️ Reply',
2726 'emoji' => true
2727 ],
2728 'value' => $session_id,
2729 'action_id' => 'reply_to_user',
2730 'style' => 'primary'
2731 ]
2732 ]
2733 ];
2734
2735 $response = wp_remote_post($slack_webhook_url, [
2736 'body' => json_encode($webhook_data),
2737 'headers' => [
2738 'Content-Type' => 'application/json',
2739 ],
2740 ]);
2741
2742 if (is_wp_error($response)) {
2743 return false;
2744 }
2745
2746 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2747 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2748
2749 $this->fallbackResponse = [
2750 'text' => $success_message,
2751 'html' => '',
2752 'images' => [],
2753 'chat_mode' => 'agent'
2754 ];
2755
2756 wp_send_json([
2757 'success' => true,
2758 'text' => $success_message,
2759 'html' => '',
2760 'chat_mode' => 'agent',
2761 'session_id' => $session_id,
2762 'fallbackResponse' => $this->fallbackResponse
2763 ]);
2764 wp_die();
2765 }
2766 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2767 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2768
2769 if (empty($slack_webhook_url)) {
2770 //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
2771 return false;
2772 }
2773
2774 $webhook_data = [
2775 'blocks' => [
2776 [
2777 'type' => 'header',
2778 'text' => [
2779 'type' => 'plain_text',
2780 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2781 'emoji' => true
2782 ]
2783 ],
2784 [
2785 'type' => 'section',
2786 'fields' => [
2787 [
2788 'type' => 'mrkdwn',
2789 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2790 ],
2791 [
2792 'type' => 'mrkdwn',
2793 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2794 ]
2795 ]
2796 ],
2797 [
2798 'type' => 'section',
2799 'text' => [
2800 'type' => 'mrkdwn',
2801 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2802 ]
2803 ],
2804 [
2805 'type' => 'actions',
2806 'elements' => [
2807 [
2808 'type' => 'button',
2809 'text' => [
2810 'type' => 'plain_text',
2811 'text' => esc_html__('✍️ Reply', 'mxchat'),
2812 'emoji' => true
2813 ],
2814 'value' => $session_id,
2815 'action_id' => 'reply_to_user',
2816 'style' => 'primary'
2817 ]
2818 ]
2819 ]
2820 ]
2821 ];
2822
2823 $response = wp_remote_post($slack_webhook_url, [
2824 'body' => json_encode($webhook_data),
2825 'headers' => [
2826 'Content-Type' => 'application/json',
2827 ],
2828 ]);
2829
2830 if (is_wp_error($response)) {
2831 //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2832 return false;
2833 }
2834
2835 //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2836 return true;
2837 }
2838 public function handle_slack_interaction(WP_REST_Request $request) {
2839 //error_log('Received Slack interaction');
2840
2841 $payload = json_decode($request->get_param('payload'), true);
2842 //error_log('Payload: ' . print_r($payload, true));
2843
2844 // Handle button click
2845 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
2846 $session_id = $payload['actions'][0]['value'];
2847 $trigger_id = $payload['trigger_id'];
2848
2849 // Get Bot Token from settings
2850 $slack_token = $this->options['live_agent_bot_token'] ?? '';
2851
2852 if (empty($slack_token)) {
2853 //error_log('Slack Bot Token not configured');
2854 return new WP_REST_Response(['error' => esc_html__('Bot token not configured', 'mxchat')], 400);
2855 }
2856 $response = wp_remote_post('https://slack.com/api/views.open', [
2857 'headers' => [
2858 'Content-Type' => 'application/json',
2859 'Authorization' => 'Bearer ' . $slack_token
2860 ],
2861 'body' => json_encode([
2862 'trigger_id' => $trigger_id,
2863 'view' => [
2864 'type' => 'modal',
2865 'callback_id' => 'reply_modal',
2866 'title' => [
2867 'type' => 'plain_text',
2868 'text' => __('Reply to User', 'mxchat')
2869 ],
2870 'submit' => [
2871 'type' => 'plain_text',
2872 'text' => __('Send', 'mxchat')
2873 ],
2874 'close' => [
2875 'type' => 'plain_text',
2876 'text' => __('Cancel', 'mxchat')
2877 ],
2878 'blocks' => [
2879 [
2880 'type' => 'input',
2881 'block_id' => 'reply_block',
2882 'label' => [
2883 'type' => 'plain_text',
2884 'text' => sprintf(__('Reply to session: %s', 'mxchat'), $session_id)
2885 ],
2886 'element' => [
2887 'type' => 'plain_text_input',
2888 'action_id' => 'message',
2889 'multiline' => true,
2890 'placeholder' => [
2891 'type' => 'plain_text',
2892 'text' => __('Type your message here...', 'mxchat')
2893 ]
2894 ]
2895 ]
2896 ],
2897 'private_metadata' => $session_id
2898 ]
2899 ])
2900 ]);
2901
2902 //error_log('Views.open response: ' . print_r($response, true));
2903
2904 // Return immediate acknowledgment
2905 return new WP_REST_Response(['ok' => true]);
2906 }
2907
2908 // Handle modal submission
2909 // Handle modal submission
2910 if ($payload['type'] === 'view_submission') {
2911 $session_id = $payload['view']['private_metadata'];
2912 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
2913
2914 // Save the message (keep the message_id but don't include in response)
2915 $this->mxchat_save_chat_message($session_id, 'agent', $message);
2916
2917 // Keep the original response format for Slack
2918 return new WP_REST_Response([
2919 'response_action' => 'clear'
2920 ]);
2921 }
2922
2923 // Default acknowledgment
2924 return new WP_REST_Response(['ok' => true]);
2925 }
2926
2927 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2928 //error_log('Received agent response request');
2929 //error_log('Request data: ' . print_r($request->get_params(), true));
2930 // error_log('Raw body: ' . file_get_contents('php://input'));
2931
2932 // Get the data from Slack's slash command format
2933 $command_text = $request->get_param('text');
2934 // error_log('Command text: ' . $command_text);
2935
2936 if (empty($command_text)) {
2937 error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2938 return new WP_REST_Response([
2939 'error' => esc_html__('Command text is required. Format: /reply session_id message', 'mxchat')
2940 ], 400);
2941 }
2942
2943 // Split the command text into session_id and message
2944 $parts = explode(' ', $command_text, 2);
2945 if (count($parts) !== 2) {
2946 //error_log('Agent response error: Invalid command format');
2947 return new WP_REST_Response([
2948 'error' => esc_html__('Invalid format. Use: /reply session_id message', 'mxchat')
2949 ], 400);
2950 }
2951
2952 $session_id = sanitize_text_field($parts[0]);
2953 $message = sanitize_text_field($parts[1]);
2954
2955 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
2956
2957 // Save the message
2958 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2959
2960 if (!$message_id) {
2961 // error_log('Failed to save agent message');
2962 return new WP_REST_Response([
2963 'error' => esc_html__('Failed to save message', 'mxchat')
2964 ], 500);
2965 }
2966
2967 // Return success response in Slack's expected format
2968 return new WP_REST_Response([
2969 'response_type' => 'in_channel',
2970 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2971 ], 200);
2972 }
2973
2974
2975 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2976 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2977
2978 // Just update mode to AI
2979 update_option("mxchat_mode_{$session_id}", 'ai');
2980
2981 // Initialize states
2982 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2983 $this->productCardHtml = '';
2984
2985 // Set the response message
2986 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2987
2988 return true; // Intent was handled
2989 }
2990
2991
2992
2993
2994 // For the word upload handler
2995 public function mxchat_handle_word_upload() {
2996 // Delegate to word handler
2997 $this->word_handler->mxchat_handle_word_upload();
2998 }
2999
3000 // For the word removal handler
3001 public function mxchat_handle_word_remove() {
3002 // Delegate to word handler
3003 $this->word_handler->mxchat_handle_word_remove();
3004 }
3005
3006 // For the word status check
3007 public function mxchat_check_word_status() {
3008 // Delegate to word handler
3009 $this->word_handler->mxchat_check_word_status();
3010 }
3011
3012
3013 private function mxchat_get_user_identifier() {
3014 return MxChat_User::mxchat_get_user_identifier();
3015 }
3016
3017 private function mxchat_generate_embedding($text, $api_key) {
3018 $endpoint = 'https://api.openai.com/v1/embeddings';
3019
3020 $body = wp_json_encode([
3021 'input' => $text,
3022 'model' => 'text-embedding-ada-002'
3023 ]);
3024
3025 $args = [
3026 'body' => $body,
3027 'headers' => [
3028 'Content-Type' => 'application/json',
3029 'Authorization' => 'Bearer ' . $api_key,
3030 ],
3031 'timeout' => 60,
3032 'redirection' => 5,
3033 'blocking' => true,
3034 'httpversion' => '1.0',
3035 'sslverify' => true,
3036 ];
3037
3038 $response = wp_remote_post($endpoint, $args);
3039
3040 if (is_wp_error($response)) {
3041 return null;
3042 }
3043
3044 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3045
3046 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3047 return $response_body['data'][0]['embedding'];
3048 } else {
3049 return null;
3050 }
3051 }
3052
3053 private function mxchat_find_relevant_content($user_embedding) {
3054 global $wpdb;
3055 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3056 $cache_key = 'mxchat_system_prompt_embeddings';
3057
3058 // Retrieve embeddings from cache or database
3059 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3060 if ($embeddings === false) {
3061 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
3062 $embeddings = $wpdb->get_results($query);
3063 if ($embeddings === null || empty($embeddings)) {
3064 return ''; // Return an empty string if no embeddings found
3065 }
3066 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3067 }
3068
3069 // Initialize array to store relevant results with similarity scores
3070 $relevant_results = [];
3071
3072 // Iterate through embeddings to calculate similarity
3073 foreach ($embeddings as $embedding) {
3074 $database_embedding = $embedding->embedding_vector
3075 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3076 : null;
3077
3078 if (is_array($database_embedding) && is_array($user_embedding)) {
3079 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3080 $relevant_results[] = [
3081 'id' => $embedding->id,
3082 'similarity' => $similarity
3083 ];
3084 }
3085 }
3086
3087 // Retrieve the similarity threshold
3088 $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
3089
3090 // Filter and sort relevant results by similarity
3091 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3092 return $result['similarity'] >= $similarity_threshold;
3093 });
3094 usort($relevant_results, function ($a, $b) {
3095 return $b['similarity'] <=> $a['similarity'];
3096 });
3097
3098 // Limit to the top 5 results
3099 $top_results = array_slice($relevant_results, 0, 5);
3100
3101 // Initialize the final content
3102 $content = '';
3103
3104 // Fetch and combine content for the top results
3105 foreach ($top_results as $result) {
3106 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3107
3108 // Check if the content is PDF-related and add surrounding pages
3109 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3110 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3111 "SELECT article_content FROM {$system_prompt_table}
3112 WHERE id IN (
3113 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3114 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3115 )",
3116 $result['id'],
3117 $result['id']
3118 ));
3119
3120 // Add previous content if it exists
3121 if (!empty($surrounding_content[0])) {
3122 $content .= $surrounding_content[0]->article_content . "\n\n";
3123 }
3124
3125 // Add the main chunk content
3126 $content .= $chunk_content . "\n\n";
3127
3128 // Add next content if it exists
3129 if (!empty($surrounding_content[1])) {
3130 $content .= $surrounding_content[1]->article_content . "\n\n";
3131 }
3132 } else {
3133 // For non-PDF content, add directly
3134 $content .= $chunk_content . "\n\n";
3135 }
3136 }
3137
3138 return trim($content); // Return the combined content
3139 }
3140
3141 private function mxchat_find_relevant_products($user_embedding) {
3142 global $wpdb;
3143 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3144 $cache_key = 'mxchat_system_prompt_embeddings';
3145
3146 // Retrieve embeddings from cache or database
3147 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3148 if ($embeddings === false) {
3149 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
3150 $embeddings = $wpdb->get_results($query);
3151 if ($embeddings === null || empty($embeddings)) {
3152 return ''; // Return an empty string if no embeddings found
3153 }
3154 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3155 }
3156
3157 // Initialize array to store relevant results with similarity scores
3158 $relevant_results = [];
3159
3160 // Iterate through embeddings to calculate similarity
3161 foreach ($embeddings as $embedding) {
3162 $database_embedding = $embedding->embedding_vector
3163 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3164 : null;
3165
3166 if (is_array($database_embedding) && is_array($user_embedding)) {
3167 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3168 $relevant_results[] = [
3169 'id' => $embedding->id,
3170 'similarity' => $similarity
3171 ];
3172 }
3173 }
3174
3175 // Use a fixed similarity threshold of 0.85
3176 $similarity_threshold = 0.85;
3177
3178 // Filter and sort relevant results by similarity
3179 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3180 return $result['similarity'] >= $similarity_threshold;
3181 });
3182 usort($relevant_results, function ($a, $b) {
3183 return $b['similarity'] <=> $a['similarity'];
3184 });
3185
3186 // Limit to the top 5 results
3187 $top_results = array_slice($relevant_results, 0, 5);
3188
3189 // Initialize the final content
3190 $content = '';
3191
3192 // Fetch and combine content for the top results
3193 foreach ($top_results as $result) {
3194 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3195
3196 // Check if the content is PDF-related and add surrounding pages
3197 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3198 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3199 "SELECT article_content FROM {$system_prompt_table}
3200 WHERE id IN (
3201 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3202 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3203 )",
3204 $result['id'],
3205 $result['id']
3206 ));
3207
3208 // Add previous content if it exists
3209 if (!empty($surrounding_content[0])) {
3210 $content .= $surrounding_content[0]->article_content . "\n\n";
3211 }
3212
3213 // Add the main chunk content
3214 $content .= $chunk_content . "\n\n";
3215
3216 // Add next content if it exists
3217 if (!empty($surrounding_content[1])) {
3218 $content .= $surrounding_content[1]->article_content . "\n\n";
3219 }
3220 } else {
3221 // For non-PDF content, add directly
3222 $content .= $chunk_content . "\n\n";
3223 }
3224 }
3225
3226 return trim($content); // Return the combined content
3227 }
3228
3229 private function fetch_content_with_product_links($most_relevant_id) {
3230 global $wpdb;
3231 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3232
3233 // Fetch the article content and associated product URL
3234 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
3235 $result = $wpdb->get_row($query);
3236
3237 if ($result) {
3238 // Append the product link to the content if available
3239 $content = $result->article_content;
3240 if (!empty($result->source_url)) {
3241 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
3242 }
3243 return $content;
3244 }
3245
3246 return null;
3247 }
3248
3249 // Function definition
3250 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
3251 try {
3252 if (!$relevant_content) {
3253 return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
3254 }
3255
3256 // Ensure conversation_history is an array
3257 if (!is_array($conversation_history)) {
3258 $conversation_history = array();
3259 }
3260
3261 // Get selected model with default fallback
3262 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3263
3264 // Extract model prefix to determine the provider
3265 $model_parts = explode('-', $selected_model);
3266 $provider = strtolower($model_parts[0]);
3267
3268 // Handle model selection based on provider prefix
3269 switch ($provider) {
3270 case 'claude':
3271 if (empty($claude_api_key)) {
3272 throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
3273 }
3274 return $this->mxchat_generate_response_claude(
3275 $selected_model,
3276 $claude_api_key,
3277 $conversation_history,
3278 $relevant_content
3279 );
3280
3281 case 'grok':
3282 if (empty($xai_api_key)) {
3283 throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
3284 }
3285 return $this->mxchat_generate_response_xai(
3286 $selected_model,
3287 $xai_api_key,
3288 $conversation_history,
3289 $relevant_content
3290 );
3291
3292 case 'deepseek':
3293 if (empty($deepseek_api_key)) {
3294 throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
3295 }
3296 return $this->mxchat_generate_response_deepseek(
3297 $selected_model,
3298 $deepseek_api_key,
3299 $conversation_history,
3300 $relevant_content
3301 );
3302
3303 case 'gpt':
3304 if (empty($api_key)) {
3305 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3306 }
3307 return $this->mxchat_generate_response_openai(
3308 $selected_model,
3309 $api_key,
3310 $conversation_history,
3311 $relevant_content
3312 );
3313
3314 default:
3315 // Default to OpenAI for custom models or unrecognized prefixes
3316 if (empty($api_key)) {
3317 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3318 }
3319 return $this->mxchat_generate_response_openai(
3320 $selected_model,
3321 $api_key,
3322 $conversation_history,
3323 $relevant_content
3324 );
3325 }
3326 } catch (Exception $e) {
3327 error_log('MXChat Error: ' . $e->getMessage());
3328 return sprintf(
3329 esc_html__('An error occurred: %s', 'mxchat'),
3330 esc_html($e->getMessage())
3331 );
3332 }
3333 }
3334
3335
3336 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3337 // Ensure conversation_history is an array
3338 if (!is_array($conversation_history)) {
3339 $conversation_history = array();
3340 }
3341
3342 // Get system prompt instructions from options
3343 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3344
3345 // Create a new array for the formatted conversation
3346 $formatted_conversation = array();
3347
3348 // Add system message first
3349 $formatted_conversation[] = array(
3350 'role' => 'system',
3351 'content' => $system_prompt_instructions . " " . $relevant_content
3352 );
3353
3354 // Add the rest of the conversation history
3355 foreach ($conversation_history as $message) {
3356 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3357 $role = $message['role'];
3358
3359 // Convert roles to supported format
3360 if ($role === 'bot' || $role === 'agent') {
3361 $role = 'assistant';
3362 }
3363 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3364 $role = 'user';
3365 }
3366
3367 $formatted_conversation[] = array(
3368 'role' => $role,
3369 'content' => $message['content']
3370 );
3371 }
3372 }
3373
3374 $body = json_encode([
3375 'model' => $selected_model,
3376 'messages' => $formatted_conversation,
3377 'temperature' => 0.8,
3378 'stream' => false
3379 ]);
3380
3381 $args = [
3382 'body' => $body,
3383 'headers' => [
3384 'Content-Type' => 'application/json',
3385 'Authorization' => 'Bearer ' . $deepseek_api_key,
3386 ],
3387 'timeout' => 60,
3388 'redirection' => 5,
3389 'blocking' => true,
3390 'httpversion' => '1.0',
3391 'sslverify' => true,
3392 ];
3393
3394 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3395
3396 if (is_wp_error($response)) {
3397 error_log('DeepSeek API Error: ' . $response->get_error_message());
3398 return "Sorry, there was an error processing your request.";
3399 }
3400
3401 $response_body = wp_remote_retrieve_body($response);
3402 $decoded_response = json_decode($response_body, true);
3403
3404 if (isset($decoded_response['choices'][0]['message']['content'])) {
3405 return trim($decoded_response['choices'][0]['message']['content']);
3406 } else {
3407 error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3408 return "Sorry, I couldn't process that request.";
3409 }
3410 }
3411
3412 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3413 // Ensure conversation_history is an array
3414 if (!is_array($conversation_history)) {
3415 $conversation_history = array();
3416 }
3417
3418 // Get system prompt instructions from options
3419 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3420
3421 // Create a new array for the formatted conversation
3422 $formatted_conversation = array();
3423
3424 // Add system message first
3425 $formatted_conversation[] = array(
3426 'role' => 'system',
3427 'content' => $system_prompt_instructions . " " . $relevant_content
3428 );
3429
3430 // Add the rest of the conversation history
3431 foreach ($conversation_history as $message) {
3432 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3433 $role = $message['role'];
3434
3435 // Convert roles to supported format
3436 if ($role === 'bot' || $role === 'agent') {
3437 $role = 'assistant';
3438 }
3439 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3440 $role = 'user';
3441 }
3442
3443 $formatted_conversation[] = array(
3444 'role' => $role,
3445 'content' => $message['content']
3446 );
3447 }
3448 }
3449
3450 $body = json_encode([
3451 'model' => $selected_model,
3452 'messages' => $formatted_conversation,
3453 'temperature' => 0.8,
3454 'stream' => false
3455 ]);
3456
3457 $args = [
3458 'body' => $body,
3459 'headers' => [
3460 'Content-Type' => 'application/json',
3461 'Authorization' => 'Bearer ' . $api_key,
3462 ],
3463 'timeout' => 60,
3464 'redirection' => 5,
3465 'blocking' => true,
3466 'httpversion' => '1.0',
3467 'sslverify' => true,
3468 ];
3469
3470 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3471
3472 if (is_wp_error($response)) {
3473 error_log('OpenAI API Error: ' . $response->get_error_message());
3474 return "Sorry, there was an error processing your request.";
3475 }
3476
3477 $response_body = wp_remote_retrieve_body($response);
3478 $decoded_response = json_decode($response_body, true);
3479
3480 if (isset($decoded_response['choices'][0]['message']['content'])) {
3481 return trim($decoded_response['choices'][0]['message']['content']);
3482 } else {
3483 error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3484 return "Sorry, I couldn't process that request.";
3485 }
3486 }
3487 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3488 // Get system prompt instructions from options
3489 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3490
3491 // Add system prompt to relevant content
3492 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3493
3494 // Prepend system instructions to the conversation history
3495 array_unshift($conversation_history, [
3496 'role' => 'system',
3497 'content' => "Here are your instructions: " . $content_with_instructions
3498 ]);
3499
3500 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3501 foreach ($conversation_history as &$message) {
3502 if ($message['role'] === 'bot') {
3503 $message['role'] = 'assistant';
3504 } elseif ($message['role'] === 'agent') {
3505 // Tag the message as coming from a live agent
3506 $message['role'] = 'assistant';
3507 if (!isset($message['metadata'])) {
3508 $message['metadata'] = ['source' => 'live_agent'];
3509 }
3510 }
3511
3512 // Ensure all roles are valid
3513 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3514 $message['role'] = 'user'; // Default to 'user'
3515 }
3516 }
3517
3518
3519 // Build the request body
3520 $body = json_encode([
3521 'model' => $selected_model,
3522 'messages' => $conversation_history,
3523 'temperature' => 0.8,
3524 'stream' => false
3525 ]);
3526
3527 // Set up the API request
3528 $args = [
3529 'body' => $body,
3530 'headers' => [
3531 'Content-Type' => 'application/json',
3532 'Authorization' => 'Bearer ' . $xai_api_key,
3533 ],
3534 'timeout' => 60,
3535 'redirection' => 5,
3536 'blocking' => true,
3537 'httpversion' => '1.0',
3538 'sslverify' => true,
3539 ];
3540
3541 // Make the API request
3542 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3543
3544 // Process the response
3545 if (is_wp_error($response)) {
3546 return "Sorry, there was an error processing your request.";
3547 }
3548
3549 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3550
3551 if (isset($response_body['choices'][0]['message']['content'])) {
3552 return trim($response_body['choices'][0]['message']['content']);
3553 } else {
3554 return "Sorry, I couldn't process that request.";
3555 }
3556 }
3557
3558 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3559 // Get system prompt instructions from options for Claude's top-level system parameter
3560 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3561
3562 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3563 foreach ($conversation_history as &$message) {
3564 if ($message['role'] === 'bot') {
3565 $message['role'] = 'assistant';
3566 } elseif ($message['role'] === 'agent') {
3567 // Tag the message as coming from a live agent
3568 $message['role'] = 'assistant';
3569 if (!isset($message['metadata'])) {
3570 $message['metadata'] = ['source' => 'live_agent'];
3571 }
3572 }
3573
3574 // Ensure all roles are valid
3575 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3576 $message['role'] = 'user'; // Default to 'user'
3577 }
3578 }
3579
3580 // Add relevant content as the latest user message in conversation history
3581 $conversation_history[] = [
3582 'role' => 'user',
3583 'content' => $relevant_content
3584 ];
3585
3586 // Build the request body with Claude's expected structure, using system instructions as a top-level parameter
3587 $body = json_encode([
3588 'model' => $selected_model,
3589 'max_tokens' => 1000,
3590 'temperature' => 0.8,
3591 'system' => $system_prompt_instructions, // Set the system prompt at the top level as required
3592 'messages' => $conversation_history
3593 ]);
3594
3595 // Set up the API request with the necessary headers
3596 $args = [
3597 'body' => $body,
3598 'headers' => [
3599 'Content-Type' => 'application/json',
3600 'x-api-key' => $claude_api_key,
3601 'anthropic-version' => '2023-06-01',
3602 ],
3603 'timeout' => 60,
3604 'redirection' => 5,
3605 'blocking' => true,
3606 'httpversion' => '1.0',
3607 'sslverify' => true,
3608 ];
3609
3610 // Make the API request
3611 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
3612
3613 /*
3614 // Check for errors and log the response for debugging
3615 if (is_wp_error($response)) {
3616 error_log("Claude API request error: " . print_r($response->get_error_message(), true));
3617 return "Sorry, there was an error processing your request.";
3618 }
3619 */
3620
3621
3622 // Decode the response and parse according to the expected Claude response structure
3623 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3624 //error_log("Claude API response: " . print_r($response_body, true));
3625
3626 // Check if the response has the expected 'content' array with 'text' blocks
3627 if (isset($response_body['content'][0]['text'])) {
3628 return trim($response_body['content'][0]['text']);
3629 } else {
3630 return "Sorry, I couldn't process that request.";
3631 }
3632 }
3633
3634 public function mxchat_dismiss_pre_chat_message() {
3635 // Get and sanitize the user identifier
3636 $user_id = $this->mxchat_get_user_identifier();
3637 $user_id = sanitize_key($user_id);
3638
3639 // Set a transient to track that the user has dismissed the pre-chat message
3640 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3641 set_transient($transient_key, true, DAY_IN_SECONDS);
3642
3643 wp_send_json_success();
3644 }
3645
3646 public function mxchat_check_pre_chat_message_status() {
3647 // Get and sanitize the user identifier
3648 $user_id = $this->mxchat_get_user_identifier();
3649 $user_id = sanitize_key($user_id);
3650
3651 // Check if the transient exists (i.e., if the message was dismissed)
3652 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3653 $dismissed = get_transient($transient_key);
3654
3655 // Log the result to see if it's being set correctly
3656 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
3657
3658 if ($dismissed) {
3659 wp_send_json_success(['dismissed' => true]);
3660 } else {
3661 wp_send_json_success(['dismissed' => false]);
3662 }
3663
3664 wp_die();
3665 }
3666
3667 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
3668 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
3669 return 0;
3670 }
3671
3672 $dotProduct = array_sum(array_map(function ($a, $b) {
3673 return $a * $b;
3674 }, $vectorA, $vectorB));
3675 $normA = sqrt(array_sum(array_map(function ($a) {
3676 return $a * $a;
3677 }, $vectorA)));
3678 $normB = sqrt(array_sum(array_map(function ($b) {
3679 return $b * $b;
3680 }, $vectorB)));
3681
3682 if ($normA == 0 || $normB == 0) {
3683 return 0;
3684 }
3685
3686 return $dotProduct / ($normA * $normB);
3687 }
3688
3689 public function mxchat_enqueue_scripts_styles() {
3690 // Define version numbers for the styles and scripts
3691 $chat_style_version = '1.6.4'; // Replace with your actual version
3692 $chat_script_version = '1.6.4'; // Replace with your actual version
3693
3694 // Enqueue the script
3695 wp_enqueue_script(
3696 'mxchat-chat-js',
3697 plugin_dir_url(__FILE__) . '../js/chat-script.js',
3698 array('jquery'),
3699 $chat_script_version,
3700 true
3701 );
3702
3703 // Enqueue the CSS
3704 wp_enqueue_style(
3705 'mxchat-chat-css',
3706 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3707 array(),
3708 $chat_style_version
3709 );
3710
3711 // Fetch options from the database
3712 $this->options = get_option('mxchat_options');
3713
3714 // Prepare settings for JavaScript
3715 $style_settings = array(
3716 'ajax_url' => admin_url('admin-ajax.php'),
3717 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
3718 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3719 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3720 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3721 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
3722 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
3723 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
3724 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
3725 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
3726 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
3727 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
3728 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
3729 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3730 'icon_color' => $this->options['icon_color'] ?? '#fff',
3731 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3732 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3733 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3734
3735 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3736 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3737 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3738 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3739 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3740 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3741 );
3742
3743 // Pass the settings to the script
3744 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3745 }
3746
3747
3748 public function mxchat_reset_rate_limits() {
3749 global $wpdb;
3750
3751 // Define a cache key pattern for rate limits
3752 $cache_key_pattern = 'mxchat_chat_limit_%';
3753
3754 // Retrieve all option names matching the pattern
3755 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3756 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3757
3758 // db call ok; no-cache ok
3759 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3760 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3761
3762 // Clear the relevant cache entries
3763 foreach ($option_names as $option_name) {
3764 wp_cache_delete($option_name, 'options');
3765 }
3766
3767 // Optionally, clear a general cache if you have one
3768 wp_cache_delete('mxchat_all_chat_limits', 'options');
3769 }
3770
3771 private function mxchat_fetch_woocommerce_products() {
3772 // Ensure WooCommerce is active
3773 if (!class_exists('WooCommerce')) {
3774 return [];
3775 }
3776
3777 $args = array(
3778 'post_type' => 'product',
3779 'post_status' => 'publish',
3780 'posts_per_page' => -1,
3781 );
3782
3783 $products = get_posts($args);
3784 $product_data = [];
3785
3786 foreach ($products as $product) {
3787 $product_id = $product->ID;
3788 $product_obj = wc_get_product($product_id);
3789
3790 $product_data[] = array(
3791 'id' => $product_id,
3792 'name' => $product_obj->get_name(),
3793 'description' => $product_obj->get_description(),
3794 'short_description' => $product_obj->get_short_description(),
3795 'url' => get_permalink($product_id),
3796 'price' => $product_obj->get_regular_price(),
3797 'sale_price' => $product_obj->get_sale_price(),
3798 'stock_status' => $product_obj->get_stock_status(),
3799 'sku' => $product_obj->get_sku(),
3800 'in_stock' => $product_obj->is_in_stock(),
3801 'total_sales' => $product_obj->get_total_sales(),
3802 );
3803 }
3804
3805 return $product_data;
3806 }
3807
3808 }
3809 ?>
3810