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

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