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

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