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

3,458 lines 128.4 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 // New function to check intents and invoke the callback function
965 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
966 global $wpdb;
967 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
968
969 //error_log('🔍 MXCHAT DEBUG: Intent Check Started ==================');
970 //error_log("🔍 MXCHAT DEBUG: Message: '$message'");
971 //error_log("🔍 MXCHAT DEBUG: Chat Mode: $chat_mode");
972
973 // Generate the user embedding
974 //error_log('🔄 MXCHAT DEBUG: Generating user embedding');
975 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
976 if (!is_array($user_embedding)) {
977 //error_log('❌ MXCHAT DEBUG: Failed to generate user embedding');
978 return false;
979 }
980 //error_log('�
981 MXCHAT DEBUG: User embedding generated successfully');
982
983 // Fetch intents from the database
984 $table_name = $wpdb->prefix . 'mxchat_intents';
985 if ($chat_mode === 'agent') {
986 //error_log('🔍 MXCHAT DEBUG: Agent mode - fetching only chatbot switch intent');
987 $query = $wpdb->prepare(
988 "SELECT * FROM $table_name WHERE callback_function = %s",
989 'mxchat_handle_switch_to_chatbot_intent'
990 );
991 $intents = $wpdb->get_results($query);
992 } else {
993 //error_log('🔍 MXCHAT DEBUG: AI mode - fetching all intents');
994 $intents = $wpdb->get_results("SELECT * FROM $table_name");
995 }
996
997 //error_log('🔍 MXCHAT DEBUG: Found ' . count($intents) . ' intents to check');
998
999 if (empty($intents)) {
1000 //error_log('❌ MXCHAT DEBUG: No intents found in database');
1001 return false;
1002 }
1003
1004 $highest_similarity = -INF;
1005 $matched_intent = null;
1006
1007 //error_log('📊 MXCHAT DEBUG: Intent Similarity Scores ==================');
1008 foreach ($intents as $intent) {
1009 //error_log("🔄 MXCHAT DEBUG: Checking intent: '{$intent->intent_label}' (callback: {$intent->callback_function})");
1010
1011 $intent_embedding_serialized = $intent->embedding_vector;
1012 $intent_embedding = $intent_embedding_serialized
1013 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1014 : null;
1015
1016 if (!is_array($intent_embedding)) {
1017 //error_log("❌ MXCHAT DEBUG: Invalid embedding for intent: {$intent->intent_label}");
1018 continue;
1019 }
1020
1021 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1022 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1023
1024
1025 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1026 $highest_similarity = $similarity;
1027 $matched_intent = $intent;
1028 //error_log("�
1029 MXCHAT DEBUG: New best match: '{$intent->intent_label}' with similarity {$similarity}");
1030 }
1031 }
1032 //error_log('📊 MXCHAT DEBUG: End Intent Scores ==================');
1033
1034 if ($matched_intent) {
1035 //error_log("🎯 MXCHAT DEBUG: Final Intent Match: '{$matched_intent->intent_label}'");
1036 //error_log("🔄 MXCHAT DEBUG: Invoking callback: {$matched_intent->callback_function}");
1037
1038 // If the callback is a method on this instance (core callback), call it directly
1039 if (method_exists($this, $matched_intent->callback_function)) {
1040 //error_log('🔍 MXCHAT DEBUG: Using direct method call for core callback');
1041 $callback_result = call_user_func(
1042 [$this, $matched_intent->callback_function],
1043 $message,
1044 $user_id,
1045 $session_id,
1046 $matched_intent,
1047 $user_context // Add user context
1048 );
1049 } else {
1050 //error_log('🔍 MXCHAT DEBUG: Using apply_filters for add-on callback');
1051 // Otherwise, use apply_filters for add-on callbacks
1052 $callback_result = apply_filters(
1053 $matched_intent->callback_function,
1054 false, // default return value
1055 $message,
1056 $user_id,
1057 $session_id,
1058 $matched_intent
1059 );
1060 }
1061
1062 //error_log('🔍 MXCHAT DEBUG: Callback result type: ' . gettype($callback_result));
1063 if ($callback_result !== false) {
1064 //error_log('�
1065 MXCHAT DEBUG: Intent handled successfully');
1066 $this->fallbackResponse = $callback_result;
1067 return true;
1068 }
1069 //error_log('❌ MXCHAT DEBUG: Callback returned false');
1070 } else {
1071 //error_log('❌ MXCHAT DEBUG: No matching intent found');
1072 }
1073
1074 //error_log('🔍 MXCHAT DEBUG: Intent Check Completed ==================');
1075 return false;
1076 }
1077
1078
1079 // Helper function to clear PDF and Word document related transients
1080 private function clear_pdf_transients($session_id) {
1081 // PDF transients
1082 delete_transient('mxchat_pdf_url_' . $session_id);
1083 delete_transient('mxchat_pdf_embeddings_' . $session_id);
1084 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
1085 delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1086
1087 // Word document transients
1088 delete_transient('mxchat_word_url_' . $session_id);
1089 delete_transient('mxchat_word_filename_' . $session_id);
1090 delete_transient('mxchat_word_embeddings_' . $session_id);
1091 delete_transient('mxchat_include_word_in_context_' . $session_id);
1092 delete_transient('mxchat_waiting_for_word_' . $session_id);
1093 }
1094
1095
1096
1097 //verified good
1098 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1099 // Log the message safely
1100 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1101
1102 // Initiate email capture flow
1103 $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1104
1105 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1106 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1107
1108 // Respond to the user
1109 wp_send_json(['message' => $response]);
1110 wp_die();
1111 }
1112
1113 //very good
1114 public function mxchat_generate_image($message, $user_id, $session_id) {
1115 // Prepare a prompt for DALL-E
1116 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1117
1118 // Use the existing OpenAI API key
1119 $openai_api_key = sanitize_text_field($this->options['api_key']);
1120
1121 // Call DALL-E to generate an image
1122 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1123
1124 // Check if the response contains an image URL
1125 if (isset($image_response['imageUrl'])) {
1126 $image_url = esc_url_raw($image_response['imageUrl']);
1127
1128 // Construct the HTML with a CSS class instead of inline styles
1129 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1130
1131 $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1132 } else {
1133 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1134 $response_html = '';
1135 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1136 }
1137
1138 // Save both text and HTML responses
1139 $this->mxchat_save_chat_message($session_id, 'bot', $response_text . "\n" . $response_html);
1140
1141 // Prepare the response data
1142 $response_data = [
1143 'message' => $response_text,
1144 'html' => $response_html,
1145 'image_url' => $image_url ?? '',
1146 ];
1147
1148 // Send the JSON response
1149 header('Content-Type: application/json; charset=' . get_option('blog_charset'));
1150 echo json_encode($response_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1151 wp_die();
1152 }
1153 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1154 $api_url = 'https://api.openai.com/v1/images/generations';
1155 $body = json_encode([
1156 'prompt' => sanitize_text_field($prompt),
1157 'n' => 1,
1158 'size' => '1024x1024',
1159 'model' => sanitize_text_field($model),
1160 ]);
1161
1162 $args = [
1163 'body' => $body,
1164 'headers' => [
1165 'Content-Type' => 'application/json',
1166 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
1167 ],
1168 'method' => 'POST',
1169 'timeout' => absint($timeout),
1170 ];
1171
1172 $response = wp_remote_post($api_url, $args);
1173
1174 if (is_wp_error($response)) {
1175 //error_log("DALL-E request failed: " . $response->get_error_message());
1176 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
1177 }
1178
1179 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1180
1181 if (isset($response_body['data'][0]['url'])) {
1182 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
1183 } else {
1184 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
1185 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
1186 }
1187 }
1188
1189 /**
1190 * Handle web search requests.
1191 *
1192 * Sends the refined search query to the Brave Search API and displays neatly formatted,
1193 * styled search results. Results are cached for performance.
1194 *
1195 * @since 1.0.0
1196 * @param string $message The user's search query.
1197 * @param string $user_id The user identifier.
1198 * @param string $session_id The current session ID.
1199 * @return void
1200 */
1201 public function mxchat_handle_search_request( $message, $user_id, $session_id ) {
1202 // Step 1: Interpret and refine the search query
1203 $refined_search_query = $this->mxchat_interpret_search_query( $message );
1204
1205 if ( empty( $refined_search_query ) ) {
1206 $this->fallbackResponse = array(
1207 'text' => esc_html__( 'I apologize, but could you please rephrase your search request?', 'mxchat' ),
1208 );
1209 return;
1210 }
1211
1212 // Retrieve and validate API settings
1213 $options = get_option( 'mxchat_options' );
1214 $api_key = isset( $options['brave_api_key'] ) ? sanitize_text_field( $options['brave_api_key'] ) : '';
1215 $results_count = isset( $options['brave_results_count'] ) ? absint( $options['brave_results_count'] ) : 5;
1216
1217 if ( empty( $api_key ) ) {
1218 $this->fallbackResponse = array(
1219 'text' => esc_html__( 'Search functionality is temporarily unavailable. Please try again later.', 'mxchat' ),
1220 );
1221 return;
1222 }
1223
1224 // Build the API request URL
1225 $api_url = add_query_arg(
1226 array(
1227 'q' => rawurlencode( $refined_search_query ),
1228 'count' => $results_count,
1229 'text_decorations' => 'true',
1230 'rich_data' => 'true',
1231 ),
1232 'https://api.search.brave.com/res/v1/web/search'
1233 );
1234
1235 // Attempt to retrieve cached results first
1236 $transient_key = 'mxchat_search_' . md5( $refined_search_query );
1237 $results = get_transient( $transient_key );
1238
1239 if ( false === $results ) {
1240 // Fetch new results from the Brave Search API
1241 $response = wp_remote_get(
1242 $api_url,
1243 array(
1244 'headers' => array(
1245 'Accept' => 'application/json',
1246 'Accept-Encoding' => 'gzip',
1247 'X-Subscription-Token'=> $api_key,
1248 ),
1249 'timeout' => 10,
1250 )
1251 );
1252
1253 if ( is_wp_error( $response ) ) {
1254 $this->fallbackResponse = array(
1255 'text' => esc_html__( 'I encountered an error while searching. Please try again.', 'mxchat' ),
1256 );
1257 return;
1258 }
1259
1260 $results = json_decode( wp_remote_retrieve_body( $response ), true );
1261
1262 if ( json_last_error() !== JSON_ERROR_NONE ) {
1263 $this->fallbackResponse = array(
1264 'text' => esc_html__( 'I received an invalid response from the search service.', 'mxchat' ),
1265 );
1266 return;
1267 }
1268
1269 // Cache results for one hour
1270 set_transient( $transient_key, $results, HOUR_IN_SECONDS );
1271 }
1272
1273 // Process and display results
1274 if ( ! empty( $results['web']['results'] ) && is_array( $results['web']['results'] ) ) {
1275 $html = $this->generate_search_results_html( $results['web']['results'], $refined_search_query );
1276
1277 // Only return HTML (no large text summary)
1278 $this->fallbackResponse = array(
1279 'html' => $html,
1280 );
1281
1282 // Save to chat history
1283 $this->mxchat_save_chat_message( $session_id, 'bot', $html );
1284 } else {
1285 $this->fallbackResponse = array(
1286 'text' => sprintf(
1287 esc_html__( 'I couldn\'t find any relevant results for "%s". Would you like to try different search terms?', 'mxchat' ),
1288 esc_html( $refined_search_query )
1289 ),
1290 );
1291 }
1292 }
1293
1294
1295 /**
1296 * Format search results into a natural text summary.
1297 *
1298 * @since 1.0.0
1299 * @param array $results The search results from the API.
1300 * @param string $query The original search query.
1301 * @return string The text summary of the top results.
1302 */
1303 private function format_search_results( $results, $query ) {
1304 $summary = sprintf(
1305 esc_html__( 'Here are the most relevant results for "%s":', 'mxchat' ),
1306 esc_html( $query )
1307 ) . "\n\n";
1308
1309 $max_results = min( count( $results ), 3 );
1310 for ( $i = 0; $i < $max_results; $i++ ) {
1311 $result = $results[ $i ];
1312 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1313 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1314
1315 // Append title and description to the summary
1316 $summary .= sprintf(
1317 "%s\n%s\n\n",
1318 esc_html( $title ),
1319 esc_html( $description )
1320 );
1321 }
1322
1323 return $summary;
1324 }
1325
1326 /**
1327 * Generate HTML markup for search results.
1328 *
1329 * @since 1.0.0
1330 * @param array $results The search results from the API.
1331 * @param string $query The user-refined query.
1332 * @return string The HTML markup for displaying the results.
1333 */
1334 private function generate_search_results_html( $results, $query ) {
1335 ob_start();
1336 ?>
1337 <div class="mxchat-search-results">
1338 <?php foreach ( $results as $result ) :
1339 $title = isset( $result['title'] ) ? wp_strip_all_tags( $result['title'] ) : '';
1340 $url = isset( $result['url'] ) ? esc_url( $result['url'] ) : '#';
1341 $description = isset( $result['description'] ) ? wp_strip_all_tags( $result['description'] ) : '';
1342 $favicon = isset( $result['meta_url']['favicon'] ) ? esc_url( $result['meta_url']['favicon'] ) : '';
1343 $thumbnail = isset( $result['thumbnail']['src'] ) ? esc_url( $result['thumbnail']['src'] ) : '';
1344 $domain = parse_url( $url, PHP_URL_HOST );
1345 ?>
1346 <div class="mxchat-search-item">
1347 <div class="mxchat-search-header">
1348 <?php if ( $favicon ) : ?>
1349 <img
1350 src="<?php echo esc_url( $favicon ); ?>"
1351 class="mxchat-site-icon"
1352 alt="<?php echo esc_attr__( 'Site icon', 'mxchat' ); ?>"
1353 width="16"
1354 height="16"
1355 />
1356 <?php endif; ?>
1357 <div class="mxchat-site-url"><?php echo esc_html( $domain ); ?></div>
1358 </div>
1359
1360 <div class="mxchat-search-content">
1361 <h3 class="mxchat-search-title">
1362 <a href="<?php echo esc_url( $url ); ?>"
1363 target="_blank"
1364 rel="noopener noreferrer"
1365 >
1366 <?php echo esc_html( $title ); ?>
1367 </a>
1368 </h3>
1369
1370 <?php if ( $thumbnail ) : ?>
1371 <div class="mxchat-search-thumbnail">
1372 <img
1373 src="<?php echo esc_url( $thumbnail ); ?>"
1374 alt="<?php echo esc_attr__( 'Thumbnail image', 'mxchat' ); ?>"
1375 loading="lazy"
1376 />
1377 </div>
1378 <?php endif; ?>
1379
1380 <div class="mxchat-search-description">
1381 <?php echo esc_html( $description ); ?>
1382 </div>
1383 </div>
1384 </div>
1385 <?php endforeach; ?>
1386 </div>
1387 <?php
1388 return ob_get_clean();
1389 }
1390
1391
1392 //very good
1393 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1394
1395 // Step 1: Interpret the search query for better results
1396 $refined_search_query = $this->mxchat_interpret_search_query($message);
1397
1398
1399 // If no query was interpreted, return a fallback message
1400 if (empty($refined_search_query)) {
1401 $this->fallbackResponse = [
1402 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1403 'html' => "",
1404 ];
1405 return;
1406 }
1407
1408 // Brave API URL
1409 $api_url = 'https://api.search.brave.com/res/v1/images/search';
1410
1411 // Retrieve Brave API settings
1412 $options = get_option('mxchat_options');
1413 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1414
1415 if (empty($api_key)) {
1416 /*
1417 if (defined('WP_DEBUG') && WP_DEBUG) {
1418 error_log("Brave API key is missing.");
1419 }
1420 */
1421
1422 $this->fallbackResponse = [
1423 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1424 'html' => "",
1425 ];
1426 return;
1427 }
1428
1429 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1430 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
1431
1432 // Append query parameters based on settings
1433 $api_url = add_query_arg([
1434 'q' => rawurlencode($refined_search_query),
1435 'count' => $image_count,
1436 'safesearch' => $safe_search,
1437 ], $api_url);
1438
1439 /*
1440 // Log the final API URL for the search
1441 if (defined('WP_DEBUG') && WP_DEBUG) {
1442 error_log("Final API URL for Brave Image Search: " . esc_url_raw($api_url));
1443 }
1444 */
1445
1446
1447 // Implement caching
1448 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1449 $body = get_transient($transient_key);
1450
1451 if (false === $body) {
1452 $args = [
1453 'headers' => [
1454 'Accept' => 'application/json',
1455 'Accept-Encoding' => 'gzip',
1456 'X-Subscription-Token' => $api_key,
1457 ],
1458 'timeout' => 10,
1459 ];
1460
1461 $response = wp_remote_get($api_url, $args);
1462
1463 if (is_wp_error($response)) {
1464 /*
1465 if (defined('WP_DEBUG') && WP_DEBUG) {
1466 error_log("Brave Image API request failed: " . $response->get_error_message());
1467 }
1468 */
1469
1470 $this->fallbackResponse = [
1471 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1472 'html' => "",
1473 ];
1474 return;
1475 }
1476
1477 $body = json_decode(wp_remote_retrieve_body($response), true);
1478 set_transient($transient_key, $body, HOUR_IN_SECONDS);
1479 }
1480
1481 // Process the API response
1482 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1483 $html_output = '<div class="mxchat-image-gallery">';
1484
1485 foreach ($body['results'] as $image) {
1486 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1487 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1488 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1489
1490 if ($image_url && $thumbnail_url) {
1491 $html_output .= '<div class="mxchat-image-item">';
1492 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
1493 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
1494 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
1495 $html_output .= '</a></div>';
1496 }
1497 }
1498
1499 $html_output .= '</div>';
1500
1501 $this->fallbackResponse = [
1502 'text' => "",
1503 'html' => $html_output,
1504 ];
1505
1506 // Save response in chat history
1507 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1508
1509 } else {
1510 /*
1511 if (defined('WP_DEBUG') && WP_DEBUG) {
1512 error_log("Brave Image API response did not contain expected data structure or was empty: " . print_r($body, true));
1513 }
1514 */
1515
1516 $this->fallbackResponse = [
1517 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1518 'html' => "",
1519 ];
1520 }
1521 }
1522 public function mxchat_interpret_search_query($user_query) {
1523 $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');
1524
1525 // Retrieve OpenAI API key using 'api_key' as the option key
1526 $api_key = isset($this->options['api_key']) ? sanitize_text_field($this->options['api_key']) : sanitize_text_field(get_option('mxchat_options')['api_key']);
1527
1528 /*
1529 // Log the API key check, without exposing the key
1530 if (defined('WP_DEBUG') && WP_DEBUG) {
1531 error_log("Retrieved OpenAI API Key: " . ($api_key ? "Present" : "Missing"));
1532 }
1533 */
1534
1535 if (empty($api_key)) {
1536 //error_log("OpenAI API key is missing.");
1537 return sanitize_text_field($user_query); // Default to the original query if API key is missing
1538 }
1539
1540 $url = 'https://api.openai.com/v1/chat/completions';
1541 $args = [
1542 'headers' => [
1543 'Authorization' => 'Bearer ' . $api_key,
1544 'Content-Type' => 'application/json',
1545 ],
1546 'body' => wp_json_encode([
1547 'model' => 'gpt-3.5-turbo',
1548 'messages' => [
1549 ['role' => 'system', 'content' => $system_prompt],
1550 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1551 ],
1552 'temperature' => 0.2,
1553 'max_tokens' => 20,
1554 ]),
1555 'method' => 'POST',
1556 ];
1557
1558 $response = wp_remote_post($url, $args);
1559
1560 if (is_wp_error($response)) {
1561 //error_log("OpenAI request failed: " . $response->get_error_message());
1562 return sanitize_text_field($user_query); // Fallback to the original query if there's an error
1563 }
1564
1565 $body = json_decode(wp_remote_retrieve_body($response), true);
1566
1567 // Check for a valid response and sanitize output
1568 if (isset($body['choices'][0]['message']['content'])) {
1569 $interpreted_query = sanitize_text_field(trim($body['choices'][0]['message']['content']));
1570
1571 /*
1572 // Log the interpreted query for debugging
1573 if (defined('WP_DEBUG') && WP_DEBUG) {
1574 error_log("Interpreted search query: " . $interpreted_query);
1575 }
1576 */
1577
1578 return $interpreted_query;
1579 } else {
1580 //error_log("Unexpected API response format: " . print_r($body, true));
1581 return sanitize_text_field($user_query);
1582 }
1583 }
1584
1585
1586
1587 private function find_product_in_message($message) {
1588 global $wpdb;
1589
1590 // Get embedding for the search query
1591 $query_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1592 if (!is_array($query_embedding)) {
1593 return null;
1594 }
1595
1596 // Get relevant content as string
1597 $relevant_content = $this->mxchat_find_relevant_products($query_embedding);
1598 if (empty($relevant_content)) {
1599 // Return null to indicate no results and set fallback response
1600 $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');
1601 return null;
1602 }
1603
1604 // Extract product URLs from the content
1605 preg_match_all('/https?:\/\/[^\s<>"\']+?\/product\/[^\s<>"\']+/', $relevant_content, $matches);
1606
1607 if (!empty($matches[0])) {
1608 // Try each URL found
1609 foreach ($matches[0] as $url) {
1610 // Clean the URL
1611 $url = rtrim($url, '/."\']');
1612
1613 // Get the product slug
1614 $path = parse_url($url, PHP_URL_PATH);
1615 $slug = basename(rtrim($path, '/'));
1616
1617 // Find product by slug
1618 $args = array(
1619 'post_type' => 'product',
1620 'post_status' => 'publish',
1621 'name' => $slug,
1622 'posts_per_page' => 1
1623 );
1624
1625 $products = get_posts($args);
1626
1627 if (!empty($products)) {
1628 $product_id = $products[0]->ID;
1629 $product = wc_get_product($product_id);
1630
1631 if ($product && $product->is_purchasable()) {
1632 return $product_id;
1633 }
1634 }
1635 }
1636 }
1637
1638 // Fallback: Look for product names in the content
1639 $products = wc_get_products([
1640 'status' => 'publish',
1641 'limit' => -1,
1642 'return' => 'all'
1643 ]);
1644
1645 foreach ($products as $product) {
1646 $name = $product->get_name();
1647 if (stripos($relevant_content, $name) !== false) {
1648 if ($product->is_purchasable()) {
1649 return $product->get_id();
1650 }
1651 }
1652 }
1653
1654 // If no product is found after all checks, set the fallback response
1655 $this->fallbackResponse['text'] = esc_html__("I couldn't find any relevant products based on your query. Try to be more specific", 'mxchat');
1656 return null;
1657 }
1658
1659 // New method to handle intent responses
1660 private function generate_intent_response($context_content, $session_id) {
1661 // Convert the context array to a structured string for the AI
1662 $context_string = $this->format_intent_context($context_content);
1663
1664 // Generate AI response using the context
1665 $response = $this->mxchat_generate_response(
1666 $context_string,
1667 $this->options['api_key'],
1668 $this->options['xai_api_key'],
1669 $this->options['claude_api_key'],
1670 $this->options['deepseek_api_key'],
1671 $this->mxchat_fetch_conversation_history_for_ai($session_id)
1672 );
1673
1674 $this->fallbackResponse['text'] = $response;
1675 return true;
1676 }
1677 // Helper method to format intent context
1678 private function format_intent_context($context) {
1679 $context_string = esc_html__("INTENT CONTEXT:\n", 'mxchat');
1680
1681 switch ($context['intent']) {
1682 case 'add_to_cart':
1683 if ($context['status'] === 'success') {
1684 $context_string .= esc_html__("Action: Successfully added product to cart\n", 'mxchat');
1685 $context_string .= sprintf(esc_html__("Product: %s\n", 'mxchat'), $context['product']['name']);
1686 $context_string .= esc_html__("Available actions: ", 'mxchat') . implode(', ', $context['available_actions']) . "\n";
1687 $context_string .= sprintf(esc_html__("Cart URL: %s\n", 'mxchat'), $context['cart_url']);
1688 $context_string .= esc_html__("\nPlease inform the user of the successful addition and their available options.", 'mxchat');
1689 } else {
1690 $context_string .= esc_html__("Action: Failed to add product to cart\n", 'mxchat');
1691 $context_string .= sprintf(esc_html__("Reason: %s\n", 'mxchat'), $context['reason']);
1692 switch ($context['reason']) {
1693 case 'woocommerce_not_available':
1694 $context_string .= esc_html__("\nPlease inform the user that shopping features are not available.", 'mxchat');
1695 break;
1696 case 'no_product_context':
1697 $context_string .= esc_html__("\nPlease ask the user to specify which product they want to add.", 'mxchat');
1698 break;
1699 case 'product_not_found':
1700 $context_string .= esc_html__("\nPlease inform the user that the product couldn't be found and ask them to try again.", 'mxchat');
1701 break;
1702 case 'add_to_cart_failed':
1703 $context_string .= esc_html__("\nPlease apologize to the user and suggest they try again or ask for assistance.", 'mxchat');
1704 break;
1705 }
1706 }
1707 break;
1708 }
1709
1710 return $context_string;
1711 }
1712
1713
1714 //very good
1715 private function add_email_to_loops($email) {
1716 // Sanitize the email
1717 $email = sanitize_email($email);
1718
1719 // Retrieve and sanitize options
1720 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
1721 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
1722
1723 // Check for missing API key or mailing list ID
1724 if (empty($api_key) || empty($mailing_list_id)) {
1725 //error_log(esc_html__('Loops API key or mailing list ID is missing.', 'mxchat'));
1726 return;
1727 }
1728
1729 $data = array(
1730 'email' => $email,
1731 'subscribed' => true,
1732 'source' => __('MxChat AI Chatbot', 'mxchat'),
1733 'mailingLists' => array($mailing_list_id => true),
1734 );
1735
1736 $url = 'https://app.loops.so/api/v1/contacts/create';
1737 $args = array(
1738 'body' => wp_json_encode($data),
1739 'headers' => array(
1740 'Authorization' => 'Bearer ' . $api_key,
1741 'Content-Type' => 'application/json',
1742 ),
1743 'method' => 'POST',
1744 'timeout' => 45,
1745 );
1746
1747 $response = wp_remote_post($url, $args);
1748
1749 // Handle errors in the API request
1750 if (is_wp_error($response)) {
1751 //error_log(esc_html__('Error adding email to Loops: ', 'mxchat') . $response->get_error_message());
1752 return;
1753 }
1754
1755 // Check for non-200 HTTP responses
1756 $response_code = wp_remote_retrieve_response_code($response);
1757 if ($response_code != 200) {
1758 $response_body = wp_remote_retrieve_body($response);
1759 //error_log(esc_html__('Loops API responded with code ', 'mxchat') . $response_code . ': ' . $response_body);
1760 }
1761 }
1762
1763 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
1764 // Get the maximum number of pages allowed from admin settings
1765 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
1766
1767 // Retrieve options for dynamic texts
1768 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
1769 $success_text = $this->options['pdf_intent_success_text'] ?? __("I've processed the PDF. What questions do you have about it?", 'mxchat');
1770 $error_text = $this->options['pdf_intent_error_text'] ?? __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1771
1772 // Check for explicit request for new PDF
1773 $new_pdf_requested = stripos($message, 'new') !== false ||
1774 stripos($message, 'another') !== false ||
1775 stripos($message, 'different') !== false;
1776
1777 // If user mentions adding/reading a PDF, set waiting flag
1778 if (stripos($message, 'pdf') !== false ||
1779 stripos($message, 'document') !== false ||
1780 stripos($message, 'read') !== false) {
1781 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
1782 $this->fallbackResponse['text'] = $trigger_text;
1783 return;
1784 }
1785
1786 // If we're waiting for a URL or user requested new PDF
1787 if ($new_pdf_requested || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1788 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1789 // Process URL... (rest of your existing URL processing code)
1790 } else {
1791 $this->fallbackResponse['text'] = $trigger_text;
1792 }
1793 return;
1794 }
1795
1796 // Default to proceeding with conversation if no specific PDF action is needed
1797 $this->fallbackResponse['text'] = '';
1798 }
1799 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
1800 $upload_dir = wp_upload_dir();
1801 $temp_file = null;
1802
1803 try {
1804 // Handle URL vs local file
1805 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1806 // Validate and download the file from URL
1807 $temp_file = wp_tempnam($pdf_source); // Safe temporary file name
1808 $response = wp_remote_get($pdf_source, ['timeout' => 60]);
1809
1810 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1811 //error_log(esc_html__("Failed to download PDF. Error: ", 'mxchat') . print_r($response, true));
1812 return false;
1813 }
1814
1815 file_put_contents($temp_file, wp_remote_retrieve_body($response));
1816
1817 // Validate that the downloaded file is a PDF
1818 $mime_type = mime_content_type($temp_file);
1819 if ($mime_type !== 'application/pdf') {
1820 //error_log(esc_html__("Invalid MIME type detected for PDF: ", 'mxchat') . $mime_type);
1821 unlink($temp_file);
1822 return false;
1823 }
1824 } else {
1825 // For local files, use the provided path directly
1826 $temp_file = $pdf_source;
1827 }
1828
1829 // Parse and process the PDF
1830 $parser = new \Smalot\PdfParser\Parser();
1831 $pdf = $parser->parseFile($temp_file);
1832 $pages = $pdf->getPages();
1833
1834 if (count($pages) > $max_pages) {
1835 //error_log(esc_html__("PDF exceeds the maximum allowed pages: ", 'mxchat') . count($pages));
1836 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
1837 unlink($temp_file);
1838 }
1839 return esc_html__('too_many_pages', 'mxchat');
1840 }
1841
1842 $embeddings = [];
1843 foreach ($pages as $page_number => $page) {
1844 $text = $page->getText();
1845
1846 // Ensure text is non-empty before generating embeddings
1847 if (empty(trim($text))) {
1848 //error_log(esc_html__("Skipping empty page: ", 'mxchat') . ($page_number + 1));
1849 continue;
1850 }
1851
1852 $embedding = $this->mxchat_generate_embedding(
1853 esc_html__("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
1854 $this->options['api_key']
1855 );
1856
1857 if ($embedding) {
1858 $embeddings[] = [
1859 'page_number' => $page_number + 1,
1860 'embedding' => $embedding,
1861 'text' => $text,
1862 ];
1863 } else {
1864 //error_log(esc_html__("Failed to generate embedding for page ", 'mxchat') . ($page_number + 1));
1865 }
1866 }
1867
1868 // Clean up downloaded file if it was from URL
1869 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
1870 unlink($temp_file);
1871 }
1872
1873 return $embeddings;
1874
1875 } catch (\Exception $e) {
1876 // error_log(esc_html__("Error parsing or processing PDF: ", 'mxchat') . $e->getMessage());
1877
1878 // Cleanup in case of exception
1879 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
1880 unlink($temp_file);
1881 }
1882
1883 return false;
1884 }
1885 }
1886 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
1887 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
1888
1889 $most_relevant = null;
1890 $highest_similarity = -INF;
1891
1892 foreach ($embeddings as $page_data) {
1893 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
1894
1895 if ($similarity > $highest_similarity) {
1896 $highest_similarity = $similarity;
1897 $most_relevant = $page_data['page_number'];
1898 }
1899 }
1900
1901 if (!is_null($most_relevant)) {
1902 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
1903 return array_filter($embeddings, function ($page) use ($page_numbers) {
1904 return in_array($page['page_number'], $page_numbers);
1905 });
1906 }
1907
1908 return [];
1909 }
1910 // Add this to your class
1911 public function handle_pdf_upload() {
1912 check_ajax_referer('mxchat_chat_nonce', 'nonce');
1913
1914 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
1915 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
1916 return;
1917 }
1918
1919 $file = $_FILES['pdf_file'];
1920 $session_id = sanitize_text_field($_POST['session_id']);
1921 $original_filename = sanitize_text_field($file['name']);
1922
1923 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
1924 if ($file_type['type'] !== 'application/pdf') {
1925 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
1926 return;
1927 }
1928
1929 $upload_dir = wp_upload_dir();
1930 $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
1931 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
1932
1933 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
1934 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
1935 return;
1936 }
1937
1938 $this->clear_pdf_transients($session_id);
1939
1940 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
1941 $embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
1942
1943 if ($embeddings === 'too_many_pages') {
1944 unlink($pdf_path);
1945 $error_message = sprintf(
1946 $this->options['pdf_intent_error_text'] ??
1947 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1948 $max_pages
1949 );
1950 wp_send_json_error($error_message);
1951 return;
1952 }
1953
1954 if ($embeddings === false || empty($embeddings)) {
1955 unlink($pdf_path);
1956 $error_message = $this->options['pdf_intent_error_text'] ??
1957 esc_html__('The uploaded PDF appears to be empty or contains unsupported content.', 'mxchat');
1958 wp_send_json_error($error_message);
1959 return;
1960 }
1961
1962 if (!empty($embeddings)) {
1963 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
1964 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
1965 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1966 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1967
1968 $success_message = $this->options['pdf_intent_success_text'] ??
1969 esc_html__("I've processed the PDF. What questions do you have about it?", 'mxchat');
1970
1971 wp_send_json_success([
1972 'message' => $success_message,
1973 'filename' => $original_filename
1974 ]);
1975 return;
1976 }
1977
1978 unlink($pdf_path);
1979 $error_message = $this->options['pdf_intent_error_text'] ??
1980 esc_html__('Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.', 'mxchat');
1981 wp_send_json_error($error_message);
1982 return;
1983 }
1984 public function handle_pdf_remove() {
1985 check_ajax_referer('mxchat_chat_nonce', 'nonce');
1986
1987 if (empty($_POST['session_id'])) {
1988 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
1989 wp_die();
1990 }
1991
1992 $session_id = sanitize_text_field($_POST['session_id']);
1993 $pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
1994
1995 if ($pdf_path && file_exists($pdf_path)) {
1996 unlink($pdf_path);
1997 }
1998
1999 $this->clear_pdf_transients($session_id);
2000
2001 wp_send_json_success([
2002 'message' => esc_html__('PDF removed successfully.', 'mxchat')
2003 ]);
2004 wp_die();
2005 }
2006
2007
2008
2009
2010 function mxchat_fetch_new_messages() {
2011 $session_id = sanitize_text_field($_POST['session_id']);
2012 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2013 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
2014 $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2015
2016 if (empty($session_id)) {
2017 //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
2018 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
2019 wp_die();
2020 }
2021
2022 $history = get_option("mxchat_history_{$session_id}", []);
2023
2024 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2025 // If persistence is enabled, show all new messages
2026 if ($persistence_enabled) {
2027 return !empty($message['id']) &&
2028 strcmp($message['id'], $last_seen_id) > 0 &&
2029 $message['role'] === 'agent';
2030 }
2031
2032 // If persistence is disabled, only show messages after initial timestamp
2033 return !empty($message['id']) &&
2034 $message['role'] === 'agent' &&
2035 $message['timestamp'] > $initial_timestamp;
2036 });
2037
2038 //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
2039
2040 wp_send_json_success([
2041 'new_messages' => array_values($new_messages)
2042 ]);
2043 wp_die();
2044 }
2045
2046
2047 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2048 // First check if live agents are available
2049 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2050 if ($live_agent_available !== 'on') {
2051 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2052 $this->fallbackResponse = [
2053 'text' => $away_message,
2054 'html' => '',
2055 'images' => [],
2056 'chat_mode' => 'ai'
2057 ];
2058 wp_send_json([
2059 'text' => $away_message,
2060 'html' => '',
2061 'chat_mode' => 'ai',
2062 'session_id' => $session_id
2063 ]);
2064 wp_die();
2065 }
2066
2067 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2068 if (empty($slack_webhook_url)) {
2069 return false;
2070 }
2071
2072 // Get recent chat history (last 5 messages)
2073 $history = get_option("mxchat_history_{$session_id}", []);
2074 $recent_history = array_slice($history, -5); // Get last 5 messages
2075
2076 // Format conversation history
2077 $conversation_context = "";
2078 if (!empty($recent_history)) {
2079 $conversation_context = "*Recent Conversation:*\n";
2080 foreach ($recent_history as $hist_message) {
2081 $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
2082 $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
2083 }
2084 $conversation_context .= "\n";
2085 }
2086
2087 update_option("mxchat_mode_{$session_id}", 'agent');
2088
2089 $webhook_data = [
2090 'blocks' => [
2091 [
2092 'type' => 'header',
2093 'text' => [
2094 'type' => 'plain_text',
2095 'text' => '🔔 New Live Agent Request',
2096 'emoji' => true
2097 ]
2098 ],
2099 [
2100 'type' => 'section',
2101 'fields' => [
2102 [
2103 'type' => 'mrkdwn',
2104 'text' => sprintf('*User ID:*\n`%s`', $user_id)
2105 ],
2106 [
2107 'type' => 'mrkdwn',
2108 'text' => sprintf('*Session ID:*\n`%s`', $session_id)
2109 ]
2110 ]
2111 ]
2112 ]
2113 ];
2114
2115 // Add conversation history if exists
2116 if (!empty($conversation_context)) {
2117 $webhook_data['blocks'][] = [
2118 'type' => 'section',
2119 'text' => [
2120 'type' => 'mrkdwn',
2121 'text' => $conversation_context
2122 ]
2123 ];
2124 }
2125
2126 // Add the current message
2127 $webhook_data['blocks'][] = [
2128 'type' => 'section',
2129 'text' => [
2130 'type' => 'mrkdwn',
2131 'text' => sprintf('*Current Message:*\n%s', $message)
2132 ]
2133 ];
2134
2135 // Add the reply button
2136 $webhook_data['blocks'][] = [
2137 'type' => 'actions',
2138 'elements' => [
2139 [
2140 'type' => 'button',
2141 'text' => [
2142 'type' => 'plain_text',
2143 'text' => '✍️ Reply',
2144 'emoji' => true
2145 ],
2146 'value' => $session_id,
2147 'action_id' => 'reply_to_user',
2148 'style' => 'primary'
2149 ]
2150 ]
2151 ];
2152
2153 $response = wp_remote_post($slack_webhook_url, [
2154 'body' => json_encode($webhook_data),
2155 'headers' => [
2156 'Content-Type' => 'application/json',
2157 ],
2158 ]);
2159
2160 if (is_wp_error($response)) {
2161 return false;
2162 }
2163
2164 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2165 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2166
2167 $this->fallbackResponse = [
2168 'text' => $success_message,
2169 'html' => '',
2170 'images' => [],
2171 'chat_mode' => 'agent'
2172 ];
2173
2174 wp_send_json([
2175 'success' => true,
2176 'text' => $success_message,
2177 'html' => '',
2178 'chat_mode' => 'agent',
2179 'session_id' => $session_id,
2180 'fallbackResponse' => $this->fallbackResponse
2181 ]);
2182 wp_die();
2183 }
2184 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2185 $slack_webhook_url = $this->options['live_agent_webhook_url'] ?? '';
2186
2187 if (empty($slack_webhook_url)) {
2188 //error_log(esc_html__('Slack Webhook URL is not configured.', 'mxchat'));
2189 return false;
2190 }
2191
2192 $webhook_data = [
2193 'blocks' => [
2194 [
2195 'type' => 'header',
2196 'text' => [
2197 'type' => 'plain_text',
2198 'text' => esc_html__('📩 New Chat Message', 'mxchat'),
2199 'emoji' => true
2200 ]
2201 ],
2202 [
2203 'type' => 'section',
2204 'fields' => [
2205 [
2206 'type' => 'mrkdwn',
2207 'text' => sprintf(esc_html__('*User ID:*\n`%s`', 'mxchat'), $user_id)
2208 ],
2209 [
2210 'type' => 'mrkdwn',
2211 'text' => sprintf(esc_html__('*Session ID:*\n`%s`', 'mxchat'), $session_id)
2212 ]
2213 ]
2214 ],
2215 [
2216 'type' => 'section',
2217 'text' => [
2218 'type' => 'mrkdwn',
2219 'text' => sprintf(esc_html__('*Message:*\n%s', 'mxchat'), $message)
2220 ]
2221 ],
2222 [
2223 'type' => 'actions',
2224 'elements' => [
2225 [
2226 'type' => 'button',
2227 'text' => [
2228 'type' => 'plain_text',
2229 'text' => esc_html__('✍️ Reply', 'mxchat'),
2230 'emoji' => true
2231 ],
2232 'value' => $session_id,
2233 'action_id' => 'reply_to_user',
2234 'style' => 'primary'
2235 ]
2236 ]
2237 ]
2238 ]
2239 ];
2240
2241 $response = wp_remote_post($slack_webhook_url, [
2242 'body' => json_encode($webhook_data),
2243 'headers' => [
2244 'Content-Type' => 'application/json',
2245 ],
2246 ]);
2247
2248 if (is_wp_error($response)) {
2249 //error_log(esc_html__('Error sending message to Slack: ', 'mxchat') . $response->get_error_message());
2250 return false;
2251 }
2252
2253 //error_log(esc_html__('Message sent to Slack successfully.', 'mxchat'));
2254 return true;
2255 }
2256 public function handle_slack_interaction(WP_REST_Request $request) {
2257 //error_log('Received Slack interaction');
2258
2259 $payload = json_decode($request->get_param('payload'), true);
2260 //error_log('Payload: ' . print_r($payload, true));
2261
2262 // Handle button click
2263 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
2264 $session_id = $payload['actions'][0]['value'];
2265 $trigger_id = $payload['trigger_id'];
2266
2267 // Get Bot Token from settings
2268 $slack_token = $this->options['live_agent_bot_token'] ?? '';
2269
2270 if (empty($slack_token)) {
2271 //error_log('Slack Bot Token not configured');
2272 return new WP_REST_Response(['error' => esc_html__('Bot token not configured', 'mxchat')], 400);
2273 }
2274 $response = wp_remote_post('https://slack.com/api/views.open', [
2275 'headers' => [
2276 'Content-Type' => 'application/json',
2277 'Authorization' => 'Bearer ' . $slack_token
2278 ],
2279 'body' => json_encode([
2280 'trigger_id' => $trigger_id,
2281 'view' => [
2282 'type' => 'modal',
2283 'callback_id' => 'reply_modal',
2284 'title' => [
2285 'type' => 'plain_text',
2286 'text' => __('Reply to User', 'mxchat')
2287 ],
2288 'submit' => [
2289 'type' => 'plain_text',
2290 'text' => __('Send', 'mxchat')
2291 ],
2292 'close' => [
2293 'type' => 'plain_text',
2294 'text' => __('Cancel', 'mxchat')
2295 ],
2296 'blocks' => [
2297 [
2298 'type' => 'input',
2299 'block_id' => 'reply_block',
2300 'label' => [
2301 'type' => 'plain_text',
2302 'text' => sprintf(__('Reply to session: %s', 'mxchat'), $session_id)
2303 ],
2304 'element' => [
2305 'type' => 'plain_text_input',
2306 'action_id' => 'message',
2307 'multiline' => true,
2308 'placeholder' => [
2309 'type' => 'plain_text',
2310 'text' => __('Type your message here...', 'mxchat')
2311 ]
2312 ]
2313 ]
2314 ],
2315 'private_metadata' => $session_id
2316 ]
2317 ])
2318 ]);
2319
2320 //error_log('Views.open response: ' . print_r($response, true));
2321
2322 // Return immediate acknowledgment
2323 return new WP_REST_Response(['ok' => true]);
2324 }
2325
2326 // Handle modal submission
2327 // Handle modal submission
2328 if ($payload['type'] === 'view_submission') {
2329 $session_id = $payload['view']['private_metadata'];
2330 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
2331
2332 // Save the message (keep the message_id but don't include in response)
2333 $this->mxchat_save_chat_message($session_id, 'agent', $message);
2334
2335 // Keep the original response format for Slack
2336 return new WP_REST_Response([
2337 'response_action' => 'clear'
2338 ]);
2339 }
2340
2341 // Default acknowledgment
2342 return new WP_REST_Response(['ok' => true]);
2343 }
2344
2345 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2346 //error_log('Received agent response request');
2347 //error_log('Request data: ' . print_r($request->get_params(), true));
2348 // error_log('Raw body: ' . file_get_contents('php://input'));
2349
2350 // Get the data from Slack's slash command format
2351 $command_text = $request->get_param('text');
2352 // error_log('Command text: ' . $command_text);
2353
2354 if (empty($command_text)) {
2355 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2356 return new WP_REST_Response([
2357 'error' => esc_html__('Command text is required. Format: /reply session_id message', 'mxchat')
2358 ], 400);
2359 }
2360
2361 // Split the command text into session_id and message
2362 $parts = explode(' ', $command_text, 2);
2363 if (count($parts) !== 2) {
2364 //error_log('Agent response error: Invalid command format');
2365 return new WP_REST_Response([
2366 'error' => esc_html__('Invalid format. Use: /reply session_id message', 'mxchat')
2367 ], 400);
2368 }
2369
2370 $session_id = sanitize_text_field($parts[0]);
2371 $message = sanitize_text_field($parts[1]);
2372
2373 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
2374
2375 // Save the message
2376 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2377
2378 if (!$message_id) {
2379 // error_log('Failed to save agent message');
2380 return new WP_REST_Response([
2381 'error' => esc_html__('Failed to save message', 'mxchat')
2382 ], 500);
2383 }
2384
2385 // Return success response in Slack's expected format
2386 return new WP_REST_Response([
2387 'response_type' => 'in_channel',
2388 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2389 ], 200);
2390 }
2391
2392
2393 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2394 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2395
2396 // Just update mode to AI
2397 update_option("mxchat_mode_{$session_id}", 'ai');
2398
2399 // Initialize states
2400 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2401 $this->productCardHtml = '';
2402
2403 // Set the response message
2404 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2405
2406 return true; // Intent was handled
2407 }
2408
2409
2410
2411
2412 // For the word upload handler
2413 public function mxchat_handle_word_upload() {
2414 // Delegate to word handler
2415 $this->word_handler->mxchat_handle_word_upload();
2416 }
2417
2418 // For the word removal handler
2419 public function mxchat_handle_word_remove() {
2420 // Delegate to word handler
2421 $this->word_handler->mxchat_handle_word_remove();
2422 }
2423
2424 // For the word status check
2425 public function mxchat_check_word_status() {
2426 // Delegate to word handler
2427 $this->word_handler->mxchat_check_word_status();
2428 }
2429
2430
2431 private function mxchat_get_user_identifier() {
2432 return MxChat_User::mxchat_get_user_identifier();
2433 }
2434
2435 private function mxchat_generate_embedding($text, $api_key) {
2436 $endpoint = 'https://api.openai.com/v1/embeddings';
2437
2438 $body = wp_json_encode([
2439 'input' => $text,
2440 'model' => 'text-embedding-ada-002'
2441 ]);
2442
2443 $args = [
2444 'body' => $body,
2445 'headers' => [
2446 'Content-Type' => 'application/json',
2447 'Authorization' => 'Bearer ' . $api_key,
2448 ],
2449 'timeout' => 60,
2450 'redirection' => 5,
2451 'blocking' => true,
2452 'httpversion' => '1.0',
2453 'sslverify' => true,
2454 ];
2455
2456 $response = wp_remote_post($endpoint, $args);
2457
2458 if (is_wp_error($response)) {
2459 return null;
2460 }
2461
2462 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2463
2464 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
2465 return $response_body['data'][0]['embedding'];
2466 } else {
2467 return null;
2468 }
2469 }
2470
2471
2472 private function mxchat_find_relevant_content($user_embedding) {
2473 //error_log('MXChat Vector Search: Starting content search...');
2474
2475 // Retrieve the add-on settings from the database.
2476 $addon_options = get_option('mxchat_pinecone_addon_options', array());
2477
2478 // Determine whether Pinecone is enabled.
2479 // We expect the sanitized setting to be a string '1' if enabled, otherwise '0'.
2480 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
2481
2482 //error_log('Pinecone enabled flag: ' . $use_pinecone);
2483
2484 if ($use_pinecone === 1) {
2485 //error_log('MXChat Vector Search: Using Pinecone database');
2486 return $this->find_relevant_content_pinecone($user_embedding);
2487 } else {
2488 //error_log('MXChat Vector Search: Using WordPress database');
2489 return $this->find_relevant_content_wordpress($user_embedding);
2490 }
2491 }
2492
2493 private function find_relevant_content_wordpress($user_embedding) {
2494 global $wpdb;
2495 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2496 $cache_key = 'mxchat_system_prompt_embeddings';
2497 $batch_size = 500;
2498
2499 // Retrieve embeddings from cache or database
2500 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2501 if ($embeddings === false) {
2502 $embeddings = [];
2503 $offset = 0;
2504
2505 // Load in batches and build cache
2506 do {
2507 $query = $wpdb->prepare(
2508 "SELECT id, embedding_vector
2509 FROM {$system_prompt_table}
2510 LIMIT %d OFFSET %d",
2511 $batch_size,
2512 $offset
2513 );
2514
2515 $batch = $wpdb->get_results($query);
2516 if (empty($batch)) {
2517 break;
2518 }
2519
2520 $embeddings = array_merge($embeddings, $batch);
2521 $offset += $batch_size;
2522
2523 // Free memory
2524 unset($batch);
2525
2526 } while (true);
2527
2528 if (empty($embeddings)) {
2529 return ''; // Return an empty string if no embeddings found
2530 }
2531 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2532 }
2533
2534 // Initialize array to store relevant results with similarity scores
2535 $relevant_results = [];
2536 // Iterate through embeddings to calculate similarity
2537 foreach ($embeddings as $embedding) {
2538 $database_embedding = $embedding->embedding_vector
2539 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2540 : null;
2541 if (is_array($database_embedding) && is_array($user_embedding)) {
2542 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2543 $relevant_results[] = [
2544 'id' => $embedding->id,
2545 'similarity' => $similarity
2546 ];
2547 }
2548 // Free memory
2549 unset($database_embedding);
2550 }
2551
2552 // Retrieve the similarity threshold
2553 $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2554
2555 // Filter and sort relevant results by similarity
2556 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2557 return $result['similarity'] >= $similarity_threshold;
2558 });
2559 usort($relevant_results, function ($a, $b) {
2560 return $b['similarity'] <=> $a['similarity'];
2561 });
2562
2563 // Limit to the top 5 results
2564 $top_results = array_slice($relevant_results, 0, 5);
2565
2566 // Initialize the final content
2567 $content = '';
2568
2569 // Fetch and combine content for the top results
2570 foreach ($top_results as $result) {
2571 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2572 // Check if the content is PDF-related and add surrounding pages
2573 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
2574 $surrounding_content = $wpdb->get_results($wpdb->prepare(
2575 "SELECT article_content FROM {$system_prompt_table}
2576 WHERE id IN (
2577 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
2578 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
2579 )",
2580 $result['id'],
2581 $result['id']
2582 ));
2583 // Add previous content if it exists
2584 if (!empty($surrounding_content[0])) {
2585 $content .= $surrounding_content[0]->article_content . "\n\n";
2586 }
2587 // Add the main chunk content
2588 $content .= $chunk_content . "\n\n";
2589 // Add next content if it exists
2590 if (!empty($surrounding_content[1])) {
2591 $content .= $surrounding_content[1]->article_content . "\n\n";
2592 }
2593 } else {
2594 // For non-PDF content, add directly
2595 $content .= $chunk_content . "\n\n";
2596 }
2597 }
2598
2599 return trim($content);
2600 }
2601 /**
2602 * Find relevant content in Pinecone vector database
2603 */
2604 private function find_relevant_content_pinecone($user_embedding) {
2605 $options = get_option('mxchat_pinecone_addon_options', array());
2606 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2607 $host = $options['mxchat_pinecone_host'] ?? '';
2608
2609 if (empty($host) || empty($api_key)) {
2610 //error_log('Pinecone credentials not properly configured');
2611 return '';
2612 }
2613
2614 // Get similarity threshold from WordPress settings
2615 $similarity_threshold = ((int) get_option('mxchat_similarity_threshold', 80)) / 100;
2616
2617 // Prepare the query request for Pinecone
2618 $api_endpoint = "https://{$host}/query";
2619
2620 $request_body = array(
2621 'vector' => $user_embedding,
2622 'topK' => 5,
2623 'includeMetadata' => true,
2624 'includeValues' => true
2625 );
2626
2627 $response = wp_remote_post($api_endpoint, array(
2628 'headers' => array(
2629 'Api-Key' => $api_key,
2630 'accept' => 'application/json',
2631 'content-type' => 'application/json'
2632 ),
2633 'body' => wp_json_encode($request_body),
2634 'timeout' => 30
2635 ));
2636
2637 if (is_wp_error($response)) {
2638 //error_log('Pinecone query error: ' . $response->get_error_message());
2639 return '';
2640 }
2641
2642 $response_code = wp_remote_retrieve_response_code($response);
2643 if ($response_code !== 200) {
2644 //error_log('Pinecone API error: ' . wp_remote_retrieve_body($response));
2645 return '';
2646 }
2647
2648 $results = json_decode(wp_remote_retrieve_body($response), true);
2649 if (empty($results['matches'])) {
2650 return '';
2651 }
2652
2653 // Initialize the final content
2654 $content = '';
2655
2656 // Process each match
2657 foreach ($results['matches'] as $match) {
2658 // Skip if similarity is below threshold
2659 if ($match['score'] < $similarity_threshold) {
2660 continue;
2661 }
2662
2663 if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
2664 // Add content with citation
2665 $content .= $match['metadata']['text'] . "\n";
2666 $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
2667 }
2668 }
2669
2670 return trim($content);
2671 }
2672
2673
2674 private function mxchat_find_relevant_products($user_embedding) {
2675 //error_log('MXChat Vector Search: Starting product search...');
2676
2677 // Retrieve the add-on settings from the database
2678 $addon_options = get_option('mxchat_pinecone_addon_options', array());
2679
2680 // Determine whether Pinecone is enabled
2681 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
2682
2683 //error_log('Pinecone enabled flag: ' . $use_pinecone);
2684
2685 if ($use_pinecone === 1) {
2686 //error_log('MXChat Vector Search: Using Pinecone database for products');
2687 return $this->find_relevant_products_pinecone($user_embedding);
2688 } else {
2689 //error_log('MXChat Vector Search: Using WordPress database for products');
2690 return $this->find_relevant_products_wordpress($user_embedding);
2691 }
2692 }
2693
2694 private function find_relevant_products_wordpress($user_embedding) {
2695 global $wpdb;
2696 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2697 $cache_key = 'mxchat_system_prompt_embeddings';
2698 $batch_size = 500;
2699
2700 // Original WordPress database search logic
2701 // [Previous implementation remains the same]
2702 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2703 if ($embeddings === false) {
2704 $embeddings = [];
2705 $offset = 0;
2706
2707 do {
2708 $query = $wpdb->prepare(
2709 "SELECT id, embedding_vector
2710 FROM {$system_prompt_table}
2711 LIMIT %d OFFSET %d",
2712 $batch_size,
2713 $offset
2714 );
2715
2716 $batch = $wpdb->get_results($query);
2717 if (empty($batch)) {
2718 break;
2719 }
2720
2721 $embeddings = array_merge($embeddings, $batch);
2722 $offset += $batch_size;
2723
2724 unset($batch);
2725
2726 } while (true);
2727
2728 if (empty($embeddings)) {
2729 return '';
2730 }
2731 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2732 }
2733
2734 $relevant_results = [];
2735 foreach ($embeddings as $embedding) {
2736 $database_embedding = $embedding->embedding_vector
2737 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2738 : null;
2739 if (is_array($database_embedding) && is_array($user_embedding)) {
2740 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2741 $relevant_results[] = [
2742 'id' => $embedding->id,
2743 'similarity' => $similarity
2744 ];
2745 }
2746 unset($database_embedding);
2747 }
2748
2749 // Use fixed threshold for products
2750 $similarity_threshold = 0.85;
2751
2752 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2753 return $result['similarity'] >= $similarity_threshold;
2754 });
2755 usort($relevant_results, function ($a, $b) {
2756 return $b['similarity'] <=> $a['similarity'];
2757 });
2758
2759 $top_results = array_slice($relevant_results, 0, 5);
2760 $content = '';
2761
2762 foreach ($top_results as $result) {
2763 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2764 $content .= $chunk_content . "\n\n";
2765 }
2766
2767 return trim($content);
2768 }
2769
2770 // Modified search function with correct filter syntax
2771 private function find_relevant_products_pinecone($user_embedding) {
2772 //error_log('Starting Pinecone product search...');
2773
2774 $options = get_option('mxchat_pinecone_addon_options', array());
2775 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2776 $host = $options['mxchat_pinecone_host'] ?? '';
2777
2778 if (empty($host) || empty($api_key)) {
2779 //error_log('Pinecone credentials not properly configured for product search');
2780 return '';
2781 }
2782
2783 $similarity_threshold = 0.85;
2784 $api_endpoint = "https://{$host}/query";
2785
2786 $request_body = array(
2787 'vector' => $user_embedding,
2788 'topK' => 5,
2789 'includeMetadata' => true,
2790 'includeValues' => true,
2791 'filter' => array(
2792 'type' => 'product'
2793 )
2794 );
2795
2796 //error_log('Sending request to Pinecone with body: ' . wp_json_encode($request_body));
2797
2798 $response = wp_remote_post($api_endpoint, array(
2799 'headers' => array(
2800 'Api-Key' => $api_key,
2801 'accept' => 'application/json',
2802 'content-type' => 'application/json'
2803 ),
2804 'body' => wp_json_encode($request_body),
2805 'timeout' => 30
2806 ));
2807
2808 if (is_wp_error($response)) {
2809 //error_log('Pinecone product query error: ' . $response->get_error_message());
2810 return '';
2811 }
2812
2813 $response_code = wp_remote_retrieve_response_code($response);
2814 //error_log('Pinecone response code: ' . $response_code);
2815
2816 if ($response_code !== 200) {
2817 //error_log('Pinecone API error during product search: ' . wp_remote_retrieve_body($response));
2818 return '';
2819 }
2820
2821 $results = json_decode(wp_remote_retrieve_body($response), true);
2822 //error_log('Pinecone raw response: ' . wp_remote_retrieve_body($response));
2823
2824 if (empty($results['matches'])) {
2825 //error_log('No matches found in Pinecone response');
2826 return '';
2827 }
2828
2829 $content = '';
2830 foreach ($results['matches'] as $match) {
2831 if ($match['score'] < $similarity_threshold) {
2832 //error_log("Match below threshold: " . $match['score']);
2833 continue;
2834 }
2835
2836 if (!empty($match['metadata']['text'])) {
2837 $content .= $match['metadata']['text'];
2838 if (!empty($match['metadata']['source_url'])) {
2839 $content .= "\n\nFor more details, check out this product: " . esc_url($match['metadata']['source_url']);
2840 }
2841 $content .= "\n\n";
2842 }
2843 }
2844
2845 return trim($content);
2846 }
2847
2848
2849 private function fetch_content_with_product_links($most_relevant_id) {
2850 global $wpdb;
2851 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2852
2853 // Fetch the article content and associated product URL
2854 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
2855 $result = $wpdb->get_row($query);
2856
2857 if ($result) {
2858 // Append the product link to the content if available
2859 $content = $result->article_content;
2860 if (!empty($result->source_url)) {
2861 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
2862 }
2863 return $content;
2864 }
2865
2866 return null;
2867 }
2868
2869 // Function definition
2870 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
2871 try {
2872 if (!$relevant_content) {
2873 return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
2874 }
2875
2876 // Ensure conversation_history is an array
2877 if (!is_array($conversation_history)) {
2878 $conversation_history = array();
2879 }
2880
2881 // Get selected model with default fallback
2882 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
2883
2884 // Extract model prefix to determine the provider
2885 $model_parts = explode('-', $selected_model);
2886 $provider = strtolower($model_parts[0]);
2887
2888 // Handle model selection based on provider prefix
2889 switch ($provider) {
2890 case 'claude':
2891 if (empty($claude_api_key)) {
2892 throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
2893 }
2894 return $this->mxchat_generate_response_claude(
2895 $selected_model,
2896 $claude_api_key,
2897 $conversation_history,
2898 $relevant_content
2899 );
2900
2901 case 'grok':
2902 if (empty($xai_api_key)) {
2903 throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
2904 }
2905 return $this->mxchat_generate_response_xai(
2906 $selected_model,
2907 $xai_api_key,
2908 $conversation_history,
2909 $relevant_content
2910 );
2911
2912 case 'deepseek':
2913 if (empty($deepseek_api_key)) {
2914 throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
2915 }
2916 return $this->mxchat_generate_response_deepseek(
2917 $selected_model,
2918 $deepseek_api_key,
2919 $conversation_history,
2920 $relevant_content
2921 );
2922
2923 case 'gpt':
2924 if (empty($api_key)) {
2925 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
2926 }
2927 return $this->mxchat_generate_response_openai(
2928 $selected_model,
2929 $api_key,
2930 $conversation_history,
2931 $relevant_content
2932 );
2933
2934 default:
2935 // Default to OpenAI for custom models or unrecognized prefixes
2936 if (empty($api_key)) {
2937 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
2938 }
2939 return $this->mxchat_generate_response_openai(
2940 $selected_model,
2941 $api_key,
2942 $conversation_history,
2943 $relevant_content
2944 );
2945 }
2946 } catch (Exception $e) {
2947 //error_log('MXChat Error: ' . $e->getMessage());
2948 return sprintf(
2949 esc_html__('An error occurred: %s', 'mxchat'),
2950 esc_html($e->getMessage())
2951 );
2952 }
2953 }
2954
2955
2956 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
2957 // Ensure conversation_history is an array
2958 if (!is_array($conversation_history)) {
2959 $conversation_history = array();
2960 }
2961
2962 // Get system prompt instructions from options
2963 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
2964
2965 // Create a new array for the formatted conversation
2966 $formatted_conversation = array();
2967
2968 // Add system message first
2969 $formatted_conversation[] = array(
2970 'role' => 'system',
2971 'content' => $system_prompt_instructions . " " . $relevant_content
2972 );
2973
2974 // Add the rest of the conversation history
2975 foreach ($conversation_history as $message) {
2976 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
2977 $role = $message['role'];
2978
2979 // Convert roles to supported format
2980 if ($role === 'bot' || $role === 'agent') {
2981 $role = 'assistant';
2982 }
2983 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
2984 $role = 'user';
2985 }
2986
2987 $formatted_conversation[] = array(
2988 'role' => $role,
2989 'content' => $message['content']
2990 );
2991 }
2992 }
2993
2994 $body = json_encode([
2995 'model' => $selected_model,
2996 'messages' => $formatted_conversation,
2997 'temperature' => 0.8,
2998 'stream' => false
2999 ]);
3000
3001 $args = [
3002 'body' => $body,
3003 'headers' => [
3004 'Content-Type' => 'application/json',
3005 'Authorization' => 'Bearer ' . $deepseek_api_key,
3006 ],
3007 'timeout' => 60,
3008 'redirection' => 5,
3009 'blocking' => true,
3010 'httpversion' => '1.0',
3011 'sslverify' => true,
3012 ];
3013
3014 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3015
3016 if (is_wp_error($response)) {
3017 //error_log('DeepSeek API Error: ' . $response->get_error_message());
3018 return "Sorry, there was an error processing your request.";
3019 }
3020
3021 $response_body = wp_remote_retrieve_body($response);
3022 $decoded_response = json_decode($response_body, true);
3023
3024 if (isset($decoded_response['choices'][0]['message']['content'])) {
3025 return trim($decoded_response['choices'][0]['message']['content']);
3026 } else {
3027 //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3028 return "Sorry, I couldn't process that request.";
3029 }
3030 }
3031
3032 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3033 // Ensure conversation_history is an array
3034 if (!is_array($conversation_history)) {
3035 $conversation_history = array();
3036 }
3037
3038 // Get system prompt instructions from options
3039 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3040
3041 // Create a new array for the formatted conversation
3042 $formatted_conversation = array();
3043
3044 // Add system message first
3045 $formatted_conversation[] = array(
3046 'role' => 'system',
3047 'content' => $system_prompt_instructions . " " . $relevant_content
3048 );
3049
3050 // Add the rest of the conversation history
3051 foreach ($conversation_history as $message) {
3052 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3053 $role = $message['role'];
3054
3055 // Convert roles to supported format
3056 if ($role === 'bot' || $role === 'agent') {
3057 $role = 'assistant';
3058 }
3059 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3060 $role = 'user';
3061 }
3062
3063 $formatted_conversation[] = array(
3064 'role' => $role,
3065 'content' => $message['content']
3066 );
3067 }
3068 }
3069
3070 $body = json_encode([
3071 'model' => $selected_model,
3072 'messages' => $formatted_conversation,
3073 'temperature' => 0.8,
3074 'stream' => false
3075 ]);
3076
3077 $args = [
3078 'body' => $body,
3079 'headers' => [
3080 'Content-Type' => 'application/json',
3081 'Authorization' => 'Bearer ' . $api_key,
3082 ],
3083 'timeout' => 60,
3084 'redirection' => 5,
3085 'blocking' => true,
3086 'httpversion' => '1.0',
3087 'sslverify' => true,
3088 ];
3089
3090 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3091
3092 if (is_wp_error($response)) {
3093 //error_log('OpenAI API Error: ' . $response->get_error_message());
3094 return "Sorry, there was an error processing your request.";
3095 }
3096
3097 $response_body = wp_remote_retrieve_body($response);
3098 $decoded_response = json_decode($response_body, true);
3099
3100 if (isset($decoded_response['choices'][0]['message']['content'])) {
3101 return trim($decoded_response['choices'][0]['message']['content']);
3102 } else {
3103 //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3104 return "Sorry, I couldn't process that request.";
3105 }
3106 }
3107 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3108 // Get system prompt instructions from options
3109 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3110
3111 // Add system prompt to relevant content
3112 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3113
3114 // Prepend system instructions to the conversation history
3115 array_unshift($conversation_history, [
3116 'role' => 'system',
3117 'content' => "Here are your instructions: " . $content_with_instructions
3118 ]);
3119
3120 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3121 foreach ($conversation_history as &$message) {
3122 if ($message['role'] === 'bot') {
3123 $message['role'] = 'assistant';
3124 } elseif ($message['role'] === 'agent') {
3125 // Tag the message as coming from a live agent
3126 $message['role'] = 'assistant';
3127 if (!isset($message['metadata'])) {
3128 $message['metadata'] = ['source' => 'live_agent'];
3129 }
3130 }
3131
3132 // Ensure all roles are valid
3133 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3134 $message['role'] = 'user'; // Default to 'user'
3135 }
3136 }
3137
3138
3139 // Build the request body
3140 $body = json_encode([
3141 'model' => $selected_model,
3142 'messages' => $conversation_history,
3143 'temperature' => 0.8,
3144 'stream' => false
3145 ]);
3146
3147 // Set up the API request
3148 $args = [
3149 'body' => $body,
3150 'headers' => [
3151 'Content-Type' => 'application/json',
3152 'Authorization' => 'Bearer ' . $xai_api_key,
3153 ],
3154 'timeout' => 60,
3155 'redirection' => 5,
3156 'blocking' => true,
3157 'httpversion' => '1.0',
3158 'sslverify' => true,
3159 ];
3160
3161 // Make the API request
3162 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3163
3164 // Process the response
3165 if (is_wp_error($response)) {
3166 return "Sorry, there was an error processing your request.";
3167 }
3168
3169 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3170
3171 if (isset($response_body['choices'][0]['message']['content'])) {
3172 return trim($response_body['choices'][0]['message']['content']);
3173 } else {
3174 return "Sorry, I couldn't process that request.";
3175 }
3176 }
3177
3178 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3179 // Get system prompt instructions from options
3180 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3181
3182 // Clean and validate conversation history
3183 foreach ($conversation_history as &$message) {
3184 // Convert bot and agent roles to assistant
3185 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
3186 $message['role'] = 'assistant';
3187 }
3188
3189 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
3190 if (!in_array($message['role'], ['assistant', 'user'])) {
3191 $message['role'] = 'user';
3192 }
3193
3194 // Ensure content field exists
3195 if (!isset($message['content']) || empty($message['content'])) {
3196 $message['content'] = '';
3197 }
3198
3199 // Remove any unsupported fields
3200 $message = array_intersect_key($message, array_flip(['role', 'content']));
3201 }
3202
3203 // Add relevant content as the latest user message
3204 $conversation_history[] = [
3205 'role' => 'user',
3206 'content' => $relevant_content
3207 ];
3208
3209 // Build request body
3210 $body = json_encode([
3211 'model' => $selected_model,
3212 'max_tokens' => 1000,
3213 'temperature' => 0.8,
3214 'messages' => $conversation_history,
3215 'system' => $system_prompt_instructions
3216 ]);
3217
3218 // Set up API request
3219 $args = [
3220 'body' => $body,
3221 'headers' => [
3222 'Content-Type' => 'application/json',
3223 'x-api-key' => $claude_api_key,
3224 'anthropic-version' => '2023-06-01'
3225 ],
3226 'timeout' => 60,
3227 'redirection' => 5,
3228 'blocking' => true,
3229 'httpversion' => '1.0',
3230 'sslverify' => true,
3231 ];
3232
3233 // Make API request
3234 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
3235
3236 // Check for WordPress errors
3237 if (is_wp_error($response)) {
3238 //error_log("Claude API request error: " . $response->get_error_message());
3239 return "Sorry, there was an error connecting to the API.";
3240 }
3241
3242 // Check HTTP response code
3243 $http_code = wp_remote_retrieve_response_code($response);
3244 if ($http_code !== 200) {
3245 $error_body = wp_remote_retrieve_body($response);
3246 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
3247
3248 // Try to extract error message from response
3249 $error_data = json_decode($error_body, true);
3250 $error_message = isset($error_data['error']['message']) ?
3251 $error_data['error']['message'] :
3252 "HTTP error " . $http_code;
3253
3254 return "Sorry, the API returned an error: " . $error_message;
3255 }
3256
3257 // Parse response
3258 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3259
3260 // Check for JSON decode errors
3261 if (json_last_error() !== JSON_ERROR_NONE) {
3262 //error_log("Claude API JSON decode error: " . json_last_error_msg());
3263 return "Sorry, there was an error processing the API response.";
3264 }
3265
3266 // Extract and validate response content
3267 if (isset($response_body['content']) &&
3268 is_array($response_body['content']) &&
3269 !empty($response_body['content']) &&
3270 isset($response_body['content'][0]['text'])) {
3271 return trim($response_body['content'][0]['text']);
3272 }
3273
3274 // Log unexpected response format
3275 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3276 return "Sorry, I received an unexpected response format from the API.";
3277 }
3278 public function mxchat_dismiss_pre_chat_message() {
3279 // Get and sanitize the user identifier
3280 $user_id = $this->mxchat_get_user_identifier();
3281 $user_id = sanitize_key($user_id);
3282
3283 // Set a transient to track that the user has dismissed the pre-chat message
3284 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3285 set_transient($transient_key, true, DAY_IN_SECONDS);
3286
3287 wp_send_json_success();
3288 }
3289
3290 public function mxchat_check_pre_chat_message_status() {
3291 // Get and sanitize the user identifier
3292 $user_id = $this->mxchat_get_user_identifier();
3293 $user_id = sanitize_key($user_id);
3294
3295 // Check if the transient exists (i.e., if the message was dismissed)
3296 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3297 $dismissed = get_transient($transient_key);
3298
3299 // Log the result to see if it's being set correctly
3300 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
3301
3302 if ($dismissed) {
3303 wp_send_json_success(['dismissed' => true]);
3304 } else {
3305 wp_send_json_success(['dismissed' => false]);
3306 }
3307
3308 wp_die();
3309 }
3310
3311 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
3312 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
3313 return 0;
3314 }
3315
3316 $dotProduct = array_sum(array_map(function ($a, $b) {
3317 return $a * $b;
3318 }, $vectorA, $vectorB));
3319 $normA = sqrt(array_sum(array_map(function ($a) {
3320 return $a * $a;
3321 }, $vectorA)));
3322 $normB = sqrt(array_sum(array_map(function ($b) {
3323 return $b * $b;
3324 }, $vectorB)));
3325
3326 if ($normA == 0 || $normB == 0) {
3327 return 0;
3328 }
3329
3330 return $dotProduct / ($normA * $normB);
3331 }
3332
3333 public function mxchat_enqueue_scripts_styles() {
3334 // Define version numbers for the styles and scripts
3335 $chat_style_version = '2.0.5'; // Replace with your actual version
3336 $chat_script_version = '2.0.5'; // Replace with your actual version
3337
3338 // Enqueue the script
3339 wp_enqueue_script(
3340 'mxchat-chat-js',
3341 plugin_dir_url(__FILE__) . '../js/chat-script.js',
3342 array('jquery'),
3343 $chat_script_version,
3344 true
3345 );
3346
3347 // Enqueue the CSS
3348 wp_enqueue_style(
3349 'mxchat-chat-css',
3350 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3351 array(),
3352 $chat_style_version
3353 );
3354
3355 // Fetch options from the database
3356 $this->options = get_option('mxchat_options');
3357 $prompts_options = get_option('mxchat_prompts_options', array());
3358
3359 // Prepare settings for JavaScript
3360 $style_settings = array(
3361 'ajax_url' => admin_url('admin-ajax.php'),
3362 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
3363 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3364 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3365 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3366 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
3367 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
3368 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
3369 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
3370 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
3371 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
3372 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
3373 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
3374 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3375 'icon_color' => $this->options['icon_color'] ?? '#fff',
3376 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3377 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3378 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3379
3380 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3381 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3382 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3383 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3384 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3385 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3386
3387 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
3388 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
3389 );
3390
3391 // Pass the settings to the script
3392 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3393 }
3394
3395
3396 public function mxchat_reset_rate_limits() {
3397 global $wpdb;
3398
3399 // Define a cache key pattern for rate limits
3400 $cache_key_pattern = 'mxchat_chat_limit_%';
3401
3402 // Retrieve all option names matching the pattern
3403 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3404 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3405
3406 // db call ok; no-cache ok
3407 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3408 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3409
3410 // Clear the relevant cache entries
3411 foreach ($option_names as $option_name) {
3412 wp_cache_delete($option_name, 'options');
3413 }
3414
3415 // Optionally, clear a general cache if you have one
3416 wp_cache_delete('mxchat_all_chat_limits', 'options');
3417 }
3418
3419 private function mxchat_fetch_woocommerce_products() {
3420 // Ensure WooCommerce is active
3421 if (!class_exists('WooCommerce')) {
3422 return [];
3423 }
3424
3425 $args = array(
3426 'post_type' => 'product',
3427 'post_status' => 'publish',
3428 'posts_per_page' => -1,
3429 );
3430
3431 $products = get_posts($args);
3432 $product_data = [];
3433
3434 foreach ($products as $product) {
3435 $product_id = $product->ID;
3436 $product_obj = wc_get_product($product_id);
3437
3438 $product_data[] = array(
3439 'id' => $product_id,
3440 'name' => $product_obj->get_name(),
3441 'description' => $product_obj->get_description(),
3442 'short_description' => $product_obj->get_short_description(),
3443 'url' => get_permalink($product_id),
3444 'price' => $product_obj->get_regular_price(),
3445 'sale_price' => $product_obj->get_sale_price(),
3446 'stock_status' => $product_obj->get_stock_status(),
3447 'sku' => $product_obj->get_sku(),
3448 'in_stock' => $product_obj->is_in_stock(),
3449 'total_sales' => $product_obj->get_total_sales(),
3450 );
3451 }
3452
3453 return $product_data;
3454 }
3455
3456 }
3457 ?>
3458