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

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