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

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

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