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

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