PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.4
MxChat – AI Chatbot & Content Generation for WordPress v2.3.4
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / includes / class-mxchat-integrator.php

class-mxchat-integrator.php in MxChat – AI Chatbot & Content Generation for WordPress 2.3.4, at includes/class-mxchat-integrator.php

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