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

3,558 lines 132.7 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 // Get the similarity threshold from the main options array only
2596 $main_options = get_option('mxchat_options', []);
2597 $similarity_threshold = isset($main_options['similarity_threshold'])
2598 ? ((int) $main_options['similarity_threshold']) / 100
2599 : 0.8; // Default to 80%
2600
2601 //error_log('[MXCHAT] Using similarity threshold: ' . ($similarity_threshold * 100) . '%');
2602
2603 // Iterate through embeddings to calculate similarity
2604 foreach ($embeddings as $embedding) {
2605 $database_embedding = $embedding->embedding_vector
2606 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2607 : null;
2608 if (is_array($database_embedding) && is_array($user_embedding)) {
2609 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2610
2611 // Log each similarity score over 0.5 to reduce log spam
2612 if ($similarity > 0.1) {
2613 //error_log(sprintf('[MXCHAT] ID: %d | Similarity Score: %.4f', $embedding->id, $similarity));
2614 }
2615
2616 $relevant_results[] = [
2617 'id' => $embedding->id,
2618 'similarity' => $similarity
2619 ];
2620 }
2621 // Free memory
2622 unset($database_embedding);
2623 }
2624
2625 // Filter and sort relevant results by similarity
2626 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2627 return $result['similarity'] >= $similarity_threshold;
2628 });
2629 usort($relevant_results, function ($a, $b) {
2630 return $b['similarity'] <=> $a['similarity'];
2631 });
2632
2633 // Log number of results that met threshold
2634 //error_log('[MXCHAT] ' . count($relevant_results) . ' results met the similarity threshold');
2635
2636 // Limit to the top 5 results
2637 $top_results = array_slice($relevant_results, 0, 5);
2638
2639 // Log the top matches
2640 //error_log('[MXCHAT] Top matching results:');
2641 foreach ($top_results as $index => $result) {
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 private function find_relevant_content_pinecone($user_embedding) {
2687 $options = get_option('mxchat_pinecone_addon_options', array());
2688 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2689 $host = $options['mxchat_pinecone_host'] ?? '';
2690
2691 if (empty($host) || empty($api_key)) {
2692 //error_log('[MXCHAT Debug] Pinecone credentials not properly configured');
2693 return '';
2694 }
2695
2696 // Get the similarity threshold from the main options array only
2697 $main_options = get_option('mxchat_options', []);
2698 $similarity_threshold = isset($main_options['similarity_threshold'])
2699 ? ((int) $main_options['similarity_threshold']) / 100
2700 : 0.8; // Default to 80%
2701
2702 //error_log('[MXCHAT Debug] Using similarity threshold: ' . ($similarity_threshold * 100) . '%');
2703
2704 // Prepare the query request for Pinecone
2705 $api_endpoint = "https://{$host}/query";
2706
2707 //error_log('[MXCHAT Debug] Querying Pinecone at: ' . $api_endpoint);
2708
2709 $request_body = array(
2710 'vector' => $user_embedding,
2711 'topK' => 5,
2712 'includeMetadata' => true,
2713 'includeValues' => true
2714 );
2715
2716 $response = wp_remote_post($api_endpoint, array(
2717 'headers' => array(
2718 'Api-Key' => $api_key,
2719 'accept' => 'application/json',
2720 'content-type' => 'application/json'
2721 ),
2722 'body' => wp_json_encode($request_body),
2723 'timeout' => 30
2724 ));
2725
2726 if (is_wp_error($response)) {
2727 //error_log('[MXCHAT Debug] Pinecone query error: ' . $response->get_error_message());
2728 return '';
2729 }
2730
2731 $response_code = wp_remote_retrieve_response_code($response);
2732 if ($response_code !== 200) {
2733 //error_log('[MXCHAT Debug] Pinecone API error: ' . wp_remote_retrieve_body($response));
2734 return '';
2735 }
2736
2737 $results = json_decode(wp_remote_retrieve_body($response), true);
2738 if (empty($results['matches'])) {
2739 //error_log('[MXCHAT Debug] No matches found in Pinecone response');
2740 return '';
2741 }
2742
2743 //error_log('[MXCHAT Debug] Found ' . count($results['matches']) . ' matches in Pinecone');
2744
2745 // Initialize the final content
2746 $content = '';
2747 $matches_above_threshold = 0;
2748
2749 // Process each match
2750 foreach ($results['matches'] as $index => $match) {
2751 // Log score for each match
2752
2753 // Skip if similarity is below threshold
2754 if ($match['score'] < $similarity_threshold) {
2755 continue;
2756 }
2757
2758 $matches_above_threshold++;
2759
2760 if (!empty($match['metadata']['text']) && !empty($match['metadata']['source_url'])) {
2761 // Add content with citation
2762 $content .= $match['metadata']['text'] . "\n";
2763 $content .= "Source: " . $match['metadata']['source_url'] . "\n\n";
2764 }
2765 }
2766
2767 //error_log('[MXCHAT Debug] Total matches used (above threshold): ' . $matches_above_threshold);
2768 //error_log('[MXCHAT Debug] Content length returned: ' . strlen(trim($content)) . ' characters');
2769
2770 return trim($content);
2771 }
2772
2773 private function mxchat_find_relevant_products($user_embedding) {
2774 //error_log('MXChat Vector Search: Starting product search...');
2775
2776 // Retrieve the add-on settings from the database
2777 $addon_options = get_option('mxchat_pinecone_addon_options', array());
2778
2779 // Determine whether Pinecone is enabled
2780 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
2781
2782 //error_log('Pinecone enabled flag: ' . $use_pinecone);
2783
2784 if ($use_pinecone === 1) {
2785 //error_log('MXChat Vector Search: Using Pinecone database for products');
2786 return $this->find_relevant_products_pinecone($user_embedding);
2787 } else {
2788 //error_log('MXChat Vector Search: Using WordPress database for products');
2789 return $this->find_relevant_products_wordpress($user_embedding);
2790 }
2791 }
2792
2793 private function find_relevant_products_wordpress($user_embedding) {
2794 global $wpdb;
2795 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2796 $cache_key = 'mxchat_system_prompt_embeddings';
2797 $batch_size = 500;
2798
2799 // Original WordPress database search logic
2800 // [Previous implementation remains the same]
2801 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
2802 if ($embeddings === false) {
2803 $embeddings = [];
2804 $offset = 0;
2805
2806 do {
2807 $query = $wpdb->prepare(
2808 "SELECT id, embedding_vector
2809 FROM {$system_prompt_table}
2810 LIMIT %d OFFSET %d",
2811 $batch_size,
2812 $offset
2813 );
2814
2815 $batch = $wpdb->get_results($query);
2816 if (empty($batch)) {
2817 break;
2818 }
2819
2820 $embeddings = array_merge($embeddings, $batch);
2821 $offset += $batch_size;
2822
2823 unset($batch);
2824
2825 } while (true);
2826
2827 if (empty($embeddings)) {
2828 return '';
2829 }
2830 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
2831 }
2832
2833 $relevant_results = [];
2834 foreach ($embeddings as $embedding) {
2835 $database_embedding = $embedding->embedding_vector
2836 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
2837 : null;
2838 if (is_array($database_embedding) && is_array($user_embedding)) {
2839 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
2840 $relevant_results[] = [
2841 'id' => $embedding->id,
2842 'similarity' => $similarity
2843 ];
2844 }
2845 unset($database_embedding);
2846 }
2847
2848 // Use fixed threshold for products
2849 $similarity_threshold = 0.85;
2850
2851 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
2852 return $result['similarity'] >= $similarity_threshold;
2853 });
2854 usort($relevant_results, function ($a, $b) {
2855 return $b['similarity'] <=> $a['similarity'];
2856 });
2857
2858 $top_results = array_slice($relevant_results, 0, 5);
2859 $content = '';
2860
2861 foreach ($top_results as $result) {
2862 $chunk_content = $this->fetch_content_with_product_links($result['id']);
2863 $content .= $chunk_content . "\n\n";
2864 }
2865
2866 return trim($content);
2867 }
2868
2869 // Modified search function with correct filter syntax
2870 private function find_relevant_products_pinecone($user_embedding) {
2871 //error_log('Starting Pinecone product search...');
2872
2873 $options = get_option('mxchat_pinecone_addon_options', array());
2874 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
2875 $host = $options['mxchat_pinecone_host'] ?? '';
2876
2877 if (empty($host) || empty($api_key)) {
2878 //error_log('Pinecone credentials not properly configured for product search');
2879 return '';
2880 }
2881
2882 $similarity_threshold = 0.85;
2883 $api_endpoint = "https://{$host}/query";
2884
2885 $request_body = array(
2886 'vector' => $user_embedding,
2887 'topK' => 5,
2888 'includeMetadata' => true,
2889 'includeValues' => true,
2890 'filter' => array(
2891 'type' => 'product'
2892 )
2893 );
2894
2895 //error_log('Sending request to Pinecone with body: ' . wp_json_encode($request_body));
2896
2897 $response = wp_remote_post($api_endpoint, array(
2898 'headers' => array(
2899 'Api-Key' => $api_key,
2900 'accept' => 'application/json',
2901 'content-type' => 'application/json'
2902 ),
2903 'body' => wp_json_encode($request_body),
2904 'timeout' => 30
2905 ));
2906
2907 if (is_wp_error($response)) {
2908 //error_log('Pinecone product query error: ' . $response->get_error_message());
2909 return '';
2910 }
2911
2912 $response_code = wp_remote_retrieve_response_code($response);
2913 //error_log('Pinecone response code: ' . $response_code);
2914
2915 if ($response_code !== 200) {
2916 //error_log('Pinecone API error during product search: ' . wp_remote_retrieve_body($response));
2917 return '';
2918 }
2919
2920 $results = json_decode(wp_remote_retrieve_body($response), true);
2921 //error_log('Pinecone raw response: ' . wp_remote_retrieve_body($response));
2922
2923 if (empty($results['matches'])) {
2924 //error_log('No matches found in Pinecone response');
2925 return '';
2926 }
2927
2928 $content = '';
2929 foreach ($results['matches'] as $match) {
2930 if ($match['score'] < $similarity_threshold) {
2931 //error_log("Match below threshold: " . $match['score']);
2932 continue;
2933 }
2934
2935 if (!empty($match['metadata']['text'])) {
2936 $content .= $match['metadata']['text'];
2937 if (!empty($match['metadata']['source_url'])) {
2938 $content .= "\n\nFor more details, check out this product: " . esc_url($match['metadata']['source_url']);
2939 }
2940 $content .= "\n\n";
2941 }
2942 }
2943
2944 return trim($content);
2945 }
2946
2947
2948 private function fetch_content_with_product_links($most_relevant_id) {
2949 global $wpdb;
2950 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
2951
2952 // Fetch the article content and associated product URL
2953 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
2954 $result = $wpdb->get_row($query);
2955
2956 if ($result) {
2957 // Append the product link to the content if available
2958 $content = $result->article_content;
2959 if (!empty($result->source_url)) {
2960 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
2961 }
2962 return $content;
2963 }
2964
2965 return null;
2966 }
2967
2968 // Function definition
2969 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $conversation_history) {
2970 try {
2971 if (!$relevant_content) {
2972 return esc_html__("I'm sorry, I couldn't find relevant information on that topic.", 'mxchat');
2973 }
2974
2975 // Ensure conversation_history is an array
2976 if (!is_array($conversation_history)) {
2977 $conversation_history = array();
2978 }
2979
2980 // Get selected model with default fallback
2981 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
2982
2983 // Extract model prefix to determine the provider
2984 $model_parts = explode('-', $selected_model);
2985 $provider = strtolower($model_parts[0]);
2986
2987 // Handle model selection based on provider prefix
2988 switch ($provider) {
2989 case 'claude':
2990 if (empty($claude_api_key)) {
2991 throw new Exception(esc_html__('Claude API key is not configured', 'mxchat'));
2992 }
2993 return $this->mxchat_generate_response_claude(
2994 $selected_model,
2995 $claude_api_key,
2996 $conversation_history,
2997 $relevant_content
2998 );
2999
3000 case 'grok':
3001 if (empty($xai_api_key)) {
3002 throw new Exception(esc_html__('X.AI API key is not configured', 'mxchat'));
3003 }
3004 return $this->mxchat_generate_response_xai(
3005 $selected_model,
3006 $xai_api_key,
3007 $conversation_history,
3008 $relevant_content
3009 );
3010
3011 case 'deepseek':
3012 if (empty($deepseek_api_key)) {
3013 throw new Exception(esc_html__('DeepSeek API key is not configured', 'mxchat'));
3014 }
3015 return $this->mxchat_generate_response_deepseek(
3016 $selected_model,
3017 $deepseek_api_key,
3018 $conversation_history,
3019 $relevant_content
3020 );
3021
3022 case 'gpt':
3023 if (empty($api_key)) {
3024 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3025 }
3026 return $this->mxchat_generate_response_openai(
3027 $selected_model,
3028 $api_key,
3029 $conversation_history,
3030 $relevant_content
3031 );
3032
3033 default:
3034 // Default to OpenAI for custom models or unrecognized prefixes
3035 if (empty($api_key)) {
3036 throw new Exception(esc_html__('OpenAI API key is not configured', 'mxchat'));
3037 }
3038 return $this->mxchat_generate_response_openai(
3039 $selected_model,
3040 $api_key,
3041 $conversation_history,
3042 $relevant_content
3043 );
3044 }
3045 } catch (Exception $e) {
3046 //error_log('MXChat Error: ' . $e->getMessage());
3047 return sprintf(
3048 esc_html__('An error occurred: %s', 'mxchat'),
3049 esc_html($e->getMessage())
3050 );
3051 }
3052 }
3053
3054
3055 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
3056 // Ensure conversation_history is an array
3057 if (!is_array($conversation_history)) {
3058 $conversation_history = array();
3059 }
3060
3061 // Get system prompt instructions from options
3062 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3063
3064 // Create a new array for the formatted conversation
3065 $formatted_conversation = array();
3066
3067 // Add system message first
3068 $formatted_conversation[] = array(
3069 'role' => 'system',
3070 'content' => $system_prompt_instructions . " " . $relevant_content
3071 );
3072
3073 // Add the rest of the conversation history
3074 foreach ($conversation_history as $message) {
3075 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3076 $role = $message['role'];
3077
3078 // Convert roles to supported format
3079 if ($role === 'bot' || $role === 'agent') {
3080 $role = 'assistant';
3081 }
3082 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3083 $role = 'user';
3084 }
3085
3086 $formatted_conversation[] = array(
3087 'role' => $role,
3088 'content' => $message['content']
3089 );
3090 }
3091 }
3092
3093 $body = json_encode([
3094 'model' => $selected_model,
3095 'messages' => $formatted_conversation,
3096 'temperature' => 0.8,
3097 'stream' => false
3098 ]);
3099
3100 $args = [
3101 'body' => $body,
3102 'headers' => [
3103 'Content-Type' => 'application/json',
3104 'Authorization' => 'Bearer ' . $deepseek_api_key,
3105 ],
3106 'timeout' => 60,
3107 'redirection' => 5,
3108 'blocking' => true,
3109 'httpversion' => '1.0',
3110 'sslverify' => true,
3111 ];
3112
3113 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
3114
3115 if (is_wp_error($response)) {
3116 //error_log('DeepSeek API Error: ' . $response->get_error_message());
3117 return "Sorry, there was an error processing your request.";
3118 }
3119
3120 $response_body = wp_remote_retrieve_body($response);
3121 $decoded_response = json_decode($response_body, true);
3122
3123 if (isset($decoded_response['choices'][0]['message']['content'])) {
3124 return trim($decoded_response['choices'][0]['message']['content']);
3125 } else {
3126 //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
3127 return "Sorry, I couldn't process that request.";
3128 }
3129 }
3130 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
3131 // Ensure conversation_history is an array
3132 if (!is_array($conversation_history)) {
3133 $conversation_history = array();
3134 }
3135
3136 // Get system prompt instructions from options
3137 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3138
3139 // Create a new array for the formatted conversation
3140 $formatted_conversation = array();
3141
3142 // Add system message first
3143 $formatted_conversation[] = array(
3144 'role' => 'system',
3145 'content' => $system_prompt_instructions . " " . $relevant_content
3146 );
3147
3148 // Add the rest of the conversation history
3149 foreach ($conversation_history as $message) {
3150 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
3151 $role = $message['role'];
3152
3153 // Convert roles to supported format
3154 if ($role === 'bot' || $role === 'agent') {
3155 $role = 'assistant';
3156 }
3157 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
3158 $role = 'user';
3159 }
3160
3161 $formatted_conversation[] = array(
3162 'role' => $role,
3163 'content' => $message['content']
3164 );
3165 }
3166 }
3167
3168 $body = json_encode([
3169 'model' => $selected_model,
3170 'messages' => $formatted_conversation,
3171 'temperature' => 0.8,
3172 'stream' => false
3173 ]);
3174
3175 $args = [
3176 'body' => $body,
3177 'headers' => [
3178 'Content-Type' => 'application/json',
3179 'Authorization' => 'Bearer ' . $api_key,
3180 ],
3181 'timeout' => 60,
3182 'redirection' => 5,
3183 'blocking' => true,
3184 'httpversion' => '1.0',
3185 'sslverify' => true,
3186 ];
3187
3188 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
3189
3190 if (is_wp_error($response)) {
3191 //error_log('OpenAI API Error: ' . $response->get_error_message());
3192 return "Sorry, there was an error processing your request.";
3193 }
3194
3195 $response_body = wp_remote_retrieve_body($response);
3196 $decoded_response = json_decode($response_body, true);
3197
3198 if (isset($decoded_response['choices'][0]['message']['content'])) {
3199 return trim($decoded_response['choices'][0]['message']['content']);
3200 } else {
3201 //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
3202 return "Sorry, I couldn't process that request.";
3203 }
3204 }
3205 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
3206 // Get system prompt instructions from options
3207 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3208
3209 // Add system prompt to relevant content
3210 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
3211
3212 // Prepend system instructions to the conversation history
3213 array_unshift($conversation_history, [
3214 'role' => 'system',
3215 'content' => "Here are your instructions: " . $content_with_instructions
3216 ]);
3217
3218 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
3219 foreach ($conversation_history as &$message) {
3220 if ($message['role'] === 'bot') {
3221 $message['role'] = 'assistant';
3222 } elseif ($message['role'] === 'agent') {
3223 // Tag the message as coming from a live agent
3224 $message['role'] = 'assistant';
3225 if (!isset($message['metadata'])) {
3226 $message['metadata'] = ['source' => 'live_agent'];
3227 }
3228 }
3229
3230 // Ensure all roles are valid
3231 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
3232 $message['role'] = 'user'; // Default to 'user'
3233 }
3234 }
3235
3236
3237 // Build the request body
3238 $body = json_encode([
3239 'model' => $selected_model,
3240 'messages' => $conversation_history,
3241 'temperature' => 0.8,
3242 'stream' => false
3243 ]);
3244
3245 // Set up the API request
3246 $args = [
3247 'body' => $body,
3248 'headers' => [
3249 'Content-Type' => 'application/json',
3250 'Authorization' => 'Bearer ' . $xai_api_key,
3251 ],
3252 'timeout' => 60,
3253 'redirection' => 5,
3254 'blocking' => true,
3255 'httpversion' => '1.0',
3256 'sslverify' => true,
3257 ];
3258
3259 // Make the API request
3260 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
3261
3262 // Process the response
3263 if (is_wp_error($response)) {
3264 return "Sorry, there was an error processing your request.";
3265 }
3266
3267 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3268
3269 if (isset($response_body['choices'][0]['message']['content'])) {
3270 return trim($response_body['choices'][0]['message']['content']);
3271 } else {
3272 return "Sorry, I couldn't process that request.";
3273 }
3274 }
3275 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
3276 // Get system prompt instructions from options
3277 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3278
3279 // Clean and validate conversation history
3280 foreach ($conversation_history as &$message) {
3281 // Convert bot and agent roles to assistant
3282 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
3283 $message['role'] = 'assistant';
3284 }
3285
3286 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
3287 if (!in_array($message['role'], ['assistant', 'user'])) {
3288 $message['role'] = 'user';
3289 }
3290
3291 // Ensure content field exists
3292 if (!isset($message['content']) || empty($message['content'])) {
3293 $message['content'] = '';
3294 }
3295
3296 // Remove any unsupported fields
3297 $message = array_intersect_key($message, array_flip(['role', 'content']));
3298 }
3299
3300 // Add relevant content as the latest user message
3301 $conversation_history[] = [
3302 'role' => 'user',
3303 'content' => $relevant_content
3304 ];
3305
3306 // Build request body
3307 $body = json_encode([
3308 'model' => $selected_model,
3309 'max_tokens' => 1000,
3310 'temperature' => 0.8,
3311 'messages' => $conversation_history,
3312 'system' => $system_prompt_instructions
3313 ]);
3314
3315 // Set up API request
3316 $args = [
3317 'body' => $body,
3318 'headers' => [
3319 'Content-Type' => 'application/json',
3320 'x-api-key' => $claude_api_key,
3321 'anthropic-version' => '2023-06-01'
3322 ],
3323 'timeout' => 60,
3324 'redirection' => 5,
3325 'blocking' => true,
3326 'httpversion' => '1.0',
3327 'sslverify' => true,
3328 ];
3329
3330 // Make API request
3331 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
3332
3333 // Check for WordPress errors
3334 if (is_wp_error($response)) {
3335 //error_log("Claude API request error: " . $response->get_error_message());
3336 return "Sorry, there was an error connecting to the API.";
3337 }
3338
3339 // Check HTTP response code
3340 $http_code = wp_remote_retrieve_response_code($response);
3341 if ($http_code !== 200) {
3342 $error_body = wp_remote_retrieve_body($response);
3343 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
3344
3345 // Try to extract error message from response
3346 $error_data = json_decode($error_body, true);
3347 $error_message = isset($error_data['error']['message']) ?
3348 $error_data['error']['message'] :
3349 "HTTP error " . $http_code;
3350
3351 return "Sorry, the API returned an error: " . $error_message;
3352 }
3353
3354 // Parse response
3355 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3356
3357 // Check for JSON decode errors
3358 if (json_last_error() !== JSON_ERROR_NONE) {
3359 //error_log("Claude API JSON decode error: " . json_last_error_msg());
3360 return "Sorry, there was an error processing the API response.";
3361 }
3362
3363 // Extract and validate response content
3364 if (isset($response_body['content']) &&
3365 is_array($response_body['content']) &&
3366 !empty($response_body['content']) &&
3367 isset($response_body['content'][0]['text'])) {
3368 return trim($response_body['content'][0]['text']);
3369 }
3370
3371 // Log unexpected response format
3372 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
3373 return "Sorry, I received an unexpected response format from the API.";
3374 }
3375
3376
3377
3378 public function mxchat_dismiss_pre_chat_message() {
3379 // Get and sanitize the user identifier
3380 $user_id = $this->mxchat_get_user_identifier();
3381 $user_id = sanitize_key($user_id);
3382
3383 // Set a transient to track that the user has dismissed the pre-chat message
3384 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3385 set_transient($transient_key, true, DAY_IN_SECONDS);
3386
3387 wp_send_json_success();
3388 }
3389
3390 public function mxchat_check_pre_chat_message_status() {
3391 // Get and sanitize the user identifier
3392 $user_id = $this->mxchat_get_user_identifier();
3393 $user_id = sanitize_key($user_id);
3394
3395 // Check if the transient exists (i.e., if the message was dismissed)
3396 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
3397 $dismissed = get_transient($transient_key);
3398
3399 // Log the result to see if it's being set correctly
3400 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
3401
3402 if ($dismissed) {
3403 wp_send_json_success(['dismissed' => true]);
3404 } else {
3405 wp_send_json_success(['dismissed' => false]);
3406 }
3407
3408 wp_die();
3409 }
3410
3411 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
3412 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
3413 return 0;
3414 }
3415
3416 $dotProduct = array_sum(array_map(function ($a, $b) {
3417 return $a * $b;
3418 }, $vectorA, $vectorB));
3419 $normA = sqrt(array_sum(array_map(function ($a) {
3420 return $a * $a;
3421 }, $vectorA)));
3422 $normB = sqrt(array_sum(array_map(function ($b) {
3423 return $b * $b;
3424 }, $vectorB)));
3425
3426 if ($normA == 0 || $normB == 0) {
3427 return 0;
3428 }
3429
3430 return $dotProduct / ($normA * $normB);
3431 }
3432
3433 public function mxchat_enqueue_scripts_styles() {
3434 // Define version numbers for the styles and scripts
3435 $chat_style_version = '2.1.0'; // Replace with your actual version
3436 $chat_script_version = '2.1.0'; // Replace with your actual version
3437
3438 // Enqueue the script
3439 wp_enqueue_script(
3440 'mxchat-chat-js',
3441 plugin_dir_url(__FILE__) . '../js/chat-script.js',
3442 array('jquery'),
3443 $chat_script_version,
3444 true
3445 );
3446
3447 // Enqueue the CSS
3448 wp_enqueue_style(
3449 'mxchat-chat-css',
3450 plugin_dir_url(__FILE__) . '../css/chat-style.css',
3451 array(),
3452 $chat_style_version
3453 );
3454
3455 // Fetch options from the database
3456 $this->options = get_option('mxchat_options');
3457 $prompts_options = get_option('mxchat_prompts_options', array());
3458
3459 // Prepare settings for JavaScript
3460 $style_settings = array(
3461 'ajax_url' => admin_url('admin-ajax.php'),
3462 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
3463 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
3464 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
3465 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
3466 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
3467 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
3468 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
3469 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
3470 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
3471 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
3472 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
3473 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
3474 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
3475 'icon_color' => $this->options['icon_color'] ?? '#fff',
3476 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
3477 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
3478 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off', // Example for consistency
3479
3480 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
3481 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
3482 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
3483 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
3484 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
3485 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
3486
3487 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
3488 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
3489 );
3490
3491 // Pass the settings to the script
3492 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
3493 }
3494
3495
3496 public function mxchat_reset_rate_limits() {
3497 global $wpdb;
3498
3499 // Define a cache key pattern for rate limits
3500 $cache_key_pattern = 'mxchat_chat_limit_%';
3501
3502 // Retrieve all option names matching the pattern
3503 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
3504 $option_names = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3505
3506 // db call ok; no-cache ok
3507 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- db call ok
3508 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'mxchat_chat_limit_%'");
3509
3510 // Clear the relevant cache entries
3511 foreach ($option_names as $option_name) {
3512 wp_cache_delete($option_name, 'options');
3513 }
3514
3515 // Optionally, clear a general cache if you have one
3516 wp_cache_delete('mxchat_all_chat_limits', 'options');
3517 }
3518
3519 private function mxchat_fetch_woocommerce_products() {
3520 // Ensure WooCommerce is active
3521 if (!class_exists('WooCommerce')) {
3522 return [];
3523 }
3524
3525 $args = array(
3526 'post_type' => 'product',
3527 'post_status' => 'publish',
3528 'posts_per_page' => -1,
3529 );
3530
3531 $products = get_posts($args);
3532 $product_data = [];
3533
3534 foreach ($products as $product) {
3535 $product_id = $product->ID;
3536 $product_obj = wc_get_product($product_id);
3537
3538 $product_data[] = array(
3539 'id' => $product_id,
3540 'name' => $product_obj->get_name(),
3541 'description' => $product_obj->get_description(),
3542 'short_description' => $product_obj->get_short_description(),
3543 'url' => get_permalink($product_id),
3544 'price' => $product_obj->get_regular_price(),
3545 'sale_price' => $product_obj->get_sale_price(),
3546 'stock_status' => $product_obj->get_stock_status(),
3547 'sku' => $product_obj->get_sku(),
3548 'in_stock' => $product_obj->is_in_stock(),
3549 'total_sales' => $product_obj->get_total_sales(),
3550 );
3551 }
3552
3553 return $product_data;
3554 }
3555
3556 }
3557 ?>
3558