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

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