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

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