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

6,140 lines 234.5 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 private $last_similarity_analysis = null;
14
15
16 /**
17 * Class constructor
18 */
19 public function __construct() {
20 $this->options = get_option('mxchat_options');
21 $this->prompts_options = get_option('mxchat_prompts_options', array());
22 $this->chat_count = get_option('mxchat_chat_count', 0);
23 $this->word_handler = new MXChat_Word_Handler($this->options);
24
25 // Add all action hooks
26 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
27 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
28 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31
32 // Add the AJAX actions for checking if the pre-chat message was dismissed
33 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
34 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
37 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
38 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
39
40 // Add REST API routes registration
41 add_action('rest_api_init', array($this, 'register_routes'));
42 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
43 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
44
45 // Rate limit action - notice we removed the old schedule setup
46 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
47
48 // File upload and handling actions
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 // Word document handling actions
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 // Email handling actions
63 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
65 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
66 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
67
68 add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
69 add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
70
71 // Testing panel AJAX actions
72 add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
73 add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
74 add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
75 add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
76
77 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
78
79
80 }
81
82 // In your core plugin's check_actions_for_addons method:
83 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
84 error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
85
86 $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
87
88 error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
89
90 return $result;
91 }
92
93 private function mxchat_increment_chat_count() {
94 $chat_count = get_option('mxchat_chat_count', 0);
95 $chat_count++;
96 update_option('mxchat_chat_count', $chat_count);
97 }
98
99 function mxchat_fetch_conversation_history() {
100 if (empty($_POST['session_id'])) {
101 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
102 wp_die();
103 }
104
105 $session_id = sanitize_text_field($_POST['session_id']);
106 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
107 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
108
109 if (empty($history)) {
110 // Even if history is empty, return the chat mode
111 wp_send_json_success([
112 'conversation' => [],
113 'chat_mode' => $chat_mode
114 ]);
115 wp_die();
116 }
117
118 wp_send_json_success([
119 'conversation' => $history,
120 'chat_mode' => $chat_mode
121 ]);
122 wp_die();
123 }
124
125 private function mxchat_fetch_conversation_history_for_ai($session_id) {
126 $history = get_option("mxchat_history_{$session_id}", []);
127 $formatted_history = [];
128
129 // Adjusted for code-heavy conversations
130 $max_tokens = 120000; // Context window size
131 $reserved_tokens = 5000; // Space for system prompts + current query
132 $current_token_count = 0;
133
134 // Allowed HTML tags for content sanitization
135 $allowed_tags = [
136 'pre' => ['class' => true],
137 'code' => ['class' => true],
138 'span' => ['class' => true],
139 'div' => ['class' => true],
140 'strong' => [],
141 'em' => []
142 ];
143
144 foreach (array_reverse($history) as $entry) {
145 // Preserve code blocks while sanitizing other HTML
146 $clean_content = wp_kses($entry['content'], $allowed_tags);
147
148 // Detect code blocks in content
149 $has_code = false;
150 // Replace the HTML check with:
151 // Allow messages that contain code blocks or are plain text
152 if (strpos($clean_content, '<pre') === false &&
153 strpos($clean_content, '<code') === false &&
154 $clean_content !== strip_tags($entry['content'])) {
155 continue;
156 }
157
158 // Skip entries that lost significant content during sanitization
159 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
160 continue;
161 }
162
163 // More accurate token estimation (1 token ≈ 4 characters)
164 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
165
166 // Check token budget with the new estimate
167 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
168 // Try to fit partial content if it's the first entry
169 if (empty($formatted_history)) {
170 $clean_content = mb_substr($clean_content, 0, ($max_tokens - $reserved_tokens) * 4);
171 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
172 } else {
173 break;
174 }
175 }
176
177 // Add to formatted history
178 $formatted_history[] = [
179 'role' => $entry['role'],
180 'content' => $clean_content
181 ];
182
183 $current_token_count += $token_estimate;
184 }
185
186 // Reverse back to maintain chronological order
187 $formatted_history = array_reverse($formatted_history);
188
189 // Add system message about code context
190 array_unshift($formatted_history, [
191 'role' => 'system',
192 'content' => 'Preserved code blocks are marked with [CODE BLOCK PRESERVED]. '
193 . 'Maintain formatting and syntax highlighting when referencing code.'
194 ]);
195
196 return $formatted_history;
197 }
198
199 public function register_routes() {
200 //error_log(esc_html__('Registering MxChat REST routes', 'mxchat'));
201
202 register_rest_route('mxchat/v1', '/stream', [
203 'methods' => 'GET',
204 'callback' => [$this, 'mxchat_stream_events'],
205 'permission_callback' => [$this, 'verify_chat_session'],
206 ]);
207
208 register_rest_route('mxchat/v1', '/agent-response', [
209 'methods' => 'POST',
210 'callback' => [$this, 'mxchat_handle_agent_response'],
211 'permission_callback' => [$this, 'verify_slack_request'],
212 ]);
213
214 register_rest_route('mxchat/v1', '/slack-interaction', [
215 'methods' => 'POST',
216 'callback' => [$this, 'handle_slack_interaction'],
217 'permission_callback' => [$this, 'verify_slack_request'],
218 ]);
219
220 register_rest_route('mxchat/v1', '/slack-messages', [
221 'methods' => 'POST',
222 'callback' => [$this, 'handle_slack_messages'],
223 'permission_callback' => [$this, 'verify_slack_request'],
224 ]);
225
226 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
227 }
228
229 /**
230 * Verify valid chat session
231 */
232 public function verify_chat_session($request) {
233 $session_id = $request->get_param('session_id');
234 if (empty($session_id)) {
235 //error_log(esc_html__('Empty session ID in chat request', 'mxchat'));
236 return false;
237 }
238
239 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
240 return $chat_mode === 'agent';
241 }
242
243 /**
244 * Verify request is coming from Slack.
245 *
246 * @param WP_REST_Request $request
247 * @return bool True if valid, false otherwise.
248 */
249 public function verify_slack_request($request) {
250 // Get the Slack signing secret from your plugin options
251 $valid_key = $this->options['live_agent_secret_key'] ?? '';
252
253 if (empty($valid_key)) {
254 //error_log(esc_html__('Slack signing secret not configured', 'mxchat'));
255 return false;
256 }
257
258 $timestamp = $request->get_header('X-Slack-Request-Timestamp');
259 $slack_signature = $request->get_header('X-Slack-Signature');
260
261 // Verify timestamp to prevent replay attacks
262 if (abs(time() - intval($timestamp)) > 300) {
263 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
264 return false;
265 }
266
267 // Get raw request body
268 $request_body = file_get_contents('php://input');
269
270 // Create the signature base string
271 $sig_basestring = "v0:{$timestamp}:{$request_body}";
272
273 // Calculate expected signature
274 $my_signature = 'v0=' . hash_hmac('sha256', $sig_basestring, $valid_key);
275
276 // Compare signatures
277 return hash_equals($my_signature, $slack_signature);
278 }
279
280 public function mxchat_stream_events(WP_REST_Request $request) {
281 header('Content-Type: text/event-stream');
282 header('Cache-Control: no-cache');
283 header('Connection: keep-alive');
284
285 $session_id = sanitize_text_field($request->get_param('session_id'));
286 $last_seen_id = sanitize_text_field($request->get_param('last_seen_id')) ?: '';
287
288 if (empty($session_id)) {
289 echo esc_html__("event: error\ndata: ", 'mxchat') . esc_html__('Missing session_id', 'mxchat') . "\n\n";
290 flush();
291 exit;
292 }
293
294 $history = get_option("mxchat_history_{$session_id}", []);
295
296 // Filter only new messages
297 $new_messages = array_filter($history, function ($message) use ($last_seen_id) {
298 return !empty($message['id']) && $message['id'] > $last_seen_id;
299 });
300
301 // Send new messages if available
302 if (!empty($new_messages)) {
303 echo esc_html__("event: newMessages\ndata: ", 'mxchat') . json_encode(array_values($new_messages)) . "\n\n";
304 } else {
305 // Keep the connection alive
306 echo esc_html__("event: keepAlive\ndata: ", 'mxchat') . "{}\n\n";
307 }
308 flush();
309 exit;
310 }
311
312
313
314
315 private function mxchat_save_chat_message($session_id, $role, $message) {
316 global $wpdb;
317 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
318 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
319
320 // Check if this is the first message in a new session (before any other database operations)
321 $is_new_session = false;
322 if ($role === 'user') { // Only check for user messages, not bot responses
323 $existing_messages = $wpdb->get_var($wpdb->prepare(
324 "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
325 $session_id
326 ));
327 $is_new_session = ($existing_messages == 0);
328 }
329
330 // 1) Extract agent name if present
331 $agent_name = '';
332 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
333 $agent_name = $matches[1];
334 $message = str_replace("Agent: $agent_name - ", '', $message);
335 $session_meta_key = "mxchat_agent_name_{$session_id}";
336 if (empty(get_option($session_meta_key))) {
337 update_option($session_meta_key, $agent_name);
338 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
339 }
340 }
341 // 2) Generate unique message_id
342 $message_id = uniqid();
343 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
344 // 3) Determine user_id
345 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
346 // 4) Determine user_identifier
347 $user_identifier = $agent_name
348 ? $agent_name
349 : MxChat_User::mxchat_get_user_identifier();
350 // 5) Determine displayed_name
351 $user_email = MxChat_User::mxchat_get_user_email();
352 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
353 // 6) Check for a saved email in wp_options
354 $email_option_key = "mxchat_email_{$session_id}";
355 $saved_email = get_option($email_option_key);
356 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
357 // If found, update DB user_email
358 if ($saved_email) {
359 $update_res = $wpdb->update(
360 $table_name,
361 ['user_email' => $saved_email],
362 ['session_id' => $session_id],
363 ['%s'],
364 ['%s']
365 );
366 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
367 }
368 // 7) Save to session history in wp_options
369 $history_key = "mxchat_history_{$session_id}";
370 $history = get_option($history_key, []);
371 $history[] = [
372 'id' => $message_id,
373 'role' => $role,
374 'content' => $message,
375 'timestamp' => round(microtime(true) * 1000),
376 'agent_name' => $displayed_name,
377 ];
378 update_option($history_key, $history, 'no');
379 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
380 // 8) Save the message to DB (INSERT)
381 $insert_data = [
382 'user_id' => $user_id,
383 'user_identifier'=> $user_identifier,
384 'user_email' => $saved_email ?: $user_email,
385 'session_id' => $session_id,
386 'role' => $role,
387 'message' => $message,
388 'timestamp' => current_time('mysql', 1),
389 ];
390 $wpdb->insert($table_name, $insert_data);
391 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
392
393 // 9) Send notification email if this is the first user message in a new session
394 if ($wpdb->insert_id && $is_new_session && $role === 'user') {
395 $this->send_new_chat_notification($session_id, array(
396 'identifier' => $user_identifier,
397 'email' => $saved_email ?: $user_email,
398 'ip' => $_SERVER['REMOTE_ADDR']
399 ));
400 }
401
402 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
403 return $message_id;
404 }
405 private function send_new_chat_notification($session_id, $user_info = array()) {
406 $options = get_option('mxchat_transcripts_options');
407
408 // Check if notifications are enabled
409 if (empty($options['mxchat_enable_notifications'])) {
410 return false;
411 }
412
413 // Get notification email
414 $to = !empty($options['mxchat_notification_email']) ?
415 $options['mxchat_notification_email'] :
416 get_option('admin_email');
417
418 if (!is_email($to)) {
419 return false;
420 }
421
422 // Prepare email content
423 $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
424
425 $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
426 $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
427 $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
428
429 $message = sprintf(
430 "A new chat session has started on your website.\n\n" .
431 "Session ID: %s\n" .
432 "User: %s\n" .
433 "Email: %s\n" .
434 "IP Address: %s\n" .
435 "Time: %s\n\n" .
436 "View transcripts: %s",
437 $session_id,
438 $user_identifier,
439 $user_email,
440 $user_ip,
441 current_time('mysql'),
442 admin_url('admin.php?page=mxchat-transcripts')
443 );
444
445 // Send email
446 return wp_mail($to, $subject, $message);
447 }
448
449 public function mxchat_handle_save_email_and_response() {
450 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
451
452 // Validate nonce
453 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
454 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
455 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
456 wp_die();
457 }
458
459 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
460 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
461
462 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
463
464 if (empty($session_id) || empty($email)) {
465 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
466 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
467 wp_die();
468 }
469
470 // 1) Always store in wp_options
471 $option_key = "mxchat_email_{$session_id}";
472 update_option($option_key, $email);
473 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$option_key} => {$email}");
474
475 // 2) (Optional) Also store in DB if a row already exists
476 global $wpdb;
477 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
478
479 // Make sure we have a valid placeholder in prepare
480 $sql = $wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE session_id = %s", $session_id);
481 $session_count = $wpdb->get_var($sql);
482
483 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
484
485 if ($session_count) {
486 // Update user_email if row(s) exist
487 $update_sql = $wpdb->prepare(
488 "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
489 $email,
490 $session_id
491 );
492 $wpdb->query($update_sql);
493 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
494 } else {
495 //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
496 }
497
498 // Provide success response
499 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
500 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
501 wp_send_json_success(['message' => $bot_message]);
502 wp_die();
503 }
504
505 public function mxchat_check_email_provided() {
506 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
507
508 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
509 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
510 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
511 }
512
513 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
514 if (empty($session_id)) {
515 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
516 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
517 }
518
519 // Check if the user is logged in
520 if (is_user_logged_in()) {
521 $current_user = wp_get_current_user();
522 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
523 wp_send_json_success(['logged_in' => true, 'email' => $current_user->user_email]);
524 }
525
526 $option_key = "mxchat_email_{$session_id}";
527 $stored_email = get_option($option_key, '');
528
529 //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
530
531 if (!empty($stored_email)) {
532 //error_log("[DEBUG] mxchat_check_email_provided -> Email found, returning success");
533 wp_send_json_success(['email' => $stored_email]);
534 } else {
535 //error_log("[DEBUG] mxchat_check_email_provided -> No email found, returning error");
536 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
537 }
538 }
539
540 public function mxchat_handle_chat_request() {
541 global $wpdb;
542
543 // NEW: Check if this is a streaming request
544 $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
545
546 // NEW: Set streaming headers if needed
547 if ($is_streaming) {
548 // Disable output buffering
549 while (ob_get_level()) {
550 ob_end_flush(); // Changed from ob_end_clean()
551 }
552
553 // Set headers for SSE
554 header('Content-Type: text/event-stream');
555 header('Cache-Control: no-cache');
556 header('Connection: keep-alive');
557 header('X-Accel-Buffering: no');
558
559 // Add these new lines:
560 ob_implicit_flush(true);
561 flush();
562 }
563
564 // Check if MX Chat Moderation is active
565 if (class_exists('MX_Chat_Moderation')) {
566 // Get user email and IP
567 $user_email = '';
568 $user_ip = $_SERVER['REMOTE_ADDR'];
569
570 // If user is logged in, get their email
571 if (is_user_logged_in()) {
572 $current_user = wp_get_current_user();
573 $user_email = $current_user->user_email;
574 }
575
576 // Create ban handler instance
577 $ban_handler = new MX_Chat_Ban_Handler();
578
579 // Check if user is banned by IP
580 if ($ban_handler->check_ban($user_ip, 'ip')) {
581 wp_send_json([
582 'success' => false,
583 'message' => esc_html__('Access denied. Your IP address has been banned.', 'mxchat'),
584 'status' => 'banned'
585 ]);
586 wp_die();
587 }
588
589 // If user is logged in, also check email
590 if (!empty($user_email) && $ban_handler->check_ban($user_email, 'email')) {
591 wp_send_json([
592 'success' => false,
593 'message' => esc_html__('Access denied. Your email address has been banned.', 'mxchat'),
594 'status' => 'banned'
595 ]);
596 wp_die();
597 }
598 }
599
600 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
601 $this->productCardHtml = '';
602
603 // Get the actual WordPress user ID if logged in
604 $is_logged_in = is_user_logged_in();
605 if ($is_logged_in) {
606 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
607 } else {
608 // For logged-out users, use your existing identifier method
609 $user_id = $this->mxchat_get_user_identifier();
610 }
611
612 // Get and sanitize the user identifier
613 $user_id = sanitize_key($user_id);
614
615 // Check rate limit using new settings structure
616 $rate_limit_result = $this->check_rate_limit();
617
618 if ($rate_limit_result !== true) {
619 wp_send_json([
620 'success' => false,
621 'message' => $rate_limit_result['message'],
622 'status' => 'rate_limit_exceeded'
623 ]);
624 wp_die();
625 }
626
627 // Rest of your existing code...
628 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
629
630 if (empty($session_id)) {
631 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
632 wp_die();
633 }
634
635 // Validate and sanitize the incoming message
636 if (empty($_POST['message'])) {
637 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
638 wp_die();
639 }
640
641 // NEW: Get page context if provided
642 $page_context = null;
643 if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
644 $page_context_raw = stripslashes($_POST['page_context']);
645 $page_context = json_decode($page_context_raw, true);
646
647 // Validate page context structure
648 if (is_array($page_context) &&
649 isset($page_context['url']) &&
650 isset($page_context['title']) &&
651 isset($page_context['content'])) {
652
653 // Sanitize page context
654 $page_context['url'] = esc_url_raw($page_context['url']);
655 $page_context['title'] = sanitize_text_field($page_context['title']);
656 $page_context['content'] = wp_kses_post($page_context['content']);
657 } else {
658 $page_context = null;
659 }
660 }
661
662 // Modify the message sanitization to preserve PHP tags in code blocks
663 $allowed_tags = [
664 'pre' => [],
665 'code' => ['class' => true],
666 'span' => ['class' => true],
667 'div' => ['class' => true],
668 ];
669
670 // First preserve code blocks
671 $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
672 return htmlspecialchars_decode($matches[0]);
673 }, $_POST['message']);
674
675 // Then apply sanitization
676 $message = wp_kses($message, $allowed_tags);
677
678 // Preserve code blocks from markdown conversion
679 $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
680 $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
681
682 // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
683 // Always initialize testing data for admins (no toggle needed)
684 $testing_data = null;
685 if (current_user_can('administrator')) {
686 // For vision messages, use the original user message for the query display
687 $query_for_testing = $message;
688 if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
689 $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
690 }
691
692 $testing_data = [
693 'query' => $query_for_testing,
694 'timestamp' => time(),
695 'top_matches' => [],
696 'action_matches' => [], // NEW: Initialize action matches array
697 'page_context' => $page_context, // NEW: Include page context in testing data
698 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed']
699 ];
700
701 // Get similarity threshold
702 $similarity_threshold = isset($this->options['similarity_threshold'])
703 ? ((int) $this->options['similarity_threshold']) / 100
704 : 0.75;
705
706 $testing_data['similarity_threshold'] = $similarity_threshold;
707
708 // Determine knowledge base type
709 $addon_options = get_option('mxchat_pinecone_addon_options', array());
710 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
711 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
712 }
713 // ===== END SIMPLIFIED TESTING INITIALIZATION =====
714
715 // Add debug before and after:
716 error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
717 $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
718 error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
719
720
721 // If the pre-processing returned a result (not the original message), use it directly
722 if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
723 // Save the AI response
724 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
725
726 // Save HTML content if provided
727 if (!empty($pre_processed_result['html'])) {
728 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
729 }
730
731 // Add testing data if admin
732 $response_data = [
733 'text' => $pre_processed_result['text'],
734 'html' => $pre_processed_result['html'] ?? '',
735 'session_id' => $session_id
736 ];
737
738 if ($testing_data !== null) {
739 $response_data['testing_data'] = $testing_data;
740 }
741
742 wp_send_json($response_data);
743 wp_die();
744 }
745
746 // Save the user's message - handle vision processed messages differently
747 if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
748 // For vision messages, save the original user message with image indicator
749 $original_message = sanitize_textarea_field($_POST['original_user_message']);
750 if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
751 $image_count = intval($_POST['vision_images_count']);
752 $original_message .= " [{$image_count} image(s)]";
753 }
754 $this->mxchat_save_chat_message($session_id, 'user', $original_message);
755 } else {
756 // Regular message - save as normal
757 $this->mxchat_save_chat_message($session_id, 'user', $message);
758 }
759
760 // Check if the message is an email address
761 if (is_email($message)) {
762 // Add the email to Loops
763 $this->add_email_to_loops($message);
764
765 // Send success response
766 $response_message = $this->options['email_capture_response'] ??
767 esc_html__('Thank you! Your coupon is on the way!', 'mxchat');
768
769 // Clear streaming headers if they were set
770 if ($is_streaming) {
771 header_remove('Content-Type');
772 header_remove('Cache-Control');
773 header_remove('Connection');
774 header_remove('X-Accel-Buffering');
775 header('Content-Type: application/json');
776 }
777
778 $email_response = [
779 'success' => true,
780 'status' => 'email_captured',
781 'message' => $response_message
782 ];
783
784 if ($testing_data !== null) {
785 $email_response['testing_data'] = $testing_data;
786 }
787
788 wp_send_json($email_response);
789 wp_die();
790 }
791
792 $intent_info = '';
793
794 // Check chat mode
795 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
796
797 // Handle agent mode
798 // Handle agent mode
799 if ($chat_mode === 'agent') {
800 // First, check for switch intent before doing anything else
801 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
802
803 // NEW: Capture action analysis for testing panel after intent check
804 if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
805 $testing_data['action_matches'] = $this->last_action_analysis;
806 }
807
808 // If we matched an intent and it's the switch intent, handle it
809 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
810 // Update chat mode first
811 update_option("mxchat_mode_{$session_id}", 'ai');
812
813 // Clear any existing PDF context to start fresh
814 $this->clear_pdf_transients($session_id);
815
816 // Prepare clean switch response
817 $response_data = [
818 'text' => $this->fallbackResponse['text'],
819 'html' => '',
820 'session_id' => $session_id,
821 'chat_mode' => 'ai'
822 ];
823
824 if ($testing_data !== null) {
825 $response_data['testing_data'] = $testing_data;
826 }
827
828 // Save the mode switch message
829 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
830 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
831
832 // Send response and exit
833 wp_send_json($response_data);
834 wp_die();
835 } elseif (!$intent_matched) {
836 // No intent matched, handle live agent message
837 try {
838 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
839
840 $agent_response = [
841 'status' => 'waiting_for_agent',
842 'message' => esc_html__('Message sent to live agent.', 'mxchat')
843 ];
844
845 if ($testing_data !== null) {
846 $agent_response['testing_data'] = $testing_data;
847 }
848
849 wp_send_json_success($agent_response);
850 } catch (\Exception $e) {
851 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
852 }
853 wp_die();
854 }
855 }
856
857 // Step 1: Check for new PDF URL in the message
858 if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
859 $new_pdf_url = $matches[0];
860
861 // Check if this is likely a PDF-related request
862 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
863 $is_pdf_request = false;
864
865 foreach ($pdf_keywords as $keyword) {
866 if (stripos($message, $keyword) !== false) {
867 $is_pdf_request = true;
868 break;
869 }
870 }
871
872 // If it looks like a PDF request or we're waiting for a PDF URL
873 if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
874 // Validate HTTPS
875 if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
876 // Extract filename from URL
877 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
878
879 // Clear previous PDF transients
880 $this->clear_pdf_transients($session_id);
881
882 // Process new PDF
883 $max_pages = $this->options['pdf_max_pages'] ?? 69;
884 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
885
886 if ($embeddings === 'too_many_pages') {
887 $error_text = sprintf(
888 $this->options['pdf_intent_error_text'] ??
889 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
890 $max_pages
891 );
892 $this->fallbackResponse['text'] = $error_text;
893 } elseif ($embeddings) {
894 // Store new PDF information
895 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
896
897 // If the filename is generic, create a more descriptive one
898 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
899 strpos($pdf_filename, '.php') !== false) {
900 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
901 }
902
903 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
904 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
905 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
906 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
907
908 $success_text = $this->options['pdf_intent_success_text'] ??
909 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
910
911 $pdf_response = [
912 'success' => true,
913 'message' => $success_text,
914 'data' => [
915 'filename' => $pdf_filename
916 ]
917 ];
918
919 if ($testing_data !== null) {
920 $pdf_response['testing_data'] = $testing_data;
921 }
922
923 wp_send_json($pdf_response);
924 wp_die();
925 } else {
926 $error_text = $this->options['pdf_intent_error_text'] ??
927 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
928 $this->fallbackResponse['text'] = $error_text;
929 }
930
931 $pdf_error_response = [
932 'success' => false,
933 'message' => $this->fallbackResponse['text']
934 ];
935
936 if ($testing_data !== null) {
937 $pdf_error_response['testing_data'] = $testing_data;
938 }
939
940 wp_send_json($pdf_error_response);
941 wp_die();
942 }
943 }
944 }
945
946 // Check if there's an active recommendation flow session
947 $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
948 if (!empty($flow_state) && isset($flow_state['flow_id'])) {
949 // Create a dummy intent object that matches the original intent
950 $dummy_intent = new stdClass();
951 $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
952 $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
953
954 // Call the recommendation flow handler directly
955 $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
956
957 // If the handler returned a response, send it
958 if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
959 // Save the bot's response to the chat history
960 if (!empty($response_data['text'])) {
961 $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
962 }
963 if (!empty($response_data['html'])) {
964 $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
965 }
966
967 if ($testing_data !== null) {
968 $response_data['testing_data'] = $testing_data;
969 }
970
971 // Send the response
972 wp_send_json($response_data);
973 wp_die();
974 }
975 }
976
977 // Step 2: Detect intent and handle intent-based responses
978 $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
979
980 // NEW: Capture action analysis for testing panel after intent check
981 if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
982 $testing_data['action_matches'] = $this->last_action_analysis;
983 }
984
985 // Step 3: Handle the intent result appropriately
986 if ($intent_result !== false) {
987 // Intent was matched - ALWAYS send as JSON response, never streaming
988
989 if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
990 // Intent returned a direct response array
991 $response_data = [
992 'text' => $intent_result['text'] ?? '',
993 'html' => $intent_result['html'] ?? '',
994 'session_id' => $session_id
995 ];
996
997 if ($testing_data !== null) {
998 $response_data['testing_data'] = $testing_data;
999 }
1000
1001 // Clear streaming headers if they were set
1002 if ($is_streaming) {
1003 header_remove('Content-Type');
1004 header_remove('Cache-Control');
1005 header_remove('Connection');
1006 header_remove('X-Accel-Buffering');
1007 header('Content-Type: application/json');
1008 }
1009
1010 wp_send_json($response_data);
1011 wp_die();
1012 }
1013 else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1014 // Intent returned true and set fallbackResponse
1015 $response_data = [
1016 'text' => $this->fallbackResponse['text'] ?? '',
1017 'html' => $this->fallbackResponse['html'] ?? '',
1018 'session_id' => $session_id
1019 ];
1020
1021 if ($testing_data !== null) {
1022 $response_data['testing_data'] = $testing_data;
1023 }
1024
1025 // Clear streaming headers if they were set
1026 if ($is_streaming) {
1027 header_remove('Content-Type');
1028 header_remove('Cache-Control');
1029 header_remove('Connection');
1030 header_remove('X-Accel-Buffering');
1031 header('Content-Type: application/json');
1032 }
1033
1034 wp_send_json($response_data);
1035 wp_die();
1036 }
1037 }
1038
1039 // If we get here, no intent matched OR the intent didn't provide a usable response
1040
1041 // Step 4: Generate AI response
1042 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1043 $this->mxchat_increment_chat_count();
1044
1045 // Generate embedding for the user's query
1046 $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1047
1048 // Check if the embedding generation returned an error
1049 if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1050 $error_message = $user_message_embedding['error'];
1051 $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1052
1053 wp_send_json_error([
1054 'error_message' => $error_message,
1055 'error_code' => $error_code
1056 ]);
1057 wp_die();
1058 }
1059
1060 // Check if the embedding is valid
1061 if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1062 wp_send_json_error([
1063 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1064 'error_code' => 'invalid_embedding'
1065 ]);
1066 wp_die();
1067 }
1068
1069 // Build context with both knowledge base and PDF content if available
1070 $context_content = "User asked: '{$message}'\n\n";
1071
1072 // NEW: Add page context if available and contextual awareness is enabled
1073 if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1074 $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1075 $context_content .= "Page URL: " . $page_context['url'] . "\n";
1076 $context_content .= "Page Title: " . $page_context['title'] . "\n";
1077 $context_content .= "Page Content: " . $page_context['content'] . "\n";
1078 $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1079 }
1080
1081 // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
1082 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1083
1084 // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1085 if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1086 // Update testing data with the REAL similarity analysis
1087 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1088 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1089 $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1090 }
1091 // ===== END SIMILARITY DATA CAPTURE =====
1092
1093 if (!empty($relevant_content)) {
1094 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1095 } else {
1096 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1097 }
1098
1099 // Check for and include PDF content
1100 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1101 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1102 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1103 if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1104 $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1105 if (!empty($relevant_pdf_pages)) {
1106 $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1107 foreach ($relevant_pdf_pages as $page_data) {
1108 $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1109 }
1110 $context_content .= "\n";
1111 }
1112 }
1113
1114 // Check for and include Word content
1115 $word_url = get_transient('mxchat_word_url_' . $session_id);
1116 $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1117 $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1118 if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1119 $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1120 if (!empty($relevant_word_chunks)) {
1121 $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1122 foreach ($relevant_word_chunks as $chunk_data) {
1123 $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1124 }
1125 $context_content .= "\n";
1126 }
1127 }
1128
1129 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1130
1131 // Generate response
1132 $response = $this->mxchat_generate_response(
1133 $context_content,
1134 $this->options['api_key'],
1135 $this->options['xai_api_key'],
1136 $this->options['claude_api_key'],
1137 $this->options['deepseek_api_key'],
1138 $this->options['gemini_api_key'],
1139 $conversation_history,
1140 $is_streaming,
1141 $session_id,
1142 $testing_data
1143 );
1144
1145 // Handle streaming vs non-streaming responses
1146 if ($is_streaming) {
1147 // Check if streaming actually happened or if it fell back to regular response
1148 if ($response === true) {
1149 wp_die();
1150 }
1151 // If we get here, streaming fell back to regular response, continue
1152 }
1153
1154 // Check if the response is an error array
1155 if (is_array($response) && isset($response['error'])) {
1156 wp_send_json_error([
1157 'error_message' => $response['error'],
1158 'error_code' => $response['error_code'] ?? 'api_error'
1159 ]);
1160 wp_die();
1161 }
1162
1163 // If we get here, the response is valid text
1164 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1165
1166 // Step 5: Save additional content if available
1167 if (!empty($this->productCardHtml)) {
1168 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1169 }
1170
1171 if (!empty($this->fallbackResponse['html'])) {
1172 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1173 }
1174
1175 // Step 6: Return the response
1176 $response_data = [
1177 'text' => $response,
1178 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1179 'session_id' => $session_id
1180 ];
1181
1182 // Always add testing data for admins (no toggle needed)
1183 if ($testing_data !== null) {
1184 $response_data['testing_data'] = $testing_data;
1185 }
1186
1187 wp_send_json($response_data);
1188 wp_die();
1189 }
1190
1191 // Updated function to check intents and invoke the callback function
1192 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1193 global $wpdb;
1194 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1195
1196 // Generate the user embedding
1197 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1198
1199 // Check if embedding generation returned an error
1200 if (is_array($user_embedding) && isset($user_embedding['error'])) {
1201 $error_message = $user_embedding['error'];
1202 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1203
1204 wp_send_json_error([
1205 'error_message' => $error_message,
1206 'error_code' => $error_code
1207 ]);
1208 wp_die();
1209 }
1210
1211 // Check if embedding is valid
1212 if (!is_array($user_embedding) || empty($user_embedding)) {
1213 wp_send_json_error([
1214 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1215 'error_code' => 'invalid_embedding'
1216 ]);
1217 wp_die();
1218 }
1219
1220 // Fetch intents from the database
1221 $table_name = $wpdb->prefix . 'mxchat_intents';
1222 if ($chat_mode === 'agent') {
1223 $query = $wpdb->prepare(
1224 "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
1225 'mxchat_handle_switch_to_chatbot_intent'
1226 );
1227 $intents = $wpdb->get_results($query);
1228 } else {
1229 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
1230 }
1231
1232 if (empty($intents)) {
1233 return false;
1234 }
1235
1236 $highest_similarity = -INF;
1237 $matched_intent = null;
1238
1239 // NEW: Array to store action analysis for testing panel
1240 $action_analysis = [];
1241
1242 foreach ($intents as $intent) {
1243 // Additional check for enabled state
1244 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1245 if (!$is_enabled) {
1246 continue;
1247 }
1248
1249 $intent_embedding_serialized = $intent->embedding_vector;
1250 $intent_embedding = $intent_embedding_serialized
1251 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1252 : null;
1253
1254 if (!is_array($intent_embedding)) {
1255 continue;
1256 }
1257
1258 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1259 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1260
1261 // NEW: Store action analysis data for testing panel
1262 $action_analysis[] = [
1263 'intent_label' => $intent->intent_label,
1264 'callback_function' => $intent->callback_function,
1265 'similarity' => round($similarity, 4),
1266 'similarity_percentage' => round($similarity * 100, 2),
1267 'threshold' => $intent_threshold,
1268 'threshold_percentage' => round($intent_threshold * 100, 2),
1269 'above_threshold' => $similarity >= $intent_threshold,
1270 'triggered' => false // Will be updated below if this intent is triggered
1271 ];
1272
1273 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1274 $highest_similarity = $similarity;
1275 $matched_intent = $intent;
1276 }
1277 }
1278
1279 // NEW: Mark the triggered action if any
1280 if ($matched_intent) {
1281 foreach ($action_analysis as &$action) {
1282 if ($action['intent_label'] === $matched_intent->intent_label) {
1283 $action['triggered'] = true;
1284 break;
1285 }
1286 }
1287 }
1288
1289 // NEW: Sort actions by similarity (highest first) and store for testing panel
1290 usort($action_analysis, function($a, $b) {
1291 return $b['similarity'] <=> $a['similarity'];
1292 });
1293
1294 // Store action analysis for testing panel capture
1295 $this->last_action_analysis = $action_analysis;
1296
1297 if ($matched_intent) {
1298 // If the callback is a method on this instance (core callback), call it directly
1299 if (method_exists($this, $matched_intent->callback_function)) {
1300 $callback_result = call_user_func(
1301 [$this, $matched_intent->callback_function],
1302 $message,
1303 $user_id,
1304 $session_id,
1305 $matched_intent,
1306 $user_context ?? null
1307 );
1308 } else {
1309 // Otherwise, use apply_filters for add-on callbacks
1310 $callback_result = apply_filters(
1311 $matched_intent->callback_function,
1312 false, // default return value
1313 $message,
1314 $user_id,
1315 $session_id,
1316 $matched_intent
1317 );
1318 }
1319
1320 if ($callback_result !== false) {
1321 $this->fallbackResponse = $callback_result;
1322 return true;
1323 }
1324 }
1325
1326 return false;
1327 }
1328
1329 // Helper function to clear PDF and Word document related transients
1330 private function clear_pdf_transients($session_id) {
1331 // PDF transients
1332 delete_transient('mxchat_pdf_url_' . $session_id);
1333 delete_transient('mxchat_pdf_embeddings_' . $session_id);
1334 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
1335 delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1336
1337 // Word document transients
1338 delete_transient('mxchat_word_url_' . $session_id);
1339 delete_transient('mxchat_word_filename_' . $session_id);
1340 delete_transient('mxchat_word_embeddings_' . $session_id);
1341 delete_transient('mxchat_include_word_in_context_' . $session_id);
1342 delete_transient('mxchat_waiting_for_word_' . $session_id);
1343 }
1344
1345
1346
1347 //verified good
1348 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1349 // Log the message safely
1350 //error_log("Triggered email capture intent for message: " . sanitize_text_field($message));
1351
1352 // Initiate email capture flow
1353 $response = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Would you like to join our mailing list? Please provide your email below.", 'mxchat'));
1354 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1355 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1356
1357 // FIXED: Return response data instead of sending JSON directly
1358 // This allows the main chat handler to add testing data before sending
1359 return [
1360 'text' => $response,
1361 'html' => '',
1362 'session_id' => $session_id
1363 ];
1364 }
1365
1366 public function mxchat_generate_image($message, $user_id, $session_id) {
1367 //error_log("Starting image generation for message: " . $message);
1368
1369 // Prepare a prompt for DALL-E
1370 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1371
1372 // Use the existing OpenAI API key
1373 $openai_api_key = sanitize_text_field($this->options['api_key']);
1374
1375 // Call DALL-E to generate an image
1376 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1377
1378 // Check if the response contains an image URL
1379 if (isset($image_response['imageUrl'])) {
1380 $image_url = esc_url_raw($image_response['imageUrl']);
1381
1382 // Construct the HTML with a CSS class instead of inline styles
1383 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1384 $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1385
1386 // Save the bot message with both text and HTML
1387 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1388 $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1389
1390 // Set the fallback response for the chat handler
1391 $this->fallbackResponse = [
1392 'text' => $response_text,
1393 'html' => $response_html,
1394 'images' => [$image_url]
1395 ];
1396
1397 // For debugging/verification - Use json_encode to verify what's being set
1398 //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1399
1400 // Return the response directly instead of relying on the property
1401 return $this->fallbackResponse;
1402 } else {
1403 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1404
1405 // Save the error message
1406 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1407
1408 // Set the fallback response for the chat handler
1409 $this->fallbackResponse = [
1410 'text' => $response_text,
1411 'html' => '',
1412 'images' => []
1413 ];
1414
1415 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1416 //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1417
1418 // Return the response directly instead of relying on the property
1419 return $this->fallbackResponse;
1420 }
1421 }
1422 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1423 $api_url = 'https://api.openai.com/v1/images/generations';
1424 $body = json_encode([
1425 'prompt' => sanitize_text_field($prompt),
1426 'n' => 1,
1427 'size' => '1024x1024',
1428 'model' => sanitize_text_field($model),
1429 ]);
1430
1431 $args = [
1432 'body' => $body,
1433 'headers' => [
1434 'Content-Type' => 'application/json',
1435 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
1436 ],
1437 'method' => 'POST',
1438 'timeout' => absint($timeout),
1439 ];
1440
1441 $response = wp_remote_post($api_url, $args);
1442
1443 if (is_wp_error($response)) {
1444 //error_log("DALL-E request failed: " . $response->get_error_message());
1445 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
1446 }
1447
1448 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1449
1450 if (isset($response_body['data'][0]['url'])) {
1451 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
1452 } else {
1453 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
1454 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
1455 }
1456 }
1457
1458 /**
1459 * Handle web search requests.
1460 *
1461 * Sends the refined search query to the Brave Search API and uses the
1462 * results to generate a conversational response with the AI model.
1463 *
1464 * @since 1.0.0
1465 * @param string $message The user's search query.
1466 * @param string $user_id The user identifier.
1467 * @param string $session_id The current session ID.
1468 * @return array Response array containing text with embedded HTML links
1469 */
1470 public function mxchat_handle_search_request($message, $user_id, $session_id) {
1471 // Step 1: Interpret and refine the search query
1472 $refined_search_query = $this->mxchat_interpret_search_query($message);
1473 if (empty($refined_search_query)) {
1474 return array(
1475 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1476 'html' => ''
1477 );
1478 }
1479
1480 // Retrieve and validate API settings
1481 $options = get_option('mxchat_options');
1482 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1483 $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1484
1485 if (empty($api_key)) {
1486 return array(
1487 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1488 'html' => ''
1489 );
1490 }
1491
1492 // Build the API request URL
1493 $api_url = add_query_arg(
1494 array(
1495 'q' => rawurlencode($refined_search_query),
1496 'count' => $results_count,
1497 'text_decorations' => 'true',
1498 'rich_data' => 'true',
1499 ),
1500 'https://api.search.brave.com/res/v1/web/search'
1501 );
1502
1503 // Attempt to retrieve cached results first
1504 $transient_key = 'mxchat_search_' . md5($refined_search_query);
1505 $results = get_transient($transient_key);
1506
1507 if (false === $results) {
1508 // Fetch new results from the Brave Search API
1509 $response = wp_remote_get(
1510 $api_url,
1511 array(
1512 'headers' => array(
1513 'Accept' => 'application/json',
1514 'Accept-Encoding' => 'gzip',
1515 'X-Subscription-Token'=> $api_key,
1516 ),
1517 'timeout' => 10,
1518 )
1519 );
1520
1521 if (is_wp_error($response)) {
1522 return array(
1523 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1524 'html' => ''
1525 );
1526 }
1527
1528 $results = json_decode(wp_remote_retrieve_body($response), true);
1529
1530 if (json_last_error() !== JSON_ERROR_NONE) {
1531 return array(
1532 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1533 'html' => ''
1534 );
1535 }
1536
1537 // Cache results for one hour
1538 set_transient($transient_key, $results, HOUR_IN_SECONDS);
1539 }
1540
1541 // Process results
1542 if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1543 // Create a more straightforward summary with HTML links
1544 $search_results_text = '';
1545
1546 // Add a simple intro
1547 $search_results_text .= sprintf(
1548 esc_html__("Here's what I found about '%s':", 'mxchat'),
1549 esc_html($refined_search_query)
1550 );
1551
1552 // Add the top results with HTML links
1553 foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1554 $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1555 $url = isset($result['url']) ? esc_url($result['url']) : '';
1556 $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1557
1558 // Add a line break after the intro
1559 $search_results_text .= '<br><br>';
1560
1561 // Add title as a link
1562 $search_results_text .= sprintf(
1563 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1564 $url,
1565 $title
1566 );
1567
1568 // Add a condensed description
1569 $search_results_text .= sprintf("%s", $description);
1570 }
1571
1572 // Save to chat history
1573 $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1574
1575 // Return the formatted text with embedded HTML links
1576 return array(
1577 'text' => $search_results_text,
1578 'html' => ''
1579 );
1580 } else {
1581 return array(
1582 'text' => sprintf(
1583 esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1584 esc_html($refined_search_query)
1585 ),
1586 'html' => ''
1587 );
1588 }
1589 }
1590
1591 //very good
1592 /**
1593 * Handle image search requests from the chatbot
1594 *
1595 * @param string $message The user's search query
1596 * @param int $user_id The user's ID
1597 * @param string $session_id The chat session ID
1598 * @return array Response array with text and HTML content
1599 */
1600 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1601 // Step 1: Interpret the search query using the user's selected AI model
1602 $refined_search_query = $this->mxchat_interpret_search_query($message);
1603
1604 // If no query was interpreted, return a fallback message
1605 if (empty($refined_search_query)) {
1606 return array(
1607 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1608 'html' => "",
1609 );
1610 }
1611
1612 // Brave API URL
1613 $api_url = 'https://api.search.brave.com/res/v1/images/search';
1614
1615 // Retrieve Brave API settings
1616 $options = get_option('mxchat_options');
1617 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1618
1619 if (empty($api_key)) {
1620 return array(
1621 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1622 'html' => "",
1623 );
1624 }
1625
1626 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1627 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
1628
1629 // Append query parameters based on settings
1630 $api_url = add_query_arg([
1631 'q' => rawurlencode($refined_search_query),
1632 'count' => $image_count,
1633 'safesearch' => $safe_search,
1634 ], $api_url);
1635
1636 // Implement caching
1637 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1638 $body = get_transient($transient_key);
1639
1640 if (false === $body) {
1641 $args = [
1642 'headers' => [
1643 'Accept' => 'application/json',
1644 'Accept-Encoding' => 'gzip',
1645 'X-Subscription-Token' => $api_key,
1646 ],
1647 'timeout' => 10,
1648 ];
1649
1650 $response = wp_remote_get($api_url, $args);
1651
1652 if (is_wp_error($response)) {
1653 return array(
1654 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
1655 'html' => "",
1656 );
1657 }
1658
1659 $body = json_decode(wp_remote_retrieve_body($response), true);
1660 set_transient($transient_key, $body, HOUR_IN_SECONDS);
1661 }
1662
1663 // Process the API response
1664 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
1665 $html_output = '<div class="mxchat-image-gallery">';
1666
1667 // Get the configured image count (1-6)
1668 $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1669 $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
1670
1671 // Use only the requested number of images
1672 for ($i = 0; $i < $display_count; $i++) {
1673 $image = $body['results'][$i];
1674 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
1675 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
1676 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
1677
1678 if ($image_url && $thumbnail_url) {
1679 $html_output .= '<div class="mxchat-image-item">';
1680 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
1681 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
1682 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
1683 $html_output .= '</a></div>';
1684 }
1685 }
1686
1687 $html_output .= '</div>';
1688
1689 // Create response text
1690 $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
1691
1692 // Save both response text and HTML to chat history
1693 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1694 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
1695
1696 // Return the combined response
1697 return array(
1698 'text' => $response_text,
1699 'html' => $html_output,
1700 );
1701 } else {
1702 $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
1703
1704 // Save the error message to chat history
1705 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1706
1707 return array(
1708 'text' => $response_text,
1709 'html' => "",
1710 );
1711 }
1712 }
1713
1714 /**
1715 * Interpret the search query using the user's selected AI model
1716 *
1717 * @param string $user_query The original query from the user
1718 * @return string The refined search query
1719 */
1720 public function mxchat_interpret_search_query($user_query) {
1721 $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');
1722
1723 // Get options and determine the selected model
1724 $options = $this->options ?? get_option('mxchat_options');
1725 $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1726
1727 // Extract model prefix to determine the provider
1728 $model_parts = explode('-', $selected_model);
1729 $provider = strtolower($model_parts[0]);
1730
1731 // Determine which API key to use based on the provider
1732 switch ($provider) {
1733 case 'gemini':
1734 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
1735 if (empty($api_key)) {
1736 return sanitize_text_field($user_query); // Default to original query if API key missing
1737 }
1738 return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
1739
1740 case 'claude':
1741 $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
1742 if (empty($api_key)) {
1743 return sanitize_text_field($user_query);
1744 }
1745 return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
1746
1747 case 'grok':
1748 $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
1749 if (empty($api_key)) {
1750 return sanitize_text_field($user_query);
1751 }
1752 return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
1753
1754 case 'deepseek':
1755 $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
1756 if (empty($api_key)) {
1757 return sanitize_text_field($user_query);
1758 }
1759 return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
1760
1761 case 'gpt':
1762 default:
1763 // Default to OpenAI for custom models or unrecognized prefixes
1764 $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
1765 if (empty($api_key)) {
1766 return sanitize_text_field($user_query);
1767 }
1768 return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
1769 }
1770 }
1771
1772 /**
1773 * Interpret query using OpenAI models
1774 */
1775 private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
1776 $url = 'https://api.openai.com/v1/chat/completions';
1777 $args = [
1778 'headers' => [
1779 'Authorization' => 'Bearer ' . $api_key,
1780 'Content-Type' => 'application/json',
1781 ],
1782 'body' => wp_json_encode([
1783 'model' => $model,
1784 'messages' => [
1785 ['role' => 'system', 'content' => $system_prompt],
1786 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1787 ],
1788 'temperature' => 0.2,
1789 'max_tokens' => 20,
1790 ]),
1791 'method' => 'POST',
1792 'timeout' => 15,
1793 ];
1794
1795 $response = wp_remote_post($url, $args);
1796 if (is_wp_error($response)) {
1797 return sanitize_text_field($user_query);
1798 }
1799
1800 $body = json_decode(wp_remote_retrieve_body($response), true);
1801 return isset($body['choices'][0]['message']['content'])
1802 ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
1803 : sanitize_text_field($user_query);
1804 }
1805
1806 /**
1807 * Interpret query using Claude models
1808 */
1809 private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
1810 $url = 'https://api.anthropic.com/v1/messages';
1811
1812 $args = [
1813 'headers' => [
1814 'Content-Type' => 'application/json',
1815 'x-api-key' => $api_key,
1816 'anthropic-version' => '2023-06-01',
1817 ],
1818 'body' => wp_json_encode([
1819 'model' => $model,
1820 'system' => $system_prompt,
1821 'messages' => [
1822 ['role' => 'user', 'content' => sanitize_text_field($user_query)]
1823 ],
1824 'max_tokens' => 20,
1825 'temperature' => 0.2,
1826 ]),
1827 'method' => 'POST',
1828 'timeout' => 15,
1829 ];
1830
1831 $response = wp_remote_post($url, $args);
1832 if (is_wp_error($response)) {
1833 return sanitize_text_field($user_query);
1834 }
1835
1836 $body = json_decode(wp_remote_retrieve_body($response), true);
1837 if (!empty($body['content'][0]['text'])) {
1838 return sanitize_text_field(trim($body['content'][0]['text']));
1839 }
1840
1841 return sanitize_text_field($user_query);
1842 }
1843
1844 /**
1845 * Interpret query using Gemini models
1846 */
1847 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
1848 // Strip "gemini-" prefix for the API
1849 $model_version = str_replace('gemini-', '', $model);
1850
1851 $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
1852
1853 $args = [
1854 'headers' => [
1855 'Content-Type' => 'application/json',
1856 ],
1857 'body' => wp_json_encode([
1858 'contents' => [
1859 [
1860 'role' => 'user',
1861 'parts' => [
1862 ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
1863 ]
1864 ]
1865 ],
1866 'generationConfig' => [
1867 'temperature' => 0.2,
1868 'maxOutputTokens' => 20,
1869 ],
1870 ]),
1871 'method' => 'POST',
1872 'timeout' => 15,
1873 ];
1874
1875 $response = wp_remote_post($url, $args);
1876 if (is_wp_error($response)) {
1877 return sanitize_text_field($user_query);
1878 }
1879
1880 $body = json_decode(wp_remote_retrieve_body($response), true);
1881 if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
1882 return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
1883 }
1884
1885 return sanitize_text_field($user_query);
1886 }
1887
1888 /**
1889 * Interpret query using X.AI (Grok) models
1890 */
1891 private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
1892 $url = 'https://api.xai.com/v1/chat/completions';
1893
1894 $args = [
1895 'headers' => [
1896 'Content-Type' => 'application/json',
1897 'Authorization' => 'Bearer ' . $api_key,
1898 ],
1899 'body' => wp_json_encode([
1900 'model' => $model,
1901 'messages' => [
1902 ['role' => 'system', 'content' => $system_prompt],
1903 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1904 ],
1905 'temperature' => 0.2,
1906 'max_tokens' => 20,
1907 ]),
1908 'method' => 'POST',
1909 'timeout' => 15,
1910 ];
1911
1912 $response = wp_remote_post($url, $args);
1913 if (is_wp_error($response)) {
1914 return sanitize_text_field($user_query);
1915 }
1916
1917 $body = json_decode(wp_remote_retrieve_body($response), true);
1918 if (isset($body['choices'][0]['message']['content'])) {
1919 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1920 }
1921
1922 return sanitize_text_field($user_query);
1923 }
1924
1925 /**
1926 * Interpret query using DeepSeek models
1927 */
1928 private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
1929 $url = 'https://api.deepseek.com/v1/chat/completions';
1930
1931 $args = [
1932 'headers' => [
1933 'Content-Type' => 'application/json',
1934 'Authorization' => 'Bearer ' . $api_key,
1935 ],
1936 'body' => wp_json_encode([
1937 'model' => $model,
1938 'messages' => [
1939 ['role' => 'system', 'content' => $system_prompt],
1940 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
1941 ],
1942 'temperature' => 0.2,
1943 'max_tokens' => 20,
1944 ]),
1945 'method' => 'POST',
1946 'timeout' => 15,
1947 ];
1948
1949 $response = wp_remote_post($url, $args);
1950 if (is_wp_error($response)) {
1951 return sanitize_text_field($user_query);
1952 }
1953
1954 $body = json_decode(wp_remote_retrieve_body($response), true);
1955 if (isset($body['choices'][0]['message']['content'])) {
1956 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
1957 }
1958
1959 return sanitize_text_field($user_query);
1960 }
1961
1962 //very good
1963 private function add_email_to_loops($email) {
1964 // Sanitize the email
1965 $email = sanitize_email($email);
1966
1967 // Retrieve and sanitize options
1968 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
1969 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
1970
1971 // Check for missing API key or mailing list ID
1972 if (empty($api_key) || empty($mailing_list_id)) {
1973 //error_log(esc_html__('Loops API key or mailing list ID is missing.', 'mxchat'));
1974 return;
1975 }
1976
1977 $data = array(
1978 'email' => $email,
1979 'subscribed' => true,
1980 'source' => __('MxChat AI Chatbot', 'mxchat'),
1981 'mailingLists' => array($mailing_list_id => true),
1982 );
1983
1984 $url = 'https://app.loops.so/api/v1/contacts/create';
1985 $args = array(
1986 'body' => wp_json_encode($data),
1987 'headers' => array(
1988 'Authorization' => 'Bearer ' . $api_key,
1989 'Content-Type' => 'application/json',
1990 ),
1991 'method' => 'POST',
1992 'timeout' => 45,
1993 );
1994
1995 $response = wp_remote_post($url, $args);
1996
1997 // Handle errors in the API request
1998 if (is_wp_error($response)) {
1999 //error_log(esc_html__('Error adding email to Loops: ', 'mxchat') . $response->get_error_message());
2000 return;
2001 }
2002
2003 // Check for non-200 HTTP responses
2004 $response_code = wp_remote_retrieve_response_code($response);
2005 if ($response_code != 200) {
2006 $response_body = wp_remote_retrieve_body($response);
2007 //error_log(esc_html__('Loops API responded with code ', 'mxchat') . $response_code . ': ' . $response_body);
2008 }
2009 }
2010
2011 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
2012 // Get the maximum number of pages allowed from admin settings
2013 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2014
2015 // Retrieve options for dynamic texts
2016 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
2017 $success_text = $this->options['pdf_intent_success_text'] ?? __("I've processed the PDF. What questions do you have about it?", 'mxchat');
2018 $error_text = $this->options['pdf_intent_error_text'] ?? __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
2019
2020 // Check for explicit request for new PDF
2021 $new_pdf_requested = stripos($message, 'new') !== false ||
2022 stripos($message, 'another') !== false ||
2023 stripos($message, 'different') !== false;
2024
2025 // If user mentions adding/reading a PDF, set waiting flag
2026 if (stripos($message, 'pdf') !== false ||
2027 stripos($message, 'document') !== false ||
2028 stripos($message, 'read') !== false) {
2029 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
2030 $this->fallbackResponse['text'] = $trigger_text;
2031 return;
2032 }
2033
2034 // If we're waiting for a URL or user requested new PDF
2035 if ($new_pdf_requested || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
2036 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
2037 // Process URL... (rest of your existing URL processing code)
2038 } else {
2039 $this->fallbackResponse['text'] = $trigger_text;
2040 }
2041 return;
2042 }
2043
2044 // Default to proceeding with conversation if no specific PDF action is needed
2045 $this->fallbackResponse['text'] = '';
2046 }
2047
2048
2049 /**
2050 * Enhanced fetch_and_split_pdf_pages with detailed debugging
2051 */
2052 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2053 // CLEAR DEBUG LOGGING
2054 error_log("=== MXCHAT PDF PROCESSING START ===");
2055 error_log("PDF Source: " . $pdf_source);
2056 error_log("Max Pages: " . $max_pages);
2057 error_log("Session ID: " . ($this->session_id ?? 'not set'));
2058
2059 // Check if Advanced Claude Toolbar is available and enabled
2060 $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
2061 $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
2062
2063 error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2064 error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2065
2066 if ($claude_available && $claude_enabled) {
2067 error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2068
2069 // Attempt Claude processing first
2070 $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
2071
2072 if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
2073 error_log("�
2074 CLAUDE PROCESSING SUCCESSFUL!");
2075 error_log("Claude returned " . count($claude_result) . " processed pages");
2076
2077 // Log first page details for verification
2078 if (isset($claude_result[0])) {
2079 $first_page = $claude_result[0];
2080 error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2081 error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2082 error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2083 }
2084
2085 error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2086 return $claude_result;
2087 } else {
2088 error_log(" CLAUDE PROCESSING FAILED or returned invalid result");
2089 error_log("Claude result type: " . gettype($claude_result));
2090 if (is_array($claude_result)) {
2091 error_log("Claude result count: " . count($claude_result));
2092 }
2093 }
2094 }
2095
2096 // Fallback to basic processing
2097 error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2098
2099 $upload_dir = wp_upload_dir();
2100 $temp_file = null;
2101
2102 try {
2103 // Your existing basic processing code here...
2104 // (I'll include the key parts with debug logging)
2105
2106 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2107 error_log("Downloading PDF from URL...");
2108 $temp_file = wp_tempnam($pdf_source);
2109 $response = wp_remote_get($pdf_source, [
2110 'timeout' => 60,
2111 'headers' => ['User-Agent' => 'MxChat PDF Processor']
2112 ]);
2113
2114 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
2115 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
2116 error_log(" BASIC PROCESSING: Failed to download PDF: " . $error_message);
2117 return false;
2118 }
2119
2120 file_put_contents($temp_file, wp_remote_retrieve_body($response));
2121 error_log("�
2122 PDF downloaded successfully");
2123 } else {
2124 $temp_file = $pdf_source;
2125 error_log("Using local PDF file: " . $temp_file);
2126 }
2127
2128 // Parse PDF
2129 error_log("Parsing PDF with basic parser...");
2130 $parser = new \Smalot\PdfParser\Parser();
2131 $pdf = $parser->parseFile($temp_file);
2132 $pages = $pdf->getPages();
2133
2134 error_log("PDF contains " . count($pages) . " pages");
2135
2136 if (count($pages) > $max_pages) {
2137 error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2138 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2139 unlink($temp_file);
2140 }
2141 return 'too_many_pages';
2142 }
2143
2144 $embeddings = [];
2145 $processed_pages = 0;
2146
2147 foreach ($pages as $page_number => $page) {
2148 $text = $page->getText();
2149
2150 if (empty(trim($text))) {
2151 error_log("Skipping empty page: " . ($page_number + 1));
2152 continue;
2153 }
2154
2155 $text = $this->mxchat_clean_text($text);
2156
2157 $embedding = $this->mxchat_generate_embedding(
2158 __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2159 $this->options['api_key']
2160 );
2161
2162 if ($embedding) {
2163 $embeddings[] = [
2164 'page_number' => $page_number + 1,
2165 'embedding' => $embedding,
2166 'text' => $text,
2167 'enhanced' => false, // CLEARLY MARK AS BASIC
2168 'processing_method' => 'basic_pdf_parser'
2169 ];
2170 $processed_pages++;
2171 }
2172 }
2173
2174 error_log("�
2175 BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2176
2177 // Cleanup
2178 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2179 unlink($temp_file);
2180 }
2181
2182 error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
2183 return $embeddings;
2184
2185 } catch (\Exception $e) {
2186 error_log(" BASIC PROCESSING ERROR: " . $e->getMessage());
2187 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2188 unlink($temp_file);
2189 }
2190 error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
2191 return false;
2192 }
2193 }
2194
2195 private function mxchat_clean_text($text) {
2196 // Remove excessive whitespace
2197 $text = preg_replace('/\s+/', ' ', $text);
2198
2199 // Remove control characters except newlines and tabs
2200 $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
2201
2202 // Normalize line endings
2203 $text = str_replace(["\r\n", "\r"], "\n", $text);
2204
2205 // Trim whitespace
2206 $text = trim($text);
2207
2208 return $text;
2209 }
2210
2211 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
2212 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
2213
2214 $most_relevant = null;
2215 $highest_similarity = -INF;
2216
2217 foreach ($embeddings as $page_data) {
2218 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
2219
2220 if ($similarity > $highest_similarity) {
2221 $highest_similarity = $similarity;
2222 $most_relevant = $page_data['page_number'];
2223 }
2224 }
2225
2226 if (!is_null($most_relevant)) {
2227 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
2228 return array_filter($embeddings, function ($page) use ($page_numbers) {
2229 return in_array($page['page_number'], $page_numbers);
2230 });
2231 }
2232
2233 return [];
2234 }
2235 // Add this to your class
2236 public function handle_pdf_upload() {
2237 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2238
2239 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
2240 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
2241 return;
2242 }
2243
2244 $file = $_FILES['pdf_file'];
2245 $session_id = sanitize_text_field($_POST['session_id']);
2246 $original_filename = sanitize_text_field($file['name']);
2247
2248 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
2249 if ($file_type['type'] !== 'application/pdf') {
2250 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
2251 return;
2252 }
2253
2254 $upload_dir = wp_upload_dir();
2255 $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
2256 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
2257
2258 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
2259 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
2260 return;
2261 }
2262
2263 $this->clear_pdf_transients($session_id);
2264
2265 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2266 $embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
2267
2268 if ($embeddings === 'too_many_pages') {
2269 unlink($pdf_path);
2270 $error_message = sprintf(
2271 $this->options['pdf_intent_error_text'] ??
2272 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
2273 $max_pages
2274 );
2275 wp_send_json_error($error_message);
2276 return;
2277 }
2278
2279 if ($embeddings === false || empty($embeddings)) {
2280 unlink($pdf_path);
2281 $error_message = $this->options['pdf_intent_error_text'] ??
2282 esc_html__('The uploaded PDF appears to be empty or contains unsupported content.', 'mxchat');
2283 wp_send_json_error($error_message);
2284 return;
2285 }
2286
2287 if (!empty($embeddings)) {
2288 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
2289 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
2290 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
2291 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
2292
2293 $success_message = $this->options['pdf_intent_success_text'] ??
2294 esc_html__("I've processed the PDF. What questions do you have about it?", 'mxchat');
2295
2296 wp_send_json_success([
2297 'message' => $success_message,
2298 'filename' => $original_filename
2299 ]);
2300 return;
2301 }
2302
2303 unlink($pdf_path);
2304 $error_message = $this->options['pdf_intent_error_text'] ??
2305 esc_html__('Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.', 'mxchat');
2306 wp_send_json_error($error_message);
2307 return;
2308 }
2309 public function handle_pdf_remove() {
2310 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2311
2312 if (empty($_POST['session_id'])) {
2313 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
2314 wp_die();
2315 }
2316
2317 $session_id = sanitize_text_field($_POST['session_id']);
2318 $pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
2319
2320 if ($pdf_path && file_exists($pdf_path)) {
2321 unlink($pdf_path);
2322 }
2323
2324 $this->clear_pdf_transients($session_id);
2325
2326 wp_send_json_success([
2327 'message' => esc_html__('PDF removed successfully.', 'mxchat')
2328 ]);
2329 wp_die();
2330 }
2331
2332
2333
2334
2335 function mxchat_fetch_new_messages() {
2336 $session_id = sanitize_text_field($_POST['session_id']);
2337 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2338 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
2339 $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2340
2341 if (empty($session_id)) {
2342 //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
2343 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
2344 wp_die();
2345 }
2346
2347 $history = get_option("mxchat_history_{$session_id}", []);
2348
2349 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2350 // If persistence is enabled, show all new messages
2351 if ($persistence_enabled) {
2352 return !empty($message['id']) &&
2353 strcmp($message['id'], $last_seen_id) > 0 &&
2354 $message['role'] === 'agent';
2355 }
2356
2357 // If persistence is disabled, only show messages after initial timestamp
2358 return !empty($message['id']) &&
2359 $message['role'] === 'agent' &&
2360 $message['timestamp'] > $initial_timestamp;
2361 });
2362
2363 //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
2364
2365 wp_send_json_success([
2366 'new_messages' => array_values($new_messages)
2367 ]);
2368 wp_die();
2369 }
2370 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2371 // First check if live agents are available
2372 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2373 if ($live_agent_available !== 'on') {
2374 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2375 $this->fallbackResponse = [
2376 'text' => $away_message,
2377 'html' => '',
2378 'images' => [],
2379 'chat_mode' => 'ai'
2380 ];
2381 wp_send_json([
2382 'text' => $away_message,
2383 'html' => '',
2384 'chat_mode' => 'ai',
2385 'session_id' => $session_id
2386 ]);
2387 wp_die();
2388 }
2389
2390 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2391
2392 if (empty($slack_bot_token)) {
2393 return false;
2394 }
2395
2396 // Check if channel already exists for this session
2397 $channel_id = get_option("mxchat_channel_{$session_id}", '');
2398
2399 if (empty($channel_id)) {
2400 // Create new channel with session ID as name
2401 $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
2402
2403 //error_log("Attempting to create channel: $channel_name");
2404
2405 $response = wp_remote_post('https://slack.com/api/conversations.create', [
2406 'headers' => [
2407 'Content-Type' => 'application/json',
2408 'Authorization' => 'Bearer ' . $slack_bot_token
2409 ],
2410 'body' => json_encode([
2411 'name' => $channel_name,
2412 'is_private' => false // Public channel - anyone in workspace can join
2413 ])
2414 ]);
2415
2416 if (!is_wp_error($response)) {
2417 $response_body = wp_remote_retrieve_body($response);
2418 $response_data = json_decode($response_body, true);
2419
2420 //error_log("Channel creation response: " . $response_body);
2421
2422 if (isset($response_data['ok']) && $response_data['ok']) {
2423 $channel_id = $response_data['channel']['id'];
2424 $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2425 //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2426 update_option("mxchat_channel_{$session_id}", $channel_id);
2427
2428 // Auto-invite agents to the channel
2429 $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2430
2431 if (!empty($agent_user_ids)) {
2432 // Parse user IDs (one per line)
2433 $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2434
2435 foreach ($user_ids as $user_id_to_invite) {
2436 //error_log("Inviting user to channel: $user_id_to_invite");
2437
2438 $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2439 'headers' => [
2440 'Content-Type' => 'application/json',
2441 'Authorization' => 'Bearer ' . $slack_bot_token
2442 ],
2443 'body' => json_encode([
2444 'channel' => $channel_id,
2445 'users' => $user_id_to_invite
2446 ])
2447 ]);
2448
2449 if (!is_wp_error($invite_response)) {
2450 $invite_body = wp_remote_retrieve_body($invite_response);
2451 $invite_data = json_decode($invite_body, true);
2452 //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2453
2454 if (isset($invite_data['ok']) && $invite_data['ok']) {
2455 //error_log("Successfully invited user $user_id_to_invite to channel");
2456 } else {
2457 //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2458 }
2459 } else {
2460 //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2461 }
2462 }
2463 } else {
2464 //error_log("No agent user IDs configured for auto-invite");
2465 }
2466 } else {
2467 //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2468 }
2469 } else {
2470 //error_log("WP Error creating channel: " . $response->get_error_message());
2471 }
2472
2473 if (empty($channel_id)) {
2474 return false; // Failed to create channel
2475 }
2476 }
2477
2478 // Get recent chat history
2479 $history = get_option("mxchat_history_{$session_id}", []);
2480 $recent_history = array_slice($history, -5);
2481
2482 // Format conversation context
2483 $conversation_context = "";
2484 if (!empty($recent_history)) {
2485 $conversation_context = "*Recent Conversation:*\n";
2486 foreach ($recent_history as $hist_message) {
2487 $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
2488 $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
2489 }
2490 $conversation_context .= "\n";
2491 }
2492
2493 update_option("mxchat_mode_{$session_id}", 'agent');
2494
2495 // Send message to channel
2496 $channel_message = "🔔 *New Live Agent Request*\n\n";
2497 $channel_message .= "*Session ID:* `{$session_id}`\n";
2498 $channel_message .= "*User ID:* `{$user_id}`\n\n";
2499
2500 if (!empty($conversation_context)) {
2501 $channel_message .= $conversation_context;
2502 }
2503
2504 $channel_message .= "*Current Message:*\n{$message}\n\n";
2505 $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2506
2507 wp_remote_post('https://slack.com/api/chat.postMessage', [
2508 'headers' => [
2509 'Content-Type' => 'application/json',
2510 'Authorization' => 'Bearer ' . $slack_bot_token
2511 ],
2512 'body' => json_encode([
2513 'channel' => $channel_id,
2514 'text' => $channel_message,
2515 'mrkdwn' => true
2516 ])
2517 ]);
2518
2519 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2520 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2521
2522 $this->fallbackResponse = [
2523 'text' => $success_message,
2524 'html' => '',
2525 'images' => [],
2526 'chat_mode' => 'agent'
2527 ];
2528
2529 wp_send_json([
2530 'success' => true,
2531 'text' => $success_message,
2532 'html' => '',
2533 'chat_mode' => 'agent',
2534 'session_id' => $session_id,
2535 'fallbackResponse' => $this->fallbackResponse
2536 ]);
2537 wp_die();
2538 }
2539 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
2540 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2541 $channel_id = get_option("mxchat_channel_{$session_id}", '');
2542
2543 if (empty($slack_bot_token) || empty($channel_id)) {
2544 return false;
2545 }
2546
2547 $user_message = "💬 *User:* {$message}";
2548
2549 $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
2550 'headers' => [
2551 'Content-Type' => 'application/json',
2552 'Authorization' => 'Bearer ' . $slack_bot_token
2553 ],
2554 'body' => json_encode([
2555 'channel' => $channel_id,
2556 'text' => $user_message,
2557 'mrkdwn' => true
2558 ])
2559 ]);
2560
2561 return !is_wp_error($response);
2562 }
2563 public function handle_slack_interaction(WP_REST_Request $request) {
2564 //error_log('Received Slack interaction');
2565
2566 $payload = json_decode($request->get_param('payload'), true);
2567 //error_log('Payload: ' . print_r($payload, true));
2568
2569 // Handle button click
2570 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
2571 $session_id = $payload['actions'][0]['value'];
2572 $trigger_id = $payload['trigger_id'];
2573
2574 // Get Bot Token from settings
2575 $slack_token = $this->options['live_agent_bot_token'] ?? '';
2576
2577 if (empty($slack_token)) {
2578 //error_log('Slack Bot Token not configured');
2579 return new WP_REST_Response(['error' => esc_html__('Bot token not configured', 'mxchat')], 400);
2580 }
2581 $response = wp_remote_post('https://slack.com/api/views.open', [
2582 'headers' => [
2583 'Content-Type' => 'application/json',
2584 'Authorization' => 'Bearer ' . $slack_token
2585 ],
2586 'body' => json_encode([
2587 'trigger_id' => $trigger_id,
2588 'view' => [
2589 'type' => 'modal',
2590 'callback_id' => 'reply_modal',
2591 'title' => [
2592 'type' => 'plain_text',
2593 'text' => __('Reply to User', 'mxchat')
2594 ],
2595 'submit' => [
2596 'type' => 'plain_text',
2597 'text' => __('Send', 'mxchat')
2598 ],
2599 'close' => [
2600 'type' => 'plain_text',
2601 'text' => __('Cancel', 'mxchat')
2602 ],
2603 'blocks' => [
2604 [
2605 'type' => 'input',
2606 'block_id' => 'reply_block',
2607 'label' => [
2608 'type' => 'plain_text',
2609 'text' => sprintf(__('Reply to session: %s', 'mxchat'), $session_id)
2610 ],
2611 'element' => [
2612 'type' => 'plain_text_input',
2613 'action_id' => 'message',
2614 'multiline' => true,
2615 'placeholder' => [
2616 'type' => 'plain_text',
2617 'text' => __('Type your message here...', 'mxchat')
2618 ]
2619 ]
2620 ]
2621 ],
2622 'private_metadata' => $session_id
2623 ]
2624 ])
2625 ]);
2626
2627 //error_log('Views.open response: ' . print_r($response, true));
2628
2629 // Return immediate acknowledgment
2630 return new WP_REST_Response(['ok' => true]);
2631 }
2632
2633 // Handle modal submission
2634 // Handle modal submission
2635 if ($payload['type'] === 'view_submission') {
2636 $session_id = $payload['view']['private_metadata'];
2637 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
2638
2639 // Save the message (keep the message_id but don't include in response)
2640 $this->mxchat_save_chat_message($session_id, 'agent', $message);
2641
2642 // Keep the original response format for Slack
2643 return new WP_REST_Response([
2644 'response_action' => 'clear'
2645 ]);
2646 }
2647
2648 // Default acknowledgment
2649 return new WP_REST_Response(['ok' => true]);
2650 }
2651 public function mxchat_handle_agent_response(WP_REST_Request $request) {
2652 //error_log('Received agent response request');
2653 //error_log('Request data: ' . print_r($request->get_params(), true));
2654 // //error_log('Raw body: ' . file_get_contents('php://input'));
2655
2656 // Get the data from Slack's slash command format
2657 $command_text = $request->get_param('text');
2658 // //error_log('Command text: ' . $command_text);
2659
2660 if (empty($command_text)) {
2661 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
2662 return new WP_REST_Response([
2663 'error' => esc_html__('Command text is required. Format: /reply session_id message', 'mxchat')
2664 ], 400);
2665 }
2666
2667 // Split the command text into session_id and message
2668 $parts = explode(' ', $command_text, 2);
2669 if (count($parts) !== 2) {
2670 //error_log('Agent response error: Invalid command format');
2671 return new WP_REST_Response([
2672 'error' => esc_html__('Invalid format. Use: /reply session_id message', 'mxchat')
2673 ], 400);
2674 }
2675
2676 $session_id = sanitize_text_field($parts[0]);
2677 $message = sanitize_text_field($parts[1]);
2678
2679 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
2680
2681 // Save the message
2682 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
2683
2684 if (!$message_id) {
2685 // //error_log('Failed to save agent message');
2686 return new WP_REST_Response([
2687 'error' => esc_html__('Failed to save message', 'mxchat')
2688 ], 500);
2689 }
2690
2691 // Return success response in Slack's expected format
2692 return new WP_REST_Response([
2693 'response_type' => 'in_channel',
2694 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
2695 ], 200);
2696 }
2697 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
2698 //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2699
2700 // Just update mode to AI
2701 update_option("mxchat_mode_{$session_id}", 'ai');
2702
2703 // Initialize states
2704 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2705 $this->productCardHtml = '';
2706
2707 // Set the response message
2708 $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2709
2710 return true; // Intent was handled
2711 }
2712 public function handle_slack_messages(WP_REST_Request $request) {
2713 // Log the incoming request for debugging
2714 //error_log('Slack events request received: ' . $request->get_body());
2715
2716 $body = $request->get_body();
2717 $data = json_decode($body, true);
2718
2719 // Handle Slack URL verification
2720 if (isset($data['type']) && $data['type'] === 'url_verification') {
2721 //error_log('Slack URL verification challenge: ' . $data['challenge']);
2722 return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
2723 }
2724
2725 // IMPORTANT: Handle Slack's event deduplication
2726 if (isset($data['event_id'])) {
2727 $event_id = $data['event_id'];
2728 $processed_events = get_transient('mxchat_slack_events') ?: [];
2729
2730 // Check if we've already processed this event
2731 if (in_array($event_id, $processed_events)) {
2732 //error_log("Duplicate event detected: $event_id");
2733 return new WP_REST_Response(['ok' => true]);
2734 }
2735
2736 // Add this event to processed list
2737 $processed_events[] = $event_id;
2738 // Keep only last 100 events to prevent memory issues
2739 if (count($processed_events) > 100) {
2740 $processed_events = array_slice($processed_events, -100);
2741 }
2742 // Store for 1 hour
2743 set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
2744 }
2745
2746 // Handle message events
2747 if (isset($data['event']) && $data['event']['type'] === 'message') {
2748 $event = $data['event'];
2749
2750 // Skip bot messages and messages with subtypes (like bot_message)
2751 if (isset($event['bot_id']) || isset($event['subtype'])) {
2752 return new WP_REST_Response(['ok' => true]);
2753 }
2754
2755 // Additional check: Skip if this is a threaded reply to our confirmation
2756 if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
2757 return new WP_REST_Response(['ok' => true]);
2758 }
2759
2760 $channel_id = $event['channel'];
2761 $message_text = $event['text'] ?? '';
2762 $message_ts = $event['ts'] ?? '';
2763
2764 // Find session ID by looking for matching channel
2765 global $wpdb;
2766 $session_option = $wpdb->get_var(
2767 $wpdb->prepare(
2768 "SELECT option_name FROM {$wpdb->options}
2769 WHERE option_name LIKE 'mxchat_channel_%'
2770 AND option_value = %s",
2771 $channel_id
2772 )
2773 );
2774
2775 if ($session_option) {
2776 $session_id = str_replace('mxchat_channel_', '', $session_option);
2777
2778 // Create a unique key for this specific message
2779 $message_key = md5($session_id . $message_ts . $message_text);
2780 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
2781
2782 // Check if we've already processed this exact message
2783 if (in_array($message_key, $processed_messages)) {
2784 //error_log("Duplicate message detected for session $session_id");
2785 return new WP_REST_Response(['ok' => true]);
2786 }
2787
2788 // Add to processed messages
2789 $processed_messages[] = $message_key;
2790 // Keep only last 50 messages per session
2791 if (count($processed_messages) > 50) {
2792 $processed_messages = array_slice($processed_messages, -50);
2793 }
2794 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
2795
2796 // Save the agent message
2797 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
2798
2799 // Send confirmation back to Slack (only once)
2800 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2801 if (!empty($slack_bot_token)) {
2802 // Use a transient to prevent duplicate confirmations
2803 $confirm_key = 'mxchat_confirm_' . $message_key;
2804 if (!get_transient($confirm_key)) {
2805 wp_remote_post('https://slack.com/api/chat.postMessage', [
2806 'headers' => [
2807 'Content-Type' => 'application/json',
2808 'Authorization' => 'Bearer ' . $slack_bot_token
2809 ],
2810 'body' => json_encode([
2811 'channel' => $channel_id,
2812 'text' => "✅ _Message sent to user_",
2813 'thread_ts' => $event['ts'] // Reply in thread
2814 ])
2815 ]);
2816 // Set transient to prevent duplicate confirmations
2817 set_transient($confirm_key, true, 300); // 5 minutes
2818 }
2819 }
2820 }
2821 }
2822
2823 return new WP_REST_Response(['ok' => true]);
2824 }
2825
2826 // For the word upload handler
2827 public function mxchat_handle_word_upload() {
2828 // Delegate to word handler
2829 $this->word_handler->mxchat_handle_word_upload();
2830 }
2831
2832 // For the word removal handler
2833 public function mxchat_handle_word_remove() {
2834 // Delegate to word handler
2835 $this->word_handler->mxchat_handle_word_remove();
2836 }
2837
2838 // For the word status check
2839 public function mxchat_check_word_status() {
2840 // Delegate to word handler
2841 $this->word_handler->mxchat_check_word_status();
2842 }
2843
2844
2845 private function mxchat_get_user_identifier() {
2846 return MxChat_User::mxchat_get_user_identifier();
2847 }
2848
2849 private function mxchat_generate_embedding($text, $api_key) {
2850 try {
2851 // Get options and selected model
2852 $options = get_option('mxchat_options');
2853 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
2854
2855 // Determine endpoint and API key based on model
2856 if (strpos($selected_model, 'voyage') === 0) {
2857 $endpoint = 'https://api.voyageai.com/v1/embeddings';
2858 $api_key = $options['voyage_api_key'] ?? '';
2859
2860 // Check if Voyage API key is missing
2861 if (empty($api_key)) {
2862 //error_log('Voyage API key is missing');
2863 return [
2864 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
2865 'error_code' => 'missing_voyage_api_key'
2866 ];
2867 }
2868 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
2869 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
2870 $api_key = $options['gemini_api_key'] ?? '';
2871
2872 // Check if Gemini API key is missing
2873 if (empty($api_key)) {
2874 //error_log('Gemini API key is missing');
2875 return [
2876 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
2877 'error_code' => 'missing_gemini_api_key'
2878 ];
2879 }
2880 } else {
2881 $endpoint = 'https://api.openai.com/v1/embeddings';
2882 // Use the passed API key for OpenAI
2883
2884 // Check if OpenAI API key is missing
2885 if (empty($api_key)) {
2886 //error_log('OpenAI API key is missing');
2887 return [
2888 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
2889 'error_code' => 'missing_openai_api_key'
2890 ];
2891 }
2892 }
2893
2894 // Check if text is empty
2895 if (empty($text)) {
2896 //error_log('Empty text provided for embedding generation');
2897 return [
2898 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
2899 'error_code' => 'empty_embedding_text'
2900 ];
2901 }
2902
2903 // Prepare request body based on provider
2904 if (strpos($selected_model, 'gemini-embedding') === 0) {
2905 // Gemini API format
2906 $request_body = [
2907 'model' => 'models/' . $selected_model,
2908 'content' => [
2909 'parts' => [
2910 ['text' => $text]
2911 ]
2912 ],
2913 'outputDimensionality' => 1536
2914 ];
2915
2916 // Prepare headers for Gemini (API key as query parameter)
2917 $endpoint .= '?key=' . $api_key;
2918 $headers = [
2919 'Content-Type' => 'application/json'
2920 ];
2921 } else {
2922 // OpenAI/Voyage API format
2923 $request_body = [
2924 'input' => $text,
2925 'model' => $selected_model
2926 ];
2927
2928 // Add output_dimension for voyage-3-large
2929 if ($selected_model === 'voyage-3-large') {
2930 $request_body['output_dimension'] = 2048;
2931 }
2932
2933 // Prepare headers for OpenAI/Voyage
2934 $headers = [
2935 'Content-Type' => 'application/json',
2936 'Authorization' => 'Bearer ' . $api_key
2937 ];
2938 }
2939
2940 // Prepare request arguments
2941 $args = [
2942 'body' => wp_json_encode($request_body),
2943 'headers' => $headers,
2944 'timeout' => 60,
2945 'redirection' => 5,
2946 'blocking' => true,
2947 'httpversion' => '1.0',
2948 'sslverify' => true,
2949 ];
2950
2951 // Make the request
2952 $response = wp_remote_post($endpoint, $args);
2953
2954 // Handle WordPress errors
2955 if (is_wp_error($response)) {
2956 $error_message = $response->get_error_message();
2957 //error_log('Embedding Generation Error: ' . $error_message);
2958 return [
2959 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
2960 'error_code' => 'embedding_connection_error'
2961 ];
2962 }
2963
2964 // Check HTTP status code
2965 $status_code = wp_remote_retrieve_response_code($response);
2966 if ($status_code !== 200) {
2967 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2968
2969 $error_message = isset($response_body['error']['message'])
2970 ? $response_body['error']['message']
2971 : 'HTTP Error ' . $status_code;
2972
2973 $error_type = isset($response_body['error']['type'])
2974 ? $response_body['error']['type']
2975 : 'unknown';
2976
2977 //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
2978
2979 // Handle specific error types
2980 switch ($error_type) {
2981 case 'invalid_request_error':
2982 if (strpos($error_message, 'API key') !== false) {
2983 return [
2984 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
2985 'error_code' => 'embedding_invalid_api_key'
2986 ];
2987 }
2988 break;
2989
2990 case 'authentication_error':
2991 return [
2992 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
2993 'error_code' => 'embedding_auth_error'
2994 ];
2995
2996 case 'rate_limit_exceeded':
2997 return [
2998 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
2999 'error_code' => 'embedding_rate_limit'
3000 ];
3001
3002 case 'quota_exceeded':
3003 return [
3004 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
3005 'error_code' => 'embedding_quota_exceeded'
3006 ];
3007 }
3008
3009 // Generic error fallback
3010 return [
3011 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3012 'error_code' => 'embedding_api_error',
3013 'status_code' => $status_code
3014 ];
3015 }
3016
3017 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3018
3019 // Handle different response formats based on provider
3020 if (strpos($selected_model, 'gemini-embedding') === 0) {
3021 // Gemini API response format
3022 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3023 return $response_body['embedding']['values'];
3024 } else {
3025 //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3026 return [
3027 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3028 'error_code' => 'invalid_gemini_embedding_response'
3029 ];
3030 }
3031 } else {
3032 // OpenAI/Voyage API response format
3033 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3034 return $response_body['data'][0]['embedding'];
3035 } else {
3036 //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3037 return [
3038 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3039 'error_code' => 'invalid_embedding_response'
3040 ];
3041 }
3042 }
3043 } catch (Exception $e) {
3044 //error_log('Embedding Exception: ' . $e->getMessage());
3045 return [
3046 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3047 'error_code' => 'embedding_exception'
3048 ];
3049 }
3050 }
3051 private function mxchat_find_relevant_content($user_embedding) {
3052 //error_log('MXChat Vector Search: Starting content search...');
3053
3054 // Retrieve the add-on settings from the database.
3055 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3056
3057 // Determine whether Pinecone is enabled.
3058 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
3059
3060 //error_log('Pinecone enabled flag: ' . $use_pinecone);
3061
3062 if ($use_pinecone === 1) {
3063 //error_log('MXChat Vector Search: Using Pinecone database');
3064 return $this->find_relevant_content_pinecone($user_embedding);
3065 } else {
3066 //error_log('MXChat Vector Search: Using WordPress database');
3067 return $this->find_relevant_content_wordpress($user_embedding);
3068 }
3069 }
3070
3071 private function find_relevant_content_wordpress($user_embedding) {
3072 global $wpdb;
3073 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3074 $cache_key = 'mxchat_system_prompt_embeddings';
3075 $batch_size = 500;
3076
3077 // Initialize similarity analysis storage
3078 $this->last_similarity_analysis = [
3079 'knowledge_base_type' => 'WordPress Database',
3080 'top_matches' => [],
3081 'threshold_used' => 0,
3082 'total_checked' => 0
3083 ];
3084
3085 // Retrieve embeddings from cache or database
3086 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3087 if ($embeddings === false) {
3088 // Cache miss - load embeddings from database WITH CONTENT for testing
3089 $embeddings = [];
3090 $offset = 0;
3091
3092 do {
3093 $query = $wpdb->prepare(
3094 "SELECT id, embedding_vector, article_content, source_url
3095 FROM {$system_prompt_table}
3096 LIMIT %d OFFSET %d",
3097 $batch_size,
3098 $offset
3099 );
3100
3101 $batch = $wpdb->get_results($query);
3102 if (empty($batch)) {
3103 break;
3104 }
3105
3106 $embeddings = array_merge($embeddings, $batch);
3107 $offset += $batch_size;
3108 unset($batch);
3109 } while (true);
3110
3111 if (empty($embeddings)) {
3112 return '';
3113 }
3114
3115 // Cache embeddings for future use (but note: this now includes content)
3116 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3117 }
3118
3119 // Get configuration options
3120 $main_options = get_option('mxchat_options', []);
3121
3122 // Get base similarity threshold (default 75%)
3123 $similarity_threshold = isset($main_options['similarity_threshold'])
3124 ? ((int) $main_options['similarity_threshold']) / 100
3125 : 0.75;
3126
3127 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3128
3129 // Calculate similarities and build results array
3130 $all_similarities = [];
3131 $relevant_results = [];
3132
3133 foreach ($embeddings as $embedding) {
3134 $database_embedding = $embedding->embedding_vector
3135 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3136 : null;
3137
3138 if (is_array($database_embedding) && is_array($user_embedding)) {
3139 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3140
3141 // Store ALL similarities for testing (top 10)
3142 $source_display = '';
3143 if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3144 $source_display = $embedding->source_url;
3145 } else {
3146 $content_preview = strip_tags($embedding->article_content ?? '');
3147 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3148 $source_display = substr(trim($content_preview), 0, 50) . '...';
3149 }
3150
3151 $all_similarities[] = [
3152 'document_id' => $embedding->id,
3153 'similarity' => $similarity,
3154 'similarity_percentage' => round($similarity * 100, 2),
3155 'above_threshold' => $similarity >= $similarity_threshold,
3156 'source_display' => $source_display,
3157 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3158 'used_for_context' => false // Initialize as false, we'll update this later
3159 ];
3160
3161 // Only consider results above threshold for actual content retrieval
3162 if ($similarity >= $similarity_threshold) {
3163 $relevant_results[] = [
3164 'id' => $embedding->id,
3165 'similarity' => $similarity
3166 ];
3167 }
3168 }
3169
3170 unset($database_embedding);
3171 }
3172
3173 // Sort ALL similarities for testing display (highest first)
3174 usort($all_similarities, function ($a, $b) {
3175 return $b['similarity'] <=> $a['similarity'];
3176 });
3177
3178 // Sort relevant results by similarity (highest first)
3179 usort($relevant_results, function ($a, $b) {
3180 return $b['similarity'] <=> $a['similarity'];
3181 });
3182
3183 // Get top 5 results for actual content (standard approach)
3184 $top_results = array_slice($relevant_results, 0, 5);
3185
3186 // NOW mark which documents are actually used for context
3187 $used_document_ids = [];
3188 foreach ($top_results as $result) {
3189 $used_document_ids[] = $result['id'];
3190 }
3191
3192 // Update the all_similarities array to mark which were actually used
3193 foreach ($all_similarities as &$similarity_item) {
3194 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
3195 }
3196
3197 // Store top 10 for testing panel (now with correct used_for_context flags)
3198 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
3199 $this->last_similarity_analysis['total_checked'] = count($embeddings);
3200
3201 //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3202
3203 // Initialize final content
3204 $content = '';
3205
3206 // Track document IDs to avoid duplicates
3207 $added_document_ids = [];
3208
3209 // Fetch and format content for each selected result
3210 foreach ($top_results as $index => $result) {
3211 if (in_array($result['id'], $added_document_ids)) {
3212 continue;
3213 }
3214
3215 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3216 $added_document_ids[] = $result['id'];
3217
3218 $content .= "## Reference " . ($index + 1) . " ##\n";
3219 $content .= $chunk_content . "\n\n";
3220
3221 // PDF surrounding pages logic (unchanged)
3222 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3223 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3224 "SELECT id, article_content FROM {$system_prompt_table}
3225 WHERE id IN (
3226 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3227 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3228 )",
3229 $result['id'],
3230 $result['id']
3231 ));
3232
3233 if (!empty($surrounding_content[0])) {
3234 $content .= "## Related Content ##\n";
3235 $content .= $surrounding_content[0]->article_content . "\n\n";
3236 $added_document_ids[] = $surrounding_content[0]->id;
3237 }
3238
3239 if (!empty($surrounding_content[1])) {
3240 $content .= "## Related Content ##\n";
3241 $content .= $surrounding_content[1]->article_content . "\n\n";
3242 $added_document_ids[] = $surrounding_content[1]->id;
3243 }
3244 }
3245 }
3246
3247 // Add response guidelines
3248 if (empty($top_results)) {
3249 $content = "No reference information was found for this query.\n\n";
3250 } else {
3251 $content .= "\n## Response Guidelines ##\n" .
3252 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3253 "Be conversational and friendly, but never mention your knowledge base or training data. " .
3254 "If you don't have specific information or are uncertain about any details, it's always " .
3255 "better to honestly say you don't know rather than making up or guessing at answers. " .
3256 "When information is incomplete, let them know you are unsure.";
3257 }
3258
3259 return trim($content);
3260 }
3261
3262 private function find_relevant_content_pinecone($user_embedding) {
3263 $options = get_option('mxchat_pinecone_addon_options', array());
3264 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3265 $host = $options['mxchat_pinecone_host'] ?? '';
3266
3267 // Initialize similarity analysis storage
3268 $this->last_similarity_analysis = [
3269 'knowledge_base_type' => 'Pinecone',
3270 'top_matches' => [],
3271 'threshold_used' => 0,
3272 'total_checked' => 0
3273 ];
3274
3275 if (empty($host) || empty($api_key)) {
3276 return '';
3277 }
3278
3279 // Get the similarity threshold from the main options
3280 $main_options = get_option('mxchat_options', []);
3281 $similarity_threshold = isset($main_options['similarity_threshold'])
3282 ? ((int) $main_options['similarity_threshold']) / 100
3283 : 0.75;
3284
3285 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3286
3287 // Prepare the query request for Pinecone (request more for testing)
3288 $api_endpoint = "https://{$host}/query";
3289
3290 $request_body = array(
3291 'vector' => $user_embedding,
3292 'topK' => 20, // Request more to get good testing data
3293 'includeMetadata' => true,
3294 'includeValues' => true
3295 );
3296
3297 $response = wp_remote_post($api_endpoint, array(
3298 'headers' => array(
3299 'Api-Key' => $api_key,
3300 'accept' => 'application/json',
3301 'content-type' => 'application/json'
3302 ),
3303 'body' => wp_json_encode($request_body),
3304 'timeout' => 30
3305 ));
3306
3307 if (is_wp_error($response)) {
3308 return '';
3309 }
3310
3311 $response_code = wp_remote_retrieve_response_code($response);
3312 if ($response_code !== 200) {
3313 return '';
3314 }
3315
3316 $results = json_decode(wp_remote_retrieve_body($response), true);
3317 if (empty($results['matches'])) {
3318 return '';
3319 }
3320
3321 // First, determine which matches will actually be used for content
3322 $matches_used_for_context = [];
3323 $matches_used = 0;
3324
3325 foreach ($results['matches'] as $index => $match) {
3326 // Skip if similarity is below threshold
3327 if ($match['score'] < $similarity_threshold) {
3328 continue;
3329 }
3330
3331 // Limit to top 5 matches above threshold
3332 if ($matches_used >= 5) {
3333 break;
3334 }
3335
3336 if (!empty($match['metadata']['text'])) {
3337 $matches_used_for_context[] = $match['id'] ?? $index;
3338 $matches_used++;
3339 }
3340 }
3341
3342 // Process ALL matches for testing data (top 10)
3343 $all_matches = [];
3344 foreach ($results['matches'] as $index => $match) {
3345 if ($index >= 10) break; // Limit to top 10 for testing
3346
3347 $source_display = '';
3348 if (!empty($match['metadata']['source_url'])) {
3349 $source_display = $match['metadata']['source_url'];
3350 } else {
3351 $content_preview = strip_tags($match['metadata']['text'] ?? '');
3352 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3353 $source_display = substr(trim($content_preview), 0, 50) . '...';
3354 }
3355
3356 $match_id = $match['id'] ?? $index;
3357
3358 $all_matches[] = [
3359 'document_id' => $match_id,
3360 'similarity' => $match['score'],
3361 'similarity_percentage' => round($match['score'] * 100, 2),
3362 'above_threshold' => $match['score'] >= $similarity_threshold,
3363 'source_display' => $source_display,
3364 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
3365 'used_for_context' => in_array($match_id, $matches_used_for_context) // Correct usage flag
3366 ];
3367 }
3368
3369 // Store for testing panel
3370 $this->last_similarity_analysis['top_matches'] = $all_matches;
3371 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
3372
3373 //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
3374
3375 // Initialize the final content
3376 $content = '';
3377 $matches_used = 0;
3378
3379 // Process each match for actual content (this is the real content generation)
3380 foreach ($results['matches'] as $index => $match) {
3381 // Skip if similarity is below threshold
3382 if ($match['score'] < $similarity_threshold) {
3383 continue;
3384 }
3385
3386 // Limit to top 5 matches above threshold
3387 if ($matches_used >= 5) {
3388 break;
3389 }
3390
3391 if (!empty($match['metadata']['text'])) {
3392 $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3393 $content .= $match['metadata']['text'] . "\n\n";
3394
3395 if (!empty($match['metadata']['source_url'])) {
3396 $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3397 }
3398
3399 $matches_used++;
3400 }
3401 }
3402
3403 // Add response guidelines
3404 if ($matches_used === 0) {
3405 $content = "No reference information was found for this query.\n\n";
3406 } else {
3407 $content .= "\n## Response Guidelines ##\n" .
3408 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3409 "Be conversational and friendly, but never mention your knowledge base or training data. " .
3410 "If you don't have specific information or are uncertain about any details, it's always " .
3411 "better to honestly say you don't know rather than making up or guessing at answers. " .
3412 "When information is incomplete, let them know you are unsure.";
3413 }
3414
3415 return trim($content);
3416 }
3417
3418 private function mxchat_find_relevant_products($user_embedding) {
3419 //error_log('MXChat Vector Search: Starting product search...');
3420
3421 // Retrieve the add-on settings from the database
3422 $addon_options = get_option('mxchat_pinecone_addon_options', array());
3423
3424 // Determine whether Pinecone is enabled
3425 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
3426
3427 //error_log('Pinecone enabled flag: ' . $use_pinecone);
3428
3429 if ($use_pinecone === 1) {
3430 //error_log('MXChat Vector Search: Using Pinecone database for products');
3431 return $this->find_relevant_products_pinecone($user_embedding);
3432 } else {
3433 //error_log('MXChat Vector Search: Using WordPress database for products');
3434 return $this->find_relevant_products_wordpress($user_embedding);
3435 }
3436 }
3437 private function find_relevant_products_wordpress($user_embedding) {
3438 global $wpdb;
3439 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3440 $cache_key = 'mxchat_system_prompt_embeddings';
3441 $batch_size = 500;
3442
3443 // Original WordPress database search logic
3444 // [Previous implementation remains the same]
3445 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3446 if ($embeddings === false) {
3447 $embeddings = [];
3448 $offset = 0;
3449
3450 do {
3451 $query = $wpdb->prepare(
3452 "SELECT id, embedding_vector
3453 FROM {$system_prompt_table}
3454 LIMIT %d OFFSET %d",
3455 $batch_size,
3456 $offset
3457 );
3458
3459 $batch = $wpdb->get_results($query);
3460 if (empty($batch)) {
3461 break;
3462 }
3463
3464 $embeddings = array_merge($embeddings, $batch);
3465 $offset += $batch_size;
3466
3467 unset($batch);
3468
3469 } while (true);
3470
3471 if (empty($embeddings)) {
3472 return '';
3473 }
3474 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3475 }
3476
3477 $relevant_results = [];
3478 foreach ($embeddings as $embedding) {
3479 $database_embedding = $embedding->embedding_vector
3480 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3481 : null;
3482 if (is_array($database_embedding) && is_array($user_embedding)) {
3483 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3484 $relevant_results[] = [
3485 'id' => $embedding->id,
3486 'similarity' => $similarity
3487 ];
3488 }
3489 unset($database_embedding);
3490 }
3491
3492 // Use fixed threshold for products
3493 $similarity_threshold = 0.85;
3494
3495 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3496 return $result['similarity'] >= $similarity_threshold;
3497 });
3498 usort($relevant_results, function ($a, $b) {
3499 return $b['similarity'] <=> $a['similarity'];
3500 });
3501
3502 $top_results = array_slice($relevant_results, 0, 5);
3503 $content = '';
3504
3505 foreach ($top_results as $result) {
3506 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3507 $content .= $chunk_content . "\n\n";
3508 }
3509
3510 return trim($content);
3511 }
3512 private function find_relevant_products_pinecone($user_embedding) {
3513 //error_log('Starting Pinecone product search...');
3514
3515 $options = get_option('mxchat_pinecone_addon_options', array());
3516 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3517 $host = $options['mxchat_pinecone_host'] ?? '';
3518
3519 if (empty($host) || empty($api_key)) {
3520 //error_log('Pinecone credentials not properly configured for product search');
3521 return '';
3522 }
3523
3524 $similarity_threshold = 0.85;
3525 $api_endpoint = "https://{$host}/query";
3526
3527 $request_body = array(
3528 'vector' => $user_embedding,
3529 'topK' => 5,
3530 'includeMetadata' => true,
3531 'includeValues' => true,
3532 'filter' => array(
3533 'type' => 'product'
3534 )
3535 );
3536
3537 //error_log('Sending request to Pinecone with body: ' . wp_json_encode($request_body));
3538
3539 $response = wp_remote_post($api_endpoint, array(
3540 'headers' => array(
3541 'Api-Key' => $api_key,
3542 'accept' => 'application/json',
3543 'content-type' => 'application/json'
3544 ),
3545 'body' => wp_json_encode($request_body),
3546 'timeout' => 30
3547 ));
3548
3549 if (is_wp_error($response)) {
3550 //error_log('Pinecone product query error: ' . $response->get_error_message());
3551 return '';
3552 }
3553
3554 $response_code = wp_remote_retrieve_response_code($response);
3555 //error_log('Pinecone response code: ' . $response_code);
3556
3557 if ($response_code !== 200) {
3558 //error_log('Pinecone API error during product search: ' . wp_remote_retrieve_body($response));
3559 return '';
3560 }
3561
3562 $results = json_decode(wp_remote_retrieve_body($response), true);
3563 //error_log('Pinecone raw response: ' . wp_remote_retrieve_body($response));
3564
3565 if (empty($results['matches'])) {
3566 //error_log('No matches found in Pinecone response');
3567 return '';
3568 }
3569
3570 $content = '';
3571 foreach ($results['matches'] as $match) {
3572 if ($match['score'] < $similarity_threshold) {
3573 //error_log("Match below threshold: " . $match['score']);
3574 continue;
3575 }
3576
3577 if (!empty($match['metadata']['text'])) {
3578 $content .= $match['metadata']['text'];
3579 if (!empty($match['metadata']['source_url'])) {
3580 $content .= "\n\nFor more details, check out this product: " . esc_url($match['metadata']['source_url']);
3581 }
3582 $content .= "\n\n";
3583 }
3584 }
3585
3586 return trim($content);
3587 }
3588 private function fetch_content_with_product_links($most_relevant_id) {
3589 global $wpdb;
3590 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3591
3592 // Fetch the article content and associated product URL
3593 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
3594 $result = $wpdb->get_row($query);
3595
3596 if ($result) {
3597 // Append the product link to the content if available
3598 $content = $result->article_content;
3599 if (!empty($result->source_url)) {
3600 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
3601 }
3602 return $content;
3603 }
3604
3605 return null;
3606 }
3607
3608 /**
3609 * Modified streaming functions to include testing data
3610 */
3611
3612 // 1. Update the main handler to pass testing data to streaming functions
3613 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null) {
3614 try {
3615 if (!$relevant_content) {
3616 $error_response = [
3617 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
3618 'error_code' => 'no_relevant_content'
3619 ];
3620
3621 // Add testing data to error response if available
3622 if ($testing_data !== null) {
3623 $error_response['testing_data'] = $testing_data;
3624 //error_log("MxChat Testing: Added testing data to no_relevant_content error");
3625 }
3626
3627 return $error_response;
3628 }
3629
3630 // Ensure conversation_history is an array
3631 if (!is_array($conversation_history)) {
3632 $conversation_history = array();
3633 }
3634
3635 // Get selected model with default fallback
3636 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3637
3638 // Extract model prefix to determine the provider
3639 $model_parts = explode('-', $selected_model);
3640 $provider = strtolower($model_parts[0]);
3641
3642 // Handle model selection based on provider prefix
3643 switch ($provider) {
3644 case 'gemini':
3645 if (empty($gemini_api_key)) {
3646 $error_response = [
3647 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3648 'error_code' => 'missing_gemini_api_key'
3649 ];
3650 if ($testing_data !== null) {
3651 $error_response['testing_data'] = $testing_data;
3652 }
3653 return $error_response;
3654 }
3655 $response = $this->mxchat_generate_response_gemini(
3656 $selected_model,
3657 $gemini_api_key,
3658 $conversation_history,
3659 $relevant_content
3660 );
3661 break;
3662
3663 case 'claude':
3664 if (empty($claude_api_key)) {
3665 $error_response = [
3666 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
3667 'error_code' => 'missing_claude_api_key'
3668 ];
3669 if ($testing_data !== null) {
3670 $error_response['testing_data'] = $testing_data;
3671 }
3672 return $error_response;
3673 }
3674 if ($streaming) {
3675 return $this->mxchat_generate_response_claude_stream(
3676 $selected_model,
3677 $claude_api_key,
3678 $conversation_history,
3679 $relevant_content,
3680 $session_id,
3681 $testing_data // Pass testing data
3682 );
3683 } else {
3684 $response = $this->mxchat_generate_response_claude(
3685 $selected_model,
3686 $claude_api_key,
3687 $conversation_history,
3688 $relevant_content
3689 );
3690 }
3691 break;
3692
3693 case 'grok':
3694 if (empty($xai_api_key)) {
3695 $error_response = [
3696 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
3697 'error_code' => 'missing_xai_api_key'
3698 ];
3699 if ($testing_data !== null) {
3700 $error_response['testing_data'] = $testing_data;
3701 }
3702 return $error_response;
3703 }
3704 if ($streaming) {
3705 return $this->mxchat_generate_response_xai_stream(
3706 $selected_model,
3707 $xai_api_key,
3708 $conversation_history,
3709 $relevant_content,
3710 $session_id,
3711 $testing_data // Pass testing data
3712 );
3713 } else {
3714 $response = $this->mxchat_generate_response_xai(
3715 $selected_model,
3716 $xai_api_key,
3717 $conversation_history,
3718 $relevant_content
3719 );
3720 }
3721 break;
3722
3723 case 'deepseek':
3724 if (empty($deepseek_api_key)) {
3725 $error_response = [
3726 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
3727 'error_code' => 'missing_deepseek_api_key'
3728 ];
3729 if ($testing_data !== null) {
3730 $error_response['testing_data'] = $testing_data;
3731 }
3732 return $error_response;
3733 }
3734 $response = $this->mxchat_generate_response_deepseek(
3735 $selected_model,
3736 $deepseek_api_key,
3737 $conversation_history,
3738 $relevant_content
3739 );
3740 break;
3741
3742 case 'gpt':
3743 case 'o1':
3744 if (empty($api_key)) {
3745 $error_response = [
3746 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3747 'error_code' => 'missing_openai_api_key'
3748 ];
3749 if ($testing_data !== null) {
3750 $error_response['testing_data'] = $testing_data;
3751 }
3752 return $error_response;
3753 }
3754 if ($streaming) {
3755 return $this->mxchat_generate_response_openai_stream(
3756 $selected_model,
3757 $api_key,
3758 $conversation_history,
3759 $relevant_content,
3760 $session_id,
3761 $testing_data // Pass testing data
3762 );
3763 } else {
3764 $response = $this->mxchat_generate_response_openai(
3765 $selected_model,
3766 $api_key,
3767 $conversation_history,
3768 $relevant_content
3769 );
3770 }
3771 break;
3772
3773 default:
3774 // Default to OpenAI for custom models or unrecognized prefixes
3775 if (empty($api_key)) {
3776 $error_response = [
3777 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3778 'error_code' => 'missing_openai_api_key'
3779 ];
3780 if ($testing_data !== null) {
3781 $error_response['testing_data'] = $testing_data;
3782 }
3783 return $error_response;
3784 }
3785 if ($streaming) {
3786 return $this->mxchat_generate_response_openai_stream(
3787 $selected_model,
3788 $api_key,
3789 $conversation_history,
3790 $relevant_content,
3791 $session_id,
3792 $testing_data // Pass testing data
3793 );
3794 } else {
3795 $response = $this->mxchat_generate_response_openai(
3796 $selected_model,
3797 $api_key,
3798 $conversation_history,
3799 $relevant_content
3800 );
3801 }
3802 break;
3803 }
3804
3805 // Check if the response is an error array from the provider-specific function
3806 if (is_array($response) && isset($response['error'])) {
3807 // Add testing data to error response if available
3808 if ($testing_data !== null) {
3809 $response['testing_data'] = $testing_data;
3810 //error_log("MxChat Testing: Added testing data to provider error response");
3811 }
3812 return $response; // Pass through the error with testing data
3813 }
3814
3815 // For successful non-streaming responses, we don't add testing data here
3816 // because it will be added in the main handler
3817 return $response;
3818
3819 } catch (Exception $e) {
3820 //error_log('MXChat Error: ' . $e->getMessage());
3821 $error_response = [
3822 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
3823 'error_code' => 'system_exception',
3824 'exception_details' => $e->getMessage()
3825 ];
3826
3827 // Add testing data to exception response if available
3828 if ($testing_data !== null) {
3829 $error_response['testing_data'] = $testing_data;
3830 //error_log("MxChat Testing: Added testing data to exception response");
3831 }
3832
3833 return $error_response;
3834 }
3835 }
3836
3837 // 2. Update Claude streaming function
3838 private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
3839 try {
3840 // Enable implicit flushing for real-time streaming
3841 ob_implicit_flush(true);
3842
3843 // Get system prompt instructions from options
3844 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
3845
3846 // Ensure conversation_history is an array
3847 if (!is_array($conversation_history)) {
3848 $conversation_history = array();
3849 }
3850
3851 // Clean and validate conversation history
3852 foreach ($conversation_history as &$message) {
3853 // Convert bot and agent roles to assistant
3854 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
3855 $message['role'] = 'assistant';
3856 }
3857
3858 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
3859 if (!in_array($message['role'], ['assistant', 'user'])) {
3860 $message['role'] = 'user';
3861 }
3862
3863 // Ensure content field exists
3864 if (!isset($message['content']) || empty($message['content'])) {
3865 $message['content'] = '';
3866 }
3867
3868 // Remove any unsupported fields
3869 $message = array_intersect_key($message, array_flip(['role', 'content']));
3870 }
3871
3872 // Add relevant content as the latest user message
3873 $conversation_history[] = [
3874 'role' => 'user',
3875 'content' => $relevant_content
3876 ];
3877
3878 // Prepare the request body with stream: true
3879 $body = json_encode([
3880 'model' => $selected_model,
3881 'messages' => $conversation_history,
3882 'max_tokens' => 1000,
3883 'temperature' => 0.8,
3884 'system' => $system_prompt_instructions,
3885 'stream' => true
3886 ]);
3887
3888 // Check if we can actually stream (headers not sent, etc.)
3889 if (headers_sent() || !function_exists('curl_init')) {
3890 // Fallback to regular response with testing data
3891 //error_log("MxChat: Streaming not possible, falling back to regular response");
3892 $regular_response = $this->mxchat_generate_response_claude(
3893 $selected_model,
3894 $claude_api_key,
3895 array_slice($conversation_history, 0, -1), // Remove the added content
3896 $relevant_content
3897 );
3898
3899 // Return as JSON with testing data
3900 $response_data = [
3901 'text' => $regular_response,
3902 'html' => '',
3903 'session_id' => $session_id
3904 ];
3905
3906 if ($testing_data !== null) {
3907 $response_data['testing_data'] = $testing_data;
3908 //error_log("MxChat Testing: Added testing data to Claude fallback response");
3909 }
3910
3911 // Clear any streaming headers and send JSON
3912 if (headers_sent() === false) {
3913 header('Content-Type: application/json');
3914 }
3915 echo json_encode($response_data);
3916 return true; // Indicate we handled the response
3917 }
3918
3919 // Use cURL for streaming support
3920 $ch = curl_init();
3921 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
3922 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
3923 curl_setopt($ch, CURLOPT_POST, true);
3924 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
3925 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
3926 'Content-Type: application/json',
3927 'x-api-key: ' . $claude_api_key,
3928 'anthropic-version: 2023-06-01'
3929 ));
3930 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
3931 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
3932
3933 // Add buffer control options for better streaming
3934 curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // Smaller buffer for faster streaming
3935 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
3936
3937 $full_response = ''; // Accumulate full response for saving
3938 $stream_started = false;
3939
3940 // Buffer control for real-time streaming
3941 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
3942 // Flush any existing output buffers
3943 if (ob_get_level()) {
3944 ob_flush();
3945 }
3946
3947 // Send testing data as the first event if available
3948 if (!$stream_started && $testing_data !== null) {
3949 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
3950 flush();
3951 if (function_exists('fastcgi_finish_request')) {
3952 fastcgi_finish_request();
3953 }
3954 $stream_started = true;
3955 //error_log("MxChat Testing: Sent testing data in Claude stream");
3956 }
3957
3958 // Process each chunk of data
3959 $lines = explode("\n", $data);
3960
3961 foreach ($lines as $line) {
3962 if (trim($line) === '') {
3963 continue;
3964 }
3965
3966 // Claude uses event: and data: format
3967 if (strpos($line, 'event: ') === 0) {
3968 // Store the event type for the next data line
3969 continue;
3970 }
3971
3972 if (strpos($line, 'data: ') === 0) {
3973 $json_str = substr($line, 6); // Remove 'data: ' prefix
3974
3975 $json = json_decode($json_str, true);
3976 if (json_last_error() !== JSON_ERROR_NONE) {
3977 continue;
3978 }
3979
3980 // Handle different event types
3981 if (isset($json['type'])) {
3982 switch ($json['type']) {
3983 case 'content_block_delta':
3984 if (isset($json['delta']['text'])) {
3985 $content = $json['delta']['text'];
3986 $full_response .= $content; // Accumulate
3987 // Send as SSE format compatible with your frontend
3988 echo "data: " . json_encode(['content' => $content]) . "\n\n";
3989 flush();
3990 if (function_exists('fastcgi_finish_request')) {
3991 fastcgi_finish_request();
3992 }
3993 }
3994 break;
3995
3996 case 'message_stop':
3997 echo "data: [DONE]\n\n";
3998 flush();
3999 if (function_exists('fastcgi_finish_request')) {
4000 fastcgi_finish_request();
4001 }
4002 break;
4003
4004 case 'error':
4005 echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
4006 flush();
4007 if (function_exists('fastcgi_finish_request')) {
4008 fastcgi_finish_request();
4009 }
4010 break;
4011 }
4012 }
4013 }
4014 }
4015
4016 return strlen($data);
4017 });
4018
4019 $response = curl_exec($ch);
4020 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4021
4022 if (curl_errno($ch)) {
4023 curl_close($ch);
4024 throw new Exception('cURL Error: ' . curl_error($ch));
4025 }
4026
4027 curl_close($ch);
4028
4029 if ($http_code !== 200) {
4030 // Fallback to regular response
4031 //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
4032 $regular_response = $this->mxchat_generate_response_claude(
4033 $selected_model,
4034 $claude_api_key,
4035 array_slice($conversation_history, 0, -1), // Remove the added content
4036 $relevant_content
4037 );
4038
4039 $response_data = [
4040 'text' => $regular_response,
4041 'html' => '',
4042 'session_id' => $session_id
4043 ];
4044
4045 if ($testing_data !== null) {
4046 $response_data['testing_data'] = $testing_data;
4047 //error_log("MxChat Testing: Added testing data to Claude error fallback");
4048 }
4049
4050 header('Content-Type: application/json');
4051 echo json_encode($response_data);
4052 return true;
4053 }
4054
4055 // Save the complete response to maintain chat persistence
4056 if (!empty($full_response) && !empty($session_id)) {
4057 $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4058 }
4059
4060 return true; // Indicate streaming completed successfully
4061
4062 } catch (Exception $e) {
4063 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
4064
4065 // Fallback to regular response on exception
4066 $regular_response = $this->mxchat_generate_response_claude(
4067 $selected_model,
4068 $claude_api_key,
4069 $conversation_history,
4070 $relevant_content
4071 );
4072
4073 $response_data = [
4074 'text' => $regular_response,
4075 'html' => '',
4076 'session_id' => $session_id
4077 ];
4078
4079 if ($testing_data !== null) {
4080 $response_data['testing_data'] = $testing_data;
4081 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
4082 }
4083
4084 header('Content-Type: application/json');
4085 echo json_encode($response_data);
4086 return true;
4087 }
4088 }
4089
4090 // 3. Update OpenAI streaming function similarly
4091 private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4092 try {
4093 // Enable implicit flushing for real-time streaming
4094 ob_implicit_flush(true);
4095
4096 // Get system prompt instructions from options
4097 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4098
4099 // Ensure conversation_history is an array
4100 if (!is_array($conversation_history)) {
4101 $conversation_history = array();
4102 }
4103
4104 // Format conversation history for OpenAI
4105 $formatted_conversation = array();
4106
4107 $formatted_conversation[] = array(
4108 'role' => 'system',
4109 'content' => $system_prompt_instructions . " " . $relevant_content
4110 );
4111
4112 foreach ($conversation_history as $message) {
4113 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4114 $role = $message['role'];
4115 if ($role === 'bot' || $role === 'agent') {
4116 $role = 'assistant';
4117 }
4118 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4119 $role = 'user';
4120 }
4121 $formatted_conversation[] = array(
4122 'role' => $role,
4123 'content' => $message['content']
4124 );
4125 }
4126 }
4127
4128 // Check if we can actually stream
4129 if (headers_sent() || !function_exists('curl_init')) {
4130 // Fallback to regular response with testing data
4131 //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4132 $regular_response = $this->mxchat_generate_response_openai(
4133 $selected_model,
4134 $api_key,
4135 $conversation_history,
4136 $relevant_content
4137 );
4138
4139 $response_data = [
4140 'text' => $regular_response,
4141 'html' => '',
4142 'session_id' => $session_id
4143 ];
4144
4145 if ($testing_data !== null) {
4146 $response_data['testing_data'] = $testing_data;
4147 //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
4148 }
4149
4150 header('Content-Type: application/json');
4151 echo json_encode($response_data);
4152 return true;
4153 }
4154
4155 // Prepare the request body with stream: true
4156 $body = json_encode([
4157 'model' => $selected_model,
4158 'messages' => $formatted_conversation,
4159 'temperature' => 0.8,
4160 'stream' => true
4161 ]);
4162
4163 // Use cURL for streaming support
4164 $ch = curl_init();
4165 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4166 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4167 curl_setopt($ch, CURLOPT_POST, true);
4168 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4169 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4170 'Content-Type: application/json',
4171 'Authorization: Bearer ' . $api_key
4172 ));
4173 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4174 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4175
4176 // Add buffer control options for better streaming
4177 curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // Smaller buffer for faster streaming
4178 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
4179
4180 $full_response = ''; // Accumulate full response for saving
4181 $stream_started = false;
4182
4183 // Buffer control for real-time streaming
4184 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4185 // Flush any existing output buffers
4186 if (ob_get_level()) {
4187 ob_flush();
4188 }
4189
4190 // Send testing data as the first event if available
4191 if (!$stream_started && $testing_data !== null) {
4192 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4193 flush();
4194 if (function_exists('fastcgi_finish_request')) {
4195 fastcgi_finish_request();
4196 }
4197 $stream_started = true;
4198 //error_log("MxChat Testing: Sent testing data in OpenAI stream");
4199 }
4200
4201 // Process each chunk of data
4202 $lines = explode("\n", $data);
4203
4204 foreach ($lines as $line) {
4205 if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4206 continue;
4207 }
4208
4209 $json_str = substr($line, 6); // Remove 'data: ' prefix
4210
4211 if ($json_str === '[DONE]') {
4212 echo "data: [DONE]\n\n";
4213 flush();
4214 if (function_exists('fastcgi_finish_request')) {
4215 fastcgi_finish_request();
4216 }
4217 continue;
4218 }
4219
4220 $json = json_decode($json_str, true);
4221 if (isset($json['choices'][0]['delta']['content'])) {
4222 $content = $json['choices'][0]['delta']['content'];
4223 $full_response .= $content; // Accumulate
4224 // Send as SSE format
4225 echo "data: " . json_encode(['content' => $content]) . "\n\n";
4226 flush();
4227 if (function_exists('fastcgi_finish_request')) {
4228 fastcgi_finish_request();
4229 }
4230 }
4231 }
4232
4233 return strlen($data);
4234 });
4235
4236 $response = curl_exec($ch);
4237 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4238
4239 if (curl_errno($ch) || $http_code !== 200) {
4240 curl_close($ch);
4241
4242 // Fallback to regular response
4243 //error_log("MxChat: OpenAI streaming failed, falling back");
4244 $regular_response = $this->mxchat_generate_response_openai(
4245 $selected_model,
4246 $api_key,
4247 $conversation_history,
4248 $relevant_content
4249 );
4250
4251 $response_data = [
4252 'text' => $regular_response,
4253 'html' => '',
4254 'session_id' => $session_id
4255 ];
4256
4257 if ($testing_data !== null) {
4258 $response_data['testing_data'] = $testing_data;
4259 //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
4260 }
4261
4262 header('Content-Type: application/json');
4263 echo json_encode($response_data);
4264 return true;
4265 }
4266
4267 curl_close($ch);
4268
4269 // Save the complete response to maintain chat persistence
4270 if (!empty($full_response) && !empty($session_id)) {
4271 $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4272 }
4273
4274 return true; // Indicate streaming completed successfully
4275
4276 } catch (Exception $e) {
4277 //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4278
4279 // Fallback to regular response
4280 $regular_response = $this->mxchat_generate_response_openai(
4281 $selected_model,
4282 $api_key,
4283 $conversation_history,
4284 $relevant_content
4285 );
4286
4287 $response_data = [
4288 'text' => $regular_response,
4289 'html' => '',
4290 'session_id' => $session_id
4291 ];
4292
4293 if ($testing_data !== null) {
4294 $response_data['testing_data'] = $testing_data;
4295 //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
4296 }
4297
4298 header('Content-Type: application/json');
4299 echo json_encode($response_data);
4300 return true;
4301 }
4302 }
4303
4304 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
4305 // Get system prompt instructions from options
4306 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4307
4308 // Clean and validate conversation history
4309 foreach ($conversation_history as &$message) {
4310 // Convert bot and agent roles to assistant
4311 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
4312 $message['role'] = 'assistant';
4313 }
4314
4315 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
4316 if (!in_array($message['role'], ['assistant', 'user'])) {
4317 $message['role'] = 'user';
4318 }
4319
4320 // Ensure content field exists
4321 if (!isset($message['content']) || empty($message['content'])) {
4322 $message['content'] = '';
4323 }
4324
4325 // Remove any unsupported fields
4326 $message = array_intersect_key($message, array_flip(['role', 'content']));
4327 }
4328
4329 // Add relevant content as the latest user message
4330 $conversation_history[] = [
4331 'role' => 'user',
4332 'content' => $relevant_content
4333 ];
4334
4335 // Build request body
4336 $body = json_encode([
4337 'model' => $selected_model,
4338 'max_tokens' => 1000,
4339 'temperature' => 0.8,
4340 'messages' => $conversation_history,
4341 'system' => $system_prompt_instructions
4342 ]);
4343
4344 // Set up API request
4345 $args = [
4346 'body' => $body,
4347 'headers' => [
4348 'Content-Type' => 'application/json',
4349 'x-api-key' => $claude_api_key,
4350 'anthropic-version' => '2023-06-01'
4351 ],
4352 'timeout' => 60,
4353 'redirection' => 5,
4354 'blocking' => true,
4355 'httpversion' => '1.0',
4356 'sslverify' => true,
4357 ];
4358
4359 // Make API request
4360 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
4361
4362 // Check for WordPress errors
4363 if (is_wp_error($response)) {
4364 //error_log("Claude API request error: " . $response->get_error_message());
4365 return "Sorry, there was an error connecting to the API.";
4366 }
4367
4368 // Check HTTP response code
4369 $http_code = wp_remote_retrieve_response_code($response);
4370 if ($http_code !== 200) {
4371 $error_body = wp_remote_retrieve_body($response);
4372 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
4373
4374 // Try to extract error message from response
4375 $error_data = json_decode($error_body, true);
4376 $error_message = isset($error_data['error']['message']) ?
4377 $error_data['error']['message'] :
4378 "HTTP error " . $http_code;
4379
4380 return "Sorry, the API returned an error: " . $error_message;
4381 }
4382
4383 // Parse response
4384 $response_body = json_decode(wp_remote_retrieve_body($response), true);
4385
4386 // Check for JSON decode errors
4387 if (json_last_error() !== JSON_ERROR_NONE) {
4388 //error_log("Claude API JSON decode error: " . json_last_error_msg());
4389 return "Sorry, there was an error processing the API response.";
4390 }
4391
4392 // Extract and validate response content
4393 if (isset($response_body['content']) &&
4394 is_array($response_body['content']) &&
4395 !empty($response_body['content']) &&
4396 isset($response_body['content'][0]['text'])) {
4397 return trim($response_body['content'][0]['text']);
4398 }
4399
4400 // Log unexpected response format
4401 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
4402 return "Sorry, I received an unexpected response format from the API.";
4403 }
4404 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
4405 try {
4406 // Ensure conversation_history is an array
4407 if (!is_array($conversation_history)) {
4408 $conversation_history = array();
4409 }
4410
4411 // Get system prompt instructions from options
4412 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4413
4414 // Create a new array for the formatted conversation
4415 $formatted_conversation = array();
4416
4417 // Add system message first
4418 $formatted_conversation[] = array(
4419 'role' => 'system',
4420 'content' => $system_prompt_instructions . " " . $relevant_content
4421 );
4422
4423 // Add the rest of the conversation history
4424 foreach ($conversation_history as $message) {
4425 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4426 $role = $message['role'];
4427
4428 // Convert roles to supported format
4429 if ($role === 'bot' || $role === 'agent') {
4430 $role = 'assistant';
4431 }
4432 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4433 $role = 'user';
4434 }
4435
4436 $formatted_conversation[] = array(
4437 'role' => $role,
4438 'content' => $message['content']
4439 );
4440 }
4441 }
4442
4443 $body = json_encode([
4444 'model' => $selected_model,
4445 'messages' => $formatted_conversation,
4446 'temperature' => 0.8,
4447 'stream' => false
4448 ]);
4449
4450 $args = [
4451 'body' => $body,
4452 'headers' => [
4453 'Content-Type' => 'application/json',
4454 'Authorization' => 'Bearer ' . $api_key,
4455 ],
4456 'timeout' => 60,
4457 'redirection' => 5,
4458 'blocking' => true,
4459 'httpversion' => '1.0',
4460 'sslverify' => true,
4461 ];
4462
4463 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4464
4465 if (is_wp_error($response)) {
4466 $error_message = $response->get_error_message();
4467 //error_log('OpenAI API Error: ' . $error_message);
4468 return [
4469 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
4470 'error_code' => 'openai_connection_error',
4471 'provider' => 'openai'
4472 ];
4473 }
4474
4475 $status_code = wp_remote_retrieve_response_code($response);
4476 if ($status_code !== 200) {
4477 $response_body = wp_remote_retrieve_body($response);
4478 $decoded_response = json_decode($response_body, true);
4479
4480 $error_message = isset($decoded_response['error']['message'])
4481 ? $decoded_response['error']['message']
4482 : 'HTTP Error ' . $status_code;
4483
4484 $error_type = isset($decoded_response['error']['type'])
4485 ? $decoded_response['error']['type']
4486 : 'unknown';
4487
4488 //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4489
4490 // Handle specific error types
4491 switch ($error_type) {
4492 case 'invalid_request_error':
4493 if (strpos($error_message, 'API key') !== false) {
4494 return [
4495 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
4496 'error_code' => 'openai_invalid_api_key',
4497 'provider' => 'openai'
4498 ];
4499 }
4500 break;
4501
4502 case 'authentication_error':
4503 return [
4504 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
4505 'error_code' => 'openai_auth_error',
4506 'provider' => 'openai'
4507 ];
4508
4509 case 'rate_limit_exceeded':
4510 return [
4511 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
4512 'error_code' => 'openai_rate_limit',
4513 'provider' => 'openai'
4514 ];
4515
4516 case 'quota_exceeded':
4517 return [
4518 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
4519 'error_code' => 'openai_quota_exceeded',
4520 'provider' => 'openai'
4521 ];
4522 }
4523
4524 // Generic error fallback
4525 return [
4526 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
4527 'error_code' => 'openai_api_error',
4528 'provider' => 'openai',
4529 'status_code' => $status_code
4530 ];
4531 }
4532
4533 $response_body = wp_remote_retrieve_body($response);
4534 $decoded_response = json_decode($response_body, true);
4535
4536 if (isset($decoded_response['choices'][0]['message']['content'])) {
4537 return trim($decoded_response['choices'][0]['message']['content']);
4538 } else {
4539 //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
4540 return [
4541 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
4542 'error_code' => 'openai_response_format_error',
4543 'provider' => 'openai'
4544 ];
4545 }
4546 } catch (Exception $e) {
4547 //error_log('OpenAI Exception: ' . $e->getMessage());
4548 return [
4549 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
4550 'error_code' => 'openai_exception',
4551 'provider' => 'openai'
4552 ];
4553 }
4554 }
4555 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
4556 try {
4557 // Get system prompt instructions from options
4558 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4559
4560 // Add system prompt to relevant content
4561 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4562
4563 // Prepend system instructions to the conversation history
4564 array_unshift($conversation_history, [
4565 'role' => 'system',
4566 'content' => "Here are your instructions: " . $content_with_instructions
4567 ]);
4568
4569 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
4570 foreach ($conversation_history as &$message) {
4571 if ($message['role'] === 'bot') {
4572 $message['role'] = 'assistant';
4573 } elseif ($message['role'] === 'agent') {
4574 // Tag the message as coming from a live agent
4575 $message['role'] = 'assistant';
4576 if (!isset($message['metadata'])) {
4577 $message['metadata'] = ['source' => 'live_agent'];
4578 }
4579 }
4580
4581 // Ensure all roles are valid
4582 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
4583 $message['role'] = 'user'; // Default to 'user'
4584 }
4585 }
4586
4587 // Build the request body
4588 $body = json_encode([
4589 'model' => $selected_model,
4590 'messages' => $conversation_history,
4591 'temperature' => 0.8,
4592 'stream' => false
4593 ]);
4594
4595 // Set up the API request
4596 $args = [
4597 'body' => $body,
4598 'headers' => [
4599 'Content-Type' => 'application/json',
4600 'Authorization' => 'Bearer ' . $xai_api_key,
4601 ],
4602 'timeout' => 60,
4603 'redirection' => 5,
4604 'blocking' => true,
4605 'httpversion' => '1.0',
4606 'sslverify' => true,
4607 ];
4608
4609 // Make the API request
4610 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
4611
4612 // Process the response
4613 if (is_wp_error($response)) {
4614 $error_message = $response->get_error_message();
4615 //error_log('X.AI API Error: ' . $error_message);
4616 return [
4617 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
4618 'error_code' => 'xai_connection_error',
4619 'provider' => 'xai'
4620 ];
4621 }
4622
4623 $status_code = wp_remote_retrieve_response_code($response);
4624 if ($status_code !== 200) {
4625 $response_body = wp_remote_retrieve_body($response);
4626 $decoded_response = json_decode($response_body, true);
4627
4628 // Log the full response for debugging
4629 //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
4630
4631 // Extract error message from X.AI's specific format
4632 $error_message = '';
4633
4634 // Check for direct error string (as seen in your logs)
4635 if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
4636 $error_message = $decoded_response['error'];
4637 }
4638 // Check for nested error object (OpenAI style)
4639 elseif (isset($decoded_response['error']['message'])) {
4640 $error_message = $decoded_response['error']['message'];
4641 }
4642 // Check for top-level message
4643 elseif (isset($decoded_response['message'])) {
4644 $error_message = $decoded_response['message'];
4645 }
4646 // Fallback
4647 else {
4648 $error_message = 'HTTP Error ' . $status_code;
4649 }
4650
4651 //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4652
4653 // Check for API key errors using string matching
4654 if (stripos($error_message, 'api key') !== false ||
4655 stripos($error_message, 'incorrect api key') !== false ||
4656 stripos($error_message, 'invalid api key') !== false) {
4657 return [
4658 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
4659 'error_code' => 'xai_invalid_api_key',
4660 'provider' => 'xai'
4661 ];
4662 }
4663
4664 // Authentication errors
4665 if ($status_code === 401 || $status_code === 403 ||
4666 stripos($error_message, 'auth') !== false) {
4667 return [
4668 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
4669 'error_code' => 'xai_auth_error',
4670 'provider' => 'xai'
4671 ];
4672 }
4673
4674 // Model errors
4675 if (stripos($error_message, 'model') !== false) {
4676 return [
4677 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
4678 'error_code' => 'xai_invalid_model',
4679 'provider' => 'xai'
4680 ];
4681 }
4682
4683 // Rate limit errors
4684 if ($status_code === 429 ||
4685 stripos($error_message, 'rate') !== false ||
4686 stripos($error_message, 'limit') !== false) {
4687 return [
4688 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
4689 'error_code' => 'xai_rate_limit',
4690 'provider' => 'xai'
4691 ];
4692 }
4693
4694 // Quota errors
4695 if (stripos($error_message, 'quota') !== false ||
4696 stripos($error_message, 'billing') !== false) {
4697 return [
4698 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
4699 'error_code' => 'xai_quota_exceeded',
4700 'provider' => 'xai'
4701 ];
4702 }
4703
4704 // Server errors
4705 if ($status_code >= 500) {
4706 return [
4707 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
4708 'error_code' => 'xai_service_unavailable',
4709 'provider' => 'xai'
4710 ];
4711 }
4712
4713 // Generic error fallback with the actual error message
4714 return [
4715 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
4716 'error_code' => 'xai_api_error',
4717 'provider' => 'xai',
4718 'status_code' => $status_code
4719 ];
4720 }
4721
4722 $response_body = wp_remote_retrieve_body($response);
4723 $decoded_response = json_decode($response_body, true);
4724
4725 if (isset($decoded_response['choices'][0]['message']['content'])) {
4726 return trim($decoded_response['choices'][0]['message']['content']);
4727 } else {
4728 //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
4729 return [
4730 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
4731 'error_code' => 'xai_response_format_error',
4732 'provider' => 'xai'
4733 ];
4734 }
4735 } catch (Exception $e) {
4736 //error_log('X.AI Exception: ' . $e->getMessage());
4737 return [
4738 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
4739 'error_code' => 'xai_exception',
4740 'provider' => 'xai'
4741 ];
4742 }
4743 }
4744 private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4745 try {
4746 // Get system prompt instructions from options
4747 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4748
4749 // Ensure conversation_history is an array
4750 if (!is_array($conversation_history)) {
4751 $conversation_history = array();
4752 }
4753
4754 // Format conversation history for X.AI (same as OpenAI format)
4755 $formatted_conversation = array();
4756
4757 $formatted_conversation[] = array(
4758 'role' => 'system',
4759 'content' => $system_prompt_instructions . " " . $relevant_content
4760 );
4761
4762 foreach ($conversation_history as $message) {
4763 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4764 $role = $message['role'];
4765 if ($role === 'bot' || $role === 'agent') {
4766 $role = 'assistant';
4767 }
4768 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4769 $role = 'user';
4770 }
4771 $formatted_conversation[] = array(
4772 'role' => $role,
4773 'content' => $message['content']
4774 );
4775 }
4776 }
4777
4778 // Check if we can actually stream
4779 if (headers_sent() || !function_exists('curl_init')) {
4780 // Fallback to regular response with testing data
4781 //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
4782 $regular_response = $this->mxchat_generate_response_xai(
4783 $selected_model,
4784 $xai_api_key,
4785 $conversation_history,
4786 $relevant_content
4787 );
4788
4789 $response_data = [
4790 'text' => $regular_response,
4791 'html' => '',
4792 'session_id' => $session_id
4793 ];
4794
4795 if ($testing_data !== null) {
4796 $response_data['testing_data'] = $testing_data;
4797 //error_log("MxChat Testing: Added testing data to X.AI fallback response");
4798 }
4799
4800 header('Content-Type: application/json');
4801 echo json_encode($response_data);
4802 return true;
4803 }
4804
4805 // Prepare the request body with stream: true
4806 $body = json_encode([
4807 'model' => $selected_model,
4808 'messages' => $formatted_conversation,
4809 'temperature' => 0.8,
4810 'stream' => true
4811 ]);
4812
4813 // Use cURL for streaming support
4814 $ch = curl_init();
4815 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
4816 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4817 curl_setopt($ch, CURLOPT_POST, true);
4818 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4819 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4820 'Content-Type: application/json',
4821 'Authorization: Bearer ' . $xai_api_key
4822 ));
4823 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4824 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4825
4826 $full_response = ''; // Accumulate full response for saving
4827 $stream_started = false;
4828
4829 // Buffer control for real-time streaming
4830 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4831 // Send testing data as the first event if available
4832 if (!$stream_started && $testing_data !== null) {
4833 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4834 flush();
4835 $stream_started = true;
4836 //error_log("MxChat Testing: Sent testing data in X.AI stream");
4837 }
4838
4839 // Process each chunk of data
4840 $lines = explode("\n", $data);
4841
4842 foreach ($lines as $line) {
4843 if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4844 continue;
4845 }
4846
4847 $json_str = substr($line, 6); // Remove 'data: ' prefix
4848
4849 if ($json_str === '[DONE]') {
4850 echo "data: [DONE]\n\n";
4851 flush();
4852 continue;
4853 }
4854
4855 $json = json_decode($json_str, true);
4856 if (isset($json['choices'][0]['delta']['content'])) {
4857 $content = $json['choices'][0]['delta']['content'];
4858 $full_response .= $content; // Accumulate
4859 // Send as SSE format
4860 echo "data: " . json_encode(['content' => $content]) . "\n\n";
4861 flush();
4862 }
4863 }
4864
4865 return strlen($data);
4866 });
4867
4868 $response = curl_exec($ch);
4869 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4870
4871 if (curl_errno($ch) || $http_code !== 200) {
4872 curl_close($ch);
4873
4874 // Fallback to regular response
4875 //error_log("MxChat: X.AI streaming failed, falling back");
4876 $regular_response = $this->mxchat_generate_response_xai(
4877 $selected_model,
4878 $xai_api_key,
4879 $conversation_history,
4880 $relevant_content
4881 );
4882
4883 $response_data = [
4884 'text' => $regular_response,
4885 'html' => '',
4886 'session_id' => $session_id
4887 ];
4888
4889 if ($testing_data !== null) {
4890 $response_data['testing_data'] = $testing_data;
4891 //error_log("MxChat Testing: Added testing data to X.AI error fallback");
4892 }
4893
4894 header('Content-Type: application/json');
4895 echo json_encode($response_data);
4896 return true;
4897 }
4898
4899 curl_close($ch);
4900
4901 // Save the complete response to maintain chat persistence
4902 if (!empty($full_response) && !empty($session_id)) {
4903 $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4904 }
4905
4906 return true; // Indicate streaming completed successfully
4907
4908 } catch (Exception $e) {
4909 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
4910
4911 // Fallback to regular response
4912 $regular_response = $this->mxchat_generate_response_xai(
4913 $selected_model,
4914 $xai_api_key,
4915 $conversation_history,
4916 $relevant_content
4917 );
4918
4919 $response_data = [
4920 'text' => $regular_response,
4921 'html' => '',
4922 'session_id' => $session_id
4923 ];
4924
4925 if ($testing_data !== null) {
4926 $response_data['testing_data'] = $testing_data;
4927 //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
4928 }
4929
4930 header('Content-Type: application/json');
4931 echo json_encode($response_data);
4932 return true;
4933 }
4934 }
4935
4936 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
4937 try {
4938 // Ensure conversation_history is an array
4939 if (!is_array($conversation_history)) {
4940 $conversation_history = array();
4941 }
4942
4943 // Get system prompt instructions from options
4944 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4945
4946 // Create a new array for the formatted conversation
4947 $formatted_conversation = array();
4948
4949 // Add system message first
4950 $formatted_conversation[] = array(
4951 'role' => 'system',
4952 'content' => $system_prompt_instructions . " " . $relevant_content
4953 );
4954
4955 // Add the rest of the conversation history
4956 foreach ($conversation_history as $message) {
4957 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4958 $role = $message['role'];
4959
4960 // Convert roles to supported format
4961 if ($role === 'bot' || $role === 'agent') {
4962 $role = 'assistant';
4963 }
4964 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4965 $role = 'user';
4966 }
4967
4968 $formatted_conversation[] = array(
4969 'role' => $role,
4970 'content' => $message['content']
4971 );
4972 }
4973 }
4974
4975 $body = json_encode([
4976 'model' => $selected_model,
4977 'messages' => $formatted_conversation,
4978 'temperature' => 0.8,
4979 'stream' => false
4980 ]);
4981
4982 $args = [
4983 'body' => $body,
4984 'headers' => [
4985 'Content-Type' => 'application/json',
4986 'Authorization' => 'Bearer ' . $deepseek_api_key,
4987 ],
4988 'timeout' => 60,
4989 'redirection' => 5,
4990 'blocking' => true,
4991 'httpversion' => '1.0',
4992 'sslverify' => true,
4993 ];
4994
4995 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
4996
4997 if (is_wp_error($response)) {
4998 $error_message = $response->get_error_message();
4999 //error_log('DeepSeek API Error: ' . $error_message);
5000 return [
5001 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
5002 'error_code' => 'deepseek_connection_error',
5003 'provider' => 'deepseek'
5004 ];
5005 }
5006
5007 $status_code = wp_remote_retrieve_response_code($response);
5008 if ($status_code !== 200) {
5009 $response_body = wp_remote_retrieve_body($response);
5010 $decoded_response = json_decode($response_body, true);
5011
5012 $error_message = isset($decoded_response['error']['message'])
5013 ? $decoded_response['error']['message']
5014 : 'HTTP Error ' . $status_code;
5015
5016 $error_type = isset($decoded_response['error']['type'])
5017 ? $decoded_response['error']['type']
5018 : 'unknown';
5019
5020 //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
5021
5022 // Handle specific error types
5023 switch ($status_code) {
5024 case 401:
5025 return [
5026 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
5027 'error_code' => 'deepseek_auth_error',
5028 'provider' => 'deepseek'
5029 ];
5030
5031 case 400:
5032 if (strpos($error_message, 'API key') !== false) {
5033 return [
5034 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
5035 'error_code' => 'deepseek_invalid_api_key',
5036 'provider' => 'deepseek'
5037 ];
5038 }
5039 break;
5040
5041 case 429:
5042 if (strpos($error_message, 'quota') !== false) {
5043 return [
5044 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
5045 'error_code' => 'deepseek_quota_exceeded',
5046 'provider' => 'deepseek'
5047 ];
5048 } else {
5049 return [
5050 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
5051 'error_code' => 'deepseek_rate_limit',
5052 'provider' => 'deepseek'
5053 ];
5054 }
5055
5056 case 500:
5057 case 502:
5058 case 503:
5059 case 504:
5060 return [
5061 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
5062 'error_code' => 'deepseek_service_unavailable',
5063 'provider' => 'deepseek'
5064 ];
5065 }
5066
5067 // Generic error fallback
5068 return [
5069 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
5070 'error_code' => 'deepseek_api_error',
5071 'provider' => 'deepseek',
5072 'status_code' => $status_code
5073 ];
5074 }
5075
5076 $response_body = wp_remote_retrieve_body($response);
5077 $decoded_response = json_decode($response_body, true);
5078
5079 if (isset($decoded_response['choices'][0]['message']['content'])) {
5080 return trim($decoded_response['choices'][0]['message']['content']);
5081 } else {
5082 //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
5083 return [
5084 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
5085 'error_code' => 'deepseek_response_format_error',
5086 'provider' => 'deepseek'
5087 ];
5088 }
5089 } catch (Exception $e) {
5090 //error_log('DeepSeek Exception: ' . $e->getMessage());
5091 return [
5092 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
5093 'error_code' => 'deepseek_exception',
5094 'provider' => 'deepseek'
5095 ];
5096 }
5097 }
5098
5099 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
5100 // Get system prompt instructions from options
5101 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5102
5103 // Add system prompt to relevant content
5104 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5105
5106 // Format messages for Gemini API
5107 $formatted_messages = [];
5108
5109 // Add system message as the first user message with role prefix
5110 // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
5111 $formatted_messages[] = [
5112 'role' => 'user',
5113 'parts' => [
5114 ['text' => "[System Instructions] " . $content_with_instructions]
5115 ]
5116 ];
5117
5118 // Add model response to acknowledge system instructions
5119 $formatted_messages[] = [
5120 'role' => 'model',
5121 'parts' => [
5122 ['text' => "I understand and will follow these instructions."]
5123 ]
5124 ];
5125
5126 // Process the rest of the conversation history
5127 $current_role = null;
5128 $current_parts = [];
5129
5130 foreach ($conversation_history as $message) {
5131 // Skip the first system message as we already handled it
5132 if ($message['role'] === 'system') {
5133 continue;
5134 }
5135
5136 // Map roles to Gemini format
5137 $gemini_role = '';
5138 if ($message['role'] === 'user') {
5139 $gemini_role = 'user';
5140 } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
5141 $gemini_role = 'model';
5142 } else {
5143 // Skip unsupported roles
5144 continue;
5145 }
5146
5147 // If we have a new role, add the previous message
5148 if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
5149 $formatted_messages[] = [
5150 'role' => $current_role,
5151 'parts' => $current_parts
5152 ];
5153 $current_parts = [];
5154 }
5155
5156 // Set current role and add text to parts
5157 $current_role = $gemini_role;
5158 $current_parts[] = ['text' => $message['content']];
5159 }
5160
5161 // Add the last message if there's content
5162 if ($current_role !== null && !empty($current_parts)) {
5163 $formatted_messages[] = [
5164 'role' => $current_role,
5165 'parts' => $current_parts
5166 ];
5167 }
5168
5169 // Build the request body
5170 $body = json_encode([
5171 'contents' => $formatted_messages,
5172 'generationConfig' => [
5173 'temperature' => 0.7,
5174 'topP' => 0.95,
5175 'topK' => 40,
5176 'maxOutputTokens' => 8192,
5177 ],
5178 'safetySettings' => [
5179 [
5180 'category' => 'HARM_CATEGORY_HARASSMENT',
5181 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5182 ],
5183 [
5184 'category' => 'HARM_CATEGORY_HATE_SPEECH',
5185 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5186 ],
5187 [
5188 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
5189 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5190 ],
5191 [
5192 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
5193 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5194 ]
5195 ]
5196 ]);
5197
5198 // Prepare the API endpoint
5199 $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5200
5201 // Set up the API request
5202 $args = [
5203 'body' => $body,
5204 'headers' => [
5205 'Content-Type' => 'application/json',
5206 ],
5207 'timeout' => 60,
5208 'redirection' => 5,
5209 'blocking' => true,
5210 'httpversion' => '1.0',
5211 'sslverify' => true,
5212 ];
5213
5214 // Make the API request
5215 $response = wp_remote_post($api_endpoint, $args);
5216
5217 // Process the response
5218 if (is_wp_error($response)) {
5219 return "Sorry, there was an error processing your request: " . $response->get_error_message();
5220 }
5221
5222 $response_body = json_decode(wp_remote_retrieve_body($response), true);
5223
5224 // Handle potential errors in the response
5225 if (isset($response_body['error'])) {
5226 //error_log('Gemini API Error: ' . json_encode($response_body['error']));
5227 return "Sorry, there was an error with the Gemini API: " .
5228 (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
5229 }
5230
5231 // Extract the response text
5232 if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
5233 return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
5234 } else {
5235 //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
5236 return "Sorry, I couldn't process that request. The response format was unexpected.";
5237 }
5238 }
5239
5240
5241
5242 public function mxchat_dismiss_pre_chat_message() {
5243 // Get and sanitize the user identifier
5244 $user_id = $this->mxchat_get_user_identifier();
5245 $user_id = sanitize_key($user_id);
5246
5247 // Set a transient to track that the user has dismissed the pre-chat message
5248 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
5249 set_transient($transient_key, true, DAY_IN_SECONDS);
5250
5251 wp_send_json_success();
5252 }
5253
5254 public function mxchat_check_pre_chat_message_status() {
5255 // Get and sanitize the user identifier
5256 $user_id = $this->mxchat_get_user_identifier();
5257 $user_id = sanitize_key($user_id);
5258
5259 // Check if the transient exists (i.e., if the message was dismissed)
5260 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
5261 $dismissed = get_transient($transient_key);
5262
5263 // Log the result to see if it's being set correctly
5264 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
5265
5266 if ($dismissed) {
5267 wp_send_json_success(['dismissed' => true]);
5268 } else {
5269 wp_send_json_success(['dismissed' => false]);
5270 }
5271
5272 wp_die();
5273 }
5274
5275 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
5276 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
5277 return 0;
5278 }
5279
5280 $dotProduct = array_sum(array_map(function ($a, $b) {
5281 return $a * $b;
5282 }, $vectorA, $vectorB));
5283 $normA = sqrt(array_sum(array_map(function ($a) {
5284 return $a * $a;
5285 }, $vectorA)));
5286 $normB = sqrt(array_sum(array_map(function ($b) {
5287 return $b * $b;
5288 }, $vectorB)));
5289
5290 if ($normA == 0 || $normB == 0) {
5291 return 0;
5292 }
5293
5294 return $dotProduct / ($normA * $normB);
5295 }
5296
5297 public function mxchat_enqueue_scripts_styles() {
5298 // Define version numbers for the styles and scripts
5299 $chat_style_version = '2.3.3';
5300 $chat_script_version = '2.3.3';
5301 // Enqueue the script
5302 wp_enqueue_script(
5303 'mxchat-chat-js',
5304 plugin_dir_url(__FILE__) . '../js/chat-script.js',
5305 array('jquery'),
5306 $chat_script_version,
5307 true
5308 );
5309 // Enqueue the CSS
5310 wp_enqueue_style(
5311 'mxchat-chat-css',
5312 plugin_dir_url(__FILE__) . '../css/chat-style.css',
5313 array(),
5314 $chat_style_version
5315 );
5316 // Fetch options from the database
5317 $this->options = get_option('mxchat_options');
5318 $prompts_options = get_option('mxchat_prompts_options', array());
5319
5320 // Prepare settings for JavaScript
5321 $style_settings = array(
5322 'ajax_url' => admin_url('admin-ajax.php'),
5323 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
5324 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
5325 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
5326 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off', // ADD THIS LINE
5327 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
5328 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
5329 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
5330 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
5331 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
5332 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
5333 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
5334 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
5335 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
5336 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
5337 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
5338 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
5339 'icon_color' => $this->options['icon_color'] ?? '#fff',
5340 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
5341 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
5342 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
5343 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
5344 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
5345 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
5346 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
5347 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
5348 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
5349 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
5350 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
5351 );
5352 // Pass the settings to the script
5353 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
5354 }
5355
5356
5357 /**
5358 * Setup the cron jobs for rate limits with guard against multiple calls
5359 */
5360 public function setup_rate_limit_cron_jobs() {
5361 // Add a guard to prevent multiple rapid calls
5362 $last_setup = get_transient('mxchat_cron_setup_guard');
5363 if ($last_setup && (time() - $last_setup) < 60) {
5364 // Don't run again if we ran less than 60 seconds ago
5365 return;
5366 }
5367
5368 // Set the guard
5369 set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
5370
5371 try {
5372 // First, check if WordPress cron is disabled
5373 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
5374 error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
5375 $this->setup_fallback_rate_limit_system();
5376 return;
5377 }
5378
5379 // Check if cron is already scheduled - if so, don't mess with it
5380 if (wp_next_scheduled('mxchat_reset_rate_limits')) {
5381 error_log('MxChat: Rate limit cron already scheduled, skipping setup');
5382 return;
5383 }
5384
5385 // Clear any orphaned hooks (but don't loop indefinitely)
5386 $hooks_to_clear = [
5387 'mxchat_reset_rate_limits',
5388 'mxchat_reset_hourly_rate_limits',
5389 'mxchat_reset_daily_rate_limits',
5390 'mxchat_reset_weekly_rate_limits',
5391 'mxchat_reset_monthly_rate_limits'
5392 ];
5393
5394 foreach ($hooks_to_clear as $hook) {
5395 // Only clear a maximum of 3 instances to prevent infinite loops
5396 $cleared = 0;
5397 while (wp_next_scheduled($hook) && $cleared < 3) {
5398 wp_clear_scheduled_hook($hook);
5399 $cleared++;
5400 }
5401 }
5402
5403 // Small delay after clearing
5404 usleep(100000); // 0.1 seconds
5405
5406 // Try to schedule the event
5407 $initial_time = time() + 300; // Start in 5 minutes
5408 $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
5409
5410 if ($result === false) {
5411 error_log('MxChat: Failed to schedule cron, using fallback system');
5412 $this->setup_fallback_rate_limit_system();
5413 } else {
5414 error_log('MxChat: Successfully scheduled rate limit reset cron');
5415 }
5416
5417 } catch (Exception $e) {
5418 error_log('MxChat: Cron setup exception: ' . $e->getMessage());
5419 $this->setup_fallback_rate_limit_system();
5420 }
5421 }
5422
5423 /**
5424 * Try alternative cron scheduling methods
5425 */
5426 private function try_alternative_cron_scheduling($initial_time) {
5427 try {
5428 // Method 1: Try with current time instead of future time
5429 $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
5430 if ($result1 !== false) {
5431 error_log('MxChat: Alternative method 1 (current time) succeeded');
5432 return true;
5433 }
5434
5435 // Method 2: Try with a different interval
5436 $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
5437 if ($result2 !== false) {
5438 error_log('MxChat: Alternative method 2 (daily interval) succeeded');
5439 return true;
5440 }
5441
5442 // Method 3: Try wp_schedule_single_event first, then recurring
5443 $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
5444 if ($result3 !== false) {
5445 error_log('MxChat: Alternative method 3 (single event) succeeded');
5446 // Schedule the next one manually in the handler
5447 return true;
5448 }
5449
5450 return false;
5451
5452 } catch (Exception $e) {
5453 error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
5454 return false;
5455 }
5456 }
5457
5458 /**
5459 * Enhanced fallback rate limit system
5460 */
5461 private function setup_fallback_rate_limit_system() {
5462 // Set a flag to use database-based rate limit cleanup
5463 update_option('mxchat_use_fallback_rate_limits', true);
5464
5465 // Schedule a one-time check to happen on the next plugin load
5466 update_option('mxchat_next_rate_limit_check', time() + 3600);
5467
5468 // Also set up a more frequent fallback check (every 4 hours)
5469 update_option('mxchat_fallback_check_interval', 4 * 3600);
5470
5471 error_log('MxChat: Fallback rate limit system activated');
5472 }
5473
5474 /**
5475 * Enhanced fallback check method
5476 */
5477 public function check_fallback_rate_limits() {
5478 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5479
5480 if (!$use_fallback) {
5481 return; // Regular cron is working
5482 }
5483
5484 $next_check = get_option('mxchat_next_rate_limit_check', 0);
5485 $check_interval = get_option('mxchat_fallback_check_interval', 3600);
5486
5487 if (time() >= $next_check) {
5488 error_log('MxChat: Running fallback rate limit cleanup');
5489 $this->mxchat_reset_rate_limits();
5490
5491 // Schedule next check
5492 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
5493 }
5494 }
5495 /**
5496 * Enhanced rate limit check that includes fallback cleanup
5497 */
5498 public function check_rate_limit() {
5499 // Check if we need to run fallback cleanup
5500 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5501 $next_check = get_option('mxchat_next_rate_limit_check', 0);
5502
5503 if ($use_fallback && time() >= $next_check) {
5504 $this->mxchat_reset_rate_limits();
5505 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
5506 }
5507
5508 // Continue with your existing rate limit logic...
5509 $all_options = get_option('mxchat_options', []);
5510
5511 // Determine user role or if logged out
5512 if (is_user_logged_in()) {
5513 $user = wp_get_current_user();
5514 $user_id = $user->ID;
5515
5516 // Get the user's primary role using reset() to safely get the first element
5517 $user_roles = $user->roles;
5518
5519 // Safely get the first role regardless of array key structure
5520 if (!empty($user_roles) && is_array($user_roles)) {
5521 $role = reset($user_roles); // This safely gets the first element regardless of key
5522 } else {
5523 $role = 'subscriber'; // Default to subscriber if no role found
5524 }
5525 } else {
5526 $role = 'logged_out';
5527 // Use IP address for non-logged-in users
5528 $user_id = $this->get_client_ip();
5529 }
5530
5531 // Check if rate limits are configured for this role
5532 if (!isset($all_options['rate_limits'][$role])) {
5533 return true; // No limit set for this role
5534 }
5535
5536 $limit = $all_options['rate_limits'][$role]['limit'];
5537
5538 // If unlimited, return true immediately
5539 if ($limit === 'unlimited') {
5540 return true;
5541 }
5542
5543 // Get the option name for this user/role with safer naming
5544 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
5545 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
5546 $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
5547
5548 // Get the counter data
5549 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
5550
5551 // If first request or counter reset needed, set the initial timestamp
5552 if ($limit_data['count'] === 0) {
5553 $limit_data['timestamp'] = time();
5554 update_option($option_name, $limit_data);
5555 }
5556
5557 // Get the timeframe
5558 $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
5559 $all_options['rate_limits'][$role]['timeframe'] : 'daily';
5560
5561 // Check if the counter needs to be reset based on timeframe
5562 $current_time = time();
5563 $timestamp = $limit_data['timestamp'];
5564 $should_reset = false;
5565
5566 switch ($timeframe) {
5567 case 'hourly':
5568 $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
5569 break;
5570 case 'daily':
5571 $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
5572 break;
5573 case 'weekly':
5574 $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
5575 break;
5576 case 'monthly':
5577 $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
5578 break;
5579 }
5580
5581 // Reset the counter if the timeframe has passed
5582 if ($should_reset) {
5583 $limit_data = ['count' => 0, 'timestamp' => $current_time];
5584 update_option($option_name, $limit_data);
5585 }
5586
5587 // Check if user has exceeded their limit
5588 if ($limit_data['count'] >= intval($limit)) {
5589 // Get the custom message for this role
5590 $message = !empty($all_options['rate_limits'][$role]['message'])
5591 ? $all_options['rate_limits'][$role]['message']
5592 : __('Rate limit exceeded. Please try again later.', 'mxchat');
5593
5594 // Add timeframe information to the message if placeholders exist
5595 $timeframe_label = '';
5596 switch ($timeframe) {
5597 case 'hourly':
5598 $timeframe_label = __('hour', 'mxchat');
5599 break;
5600 case 'daily':
5601 $timeframe_label = __('day', 'mxchat');
5602 break;
5603 case 'weekly':
5604 $timeframe_label = __('week', 'mxchat');
5605 break;
5606 case 'monthly':
5607 $timeframe_label = __('month', 'mxchat');
5608 break;
5609 }
5610
5611 // Replace placeholders in the message
5612 $message = str_replace(
5613 ['{limit}', '{count}', '{remaining}', '{timeframe}'],
5614 [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
5615 $message
5616 );
5617
5618 // Process HTML links in the message
5619 $message = $this->process_rate_limit_message_html($message);
5620
5621 // Return error with the processed message
5622 return [
5623 'error' => true,
5624 'message' => $message
5625 ];
5626 }
5627
5628 // Increment the counter
5629 $limit_data['count']++;
5630 update_option($option_name, $limit_data);
5631
5632 return true;
5633 }
5634
5635 /**
5636 * Enhanced rate limit reset with better error handling
5637 */
5638 public function mxchat_reset_rate_limits() {
5639 try {
5640 global $wpdb;
5641 $all_options = get_option('mxchat_options', []);
5642 $current_time = time();
5643
5644 // Get rate limit options with a safer query and limit
5645 $option_names = $wpdb->get_col(
5646 $wpdb->prepare(
5647 "SELECT option_name FROM {$wpdb->options}
5648 WHERE option_name LIKE %s
5649 LIMIT 1000",
5650 'mxchat_chat_limit_%'
5651 )
5652 );
5653
5654 if (empty($option_names)) {
5655 return;
5656 }
5657
5658 $processed_count = 0;
5659 $max_processing_time = 30; // Maximum 30 seconds
5660 $start_time = time();
5661
5662 foreach ($option_names as $option_name) {
5663 // Check processing time limit
5664 if ((time() - $start_time) > $max_processing_time) {
5665 error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
5666 break;
5667 }
5668
5669 // Parse the option name more safely
5670 if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
5671 continue;
5672 }
5673
5674 $role_and_user = $matches[1] . '_' . $matches[2];
5675 $parts = explode('_', $role_and_user);
5676
5677 if (count($parts) < 2) {
5678 continue;
5679 }
5680
5681 // Extract role (everything except the last part which is user ID)
5682 $user_id_part = array_pop($parts);
5683 $role = implode('_', $parts);
5684
5685 // Skip if role doesn't exist in our settings
5686 if (!isset($all_options['rate_limits'][$role])) {
5687 // Clean up orphaned entries
5688 delete_option($option_name);
5689 continue;
5690 }
5691
5692 $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
5693 $limit_data = get_option($option_name);
5694
5695 if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
5696 // Clean up invalid entries
5697 delete_option($option_name);
5698 continue;
5699 }
5700
5701 $timestamp = $limit_data['timestamp'];
5702 $should_reset = false;
5703
5704 // Determine if we should reset based on the timeframe
5705 switch ($timeframe) {
5706 case 'hourly':
5707 $should_reset = ($current_time - $timestamp) >= 3600;
5708 break;
5709 case 'daily':
5710 $should_reset = ($current_time - $timestamp) >= 86400;
5711 break;
5712 case 'weekly':
5713 $should_reset = ($current_time - $timestamp) >= 604800;
5714 break;
5715 case 'monthly':
5716 $should_reset = ($current_time - $timestamp) >= 2592000;
5717 break;
5718 }
5719
5720 // Reset the counter if the timeframe has passed
5721 if ($should_reset) {
5722 delete_option($option_name);
5723 wp_cache_delete($option_name, 'options');
5724 $processed_count++;
5725 }
5726 }
5727
5728 // Clean up any orphaned cache entries
5729 wp_cache_delete('mxchat_all_chat_limits', 'options');
5730
5731 error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
5732
5733 } catch (Exception $e) {
5734 error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
5735 }
5736 }
5737
5738
5739 /**
5740 * Process HTML links in rate limit messages
5741 *
5742 * @param string $message The rate limit message
5743 * @return string The processed message with safe HTML links
5744 */
5745 private function process_rate_limit_message_html($message) {
5746 // Return original message if empty
5747 if (empty($message)) {
5748 return $message;
5749 }
5750
5751 // First, convert markdown links to HTML
5752 $message = $this->convert_markdown_links($message);
5753
5754 // Then, auto-convert any remaining plain URLs to links
5755 $message = $this->auto_link_urls($message);
5756
5757 // Allow basic HTML tags for links and formatting
5758 $allowed_tags = [
5759 'a' => [
5760 'href' => true,
5761 'target' => true,
5762 'rel' => true,
5763 'title' => true,
5764 'class' => true
5765 ],
5766 'strong' => [],
5767 'em' => [],
5768 'br' => [],
5769 'b' => [],
5770 'i' => [],
5771 'span' => ['class' => true]
5772 ];
5773
5774 // Sanitize but allow the specified HTML tags
5775 $processed_message = wp_kses($message, $allowed_tags);
5776
5777 // If wp_kses stripped everything, return the original message as plain text
5778 if (empty($processed_message) && !empty($message)) {
5779 // Strip all HTML and return plain text as fallback
5780 return wp_strip_all_tags($message);
5781 }
5782
5783 return $processed_message;
5784 }
5785
5786 /**
5787 * Convert markdown links to HTML
5788 *
5789 * @param string $text The text to process
5790 * @return string The text with markdown links converted to HTML
5791 */
5792 private function convert_markdown_links($text) {
5793 // Return original text if empty
5794 if (empty($text)) {
5795 return $text;
5796 }
5797
5798 // Pattern to match markdown links: [text](url)
5799 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
5800
5801 $processed_text = preg_replace_callback($pattern, function($matches) {
5802 $link_text = $matches[1];
5803 $url = $matches[2];
5804
5805 // Clean up any trailing punctuation from the URL
5806 $url = rtrim($url, '.,;:!?');
5807
5808 // Sanitize the link text and URL
5809 $safe_text = esc_html($link_text);
5810 $safe_url = esc_url($url);
5811
5812 // Create the HTML link
5813 return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
5814 }, $text);
5815
5816 // If preg_replace_callback failed, return original text
5817 if ($processed_text === null) {
5818 return $text;
5819 }
5820
5821 return $processed_text;
5822 }
5823
5824 /**
5825 * Auto-convert plain URLs to clickable links
5826 *
5827 * @param string $text The text to process
5828 * @return string The text with URLs converted to links
5829 */
5830 private function auto_link_urls($text) {
5831 // Return original text if empty
5832 if (empty($text)) {
5833 return $text;
5834 }
5835
5836 // Simple pattern that avoids complex lookbehinds
5837 // This will match URLs that are not already inside href attributes or markdown links
5838 $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
5839
5840 $processed_text = preg_replace_callback($pattern, function($matches) {
5841 $url = $matches[0];
5842 // Clean up any trailing punctuation that might have been captured
5843 $url = rtrim($url, '.,;:!?');
5844
5845 // Add target="_blank" and rel="noopener noreferrer" for security
5846 return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
5847 }, $text);
5848
5849 // If preg_replace_callback failed, return original text
5850 if ($processed_text === null) {
5851 return $text;
5852 }
5853
5854 return $processed_text;
5855 }
5856
5857
5858 // Helper function to get client IP address
5859 private function get_client_ip() {
5860 // Check for shared internet/ISP IP
5861 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
5862 return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
5863 }
5864
5865 // Check for IPs passing through proxies
5866 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
5867 // Use the first value in the comma-separated list
5868 $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
5869 return trim($forwarded_for[0]);
5870 }
5871
5872 if (!empty($_SERVER['REMOTE_ADDR'])) {
5873 return sanitize_text_field($_SERVER['REMOTE_ADDR']);
5874 }
5875
5876 // Fallback
5877 return 'unknown';
5878 }
5879
5880 /**
5881 * AJAX handler to get system information for testing panel
5882 */
5883 public function mxchat_get_system_info() {
5884 // Verify nonce for security
5885 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5886 wp_send_json_error(['message' => 'Invalid nonce']);
5887 return;
5888 }
5889
5890 // Only allow admin users
5891 if (!current_user_can('administrator')) {
5892 wp_send_json_error(['message' => 'Unauthorized']);
5893 return;
5894 }
5895
5896 // Get system prompt from options
5897 $system_prompt = isset($this->options['system_prompt_instructions'])
5898 ? $this->options['system_prompt_instructions']
5899 : 'No system prompt configured';
5900
5901 // Get selected model
5902 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
5903
5904 // Get API key status (just check if they exist, don't expose the keys)
5905 $api_status = [];
5906 $api_status['openai'] = !empty($this->options['api_key']);
5907 $api_status['claude'] = !empty($this->options['claude_api_key']);
5908 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
5909 $api_status['xai'] = !empty($this->options['xai_api_key']);
5910 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
5911
5912 wp_send_json_success([
5913 'system_prompt' => $system_prompt,
5914 'selected_model' => $selected_model,
5915 'api_status' => $api_status
5916 ]);
5917 }
5918
5919 /**
5920 * AJAX handler to get similarity threshold
5921 */
5922 public function mxchat_get_similarity_threshold() {
5923 // Verify nonce for security
5924 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5925 wp_send_json_error(['message' => 'Invalid nonce']);
5926 return;
5927 }
5928
5929 // Only allow admin users
5930 if (!current_user_can('administrator')) {
5931 wp_send_json_error(['message' => 'Unauthorized']);
5932 return;
5933 }
5934
5935 // Get similarity threshold from main options (default 75%)
5936 $similarity_threshold = isset($this->options['similarity_threshold'])
5937 ? ((int) $this->options['similarity_threshold']) / 100
5938 : 0.75;
5939
5940 wp_send_json_success([
5941 'threshold' => $similarity_threshold,
5942 'threshold_percentage' => ($similarity_threshold * 100) . '%'
5943 ]);
5944 }
5945
5946 /**
5947 * AJAX handler to get knowledge base status
5948 */
5949 public function mxchat_get_kb_status() {
5950 // Verify nonce for security
5951 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5952 wp_send_json_error(['message' => 'Invalid nonce']);
5953 return;
5954 }
5955
5956 // Only allow admin users
5957 if (!current_user_can('administrator')) {
5958 wp_send_json_error(['message' => 'Unauthorized']);
5959 return;
5960 }
5961
5962 // Check Pinecone vs WordPress
5963 $addon_options = get_option('mxchat_pinecone_addon_options', array());
5964 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
5965
5966 $kb_info = [
5967 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
5968 'status' => 'Active'
5969 ];
5970
5971 // Get document count
5972 if ($use_pinecone) {
5973 $kb_info['documents'] = 'Connected to Pinecone';
5974 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
5975 } else {
5976 // Count documents in WordPress database
5977 global $wpdb;
5978 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5979 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
5980 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
5981 }
5982
5983 wp_send_json_success($kb_info);
5984 }
5985
5986 /**
5987 * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
5988 */
5989 public function mxchat_start_fresh_session() {
5990 // Verify nonce for security
5991 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
5992 wp_send_json_error(['message' => 'Invalid nonce']);
5993 return;
5994 }
5995
5996 // Only allow admin users
5997 if (!current_user_can('administrator')) {
5998 wp_send_json_error(['message' => 'Unauthorized']);
5999 return;
6000 }
6001
6002 $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
6003 $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
6004
6005 if (empty($old_session_id)) {
6006 wp_send_json_error(['message' => 'Old session ID required']);
6007 return;
6008 }
6009
6010 // If no new session ID provided, generate one
6011 if (empty($new_session_id)) {
6012 $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
6013 }
6014
6015 // Clear ALL data associated with the old session
6016 $this->clear_complete_session_data($old_session_id);
6017
6018 // Initialize the new session
6019 $this->initialize_fresh_session($new_session_id);
6020
6021 wp_send_json_success([
6022 'message' => 'Fresh session started successfully',
6023 'new_session_id' => $new_session_id,
6024 'old_session_id' => $old_session_id
6025 ]);
6026 }
6027
6028 /**
6029 * Clear ALL data associated with a session (ENHANCED)
6030 */
6031 private function clear_complete_session_data($session_id) {
6032 // Clear chat history
6033 delete_option("mxchat_history_{$session_id}");
6034
6035 // Clear chat mode
6036 delete_option("mxchat_mode_{$session_id}");
6037
6038 // Clear any PDF/Word transients
6039 $this->clear_pdf_transients($session_id);
6040 if (method_exists($this, 'clear_word_transients')) {
6041 $this->clear_word_transients($session_id);
6042 }
6043
6044 // Clear agent-related data
6045 delete_option("mxchat_channel_{$session_id}");
6046 delete_option("mxchat_agent_name_{$session_id}");
6047 delete_option("mxchat_email_{$session_id}");
6048
6049 // Clear any recommendation flow state
6050 delete_option("mxchat_sr_flow_state_{$session_id}");
6051
6052 // Clear any cached embeddings or context
6053 delete_transient("mxchat_context_{$session_id}");
6054 delete_transient("mxchat_last_query_{$session_id}");
6055
6056 // Clear any testing data
6057 delete_transient("mxchat_testing_data_{$session_id}");
6058
6059 // Clear any rate limiting data for this session
6060 delete_transient("mxchat_rate_limit_{$session_id}");
6061
6062 // Clear any other session-specific transients
6063 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
6064 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
6065 delete_transient("mxchat_include_word_in_context_{$session_id}");
6066
6067 //error_log("MxChat: Cleared all data for session: {$session_id}");
6068 }
6069
6070 /**
6071 * Initialize a fresh session with default data
6072 */
6073 private function initialize_fresh_session($session_id) {
6074 // Set default chat mode
6075 update_option("mxchat_mode_{$session_id}", 'ai');
6076
6077 //error_log("MxChat: Initialized fresh session: {$session_id}");
6078 }
6079
6080 /**
6081 * Helper method to clear Word document transients (if you have Word support)
6082 */
6083 private function clear_word_transients($session_id) {
6084 delete_transient('mxchat_word_url_' . $session_id);
6085 delete_transient('mxchat_word_filename_' . $session_id);
6086 delete_transient('mxchat_word_embeddings_' . $session_id);
6087 delete_transient('mxchat_include_word_in_context_' . $session_id);
6088 }
6089
6090 /**
6091 * Simplified testing data capture method (CLEANED UP)
6092 */
6093 private function capture_testing_data($user_embedding, $message, $session_id) {
6094 // Only capture for admin users
6095 if (!current_user_can('administrator')) {
6096 return null;
6097 }
6098
6099 $testing_data = [
6100 'query' => $message,
6101 'timestamp' => time(),
6102 'top_matches' => [],
6103 'action_matches' => [] // NEW: Add action matches
6104 ];
6105
6106 // Get similarity threshold
6107 $similarity_threshold = isset($this->options['similarity_threshold'])
6108 ? ((int) $this->options['similarity_threshold']) / 100
6109 : 0.75;
6110
6111 $testing_data['similarity_threshold'] = $similarity_threshold;
6112
6113 // Use the real similarity analysis if available
6114 if ($this->last_similarity_analysis !== null) {
6115 $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
6116 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
6117 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6118 } else {
6119 // Fallback: determine knowledge base type
6120 $addon_options = get_option('mxchat_pinecone_addon_options', array());
6121 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
6122
6123 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
6124 }
6125
6126 // NEW: Include action analysis if available
6127 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
6128 $testing_data['action_matches'] = $this->last_action_analysis;
6129
6130 // Clear it after capturing to avoid stale data
6131 $this->last_action_analysis = null;
6132 }
6133
6134 return $testing_data;
6135 }
6136
6137
6138 }
6139 ?>
6140