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

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