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

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

3,852 lines 150.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 $batch_size = 500;
3058
3059 // Retrieve embeddings from cache or database
3060 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3061 if ($embeddings === false) {
3062 $embeddings = [];
3063 $offset = 0;
3064
3065 // Load in batches and build cache
3066 do {
3067 $query = $wpdb->prepare(
3068 "SELECT id, embedding_vector
3069 FROM {$system_prompt_table}
3070 LIMIT %d OFFSET %d",
3071 $batch_size,
3072 $offset
3073 );
3074
3075 $batch = $wpdb->get_results($query);
3076 if (empty($batch)) {
3077 break;
3078 }
3079
3080 $embeddings = array_merge($embeddings, $batch);
3081 $offset += $batch_size;
3082
3083 // Free memory
3084 unset($batch);
3085
3086 } while (true);
3087
3088 if (empty($embeddings)) {
3089 return ''; // Return an empty string if no embeddings found
3090 }
3091 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3092 }
3093
3094 // Initialize array to store relevant results with similarity scores
3095 $relevant_results = [];
3096 // Iterate through embeddings to calculate similarity
3097 foreach ($embeddings as $embedding) {
3098 $database_embedding = $embedding->embedding_vector
3099 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3100 : null;
3101 if (is_array($database_embedding) && is_array($user_embedding)) {
3102 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3103 $relevant_results[] = [
3104 'id' => $embedding->id,
3105 'similarity' => $similarity
3106 ];
3107 }
3108 // Free memory
3109 unset($database_embedding);
3110 }
3111
3112 // Retrieve the similarity threshold
3113 $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
3114
3115 // Filter and sort relevant results by similarity
3116 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3117 return $result['similarity'] >= $similarity_threshold;
3118 });
3119 usort($relevant_results, function ($a, $b) {
3120 return $b['similarity'] <=> $a['similarity'];
3121 });
3122
3123 // Limit to the top 5 results
3124 $top_results = array_slice($relevant_results, 0, 5);
3125
3126 // Initialize the final content
3127 $content = '';
3128
3129 // Fetch and combine content for the top results
3130 foreach ($top_results as $result) {
3131 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3132 // Check if the content is PDF-related and add surrounding pages
3133 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3134 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3135 "SELECT article_content FROM {$system_prompt_table}
3136 WHERE id IN (
3137 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3138 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3139 )",
3140 $result['id'],
3141 $result['id']
3142 ));
3143 // Add previous content if it exists
3144 if (!empty($surrounding_content[0])) {
3145 $content .= $surrounding_content[0]->article_content . "\n\n";
3146 }
3147 // Add the main chunk content
3148 $content .= $chunk_content . "\n\n";
3149 // Add next content if it exists
3150 if (!empty($surrounding_content[1])) {
3151 $content .= $surrounding_content[1]->article_content . "\n\n";
3152 }
3153 } else {
3154 // For non-PDF content, add directly
3155 $content .= $chunk_content . "\n\n";
3156 }
3157 }
3158
3159 return trim($content);
3160 }
3161
3162 private function mxchat_find_relevant_products($user_embedding) {
3163 global $wpdb;
3164 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3165 $cache_key = 'mxchat_system_prompt_embeddings';
3166 $batch_size = 500;
3167
3168 // Retrieve embeddings from cache or database
3169 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3170 if ($embeddings === false) {
3171 $embeddings = [];
3172 $offset = 0;
3173
3174 // Load in batches and build cache
3175 do {
3176 $query = $wpdb->prepare(
3177 "SELECT id, embedding_vector
3178 FROM {$system_prompt_table}
3179 LIMIT %d OFFSET %d",
3180 $batch_size,
3181 $offset
3182 );
3183
3184 $batch = $wpdb->get_results($query);
3185 if (empty($batch)) {
3186 break;
3187 }
3188
3189 $embeddings = array_merge($embeddings, $batch);
3190 $offset += $batch_size;
3191
3192 // Free memory
3193 unset($batch);
3194
3195 } while (true);
3196
3197 if (empty($embeddings)) {
3198 return ''; // Return an empty string if no embeddings found
3199 }
3200 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3201 }
3202
3203 // Initialize array to store relevant results with similarity scores
3204 $relevant_results = [];
3205 // Iterate through embeddings to calculate similarity
3206 foreach ($embeddings as $embedding) {
3207 $database_embedding = $embedding->embedding_vector
3208 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3209 : null;
3210 if (is_array($database_embedding) && is_array($user_embedding)) {
3211 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3212 $relevant_results[] = [
3213 'id' => $embedding->id,
3214 'similarity' => $similarity
3215 ];
3216 }
3217 // Free memory
3218 unset($database_embedding);
3219 }
3220
3221 // Use a fixed similarity threshold of 0.85
3222 $similarity_threshold = 0.85;
3223
3224 // Filter and sort relevant results by similarity
3225 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3226 return $result['similarity'] >= $similarity_threshold;
3227 });
3228 usort($relevant_results, function ($a, $b) {
3229 return $b['similarity'] <=> $a['similarity'];
3230 });
3231
3232 // Limit to the top 5 results
3233 $top_results = array_slice($relevant_results, 0, 5);
3234
3235 // Initialize the final content
3236 $content = '';
3237
3238 // Fetch and combine content for the top results
3239 foreach ($top_results as $result) {
3240 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3241 // Check if the content is PDF-related and add surrounding pages
3242 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3243 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3244 "SELECT article_content FROM {$system_prompt_table}
3245 WHERE id IN (
3246 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3247 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3248 )",
3249 $result['id'],
3250 $result['id']
3251 ));
3252 // Add previous content if it exists
3253 if (!empty($surrounding_content[0])) {
3254 $content .= $surrounding_content[0]->article_content . "\n\n";
3255 }
3256 // Add the main chunk content
3257 $content .= $chunk_content . "\n\n";
3258 // Add next content if it exists
3259 if (!empty($surrounding_content[1])) {
3260 $content .= $surrounding_content[1]->article_content . "\n\n";
3261 }
3262 } else {
3263 // For non-PDF content, add directly
3264 $content .= $chunk_content . "\n\n";
3265 }
3266 }
3267
3268 return trim($content);
3269 }
3270
3271 private function fetch_content_with_product_links($most_relevant_id) {
3272 global $wpdb;
3273 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3274
3275 // Fetch the article content and associated product URL
3276 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
3277 $result = $wpdb->get_row($query);
3278
3279 if ($result) {
3280 // Append the product link to the content if available
3281 $content = $result->article_content;
3282 if (!empty($result->source_url)) {
3283 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
3284 }
3285 return $content;
3286 }
3287
3288 return null;
3289 }
3290
3291 // Function definition
3292 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
3293 try {
3294 if (!$relevant_content) {
3295 return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
3296 }
3297
3298 // Ensure conversation_history is an array
3299 if (!is_array($conversation_history)) {
3300 $conversation_history = array();
3301 }
3302
3303 // Get selected model with default fallback
3304 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3305
3306 // Extract model prefix to determine the provider
3307 $model_parts = explode('-', $selected_model);
3308 $provider = strtolower($model_parts[0]);
3309
3310 // Handle model selection based on provider prefix
3311 switch ($provider) {
3312 case 'claude':
3313 if (empty($claude_api_key)) {
3314 throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
3315 }
3316 return $this->mxchat_generate_response_claude(
3317 $selected_model,
3318 $claude_api_key,
3319 $conversation_history,
3320 $relevant_content
3321 );
3322
3323 case 'grok':
3324 if (empty($xai_api_key)) {
3325 throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
3326 }
3327 return $this->mxchat_generate_response_xai(
3328 $selected_model,
3329 $xai_api_key,
3330 $conversation_history,
3331 $relevant_content
3332 );
3333
3334 case 'deepseek':
3335 if (empty($deepseek_api_key)) {
3336 throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
3337 }
3338 return $this->mxchat_generate_response_deepseek(
3339 $selected_model,
3340 $deepseek_api_key,
3341 $conversation_history,
3342 $relevant_content
3343 );
3344
3345 case 'gpt':
3346 if (empty($api_key)) {
3347 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3348 }
3349 return $this->mxchat_generate_response_openai(
3350 $selected_model,
3351 $api_key,
3352 $conversation_history,
3353 $relevant_content
3354 );
3355
3356 default:
3357 // Default to OpenAI for custom models or unrecognized prefixes
3358 if (empty($api_key)) {
3359 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3360 }
3361 return $this->mxchat_generate_response_openai(
3362 $selected_model,
3363 $api_key,
3364 $conversation_history,
3365 $relevant_content
3366 );
3367 }
3368 } catch (Exception $e) {
3369 error_log('MXChat Error: ' . $e->getMessage());
3370 return sprintf(
3371 esc_html__('An error occurred: %s', 'mxchat'),
3372 esc_html($e->getMessage())
3373 );
3374 }
3375 }
3376
3377
3378 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3379 // Ensure conversation_history is an array
3380 if (!is_array($conversation_history)) {
3381 $conversation_history = array();
3382 }
3383
3384 // Get system prompt instructions from options
3385 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3386
3387 // Create a new array for the formatted conversation
3388 $formatted_conversation = array();
3389
3390 // Add system message first
3391 $formatted_conversation[] = array(
3392 'role' => 'system',
3393 'content' => $system_prompt_instructions . " " . $relevant_content
3394 );
3395
3396 // Add the rest of the conversation history
3397 foreach ($conversation_history as $message) {
3398 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3399 $role = $message['role'];
3400
3401 // Convert roles to supported format
3402 if ($role === 'bot' || $role === 'agent') {
3403 $role = 'assistant';
3404 }
3405 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3406 $role = 'user';
3407 }
3408
3409 $formatted_conversation[] = array(
3410 'role' => $role,
3411 'content' => $message['content']
3412 );
3413 }
3414 }
3415
3416 $body = json_encode([
3417 'model' => $selected_model,
3418 'messages' => $formatted_conversation,
3419 'temperature' => 0.8,
3420 'stream' => false
3421 ]);
3422
3423 $args = [
3424 'body' => $body,
3425 'headers' => [
3426 'Content-Type' => 'application/json',
3427 'Authorization' => 'Bearer ' . $deepseek_api_key,
3428 ],
3429 'timeout' => 60,
3430 'redirection' => 5,
3431 'blocking' => true,
3432 'httpversion' => '1.0',
3433 'sslverify' => true,
3434 ];
3435
3436 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3437
3438 if (is_wp_error($response)) {
3439 error_log('DeepSeek API Error: ' . $response->get_error_message());
3440 return "Sorry, there was an error processing your request.";
3441 }
3442
3443 $response_body = wp_remote_retrieve_body($response);
3444 $decoded_response = json_decode($response_body, true);
3445
3446 if (isset($decoded_response['choices'][0]['message']['content'])) {
3447 return trim($decoded_response['choices'][0]['message']['content']);
3448 } else {
3449 error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3450 return "Sorry, I couldn't process that request.";
3451 }
3452 }
3453
3454 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3455 // Ensure conversation_history is an array
3456 if (!is_array($conversation_history)) {
3457 $conversation_history = array();
3458 }
3459
3460 // Get system prompt instructions from options
3461 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3462
3463 // Create a new array for the formatted conversation
3464 $formatted_conversation = array();
3465
3466 // Add system message first
3467 $formatted_conversation[] = array(
3468 'role' => 'system',
3469 'content' => $system_prompt_instructions . " " . $relevant_content
3470 );
3471
3472 // Add the rest of the conversation history
3473 foreach ($conversation_history as $message) {
3474 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3475 $role = $message['role'];
3476
3477 // Convert roles to supported format
3478 if ($role === 'bot' || $role === 'agent') {
3479 $role = 'assistant';
3480 }
3481 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3482 $role = 'user';
3483 }
3484
3485 $formatted_conversation[] = array(
3486 'role' => $role,
3487 'content' => $message['content']
3488 );
3489 }
3490 }
3491
3492 $body = json_encode([
3493 'model' => $selected_model,
3494 'messages' => $formatted_conversation,
3495 'temperature' => 0.8,
3496 'stream' => false
3497 ]);
3498
3499 $args = [
3500 'body' => $body,
3501 'headers' => [
3502 'Content-Type' => 'application/json',
3503 'Authorization' => 'Bearer ' . $api_key,
3504 ],
3505 'timeout' => 60,
3506 'redirection' => 5,
3507 'blocking' => true,
3508 'httpversion' => '1.0',
3509 'sslverify' => true,
3510 ];
3511
3512 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3513
3514 if (is_wp_error($response)) {
3515 error_log('OpenAI API Error: ' . $response->get_error_message());
3516 return "Sorry, there was an error processing your request.";
3517 }
3518
3519 $response_body = wp_remote_retrieve_body($response);
3520 $decoded_response = json_decode($response_body, true);
3521
3522 if (isset($decoded_response['choices'][0]['message']['content'])) {
3523 return trim($decoded_response['choices'][0]['message']['content']);
3524 } else {
3525 error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3526 return "Sorry, I couldn't process that request.";
3527 }
3528 }
3529 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3530 // Get system prompt instructions from options
3531 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3532
3533 // Add system prompt to relevant content
3534 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3535
3536 // Prepend system instructions to the conversation history
3537 array_unshift($conversation_history, [
3538 'role' => 'system',
3539 'content' => "Here are your instructions: " . $content_with_instructions
3540 ]);
3541
3542 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3543 foreach ($conversation_history as &$message) {
3544 if ($message['role'] === 'bot') {
3545 $message['role'] = 'assistant';
3546 } elseif ($message['role'] === 'agent') {
3547 // Tag the message as coming from a live agent
3548 $message['role'] = 'assistant';
3549 if (!isset($message['metadata'])) {
3550 $message['metadata'] = ['source' => 'live_agent'];
3551 }
3552 }
3553
3554 // Ensure all roles are valid
3555 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3556 $message['role'] = 'user'; // Default to 'user'
3557 }
3558 }
3559
3560
3561 // Build the request body
3562 $body = json_encode([
3563 'model' => $selected_model,
3564 'messages' => $conversation_history,
3565 'temperature' => 0.8,
3566 'stream' => false
3567 ]);
3568
3569 // Set up the API request
3570 $args = [
3571 'body' => $body,
3572 'headers' => [
3573 'Content-Type' => 'application/json',
3574 'Authorization' => 'Bearer ' . $xai_api_key,
3575 ],
3576 'timeout' => 60,
3577 'redirection' => 5,
3578 'blocking' => true,
3579 'httpversion' => '1.0',
3580 'sslverify' => true,
3581 ];
3582
3583 // Make the API request
3584 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3585
3586 // Process the response
3587 if (is_wp_error($response)) {
3588 return "Sorry, there was an error processing your request.";
3589 }
3590
3591 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3592
3593 if (isset($response_body['choices'][0]['message']['content'])) {
3594 return trim($response_body['choices'][0]['message']['content']);
3595 } else {
3596 return "Sorry, I couldn't process that request.";
3597 }
3598 }
3599
3600 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3601 // Get system prompt instructions from options for Claude's top-level system parameter
3602 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3603
3604 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3605 foreach ($conversation_history as &$message) {
3606 if ($message['role'] === 'bot') {
3607 $message['role'] = 'assistant';
3608 } elseif ($message['role'] === 'agent') {
3609 // Tag the message as coming from a live agent
3610 $message['role'] = 'assistant';
3611 if (!isset($message['metadata'])) {
3612 $message['metadata'] = ['source' => 'live_agent'];
3613 }
3614 }
3615
3616 // Ensure all roles are valid
3617 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3618 $message['role'] = 'user'; // Default to 'user'
3619 }
3620 }
3621
3622 // Add relevant content as the latest user message in conversation history
3623 $conversation_history[] = [
3624 'role' => 'user',
3625 'content' => $relevant_content
3626 ];
3627
3628 // Build the request body with Claude's expected structure, using system instructions as a top-level parameter
3629 $body = json_encode([
3630 'model' => $selected_model,
3631 'max_tokens' => 1000,
3632 'temperature' => 0.8,
3633 'system' => $system_prompt_instructions, // Set the system prompt at the top level as required
3634 'messages' => $conversation_history
3635 ]);
3636
3637 // Set up the API request with the necessary headers
3638 $args = [
3639 'body' => $body,
3640 'headers' => [
3641 'Content-Type' => 'application/json',
3642 'x-api-key' => $claude_api_key,
3643 'anthropic-version' => '2023-06-01',
3644 ],
3645 'timeout' => 60,
3646 'redirection' => 5,
3647 'blocking' => true,
3648 'httpversion' => '1.0',
3649 'sslverify' => true,
3650 ];
3651
3652 // Make the API request
3653 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
3654
3655 /*
3656 // Check for errors and log the response for debugging
3657 if (is_wp_error($response)) {
3658 error_log("Claude API request error: " . print_r($response->get_error_message(), true));
3659 return "Sorry, there was an error processing your request.";
3660 }
3661 */
3662
3663
3664 // Decode the response and parse according to the expected Claude response structure
3665 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3666 //error_log("Claude API response: " . print_r($response_body, true));
3667
3668 // Check if the response has the expected 'content' array with 'text' blocks
3669 if (isset($response_body['content'][0]['text'])) {
3670 return trim($response_body['content'][0]['text']);
3671 } else {
3672 return "Sorry, I couldn't process that request.";
3673 }
3674 }
3675
3676 public function mxchat_dismiss_pre_chat_message() {
3677 // Get and sanitize the user identifier
3678 $user_id = $this->mxchat_get_user_identifier();
3679 $user_id = sanitize_key($user_id);
3680
3681 // Set a transient to track that the user has dismissed the pre-chat message
3682 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3683 set_transient($transient_key, true, DAY_IN_SECONDS);
3684
3685 wp_send_json_success();
3686 }
3687
3688 public function mxchat_check_pre_chat_message_status() {
3689 // Get and sanitize the user identifier
3690 $user_id = $this->mxchat_get_user_identifier();
3691 $user_id = sanitize_key($user_id);
3692
3693 // Check if the transient exists (i.e., if the message was dismissed)
3694 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3695 $dismissed = get_transient($transient_key);
3696
3697 // Log the result to see if it's being set correctly
3698 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
3699
3700 if ($dismissed) {
3701 wp_send_json_success(['dismissed' => true]);
3702 } else {
3703 wp_send_json_success(['dismissed' => false]);
3704 }
3705
3706 wp_die();
3707 }
3708
3709 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
3710 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
3711 return 0;
3712 }
3713
3714 $dotProduct = array_sum(array_map(function ($a, $b) {
3715 return $a * $b;
3716 }, $vectorA, $vectorB));
3717 $normA = sqrt(array_sum(array_map(function ($a) {
3718 return $a * $a;
3719 }, $vectorA)));
3720 $normB = sqrt(array_sum(array_map(function ($b) {
3721 return $b * $b;
3722 }, $vectorB)));
3723
3724 if ($normA == 0 || $normB == 0) {
3725 return 0;
3726 }
3727
3728 return $dotProduct / ($normA * $normB);
3729 }
3730
3731 public function mxchat_enqueue_scripts_styles() {
3732 // Define version numbers for the styles and scripts
3733 $chat_style_version = '1.6.5'; // Replace with your actual version
3734 $chat_script_version = '1.6.5'; // Replace with your actual version
3735
3736 // Enqueue the script
3737 wp_enqueue_script(
3738 'mxchat-chat-js',
3739 plugin_dir_url(__FILE__) . '../js/chat-script.js',
3740 array('jquery'),
3741 $chat_script_version,
3742 true
3743 );
3744
3745 // Enqueue the CSS
3746 wp_enqueue_style(
3747 'mxchat-chat-css',
3748 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3749 array(),
3750 $chat_style_version
3751 );
3752
3753 // Fetch options from the database
3754 $this->options = get_option('mxchat_options');
3755
3756 // Prepare settings for JavaScript
3757 $style_settings = array(
3758 'ajax_url' => admin_url('admin-ajax.php'),
3759 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
3760 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3761 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3762 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3763 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
3764 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
3765 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
3766 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
3767 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
3768 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
3769 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
3770 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
3771 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3772 'icon_color' => $this->options['icon_color'] ?? '#fff',
3773 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3774 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3775 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3776
3777 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3778 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3779 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3780 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3781 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3782 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3783 );
3784
3785 // Pass the settings to the script
3786 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3787 }
3788
3789
3790 public function mxchat_reset_rate_limits() {
3791 global $wpdb;
3792
3793 // Define a cache key pattern for rate limits
3794 $cache_key_pattern = 'mxchat_chat_limit_%';
3795
3796 // Retrieve all option names matching the pattern
3797 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3798 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3799
3800 // db call ok; no-cache ok
3801 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3802 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3803
3804 // Clear the relevant cache entries
3805 foreach ($option_names as $option_name) {
3806 wp_cache_delete($option_name, 'options');
3807 }
3808
3809 // Optionally, clear a general cache if you have one
3810 wp_cache_delete('mxchat_all_chat_limits', 'options');
3811 }
3812
3813 private function mxchat_fetch_woocommerce_products() {
3814 // Ensure WooCommerce is active
3815 if (!class_exists('WooCommerce')) {
3816 return [];
3817 }
3818
3819 $args = array(
3820 'post_type' => 'product',
3821 'post_status' => 'publish',
3822 'posts_per_page' => -1,
3823 );
3824
3825 $products = get_posts($args);
3826 $product_data = [];
3827
3828 foreach ($products as $product) {
3829 $product_id = $product->ID;
3830 $product_obj = wc_get_product($product_id);
3831
3832 $product_data[] = array(
3833 'id' => $product_id,
3834 'name' => $product_obj->get_name(),
3835 'description' => $product_obj->get_description(),
3836 'short_description' => $product_obj->get_short_description(),
3837 'url' => get_permalink($product_id),
3838 'price' => $product_obj->get_regular_price(),
3839 'sale_price' => $product_obj->get_sale_price(),
3840 'stock_status' => $product_obj->get_stock_status(),
3841 'sku' => $product_obj->get_sku(),
3842 'in_stock' => $product_obj->is_in_stock(),
3843 'total_sales' => $product_obj->get_total_sales(),
3844 );
3845 }
3846
3847 return $product_data;
3848 }
3849
3850 }
3851 ?>
3852