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

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

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