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

3,569 lines 133.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 class MxChat_Integrator {
7 private $options;
8 private $chat_count;
9 private $fallbackResponse;
10 private $productCardHtml;
11 private $word_handler;
12
13 public function __construct() {
14 $this->options = get_option('mxchat_options');
15 $this->chat_count = get_option('mxchat_chat_count', 0);
16 $this->word_handler = new MXChat_Word_Handler($this->options);
17
18 // Add WooCommerce hooks
19 add_action('wp_insert_post', array($this, 'mxchat_handle_product_change'), 10, 3);
20
21 // Ensure embeddings are removed when a product is moved to trash or permanently deleted
22 add_action('wp_trash_post', array($this, 'mxchat_handle_product_delete'));
23 add_action('before_delete_post', array($this, 'mxchat_handle_product_delete'));
24
25 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
26 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
27 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
28
29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31 // Add the AJAX actions for checking if the pre-chat message was dismissed
32 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
33 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
34
35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
37
38 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
39 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
40
41 if (!wp_next_scheduled('mxchat_reset_rate_limits')) {
42 wp_schedule_event(time(), 'daily', 'mxchat_reset_rate_limits');
43 }
44
45 // Add REST API routes registration
46 add_action('rest_api_init', array($this, 'register_routes'));
47
48 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
49 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
50
51
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'] ?? 'off';
2544 if ($live_agent_available !== 'on') { // Changed from 'online' to 'on'
2545 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2546 $this->fallbackResponse = [
2547 'text' => $away_message,
2548 'html' => '',
2549 'images' => [],
2550 'chat_mode' => 'ai'
2551 ];
2552 wp_send_json([
2553 'text' => $away_message,
2554 'html' => '',
2555 'chat_mode' => 'ai',
2556 'session_id' => $session_id
2557 ]);
2558 wp_die();
2559 }
2560
2561 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2562 //error_log('Slack Webhook URL retrieved: ' . $slack_webhook_url);
2563
2564 if (empty($slack_webhook_url)) {
2565 //error_log('Slack Webhook URL is not configured.');
2566 return false;
2567 }
2568
2569 update_option("mxchat_mode_{$session_id}", 'agent');
2570
2571 $webhook_data = [
2572 'blocks' => [
2573 [
2574 'type' => 'header',
2575 'text' => [
2576 'type' => 'plain_text',
2577 'text' => '🔔 New Live Agent Request',
2578 'emoji' => true
2579 ]
2580 ],
2581 [
2582 'type' => 'section',
2583 'fields' => [
2584 [
2585 'type' => 'mrkdwn',
2586 'text' => "*User ID:*\n`$user_id`"
2587 ],
2588 [
2589 'type' => 'mrkdwn',
2590 'text' => "*Session ID:*\n`$session_id`"
2591 ]
2592 ]
2593 ],
2594 [
2595 'type' => 'section',
2596 'text' => [
2597 'type' => 'mrkdwn',
2598 'text' => "*Initial Message:*\n$message"
2599 ]
2600 ],
2601 [
2602 'type' => 'actions',
2603 'elements' => [
2604 [
2605 'type' => 'button',
2606 'text' => [
2607 'type' => 'plain_text',
2608 'text' => '✍️ Reply',
2609 'emoji' => true
2610 ],
2611 'value' => $session_id,
2612 'action_id' => 'reply_to_user',
2613 'style' => 'primary'
2614 ]
2615 ]
2616 ]
2617 ]
2618 ];
2619
2620 $response = wp_remote_post($slack_webhook_url, [
2621 'body' => json_encode($webhook_data),
2622 'headers' => [
2623 'Content-Type' => 'application/json',
2624 ],
2625 ]);
2626
2627 if (is_wp_error($response)) {
2628 //error_log('Error sending live agent handover: ' . $response->get_error_message());
2629 return false;
2630 }
2631
2632 //error_log('Live agent handover triggered successfully.');
2633
2634 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2635
2636 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2637
2638 $this->fallbackResponse = [
2639 'text' => $success_message,
2640 'html' => '',
2641 'images' => [],
2642 'chat_mode' => 'agent'
2643 ];
2644
2645 wp_send_json([
2646 'success' => true,
2647 'text' => $success_message,
2648 'html' => '',
2649 'chat_mode' => 'agent',
2650 'session_id' => $session_id,
2651 'fallbackResponse' => $this->fallbackResponse
2652 ]);
2653 wp_die();
2654 }
2655 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2656 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2657
2658 if (empty($slack_webhook_url)) {
2659 //error_log('Slack Webhook URL is not configured.');
2660 return false;
2661 }
2662
2663 $webhook_data = [
2664 'blocks' => [
2665 [
2666 'type' => 'header',
2667 'text' => [
2668 'type' => 'plain_text',
2669 'text' => '📩 New Chat Message',
2670 'emoji' => true
2671 ]
2672 ],
2673 [
2674 'type' => 'section',
2675 'fields' => [
2676 [
2677 'type' => 'mrkdwn',
2678 'text' => "*User ID:*\n`$user_id`"
2679 ],
2680 [
2681 'type' => 'mrkdwn',
2682 'text' => "*Session ID:*\n`$session_id`"
2683 ]
2684 ]
2685 ],
2686 [
2687 'type' => 'section',
2688 'text' => [
2689 'type' => 'mrkdwn',
2690 'text' => "*Message:*\n$message"
2691 ]
2692 ],
2693 [
2694 'type' => 'actions',
2695 'elements' => [
2696 [
2697 'type' => 'button',
2698 'text' => [
2699 'type' => 'plain_text',
2700 'text' => '✍️ Reply',
2701 'emoji' => true
2702 ],
2703 'value' => $session_id,
2704 'action_id' => 'reply_to_user',
2705 'style' => 'primary'
2706 ]
2707 ]
2708 ]
2709 ]
2710 ];
2711
2712 $response = wp_remote_post($slack_webhook_url, [
2713 'body' => json_encode($webhook_data),
2714 'headers' => [
2715 'Content-Type' => 'application/json',
2716 ],
2717 ]);
2718
2719 if (is_wp_error($response)) {
2720 //error_log('Error sending message to Slack: ' . $response->get_error_message());
2721 return false;
2722 }
2723
2724 //error_log('Message sent to Slack successfully.');
2725 return true;
2726 }
2727 public function handle_slack_interaction(WP_REST_Request $request) {
2728 //error_log('Received Slack interaction');
2729
2730 $payload = json_decode($request->get_param('payload'), true);
2731 //error_log('Payload: ' . print_r($payload, true));
2732
2733 // Handle button click
2734 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
2735 $session_id = $payload['actions'][0]['value'];
2736 $trigger_id = $payload['trigger_id'];
2737
2738 // Get Bot Token from settings
2739 $slack_token = $this->options['live_agent_bot_token'] ?? '';
2740
2741 if (empty($slack_token)) {
2742 //error_log('Slack Bot Token not configured');
2743 return new WP_REST_Response(['error' => 'Bot token not configured'], 400);
2744 }
2745 $response = wp_remote_post('https://slack.com/api/views.open', [
2746 'headers' => [
2747 'Content-Type' => 'application/json',
2748 'Authorization' => 'Bearer ' . $slack_token
2749 ],
2750 'body' => json_encode([
2751 'trigger_id' => $trigger_id,
2752 'view' => [
2753 'type' => 'modal',
2754 'callback_id' => 'reply_modal',
2755 'title' => [
2756 'type' => 'plain_text',
2757 'text' => 'Reply to User'
2758 ],
2759 'submit' => [
2760 'type' => 'plain_text',
2761 'text' => 'Send'
2762 ],
2763 'close' => [
2764 'type' => 'plain_text',
2765 'text' => 'Cancel'
2766 ],
2767 'blocks' => [
2768 [
2769 'type' => 'input',
2770 'block_id' => 'reply_block',
2771 'label' => [
2772 'type' => 'plain_text',
2773 'text' => "Reply to session: $session_id"
2774 ],
2775 'element' => [
2776 'type' => 'plain_text_input',
2777 'action_id' => 'message',
2778 'multiline' => true,
2779 'placeholder' => [
2780 'type' => 'plain_text',
2781 'text' => 'Type your message here...'
2782 ]
2783 ]
2784 ]
2785 ],
2786 'private_metadata' => $session_id
2787 ]
2788 ])
2789 ]);
2790
2791 //error_log('Views.open response: ' . print_r($response, true));
2792
2793 // Return immediate acknowledgment
2794 return new WP_REST_Response(['ok' => true]);
2795 }
2796
2797 // Handle modal submission
2798 if ($payload['type'] === 'view_submission') {
2799 $session_id = $payload['view']['private_metadata'];
2800 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
2801
2802 // Save the message
2803 $this->mxchat_save_chat_message($session_id, 'agent', $message);
2804
2805 return new WP_REST_Response([
2806 'response_action' => 'clear'
2807 ]);
2808 }
2809
2810 // Default acknowledgment
2811 return new WP_REST_Response(['ok' => true]);
2812 }
2813
2814 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2815 //error_log('Received agent response request');
2816 //error_log('Request data: ' . print_r($request->get_params(), true));
2817 // error_log('Raw body: ' . file_get_contents('php://input'));
2818
2819 // Get the data from Slack's slash command format
2820 $command_text = $request->get_param('text');
2821 // error_log('Command text: ' . $command_text);
2822
2823 if (empty($command_text)) {
2824 error_log('Agent response error: No command text received');
2825 return new WP_REST_Response([
2826 'error' => 'Command text is required. Format: /reply session_id message'
2827 ], 400);
2828 }
2829
2830 // Split the command text into session_id and message
2831 $parts = explode(' ', $command_text, 2);
2832 if (count($parts) !== 2) {
2833 //error_log('Agent response error: Invalid command format');
2834 return new WP_REST_Response([
2835 'error' => 'Invalid format. Use: /reply session_id message'
2836 ], 400);
2837 }
2838
2839 $session_id = sanitize_text_field($parts[0]);
2840 $message = sanitize_text_field($parts[1]);
2841
2842 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
2843
2844 // Save the message
2845 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2846
2847 if (!$message_id) {
2848 // error_log('Failed to save agent message');
2849 return new WP_REST_Response([
2850 'error' => 'Failed to save message'
2851 ], 500);
2852 }
2853
2854 // Return success response in Slack's expected format
2855 return new WP_REST_Response([
2856 'response_type' => 'in_channel',
2857 'text' => "Message sent successfully to session $session_id"
2858 ], 200);
2859 }
2860
2861
2862 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2863 //error_log("Switching back to chatbot mode via intent.");
2864
2865 // Just update mode to AI
2866 update_option("mxchat_mode_{$session_id}", 'ai');
2867
2868 // Initialize states
2869 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2870 $this->productCardHtml = '';
2871
2872 // Set the response message
2873 $this->fallbackResponse['text'] = 'You are now chatting with the AI chatbot.';
2874
2875 return true; // Intent was handled
2876 }
2877
2878
2879
2880
2881 // For the word upload handler
2882 public function mxchat_handle_word_upload() {
2883 // Delegate to word handler
2884 $this->word_handler->mxchat_handle_word_upload();
2885 }
2886
2887 // For the word removal handler
2888 public function mxchat_handle_word_remove() {
2889 // Delegate to word handler
2890 $this->word_handler->mxchat_handle_word_remove();
2891 }
2892
2893 // For the word status check
2894 public function mxchat_check_word_status() {
2895 // Delegate to word handler
2896 $this->word_handler->mxchat_check_word_status();
2897 }
2898
2899
2900 private function mxchat_get_user_identifier() {
2901 return MxChat_User::mxchat_get_user_identifier();
2902 }
2903
2904 private function mxchat_generate_embedding($text, $api_key) {
2905 $endpoint = 'https://api.openai.com/v1/embeddings';
2906
2907 $body = wp_json_encode([
2908 'input' => $text,
2909 'model' => 'text-embedding-ada-002'
2910 ]);
2911
2912 $args = [
2913 'body' => $body,
2914 'headers' => [
2915 'Content-Type' => 'application/json',
2916 'Authorization' => 'Bearer ' . $api_key,
2917 ],
2918 'timeout' => 60,
2919 'redirection' => 5,
2920 'blocking' => true,
2921 'httpversion' => '1.0',
2922 'sslverify' => true,
2923 ];
2924
2925 $response = wp_remote_post($endpoint, $args);
2926
2927 if (is_wp_error($response)) {
2928 return null;
2929 }
2930
2931 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2932
2933 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
2934 return $response_body['data'][0]['embedding'];
2935 } else {
2936 return null;
2937 }
2938 }
2939
2940 private function mxchat_find_relevant_content($user_embedding) {
2941 global $wpdb;
2942 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2943 $cache_key = 'mxchat_system_prompt_embeddings';
2944
2945 // Retrieve embeddings from cache or database
2946 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2947 if ($embeddings === false) {
2948 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
2949 $embeddings = $wpdb->get_results($query);
2950 if ($embeddings === null || empty($embeddings)) {
2951 return ''; // Return an empty string if no embeddings found
2952 }
2953 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2954 }
2955
2956 // Initialize array to store relevant results with similarity scores
2957 $relevant_results = [];
2958
2959 // Iterate through embeddings to calculate similarity
2960 foreach ($embeddings as $embedding) {
2961 $database_embedding = $embedding->embedding_vector
2962 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2963 : null;
2964
2965 if (is_array($database_embedding) && is_array($user_embedding)) {
2966 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2967 $relevant_results[] = [
2968 'id' => $embedding->id,
2969 'similarity' => $similarity
2970 ];
2971 }
2972 }
2973
2974 // Retrieve the similarity threshold
2975 $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2976
2977 // Filter and sort relevant results by similarity
2978 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2979 return $result['similarity'] >= $similarity_threshold;
2980 });
2981 usort($relevant_results, function ($a, $b) {
2982 return $b['similarity'] <=> $a['similarity'];
2983 });
2984
2985 // Limit to the top 5 results
2986 $top_results = array_slice($relevant_results, 0, 5);
2987
2988 // Initialize the final content
2989 $content = '';
2990
2991 // Fetch and combine content for the top results
2992 foreach ($top_results as $result) {
2993 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2994
2995 // Check if the content is PDF-related and add surrounding pages
2996 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2997 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2998 "SELECT article_content FROM {$system_prompt_table}
2999 WHERE id IN (
3000 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3001 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3002 )",
3003 $result['id'],
3004 $result['id']
3005 ));
3006
3007 // Add previous content if it exists
3008 if (!empty($surrounding_content[0])) {
3009 $content .= $surrounding_content[0]->article_content . "\n\n";
3010 }
3011
3012 // Add the main chunk content
3013 $content .= $chunk_content . "\n\n";
3014
3015 // Add next content if it exists
3016 if (!empty($surrounding_content[1])) {
3017 $content .= $surrounding_content[1]->article_content . "\n\n";
3018 }
3019 } else {
3020 // For non-PDF content, add directly
3021 $content .= $chunk_content . "\n\n";
3022 }
3023 }
3024
3025 return trim($content); // Return the combined content
3026 }
3027
3028 private function mxchat_find_relevant_products($user_embedding) {
3029 global $wpdb;
3030 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3031 $cache_key = 'mxchat_system_prompt_embeddings';
3032
3033 // Retrieve embeddings from cache or database
3034 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3035 if ($embeddings === false) {
3036 $query = "SELECT id, embedding_vector FROM {$system_prompt_table}";
3037 $embeddings = $wpdb->get_results($query);
3038 if ($embeddings === null || empty($embeddings)) {
3039 return ''; // Return an empty string if no embeddings found
3040 }
3041 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3042 }
3043
3044 // Initialize array to store relevant results with similarity scores
3045 $relevant_results = [];
3046
3047 // Iterate through embeddings to calculate similarity
3048 foreach ($embeddings as $embedding) {
3049 $database_embedding = $embedding->embedding_vector
3050 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3051 : null;
3052
3053 if (is_array($database_embedding) && is_array($user_embedding)) {
3054 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3055 $relevant_results[] = [
3056 'id' => $embedding->id,
3057 'similarity' => $similarity
3058 ];
3059 }
3060 }
3061
3062 // Use a fixed similarity threshold of 0.85
3063 $similarity_threshold = 0.85;
3064
3065 // Filter and sort relevant results by similarity
3066 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3067 return $result['similarity'] >= $similarity_threshold;
3068 });
3069 usort($relevant_results, function ($a, $b) {
3070 return $b['similarity'] <=> $a['similarity'];
3071 });
3072
3073 // Limit to the top 5 results
3074 $top_results = array_slice($relevant_results, 0, 5);
3075
3076 // Initialize the final content
3077 $content = '';
3078
3079 // Fetch and combine content for the top results
3080 foreach ($top_results as $result) {
3081 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3082
3083 // Check if the content is PDF-related and add surrounding pages
3084 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3085 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3086 "SELECT article_content FROM {$system_prompt_table}
3087 WHERE id IN (
3088 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3089 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3090 )",
3091 $result['id'],
3092 $result['id']
3093 ));
3094
3095 // Add previous content if it exists
3096 if (!empty($surrounding_content[0])) {
3097 $content .= $surrounding_content[0]->article_content . "\n\n";
3098 }
3099
3100 // Add the main chunk content
3101 $content .= $chunk_content . "\n\n";
3102
3103 // Add next content if it exists
3104 if (!empty($surrounding_content[1])) {
3105 $content .= $surrounding_content[1]->article_content . "\n\n";
3106 }
3107 } else {
3108 // For non-PDF content, add directly
3109 $content .= $chunk_content . "\n\n";
3110 }
3111 }
3112
3113 return trim($content); // Return the combined content
3114 }
3115
3116 private function fetch_content_with_product_links($most_relevant_id) {
3117 global $wpdb;
3118 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3119
3120 // Fetch the article content and associated product URL
3121 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
3122 $result = $wpdb->get_row($query);
3123
3124 if ($result) {
3125 // Append the product link to the content if available
3126 $content = $result->article_content;
3127 if (!empty($result->source_url)) {
3128 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
3129 }
3130 return $content;
3131 }
3132
3133 return null;
3134 }
3135
3136 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $conversation_history) {
3137 if (!$relevant_content) {
3138 return "I'm sorry, I couldn't find relevant information on that topic.";
3139 }
3140
3141 // Check the selected model
3142 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-3.5-turbo';
3143
3144 // Call the appropriate function based on the selected model
3145 if (strpos($selected_model, 'claude') !== false) {
3146 return $this->mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content);
3147 } elseif ($selected_model === 'grok-beta') {
3148 return $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content);
3149 } else {
3150 return $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content);
3151 }
3152 }
3153
3154 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3155 // Get system prompt instructions from options
3156 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3157
3158 // Add system prompt to relevant content
3159 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3160
3161 // Prepend system instructions to the conversation history
3162 array_unshift($conversation_history, [
3163 'role' => 'system',
3164 'content' => "Here are your instructions: " . $content_with_instructions
3165 ]);
3166
3167 // Log the system prompt and relevant content
3168 //error_log("System Prompt Instructions: " . $system_prompt_instructions);
3169 //error_log("Relevant Content: " . substr($relevant_content, 0, 500)); // Log first 500 characters for brevity
3170
3171 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3172 foreach ($conversation_history as &$message) {
3173 if ($message['role'] === 'bot') {
3174 $message['role'] = 'assistant';
3175 } elseif ($message['role'] === 'agent') {
3176 // Tag the message as coming from a live agent
3177 $message['role'] = 'assistant';
3178 if (!isset($message['metadata'])) {
3179 $message['metadata'] = ['source' => 'live_agent'];
3180 }
3181 }
3182
3183 // Ensure all roles are valid
3184 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3185 $message['role'] = 'user'; // Default to 'user'
3186 }
3187 }
3188
3189 // Log the formatted conversation history
3190 //error_log("Conversation History: " . json_encode($conversation_history, JSON_PRETTY_PRINT));
3191
3192 // Build the request body
3193 $body = json_encode([
3194 'model' => $selected_model,
3195 'messages' => $conversation_history,
3196 'temperature' => 0.8,
3197 'stream' => false
3198 ]);
3199
3200 // Log the full API request body
3201 //error_log("OpenAI API Request Body: " . $body);
3202
3203 // Set up the API request
3204 $args = [
3205 'body' => $body,
3206 'headers' => [
3207 'Content-Type' => 'application/json',
3208 'Authorization' => 'Bearer ' . $api_key,
3209 ],
3210 'timeout' => 60,
3211 'redirection' => 5,
3212 'blocking' => true,
3213 'httpversion' => '1.0',
3214 'sslverify' => true,
3215 ];
3216
3217 // Make the API request
3218 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3219
3220 // Log the response or error
3221 if (is_wp_error($response)) {
3222 //error_log("OpenAI API Error: " . $response->get_error_message());
3223 return "Sorry, there was an error processing your request.";
3224 }
3225
3226 // Log the raw API response
3227 //error_log("OpenAI API Raw Response: " . print_r($response, true));
3228
3229 $response_body = wp_remote_retrieve_body($response);
3230 $decoded_response = json_decode($response_body, true);
3231
3232 // Log the decoded API response
3233 error_log("OpenAI API Decoded Response: " . json_encode($decoded_response, JSON_PRETTY_PRINT));
3234
3235 if (isset($decoded_response['choices'][0]['message']['content'])) {
3236 $response_text = trim($decoded_response['choices'][0]['message']['content']);
3237 //error_log("OpenAI API Final Response: " . $response_text);
3238 return $response_text;
3239 } else {
3240 // Log an error if the expected response format is missing
3241 //error_log("OpenAI API Response Format Error: Expected 'choices[0][message][content]' not found.");
3242 return "Sorry, I couldn't process that request.";
3243 }
3244 }
3245
3246 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3247 // Get system prompt instructions from options
3248 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3249
3250 // Add system prompt to relevant content
3251 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3252
3253 // Prepend system instructions to the conversation history
3254 array_unshift($conversation_history, [
3255 'role' => 'system',
3256 'content' => "Here are your instructions: " . $content_with_instructions
3257 ]);
3258
3259 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3260 foreach ($conversation_history as &$message) {
3261 if ($message['role'] === 'bot') {
3262 $message['role'] = 'assistant';
3263 } elseif ($message['role'] === 'agent') {
3264 // Tag the message as coming from a live agent
3265 $message['role'] = 'assistant';
3266 if (!isset($message['metadata'])) {
3267 $message['metadata'] = ['source' => 'live_agent'];
3268 }
3269 }
3270
3271 // Ensure all roles are valid
3272 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3273 $message['role'] = 'user'; // Default to 'user'
3274 }
3275 }
3276
3277
3278 // Build the request body
3279 $body = json_encode([
3280 'model' => $selected_model,
3281 'messages' => $conversation_history,
3282 'temperature' => 0.8,
3283 'stream' => false
3284 ]);
3285
3286 // Set up the API request
3287 $args = [
3288 'body' => $body,
3289 'headers' => [
3290 'Content-Type' => 'application/json',
3291 'Authorization' => 'Bearer ' . $xai_api_key,
3292 ],
3293 'timeout' => 60,
3294 'redirection' => 5,
3295 'blocking' => true,
3296 'httpversion' => '1.0',
3297 'sslverify' => true,
3298 ];
3299
3300 // Make the API request
3301 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3302
3303 // Process the response
3304 if (is_wp_error($response)) {
3305 return "Sorry, there was an error processing your request.";
3306 }
3307
3308 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3309
3310 if (isset($response_body['choices'][0]['message']['content'])) {
3311 return trim($response_body['choices'][0]['message']['content']);
3312 } else {
3313 return "Sorry, I couldn't process that request.";
3314 }
3315 }
3316
3317 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3318 // Get system prompt instructions from options for Claude's top-level system parameter
3319 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3320
3321 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3322 foreach ($conversation_history as &$message) {
3323 if ($message['role'] === 'bot') {
3324 $message['role'] = 'assistant';
3325 } elseif ($message['role'] === 'agent') {
3326 // Tag the message as coming from a live agent
3327 $message['role'] = 'assistant';
3328 if (!isset($message['metadata'])) {
3329 $message['metadata'] = ['source' => 'live_agent'];
3330 }
3331 }
3332
3333 // Ensure all roles are valid
3334 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3335 $message['role'] = 'user'; // Default to 'user'
3336 }
3337 }
3338
3339 // Add relevant content as the latest user message in conversation history
3340 $conversation_history[] = [
3341 'role' => 'user',
3342 'content' => $relevant_content
3343 ];
3344
3345 // Build the request body with Claude's expected structure, using system instructions as a top-level parameter
3346 $body = json_encode([
3347 'model' => $selected_model,
3348 'max_tokens' => 1000,
3349 'temperature' => 0.8,
3350 'system' => $system_prompt_instructions, // Set the system prompt at the top level as required
3351 'messages' => $conversation_history
3352 ]);
3353
3354 // Set up the API request with the necessary headers
3355 $args = [
3356 'body' => $body,
3357 'headers' => [
3358 'Content-Type' => 'application/json',
3359 'x-api-key' => $claude_api_key,
3360 'anthropic-version' => '2023-06-01',
3361 ],
3362 'timeout' => 60,
3363 'redirection' => 5,
3364 'blocking' => true,
3365 'httpversion' => '1.0',
3366 'sslverify' => true,
3367 ];
3368
3369 // Make the API request
3370 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
3371
3372 /*
3373 // Check for errors and log the response for debugging
3374 if (is_wp_error($response)) {
3375 error_log("Claude API request error: " . print_r($response->get_error_message(), true));
3376 return "Sorry, there was an error processing your request.";
3377 }
3378 */
3379
3380
3381 // Decode the response and parse according to the expected Claude response structure
3382 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3383 //error_log("Claude API response: " . print_r($response_body, true));
3384
3385 // Check if the response has the expected 'content' array with 'text' blocks
3386 if (isset($response_body['content'][0]['text'])) {
3387 return trim($response_body['content'][0]['text']);
3388 } else {
3389 return "Sorry, I couldn't process that request.";
3390 }
3391 }
3392
3393 public function mxchat_dismiss_pre_chat_message() {
3394 // Get and sanitize the user identifier
3395 $user_id = $this->mxchat_get_user_identifier();
3396 $user_id = sanitize_key($user_id);
3397
3398 // Set a transient to track that the user has dismissed the pre-chat message
3399 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3400 set_transient($transient_key, true, DAY_IN_SECONDS);
3401
3402 wp_send_json_success();
3403 }
3404
3405 public function mxchat_check_pre_chat_message_status() {
3406 // Get and sanitize the user identifier
3407 $user_id = $this->mxchat_get_user_identifier();
3408 $user_id = sanitize_key($user_id);
3409
3410 // Check if the transient exists (i.e., if the message was dismissed)
3411 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3412 $dismissed = get_transient($transient_key);
3413
3414 // Log the result to see if it's being set correctly
3415 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
3416
3417 if ($dismissed) {
3418 wp_send_json_success(['dismissed' => true]);
3419 } else {
3420 wp_send_json_success(['dismissed' => false]);
3421 }
3422
3423 wp_die();
3424 }
3425
3426 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
3427 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
3428 return 0;
3429 }
3430
3431 $dotProduct = array_sum(array_map(function ($a, $b) {
3432 return $a * $b;
3433 }, $vectorA, $vectorB));
3434 $normA = sqrt(array_sum(array_map(function ($a) {
3435 return $a * $a;
3436 }, $vectorA)));
3437 $normB = sqrt(array_sum(array_map(function ($b) {
3438 return $b * $b;
3439 }, $vectorB)));
3440
3441 if ($normA == 0 || $normB == 0) {
3442 return 0;
3443 }
3444
3445 return $dotProduct / ($normA * $normB);
3446 }
3447
3448 public function mxchat_enqueue_scripts_styles() {
3449 // Define version numbers for the styles and scripts
3450 $chat_style_version = '1.6.2'; // Replace with your actual version
3451 $chat_script_version = '1.6.2'; // Replace with your actual version
3452
3453 // Enqueue the script
3454 wp_enqueue_script(
3455 'mxchat-chat-js',
3456 plugin_dir_url(__FILE__) . '../js/chat-script.js',
3457 array('jquery'),
3458 $chat_script_version,
3459 true
3460 );
3461
3462 // Enqueue the CSS
3463 wp_enqueue_style(
3464 'mxchat-chat-css',
3465 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3466 array(),
3467 $chat_style_version
3468 );
3469
3470 // Fetch options from the database
3471 $this->options = get_option('mxchat_options');
3472
3473 // Prepare settings for JavaScript
3474 $style_settings = array(
3475 'ajax_url' => admin_url('admin-ajax.php'),
3476 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
3477 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3478 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3479 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3480 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
3481 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
3482 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
3483 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
3484 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
3485 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
3486 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
3487 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
3488 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3489 'icon_color' => $this->options['icon_color'] ?? '#fff',
3490 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3491 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3492 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3493
3494 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3495 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3496 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3497 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3498 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3499 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3500 );
3501
3502 // Pass the settings to the script
3503 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3504 }
3505
3506
3507 public function mxchat_reset_rate_limits() {
3508 global $wpdb;
3509
3510 // Define a cache key pattern for rate limits
3511 $cache_key_pattern = 'mxchat_chat_limit_%';
3512
3513 // Retrieve all option names matching the pattern
3514 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3515 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3516
3517 // db call ok; no-cache ok
3518 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3519 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3520
3521 // Clear the relevant cache entries
3522 foreach ($option_names as $option_name) {
3523 wp_cache_delete($option_name, 'options');
3524 }
3525
3526 // Optionally, clear a general cache if you have one
3527 wp_cache_delete('mxchat_all_chat_limits', 'options');
3528 }
3529
3530 private function mxchat_fetch_woocommerce_products() {
3531 // Ensure WooCommerce is active
3532 if (!class_exists('WooCommerce')) {
3533 return [];
3534 }
3535
3536 $args = array(
3537 'post_type' => 'product',
3538 'post_status' => 'publish',
3539 'posts_per_page' => -1,
3540 );
3541
3542 $products = get_posts($args);
3543 $product_data = [];
3544
3545 foreach ($products as $product) {
3546 $product_id = $product->ID;
3547 $product_obj = wc_get_product($product_id);
3548
3549 $product_data[] = array(
3550 'id' => $product_id,
3551 'name' => $product_obj->get_name(),
3552 'description' => $product_obj->get_description(),
3553 'short_description' => $product_obj->get_short_description(),
3554 'url' => get_permalink($product_id),
3555 'price' => $product_obj->get_regular_price(),
3556 'sale_price' => $product_obj->get_sale_price(),
3557 'stock_status' => $product_obj->get_stock_status(),
3558 'sku' => $product_obj->get_sku(),
3559 'in_stock' => $product_obj->is_in_stock(),
3560 'total_sales' => $product_obj->get_total_sales(),
3561 );
3562 }
3563
3564 return $product_data;
3565 }
3566
3567 }
3568 ?>
3569