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

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

8,835 lines 351.7 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 private $current_valid_urls = [];
15 private $is_streaming = false; // ADDED: Track if current request is streaming
16 private $streaming_headers_sent = false; // Track if streaming headers have been sent
17
18 /**
19 * Setup streaming headers - call this right before actually streaming
20 * This delays header setup to allow actions/forms to return JSON responses
21 */
22 private function setup_streaming_headers() {
23 if ($this->streaming_headers_sent || headers_sent()) {
24 return false;
25 }
26
27 // Disable output buffering
28 while (ob_get_level()) {
29 ob_end_flush();
30 }
31
32 // Set headers for SSE
33 header('Content-Type: text/event-stream');
34 header('Cache-Control: no-cache');
35 header('Connection: keep-alive');
36 header('X-Accel-Buffering: no');
37
38 ob_implicit_flush(true);
39 flush();
40
41 $this->streaming_headers_sent = true;
42 return true;
43 }
44
45 /**
46 * Class constructor
47 */
48 public function __construct() {
49 $this->options = get_option('mxchat_options');
50 $this->prompts_options = get_option('mxchat_prompts_options', array());
51 $this->chat_count = get_option('mxchat_chat_count', 0);
52 $this->word_handler = new MXChat_Word_Handler($this->options);
53
54 // Add all action hooks
55 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
56 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
57 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
58 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
59 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
60
61 // Add the AJAX actions for checking if the pre-chat message was dismissed
62 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
63 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
64 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
65 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
66 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
67 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
68
69 // Add REST API routes registration
70 add_action('rest_api_init', array($this, 'register_routes'));
71 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
72 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
73
74 // Rate limit action - notice we removed the old schedule setup
75 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
76
77 // File upload and handling actions
78 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
79 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
80 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
81 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
82
83 // Word document handling actions
84 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
85 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
86 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
87 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
88 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
89 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
90
91 // Email handling actions
92 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
93 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
94 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
95 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
96
97 add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
98 add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
99
100 // Testing panel AJAX actions
101 add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
102 add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
103 add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
104 add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
105 // Add to your existing constructor, in the section with other AJAX actions:
106 add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
107 add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
108 add_action('wp_ajax_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
109 add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
110 // Add chat mode checking actions
111 add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
112 add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
113
114 // Auto-email transcript action
115 add_action('mxchat_send_delayed_transcript', array($this, 'mxchat_send_delayed_transcript'), 10, 1);
116
117 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
118
119
120 }
121
122 // In your core plugin's check_actions_for_addons method:
123 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
124 //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
125
126 $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
127
128 //error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
129
130 return $result;
131 }
132
133 private function mxchat_increment_chat_count() {
134 $chat_count = get_option('mxchat_chat_count', 0);
135 $chat_count++;
136 update_option('mxchat_chat_count', $chat_count);
137 }
138
139 function mxchat_fetch_conversation_history() {
140 if (empty($_POST['session_id'])) {
141 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
142 wp_die();
143 }
144
145 $session_id = sanitize_text_field($_POST['session_id']);
146
147 // SECURITY FIX: Verify session ownership before retrieving data
148 // If IP/user changed, signal frontend to reset session instead of blocking
149 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
150
151 // Check if this session has an owner recorded
152 $session_owner = get_option("mxchat_session_owner_{$session_id}");
153
154 // If session has an owner and it doesn't match current user, trigger session reset
155 if ($session_owner && $session_owner !== $current_user_identifier) {
156 wp_send_json_error([
157 'message' => esc_html__('Your session has expired. Starting a new conversation.', 'mxchat'),
158 'code' => 'session_expired',
159 'action' => 'reset_session'
160 ]);
161 wp_die();
162 }
163
164 // If no owner is set yet, claim ownership (for legacy sessions)
165 if (!$session_owner) {
166 update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
167 }
168
169 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
170 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
171
172 if (empty($history)) {
173 // Even if history is empty, return the chat mode
174 wp_send_json_success([
175 'conversation' => [],
176 'chat_mode' => $chat_mode
177 ]);
178 wp_die();
179 }
180
181 wp_send_json_success([
182 'conversation' => $history,
183 'chat_mode' => $chat_mode
184 ]);
185 wp_die();
186 }
187 private function mxchat_fetch_conversation_history_for_ai($session_id) {
188 $history = get_option("mxchat_history_{$session_id}", []);
189 $formatted_history = [];
190
191 // Adjusted for code-heavy conversations
192 $max_tokens = 120000; // Context window size
193 $reserved_tokens = 5000; // Space for system prompts + current query
194 $current_token_count = 0;
195
196 // Allowed HTML tags for content sanitization
197 $allowed_tags = [
198 'pre' => ['class' => true],
199 'code' => ['class' => true],
200 'span' => ['class' => true],
201 'div' => ['class' => true],
202 'strong' => [],
203 'em' => []
204 ];
205
206 foreach (array_reverse($history) as $entry) {
207 // Preserve code blocks while sanitizing other HTML
208 $clean_content = wp_kses($entry['content'], $allowed_tags);
209
210 // Detect code blocks in content
211 $has_code = false;
212 // Replace the HTML check with:
213 // Allow messages that contain code blocks or are plain text
214 if (strpos($clean_content, '<pre') === false &&
215 strpos($clean_content, '<code') === false &&
216 $clean_content !== strip_tags($entry['content'])) {
217 continue;
218 }
219
220 // Skip entries that lost significant content during sanitization
221 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
222 continue;
223 }
224
225 // More accurate token estimation (1 token ≈ 4 characters)
226 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
227
228 // Check token budget with the new estimate
229 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
230 // Try to fit partial content if it's the first entry
231 if (empty($formatted_history)) {
232 $clean_content = mb_substr($clean_content, 0, ($max_tokens - $reserved_tokens) * 4);
233 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
234 } else {
235 break;
236 }
237 }
238
239 // Add to formatted history
240 $formatted_history[] = [
241 'role' => $entry['role'],
242 'content' => $clean_content
243 ];
244
245 $current_token_count += $token_estimate;
246 }
247
248 // Reverse back to maintain chronological order
249 $formatted_history = array_reverse($formatted_history);
250
251 // Add system message about code context
252 array_unshift($formatted_history, [
253 'role' => 'system',
254 'content' => 'Preserved code blocks are marked with [CODE BLOCK PRESERVED]. '
255 . 'Maintain formatting and syntax highlighting when referencing code.'
256 ]);
257
258 return $formatted_history;
259 }
260
261 public function register_routes() {
262 //error_log(esc_html__('Registering MxChat REST routes', 'mxchat'));
263
264 register_rest_route('mxchat/v1', '/stream', [
265 'methods' => 'GET',
266 'callback' => [$this, 'mxchat_stream_events'],
267 'permission_callback' => [$this, 'verify_chat_session'],
268 ]);
269
270 register_rest_route('mxchat/v1', '/agent-response', [
271 'methods' => 'POST',
272 'callback' => [$this, 'mxchat_handle_agent_response'],
273 'permission_callback' => [$this, 'verify_slack_request'],
274 ]);
275
276 register_rest_route('mxchat/v1', '/slack-interaction', [
277 'methods' => 'POST',
278 'callback' => [$this, 'handle_slack_interaction'],
279 'permission_callback' => [$this, 'verify_slack_request'],
280 ]);
281
282 register_rest_route('mxchat/v1', '/slack-messages', [
283 'methods' => 'POST',
284 'callback' => [$this, 'handle_slack_messages'],
285 'permission_callback' => [$this, 'verify_slack_request'],
286 ]);
287
288 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
289 }
290
291 /**
292 * Verify valid chat session
293 */
294 public function verify_chat_session($request) {
295 $session_id = $request->get_param('session_id');
296 if (empty($session_id)) {
297 //error_log(esc_html__('Empty session ID in chat request', 'mxchat'));
298 return false;
299 }
300
301 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
302 return $chat_mode === 'agent';
303 }
304
305 /**
306 * Verify request is coming from Slack.
307 *
308 * @param WP_REST_Request $request
309 * @return bool True if valid, false otherwise.
310 */
311 public function verify_slack_request($request) {
312 // Get the Slack signing secret from your plugin options
313 $valid_key = $this->options['live_agent_secret_key'] ?? '';
314
315 if (empty($valid_key)) {
316 //error_log(esc_html__('Slack signing secret not configured', 'mxchat'));
317 return false;
318 }
319
320 $timestamp = $request->get_header('X-Slack-Request-Timestamp');
321 $slack_signature = $request->get_header('X-Slack-Signature');
322
323 // Verify timestamp to prevent replay attacks
324 if (abs(time() - intval($timestamp)) > 300) {
325 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
326 return false;
327 }
328
329 // Get raw request body
330 $request_body = file_get_contents('php://input');
331
332 // Create the signature base string
333 $sig_basestring = "v0:{$timestamp}:{$request_body}";
334
335 // Calculate expected signature
336 $my_signature = 'v0=' . hash_hmac('sha256', $sig_basestring, $valid_key);
337
338 // Compare signatures
339 return hash_equals($my_signature, $slack_signature);
340 }
341 public function mxchat_stream_events(WP_REST_Request $request) {
342 header('Content-Type: text/event-stream');
343 header('Cache-Control: no-cache');
344 header('Connection: keep-alive');
345
346 $session_id = sanitize_text_field($request->get_param('session_id'));
347 $last_seen_id = sanitize_text_field($request->get_param('last_seen_id')) ?: '';
348
349 if (empty($session_id)) {
350 echo esc_html__("event: error\ndata: ", 'mxchat') . esc_html__('Missing session_id', 'mxchat') . "\n\n";
351 flush();
352 exit;
353 }
354
355 $history = get_option("mxchat_history_{$session_id}", []);
356
357 // Filter only new messages
358 $new_messages = array_filter($history, function ($message) use ($last_seen_id) {
359 return !empty($message['id']) && $message['id'] > $last_seen_id;
360 });
361
362 // Send new messages if available
363 if (!empty($new_messages)) {
364 echo esc_html__("event: newMessages\ndata: ", 'mxchat') . json_encode(array_values($new_messages)) . "\n\n";
365 } else {
366 // Keep the connection alive
367 echo esc_html__("event: keepAlive\ndata: ", 'mxchat') . "{}\n\n";
368 }
369 flush();
370 exit;
371 }
372
373
374
375
376 private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
377 global $wpdb;
378 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
379 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
380
381 // Check if this is the first message in a new session (before any other database operations)
382 $is_new_session = false;
383 if ($role === 'user') { // Only check for user messages, not bot responses
384 $existing_messages = $wpdb->get_var($wpdb->prepare(
385 "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
386 $session_id
387 ));
388 $is_new_session = ($existing_messages == 0);
389
390 // Log for debugging
391 if ($is_new_session) {
392 //error_log("[DEBUG] This is a NEW session - first message");
393 }
394 }
395
396 // SECURITY FIX: Set session ownership for new sessions
397 if ($is_new_session && $role === 'user') {
398 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
399 $session_owner_key = "mxchat_session_owner_{$session_id}";
400
401 // Only set ownership if not already set
402 if (!get_option($session_owner_key)) {
403 update_option($session_owner_key, $current_user_identifier, 'no');
404 //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
405 }
406 }
407
408 // 1) Extract agent name if present
409 $agent_name = '';
410 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
411 $agent_name = $matches[1];
412 $message = str_replace("Agent: $agent_name - ", '', $message);
413 $session_meta_key = "mxchat_agent_name_{$session_id}";
414 if (empty(get_option($session_meta_key))) {
415 update_option($session_meta_key, $agent_name);
416 //error_log("[DEBUG] mxchat_save_chat_message -> Stored agent_name in option: {$session_meta_key} => {$agent_name}");
417 }
418 }
419
420 // 2) Generate unique message_id
421 $message_id = uniqid();
422 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
423
424 // 3) Determine user_id
425 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
426
427 // 4) Determine user_identifier
428 $user_identifier = $agent_name
429 ? $agent_name
430 : MxChat_User::mxchat_get_user_identifier();
431
432 // 5) Determine displayed_name
433 $user_email = MxChat_User::mxchat_get_user_email();
434 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
435
436 // 6) Check for a saved email in wp_options
437 $email_option_key = "mxchat_email_{$session_id}";
438 $saved_email = get_option($email_option_key);
439 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
440
441 // Check for a saved name in wp_options
442 $name_option_key = "mxchat_name_{$session_id}";
443 $saved_name = get_option($name_option_key);
444 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
445
446 // If found, update DB user_email and user_name
447 if ($saved_email || $saved_name) {
448 $update_data = [];
449 if ($saved_email) {
450 $update_data['user_email'] = $saved_email;
451 }
452 if ($saved_name) {
453 $update_data['user_name'] = $saved_name;
454 }
455
456 if (!empty($update_data)) {
457 $update_res = $wpdb->update(
458 $table_name,
459 $update_data,
460 ['session_id' => $session_id],
461 array_fill(0, count($update_data), '%s'),
462 ['%s']
463 );
464 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email/user_name update for session_id {$session_id}. update_res: {$update_res}");
465 }
466 }
467
468 // 7) Save to session history in wp_options
469 $history_key = "mxchat_history_{$session_id}";
470 $history = get_option($history_key, []);
471 $history[] = [
472 'id' => $message_id,
473 'role' => $role,
474 'content' => $message,
475 'timestamp' => round(microtime(true) * 1000),
476 'agent_name' => $displayed_name,
477 ];
478 update_option($history_key, $history, 'no');
479 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
480
481 // 8) Save the message to DB (INSERT)
482 $insert_data = [
483 'user_id' => $user_id,
484 'user_identifier'=> $user_identifier,
485 'user_email' => $saved_email ?: $user_email,
486 'user_name' => $saved_name ?: '', // Add name to insert data
487 'session_id' => $session_id,
488 'role' => $role,
489 'message' => $message,
490 'timestamp' => current_time('mysql', 1),
491 ];
492
493 // IMPROVED: Handle originating page data
494 $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
495
496 if ($columns_exist) {
497 if ($is_new_session && $role === 'user') {
498 // For the first user message, set originating page data
499
500 // First check if we have it from the parameter
501 if ($originating_page && !empty($originating_page['url'])) {
502 $insert_data['originating_page_url'] = $originating_page['url'];
503 $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
504
505 //error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
506 }
507 // Otherwise check if it's stored in the instance property
508 else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
509 $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
510 $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
511
512 //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
513
514 // Clear after using
515 unset($this->pending_originating_page);
516 }
517 // Fallback to HTTP_REFERER if nothing else is available
518 else if (isset($_SERVER['HTTP_REFERER'])) {
519 $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
520 $insert_data['originating_page_url'] = $referer_url;
521
522 // Generate title from URL
523 $parsed_url = parse_url($referer_url);
524 $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
525
526 if (empty($path) || $path === 'index.php' || $path === 'index.html') {
527 $insert_data['originating_page_title'] = 'Homepage';
528 } else {
529 $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
530 $insert_data['originating_page_title'] = ucwords(trim($title));
531 }
532
533 //error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
534 }
535
536 // Store for this session so all messages have the same originating page
537 if (!empty($insert_data['originating_page_url'])) {
538 update_option("mxchat_originating_page_{$session_id}", [
539 'url' => $insert_data['originating_page_url'],
540 'title' => $insert_data['originating_page_title']
541 ], 'no');
542 }
543 } else {
544 // For subsequent messages in the session, use the stored originating page
545 $stored_originating = get_option("mxchat_originating_page_{$session_id}");
546 if ($stored_originating && !empty($stored_originating['url'])) {
547 $insert_data['originating_page_url'] = $stored_originating['url'];
548 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
549 }
550 }
551 }
552
553 // Add RAG context if provided (for bot messages)
554 if ($rag_context !== null && $role === 'bot') {
555 $rag_context_column_exists = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'rag_context'");
556 if ($rag_context_column_exists) {
557 $insert_data['rag_context'] = is_array($rag_context) ? wp_json_encode($rag_context) : $rag_context;
558 }
559 }
560
561 $wpdb->insert($table_name, $insert_data);
562 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
563
564 // 9) Send notification email if this is the first user message in a new session
565 if ($wpdb->insert_id && $is_new_session && $role === 'user') {
566 $this->send_new_chat_notification($session_id, array(
567 'identifier' => $user_identifier,
568 'email' => $saved_email ?: $user_email,
569 'ip' => $_SERVER['REMOTE_ADDR']
570 ));
571 }
572
573 // 10) Schedule delayed transcript email if enabled and message is from user
574 if ($wpdb->insert_id && $role === 'user') {
575 $this->schedule_delayed_transcript_email($session_id);
576 }
577
578 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
579 return $message_id;
580 }
581
582 private function send_new_chat_notification($session_id, $user_info = array()) {
583 $options = get_option('mxchat_transcripts_options');
584
585 // Check if notifications are enabled
586 if (empty($options['mxchat_enable_notifications'])) {
587 return false;
588 }
589
590 // Get notification email
591 $to = !empty($options['mxchat_notification_email']) ?
592 $options['mxchat_notification_email'] :
593 get_option('admin_email');
594
595 if (!is_email($to)) {
596 return false;
597 }
598
599 // Prepare email content
600 $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
601
602 $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
603 $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
604 $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
605
606 $message = sprintf(
607 "A new chat session has started on your website.\n\n" .
608 "Session ID: %s\n" .
609 "User: %s\n" .
610 "Email: %s\n" .
611 "IP Address: %s\n" .
612 "Time: %s\n\n" .
613 "View transcripts: %s",
614 $session_id,
615 $user_identifier,
616 $user_email,
617 $user_ip,
618 current_time('mysql'),
619 admin_url('admin.php?page=mxchat-transcripts')
620 );
621
622 // Send email
623 return wp_mail($to, $subject, $message);
624 }
625
626 /**
627 * Schedule delayed transcript email for a session
628 * Reschedules if a new user message is received
629 */
630 private function schedule_delayed_transcript_email($session_id) {
631 $options = get_option('mxchat_transcripts_options');
632
633 // Check if auto-email is enabled
634 if (empty($options['mxchat_auto_email_transcript_enabled'])) {
635 return;
636 }
637
638 // Get notification email
639 $email = !empty($options['mxchat_notification_email']) ?
640 $options['mxchat_notification_email'] :
641 get_option('admin_email');
642
643 if (!is_email($email)) {
644 return;
645 }
646
647 // Get delay in minutes (default 30)
648 $delay_minutes = isset($options['mxchat_auto_email_transcript_delay']) ?
649 intval($options['mxchat_auto_email_transcript_delay']) : 30;
650
651 // Clear any existing scheduled event for this session
652 $hook = 'mxchat_send_delayed_transcript';
653 $args = array($session_id);
654 $timestamp = wp_next_scheduled($hook, $args);
655
656 if ($timestamp) {
657 wp_unschedule_event($timestamp, $hook, $args);
658 }
659
660 // Schedule new event
661 $schedule_time = time() + ($delay_minutes * 60);
662 wp_schedule_single_event($schedule_time, $hook, $args);
663 }
664
665 /**
666 * Check if chat messages contain contact information (email or phone number)
667 *
668 * @param array $messages Array of message objects with 'message' property
669 * @param object|null $session_data Session data object with user_email property
670 * @return bool True if contact info found, false otherwise
671 */
672 private function chat_contains_contact_info($messages, $session_data = null) {
673 // Check if session already has a stored email
674 if ($session_data && !empty($session_data->user_email)) {
675 return true;
676 }
677
678 // Email regex pattern
679 $email_pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
680
681 // Phone number patterns (covers various formats including international, WhatsApp style)
682 // Matches: +1234567890, (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 234 567 8900, etc.
683 $phone_pattern = '/(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{2,4}[-.\s]?\d{2,4}(?:[-.\s]?\d{1,4})?/';
684
685 // Only check user messages (not assistant responses)
686 foreach ($messages as $msg) {
687 if ($msg->role !== 'user') {
688 continue;
689 }
690
691 $message_text = $msg->message;
692
693 // Check for email
694 if (preg_match($email_pattern, $message_text)) {
695 return true;
696 }
697
698 // Check for phone number (must be at least 7 digits total to avoid false positives)
699 if (preg_match($phone_pattern, $message_text, $matches)) {
700 // Count actual digits to avoid matching short numbers
701 $digits_only = preg_replace('/\D/', '', $matches[0]);
702 if (strlen($digits_only) >= 7) {
703 return true;
704 }
705 }
706 }
707
708 return false;
709 }
710
711 /**
712 * Send the delayed transcript email with .txt attachment
713 */
714 public function mxchat_send_delayed_transcript($session_id) {
715 global $wpdb;
716
717 $options = get_option('mxchat_transcripts_options');
718
719 // Get notification email
720 $to = !empty($options['mxchat_notification_email']) ?
721 $options['mxchat_notification_email'] :
722 get_option('admin_email');
723
724 if (!is_email($to)) {
725 return false;
726 }
727
728 // Get all messages for this session
729 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
730 $messages = $wpdb->get_results($wpdb->prepare(
731 "SELECT role, message, timestamp FROM {$table_name}
732 WHERE session_id = %s
733 ORDER BY timestamp ASC",
734 $session_id
735 ));
736
737 if (empty($messages)) {
738 return false;
739 }
740
741 // Get session metadata
742 $sessions_table = $wpdb->prefix . 'mxchat_sessions';
743 $session_data = $wpdb->get_row($wpdb->prepare(
744 "SELECT * FROM {$sessions_table} WHERE session_id = %s",
745 $session_id
746 ));
747
748 // Check if contact info is required and if it's present
749 $require_contact = !empty($options['mxchat_auto_email_transcript_require_contact']);
750 if ($require_contact && !$this->chat_contains_contact_info($messages, $session_data)) {
751 // Contact info required but not found - skip sending
752 return false;
753 }
754
755 // Build transcript content
756 $transcript_content = "Chat Transcript\n";
757 $transcript_content .= "================\n\n";
758 $transcript_content .= "Session ID: " . $session_id . "\n";
759
760 if ($session_data) {
761 $transcript_content .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
762 $transcript_content .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
763 $transcript_content .= "Started: " . $session_data->created_at . "\n";
764 }
765
766 $transcript_content .= "\n" . str_repeat("=", 50) . "\n\n";
767
768 // Add messages
769 foreach ($messages as $msg) {
770 $role_label = ($msg->role === 'user') ? 'User' : 'Assistant';
771 $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
772 $transcript_content .= $msg->message . "\n\n";
773 }
774
775 // Create temporary file for attachment
776 $upload_dir = wp_upload_dir();
777 $temp_file = $upload_dir['basedir'] . '/mxchat-transcript-' . $session_id . '.txt';
778 file_put_contents($temp_file, $transcript_content);
779
780 // Prepare email
781 $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
782
783 $message = "Please find attached the full chat transcript.\n\n";
784 $message .= "Session ID: {$session_id}\n";
785
786 if ($session_data) {
787 $message .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
788 $message .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
789 }
790
791 $message .= "\nView online: " . admin_url('admin.php?page=mxchat-transcripts');
792
793 // Send email with attachment
794 $attachments = array($temp_file);
795 $result = wp_mail($to, $subject, $message, '', $attachments);
796
797 // Clean up temporary file
798 if (file_exists($temp_file)) {
799 unlink($temp_file);
800 }
801
802 return $result;
803 }
804
805
806
807 public function mxchat_handle_save_email_and_response() {
808 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
809 //error_log('DEBUG: POST data: ' . print_r($_POST, true));
810
811 // Validate nonce
812 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
813 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
814 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
815 wp_die();
816 }
817
818 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
819 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
820 $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
821
822 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
823
824 if (empty($session_id) || empty($email)) {
825 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
826 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
827 wp_die();
828 }
829
830 // Validate name if provided (check if name field is enabled and name is required)
831 $options = get_option('mxchat_options', []);
832 $name_field_enabled = isset($options['enable_name_field']) &&
833 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
834
835 if ($name_field_enabled && (empty($name) || strlen(trim($name)) < 2 || strlen(trim($name)) > 100)) {
836 //error_log("[ERROR] Invalid name: {$name} (enabled: {$name_field_enabled})");
837 wp_send_json_error(['message' => esc_html__('Name must be between 2 and 100 characters.', 'mxchat')]);
838 wp_die();
839 }
840
841 // 1) Always store email in wp_options
842 $email_option_key = "mxchat_email_{$session_id}";
843 update_option($email_option_key, $email);
844 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
845
846 // Store name in wp_options if provided
847 if (!empty($name)) {
848 $name_option_key = "mxchat_name_{$session_id}";
849 update_option($name_option_key, $name);
850 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
851 }
852
853 // 2) (Optional) Also store in DB if a row already exists
854 global $wpdb;
855 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
856
857 // Make sure we have a valid placeholder in prepare
858 $sql = $wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE session_id = %s", $session_id);
859 $session_count = $wpdb->get_var($sql);
860
861 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
862
863 if ($session_count) {
864 // Update both user_email and user_name if row(s) exist
865 if (!empty($name)) {
866 $update_sql = $wpdb->prepare(
867 "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
868 $email,
869 $name,
870 $session_id
871 );
872 } else {
873 $update_sql = $wpdb->prepare(
874 "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
875 $email,
876 $session_id
877 );
878 }
879 $wpdb->query($update_sql);
880 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
881 } else {
882 //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email/name is only in wp_options.");
883 }
884
885 // Provide success response (same as original)
886 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
887 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
888 wp_send_json_success(['message' => $bot_message]);
889 wp_die();
890 }
891
892 public function mxchat_check_email_provided() {
893 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
894
895 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
896 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
897 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
898 }
899
900 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
901 if (empty($session_id)) {
902 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
903 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
904 }
905
906 // Check if the user is logged in
907 if (is_user_logged_in()) {
908 $current_user = wp_get_current_user();
909 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
910
911 // Get user's display name for logged in users
912 $user_name = !empty($current_user->display_name) ? $current_user->display_name :
913 (!empty($current_user->first_name) ? $current_user->first_name : '');
914
915 $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
916 if (!empty($user_name)) {
917 $response_data['name'] = $user_name;
918 }
919
920 wp_send_json_success($response_data);
921 }
922
923 // Check if name field is required
924 $options = get_option('mxchat_options', []);
925 $name_field_enabled = isset($options['enable_name_field']) &&
926 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
927
928 $email_option_key = "mxchat_email_{$session_id}";
929 $stored_email = get_option($email_option_key, '');
930
931 // Check for stored name
932 $name_option_key = "mxchat_name_{$session_id}";
933 $stored_name = get_option($name_option_key, '');
934
935 //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
936 //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
937
938 // Check if we have email and name (if name is required)
939 $has_required_info = !empty($stored_email);
940
941 if ($name_field_enabled) {
942 $has_required_info = $has_required_info && !empty($stored_name);
943 }
944
945 if ($has_required_info) {
946 //error_log("[DEBUG] mxchat_check_email_provided -> Required info found, returning success");
947
948 $response_data = ['email' => $stored_email];
949 if (!empty($stored_name)) {
950 $response_data['name'] = $stored_name;
951 }
952
953 wp_send_json_success($response_data);
954 } else {
955 //error_log("[DEBUG] mxchat_check_email_provided -> Required info missing, returning error");
956 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
957 }
958 }
959
960 /**
961 * Send error response in appropriate format based on streaming mode
962 * ADDED: Helper method to consistently handle errors in both streaming and non-streaming modes
963 *
964 * @param string $error_message The error message to display
965 * @param string $error_code Optional error code for debugging
966 */
967 private function send_error_response($error_message, $error_code = 'api_error') {
968 if ($this->is_streaming) {
969 echo "data: " . json_encode([
970 'error' => true,
971 'error_message' => $error_message,
972 'error_code' => $error_code,
973 'text' => $error_message,
974 'message' => $error_message
975 ]) . "\n\n";
976 echo "data: [DONE]\n\n";
977 flush();
978 } else {
979 wp_send_json_error([
980 'error_message' => $error_message,
981 'error_code' => $error_code
982 ]);
983 }
984 wp_die();
985 }
986
987 public function mxchat_handle_chat_request() {
988 global $wpdb;
989
990 // Debug: Log incoming bot_id
991 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
992 error_log("=== MXCHAT DEBUG: Starting chat request ===");
993 error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
994
995 // Get bot-specific options
996 $bot_options = $this->get_bot_options($bot_id);
997 $current_options = !empty($bot_options) ? $bot_options : $this->options;
998
999 // Check if this is a streaming request
1000 // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing)
1001 $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator');
1002 $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
1003 ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on'));
1004
1005 // ADDED: Store streaming state in class property for use in private methods
1006 $this->is_streaming = $is_streaming;
1007
1008 // NOTE: Streaming headers are now set later via setup_streaming_headers()
1009 // This allows actions/forms to return JSON responses without header conflicts
1010
1011 // Check if MX Chat Moderation is active
1012 if (class_exists('MX_Chat_Moderation')) {
1013 // Get user email and IP
1014 $user_email = '';
1015 $user_ip = $_SERVER['REMOTE_ADDR'];
1016
1017 // If user is logged in, get their email
1018 if (is_user_logged_in()) {
1019 $current_user = wp_get_current_user();
1020 $user_email = $current_user->user_email;
1021 }
1022
1023 // Create ban handler instance
1024 $ban_handler = new MX_Chat_Ban_Handler();
1025
1026 // Check if user is banned by IP
1027 if ($ban_handler->check_ban($user_ip, 'ip')) {
1028 wp_send_json([
1029 'success' => false,
1030 'message' => esc_html__('Access denied. Your IP address has been banned.', 'mxchat'),
1031 'status' => 'banned'
1032 ]);
1033 wp_die();
1034 }
1035
1036 // If user is logged in, also check email
1037 if (!empty($user_email) && $ban_handler->check_ban($user_email, 'email')) {
1038 wp_send_json([
1039 'success' => false,
1040 'message' => esc_html__('Access denied. Your email address has been banned.', 'mxchat'),
1041 'status' => 'banned'
1042 ]);
1043 wp_die();
1044 }
1045 }
1046
1047 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
1048 $this->productCardHtml = '';
1049
1050 // Get the actual WordPress user ID if logged in
1051 $is_logged_in = is_user_logged_in();
1052 if ($is_logged_in) {
1053 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
1054 } else {
1055 // For logged-out users, use your existing identifier method
1056 $user_id = $this->mxchat_get_user_identifier();
1057 }
1058
1059 // Get and sanitize the user identifier
1060 $user_id = sanitize_key($user_id);
1061
1062 // Check rate limit using new settings structure
1063 $rate_limit_result = $this->check_rate_limit();
1064
1065 if ($rate_limit_result !== true) {
1066 wp_send_json([
1067 'success' => false,
1068 'message' => $rate_limit_result['message'],
1069 'status' => 'rate_limit_exceeded'
1070 ]);
1071 wp_die();
1072 }
1073
1074 // Rest of your existing code...
1075 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1076
1077 if (empty($session_id)) {
1078 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1079 wp_die();
1080 }
1081
1082 // SECURITY FIX: Verify session ownership before processing chat request
1083 // If IP/user changed, signal frontend to reset session instead of blocking
1084 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1085 $session_owner = get_option("mxchat_session_owner_{$session_id}");
1086
1087 if ($session_owner && $session_owner !== $current_user_identifier) {
1088 // Instead of blocking, tell frontend to start a fresh session
1089 wp_send_json_error([
1090 'message' => esc_html__('Your session has expired. Starting a new conversation.', 'mxchat'),
1091 'code' => 'session_expired',
1092 'action' => 'reset_session'
1093 ]);
1094 wp_die();
1095 }
1096
1097 // Validate and sanitize the incoming message
1098 if (empty($_POST['message'])) {
1099 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1100 wp_die();
1101 }
1102
1103
1104 // Track originating page for first message in session
1105 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1106
1107 // Check if originating page columns exist
1108 $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
1109
1110 if ($columns_exist) {
1111 // Check if this session already has messages
1112 $message_count = $wpdb->get_var($wpdb->prepare(
1113 "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
1114 $session_id
1115 ));
1116
1117 // If this is the first message in the session
1118 if ($message_count == 0) {
1119 // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
1120 $originating_url = '';
1121 $originating_title = '';
1122
1123 // Try to get from POST data first (sent by JavaScript)
1124 if (isset($_POST['current_page_url'])) {
1125 $originating_url = esc_url_raw($_POST['current_page_url']);
1126 $originating_title = isset($_POST['current_page_title'])
1127 ? sanitize_text_field($_POST['current_page_title'])
1128 : '';
1129 }
1130 // Fallback to HTTP_REFERER if not provided by JavaScript
1131 else if (isset($_SERVER['HTTP_REFERER'])) {
1132 $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
1133 }
1134
1135 // Generate title if we have URL but no title
1136 if ($originating_url && empty($originating_title)) {
1137 $parsed_url = parse_url($originating_url);
1138 $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1139
1140 if (empty($path) || $path === 'index.php' || $path === 'index.html') {
1141 $originating_title = 'Homepage';
1142 } else {
1143 // Clean up the path to make a readable title
1144 $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
1145 $originating_title = ucwords(trim($originating_title));
1146 }
1147 }
1148
1149 // Store for later use when saving the message
1150 $this->pending_originating_page = [
1151 'url' => $originating_url,
1152 'title' => $originating_title
1153 ];
1154 }
1155 }
1156
1157
1158
1159 // Get page context if provided
1160 $page_context = null;
1161 if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
1162 $page_context_raw = stripslashes($_POST['page_context']);
1163 $page_context = json_decode($page_context_raw, true);
1164
1165 // Validate page context structure
1166 if (is_array($page_context) &&
1167 isset($page_context['url']) &&
1168 isset($page_context['title']) &&
1169 isset($page_context['content'])) {
1170
1171 // Sanitize page context
1172 $page_context['url'] = esc_url_raw($page_context['url']);
1173 $page_context['title'] = sanitize_text_field($page_context['title']);
1174 $page_context['content'] = wp_kses_post($page_context['content']);
1175 } else {
1176 $page_context = null;
1177 }
1178 }
1179
1180 // Modify the message sanitization to preserve PHP tags in code blocks
1181 $allowed_tags = [
1182 'pre' => [],
1183 'code' => ['class' => true],
1184 'span' => ['class' => true],
1185 'div' => ['class' => true],
1186 ];
1187
1188 // First preserve code blocks
1189 $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
1190 return htmlspecialchars_decode($matches[0]);
1191 }, $_POST['message']);
1192
1193 // Then apply sanitization
1194 $message = wp_kses($message, $allowed_tags);
1195
1196 // Preserve code blocks from markdown conversion
1197 $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
1198 $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
1199
1200 // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
1201 // Always initialize testing data for admins (no toggle needed)
1202 $testing_data = null;
1203 if (current_user_can('administrator')) {
1204 // For vision messages, use the original user message for the query display
1205 $query_for_testing = $message;
1206 if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1207 $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1208 }
1209
1210 $testing_data = [
1211 'query' => $query_for_testing,
1212 'timestamp' => time(),
1213 'top_matches' => [],
1214 'action_matches' => [], // Initialize action matches array
1215 'page_context' => $page_context, // Include page context in testing data
1216 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
1217 'bot_id' => $bot_id // Include bot ID in testing data
1218 ];
1219
1220 // Get similarity threshold from bot options or default options
1221 $similarity_threshold = isset($current_options['similarity_threshold'])
1222 ? ((int) $current_options['similarity_threshold']) / 100
1223 : 0.35;
1224
1225 $testing_data['similarity_threshold'] = $similarity_threshold;
1226
1227 // Determine knowledge base type using bot-specific config
1228 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
1229 $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
1230 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
1231 }
1232 // ===== END SIMPLIFIED TESTING INITIALIZATION =====
1233
1234 // Add debug before and after:
1235 //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
1236 $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
1237 //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
1238
1239
1240 // If the pre-processing returned a result (not the original message), use it directly
1241 if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
1242 // Save the AI response
1243 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
1244
1245 // Save HTML content if provided
1246 if (!empty($pre_processed_result['html'])) {
1247 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1248 }
1249
1250 // Add testing data if admin
1251 $response_data = [
1252 'text' => $pre_processed_result['text'],
1253 'html' => $pre_processed_result['html'] ?? '',
1254 'session_id' => $session_id
1255 ];
1256
1257 if ($testing_data !== null) {
1258 $response_data['testing_data'] = $testing_data;
1259 }
1260
1261 wp_send_json($response_data);
1262 wp_die();
1263 }
1264
1265 // Save the user's message - handle vision processed messages differently
1266 if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1267 // For vision messages, save the original user message with image indicator
1268 $original_message = sanitize_textarea_field($_POST['original_user_message']);
1269 if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
1270 $image_count = intval($_POST['vision_images_count']);
1271 $original_message .= " [{$image_count} image(s)]";
1272 }
1273 $this->mxchat_save_chat_message($session_id, 'user', $original_message);
1274 } else {
1275 // Regular message - save as normal
1276 $this->mxchat_save_chat_message($session_id, 'user', $message);
1277 }
1278
1279
1280 if (is_email($message)) {
1281 // Add the email to Loops
1282 $this->add_email_to_loops($message);
1283
1284 // Get the user's success message instruction using current_options
1285 $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1286
1287 // Set instruction for AI using the user's success message
1288 $this->current_action_instruction = $user_success_message;
1289
1290 // Clear the email capture transient since we got the email
1291 delete_transient('mxchat_email_capture_' . $user_id);
1292 }
1293
1294 // Check if we're in an email capture flow but user hasn't provided email yet
1295 elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1296 // Check if the message contains an email (not the whole message being an email)
1297 if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1298 $extracted_email = $matches[0];
1299
1300 // Add the extracted email to Loops
1301 $this->add_email_to_loops($extracted_email);
1302
1303 // Get the user's success message instruction using current_options
1304 $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1305
1306 // Set instruction for AI using the user's success message
1307 $this->current_action_instruction = $user_success_message;
1308
1309 // Clear the email capture transient since we got the email
1310 delete_transient('mxchat_email_capture_' . $user_id);
1311 }
1312 // If no email found but we're in capture mode, remind them
1313 else {
1314 // Get the original instruction to remind them using current_options
1315 $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1316 $this->current_action_instruction = $original_instruction;
1317 }
1318 }
1319
1320 $intent_info = '';
1321
1322 // Check chat mode
1323 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1324
1325 // Handle agent mode
1326 // Handle agent mode
1327 if ($chat_mode === 'agent') {
1328 // First, check for switch intent before doing anything else
1329 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1330
1331 // Capture action analysis for testing panel after intent check
1332 if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1333 $testing_data['action_matches'] = $this->last_action_analysis;
1334 }
1335
1336 // Around line 506, in the agent mode handling section:
1337 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1338 // Update chat mode first
1339 update_option("mxchat_mode_{$session_id}", 'ai');
1340
1341 // Clear any existing PDF context to start fresh
1342 $this->clear_pdf_transients($session_id);
1343
1344 // Prepare clean switch response with explicit chat_mode
1345 $response_data = [
1346 'text' => $this->fallbackResponse['text'],
1347 'html' => $this->fallbackResponse['html'] ?? '',
1348 'session_id' => $session_id,
1349 'chat_mode' => 'ai' // EXPLICITLY SET THIS
1350 ];
1351
1352 if ($testing_data !== null) {
1353 $response_data['testing_data'] = $testing_data;
1354 }
1355
1356 // Save the mode switch message
1357 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1358 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1359
1360 // Send response and exit
1361 wp_send_json($response_data);
1362 wp_die();
1363 } elseif (!$intent_matched) {
1364 // No intent matched, handle live agent message
1365 try {
1366 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1367
1368 $agent_response = [
1369 'status' => 'waiting_for_agent',
1370 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1371 ];
1372
1373 if ($testing_data !== null) {
1374 $agent_response['testing_data'] = $testing_data;
1375 }
1376
1377 wp_send_json_success($agent_response);
1378 } catch (\Exception $e) {
1379 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1380 }
1381 wp_die();
1382 }
1383 }
1384
1385 // Step 1: Check for new PDF URL in the message
1386 if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1387 $new_pdf_url = $matches[0];
1388
1389 // Check if this is likely a PDF-related request
1390 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1391 $is_pdf_request = false;
1392
1393 foreach ($pdf_keywords as $keyword) {
1394 if (stripos($message, $keyword) !== false) {
1395 $is_pdf_request = true;
1396 break;
1397 }
1398 }
1399
1400 // If it looks like a PDF request or we're waiting for a PDF URL
1401 if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1402 // Validate HTTPS
1403 if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1404 // Extract filename from URL
1405 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1406
1407 // Clear previous PDF transients
1408 $this->clear_pdf_transients($session_id);
1409
1410 // Process new PDF using current_options
1411 $max_pages = $current_options['pdf_max_pages'] ?? 69;
1412 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1413
1414 if ($embeddings === 'too_many_pages') {
1415 $error_text = sprintf(
1416 $current_options['pdf_intent_error_text'] ??
1417 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1418 $max_pages
1419 );
1420 $this->fallbackResponse['text'] = $error_text;
1421 } elseif ($embeddings) {
1422 // Store new PDF information
1423 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1424
1425 // If the filename is generic, create a more descriptive one
1426 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1427 strpos($pdf_filename, '.php') !== false) {
1428 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1429 }
1430
1431 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1432 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1433 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1434 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1435
1436 $success_text = $current_options['pdf_intent_success_text'] ??
1437 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1438
1439 $pdf_response = [
1440 'success' => true,
1441 'message' => $success_text,
1442 'data' => [
1443 'filename' => $pdf_filename
1444 ]
1445 ];
1446
1447 if ($testing_data !== null) {
1448 $pdf_response['testing_data'] = $testing_data;
1449 }
1450
1451 wp_send_json($pdf_response);
1452 wp_die();
1453 } else {
1454 $error_text = $current_options['pdf_intent_error_text'] ??
1455 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1456 $this->fallbackResponse['text'] = $error_text;
1457 }
1458
1459 $pdf_error_response = [
1460 'success' => false,
1461 'message' => $this->fallbackResponse['text']
1462 ];
1463
1464 if ($testing_data !== null) {
1465 $pdf_error_response['testing_data'] = $testing_data;
1466 }
1467
1468 wp_send_json($pdf_error_response);
1469 wp_die();
1470 }
1471 }
1472 }
1473
1474
1475 // Step 2: Detect intent and handle intent-based responses
1476 $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1477
1478 // Capture action analysis for testing panel after intent check
1479 if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1480 $testing_data['action_matches'] = $this->last_action_analysis;
1481 }
1482
1483 // Step 3: Handle the intent result appropriately
1484 if ($intent_result !== false) {
1485 // Intent was matched - ALWAYS send as JSON response, never streaming
1486
1487 if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1488 // Intent returned a direct response array
1489 $response_data = [
1490 'text' => $intent_result['text'] ?? '',
1491 'html' => $intent_result['html'] ?? '',
1492 'session_id' => $session_id
1493 ];
1494
1495 // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1496 if (isset($intent_result['chat_mode'])) {
1497 $response_data['chat_mode'] = $intent_result['chat_mode'];
1498 }
1499
1500 if ($testing_data !== null) {
1501 $response_data['testing_data'] = $testing_data;
1502 }
1503
1504 wp_send_json($response_data);
1505 wp_die();
1506 } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1507 // Intent returned true and set fallbackResponse
1508
1509 // SAVE TO TRANSCRIPT FIRST
1510 if (!empty($this->fallbackResponse['text'])) {
1511 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1512 }
1513 if (!empty($this->fallbackResponse['html'])) {
1514 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1515 }
1516
1517 $response_data = [
1518 'text' => $this->fallbackResponse['text'] ?? '',
1519 'html' => $this->fallbackResponse['html'] ?? '',
1520 'session_id' => $session_id
1521 ];
1522
1523 if (isset($this->fallbackResponse['chat_mode'])) {
1524 $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1525 }
1526
1527 if ($testing_data !== null) {
1528 $response_data['testing_data'] = $testing_data;
1529 }
1530
1531 wp_send_json($response_data);
1532 wp_die();
1533 }
1534 }
1535
1536 // If we get here, no intent matched OR the intent didn't provide a usable response
1537
1538 // Step 4: Generate AI response
1539 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1540 $this->mxchat_increment_chat_count();
1541
1542 // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1543 $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1544 $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1545
1546 // Check if the embedding generation returned an error
1547 if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1548 $error_message = $user_message_embedding['error'];
1549 $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1550
1551 // FIXED: Send error in appropriate format based on streaming mode
1552 if ($is_streaming) {
1553 echo "data: " . json_encode([
1554 'error' => true,
1555 'error_message' => $error_message,
1556 'error_code' => $error_code,
1557 'text' => $error_message,
1558 'message' => $error_message
1559 ]) . "\n\n";
1560 echo "data: [DONE]\n\n";
1561 flush();
1562 } else {
1563 wp_send_json_error([
1564 'error_message' => $error_message,
1565 'error_code' => $error_code
1566 ]);
1567 }
1568 wp_die();
1569 }
1570
1571 // Check if the embedding is valid
1572 if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1573 $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1574
1575 // FIXED: Send error in appropriate format based on streaming mode
1576 if ($is_streaming) {
1577 echo "data: " . json_encode([
1578 'error' => true,
1579 'error_message' => $error_message,
1580 'error_code' => 'invalid_embedding',
1581 'text' => $error_message,
1582 'message' => $error_message
1583 ]) . "\n\n";
1584 echo "data: [DONE]\n\n";
1585 flush();
1586 } else {
1587 wp_send_json_error([
1588 'error_message' => $error_message,
1589 'error_code' => 'invalid_embedding'
1590 ]);
1591 }
1592 wp_die();
1593 }
1594
1595 // Build context with both knowledge base and PDF content if available
1596 $context_content = "User asked: '{$message}'\n\n";
1597
1598 // Add action instruction if present (add this right after the above line)
1599 if (!empty($this->current_action_instruction)) {
1600 $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1601 $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1602 $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1603 $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1604
1605 // Clear the instruction after using it
1606 $this->current_action_instruction = null;
1607 }
1608
1609
1610 // Add page context if available and contextual awareness is enabled using current_options
1611 if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
1612 $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1613 $context_content .= "Page URL: " . $page_context['url'] . "\n";
1614 $context_content .= "Page Title: " . $page_context['title'] . "\n";
1615 $context_content .= "Page Content: " . $page_context['content'] . "\n";
1616 $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1617 }
1618
1619 // Get relevant content from knowledge base - PASS BOT_ID
1620 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id);
1621
1622 // NEW: Also extract URLs from system instructions
1623 $system_instructions = $this->get_system_instructions($bot_id);
1624 if (!empty($system_instructions)) {
1625 preg_match_all(
1626 '#\bhttps?://[^\s<>"\']+#i',
1627 $system_instructions,
1628 $system_instruction_urls
1629 );
1630
1631 if (!empty($system_instruction_urls[0])) {
1632 // Merge with existing valid URLs
1633 $this->current_valid_urls = array_merge(
1634 $this->current_valid_urls,
1635 $system_instruction_urls[0]
1636 );
1637 // Remove duplicates
1638 $this->current_valid_urls = array_unique($this->current_valid_urls);
1639
1640 error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1641 }
1642 }
1643
1644 // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1645 if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1646 // Update testing data with the REAL similarity analysis
1647 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1648 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1649 $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1650 }
1651 // ===== END SIMILARITY DATA CAPTURE =====
1652
1653 // NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
1654 if ($testing_data !== null && !empty($this->current_valid_urls)) {
1655 $testing_data['approved_urls'] = array_values($this->current_valid_urls);
1656 error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
1657 }
1658
1659 if (!empty($relevant_content)) {
1660 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1661 } else {
1662 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1663 }
1664
1665 // NEW: Add approved URLs list to context for AI
1666 if (!empty($this->current_valid_urls)) {
1667 $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
1668 $context_content .= "You may ONLY use these exact URLs in your response:\n";
1669 foreach ($this->current_valid_urls as $url) {
1670 $context_content .= "- " . $url . "\n";
1671 }
1672 $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
1673 $context_content .= "===== END APPROVED URLS =====\n\n";
1674 }
1675
1676 // Check for and include PDF content
1677 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1678 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1679 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1680 if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1681 $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1682 if (!empty($relevant_pdf_pages)) {
1683 $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1684 foreach ($relevant_pdf_pages as $page_data) {
1685 $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1686 }
1687 $context_content .= "\n";
1688 }
1689 }
1690
1691 // Check for and include Word content
1692 $word_url = get_transient('mxchat_word_url_' . $session_id);
1693 $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1694 $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1695 if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1696 $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1697 if (!empty($relevant_word_chunks)) {
1698 $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1699 foreach ($relevant_word_chunks as $chunk_data) {
1700 $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1701 }
1702 $context_content .= "\n";
1703 }
1704 }
1705
1706 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1707
1708 // Extract model from current options for bot-specific model support
1709 $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-4o';
1710
1711 $response = $this->mxchat_generate_response(
1712 $context_content,
1713 $current_options['api_key'] ?? $this->options['api_key'],
1714 $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1715 $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1716 $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1717 $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1718 $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
1719 $conversation_history,
1720 $is_streaming,
1721 $session_id,
1722 $testing_data,
1723 $selected_model
1724 );
1725
1726 // Handle streaming vs non-streaming responses
1727 if ($is_streaming) {
1728 // Check if streaming actually happened or if it fell back to regular response
1729 if ($response === true) {
1730 wp_die();
1731 }
1732 // If we get here, streaming fell back to regular response, continue
1733 // But if there's an error, we need to send it as SSE format since headers are already set
1734 if (is_array($response) && isset($response['error'])) {
1735 $error_message = $response['error'];
1736 $error_code = $response['error_code'] ?? 'api_error';
1737 // Send error in SSE format that the client JS can handle
1738 echo "data: " . json_encode([
1739 'error' => true,
1740 'error_message' => $error_message,
1741 'error_code' => $error_code,
1742 'text' => $error_message, // Also include as text for fallback handling
1743 'message' => $error_message
1744 ]) . "\n\n";
1745 echo "data: [DONE]\n\n";
1746 flush();
1747 wp_die();
1748 }
1749 }
1750
1751 // Check if the response is an error array (non-streaming mode)
1752 if (is_array($response) && isset($response['error'])) {
1753 wp_send_json_error([
1754 'error_message' => $response['error'],
1755 'error_code' => $response['error_code'] ?? 'api_error'
1756 ]);
1757 wp_die();
1758 }
1759
1760 // DEBUG: Check what we have
1761 error_log("=== BEFORE URL VALIDATION ===");
1762 error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
1763 error_log("current_valid_urls count: " . count($this->current_valid_urls));
1764 error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
1765
1766 // If we get here, the response is valid text - now validate URLs
1767 if (!empty($this->current_valid_urls)) {
1768 error_log("CALLING validate_and_clean_urls");
1769 $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
1770 } else {
1771 error_log("SKIPPING validation - current_valid_urls is empty");
1772 }
1773 // ===== END URL VALIDATION =====
1774
1775 // Prepare RAG context data for storage (only include documents used for context)
1776 $rag_context_for_storage = null;
1777 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) {
1778 $rag_context_for_storage = [
1779 'top_matches' => $this->last_similarity_analysis['top_matches'],
1780 'approved_urls' => $this->current_valid_urls ?? [],
1781 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35,
1782 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database',
1783 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0
1784 ];
1785 }
1786
1787 // Save the cleaned response with RAG context
1788 $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
1789
1790 // Step 5: Save additional content if available
1791 if (!empty($this->productCardHtml)) {
1792 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1793 }
1794
1795 if (!empty($this->fallbackResponse['html'])) {
1796 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1797 }
1798
1799 // Step 6: Return the response
1800 // DEBUG: Check if newlines exist in the response
1801 error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
1802 error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
1803 error_log("Response first 500 chars: " . substr($response, 0, 500));
1804
1805 $response_data = [
1806 'text' => $response,
1807 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1808 'session_id' => $session_id
1809 ];
1810
1811 // Always add testing data for admins (no toggle needed)
1812 if ($testing_data !== null) {
1813 $response_data['testing_data'] = $testing_data;
1814 }
1815
1816 wp_send_json($response_data);
1817 wp_die();
1818 }
1819
1820 /**
1821 * Get bot-specific options for multi-bot functionality
1822 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1823 */
1824 // Also debug the bot options retrieval
1825 private function get_bot_options($bot_id = 'default') {
1826 error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1827
1828 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1829 error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1830 return array();
1831 }
1832
1833 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1834
1835 if (!empty($bot_options)) {
1836 error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1837 if (isset($bot_options['similarity_threshold'])) {
1838 error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1839 }
1840 }
1841
1842 return is_array($bot_options) ? $bot_options : array();
1843 }
1844
1845 /**
1846 * Get bot-specific Pinecone configuration
1847 * Used in the knowledge retrieval functions
1848 */
1849 // Also add debugging to your get_bot_pinecone_config function
1850 private function get_bot_pinecone_config($bot_id = 'default') {
1851 error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1852
1853 // If default bot or multi-bot add-on not active, use default Pinecone config
1854 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1855 error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1856 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1857 $config = array(
1858 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1859 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1860 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1861 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1862 );
1863 error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1864 return $config;
1865 }
1866
1867 error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1868
1869 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1870 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1871
1872 if (!empty($bot_pinecone_config)) {
1873 error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1874 error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1875 error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1876 error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1877 } else {
1878 error_log("MXCHAT DEBUG: Filter returned empty config!");
1879 }
1880
1881 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1882 }
1883
1884
1885 // Updated function to check intents and invoke the callback function
1886 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1887 global $wpdb;
1888 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1889
1890 // Get the current bot_id
1891 $current_bot_id = $this->get_current_bot_id($session_id);
1892
1893 // Generate the user embedding
1894 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1895
1896 // Check if embedding generation returned an error
1897 if (is_array($user_embedding) && isset($user_embedding['error'])) {
1898 $error_message = $user_embedding['error'];
1899 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1900
1901 // FIXED: Send error in appropriate format based on streaming mode
1902 if ($this->is_streaming) {
1903 echo "data: " . json_encode([
1904 'error' => true,
1905 'error_message' => $error_message,
1906 'error_code' => $error_code,
1907 'text' => $error_message,
1908 'message' => $error_message
1909 ]) . "\n\n";
1910 echo "data: [DONE]\n\n";
1911 flush();
1912 } else {
1913 wp_send_json_error([
1914 'error_message' => $error_message,
1915 'error_code' => $error_code
1916 ]);
1917 }
1918 wp_die();
1919 }
1920
1921 // Check if embedding is valid
1922 if (!is_array($user_embedding) || empty($user_embedding)) {
1923 $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1924
1925 // FIXED: Send error in appropriate format based on streaming mode
1926 if ($this->is_streaming) {
1927 echo "data: " . json_encode([
1928 'error' => true,
1929 'error_message' => $error_message,
1930 'error_code' => 'invalid_embedding',
1931 'text' => $error_message,
1932 'message' => $error_message
1933 ]) . "\n\n";
1934 echo "data: [DONE]\n\n";
1935 flush();
1936 } else {
1937 wp_send_json_error([
1938 'error_message' => $error_message,
1939 'error_code' => 'invalid_embedding'
1940 ]);
1941 }
1942 wp_die();
1943 }
1944
1945 // Fetch intents from the database
1946 $table_name = $wpdb->prefix . 'mxchat_intents';
1947 if ($chat_mode === 'agent') {
1948 $query = $wpdb->prepare(
1949 "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
1950 'mxchat_handle_switch_to_chatbot_intent'
1951 );
1952 $intents = $wpdb->get_results($query);
1953 } else {
1954 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
1955 }
1956
1957 if (empty($intents)) {
1958 return false;
1959 }
1960
1961 $highest_similarity = -INF;
1962 $matched_intent = null;
1963
1964 // Array to store action analysis for testing panel
1965 $action_analysis = [];
1966
1967 foreach ($intents as $intent) {
1968 // Additional check for enabled state
1969 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1970 if (!$is_enabled) {
1971 continue;
1972 }
1973
1974 // Check if this action is enabled for the current bot
1975 if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
1976 continue;
1977 }
1978
1979 $intent_embedding_serialized = $intent->embedding_vector;
1980 $intent_embedding = $intent_embedding_serialized
1981 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1982 : null;
1983
1984 if (!is_array($intent_embedding)) {
1985 continue;
1986 }
1987
1988 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1989 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1990
1991 // Store action analysis data for testing panel
1992 $action_analysis[] = [
1993 'intent_label' => $intent->intent_label,
1994 'callback_function' => $intent->callback_function,
1995 'similarity' => round($similarity, 4),
1996 'similarity_percentage' => round($similarity * 100, 2),
1997 'threshold' => $intent_threshold,
1998 'threshold_percentage' => round($intent_threshold * 100, 2),
1999 'above_threshold' => $similarity >= $intent_threshold,
2000 'triggered' => false // Will be updated below if this intent is triggered
2001 ];
2002
2003 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2004 $highest_similarity = $similarity;
2005 $matched_intent = $intent;
2006 }
2007 }
2008
2009 // Mark the triggered action if any
2010 if ($matched_intent) {
2011 foreach ($action_analysis as &$action) {
2012 if ($action['intent_label'] === $matched_intent->intent_label) {
2013 $action['triggered'] = true;
2014 break;
2015 }
2016 }
2017 }
2018
2019 // Sort actions by similarity (highest first) and store for testing panel
2020 usort($action_analysis, function($a, $b) {
2021 return $b['similarity'] <=> $a['similarity'];
2022 });
2023
2024 // Store action analysis for testing panel capture
2025 $this->last_action_analysis = $action_analysis;
2026
2027 // Around line 715 in your mxchat_check_intent_and_invoke_callback function
2028 if ($matched_intent) {
2029 // If the callback is a method on this instance (core callback), call it directly
2030 if (method_exists($this, $matched_intent->callback_function)) {
2031 $callback_result = call_user_func(
2032 [$this, $matched_intent->callback_function],
2033 $message,
2034 $user_id,
2035 $session_id,
2036 $matched_intent,
2037 $user_context ?? null
2038 );
2039 } else {
2040 // Otherwise, use apply_filters for add-on callbacks
2041 $callback_result = apply_filters(
2042 $matched_intent->callback_function,
2043 false,
2044 $message,
2045 $user_id,
2046 $session_id,
2047 $matched_intent
2048 );
2049 }
2050
2051 // Handle the callback result properly
2052 if ($callback_result !== false) {
2053 // If callback returned an array with chat_mode, use it directly
2054 if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
2055 $this->fallbackResponse = $callback_result;
2056 return $callback_result; // Return the full array
2057 } else {
2058 $this->fallbackResponse = $callback_result;
2059 return true;
2060 }
2061 }
2062 }
2063
2064 return false;
2065 }
2066
2067 /**
2068 * Check if an action is enabled for a specific bot
2069 */
2070 private function is_action_enabled_for_bot($intent, $bot_id) {
2071 // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2072 if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2073 return true;
2074 }
2075
2076 $enabled_bots = json_decode($intent->enabled_bots, true);
2077
2078 // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2079 if (!is_array($enabled_bots) || empty($enabled_bots)) {
2080 return true;
2081 }
2082
2083 // Check if the current bot is in the enabled bots list
2084 return in_array($bot_id, $enabled_bots);
2085 }
2086
2087 // Helper function to clear PDF and Word document related transients
2088 private function clear_pdf_transients($session_id) {
2089 // PDF transients
2090 delete_transient('mxchat_pdf_url_' . $session_id);
2091 delete_transient('mxchat_pdf_embeddings_' . $session_id);
2092 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
2093 delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
2094
2095 // Word document transients
2096 delete_transient('mxchat_word_url_' . $session_id);
2097 delete_transient('mxchat_word_filename_' . $session_id);
2098 delete_transient('mxchat_word_embeddings_' . $session_id);
2099 delete_transient('mxchat_include_word_in_context_' . $session_id);
2100 delete_transient('mxchat_waiting_for_word_' . $session_id);
2101 }
2102
2103
2104
2105 //verified good
2106 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
2107 // Get the user's original instruction/message
2108 $user_instruction = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Please provide your email address.", 'mxchat'));
2109
2110 // Set instruction for AI - just pass along what the user wanted to say
2111 $this->current_action_instruction = $user_instruction;
2112
2113 // Set the transient to track email capture flow
2114 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
2115
2116 // Return false to let the AI generate the response
2117 return false;
2118 }
2119
2120 public function mxchat_generate_image($message, $user_id, $session_id) {
2121 //error_log("Starting image generation for message: " . $message);
2122
2123 // Prepare a prompt for DALL-E
2124 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2125
2126 // Use the existing OpenAI API key
2127 $openai_api_key = sanitize_text_field($this->options['api_key']);
2128
2129 // Call DALL-E to generate an image
2130 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
2131
2132 // Check if the response contains an image URL
2133 if (isset($image_response['imageUrl'])) {
2134 $image_url = esc_url_raw($image_response['imageUrl']);
2135
2136 // Construct the HTML with a CSS class instead of inline styles
2137 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2138 $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2139
2140 // Save the bot message with both text and HTML
2141 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2142 $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2143
2144 // Set the fallback response for the chat handler
2145 $this->fallbackResponse = [
2146 'text' => $response_text,
2147 'html' => $response_html,
2148 'images' => [$image_url]
2149 ];
2150
2151 // For debugging/verification - Use json_encode to verify what's being set
2152 //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
2153
2154 // Return the response directly instead of relying on the property
2155 return $this->fallbackResponse;
2156 } else {
2157 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2158
2159 // Save the error message
2160 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2161
2162 // Set the fallback response for the chat handler
2163 $this->fallbackResponse = [
2164 'text' => $response_text,
2165 'html' => '',
2166 'images' => []
2167 ];
2168
2169 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
2170 //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
2171
2172 // Return the response directly instead of relying on the property
2173 return $this->fallbackResponse;
2174 }
2175 }
2176 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
2177 $api_url = 'https://api.openai.com/v1/images/generations';
2178 $body = json_encode([
2179 'prompt' => sanitize_text_field($prompt),
2180 'n' => 1,
2181 'size' => '1024x1024',
2182 'model' => sanitize_text_field($model),
2183 ]);
2184
2185 $args = [
2186 'body' => $body,
2187 'headers' => [
2188 'Content-Type' => 'application/json',
2189 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2190 ],
2191 'method' => 'POST',
2192 'timeout' => absint($timeout),
2193 ];
2194
2195 $response = wp_remote_post($api_url, $args);
2196
2197 if (is_wp_error($response)) {
2198 //error_log("DALL-E request failed: " . $response->get_error_message());
2199 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2200 }
2201
2202 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2203
2204 if (isset($response_body['data'][0]['url'])) {
2205 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2206 } else {
2207 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2208 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2209 }
2210 }
2211
2212 /**
2213 * Handle web search requests.
2214 *
2215 * Sends the refined search query to the Brave Search API and uses the
2216 * results to generate a conversational response with the AI model.
2217 *
2218 * @since 1.0.0
2219 * @param string $message The user's search query.
2220 * @param string $user_id The user identifier.
2221 * @param string $session_id The current session ID.
2222 * @return array Response array containing text with embedded HTML links
2223 */
2224 public function mxchat_handle_search_request($message, $user_id, $session_id) {
2225 // Step 1: Interpret and refine the search query
2226 $refined_search_query = $this->mxchat_interpret_search_query($message);
2227 if (empty($refined_search_query)) {
2228 return array(
2229 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
2230 'html' => ''
2231 );
2232 }
2233
2234 // Retrieve and validate API settings
2235 $options = get_option('mxchat_options');
2236 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
2237 $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
2238
2239 if (empty($api_key)) {
2240 return array(
2241 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
2242 'html' => ''
2243 );
2244 }
2245
2246 // Build the API request URL
2247 $api_url = add_query_arg(
2248 array(
2249 'q' => rawurlencode($refined_search_query),
2250 'count' => $results_count,
2251 'text_decorations' => 'true',
2252 'rich_data' => 'true',
2253 ),
2254 'https://api.search.brave.com/res/v1/web/search'
2255 );
2256
2257 // Attempt to retrieve cached results first
2258 $transient_key = 'mxchat_search_' . md5($refined_search_query);
2259 $results = get_transient($transient_key);
2260
2261 if (false === $results) {
2262 // SECURITY FIX: Changed to wp_safe_remote_get
2263 $response = wp_safe_remote_get(
2264 $api_url,
2265 array(
2266 'headers' => array(
2267 'Accept' => 'application/json',
2268 'Accept-Encoding' => 'gzip',
2269 'X-Subscription-Token'=> $api_key,
2270 ),
2271 'timeout' => 10,
2272 )
2273 );
2274
2275 if (is_wp_error($response)) {
2276 return array(
2277 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
2278 'html' => ''
2279 );
2280 }
2281
2282 $results = json_decode(wp_remote_retrieve_body($response), true);
2283
2284 if (json_last_error() !== JSON_ERROR_NONE) {
2285 return array(
2286 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
2287 'html' => ''
2288 );
2289 }
2290
2291 // Cache results for one hour
2292 set_transient($transient_key, $results, HOUR_IN_SECONDS);
2293 }
2294
2295 // Process results
2296 if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
2297 // Create a more straightforward summary with HTML links
2298 $search_results_text = '';
2299
2300 // Add a simple intro
2301 $search_results_text .= sprintf(
2302 esc_html__("Here's what I found about '%s':", 'mxchat'),
2303 esc_html($refined_search_query)
2304 );
2305
2306 // Add the top results with HTML links
2307 foreach (array_slice($results['web']['results'], 0, 5) as $result) {
2308 $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
2309 $url = isset($result['url']) ? esc_url($result['url']) : '';
2310 $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
2311
2312 // Add a line break after the intro
2313 $search_results_text .= '<br><br>';
2314
2315 // Add title as a link
2316 $search_results_text .= sprintf(
2317 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
2318 $url,
2319 $title
2320 );
2321
2322 // Add a condensed description
2323 $search_results_text .= sprintf("%s", $description);
2324 }
2325
2326 // Save to chat history
2327 $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
2328
2329 // Return the formatted text with embedded HTML links
2330 return array(
2331 'text' => $search_results_text,
2332 'html' => ''
2333 );
2334 } else {
2335 return array(
2336 'text' => sprintf(
2337 esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
2338 esc_html($refined_search_query)
2339 ),
2340 'html' => ''
2341 );
2342 }
2343 }
2344
2345 //very good
2346 /**
2347 * Handle image search requests from the chatbot
2348 *
2349 * @param string $message The user's search query
2350 * @param int $user_id The user's ID
2351 * @param string $session_id The chat session ID
2352 * @return array Response array with text and HTML content
2353 */
2354 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
2355 // Step 1: Interpret the search query using the user's selected AI model
2356 $refined_search_query = $this->mxchat_interpret_search_query($message);
2357
2358 // If no query was interpreted, return a fallback message
2359 if (empty($refined_search_query)) {
2360 return array(
2361 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
2362 'html' => "",
2363 );
2364 }
2365
2366 // Brave API URL
2367 $api_url = 'https://api.search.brave.com/res/v1/images/search';
2368
2369 // Retrieve Brave API settings
2370 $options = get_option('mxchat_options');
2371 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
2372
2373 if (empty($api_key)) {
2374 return array(
2375 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
2376 'html' => "",
2377 );
2378 }
2379
2380 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
2381 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
2382
2383 // Append query parameters based on settings
2384 $api_url = add_query_arg([
2385 'q' => rawurlencode($refined_search_query),
2386 'count' => $image_count,
2387 'safesearch' => $safe_search,
2388 ], $api_url);
2389
2390 // Implement caching
2391 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
2392 $body = get_transient($transient_key);
2393
2394 if (false === $body) {
2395 $args = [
2396 'headers' => [
2397 'Accept' => 'application/json',
2398 'Accept-Encoding' => 'gzip',
2399 'X-Subscription-Token' => $api_key,
2400 ],
2401 'timeout' => 10,
2402 ];
2403
2404 // SECURITY FIX: Changed to wp_safe_remote_get
2405 $response = wp_safe_remote_get($api_url, $args);
2406
2407 if (is_wp_error($response)) {
2408 return array(
2409 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
2410 'html' => "",
2411 );
2412 }
2413
2414 $body = json_decode(wp_remote_retrieve_body($response), true);
2415 set_transient($transient_key, $body, HOUR_IN_SECONDS);
2416 }
2417
2418 // Process the API response
2419 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
2420 $html_output = '<div class="mxchat-image-gallery">';
2421
2422 // Get the configured image count (1-6)
2423 $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
2424 $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
2425
2426 // Use only the requested number of images
2427 for ($i = 0; $i < $display_count; $i++) {
2428 $image = $body['results'][$i];
2429 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
2430 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
2431 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
2432
2433 if ($image_url && $thumbnail_url) {
2434 $html_output .= '<div class="mxchat-image-item">';
2435 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
2436 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
2437 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
2438 $html_output .= '</a></div>';
2439 }
2440 }
2441
2442 $html_output .= '</div>';
2443
2444 // Create response text
2445 $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
2446
2447 // Save both response text and HTML to chat history
2448 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2449 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
2450
2451 // Return the combined response
2452 return array(
2453 'text' => $response_text,
2454 'html' => $html_output,
2455 );
2456 } else {
2457 $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
2458
2459 // Save the error message to chat history
2460 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2461
2462 return array(
2463 'text' => $response_text,
2464 'html' => "",
2465 );
2466 }
2467 }
2468
2469 /**
2470 * Interpret the search query using the user's selected AI model
2471 *
2472 * @param string $user_query The original query from the user
2473 * @return string The refined search query
2474 */
2475 public function mxchat_interpret_search_query($user_query) {
2476 $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');
2477
2478 // Get options and determine the selected model
2479 $options = $this->options ?? get_option('mxchat_options');
2480 $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2481
2482 // Extract model prefix to determine the provider
2483 $model_parts = explode('-', $selected_model);
2484 $provider = strtolower($model_parts[0]);
2485
2486 // Determine which API key to use based on the provider
2487 switch ($provider) {
2488 case 'gemini':
2489 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
2490 if (empty($api_key)) {
2491 return sanitize_text_field($user_query); // Default to original query if API key missing
2492 }
2493 return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
2494
2495 case 'claude':
2496 $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
2497 if (empty($api_key)) {
2498 return sanitize_text_field($user_query);
2499 }
2500 return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
2501
2502 case 'grok':
2503 $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
2504 if (empty($api_key)) {
2505 return sanitize_text_field($user_query);
2506 }
2507 return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
2508
2509 case 'deepseek':
2510 $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
2511 if (empty($api_key)) {
2512 return sanitize_text_field($user_query);
2513 }
2514 return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
2515
2516 case 'gpt':
2517 default:
2518 // Default to OpenAI for custom models or unrecognized prefixes
2519 $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
2520 if (empty($api_key)) {
2521 return sanitize_text_field($user_query);
2522 }
2523 return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
2524 }
2525 }
2526
2527 /**
2528 * Interpret query using OpenAI models
2529 */
2530 private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
2531 $url = 'https://api.openai.com/v1/chat/completions';
2532 $args = [
2533 'headers' => [
2534 'Authorization' => 'Bearer ' . $api_key,
2535 'Content-Type' => 'application/json',
2536 ],
2537 'body' => wp_json_encode([
2538 'model' => $model,
2539 'messages' => [
2540 ['role' => 'system', 'content' => $system_prompt],
2541 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2542 ],
2543 'temperature' => 0.2,
2544 'max_tokens' => 20,
2545 ]),
2546 'method' => 'POST',
2547 'timeout' => 15,
2548 ];
2549
2550 $response = wp_remote_post($url, $args);
2551 if (is_wp_error($response)) {
2552 return sanitize_text_field($user_query);
2553 }
2554
2555 $body = json_decode(wp_remote_retrieve_body($response), true);
2556 return isset($body['choices'][0]['message']['content'])
2557 ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
2558 : sanitize_text_field($user_query);
2559 }
2560
2561 /**
2562 * Interpret query using Claude models
2563 */
2564 private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
2565 $url = 'https://api.anthropic.com/v1/messages';
2566
2567 $args = [
2568 'headers' => [
2569 'Content-Type' => 'application/json',
2570 'x-api-key' => $api_key,
2571 'anthropic-version' => '2023-06-01',
2572 ],
2573 'body' => wp_json_encode([
2574 'model' => $model,
2575 'system' => $system_prompt,
2576 'messages' => [
2577 ['role' => 'user', 'content' => sanitize_text_field($user_query)]
2578 ],
2579 'max_tokens' => 20,
2580 'temperature' => 0.2,
2581 ]),
2582 'method' => 'POST',
2583 'timeout' => 15,
2584 ];
2585
2586 $response = wp_remote_post($url, $args);
2587 if (is_wp_error($response)) {
2588 return sanitize_text_field($user_query);
2589 }
2590
2591 $body = json_decode(wp_remote_retrieve_body($response), true);
2592 if (!empty($body['content'][0]['text'])) {
2593 return sanitize_text_field(trim($body['content'][0]['text']));
2594 }
2595
2596 return sanitize_text_field($user_query);
2597 }
2598
2599 /**
2600 * Interpret query using Gemini models
2601 */
2602 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2603 // Strip "gemini-" prefix for the API
2604 $model_version = str_replace('gemini-', '', $model);
2605
2606 $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2607
2608 $args = [
2609 'headers' => [
2610 'Content-Type' => 'application/json',
2611 ],
2612 'body' => wp_json_encode([
2613 'contents' => [
2614 [
2615 'role' => 'user',
2616 'parts' => [
2617 ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
2618 ]
2619 ]
2620 ],
2621 'generationConfig' => [
2622 'temperature' => 0.2,
2623 'maxOutputTokens' => 20,
2624 ],
2625 ]),
2626 'method' => 'POST',
2627 'timeout' => 15,
2628 ];
2629
2630 $response = wp_remote_post($url, $args);
2631 if (is_wp_error($response)) {
2632 return sanitize_text_field($user_query);
2633 }
2634
2635 $body = json_decode(wp_remote_retrieve_body($response), true);
2636 if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
2637 return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
2638 }
2639
2640 return sanitize_text_field($user_query);
2641 }
2642
2643 /**
2644 * Interpret query using X.AI (Grok) models
2645 */
2646 private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
2647 $url = 'https://api.xai.com/v1/chat/completions';
2648
2649 $args = [
2650 'headers' => [
2651 'Content-Type' => 'application/json',
2652 'Authorization' => 'Bearer ' . $api_key,
2653 ],
2654 'body' => wp_json_encode([
2655 'model' => $model,
2656 'messages' => [
2657 ['role' => 'system', 'content' => $system_prompt],
2658 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2659 ],
2660 'temperature' => 0.2,
2661 'max_tokens' => 20,
2662 ]),
2663 'method' => 'POST',
2664 'timeout' => 15,
2665 ];
2666
2667 $response = wp_remote_post($url, $args);
2668 if (is_wp_error($response)) {
2669 return sanitize_text_field($user_query);
2670 }
2671
2672 $body = json_decode(wp_remote_retrieve_body($response), true);
2673 if (isset($body['choices'][0]['message']['content'])) {
2674 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
2675 }
2676
2677 return sanitize_text_field($user_query);
2678 }
2679
2680 /**
2681 * Interpret query using DeepSeek models
2682 */
2683 private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
2684 $url = 'https://api.deepseek.com/v1/chat/completions';
2685
2686 $args = [
2687 'headers' => [
2688 'Content-Type' => 'application/json',
2689 'Authorization' => 'Bearer ' . $api_key,
2690 ],
2691 'body' => wp_json_encode([
2692 'model' => $model,
2693 'messages' => [
2694 ['role' => 'system', 'content' => $system_prompt],
2695 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2696 ],
2697 'temperature' => 0.2,
2698 'max_tokens' => 20,
2699 ]),
2700 'method' => 'POST',
2701 'timeout' => 15,
2702 ];
2703
2704 $response = wp_remote_post($url, $args);
2705 if (is_wp_error($response)) {
2706 return sanitize_text_field($user_query);
2707 }
2708
2709 $body = json_decode(wp_remote_retrieve_body($response), true);
2710 if (isset($body['choices'][0]['message']['content'])) {
2711 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
2712 }
2713
2714 return sanitize_text_field($user_query);
2715 }
2716
2717 //very good
2718 private function add_email_to_loops($email) {
2719 // Sanitize the email
2720 $email = sanitize_email($email);
2721
2722 // Retrieve and sanitize options
2723 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
2724 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
2725
2726 // Check for missing API key or mailing list ID
2727 if (empty($api_key) || empty($mailing_list_id)) {
2728 //error_log(esc_html__('Loops API key or mailing list ID is missing.', 'mxchat'));
2729 return;
2730 }
2731
2732 $data = array(
2733 'email' => $email,
2734 'subscribed' => true,
2735 'source' => __('MxChat AI Chatbot', 'mxchat'),
2736 'mailingLists' => array($mailing_list_id => true),
2737 );
2738
2739 $url = 'https://app.loops.so/api/v1/contacts/create';
2740 $args = array(
2741 'body' => wp_json_encode($data),
2742 'headers' => array(
2743 'Authorization' => 'Bearer ' . $api_key,
2744 'Content-Type' => 'application/json',
2745 ),
2746 'method' => 'POST',
2747 'timeout' => 45,
2748 );
2749
2750 $response = wp_remote_post($url, $args);
2751
2752 // Handle errors in the API request
2753 if (is_wp_error($response)) {
2754 //error_log(esc_html__('Error adding email to Loops: ', 'mxchat') . $response->get_error_message());
2755 return;
2756 }
2757
2758 // Check for non-200 HTTP responses
2759 $response_code = wp_remote_retrieve_response_code($response);
2760 if ($response_code != 200) {
2761 $response_body = wp_remote_retrieve_body($response);
2762 //error_log(esc_html__('Loops API responded with code ', 'mxchat') . $response_code . ': ' . $response_body);
2763 }
2764 }
2765
2766 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
2767 // Get the maximum number of pages allowed from admin settings
2768 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2769
2770 // Retrieve options for dynamic texts
2771 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
2772 $success_text = $this->options['pdf_intent_success_text'] ?? __("I've processed the PDF. What questions do you have about it?", 'mxchat');
2773 $error_text = $this->options['pdf_intent_error_text'] ?? __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
2774
2775 // Check for explicit request for new PDF
2776 $new_pdf_requested = stripos($message, 'new') !== false ||
2777 stripos($message, 'another') !== false ||
2778 stripos($message, 'different') !== false;
2779
2780 // If user mentions adding/reading a PDF, set waiting flag
2781 if (stripos($message, 'pdf') !== false ||
2782 stripos($message, 'document') !== false ||
2783 stripos($message, 'read') !== false) {
2784 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
2785 $this->fallbackResponse['text'] = $trigger_text;
2786 return;
2787 }
2788
2789 // If we're waiting for a URL or user requested new PDF
2790 if ($new_pdf_requested || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
2791 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
2792 // Process URL... (rest of your existing URL processing code)
2793 } else {
2794 $this->fallbackResponse['text'] = $trigger_text;
2795 }
2796 return;
2797 }
2798
2799 // Default to proceeding with conversation if no specific PDF action is needed
2800 $this->fallbackResponse['text'] = '';
2801 }
2802
2803
2804 /**
2805 * Enhanced fetch_and_split_pdf_pages with SSRF protection
2806 */
2807 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2808 // CLEAR DEBUG LOGGING
2809 //error_log("=== MXCHAT PDF PROCESSING START ===");
2810 //error_log("PDF Source: " . $pdf_source);
2811 //error_log("Max Pages: " . $max_pages);
2812 //error_log("Session ID: " . ($this->session_id ?? 'not set'));
2813
2814 // Check if Advanced Claude Toolbar is available and enabled
2815 $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
2816 $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
2817
2818 //error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2819 //error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2820
2821 if ($claude_available && $claude_enabled) {
2822 //error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2823
2824 // Attempt Claude processing first
2825 $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
2826
2827 if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
2828 //error_log("�
2829 CLAUDE PROCESSING SUCCESSFUL!");
2830 //error_log("Claude returned " . count($claude_result) . " processed pages");
2831
2832 // Log first page details for verification
2833 if (isset($claude_result[0])) {
2834 $first_page = $claude_result[0];
2835 //error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2836 //error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2837 //error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2838 }
2839
2840 //error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2841 return $claude_result;
2842 } else {
2843 //error_log(" CLAUDE PROCESSING FAILED or returned invalid result");
2844 //error_log("Claude result type: " . gettype($claude_result));
2845 if (is_array($claude_result)) {
2846 //error_log("Claude result count: " . count($claude_result));
2847 }
2848 }
2849 }
2850
2851 // Fallback to basic processing
2852 //error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2853
2854 $upload_dir = wp_upload_dir();
2855 $temp_file = null;
2856
2857 try {
2858 // Your existing basic processing code here...
2859 // (I'll include the key parts with debug logging)
2860
2861 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2862 //error_log("Downloading PDF from URL...");
2863
2864 // SECURITY FIX: Validate URL before processing
2865 if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
2866 //error_log(" SECURITY: Blocked unsafe PDF URL");
2867 return false;
2868 }
2869
2870 $temp_file = wp_tempnam($pdf_source);
2871
2872 // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
2873 $response = wp_safe_remote_get($pdf_source, [
2874 'timeout' => 60,
2875 'headers' => ['User-Agent' => 'MxChat PDF Processor']
2876 ]);
2877
2878 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
2879 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
2880 //error_log(" BASIC PROCESSING: Failed to download PDF: " . $error_message);
2881 return false;
2882 }
2883
2884 file_put_contents($temp_file, wp_remote_retrieve_body($response));
2885 //error_log("�
2886 PDF downloaded successfully");
2887 } else {
2888 $temp_file = $pdf_source;
2889 //error_log("Using local PDF file: " . $temp_file);
2890 }
2891
2892 // Parse PDF
2893 //error_log("Parsing PDF with basic parser...");
2894 $parser = new \Smalot\PdfParser\Parser();
2895 $pdf = $parser->parseFile($temp_file);
2896 $pages = $pdf->getPages();
2897
2898 //error_log("PDF contains " . count($pages) . " pages");
2899
2900 if (count($pages) > $max_pages) {
2901 //error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2902 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2903 unlink($temp_file);
2904 }
2905 return 'too_many_pages';
2906 }
2907
2908 $embeddings = [];
2909 $processed_pages = 0;
2910
2911 foreach ($pages as $page_number => $page) {
2912 $text = $page->getText();
2913
2914 if (empty(trim($text))) {
2915 //error_log("Skipping empty page: " . ($page_number + 1));
2916 continue;
2917 }
2918
2919 $text = $this->mxchat_clean_text($text);
2920
2921 $embedding = $this->mxchat_generate_embedding(
2922 __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2923 $this->options['api_key']
2924 );
2925
2926 if ($embedding) {
2927 $embeddings[] = [
2928 'page_number' => $page_number + 1,
2929 'embedding' => $embedding,
2930 'text' => $text,
2931 'enhanced' => false, // CLEARLY MARK AS BASIC
2932 'processing_method' => 'basic_pdf_parser'
2933 ];
2934 $processed_pages++;
2935 }
2936 }
2937
2938 //error_log("�
2939 BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2940
2941 // Cleanup
2942 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2943 unlink($temp_file);
2944 }
2945
2946 //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
2947 return $embeddings;
2948
2949 } catch (\Exception $e) {
2950 //error_log(" BASIC PROCESSING ERROR: " . $e->getMessage());
2951 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2952 unlink($temp_file);
2953 }
2954 //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
2955 return false;
2956 }
2957 }
2958
2959
2960 /**
2961 * Validate PDF URL for security
2962 * Prevents SSRF attacks by blocking dangerous URLs
2963 */
2964
2965 private function mxchat_is_safe_pdf_url($url) {
2966 // Use WordPress core function for comprehensive validation
2967 // This blocks localhost, private IPs, and reserved IP ranges
2968 $validated_url = wp_http_validate_url($url);
2969
2970 if ($validated_url === false) {
2971 return false;
2972 }
2973
2974 // Additional check: only allow HTTP/HTTPS schemes
2975 $parsed = parse_url($url);
2976 if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
2977 return false;
2978 }
2979
2980 return true;
2981 }
2982
2983
2984 private function mxchat_clean_text($text) {
2985 // Remove excessive whitespace
2986 $text = preg_replace('/\s+/', ' ', $text);
2987
2988 // Remove control characters except newlines and tabs
2989 $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
2990
2991 // Normalize line endings
2992 $text = str_replace(["\r\n", "\r"], "\n", $text);
2993
2994 // Trim whitespace
2995 $text = trim($text);
2996
2997 return $text;
2998 }
2999
3000 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
3001 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
3002
3003 $most_relevant = null;
3004 $highest_similarity = -INF;
3005
3006 foreach ($embeddings as $page_data) {
3007 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
3008
3009 if ($similarity > $highest_similarity) {
3010 $highest_similarity = $similarity;
3011 $most_relevant = $page_data['page_number'];
3012 }
3013 }
3014
3015 if (!is_null($most_relevant)) {
3016 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
3017 return array_filter($embeddings, function ($page) use ($page_numbers) {
3018 return in_array($page['page_number'], $page_numbers);
3019 });
3020 }
3021
3022 return [];
3023 }
3024
3025
3026 public function handle_pdf_upload() {
3027 check_ajax_referer('mxchat_chat_nonce', 'nonce');
3028
3029 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
3030 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3031 return;
3032 }
3033
3034 // SECURITY FIX: Check if PDF uploads are enabled in settings
3035 $options = get_option('mxchat_options', array());
3036 $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3037
3038 if ($show_pdf_button !== 'on') {
3039 wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3040 return;
3041 }
3042
3043 $file = $_FILES['pdf_file'];
3044 $session_id = sanitize_text_field($_POST['session_id']);
3045 $original_filename = sanitize_text_field($file['name']);
3046
3047 // SECURITY FIX: Verify session ownership before allowing upload
3048 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3049 $session_owner = get_option("mxchat_session_owner_{$session_id}");
3050
3051 if ($session_owner && $session_owner !== $current_user_identifier) {
3052 wp_send_json_error(esc_html__('Unauthorized access.', 'mxchat'));
3053 return;
3054 }
3055
3056 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3057 if ($file_type['type'] !== 'application/pdf') {
3058 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3059 return;
3060 }
3061
3062 $upload_dir = wp_upload_dir();
3063
3064 // SECURITY FIX: Generate random filename without exposing session_id
3065 $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3066 $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
3067 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3068
3069 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3070 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
3071 return;
3072 }
3073
3074 $this->clear_pdf_transients($session_id);
3075
3076 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
3077 $embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
3078
3079 if ($embeddings === 'too_many_pages') {
3080 unlink($pdf_path);
3081 $error_message = sprintf(
3082 $this->options['pdf_intent_error_text'] ??
3083 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
3084 $max_pages
3085 );
3086 wp_send_json_error($error_message);
3087 return;
3088 }
3089
3090 if ($embeddings === false || empty($embeddings)) {
3091 unlink($pdf_path);
3092 $error_message = $this->options['pdf_intent_error_text'] ??
3093 esc_html__('The uploaded PDF appears to be empty or contains unsupported content.', 'mxchat');
3094 wp_send_json_error($error_message);
3095 return;
3096 }
3097
3098 if (!empty($embeddings)) {
3099 // Store the mapping between session and the random filename
3100 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3101 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3102 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3103 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
3104
3105 $success_message = $this->options['pdf_intent_success_text'] ??
3106 esc_html__("I've processed the PDF. What questions do you have about it?", 'mxchat');
3107
3108 wp_send_json_success([
3109 'message' => $success_message,
3110 'filename' => $original_filename
3111 ]);
3112 return;
3113 }
3114
3115 unlink($pdf_path);
3116 $error_message = $this->options['pdf_intent_error_text'] ??
3117 esc_html__('Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.', 'mxchat');
3118 wp_send_json_error($error_message);
3119 return;
3120 }
3121 public function handle_pdf_remove() {
3122 check_ajax_referer('mxchat_chat_nonce', 'nonce');
3123
3124 if (empty($_POST['session_id'])) {
3125 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
3126 wp_die();
3127 }
3128
3129 $session_id = sanitize_text_field($_POST['session_id']);
3130 $pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
3131
3132 if ($pdf_path && file_exists($pdf_path)) {
3133 unlink($pdf_path);
3134 }
3135
3136 $this->clear_pdf_transients($session_id);
3137
3138 wp_send_json_success([
3139 'message' => esc_html__('PDF removed successfully.', 'mxchat')
3140 ]);
3141 wp_die();
3142 }
3143
3144
3145 function mxchat_fetch_new_messages() {
3146 $session_id = sanitize_text_field($_POST['session_id']);
3147 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3148 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
3149 $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
3150
3151 if (empty($session_id)) {
3152 //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
3153 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
3154 wp_die();
3155 }
3156
3157 $history = get_option("mxchat_history_{$session_id}", []);
3158
3159 error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3160 error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3161 error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3162 error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3163
3164 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3165 error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3166
3167 // If persistence is enabled, show all new messages
3168 if ($persistence_enabled) {
3169 $has_id = !empty($message['id']);
3170 $is_agent = $message['role'] === 'agent';
3171
3172 // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3173 if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3174 $is_newer = true;
3175 } else {
3176 $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3177 }
3178
3179 error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3180
3181 return $has_id && $is_newer && $is_agent;
3182 }
3183
3184 // If persistence is disabled, only show messages after initial timestamp
3185 return !empty($message['id']) &&
3186 $message['role'] === 'agent' &&
3187 $message['timestamp'] > $initial_timestamp;
3188 });
3189
3190 error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
3191
3192 wp_send_json_success([
3193 'new_messages' => array_values($new_messages)
3194 ]);
3195 wp_die();
3196 }
3197 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
3198 // First check if live agents are available
3199 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
3200 if ($live_agent_available !== 'on') {
3201 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
3202 $this->fallbackResponse = [
3203 'text' => $away_message,
3204 'html' => '',
3205 'images' => [],
3206 'chat_mode' => 'ai'
3207 ];
3208 wp_send_json([
3209 'text' => $away_message,
3210 'html' => '',
3211 'chat_mode' => 'ai',
3212 'session_id' => $session_id
3213 ]);
3214 wp_die();
3215 }
3216
3217 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3218
3219 if (empty($slack_bot_token)) {
3220 return false;
3221 }
3222
3223 // Check if channel already exists for this session
3224 $channel_id = get_option("mxchat_channel_{$session_id}", '');
3225
3226 if (empty($channel_id)) {
3227 // Create new channel with session ID as name
3228 $channel_name = $this->generate_channel_name($session_id);
3229
3230 //error_log("Attempting to create channel: $channel_name");
3231
3232 $response = wp_remote_post('https://slack.com/api/conversations.create', [
3233 'headers' => [
3234 'Content-Type' => 'application/json',
3235 'Authorization' => 'Bearer ' . $slack_bot_token
3236 ],
3237 'body' => json_encode([
3238 'name' => $channel_name,
3239 'is_private' => false // Public channel - anyone in workspace can join
3240 ])
3241 ]);
3242
3243 if (!is_wp_error($response)) {
3244 $response_body = wp_remote_retrieve_body($response);
3245 $response_data = json_decode($response_body, true);
3246
3247 //error_log("Channel creation response: " . $response_body);
3248
3249 if (isset($response_data['ok']) && $response_data['ok']) {
3250 $channel_id = $response_data['channel']['id'];
3251 $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
3252 //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
3253 update_option("mxchat_channel_{$session_id}", $channel_id);
3254
3255 // Auto-invite agents to the channel
3256 $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
3257
3258 if (!empty($agent_user_ids)) {
3259 // Parse user IDs (one per line)
3260 $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
3261
3262 foreach ($user_ids as $user_id_to_invite) {
3263 //error_log("Inviting user to channel: $user_id_to_invite");
3264
3265 $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
3266 'headers' => [
3267 'Content-Type' => 'application/json',
3268 'Authorization' => 'Bearer ' . $slack_bot_token
3269 ],
3270 'body' => json_encode([
3271 'channel' => $channel_id,
3272 'users' => $user_id_to_invite
3273 ])
3274 ]);
3275
3276 if (!is_wp_error($invite_response)) {
3277 $invite_body = wp_remote_retrieve_body($invite_response);
3278 $invite_data = json_decode($invite_body, true);
3279 //error_log("Invite response for $user_id_to_invite: " . $invite_body);
3280
3281 if (isset($invite_data['ok']) && $invite_data['ok']) {
3282 //error_log("Successfully invited user $user_id_to_invite to channel");
3283 } else {
3284 //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
3285 }
3286 } else {
3287 //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
3288 }
3289 }
3290 } else {
3291 //error_log("No agent user IDs configured for auto-invite");
3292 }
3293 } else {
3294 //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
3295 }
3296 } else {
3297 //error_log("WP Error creating channel: " . $response->get_error_message());
3298 }
3299
3300 if (empty($channel_id)) {
3301 return false; // Failed to create channel
3302 }
3303 }
3304
3305 // Get recent chat history
3306 $history = get_option("mxchat_history_{$session_id}", []);
3307 $recent_history = array_slice($history, -5);
3308
3309 // Format conversation context
3310 $conversation_context = "";
3311 if (!empty($recent_history)) {
3312 $conversation_context = "*Recent Conversation:*\n";
3313 foreach ($recent_history as $hist_message) {
3314 $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
3315 $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
3316 }
3317 $conversation_context .= "\n";
3318 }
3319
3320 update_option("mxchat_mode_{$session_id}", 'agent');
3321
3322 // Send message to channel
3323 $channel_message = "🔔 *New Live Agent Request*\n\n";
3324 $channel_message .= "*Session ID:* `{$session_id}`\n";
3325 $channel_message .= "*User ID:* `{$user_id}`\n\n";
3326
3327 if (!empty($conversation_context)) {
3328 $channel_message .= $conversation_context;
3329 }
3330
3331 $channel_message .= "*Current Message:*\n{$message}\n\n";
3332 $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
3333
3334 wp_remote_post('https://slack.com/api/chat.postMessage', [
3335 'headers' => [
3336 'Content-Type' => 'application/json',
3337 'Authorization' => 'Bearer ' . $slack_bot_token
3338 ],
3339 'body' => json_encode([
3340 'channel' => $channel_id,
3341 'text' => $channel_message,
3342 'mrkdwn' => true
3343 ])
3344 ]);
3345
3346 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
3347 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
3348
3349 $this->fallbackResponse = [
3350 'text' => $success_message,
3351 'html' => '',
3352 'images' => [],
3353 'chat_mode' => 'agent'
3354 ];
3355
3356 wp_send_json([
3357 'success' => true,
3358 'text' => $success_message,
3359 'html' => '',
3360 'chat_mode' => 'agent',
3361 'session_id' => $session_id,
3362 'fallbackResponse' => $this->fallbackResponse
3363 ]);
3364 wp_die();
3365 }
3366
3367 private function generate_channel_name($session_id) {
3368 $email = null;
3369 $name = null;
3370
3371 // 1. First priority: Check if user is logged in and get their info
3372 if (is_user_logged_in()) {
3373 $current_user = wp_get_current_user();
3374 if (!empty($current_user->user_email)) {
3375 $email = $current_user->user_email;
3376 //error_log("[DEBUG] Using logged-in user email for channel: {$email}");
3377 }
3378 if (!empty($current_user->display_name)) {
3379 $name = $current_user->display_name;
3380 //error_log("[DEBUG] Using logged-in user name for channel: {$name}");
3381 }
3382 }
3383
3384 // 2. Second priority: Check for saved email/name from "require email to chat" option
3385 if (empty($email)) {
3386 $email_option_key = "mxchat_email_{$session_id}";
3387 $saved_email = get_option($email_option_key);
3388 if (!empty($saved_email)) {
3389 $email = $saved_email;
3390 //error_log("[DEBUG] Using saved email from session for channel: {$email}");
3391 }
3392 }
3393
3394 if (empty($name)) {
3395 $name_option_key = "mxchat_name_{$session_id}";
3396 $saved_name = get_option($name_option_key);
3397 if (!empty($saved_name)) {
3398 $name = $saved_name;
3399 //error_log("[DEBUG] Using saved name from session for channel: {$name}");
3400 }
3401 }
3402
3403 // 3. Third priority: Check existing chat transcript for email/name
3404 if (empty($email) || empty($name)) {
3405 global $wpdb;
3406 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
3407 $existing_data = $wpdb->get_row($wpdb->prepare(
3408 "SELECT user_email, user_name FROM $table_name WHERE session_id = %s AND (user_email IS NOT NULL OR user_name IS NOT NULL) LIMIT 1",
3409 $session_id
3410 ));
3411
3412 if ($existing_data) {
3413 if (empty($email) && !empty($existing_data->user_email)) {
3414 $email = $existing_data->user_email;
3415 //error_log("[DEBUG] Using email from chat transcript for channel: {$email}");
3416 }
3417 if (empty($name) && !empty($existing_data->user_name)) {
3418 $name = $existing_data->user_name;
3419 //error_log("[DEBUG] Using name from chat transcript for channel: {$name}");
3420 }
3421 }
3422 }
3423
3424 // 4. Generate channel name based on priority: Name > Email > Session ID
3425 $channel_name = '';
3426
3427 if (!empty($name)) {
3428 // Convert name to valid Slack channel name
3429 $base_name = strtolower(trim($name));
3430 // Replace spaces and invalid characters
3431 $base_name = preg_replace('/[^a-z0-9\s]/', '', $base_name);
3432 $base_name = preg_replace('/\s+/', '-', $base_name);
3433 $base_name = trim($base_name, '-');
3434
3435 // Get last 4 characters of session ID for uniqueness
3436 $session_suffix = substr($session_id, -4);
3437 $channel_name = 'chat-' . $base_name . '-' . strtolower($session_suffix);
3438
3439 // Slack channel names have a 21 character limit
3440 if (strlen($channel_name) > 21) {
3441 // Calculate available space for name (21 - 'chat-' - '-' - session_suffix)
3442 $available_space = 21 - 5 - 1 - strlen($session_suffix); // 'chat-' = 5, '-' = 1
3443 $truncated_name = substr($base_name, 0, $available_space);
3444 $truncated_name = rtrim($truncated_name, '-'); // Remove trailing hyphen
3445 $channel_name = 'chat-' . $truncated_name . '-' . strtolower($session_suffix);
3446 }
3447
3448 //error_log("[DEBUG] Using name for channel: {$channel_name} (from name: {$name})");
3449
3450 } elseif (!empty($email)) {
3451 // Convert email to valid Slack channel name (your existing logic)
3452 $channel_name = 'chat-' . strtolower(str_replace(['@', '.', '+', '_'], ['-at-', '-', '-plus-', '-'], $email));
3453 // Remove any remaining invalid characters
3454 $channel_name = preg_replace('/[^a-z0-9\-]/', '', $channel_name);
3455 // Ensure it doesn't end with a hyphen
3456 $channel_name = rtrim($channel_name, '-');
3457 // Slack channel names have a 21 character limit, so truncate if needed
3458 if (strlen($channel_name) > 21) {
3459 $channel_name = substr($channel_name, 0, 21);
3460 $channel_name = rtrim($channel_name, '-'); // Remove trailing hyphen if truncation created one
3461 }
3462
3463 //error_log("[DEBUG] Using email for channel: {$channel_name} (from email: {$email})");
3464
3465 } else {
3466 // Fallback to session ID if no name or email found
3467 $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
3468 //error_log("[DEBUG] No name or email found, using session ID for channel: {$channel_name}");
3469 }
3470
3471 // Final validation - ensure channel name meets Slack requirements
3472 if (strlen($channel_name) > 21) {
3473 $channel_name = substr($channel_name, 0, 21);
3474 $channel_name = rtrim($channel_name, '-');
3475 }
3476
3477 //error_log("[DEBUG] Generated channel name: {$channel_name}");
3478 return $channel_name;
3479 }
3480 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
3481 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3482 $channel_id = get_option("mxchat_channel_{$session_id}", '');
3483
3484 if (empty($slack_bot_token) || empty($channel_id)) {
3485 return false;
3486 }
3487
3488 $user_message = "💬 *User:* {$message}";
3489
3490 $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
3491 'headers' => [
3492 'Content-Type' => 'application/json',
3493 'Authorization' => 'Bearer ' . $slack_bot_token
3494 ],
3495 'body' => json_encode([
3496 'channel' => $channel_id,
3497 'text' => $user_message,
3498 'mrkdwn' => true
3499 ])
3500 ]);
3501
3502 return !is_wp_error($response);
3503 }
3504 public function handle_slack_interaction(WP_REST_Request $request) {
3505 //error_log('Received Slack interaction');
3506
3507 $payload = json_decode($request->get_param('payload'), true);
3508 //error_log('Payload: ' . print_r($payload, true));
3509
3510 // Handle button click
3511 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
3512 $session_id = $payload['actions'][0]['value'];
3513 $trigger_id = $payload['trigger_id'];
3514
3515 // Get Bot Token from settings
3516 $slack_token = $this->options['live_agent_bot_token'] ?? '';
3517
3518 if (empty($slack_token)) {
3519 //error_log('Slack Bot Token not configured');
3520 return new WP_REST_Response(['error' => esc_html__('Bot token not configured', 'mxchat')], 400);
3521 }
3522 $response = wp_remote_post('https://slack.com/api/views.open', [
3523 'headers' => [
3524 'Content-Type' => 'application/json',
3525 'Authorization' => 'Bearer ' . $slack_token
3526 ],
3527 'body' => json_encode([
3528 'trigger_id' => $trigger_id,
3529 'view' => [
3530 'type' => 'modal',
3531 'callback_id' => 'reply_modal',
3532 'title' => [
3533 'type' => 'plain_text',
3534 'text' => __('Reply to User', 'mxchat')
3535 ],
3536 'submit' => [
3537 'type' => 'plain_text',
3538 'text' => __('Send', 'mxchat')
3539 ],
3540 'close' => [
3541 'type' => 'plain_text',
3542 'text' => __('Cancel', 'mxchat')
3543 ],
3544 'blocks' => [
3545 [
3546 'type' => 'input',
3547 'block_id' => 'reply_block',
3548 'label' => [
3549 'type' => 'plain_text',
3550 'text' => sprintf(__('Reply to session: %s', 'mxchat'), $session_id)
3551 ],
3552 'element' => [
3553 'type' => 'plain_text_input',
3554 'action_id' => 'message',
3555 'multiline' => true,
3556 'placeholder' => [
3557 'type' => 'plain_text',
3558 'text' => __('Type your message here...', 'mxchat')
3559 ]
3560 ]
3561 ]
3562 ],
3563 'private_metadata' => $session_id
3564 ]
3565 ])
3566 ]);
3567
3568 //error_log('Views.open response: ' . print_r($response, true));
3569
3570 // Return immediate acknowledgment
3571 return new WP_REST_Response(['ok' => true]);
3572 }
3573
3574 // Handle modal submission
3575 // Handle modal submission
3576 if ($payload['type'] === 'view_submission') {
3577 $session_id = $payload['view']['private_metadata'];
3578 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
3579
3580 // Save the message (keep the message_id but don't include in response)
3581 $this->mxchat_save_chat_message($session_id, 'agent', $message);
3582
3583 // Keep the original response format for Slack
3584 return new WP_REST_Response([
3585 'response_action' => 'clear'
3586 ]);
3587 }
3588
3589 // Default acknowledgment
3590 return new WP_REST_Response(['ok' => true]);
3591 }
3592 public function mxchat_handle_agent_response(WP_REST_Request $request) {
3593 //error_log('Received agent response request');
3594 //error_log('Request data: ' . print_r($request->get_params(), true));
3595 // //error_log('Raw body: ' . file_get_contents('php://input'));
3596
3597 // Get the data from Slack's slash command format
3598 $command_text = $request->get_param('text');
3599 // //error_log('Command text: ' . $command_text);
3600
3601 if (empty($command_text)) {
3602 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
3603 return new WP_REST_Response([
3604 'error' => esc_html__('Command text is required. Format: /reply session_id message', 'mxchat')
3605 ], 400);
3606 }
3607
3608 // Split the command text into session_id and message
3609 $parts = explode(' ', $command_text, 2);
3610 if (count($parts) !== 2) {
3611 //error_log('Agent response error: Invalid command format');
3612 return new WP_REST_Response([
3613 'error' => esc_html__('Invalid format. Use: /reply session_id message', 'mxchat')
3614 ], 400);
3615 }
3616
3617 $session_id = sanitize_text_field($parts[0]);
3618 $message = sanitize_text_field($parts[1]);
3619
3620 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
3621
3622 // Save the message
3623 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
3624
3625 if (!$message_id) {
3626 // //error_log('Failed to save agent message');
3627 return new WP_REST_Response([
3628 'error' => esc_html__('Failed to save message', 'mxchat')
3629 ], 500);
3630 }
3631
3632 // Return success response in Slack's expected format
3633 return new WP_REST_Response([
3634 'response_type' => 'in_channel',
3635 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
3636 ], 200);
3637 }
3638 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
3639 // Update mode to AI
3640 update_option("mxchat_mode_{$session_id}", 'ai');
3641
3642 // Clear any existing PDF context to start fresh
3643 $this->clear_pdf_transients($session_id);
3644
3645 // Set the response with explicit chat_mode
3646 $this->fallbackResponse = [
3647 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
3648 'html' => '',
3649 'images' => [],
3650 'chat_mode' => 'ai' // Ensure this is set
3651 ];
3652
3653 // Return the complete response array instead of just true
3654 return $this->fallbackResponse;
3655 }
3656
3657 public function handle_slack_messages(WP_REST_Request $request) {
3658 // Log the incoming request for debugging
3659 //error_log('Slack events request received: ' . $request->get_body());
3660
3661 $body = $request->get_body();
3662 $data = json_decode($body, true);
3663
3664 // Handle Slack URL verification
3665 if (isset($data['type']) && $data['type'] === 'url_verification') {
3666 //error_log('Slack URL verification challenge: ' . $data['challenge']);
3667 return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
3668 }
3669
3670 // IMPORTANT: Handle Slack's event deduplication
3671 if (isset($data['event_id'])) {
3672 $event_id = $data['event_id'];
3673 $processed_events = get_transient('mxchat_slack_events') ?: [];
3674
3675 // Check if we've already processed this event
3676 if (in_array($event_id, $processed_events)) {
3677 //error_log("Duplicate event detected: $event_id");
3678 return new WP_REST_Response(['ok' => true]);
3679 }
3680
3681 // Add this event to processed list
3682 $processed_events[] = $event_id;
3683 // Keep only last 100 events to prevent memory issues
3684 if (count($processed_events) > 100) {
3685 $processed_events = array_slice($processed_events, -100);
3686 }
3687 // Store for 1 hour
3688 set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
3689 }
3690
3691 // Handle message events
3692 if (isset($data['event']) && $data['event']['type'] === 'message') {
3693 $event = $data['event'];
3694
3695 // Skip bot messages and messages with subtypes (like bot_message)
3696 if (isset($event['bot_id']) || isset($event['subtype'])) {
3697 return new WP_REST_Response(['ok' => true]);
3698 }
3699
3700 // Additional check: Skip if this is a threaded reply to our confirmation
3701 if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
3702 return new WP_REST_Response(['ok' => true]);
3703 }
3704
3705 $channel_id = $event['channel'];
3706 $message_text = $event['text'] ?? '';
3707 $message_ts = $event['ts'] ?? '';
3708
3709 // Find session ID by looking for matching channel
3710 global $wpdb;
3711 $session_option = $wpdb->get_var(
3712 $wpdb->prepare(
3713 "SELECT option_name FROM {$wpdb->options}
3714 WHERE option_name LIKE 'mxchat_channel_%'
3715 AND option_value = %s",
3716 $channel_id
3717 )
3718 );
3719
3720 if ($session_option) {
3721 $session_id = str_replace('mxchat_channel_', '', $session_option);
3722
3723 // Create a unique key for this specific message
3724 $message_key = md5($session_id . $message_ts . $message_text);
3725 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
3726
3727 // Check if we've already processed this exact message
3728 if (in_array($message_key, $processed_messages)) {
3729 //error_log("Duplicate message detected for session $session_id");
3730 return new WP_REST_Response(['ok' => true]);
3731 }
3732
3733 // Add to processed messages
3734 $processed_messages[] = $message_key;
3735 // Keep only last 50 messages per session
3736 if (count($processed_messages) > 50) {
3737 $processed_messages = array_slice($processed_messages, -50);
3738 }
3739 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
3740
3741 // Save the agent message
3742 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
3743
3744 // Send confirmation back to Slack (only once)
3745 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3746 if (!empty($slack_bot_token)) {
3747 // Use a transient to prevent duplicate confirmations
3748 $confirm_key = 'mxchat_confirm_' . $message_key;
3749 if (!get_transient($confirm_key)) {
3750 wp_remote_post('https://slack.com/api/chat.postMessage', [
3751 'headers' => [
3752 'Content-Type' => 'application/json',
3753 'Authorization' => 'Bearer ' . $slack_bot_token
3754 ],
3755 'body' => json_encode([
3756 'channel' => $channel_id,
3757 'text' => "✅ _Message sent to user_",
3758 'thread_ts' => $event['ts'] // Reply in thread
3759 ])
3760 ]);
3761 // Set transient to prevent duplicate confirmations
3762 set_transient($confirm_key, true, 300); // 5 minutes
3763 }
3764 }
3765 }
3766 }
3767
3768 return new WP_REST_Response(['ok' => true]);
3769 }
3770
3771 // For the word upload handler
3772 public function mxchat_handle_word_upload() {
3773 // Delegate to word handler
3774 $this->word_handler->mxchat_handle_word_upload();
3775 }
3776
3777 // For the word removal handler
3778 public function mxchat_handle_word_remove() {
3779 // Delegate to word handler
3780 $this->word_handler->mxchat_handle_word_remove();
3781 }
3782
3783 // For the word status check
3784 public function mxchat_check_word_status() {
3785 // Delegate to word handler
3786 $this->word_handler->mxchat_check_word_status();
3787 }
3788
3789
3790 private function mxchat_get_user_identifier() {
3791 return MxChat_User::mxchat_get_user_identifier();
3792 }
3793
3794 private function mxchat_generate_embedding($text, $api_key) {
3795 try {
3796 // Get options and selected model
3797 $options = get_option('mxchat_options');
3798 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3799
3800 // Determine endpoint and API key based on model
3801 if (strpos($selected_model, 'voyage') === 0) {
3802 $endpoint = 'https://api.voyageai.com/v1/embeddings';
3803 $api_key = $options['voyage_api_key'] ?? '';
3804
3805 // Check if Voyage API key is missing
3806 if (empty($api_key)) {
3807 //error_log('Voyage API key is missing');
3808 return [
3809 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
3810 'error_code' => 'missing_voyage_api_key'
3811 ];
3812 }
3813 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3814 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3815 $api_key = $options['gemini_api_key'] ?? '';
3816
3817 // Check if Gemini API key is missing
3818 if (empty($api_key)) {
3819 //error_log('Gemini API key is missing');
3820 return [
3821 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3822 'error_code' => 'missing_gemini_api_key'
3823 ];
3824 }
3825 } else {
3826 $endpoint = 'https://api.openai.com/v1/embeddings';
3827 // Use the passed API key for OpenAI
3828
3829 // Check if OpenAI API key is missing
3830 if (empty($api_key)) {
3831 //error_log('OpenAI API key is missing');
3832 return [
3833 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3834 'error_code' => 'missing_openai_api_key'
3835 ];
3836 }
3837 }
3838
3839 // Check if text is empty
3840 if (empty($text)) {
3841 //error_log('Empty text provided for embedding generation');
3842 return [
3843 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
3844 'error_code' => 'empty_embedding_text'
3845 ];
3846 }
3847
3848 // Prepare request body based on provider
3849 if (strpos($selected_model, 'gemini-embedding') === 0) {
3850 // Gemini API format
3851 $request_body = [
3852 'model' => 'models/' . $selected_model,
3853 'content' => [
3854 'parts' => [
3855 ['text' => $text]
3856 ]
3857 ],
3858 'outputDimensionality' => 1536
3859 ];
3860
3861 // Prepare headers for Gemini (API key as query parameter)
3862 $endpoint .= '?key=' . $api_key;
3863 $headers = [
3864 'Content-Type' => 'application/json'
3865 ];
3866 } else {
3867 // OpenAI/Voyage API format
3868 $request_body = [
3869 'input' => $text,
3870 'model' => $selected_model
3871 ];
3872
3873 // Add output_dimension for voyage-3-large
3874 if ($selected_model === 'voyage-3-large') {
3875 $request_body['output_dimension'] = 2048;
3876 }
3877
3878 // Prepare headers for OpenAI/Voyage
3879 $headers = [
3880 'Content-Type' => 'application/json',
3881 'Authorization' => 'Bearer ' . $api_key
3882 ];
3883 }
3884
3885 // Prepare request arguments
3886 $args = [
3887 'body' => wp_json_encode($request_body),
3888 'headers' => $headers,
3889 'timeout' => 60,
3890 'redirection' => 5,
3891 'blocking' => true,
3892 'httpversion' => '1.0',
3893 'sslverify' => true,
3894 ];
3895
3896 // Make the request
3897 $response = wp_remote_post($endpoint, $args);
3898
3899 // Handle WordPress errors
3900 if (is_wp_error($response)) {
3901 $error_message = $response->get_error_message();
3902 //error_log('Embedding Generation Error: ' . $error_message);
3903 return [
3904 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
3905 'error_code' => 'embedding_connection_error'
3906 ];
3907 }
3908
3909 // Check HTTP status code
3910 $status_code = wp_remote_retrieve_response_code($response);
3911 if ($status_code !== 200) {
3912 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3913
3914 $error_message = isset($response_body['error']['message'])
3915 ? $response_body['error']['message']
3916 : 'HTTP Error ' . $status_code;
3917
3918 $error_type = isset($response_body['error']['type'])
3919 ? $response_body['error']['type']
3920 : 'unknown';
3921
3922 //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
3923
3924 // Handle specific error types
3925 switch ($error_type) {
3926 case 'invalid_request_error':
3927 if (strpos($error_message, 'API key') !== false) {
3928 return [
3929 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
3930 'error_code' => 'embedding_invalid_api_key'
3931 ];
3932 }
3933 break;
3934
3935 case 'authentication_error':
3936 return [
3937 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
3938 'error_code' => 'embedding_auth_error'
3939 ];
3940
3941 case 'rate_limit_exceeded':
3942 return [
3943 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
3944 'error_code' => 'embedding_rate_limit'
3945 ];
3946
3947 case 'quota_exceeded':
3948 return [
3949 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
3950 'error_code' => 'embedding_quota_exceeded'
3951 ];
3952 }
3953
3954 // Generic error fallback
3955 return [
3956 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3957 'error_code' => 'embedding_api_error',
3958 'status_code' => $status_code
3959 ];
3960 }
3961
3962 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3963
3964 // Handle different response formats based on provider
3965 if (strpos($selected_model, 'gemini-embedding') === 0) {
3966 // Gemini API response format
3967 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3968 return $response_body['embedding']['values'];
3969 } else {
3970 //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3971 return [
3972 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3973 'error_code' => 'invalid_gemini_embedding_response'
3974 ];
3975 }
3976 } else {
3977 // OpenAI/Voyage API response format
3978 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3979 return $response_body['data'][0]['embedding'];
3980 } else {
3981 //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3982 return [
3983 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3984 'error_code' => 'invalid_embedding_response'
3985 ];
3986 }
3987 }
3988 } catch (Exception $e) {
3989 //error_log('Embedding Exception: ' . $e->getMessage());
3990 return [
3991 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3992 'error_code' => 'embedding_exception'
3993 ];
3994 }
3995 }
3996
3997
3998 private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default') {
3999 error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
4000
4001 // Get bot-specific Pinecone configuration
4002 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
4003
4004 // Debug: Log the Pinecone configuration
4005 error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
4006 error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
4007 error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
4008 error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
4009 error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
4010
4011 // Determine whether to use Pinecone based on bot configuration
4012 $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
4013
4014 error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
4015
4016 if ($use_pinecone) {
4017 return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
4018 } else {
4019 return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
4020 }
4021 }
4022
4023 private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
4024 global $wpdb;
4025 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4026 $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id;
4027 $batch_size = 500;
4028
4029 // Initialize similarity analysis storage
4030 $this->last_similarity_analysis = [
4031 'knowledge_base_type' => 'WordPress Database',
4032 'bot_id' => $bot_id,
4033 'top_matches' => [],
4034 'threshold_used' => 0,
4035 'total_checked' => 0
4036 ];
4037
4038 // NEW: Initialize valid URLs array
4039 $valid_urls = [];
4040
4041 // Get bot-specific options for similarity threshold
4042 $bot_options = $this->get_bot_options($bot_id);
4043 $current_options = !empty($bot_options) ? $bot_options : $this->options;
4044
4045 // Retrieve embeddings from cache or database
4046 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
4047 if ($embeddings === false) {
4048 // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
4049 $embeddings = [];
4050 $offset = 0;
4051
4052 do {
4053 // Add bot_id filter if not default and if bot_metadata column exists
4054 $bot_filter = '';
4055 if ($bot_id !== 'default') {
4056 // Check if bot_metadata column exists
4057 $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
4058 if ($column_exists) {
4059 $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
4060 }
4061 }
4062
4063 $query = $wpdb->prepare(
4064 "SELECT id, embedding_vector, article_content, source_url, role_restriction
4065 FROM {$system_prompt_table}
4066 WHERE 1=1 {$bot_filter}
4067 LIMIT %d OFFSET %d",
4068 $batch_size,
4069 $offset
4070 );
4071
4072 $batch = $wpdb->get_results($query);
4073 if (empty($batch)) {
4074 break;
4075 }
4076
4077 $embeddings = array_merge($embeddings, $batch);
4078 $offset += $batch_size;
4079 unset($batch);
4080 } while (true);
4081
4082 if (empty($embeddings)) {
4083 // Store empty array for valid URLs since no content found
4084 $this->current_valid_urls = [];
4085 return '';
4086 }
4087
4088 // Cache embeddings for future use (but note: this now includes content and role restrictions)
4089 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4090 }
4091
4092 // Get knowledge manager instance for role checking
4093 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4094
4095 // Get base similarity threshold from bot options or default options
4096 $similarity_threshold = isset($current_options['similarity_threshold'])
4097 ? ((int) $current_options['similarity_threshold']) / 100
4098 : 0.35;
4099
4100 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
4101
4102 // Calculate similarities and build results array
4103 $all_similarities = [];
4104 $url_groups = array(); // NEW: Group by source_url for chunk reassembly
4105
4106 foreach ($embeddings as $embedding) {
4107 $database_embedding = $embedding->embedding_vector
4108 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
4109 : null;
4110
4111 if (is_array($database_embedding) && is_array($user_embedding)) {
4112 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4113
4114 // Check role access
4115 $role_restriction = $embedding->role_restriction ?? 'public';
4116 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4117
4118 // Store ALL similarities for testing (top 10)
4119 $source_display = '';
4120 $source_url = $embedding->source_url ?? '';
4121 if (!empty($source_url) && $source_url !== '#') {
4122 $source_display = $source_url;
4123 } else {
4124 $content_preview = strip_tags($embedding->article_content ?? '');
4125 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4126 $source_display = substr(trim($content_preview), 0, 50) . '...';
4127 }
4128
4129 // Parse chunk metadata for display
4130 $article_content_for_parse = $embedding->article_content ?? '';
4131 $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
4132 $is_chunk = $parsed_for_display['is_chunked'];
4133 $chunk_meta = $parsed_for_display['metadata'];
4134
4135 $all_similarities[] = [
4136 'document_id' => $embedding->id,
4137 'similarity' => $similarity,
4138 'similarity_percentage' => round($similarity * 100, 2),
4139 'above_threshold' => $similarity >= $similarity_threshold,
4140 'source_display' => $source_display,
4141 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
4142 'used_for_context' => false,
4143 'role_restriction' => $role_restriction,
4144 'has_access' => $has_access,
4145 'filtered_out' => !$has_access,
4146 'is_chunk' => $is_chunk,
4147 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
4148 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
4149 ];
4150
4151 // Only consider results above threshold AND with access for content retrieval
4152 if ($similarity >= $similarity_threshold && $has_access && !empty($source_url)) {
4153 // Parse chunk metadata if present
4154 $article_content = $embedding->article_content ?? '';
4155 $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
4156 $is_chunked = $parsed['is_chunked'];
4157 $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
4158 $text_content = $parsed['text'];
4159
4160 // Group by source URL
4161 if (!isset($url_groups[$source_url])) {
4162 $url_groups[$source_url] = array(
4163 'source_url' => $source_url,
4164 'best_score' => 0,
4165 'is_chunked' => $is_chunked,
4166 'chunks' => array(),
4167 'single_text' => '',
4168 'single_id' => null
4169 );
4170 }
4171
4172 // Track best score for this URL
4173 if ($similarity > $url_groups[$source_url]['best_score']) {
4174 $url_groups[$source_url]['best_score'] = $similarity;
4175 }
4176
4177 // Store chunk info or single text
4178 if ($is_chunked) {
4179 $url_groups[$source_url]['is_chunked'] = true;
4180 $url_groups[$source_url]['chunks'][] = array(
4181 'id' => $embedding->id,
4182 'score' => $similarity,
4183 'chunk_index' => $chunk_index,
4184 'text' => $text_content
4185 );
4186 } else {
4187 $url_groups[$source_url]['single_text'] = $text_content;
4188 $url_groups[$source_url]['single_id'] = $embedding->id;
4189 }
4190 }
4191 }
4192
4193 unset($database_embedding);
4194 }
4195
4196 // Sort ALL similarities for testing display (highest first)
4197 usort($all_similarities, function ($a, $b) {
4198 return $b['similarity'] <=> $a['similarity'];
4199 });
4200
4201 // Sort URL groups by best score (highest first)
4202 uasort($url_groups, function($a, $b) {
4203 return $b['best_score'] <=> $a['best_score'];
4204 });
4205
4206 // Take top 3 unique URLs
4207 $top_urls = array_slice($url_groups, 0, 3, true);
4208
4209 // Track which document IDs are used for context
4210 $used_document_ids = [];
4211 foreach ($top_urls as $group) {
4212 if ($group['is_chunked']) {
4213 foreach ($group['chunks'] as $chunk) {
4214 $used_document_ids[] = $chunk['id'];
4215 }
4216 } elseif ($group['single_id']) {
4217 $used_document_ids[] = $group['single_id'];
4218 }
4219 }
4220
4221 // Update the all_similarities array to mark which were actually used
4222 foreach ($all_similarities as &$similarity_item) {
4223 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
4224 }
4225
4226 // Store top 10 for testing panel
4227 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
4228 $this->last_similarity_analysis['total_checked'] = count($embeddings);
4229
4230 // Initialize final content
4231 $content = '';
4232 $matches_used = 0;
4233
4234 // Build content from top URLs
4235 foreach ($top_urls as $source_url => $group) {
4236 $full_text = '';
4237
4238 if ($group['is_chunked']) {
4239 // Fetch all chunks for this URL and reassemble
4240 $full_text = $this->reassemble_chunks_from_wordpress($source_url);
4241
4242 // If fetching all chunks fails, fall back to matched chunks
4243 if (empty($full_text)) {
4244 // Sort matched chunks by index and concatenate
4245 usort($group['chunks'], function($a, $b) {
4246 return $a['chunk_index'] <=> $b['chunk_index'];
4247 });
4248
4249 $chunk_texts = array();
4250 foreach ($group['chunks'] as $chunk) {
4251 $chunk_texts[] = $chunk['text'];
4252 }
4253 $full_text = implode("\n\n", $chunk_texts);
4254 }
4255 } else {
4256 $full_text = $group['single_text'];
4257 }
4258
4259 if (!empty($full_text)) {
4260 $content .= "## Reference " . ($matches_used + 1) . " ##\n";
4261 $content .= $full_text . "\n\n";
4262
4263 if (!empty($source_url) && $source_url !== '#') {
4264 $valid_urls[] = $source_url;
4265 $content .= "URL: " . $source_url . "\n\n";
4266 }
4267
4268 // Extract any URLs from the text content itself
4269 preg_match_all(
4270 '#\bhttps?://[^\s<>"\']+#i',
4271 $full_text,
4272 $content_urls
4273 );
4274 if (!empty($content_urls[0])) {
4275 $valid_urls = array_merge($valid_urls, $content_urls[0]);
4276 }
4277
4278 $matches_used++;
4279 }
4280 }
4281
4282 // NEW: Store unique valid URLs for validation
4283 $this->current_valid_urls = array_unique($valid_urls);
4284
4285 // Add response guidelines
4286 if (empty($top_urls)) {
4287 $content = "No reference information was found for this query.\n\n";
4288 } else {
4289 $content .= "\n## Response Guidelines ##\n" .
4290 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
4291 "Be conversational and friendly, but never mention your knowledge base or training data. " .
4292 "If you don't have specific information or are uncertain about any details, it's always " .
4293 "better to honestly say you don't know rather than making up or guessing at answers. " .
4294 "When information is incomplete, let them know you are unsure.\n\n" .
4295 "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
4296 "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
4297 }
4298
4299 return trim($content);
4300 }
4301
4302 /**
4303 * Fetch and reassemble all chunks for a URL from WordPress database
4304 *
4305 * @param string $source_url The source URL to fetch chunks for
4306 * @return string Reassembled content from all chunks
4307 */
4308 private function reassemble_chunks_from_wordpress($source_url) {
4309 global $wpdb;
4310 $table = $wpdb->prefix . 'mxchat_system_prompt_content';
4311
4312 // Fetch all rows with this source_url
4313 $rows = $wpdb->get_results($wpdb->prepare(
4314 "SELECT article_content FROM {$table}
4315 WHERE source_url = %s
4316 ORDER BY id ASC",
4317 $source_url
4318 ));
4319
4320 if (empty($rows)) {
4321 return '';
4322 }
4323
4324 // Parse and sort chunks by index
4325 $chunks = array();
4326 foreach ($rows as $row) {
4327 $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
4328
4329 if ($parsed['is_chunked']) {
4330 $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
4331 $chunks[$chunk_index] = $parsed['text'];
4332 } else {
4333 // Non-chunked content - just return it
4334 $chunks[] = $parsed['text'];
4335 }
4336 }
4337
4338 // Sort by chunk index
4339 ksort($chunks);
4340
4341 // Reassemble content
4342 return implode("\n\n", $chunks);
4343 }
4344
4345 private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
4346 global $wpdb;
4347
4348 error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
4349 error_log(" - bot_id: " . $bot_id);
4350 error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
4351 error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
4352
4353 // Use bot-specific config or fall back to default
4354 if ($bot_config === null) {
4355 $bot_config = $this->get_bot_pinecone_config($bot_id);
4356 }
4357
4358 $api_key = $bot_config['api_key'] ?? '';
4359 $host = $bot_config['host'] ?? '';
4360 $namespace = $bot_config['namespace'] ?? '';
4361
4362 error_log("MXCHAT DEBUG: Pinecone query parameters:");
4363 error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
4364 error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
4365 error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
4366
4367 // Initialize similarity analysis storage
4368 $this->last_similarity_analysis = [
4369 'knowledge_base_type' => 'Pinecone',
4370 'bot_id' => $bot_id,
4371 'namespace' => $namespace,
4372 'top_matches' => [],
4373 'threshold_used' => 0,
4374 'total_checked' => 0
4375 ];
4376
4377 // NEW: Initialize valid URLs array
4378 $valid_urls = [];
4379
4380 if (empty($host) || empty($api_key)) {
4381 error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
4382 error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
4383 error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
4384 // Store empty array for valid URLs since we can't proceed
4385 $this->current_valid_urls = [];
4386 return '';
4387 }
4388
4389 // Get knowledge manager instance for role checking
4390 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
4391
4392 // Get the similarity threshold from the bot options or main options
4393 $bot_options = $this->get_bot_options($bot_id);
4394 $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
4395
4396 $similarity_threshold = isset($current_options['similarity_threshold'])
4397 ? ((int) $current_options['similarity_threshold']) / 100
4398 : 0.35;
4399
4400 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
4401
4402 // Prepare the query request for Pinecone
4403 $api_endpoint = "https://{$host}/query";
4404
4405 $request_body = array(
4406 'vector' => $user_embedding,
4407 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top 3 unique URLs
4408 'includeMetadata' => true,
4409 'includeValues' => true
4410 );
4411
4412 // Add namespace if specified for this bot
4413 if (!empty($namespace)) {
4414 $request_body['namespace'] = $namespace;
4415 }
4416
4417 error_log("MXCHAT DEBUG: About to call Pinecone API");
4418 error_log(" - Endpoint: " . $api_endpoint);
4419 error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
4420
4421 $response = wp_remote_post($api_endpoint, array(
4422 'headers' => array(
4423 'Api-Key' => $api_key,
4424 'accept' => 'application/json',
4425 'content-type' => 'application/json'
4426 ),
4427 'body' => wp_json_encode($request_body),
4428 'timeout' => 30
4429 ));
4430
4431 if (is_wp_error($response)) {
4432 error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
4433 // Store empty array for valid URLs
4434 $this->current_valid_urls = [];
4435 return '';
4436 }
4437
4438 $response_code = wp_remote_retrieve_response_code($response);
4439 error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
4440
4441 if ($response_code !== 200) {
4442 $response_body = wp_remote_retrieve_body($response);
4443 error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
4444 // Store empty array for valid URLs
4445 $this->current_valid_urls = [];
4446 return '';
4447 }
4448
4449 // ADD DETAILED DEBUG SECTION HERE
4450 $response_body = wp_remote_retrieve_body($response);
4451 error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
4452
4453 $results = json_decode($response_body, true);
4454
4455 if (json_last_error() !== JSON_ERROR_NONE) {
4456 error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
4457 error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
4458 // Store empty array for valid URLs
4459 $this->current_valid_urls = [];
4460 return '';
4461 }
4462
4463 error_log("MXCHAT DEBUG: Pinecone response structure:");
4464 error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
4465 error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
4466
4467 if (empty($results['matches'])) {
4468 error_log("MXCHAT DEBUG: No matches found in Pinecone response");
4469 error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
4470 // Store empty array for valid URLs
4471 $this->current_valid_urls = [];
4472 return '';
4473 }
4474
4475 error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
4476
4477 // Log first match details for debugging
4478 if (!empty($results['matches'][0])) {
4479 $first_match = $results['matches'][0];
4480 error_log("MXCHAT DEBUG: First match details:");
4481 error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
4482 error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
4483 if (isset($first_match['metadata'])) {
4484 error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
4485 }
4486 }
4487
4488 // Initialize the final content
4489 $content = '';
4490 $matches_used = 0;
4491 $matches_used_for_context = [];
4492
4493 // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
4494 $url_groups = array();
4495
4496 foreach ($results['matches'] as $index => $match) {
4497 // Skip if similarity is below threshold
4498 if ($match['score'] < $similarity_threshold) {
4499 continue;
4500 }
4501
4502 $metadata = $match['metadata'] ?? array();
4503 $source_url = $metadata['source_url'] ?? '';
4504
4505 if (empty($source_url)) {
4506 continue;
4507 }
4508
4509 // LAZY ROLE CHECK: Only check role for content we're actually considering
4510 $match_id = $match['id'] ?? '';
4511 $role_restriction = $this->get_single_vector_role($match_id, $metadata);
4512 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4513
4514 // Skip if user doesn't have access
4515 if (!$has_access) {
4516 continue;
4517 }
4518
4519 // Group by source URL
4520 if (!isset($url_groups[$source_url])) {
4521 $url_groups[$source_url] = array(
4522 'source_url' => $source_url,
4523 'best_score' => 0,
4524 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
4525 'chunks' => array(),
4526 'single_text' => ''
4527 );
4528 }
4529
4530 // Track best score for this URL
4531 if ($match['score'] > $url_groups[$source_url]['best_score']) {
4532 $url_groups[$source_url]['best_score'] = $match['score'];
4533 }
4534
4535 // Store chunk info or single text
4536 if ($url_groups[$source_url]['is_chunked']) {
4537 $url_groups[$source_url]['chunks'][] = array(
4538 'id' => $match_id,
4539 'score' => $match['score'],
4540 'chunk_index' => $metadata['chunk_index'] ?? 0,
4541 'text' => $metadata['text'] ?? ''
4542 );
4543 } else {
4544 // Non-chunked content - just store the text
4545 $url_groups[$source_url]['single_text'] = $metadata['text'] ?? '';
4546 $url_groups[$source_url]['single_id'] = $match_id;
4547 }
4548 }
4549
4550 // Sort URL groups by best score (highest first)
4551 uasort($url_groups, function($a, $b) {
4552 return $b['best_score'] <=> $a['best_score'];
4553 });
4554
4555 // Take top 3 unique URLs
4556 $top_urls = array_slice($url_groups, 0, 3, true);
4557
4558 // Track which match IDs are actually used for context (only from top 3 URLs)
4559 foreach ($top_urls as $group) {
4560 if ($group['is_chunked']) {
4561 foreach ($group['chunks'] as $chunk) {
4562 $matches_used_for_context[] = $chunk['id'];
4563 }
4564 } elseif (!empty($group['single_id'])) {
4565 $matches_used_for_context[] = $group['single_id'];
4566 }
4567 }
4568
4569 // Build content from top URLs
4570 foreach ($top_urls as $source_url => $group) {
4571 $full_text = '';
4572
4573 if ($group['is_chunked']) {
4574 // Fetch all chunks for this URL and reassemble
4575 $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config);
4576
4577 // If fetching all chunks fails, fall back to matched chunks
4578 if (empty($full_text)) {
4579 // Sort matched chunks by index and concatenate
4580 usort($group['chunks'], function($a, $b) {
4581 return $a['chunk_index'] <=> $b['chunk_index'];
4582 });
4583
4584 $chunk_texts = array();
4585 foreach ($group['chunks'] as $chunk) {
4586 $chunk_texts[] = $chunk['text'];
4587 }
4588 $full_text = implode("\n\n", $chunk_texts);
4589 }
4590 } else {
4591 $full_text = $group['single_text'];
4592 }
4593
4594 if (!empty($full_text)) {
4595 $content .= "## Reference " . ($matches_used + 1) . " ##\n";
4596 $content .= $full_text . "\n\n";
4597
4598 if (!empty($source_url) && $source_url !== '#') {
4599 $valid_urls[] = $source_url;
4600 $content .= "URL: " . $source_url . "\n\n";
4601 }
4602
4603 // Extract any URLs from the text content itself
4604 preg_match_all(
4605 '#\bhttps?://[^\s<>"\']+#i',
4606 $full_text,
4607 $content_urls
4608 );
4609 if (!empty($content_urls[0])) {
4610 $valid_urls = array_merge($valid_urls, $content_urls[0]);
4611 }
4612
4613 $matches_used++;
4614 }
4615 }
4616
4617 // Process ALL matches for testing data (top 10) - with role checking for testing display
4618 $all_matches = [];
4619 foreach ($results['matches'] as $index => $match) {
4620 if ($index >= 10) break; // Limit to top 10 for testing
4621
4622 $match_id = $match['id'] ?? '';
4623
4624 // Check role access for testing display (use cache if available)
4625 $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
4626 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
4627
4628 $source_display = '';
4629 if (!empty($match['metadata']['source_url'])) {
4630 $source_display = $match['metadata']['source_url'];
4631 } else {
4632 $content_preview = strip_tags($match['metadata']['text'] ?? '');
4633 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
4634 $source_display = substr(trim($content_preview), 0, 50) . '...';
4635 }
4636
4637 $match_id_for_display = $match['id'] ?? $index;
4638
4639 // Check for chunk metadata in Pinecone
4640 $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
4641 $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
4642 $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
4643
4644 // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
4645 if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
4646 $is_chunk = true;
4647 }
4648
4649 $all_matches[] = [
4650 'document_id' => $match_id_for_display,
4651 'similarity' => $match['score'],
4652 'similarity_percentage' => round($match['score'] * 100, 2),
4653 'above_threshold' => $match['score'] >= $similarity_threshold,
4654 'source_display' => $source_display,
4655 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
4656 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
4657 'role_restriction' => $role_restriction,
4658 'has_access' => $has_access,
4659 'filtered_out' => !$has_access,
4660 'is_chunk' => $is_chunk,
4661 'chunk_index' => $chunk_index,
4662 'total_chunks' => $total_chunks
4663 ];
4664 }
4665
4666 // Store for testing panel
4667 $this->last_similarity_analysis['top_matches'] = $all_matches;
4668 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
4669
4670 // NEW: Store unique valid URLs for validation
4671 $this->current_valid_urls = array_unique($valid_urls);
4672
4673 // Add response guidelines
4674 if ($matches_used === 0) {
4675 $content = "No reference information was found for this query.\n\n";
4676 } else {
4677 $content .= "\n## Response Guidelines ##\n" .
4678 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
4679 "Be conversational and friendly, but never mention your knowledge base or training data. " .
4680 "If you don't have specific information or are uncertain about any details, it's always " .
4681 "better to honestly say you don't know rather than making up or guessing at answers. " .
4682 "When information is incomplete, let them know you are unsure.\n\n" .
4683 "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
4684 "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
4685 }
4686
4687 return trim($content);
4688 }
4689
4690 /**
4691 * Get role restriction for a single vector (with caching)
4692 */
4693 private function get_single_vector_role($vector_id, $metadata = array()) {
4694 global $wpdb;
4695
4696 if (empty($vector_id)) {
4697 return 'public';
4698 }
4699
4700 // Check cache first
4701 $cache_key = 'mxchat_vector_role_' . $vector_id;
4702 $cached_role = wp_cache_get($cache_key, 'mxchat_vector_roles');
4703
4704 if ($cached_role !== false) {
4705 return $cached_role;
4706 }
4707
4708 $role_restriction = 'public';
4709
4710 // First try Pinecone metadata
4711 if (!empty($metadata['role_restriction'])) {
4712 $role_restriction = $metadata['role_restriction'];
4713 } else {
4714 // Check WordPress table for user-modified roles
4715 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4716 $stored_role = $wpdb->get_var($wpdb->prepare(
4717 "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
4718 $vector_id
4719 ));
4720
4721 if ($stored_role) {
4722 $role_restriction = $stored_role;
4723 }
4724 }
4725
4726 // Cache individual role for 1 hour
4727 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
4728
4729 return $role_restriction;
4730 }
4731
4732 /**
4733 * Fetch and reassemble all chunks for a URL from Pinecone
4734 *
4735 * @param string $source_url The source URL to fetch chunks for
4736 * @param array $bot_config Bot-specific Pinecone configuration
4737 * @return string Reassembled content from all chunks
4738 */
4739 private function reassemble_chunks_from_pinecone($source_url, $bot_config) {
4740 $api_key = $bot_config['api_key'] ?? '';
4741 $host = $bot_config['host'] ?? '';
4742 $namespace = $bot_config['namespace'] ?? '';
4743
4744 if (empty($host) || empty($api_key)) {
4745 return '';
4746 }
4747
4748 $base_hash = md5($source_url);
4749
4750 // Use Pinecone list API to find all chunk vectors with this prefix
4751 $list_url = "https://{$host}/vectors/list";
4752
4753 $list_body = array(
4754 'prefix' => $base_hash . '_chunk_',
4755 'limit' => 100
4756 );
4757
4758 if (!empty($namespace)) {
4759 $list_body['namespace'] = $namespace;
4760 }
4761
4762 $list_response = wp_remote_post($list_url, array(
4763 'headers' => array(
4764 'Api-Key' => $api_key,
4765 'accept' => 'application/json',
4766 'content-type' => 'application/json'
4767 ),
4768 'body' => wp_json_encode($list_body),
4769 'timeout' => 30
4770 ));
4771
4772 if (is_wp_error($list_response)) {
4773 //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
4774 return '';
4775 }
4776
4777 $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
4778
4779 if (empty($list_data['vectors'])) {
4780 //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
4781 return '';
4782 }
4783
4784 // Extract vector IDs
4785 $vector_ids = array();
4786 foreach ($list_data['vectors'] as $vector) {
4787 if (isset($vector['id'])) {
4788 $vector_ids[] = $vector['id'];
4789 }
4790 }
4791
4792 if (empty($vector_ids)) {
4793 return '';
4794 }
4795
4796 // Fetch all chunk content
4797 $fetch_url = "https://{$host}/vectors/fetch";
4798
4799 $fetch_body = array(
4800 'ids' => $vector_ids
4801 );
4802
4803 if (!empty($namespace)) {
4804 $fetch_body['namespace'] = $namespace;
4805 }
4806
4807 $fetch_response = wp_remote_post($fetch_url, array(
4808 'headers' => array(
4809 'Api-Key' => $api_key,
4810 'accept' => 'application/json',
4811 'content-type' => 'application/json'
4812 ),
4813 'body' => wp_json_encode($fetch_body),
4814 'timeout' => 30
4815 ));
4816
4817 if (is_wp_error($fetch_response)) {
4818 //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
4819 return '';
4820 }
4821
4822 $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
4823
4824 if (empty($fetch_data['vectors'])) {
4825 return '';
4826 }
4827
4828 // Sort chunks by index and reassemble
4829 $chunks = array();
4830 foreach ($fetch_data['vectors'] as $id => $vector) {
4831 $metadata = $vector['metadata'] ?? array();
4832 $chunk_index = $metadata['chunk_index'] ?? 0;
4833 $text = $metadata['text'] ?? '';
4834
4835 // Store chunk with its index
4836 $chunks[$chunk_index] = $text;
4837 }
4838
4839 // Sort by chunk index
4840 ksort($chunks);
4841
4842 // Reassemble content
4843 return implode("\n\n", $chunks);
4844 }
4845
4846 private function mxchat_find_relevant_products($user_embedding) {
4847 //error_log('MXChat Vector Search: Starting product search...');
4848
4849 // Retrieve the add-on settings from the database
4850 $addon_options = get_option('mxchat_pinecone_addon_options', array());
4851
4852 // Determine whether Pinecone is enabled
4853 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
4854
4855 //error_log('Pinecone enabled flag: ' . $use_pinecone);
4856
4857 if ($use_pinecone === 1) {
4858 //error_log('MXChat Vector Search: Using Pinecone database for products');
4859 return $this->find_relevant_products_pinecone($user_embedding);
4860 } else {
4861 //error_log('MXChat Vector Search: Using WordPress database for products');
4862 return $this->find_relevant_products_wordpress($user_embedding);
4863 }
4864 }
4865 private function find_relevant_products_wordpress($user_embedding) {
4866 global $wpdb;
4867 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4868 $cache_key = 'mxchat_system_prompt_embeddings';
4869 $batch_size = 500;
4870
4871 // Original WordPress database search logic
4872 // [Previous implementation remains the same]
4873 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
4874 if ($embeddings === false) {
4875 $embeddings = [];
4876 $offset = 0;
4877
4878 do {
4879 $query = $wpdb->prepare(
4880 "SELECT id, embedding_vector
4881 FROM {$system_prompt_table}
4882 LIMIT %d OFFSET %d",
4883 $batch_size,
4884 $offset
4885 );
4886
4887 $batch = $wpdb->get_results($query);
4888 if (empty($batch)) {
4889 break;
4890 }
4891
4892 $embeddings = array_merge($embeddings, $batch);
4893 $offset += $batch_size;
4894
4895 unset($batch);
4896
4897 } while (true);
4898
4899 if (empty($embeddings)) {
4900 return '';
4901 }
4902 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4903 }
4904
4905 $relevant_results = [];
4906 foreach ($embeddings as $embedding) {
4907 $database_embedding = $embedding->embedding_vector
4908 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
4909 : null;
4910 if (is_array($database_embedding) && is_array($user_embedding)) {
4911 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4912 $relevant_results[] = [
4913 'id' => $embedding->id,
4914 'similarity' => $similarity
4915 ];
4916 }
4917 unset($database_embedding);
4918 }
4919
4920 // Use fixed threshold for products
4921 $similarity_threshold = 0.85;
4922
4923 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
4924 return $result['similarity'] >= $similarity_threshold;
4925 });
4926 usort($relevant_results, function ($a, $b) {
4927 return $b['similarity'] <=> $a['similarity'];
4928 });
4929
4930 $top_results = array_slice($relevant_results, 0, 3);
4931 $content = '';
4932
4933 foreach ($top_results as $result) {
4934 $chunk_content = $this->fetch_content_with_product_links($result['id']);
4935 $content .= $chunk_content . "\n\n";
4936 }
4937
4938 return trim($content);
4939 }
4940
4941
4942 private function find_relevant_products_pinecone($user_embedding) {
4943 //error_log('Starting Pinecone product search...');
4944
4945 $options = get_option('mxchat_pinecone_addon_options', array());
4946 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
4947 $host = $options['mxchat_pinecone_host'] ?? '';
4948
4949 if (empty($host) || empty($api_key)) {
4950 //error_log('Pinecone credentials not properly configured for product search');
4951 return '';
4952 }
4953
4954 $similarity_threshold = 0.85;
4955 $api_endpoint = "https://{$host}/query";
4956
4957 $request_body = array(
4958 'vector' => $user_embedding,
4959 'topK' => 5,
4960 'includeMetadata' => true,
4961 'includeValues' => true,
4962 'filter' => array(
4963 'type' => 'product'
4964 )
4965 );
4966
4967 //error_log('Sending request to Pinecone with body: ' . wp_json_encode($request_body));
4968
4969 $response = wp_remote_post($api_endpoint, array(
4970 'headers' => array(
4971 'Api-Key' => $api_key,
4972 'accept' => 'application/json',
4973 'content-type' => 'application/json'
4974 ),
4975 'body' => wp_json_encode($request_body),
4976 'timeout' => 30
4977 ));
4978
4979 if (is_wp_error($response)) {
4980 //error_log('Pinecone product query error: ' . $response->get_error_message());
4981 return '';
4982 }
4983
4984 $response_code = wp_remote_retrieve_response_code($response);
4985 //error_log('Pinecone response code: ' . $response_code);
4986
4987 if ($response_code !== 200) {
4988 //error_log('Pinecone API error during product search: ' . wp_remote_retrieve_body($response));
4989 return '';
4990 }
4991
4992 $results = json_decode(wp_remote_retrieve_body($response), true);
4993 //error_log('Pinecone raw response: ' . wp_remote_retrieve_body($response));
4994
4995 if (empty($results['matches'])) {
4996 //error_log('No matches found in Pinecone response');
4997 return '';
4998 }
4999
5000 $content = '';
5001 foreach ($results['matches'] as $match) {
5002 if ($match['score'] < $similarity_threshold) {
5003 //error_log("Match below threshold: " . $match['score']);
5004 continue;
5005 }
5006
5007 if (!empty($match['metadata']['text'])) {
5008 $content .= $match['metadata']['text'];
5009 if (!empty($match['metadata']['source_url'])) {
5010 $content .= "\n\nFor more details, check out this product: " . esc_url($match['metadata']['source_url']);
5011 }
5012 $content .= "\n\n";
5013 }
5014 }
5015
5016 return trim($content);
5017 }
5018
5019
5020 private function fetch_content_with_product_links($most_relevant_id) {
5021 global $wpdb;
5022 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
5023
5024 // Fetch the article content and associated product URL
5025 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
5026 $result = $wpdb->get_row($query);
5027
5028 if ($result) {
5029 // Append the product link to the content if available
5030 $content = $result->article_content;
5031 if (!empty($result->source_url)) {
5032 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
5033 }
5034 return $content;
5035 }
5036
5037 return null;
5038 }
5039
5040 /**
5041 * Get system instructions for a specific bot or default
5042 * Checks for multi-bot add-on and uses bot-specific instructions if available
5043 */
5044 private function get_system_instructions($bot_id = 'default') {
5045 // Check if multi-bot add-on is active
5046 if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
5047 // Get bot-specific options from multi-bot add-on
5048 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
5049
5050 // If bot has custom system instructions, use those
5051 if (!empty($bot_options['system_prompt_instructions'])) {
5052 return $bot_options['system_prompt_instructions'];
5053 }
5054 }
5055
5056 // Fall back to default system instructions
5057 return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5058 }
5059 /**
5060 * Get the current bot ID from session or request context
5061 */
5062 private function get_current_bot_id($session_id = '') {
5063 // First, check if bot_id is passed in the current request
5064 if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
5065 return sanitize_key($_POST['bot_id']);
5066 }
5067
5068 // If not in POST, try to get it from session data
5069 if (!empty($session_id)) {
5070 $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
5071 if (!empty($bot_id)) {
5072 return $bot_id;
5073 }
5074 }
5075
5076 // Fall back to default
5077 return 'default';
5078 }
5079 private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $openrouter_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null, $selected_model = 'gpt-4o') {
5080 try {
5081 if (!$relevant_content) {
5082 $error_response = [
5083 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
5084 'error_code' => 'no_relevant_content'
5085 ];
5086
5087 if ($testing_data !== null) {
5088 $error_response['testing_data'] = $testing_data;
5089 }
5090
5091 return $error_response;
5092 }
5093
5094 if (!is_array($conversation_history)) {
5095 $conversation_history = array();
5096 }
5097
5098 // Check if this is an OpenRouter model
5099 if ($selected_model === 'openrouter') {
5100 // Get the actual OpenRouter model from options
5101 $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
5102
5103 if (empty($openrouter_selected_model)) {
5104 $error_response = [
5105 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
5106 'error_code' => 'no_openrouter_model_selected'
5107 ];
5108 if ($testing_data !== null) {
5109 $error_response['testing_data'] = $testing_data;
5110 }
5111 return $error_response;
5112 }
5113
5114 if (empty($openrouter_api_key)) {
5115 $error_response = [
5116 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
5117 'error_code' => 'missing_openrouter_api_key'
5118 ];
5119 if ($testing_data !== null) {
5120 $error_response['testing_data'] = $testing_data;
5121 }
5122 return $error_response;
5123 }
5124
5125 if ($streaming) {
5126 return $this->mxchat_generate_response_openrouter_stream(
5127 $openrouter_selected_model,
5128 $openrouter_api_key,
5129 $conversation_history,
5130 $relevant_content,
5131 $session_id,
5132 $testing_data
5133 );
5134 } else {
5135 $response = $this->mxchat_generate_response_openrouter(
5136 $openrouter_selected_model,
5137 $openrouter_api_key,
5138 $conversation_history,
5139 $relevant_content
5140 );
5141 }
5142
5143 if (is_array($response) && isset($response['error'])) {
5144 if ($testing_data !== null) {
5145 $response['testing_data'] = $testing_data;
5146 }
5147 return $response;
5148 }
5149
5150 return $response;
5151 }
5152
5153 // Extract model prefix to determine the provider
5154 $model_parts = explode('-', $selected_model);
5155 $provider = strtolower($model_parts[0]);
5156
5157 // Handle model selection based on provider prefix
5158 switch ($provider) {
5159 case 'gemini':
5160 if (empty($gemini_api_key)) {
5161 $error_response = [
5162 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
5163 'error_code' => 'missing_gemini_api_key'
5164 ];
5165 if ($testing_data !== null) {
5166 $error_response['testing_data'] = $testing_data;
5167 }
5168 return $error_response;
5169 }
5170 $response = $this->mxchat_generate_response_gemini(
5171 $selected_model,
5172 $gemini_api_key,
5173 $conversation_history,
5174 $relevant_content
5175 );
5176 break;
5177
5178 case 'claude':
5179 if (empty($claude_api_key)) {
5180 $error_response = [
5181 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
5182 'error_code' => 'missing_claude_api_key'
5183 ];
5184 if ($testing_data !== null) {
5185 $error_response['testing_data'] = $testing_data;
5186 }
5187 return $error_response;
5188 }
5189 if ($streaming) {
5190 return $this->mxchat_generate_response_claude_stream(
5191 $selected_model,
5192 $claude_api_key,
5193 $conversation_history,
5194 $relevant_content,
5195 $session_id,
5196 $testing_data
5197 );
5198 } else {
5199 $response = $this->mxchat_generate_response_claude(
5200 $selected_model,
5201 $claude_api_key,
5202 $conversation_history,
5203 $relevant_content
5204 );
5205 }
5206 break;
5207
5208 case 'grok':
5209 if (empty($xai_api_key)) {
5210 $error_response = [
5211 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
5212 'error_code' => 'missing_xai_api_key'
5213 ];
5214 if ($testing_data !== null) {
5215 $error_response['testing_data'] = $testing_data;
5216 }
5217 return $error_response;
5218 }
5219 if ($streaming) {
5220 return $this->mxchat_generate_response_xai_stream(
5221 $selected_model,
5222 $xai_api_key,
5223 $conversation_history,
5224 $relevant_content,
5225 $session_id,
5226 $testing_data
5227 );
5228 } else {
5229 $response = $this->mxchat_generate_response_xai(
5230 $selected_model,
5231 $xai_api_key,
5232 $conversation_history,
5233 $relevant_content
5234 );
5235 }
5236 break;
5237
5238 case 'deepseek':
5239 if (empty($deepseek_api_key)) {
5240 $error_response = [
5241 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
5242 'error_code' => 'missing_deepseek_api_key'
5243 ];
5244 if ($testing_data !== null) {
5245 $error_response['testing_data'] = $testing_data;
5246 }
5247 return $error_response;
5248 }
5249 if ($streaming) {
5250 return $this->mxchat_generate_response_deepseek_stream(
5251 $selected_model,
5252 $deepseek_api_key,
5253 $conversation_history,
5254 $relevant_content,
5255 $session_id,
5256 $testing_data
5257 );
5258 } else {
5259 $response = $this->mxchat_generate_response_deepseek(
5260 $selected_model,
5261 $deepseek_api_key,
5262 $conversation_history,
5263 $relevant_content
5264 );
5265 }
5266 break;
5267
5268 case 'gpt':
5269 case 'o1':
5270 if (empty($api_key)) {
5271 $error_response = [
5272 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
5273 'error_code' => 'missing_openai_api_key'
5274 ];
5275 if ($testing_data !== null) {
5276 $error_response['testing_data'] = $testing_data;
5277 }
5278 return $error_response;
5279 }
5280 if ($streaming) {
5281 return $this->mxchat_generate_response_openai_stream(
5282 $selected_model,
5283 $api_key,
5284 $conversation_history,
5285 $relevant_content,
5286 $session_id,
5287 $testing_data
5288 );
5289 } else {
5290 $response = $this->mxchat_generate_response_openai(
5291 $selected_model,
5292 $api_key,
5293 $conversation_history,
5294 $relevant_content
5295 );
5296 }
5297 break;
5298
5299 default:
5300 if (empty($api_key)) {
5301 $error_response = [
5302 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
5303 'error_code' => 'missing_openai_api_key'
5304 ];
5305 if ($testing_data !== null) {
5306 $error_response['testing_data'] = $testing_data;
5307 }
5308 return $error_response;
5309 }
5310 if ($streaming) {
5311 return $this->mxchat_generate_response_openai_stream(
5312 $selected_model,
5313 $api_key,
5314 $conversation_history,
5315 $relevant_content,
5316 $session_id,
5317 $testing_data
5318 );
5319 } else {
5320 $response = $this->mxchat_generate_response_openai(
5321 $selected_model,
5322 $api_key,
5323 $conversation_history,
5324 $relevant_content
5325 );
5326 }
5327 break;
5328 }
5329
5330 if (is_array($response) && isset($response['error'])) {
5331 if ($testing_data !== null) {
5332 $response['testing_data'] = $testing_data;
5333 }
5334 return $response;
5335 }
5336
5337 return $response;
5338
5339 } catch (Exception $e) {
5340 $error_response = [
5341 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
5342 'error_code' => 'system_exception',
5343 'exception_details' => $e->getMessage()
5344 ];
5345
5346 if ($testing_data !== null) {
5347 $error_response['testing_data'] = $testing_data;
5348 }
5349
5350 return $error_response;
5351 }
5352 }
5353 private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5354 try {
5355 $bot_id = $this->get_current_bot_id($session_id);
5356 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5357
5358 if (!is_array($conversation_history)) {
5359 $conversation_history = array();
5360 }
5361
5362 $formatted_conversation = array();
5363
5364 $formatted_conversation[] = array(
5365 'role' => 'system',
5366 'content' => $system_prompt_instructions . " " . $relevant_content
5367 );
5368
5369 foreach ($conversation_history as $message) {
5370 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5371 $role = $message['role'];
5372 if ($role === 'bot' || $role === 'agent') {
5373 $role = 'assistant';
5374 }
5375 if (!in_array($role, ['system', 'assistant', 'user'])) {
5376 $role = 'user';
5377 }
5378 $formatted_conversation[] = array(
5379 'role' => $role,
5380 'content' => $message['content']
5381 );
5382 }
5383 }
5384
5385 if (headers_sent() || !function_exists('curl_init')) {
5386 $regular_response = $this->mxchat_generate_response_openrouter(
5387 $selected_model,
5388 $openrouter_api_key,
5389 $conversation_history,
5390 $relevant_content
5391 );
5392
5393 // Save bot response to transcript
5394 if (!empty($regular_response) && !empty($session_id)) {
5395 $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
5396 }
5397
5398 $response_data = [
5399 'text' => $regular_response,
5400 'html' => '',
5401 'session_id' => $session_id
5402 ];
5403
5404 if ($testing_data !== null) {
5405 $response_data['testing_data'] = $testing_data;
5406 }
5407
5408 header('Content-Type: application/json');
5409 echo json_encode($response_data);
5410 return true;
5411 }
5412
5413 $body = json_encode([
5414 'model' => $selected_model,
5415 'messages' => $formatted_conversation,
5416 'temperature' => 1,
5417 'stream' => true
5418 ]);
5419
5420 // Setup streaming headers now that we know we're actually streaming
5421 $this->setup_streaming_headers();
5422
5423 $ch = curl_init();
5424 curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
5425 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5426 curl_setopt($ch, CURLOPT_POST, true);
5427 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5428 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5429 'Content-Type: application/json',
5430 'Authorization: Bearer ' . $openrouter_api_key,
5431 'HTTP-Referer: ' . home_url(),
5432 'X-Title: ' . get_bloginfo('name')
5433 ));
5434 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5435 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5436
5437 $full_response = '';
5438 $stream_started = false;
5439 $buffer = '';
5440
5441 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5442 if (!$stream_started && $testing_data !== null) {
5443 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5444 flush();
5445 $stream_started = true;
5446 }
5447
5448 $buffer .= $data;
5449 $lines = explode("\n", $buffer);
5450 $buffer = array_pop($lines);
5451
5452 foreach ($lines as $line) {
5453 if (trim($line) === '') {
5454 continue;
5455 }
5456
5457 if (strpos($line, 'data: ') !== 0) {
5458 continue;
5459 }
5460
5461 $json_str = substr($line, 6);
5462
5463 if (trim($json_str) === '[DONE]') {
5464 echo "data: [DONE]\n\n";
5465 flush();
5466 continue;
5467 }
5468
5469 $json = json_decode(trim($json_str), true);
5470 if ($json && isset($json['choices'][0]['delta']['content'])) {
5471 $content = $json['choices'][0]['delta']['content'];
5472 $full_response .= $content;
5473
5474 echo "data: " . json_encode(['content' => $content]) . "\n\n";
5475 flush();
5476 }
5477 }
5478
5479 return strlen($data);
5480 });
5481
5482 $response = curl_exec($ch);
5483 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5484
5485 if (curl_errno($ch) || $http_code !== 200) {
5486 curl_close($ch);
5487
5488 $regular_response = $this->mxchat_generate_response_openrouter(
5489 $selected_model,
5490 $openrouter_api_key,
5491 $conversation_history,
5492 $relevant_content
5493 );
5494
5495 $response_data = [
5496 'text' => $regular_response,
5497 'html' => '',
5498 'session_id' => $session_id
5499 ];
5500
5501 if ($testing_data !== null) {
5502 $response_data['testing_data'] = $testing_data;
5503 }
5504
5505 header('Content-Type: application/json');
5506 echo json_encode($response_data);
5507 return true;
5508 }
5509
5510 curl_close($ch);
5511
5512 if (!empty($full_response) && !empty($session_id)) {
5513 // Prepare RAG context for streaming response
5514 $rag_context_for_storage = null;
5515 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) {
5516 $rag_context_for_storage = [
5517 'top_matches' => $this->last_similarity_analysis['top_matches'],
5518 'approved_urls' => $this->current_valid_urls ?? [],
5519 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35,
5520 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database',
5521 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0
5522 ];
5523 }
5524 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5525 }
5526
5527 return true;
5528
5529 } catch (Exception $e) {
5530 $regular_response = $this->mxchat_generate_response_openrouter(
5531 $selected_model,
5532 $openrouter_api_key,
5533 $conversation_history,
5534 $relevant_content
5535 );
5536
5537 $response_data = [
5538 'text' => $regular_response,
5539 'html' => '',
5540 'session_id' => $session_id
5541 ];
5542
5543 if ($testing_data !== null) {
5544 $response_data['testing_data'] = $testing_data;
5545 }
5546
5547 header('Content-Type: application/json');
5548 echo json_encode($response_data);
5549 return true;
5550 }
5551 }
5552 private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5553 try {
5554 $bot_id = $this->get_current_bot_id($session_id);
5555
5556 // Get system prompt instructions using centralized function
5557 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5558
5559 // Ensure conversation_history is an array
5560 if (!is_array($conversation_history)) {
5561 $conversation_history = array();
5562 }
5563
5564 // Format conversation history for OpenAI
5565 $formatted_conversation = array();
5566
5567 $formatted_conversation[] = array(
5568 'role' => 'system',
5569 'content' => $system_prompt_instructions . " " . $relevant_content
5570 );
5571
5572 foreach ($conversation_history as $message) {
5573 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5574 $role = $message['role'];
5575 if ($role === 'bot' || $role === 'agent') {
5576 $role = 'assistant';
5577 }
5578 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5579 $role = 'user';
5580 }
5581 $formatted_conversation[] = array(
5582 'role' => $role,
5583 'content' => $message['content']
5584 );
5585 }
5586 }
5587
5588 // Check if we can actually stream
5589 if (headers_sent() || !function_exists('curl_init')) {
5590 // Fallback to regular response with testing data
5591 $regular_response = $this->mxchat_generate_response_openai(
5592 $selected_model,
5593 $api_key,
5594 $conversation_history,
5595 $relevant_content
5596 );
5597
5598 // Save bot response to transcript
5599 if (!empty($regular_response) && !empty($session_id)) {
5600 $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
5601 }
5602
5603 $response_data = [
5604 'text' => $regular_response,
5605 'html' => '',
5606 'session_id' => $session_id
5607 ];
5608
5609 if ($testing_data !== null) {
5610 $response_data['testing_data'] = $testing_data;
5611 }
5612
5613 header('Content-Type: application/json');
5614 echo json_encode($response_data);
5615 return true;
5616 }
5617
5618 // Check if this is a GPT-5 model (supports reasoning_effort parameter)
5619 $is_gpt5_model = (
5620 strpos($selected_model, 'gpt-5') === 0 ||
5621 $selected_model === 'gpt-5.2' ||
5622 $selected_model === 'gpt-5.1-2025-11-13' ||
5623 $selected_model === 'gpt-5' ||
5624 $selected_model === 'gpt-5-mini' ||
5625 $selected_model === 'gpt-5-nano'
5626 );
5627
5628 // Build request body with optimal settings for fast streaming
5629 $request_body = [
5630 'model' => $selected_model,
5631 'messages' => $formatted_conversation,
5632 'temperature' => 1,
5633 'stream' => true
5634 ];
5635
5636 // Add reasoning_effort only for GPT-5 models (not GPT-5.2 which doesn't support it)
5637 if ($is_gpt5_model && $selected_model !== 'gpt-5.2') {
5638 // GPT-5.1 uses 'low' instead of 'minimal'
5639 if ($selected_model === 'gpt-5.1-2025-11-13') {
5640 $request_body['reasoning_effort'] = 'low';
5641 } else {
5642 $request_body['reasoning_effort'] = 'minimal'; // For other GPT-5 models
5643 }
5644 }
5645
5646 $body = json_encode($request_body);
5647
5648 // Setup streaming headers now that we know we're actually streaming
5649 $this->setup_streaming_headers();
5650
5651 // Use cURL for streaming support
5652 $ch = curl_init();
5653 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
5654 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5655 curl_setopt($ch, CURLOPT_POST, true);
5656 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5657 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5658 'Content-Type: application/json',
5659 'Authorization: Bearer ' . $api_key
5660 ));
5661 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5662 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5663
5664 $full_response = ''; // Accumulate full response for saving
5665 $stream_started = false;
5666 $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
5667
5668 // Buffer control for real-time streaming
5669 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5670 // Send testing data as the first event if available
5671 if (!$stream_started && $testing_data !== null) {
5672 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5673 flush();
5674 $stream_started = true;
5675 }
5676
5677 // CRITICAL FIX: Append new data to buffer
5678 $buffer .= $data;
5679
5680 // Process complete lines only
5681 $lines = explode("\n", $buffer);
5682
5683 // CRITICAL FIX: Keep the last incomplete line in the buffer
5684 // The last element might be incomplete, so keep it in buffer
5685 $buffer = array_pop($lines);
5686
5687 foreach ($lines as $line) {
5688 // Skip empty lines
5689 if (trim($line) === '') {
5690 continue;
5691 }
5692
5693 // Only process lines that start with "data: "
5694 if (strpos($line, 'data: ') !== 0) {
5695 continue;
5696 }
5697
5698 $json_str = substr($line, 6); // Remove 'data: ' prefix
5699
5700 if (trim($json_str) === '[DONE]') {
5701 echo "data: [DONE]\n\n";
5702 flush();
5703 continue;
5704 }
5705
5706 // Try to decode JSON
5707 $json = json_decode(trim($json_str), true);
5708 if ($json && isset($json['choices'][0]['delta']['content'])) {
5709 $content = $json['choices'][0]['delta']['content'];
5710 $full_response .= $content; // Accumulate the full response
5711
5712 // Send as SSE format
5713 echo "data: " . json_encode(['content' => $content]) . "\n\n";
5714 flush();
5715 }
5716 }
5717
5718 return strlen($data);
5719 });
5720
5721 $response = curl_exec($ch);
5722 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5723
5724 if (curl_errno($ch) || $http_code !== 200) {
5725 $curl_error = curl_error($ch);
5726 curl_close($ch);
5727
5728 // Fallback to regular response
5729 $regular_response = $this->mxchat_generate_response_openai(
5730 $selected_model,
5731 $api_key,
5732 $conversation_history,
5733 $relevant_content
5734 );
5735
5736 // FIXED: Check if regular response returned an error
5737 if (is_array($regular_response) && isset($regular_response['error'])) {
5738 // Send error in SSE format since we're in streaming mode
5739 echo "data: " . json_encode([
5740 'error' => true,
5741 'error_message' => $regular_response['error'],
5742 'error_code' => $regular_response['error_code'] ?? 'api_error',
5743 'text' => $regular_response['error'],
5744 'message' => $regular_response['error']
5745 ]) . "\n\n";
5746 echo "data: [DONE]\n\n";
5747 flush();
5748 return true;
5749 }
5750
5751 $response_data = [
5752 'text' => $regular_response,
5753 'html' => '',
5754 'session_id' => $session_id
5755 ];
5756
5757 if ($testing_data !== null) {
5758 $response_data['testing_data'] = $testing_data;
5759 }
5760
5761 header('Content-Type: application/json');
5762 echo json_encode($response_data);
5763 return true;
5764 }
5765
5766 curl_close($ch);
5767
5768 // Save the complete response to maintain chat persistence
5769 if (!empty($full_response) && !empty($session_id)) {
5770 // Prepare RAG context for streaming response
5771 $rag_context_for_storage = null;
5772 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) {
5773 $rag_context_for_storage = [
5774 'top_matches' => $this->last_similarity_analysis['top_matches'],
5775 'approved_urls' => $this->current_valid_urls ?? [],
5776 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35,
5777 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database',
5778 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0
5779 ];
5780 }
5781 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5782 }
5783
5784 return true; // Indicate streaming completed successfully
5785
5786 } catch (Exception $e) {
5787 // Fallback to regular response
5788 $regular_response = $this->mxchat_generate_response_openai(
5789 $selected_model,
5790 $api_key,
5791 $conversation_history,
5792 $relevant_content
5793 );
5794
5795 // FIXED: Check if regular response returned an error
5796 if (is_array($regular_response) && isset($regular_response['error'])) {
5797 // Send error in SSE format since we're in streaming mode
5798 echo "data: " . json_encode([
5799 'error' => true,
5800 'error_message' => $regular_response['error'],
5801 'error_code' => $regular_response['error_code'] ?? 'api_error',
5802 'text' => $regular_response['error'],
5803 'message' => $regular_response['error']
5804 ]) . "\n\n";
5805 echo "data: [DONE]\n\n";
5806 flush();
5807 return true;
5808 }
5809
5810 $response_data = [
5811 'text' => $regular_response,
5812 'html' => '',
5813 'session_id' => $session_id
5814 ];
5815
5816 if ($testing_data !== null) {
5817 $response_data['testing_data'] = $testing_data;
5818 }
5819
5820 header('Content-Type: application/json');
5821 echo json_encode($response_data);
5822 return true;
5823 }
5824 }
5825 private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5826 try {
5827 // Get bot ID from session or request
5828 $bot_id = $this->get_current_bot_id($session_id);
5829
5830 // Get system prompt instructions using centralized function
5831 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5832 // Ensure conversation_history is an array
5833 if (!is_array($conversation_history)) {
5834 $conversation_history = array();
5835 }
5836
5837 // Clean and validate conversation history
5838 foreach ($conversation_history as &$message) {
5839 // Convert bot and agent roles to assistant
5840 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
5841 $message['role'] = 'assistant';
5842 }
5843
5844 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
5845 if (!in_array($message['role'], ['assistant', 'user'])) {
5846 $message['role'] = 'user';
5847 }
5848
5849 // Ensure content field exists
5850 if (!isset($message['content']) || empty($message['content'])) {
5851 $message['content'] = '';
5852 }
5853
5854 // Remove any unsupported fields
5855 $message = array_intersect_key($message, array_flip(['role', 'content']));
5856 }
5857
5858 // Add relevant content as the latest user message
5859 $conversation_history[] = [
5860 'role' => 'user',
5861 'content' => $relevant_content
5862 ];
5863
5864 // Prepare the request body with stream: true
5865 $body = json_encode([
5866 'model' => $selected_model,
5867 'messages' => $conversation_history,
5868 'max_tokens' => 1000,
5869 'temperature' => 0.8,
5870 'system' => $system_prompt_instructions,
5871 'stream' => true
5872 ]);
5873
5874 // Check if we can actually stream (headers not sent, etc.)
5875 if (headers_sent() || !function_exists('curl_init')) {
5876 // Fallback to regular response with testing data
5877 //error_log("MxChat: Streaming not possible, falling back to regular response");
5878 $regular_response = $this->mxchat_generate_response_claude(
5879 $selected_model,
5880 $claude_api_key,
5881 array_slice($conversation_history, 0, -1), // Remove the added content
5882 $relevant_content
5883 );
5884
5885 // Save bot response to transcript
5886 if (!empty($regular_response) && !empty($session_id)) {
5887 $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
5888 }
5889
5890 // Return as JSON with testing data
5891 $response_data = [
5892 'text' => $regular_response,
5893 'html' => '',
5894 'session_id' => $session_id
5895 ];
5896
5897 if ($testing_data !== null) {
5898 $response_data['testing_data'] = $testing_data;
5899 //error_log("MxChat Testing: Added testing data to Claude fallback response");
5900 }
5901
5902 // Clear any streaming headers and send JSON
5903 if (headers_sent() === false) {
5904 header('Content-Type: application/json');
5905 }
5906 echo json_encode($response_data);
5907 return true; // Indicate we handled the response
5908 }
5909
5910 // Setup streaming headers now that we know we're actually streaming
5911 $this->setup_streaming_headers();
5912
5913 // Use cURL for streaming support
5914 $ch = curl_init();
5915 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
5916 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5917 curl_setopt($ch, CURLOPT_POST, true);
5918 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5919 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5920 'Content-Type: application/json',
5921 'x-api-key: ' . $claude_api_key,
5922 'anthropic-version: 2023-06-01'
5923 ));
5924 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5925 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5926
5927 $full_response = ''; // Accumulate full response for saving
5928 $stream_started = false;
5929 $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
5930
5931 // Buffer control for real-time streaming
5932 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
5933 // Send testing data as the first event if available
5934 if (!$stream_started && $testing_data !== null) {
5935 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5936 flush();
5937 $stream_started = true;
5938 //error_log("MxChat Testing: Sent testing data in Claude stream");
5939 }
5940
5941 // CRITICAL FIX: Append new data to buffer
5942 $buffer .= $data;
5943
5944 // Process complete lines only
5945 $lines = explode("\n", $buffer);
5946
5947 // CRITICAL FIX: Keep the last incomplete line in the buffer
5948 // The last element might be incomplete, so keep it in buffer
5949 $buffer = array_pop($lines);
5950
5951 foreach ($lines as $line) {
5952 if (trim($line) === '') {
5953 continue;
5954 }
5955
5956 // Claude uses event: and data: format
5957 if (strpos($line, 'event: ') === 0) {
5958 // Store the event type for the next data line
5959 continue;
5960 }
5961
5962 if (strpos($line, 'data: ') === 0) {
5963 $json_str = substr($line, 6); // Remove 'data: ' prefix
5964
5965 $json = json_decode(trim($json_str), true);
5966 if (json_last_error() !== JSON_ERROR_NONE) {
5967 continue;
5968 }
5969
5970 // Handle different event types
5971 if (isset($json['type'])) {
5972 switch ($json['type']) {
5973 case 'content_block_delta':
5974 if (isset($json['delta']['text'])) {
5975 $content = $json['delta']['text'];
5976 $full_response .= $content; // Accumulate
5977 // Send as SSE format compatible with your frontend
5978 echo "data: " . json_encode(['content' => $content]) . "\n\n";
5979 flush();
5980 }
5981 break;
5982
5983 case 'message_stop':
5984 echo "data: [DONE]\n\n";
5985 flush();
5986 break;
5987
5988 case 'error':
5989 echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
5990 flush();
5991 break;
5992 }
5993 }
5994 }
5995 }
5996
5997 return strlen($data);
5998 });
5999
6000 $response = curl_exec($ch);
6001 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6002
6003 if (curl_errno($ch)) {
6004 curl_close($ch);
6005 throw new Exception('cURL Error: ' . curl_error($ch));
6006 }
6007
6008 curl_close($ch);
6009
6010 if ($http_code !== 200) {
6011 // Fallback to regular response
6012 //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
6013 $regular_response = $this->mxchat_generate_response_claude(
6014 $selected_model,
6015 $claude_api_key,
6016 array_slice($conversation_history, 0, -1), // Remove the added content
6017 $relevant_content
6018 );
6019
6020 // FIXED: Check if regular response returned an error
6021 if (is_array($regular_response) && isset($regular_response['error'])) {
6022 // Send error in SSE format since we're in streaming mode
6023 echo "data: " . json_encode([
6024 'error' => true,
6025 'error_message' => $regular_response['error'],
6026 'error_code' => $regular_response['error_code'] ?? 'api_error',
6027 'text' => $regular_response['error'],
6028 'message' => $regular_response['error']
6029 ]) . "\n\n";
6030 echo "data: [DONE]\n\n";
6031 flush();
6032 return true;
6033 }
6034
6035 $response_data = [
6036 'text' => $regular_response,
6037 'html' => '',
6038 'session_id' => $session_id
6039 ];
6040
6041 if ($testing_data !== null) {
6042 $response_data['testing_data'] = $testing_data;
6043 //error_log("MxChat Testing: Added testing data to Claude error fallback");
6044 }
6045
6046 header('Content-Type: application/json');
6047 echo json_encode($response_data);
6048 return true;
6049 }
6050
6051 // Save the complete response to maintain chat persistence
6052 if (!empty($full_response) && !empty($session_id)) {
6053 // Prepare RAG context for streaming response
6054 $rag_context_for_storage = null;
6055 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) {
6056 $rag_context_for_storage = [
6057 'top_matches' => $this->last_similarity_analysis['top_matches'],
6058 'approved_urls' => $this->current_valid_urls ?? [],
6059 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35,
6060 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database',
6061 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0
6062 ];
6063 }
6064 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6065 }
6066
6067 return true; // Indicate streaming completed successfully
6068
6069 } catch (Exception $e) {
6070 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
6071
6072 // Fallback to regular response on exception
6073 $regular_response = $this->mxchat_generate_response_claude(
6074 $selected_model,
6075 $claude_api_key,
6076 $conversation_history,
6077 $relevant_content
6078 );
6079
6080 // FIXED: Check if regular response returned an error
6081 if (is_array($regular_response) && isset($regular_response['error'])) {
6082 // Send error in SSE format since we're in streaming mode
6083 echo "data: " . json_encode([
6084 'error' => true,
6085 'error_message' => $regular_response['error'],
6086 'error_code' => $regular_response['error_code'] ?? 'api_error',
6087 'text' => $regular_response['error'],
6088 'message' => $regular_response['error']
6089 ]) . "\n\n";
6090 echo "data: [DONE]\n\n";
6091 flush();
6092 return true;
6093 }
6094
6095 $response_data = [
6096 'text' => $regular_response,
6097 'html' => '',
6098 'session_id' => $session_id
6099 ];
6100
6101 if ($testing_data !== null) {
6102 $response_data['testing_data'] = $testing_data;
6103 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
6104 }
6105
6106 header('Content-Type: application/json');
6107 echo json_encode($response_data);
6108 return true;
6109 }
6110 }
6111 private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6112 try {
6113 // Get bot ID from session or request
6114 $bot_id = $this->get_current_bot_id($session_id);
6115
6116 // Get system prompt instructions using centralized function
6117 $system_prompt_instructions = $this->get_system_instructions($bot_id);
6118
6119 // Ensure conversation_history is an array
6120 if (!is_array($conversation_history)) {
6121 $conversation_history = array();
6122 }
6123
6124 // Format conversation history for X.AI (same as OpenAI format)
6125 $formatted_conversation = array();
6126
6127 $formatted_conversation[] = array(
6128 'role' => 'system',
6129 'content' => $system_prompt_instructions . " " . $relevant_content
6130 );
6131
6132 foreach ($conversation_history as $message) {
6133 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6134 $role = $message['role'];
6135 if ($role === 'bot' || $role === 'agent') {
6136 $role = 'assistant';
6137 }
6138 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
6139 $role = 'user';
6140 }
6141 $formatted_conversation[] = array(
6142 'role' => $role,
6143 'content' => $message['content']
6144 );
6145 }
6146 }
6147
6148 // Check if we can actually stream
6149 if (headers_sent() || !function_exists('curl_init')) {
6150 // Fallback to regular response with testing data
6151 //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
6152 $regular_response = $this->mxchat_generate_response_xai(
6153 $selected_model,
6154 $xai_api_key,
6155 $conversation_history,
6156 $relevant_content
6157 );
6158
6159 // Save bot response to transcript
6160 if (!empty($regular_response) && !empty($session_id)) {
6161 $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6162 }
6163
6164 $response_data = [
6165 'text' => $regular_response,
6166 'html' => '',
6167 'session_id' => $session_id
6168 ];
6169
6170 if ($testing_data !== null) {
6171 $response_data['testing_data'] = $testing_data;
6172 //error_log("MxChat Testing: Added testing data to X.AI fallback response");
6173 }
6174
6175 header('Content-Type: application/json');
6176 echo json_encode($response_data);
6177 return true;
6178 }
6179
6180 // Prepare the request body with stream: true
6181 $body = json_encode([
6182 'model' => $selected_model,
6183 'messages' => $formatted_conversation,
6184 'temperature' => 0.8,
6185 'stream' => true
6186 ]);
6187
6188 // Setup streaming headers now that we know we're actually streaming
6189 $this->setup_streaming_headers();
6190
6191 // Use cURL for streaming support
6192 $ch = curl_init();
6193 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
6194 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6195 curl_setopt($ch, CURLOPT_POST, true);
6196 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
6197 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
6198 'Content-Type: application/json',
6199 'Authorization: Bearer ' . $xai_api_key
6200 ));
6201 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6202 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
6203
6204 $full_response = ''; // Accumulate full response for saving
6205 $stream_started = false;
6206 $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
6207
6208 // Buffer control for real-time streaming
6209 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
6210 // Send testing data as the first event if available
6211 if (!$stream_started && $testing_data !== null) {
6212 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
6213 flush();
6214 $stream_started = true;
6215 //error_log("MxChat Testing: Sent testing data in X.AI stream");
6216 }
6217
6218 // CRITICAL FIX: Append new data to buffer
6219 $buffer .= $data;
6220
6221 // Process complete lines only
6222 $lines = explode("\n", $buffer);
6223
6224 // CRITICAL FIX: Keep the last incomplete line in the buffer
6225 // The last element might be incomplete, so keep it in buffer
6226 $buffer = array_pop($lines);
6227
6228 foreach ($lines as $line) {
6229 // Skip empty lines
6230 if (trim($line) === '') {
6231 continue;
6232 }
6233
6234 // Only process lines that start with "data: "
6235 if (strpos($line, 'data: ') !== 0) {
6236 continue;
6237 }
6238
6239 $json_str = substr($line, 6); // Remove 'data: ' prefix
6240
6241 if (trim($json_str) === '[DONE]') {
6242 echo "data: [DONE]\n\n";
6243 flush();
6244 continue;
6245 }
6246
6247 // Try to decode JSON
6248 $json = json_decode(trim($json_str), true);
6249 if ($json && isset($json['choices'][0]['delta']['content'])) {
6250 $content = $json['choices'][0]['delta']['content'];
6251 $full_response .= $content; // Accumulate
6252 // Send as SSE format
6253 echo "data: " . json_encode(['content' => $content]) . "\n\n";
6254 flush();
6255 }
6256 }
6257
6258 return strlen($data);
6259 });
6260
6261 $response = curl_exec($ch);
6262 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6263
6264 if (curl_errno($ch) || $http_code !== 200) {
6265 curl_close($ch);
6266
6267 // Fallback to regular response
6268 //error_log("MxChat: X.AI streaming failed, falling back");
6269 $regular_response = $this->mxchat_generate_response_xai(
6270 $selected_model,
6271 $xai_api_key,
6272 $conversation_history,
6273 $relevant_content
6274 );
6275
6276 $response_data = [
6277 'text' => $regular_response,
6278 'html' => '',
6279 'session_id' => $session_id
6280 ];
6281
6282 if ($testing_data !== null) {
6283 $response_data['testing_data'] = $testing_data;
6284 //error_log("MxChat Testing: Added testing data to X.AI error fallback");
6285 }
6286
6287 header('Content-Type: application/json');
6288 echo json_encode($response_data);
6289 return true;
6290 }
6291
6292 curl_close($ch);
6293
6294 // Save the complete response to maintain chat persistence
6295 if (!empty($full_response) && !empty($session_id)) {
6296 // Prepare RAG context for streaming response
6297 $rag_context_for_storage = null;
6298 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) {
6299 $rag_context_for_storage = [
6300 'top_matches' => $this->last_similarity_analysis['top_matches'],
6301 'approved_urls' => $this->current_valid_urls ?? [],
6302 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35,
6303 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database',
6304 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0
6305 ];
6306 }
6307 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6308 }
6309
6310 return true; // Indicate streaming completed successfully
6311
6312 } catch (Exception $e) {
6313 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
6314
6315 // Fallback to regular response
6316 $regular_response = $this->mxchat_generate_response_xai(
6317 $selected_model,
6318 $xai_api_key,
6319 $conversation_history,
6320 $relevant_content
6321 );
6322
6323 $response_data = [
6324 'text' => $regular_response,
6325 'html' => '',
6326 'session_id' => $session_id
6327 ];
6328
6329 if ($testing_data !== null) {
6330 $response_data['testing_data'] = $testing_data;
6331 //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
6332 }
6333
6334 header('Content-Type: application/json');
6335 echo json_encode($response_data);
6336 return true;
6337 }
6338 }
6339 private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
6340 try {
6341 // Get bot ID from session or request
6342 $bot_id = $this->get_current_bot_id($session_id);
6343
6344 // Get system prompt instructions using centralized function
6345 $system_prompt_instructions = $this->get_system_instructions($bot_id);
6346
6347 // Ensure conversation_history is an array
6348 if (!is_array($conversation_history)) {
6349 $conversation_history = array();
6350 }
6351
6352 // Format conversation history for DeepSeek
6353 $formatted_conversation = array();
6354
6355 $formatted_conversation[] = array(
6356 'role' => 'system',
6357 'content' => $system_prompt_instructions . " " . $relevant_content
6358 );
6359
6360 foreach ($conversation_history as $message) {
6361 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6362 $role = $message['role'];
6363 if ($role === 'bot' || $role === 'agent') {
6364 $role = 'assistant';
6365 }
6366 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
6367 $role = 'user';
6368 }
6369 $formatted_conversation[] = array(
6370 'role' => $role,
6371 'content' => $message['content']
6372 );
6373 }
6374 }
6375
6376 // Check if we can actually stream
6377 if (headers_sent() || !function_exists('curl_init')) {
6378 // Fallback to regular response with testing data
6379 //error_log("MxChat: DeepSeek streaming not possible, falling back to regular response");
6380 $regular_response = $this->mxchat_generate_response_deepseek(
6381 $selected_model,
6382 $deepseek_api_key,
6383 $conversation_history,
6384 $relevant_content
6385 );
6386
6387 // Save bot response to transcript
6388 if (!empty($regular_response) && !empty($session_id)) {
6389 $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
6390 }
6391
6392 $response_data = [
6393 'text' => $regular_response,
6394 'html' => '',
6395 'session_id' => $session_id
6396 ];
6397
6398 if ($testing_data !== null) {
6399 $response_data['testing_data'] = $testing_data;
6400 //error_log("MxChat Testing: Added testing data to DeepSeek fallback response");
6401 }
6402
6403 header('Content-Type: application/json');
6404 echo json_encode($response_data);
6405 return true;
6406 }
6407
6408 // Prepare the request body with stream: true
6409 $body = json_encode([
6410 'model' => $selected_model,
6411 'messages' => $formatted_conversation,
6412 'temperature' => 0.8,
6413 'stream' => true
6414 ]);
6415
6416 // Setup streaming headers now that we know we're actually streaming
6417 $this->setup_streaming_headers();
6418
6419 // Use cURL for streaming support
6420 $ch = curl_init();
6421 curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
6422 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
6423 curl_setopt($ch, CURLOPT_POST, true);
6424 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
6425 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
6426 'Content-Type: application/json',
6427 'Authorization: Bearer ' . $deepseek_api_key
6428 ));
6429 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6430 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
6431
6432 $full_response = ''; // Accumulate full response for saving
6433 $stream_started = false;
6434 $buffer = ''; // CRITICAL: Add persistent buffer for incomplete chunks
6435
6436 // Buffer control for real-time streaming
6437 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, $testing_data) {
6438 // Send testing data as the first event if available
6439 if (!$stream_started && $testing_data !== null) {
6440 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
6441 flush();
6442 $stream_started = true;
6443 //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
6444 }
6445
6446 // CRITICAL FIX: Append new data to buffer
6447 $buffer .= $data;
6448
6449 // Process complete lines only
6450 $lines = explode("\n", $buffer);
6451
6452 // CRITICAL FIX: Keep the last incomplete line in the buffer
6453 // The last element might be incomplete, so keep it in buffer
6454 $buffer = array_pop($lines);
6455
6456 foreach ($lines as $line) {
6457 // Skip empty lines
6458 if (trim($line) === '') {
6459 continue;
6460 }
6461
6462 // Only process lines that start with "data: "
6463 if (strpos($line, 'data: ') !== 0) {
6464 continue;
6465 }
6466
6467 $json_str = substr($line, 6); // Remove 'data: ' prefix
6468
6469 if (trim($json_str) === '[DONE]') {
6470 echo "data: [DONE]\n\n";
6471 flush();
6472 continue;
6473 }
6474
6475 // Try to decode JSON
6476 $json = json_decode(trim($json_str), true);
6477 if ($json && isset($json['choices'][0]['delta']['content'])) {
6478 $content = $json['choices'][0]['delta']['content'];
6479 $full_response .= $content; // Accumulate the full response
6480
6481 // Send as SSE format
6482 echo "data: " . json_encode(['content' => $content]) . "\n\n";
6483 flush();
6484 }
6485 }
6486
6487 return strlen($data);
6488 });
6489
6490 $response = curl_exec($ch);
6491 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6492
6493 if (curl_errno($ch) || $http_code !== 200) {
6494 $curl_error = curl_error($ch);
6495 curl_close($ch);
6496
6497 // Log the specific error for debugging
6498 //error_log("MxChat: DeepSeek streaming failed - HTTP: $http_code, cURL: $curl_error");
6499
6500 // Fallback to regular response
6501 $regular_response = $this->mxchat_generate_response_deepseek(
6502 $selected_model,
6503 $deepseek_api_key,
6504 $conversation_history,
6505 $relevant_content
6506 );
6507
6508 // Handle error response from regular function
6509 if (is_array($regular_response) && isset($regular_response['error'])) {
6510 if ($testing_data !== null) {
6511 $regular_response['testing_data'] = $testing_data;
6512 }
6513 header('Content-Type: application/json');
6514 echo json_encode($regular_response);
6515 return true;
6516 }
6517
6518 $response_data = [
6519 'text' => $regular_response,
6520 'html' => '',
6521 'session_id' => $session_id
6522 ];
6523
6524 if ($testing_data !== null) {
6525 $response_data['testing_data'] = $testing_data;
6526 //error_log("MxChat Testing: Added testing data to DeepSeek error fallback");
6527 }
6528
6529 header('Content-Type: application/json');
6530 echo json_encode($response_data);
6531 return true;
6532 }
6533
6534 curl_close($ch);
6535
6536 // Save the complete response to maintain chat persistence
6537 if (!empty($full_response) && !empty($session_id)) {
6538 // Prepare RAG context for streaming response
6539 $rag_context_for_storage = null;
6540 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) {
6541 $rag_context_for_storage = [
6542 'top_matches' => $this->last_similarity_analysis['top_matches'],
6543 'approved_urls' => $this->current_valid_urls ?? [],
6544 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35,
6545 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database',
6546 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0
6547 ];
6548 }
6549 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
6550 }
6551
6552 return true; // Indicate streaming completed successfully
6553
6554 } catch (Exception $e) {
6555 //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
6556
6557 // Fallback to regular response
6558 $regular_response = $this->mxchat_generate_response_deepseek(
6559 $selected_model,
6560 $deepseek_api_key,
6561 $conversation_history,
6562 $relevant_content
6563 );
6564
6565 // Handle error response from regular function
6566 if (is_array($regular_response) && isset($regular_response['error'])) {
6567 if ($testing_data !== null) {
6568 $regular_response['testing_data'] = $testing_data;
6569 }
6570 header('Content-Type: application/json');
6571 echo json_encode($regular_response);
6572 return true;
6573 }
6574
6575 $response_data = [
6576 'text' => $regular_response,
6577 'html' => '',
6578 'session_id' => $session_id
6579 ];
6580
6581 if ($testing_data !== null) {
6582 $response_data['testing_data'] = $testing_data;
6583 //error_log("MxChat Testing: Added testing data to DeepSeek exception fallback");
6584 }
6585
6586 header('Content-Type: application/json');
6587 echo json_encode($response_data);
6588 return true;
6589 }
6590 }
6591
6592
6593 private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
6594 try {
6595 if (!is_array($conversation_history)) {
6596 $conversation_history = array();
6597 }
6598
6599 $bot_id = $this->get_current_bot_id('');
6600 $system_prompt_instructions = $this->get_system_instructions($bot_id);
6601
6602 $formatted_conversation = array();
6603
6604 $formatted_conversation[] = array(
6605 'role' => 'system',
6606 'content' => $system_prompt_instructions . " " . $relevant_content
6607 );
6608
6609 foreach ($conversation_history as $message) {
6610 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6611 $role = $message['role'];
6612
6613 if ($role === 'bot' || $role === 'agent') {
6614 $role = 'assistant';
6615 }
6616 if (!in_array($role, ['system', 'assistant', 'user'])) {
6617 $role = 'user';
6618 }
6619
6620 $formatted_conversation[] = array(
6621 'role' => $role,
6622 'content' => $message['content']
6623 );
6624 }
6625 }
6626
6627 $body = json_encode([
6628 'model' => $selected_model,
6629 'messages' => $formatted_conversation,
6630 'temperature' => 1,
6631 ]);
6632
6633 $args = [
6634 'body' => $body,
6635 'headers' => [
6636 'Content-Type' => 'application/json',
6637 'Authorization' => 'Bearer ' . $openrouter_api_key,
6638 'HTTP-Referer' => home_url(),
6639 'X-Title' => get_bloginfo('name'),
6640 ],
6641 'timeout' => 60,
6642 'redirection' => 5,
6643 'blocking' => true,
6644 'httpversion' => '1.0',
6645 'sslverify' => true,
6646 ];
6647
6648 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', $args);
6649
6650 if (is_wp_error($response)) {
6651 $error_message = $response->get_error_message();
6652 return [
6653 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
6654 'error_code' => 'openrouter_connection_error',
6655 'provider' => 'openrouter'
6656 ];
6657 }
6658
6659 $status_code = wp_remote_retrieve_response_code($response);
6660 if ($status_code !== 200) {
6661 $response_body = wp_remote_retrieve_body($response);
6662 $decoded_response = json_decode($response_body, true);
6663
6664 $error_message = isset($decoded_response['error']['message'])
6665 ? $decoded_response['error']['message']
6666 : 'HTTP Error ' . $status_code;
6667
6668 return [
6669 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
6670 'error_code' => 'openrouter_api_error',
6671 'provider' => 'openrouter',
6672 'status_code' => $status_code
6673 ];
6674 }
6675
6676 $response_body = wp_remote_retrieve_body($response);
6677 $decoded_response = json_decode($response_body, true);
6678
6679 if (isset($decoded_response['choices'][0]['message']['content'])) {
6680 return trim($decoded_response['choices'][0]['message']['content']);
6681 } else {
6682 return [
6683 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
6684 'error_code' => 'openrouter_response_format_error',
6685 'provider' => 'openrouter'
6686 ];
6687 }
6688 } catch (Exception $e) {
6689 return [
6690 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
6691 'error_code' => 'openrouter_exception',
6692 'provider' => 'openrouter'
6693 ];
6694 }
6695 }
6696 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
6697
6698 // Get bot ID from session or request
6699 $bot_id = $this->get_current_bot_id($session_id);
6700
6701 // Get system prompt instructions using centralized function
6702 $system_prompt_instructions = $this->get_system_instructions($bot_id);
6703
6704 // Clean and validate conversation history
6705 foreach ($conversation_history as &$message) {
6706 // Convert bot and agent roles to assistant
6707 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
6708 $message['role'] = 'assistant';
6709 }
6710
6711 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
6712 if (!in_array($message['role'], ['assistant', 'user'])) {
6713 $message['role'] = 'user';
6714 }
6715
6716 // Ensure content field exists
6717 if (!isset($message['content']) || empty($message['content'])) {
6718 $message['content'] = '';
6719 }
6720
6721 // Remove any unsupported fields
6722 $message = array_intersect_key($message, array_flip(['role', 'content']));
6723 }
6724
6725 // Add relevant content as the latest user message
6726 $conversation_history[] = [
6727 'role' => 'user',
6728 'content' => $relevant_content
6729 ];
6730
6731 // Build request body
6732 $body = json_encode([
6733 'model' => $selected_model,
6734 'max_tokens' => 1000,
6735 'temperature' => 0.8,
6736 'messages' => $conversation_history,
6737 'system' => $system_prompt_instructions
6738 ]);
6739
6740 // Set up API request
6741 $args = [
6742 'body' => $body,
6743 'headers' => [
6744 'Content-Type' => 'application/json',
6745 'x-api-key' => $claude_api_key,
6746 'anthropic-version' => '2023-06-01'
6747 ],
6748 'timeout' => 60,
6749 'redirection' => 5,
6750 'blocking' => true,
6751 'httpversion' => '1.0',
6752 'sslverify' => true,
6753 ];
6754
6755 // Make API request
6756 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
6757
6758 // Check for WordPress errors
6759 if (is_wp_error($response)) {
6760 //error_log("Claude API request error: " . $response->get_error_message());
6761 return "Sorry, there was an error connecting to the API.";
6762 }
6763
6764 // Check HTTP response code
6765 $http_code = wp_remote_retrieve_response_code($response);
6766 if ($http_code !== 200) {
6767 $error_body = wp_remote_retrieve_body($response);
6768 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
6769
6770 // Try to extract error message from response
6771 $error_data = json_decode($error_body, true);
6772 $error_message = isset($error_data['error']['message']) ?
6773 $error_data['error']['message'] :
6774 "HTTP error " . $http_code;
6775
6776 return "Sorry, the API returned an error: " . $error_message;
6777 }
6778
6779 // Parse response
6780 $response_body = json_decode(wp_remote_retrieve_body($response), true);
6781
6782 // Check for JSON decode errors
6783 if (json_last_error() !== JSON_ERROR_NONE) {
6784 //error_log("Claude API JSON decode error: " . json_last_error_msg());
6785 return "Sorry, there was an error processing the API response.";
6786 }
6787
6788 // Extract and validate response content
6789 if (isset($response_body['content']) &&
6790 is_array($response_body['content']) &&
6791 !empty($response_body['content']) &&
6792 isset($response_body['content'][0]['text'])) {
6793 return trim($response_body['content'][0]['text']);
6794 }
6795
6796 // Log unexpected response format
6797 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
6798 return "Sorry, I received an unexpected response format from the API.";
6799 }
6800 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
6801 try {
6802 // Ensure conversation_history is an array
6803 if (!is_array($conversation_history)) {
6804 $conversation_history = array();
6805 }
6806
6807 // Get bot ID from session or request
6808 $bot_id = $this->get_current_bot_id('');
6809
6810 // Get system prompt instructions using centralized function
6811 $system_prompt_instructions = $this->get_system_instructions($bot_id);
6812
6813 // Create a new array for the formatted conversation
6814 $formatted_conversation = array();
6815
6816 // Add system message first
6817 $formatted_conversation[] = array(
6818 'role' => 'system',
6819 'content' => $system_prompt_instructions . " " . $relevant_content
6820 );
6821
6822 // Add the rest of the conversation history
6823 foreach ($conversation_history as $message) {
6824 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
6825 $role = $message['role'];
6826
6827 // Convert roles to supported format
6828 if ($role === 'bot' || $role === 'agent') {
6829 $role = 'assistant';
6830 }
6831 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
6832 $role = 'user';
6833 }
6834
6835 $formatted_conversation[] = array(
6836 'role' => $role,
6837 'content' => $message['content']
6838 );
6839 }
6840 }
6841
6842 // Check if this is a GPT-5 model (supports reasoning_effort parameter)
6843 $is_gpt5_model = (
6844 strpos($selected_model, 'gpt-5') === 0 ||
6845 $selected_model === 'gpt-5.2' ||
6846 $selected_model === 'gpt-5.1-2025-11-13' ||
6847 $selected_model === 'gpt-5' ||
6848 $selected_model === 'gpt-5-mini' ||
6849 $selected_model === 'gpt-5-nano'
6850 );
6851
6852 // Build request body with optimal settings for fast responses
6853 $request_body = [
6854 'model' => $selected_model,
6855 'messages' => $formatted_conversation,
6856 'temperature' => 1,
6857 'stream' => false
6858 ];
6859
6860 // Add reasoning_effort only for GPT-5 models (not GPT-5.2 which doesn't support it)
6861 if ($is_gpt5_model && $selected_model !== 'gpt-5.2') {
6862 // GPT-5.1 uses 'low' instead of 'minimal'
6863 if ($selected_model === 'gpt-5.1-2025-11-13') {
6864 $request_body['reasoning_effort'] = 'low';
6865 } else {
6866 $request_body['reasoning_effort'] = 'minimal'; // For other GPT-5 models
6867 }
6868 }
6869
6870 $body = json_encode($request_body);
6871
6872 $args = [
6873 'body' => $body,
6874 'headers' => [
6875 'Content-Type' => 'application/json',
6876 'Authorization' => 'Bearer ' . $api_key,
6877 ],
6878 'timeout' => 60,
6879 'redirection' => 5,
6880 'blocking' => true,
6881 'httpversion' => '1.0',
6882 'sslverify' => true,
6883 ];
6884
6885 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
6886
6887 if (is_wp_error($response)) {
6888 $error_message = $response->get_error_message();
6889 return [
6890 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
6891 'error_code' => 'openai_connection_error',
6892 'provider' => 'openai'
6893 ];
6894 }
6895
6896 $status_code = wp_remote_retrieve_response_code($response);
6897 if ($status_code !== 200) {
6898 $response_body = wp_remote_retrieve_body($response);
6899 $decoded_response = json_decode($response_body, true);
6900
6901 $error_message = isset($decoded_response['error']['message'])
6902 ? $decoded_response['error']['message']
6903 : 'HTTP Error ' . $status_code;
6904
6905 $error_type = isset($decoded_response['error']['type'])
6906 ? $decoded_response['error']['type']
6907 : 'unknown';
6908
6909 // Handle specific error types
6910 switch ($error_type) {
6911 case 'invalid_request_error':
6912 if (strpos($error_message, 'API key') !== false) {
6913 return [
6914 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
6915 'error_code' => 'openai_invalid_api_key',
6916 'provider' => 'openai'
6917 ];
6918 }
6919 break;
6920
6921 case 'authentication_error':
6922 return [
6923 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
6924 'error_code' => 'openai_auth_error',
6925 'provider' => 'openai'
6926 ];
6927
6928 case 'rate_limit_exceeded':
6929 return [
6930 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
6931 'error_code' => 'openai_rate_limit',
6932 'provider' => 'openai'
6933 ];
6934
6935 case 'quota_exceeded':
6936 return [
6937 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
6938 'error_code' => 'openai_quota_exceeded',
6939 'provider' => 'openai'
6940 ];
6941 }
6942
6943 // Generic error fallback
6944 return [
6945 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
6946 'error_code' => 'openai_api_error',
6947 'provider' => 'openai',
6948 'status_code' => $status_code
6949 ];
6950 }
6951
6952 $response_body = wp_remote_retrieve_body($response);
6953 $decoded_response = json_decode($response_body, true);
6954
6955 if (isset($decoded_response['choices'][0]['message']['content'])) {
6956 return trim($decoded_response['choices'][0]['message']['content']);
6957 } else {
6958 return [
6959 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
6960 'error_code' => 'openai_response_format_error',
6961 'provider' => 'openai'
6962 ];
6963 }
6964 } catch (Exception $e) {
6965 return [
6966 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
6967 'error_code' => 'openai_exception',
6968 'provider' => 'openai'
6969 ];
6970 }
6971 }
6972
6973 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
6974 try {
6975 // Get bot ID from session or request
6976 $bot_id = $this->get_current_bot_id($session_id);
6977
6978 // Get system prompt instructions using centralized function
6979 $system_prompt_instructions = $this->get_system_instructions($bot_id);
6980
6981 // Add system prompt to relevant content
6982 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
6983
6984 // Prepend system instructions to the conversation history
6985 array_unshift($conversation_history, [
6986 'role' => 'system',
6987 'content' => "Here are your instructions: " . $content_with_instructions
6988 ]);
6989
6990 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
6991 foreach ($conversation_history as &$message) {
6992 if ($message['role'] === 'bot') {
6993 $message['role'] = 'assistant';
6994 } elseif ($message['role'] === 'agent') {
6995 // Tag the message as coming from a live agent
6996 $message['role'] = 'assistant';
6997 if (!isset($message['metadata'])) {
6998 $message['metadata'] = ['source' => 'live_agent'];
6999 }
7000 }
7001
7002 // Ensure all roles are valid
7003 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
7004 $message['role'] = 'user'; // Default to 'user'
7005 }
7006 }
7007
7008 // Build the request body
7009 $body = json_encode([
7010 'model' => $selected_model,
7011 'messages' => $conversation_history,
7012 'temperature' => 0.8,
7013 'stream' => false
7014 ]);
7015
7016 // Set up the API request
7017 $args = [
7018 'body' => $body,
7019 'headers' => [
7020 'Content-Type' => 'application/json',
7021 'Authorization' => 'Bearer ' . $xai_api_key,
7022 ],
7023 'timeout' => 60,
7024 'redirection' => 5,
7025 'blocking' => true,
7026 'httpversion' => '1.0',
7027 'sslverify' => true,
7028 ];
7029
7030 // Make the API request
7031 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
7032
7033 // Process the response
7034 if (is_wp_error($response)) {
7035 $error_message = $response->get_error_message();
7036 //error_log('X.AI API Error: ' . $error_message);
7037 return [
7038 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
7039 'error_code' => 'xai_connection_error',
7040 'provider' => 'xai'
7041 ];
7042 }
7043
7044 $status_code = wp_remote_retrieve_response_code($response);
7045 if ($status_code !== 200) {
7046 $response_body = wp_remote_retrieve_body($response);
7047 $decoded_response = json_decode($response_body, true);
7048
7049 // Log the full response for debugging
7050 //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
7051
7052 // Extract error message from X.AI's specific format
7053 $error_message = '';
7054
7055 // Check for direct error string (as seen in your logs)
7056 if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
7057 $error_message = $decoded_response['error'];
7058 }
7059 // Check for nested error object (OpenAI style)
7060 elseif (isset($decoded_response['error']['message'])) {
7061 $error_message = $decoded_response['error']['message'];
7062 }
7063 // Check for top-level message
7064 elseif (isset($decoded_response['message'])) {
7065 $error_message = $decoded_response['message'];
7066 }
7067 // Fallback
7068 else {
7069 $error_message = 'HTTP Error ' . $status_code;
7070 }
7071
7072 //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
7073
7074 // Check for API key errors using string matching
7075 if (stripos($error_message, 'api key') !== false ||
7076 stripos($error_message, 'incorrect api key') !== false ||
7077 stripos($error_message, 'invalid api key') !== false) {
7078 return [
7079 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
7080 'error_code' => 'xai_invalid_api_key',
7081 'provider' => 'xai'
7082 ];
7083 }
7084
7085 // Authentication errors
7086 if ($status_code === 401 || $status_code === 403 ||
7087 stripos($error_message, 'auth') !== false) {
7088 return [
7089 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
7090 'error_code' => 'xai_auth_error',
7091 'provider' => 'xai'
7092 ];
7093 }
7094
7095 // Model errors
7096 if (stripos($error_message, 'model') !== false) {
7097 return [
7098 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
7099 'error_code' => 'xai_invalid_model',
7100 'provider' => 'xai'
7101 ];
7102 }
7103
7104 // Rate limit errors
7105 if ($status_code === 429 ||
7106 stripos($error_message, 'rate') !== false ||
7107 stripos($error_message, 'limit') !== false) {
7108 return [
7109 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
7110 'error_code' => 'xai_rate_limit',
7111 'provider' => 'xai'
7112 ];
7113 }
7114
7115 // Quota errors
7116 if (stripos($error_message, 'quota') !== false ||
7117 stripos($error_message, 'billing') !== false) {
7118 return [
7119 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
7120 'error_code' => 'xai_quota_exceeded',
7121 'provider' => 'xai'
7122 ];
7123 }
7124
7125 // Server errors
7126 if ($status_code >= 500) {
7127 return [
7128 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
7129 'error_code' => 'xai_service_unavailable',
7130 'provider' => 'xai'
7131 ];
7132 }
7133
7134 // Generic error fallback with the actual error message
7135 return [
7136 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
7137 'error_code' => 'xai_api_error',
7138 'provider' => 'xai',
7139 'status_code' => $status_code
7140 ];
7141 }
7142
7143 $response_body = wp_remote_retrieve_body($response);
7144 $decoded_response = json_decode($response_body, true);
7145
7146 if (isset($decoded_response['choices'][0]['message']['content'])) {
7147 return trim($decoded_response['choices'][0]['message']['content']);
7148 } else {
7149 //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
7150 return [
7151 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
7152 'error_code' => 'xai_response_format_error',
7153 'provider' => 'xai'
7154 ];
7155 }
7156 } catch (Exception $e) {
7157 //error_log('X.AI Exception: ' . $e->getMessage());
7158 return [
7159 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
7160 'error_code' => 'xai_exception',
7161 'provider' => 'xai'
7162 ];
7163 }
7164
7165
7166 }
7167 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
7168 try {
7169 // Ensure conversation_history is an array
7170 if (!is_array($conversation_history)) {
7171 $conversation_history = array();
7172 }
7173
7174 // Get bot ID from session or request
7175 $bot_id = $this->get_current_bot_id($session_id);
7176
7177 // Get system prompt instructions using centralized function
7178 $system_prompt_instructions = $this->get_system_instructions($bot_id);
7179
7180 // Create a new array for the formatted conversation
7181 $formatted_conversation = array();
7182
7183 // Add system message first
7184 $formatted_conversation[] = array(
7185 'role' => 'system',
7186 'content' => $system_prompt_instructions . " " . $relevant_content
7187 );
7188
7189 // Add the rest of the conversation history
7190 foreach ($conversation_history as $message) {
7191 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7192 $role = $message['role'];
7193
7194 // Convert roles to supported format
7195 if ($role === 'bot' || $role === 'agent') {
7196 $role = 'assistant';
7197 }
7198 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
7199 $role = 'user';
7200 }
7201
7202 $formatted_conversation[] = array(
7203 'role' => $role,
7204 'content' => $message['content']
7205 );
7206 }
7207 }
7208
7209 $body = json_encode([
7210 'model' => $selected_model,
7211 'messages' => $formatted_conversation,
7212 'temperature' => 0.8,
7213 'stream' => false
7214 ]);
7215
7216 $args = [
7217 'body' => $body,
7218 'headers' => [
7219 'Content-Type' => 'application/json',
7220 'Authorization' => 'Bearer ' . $deepseek_api_key,
7221 ],
7222 'timeout' => 60,
7223 'redirection' => 5,
7224 'blocking' => true,
7225 'httpversion' => '1.0',
7226 'sslverify' => true,
7227 ];
7228
7229 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
7230
7231 if (is_wp_error($response)) {
7232 $error_message = $response->get_error_message();
7233 //error_log('DeepSeek API Error: ' . $error_message);
7234 return [
7235 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
7236 'error_code' => 'deepseek_connection_error',
7237 'provider' => 'deepseek'
7238 ];
7239 }
7240
7241 $status_code = wp_remote_retrieve_response_code($response);
7242 if ($status_code !== 200) {
7243 $response_body = wp_remote_retrieve_body($response);
7244 $decoded_response = json_decode($response_body, true);
7245
7246 $error_message = isset($decoded_response['error']['message'])
7247 ? $decoded_response['error']['message']
7248 : 'HTTP Error ' . $status_code;
7249
7250 $error_type = isset($decoded_response['error']['type'])
7251 ? $decoded_response['error']['type']
7252 : 'unknown';
7253
7254 //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
7255
7256 // Handle specific error types
7257 switch ($status_code) {
7258 case 401:
7259 return [
7260 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
7261 'error_code' => 'deepseek_auth_error',
7262 'provider' => 'deepseek'
7263 ];
7264
7265 case 400:
7266 if (strpos($error_message, 'API key') !== false) {
7267 return [
7268 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
7269 'error_code' => 'deepseek_invalid_api_key',
7270 'provider' => 'deepseek'
7271 ];
7272 }
7273 break;
7274
7275 case 429:
7276 if (strpos($error_message, 'quota') !== false) {
7277 return [
7278 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
7279 'error_code' => 'deepseek_quota_exceeded',
7280 'provider' => 'deepseek'
7281 ];
7282 } else {
7283 return [
7284 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
7285 'error_code' => 'deepseek_rate_limit',
7286 'provider' => 'deepseek'
7287 ];
7288 }
7289
7290 case 500:
7291 case 502:
7292 case 503:
7293 case 504:
7294 return [
7295 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
7296 'error_code' => 'deepseek_service_unavailable',
7297 'provider' => 'deepseek'
7298 ];
7299 }
7300
7301 // Generic error fallback
7302 return [
7303 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
7304 'error_code' => 'deepseek_api_error',
7305 'provider' => 'deepseek',
7306 'status_code' => $status_code
7307 ];
7308 }
7309
7310 $response_body = wp_remote_retrieve_body($response);
7311 $decoded_response = json_decode($response_body, true);
7312
7313 if (isset($decoded_response['choices'][0]['message']['content'])) {
7314 return trim($decoded_response['choices'][0]['message']['content']);
7315 } else {
7316 //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
7317 return [
7318 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
7319 'error_code' => 'deepseek_response_format_error',
7320 'provider' => 'deepseek'
7321 ];
7322 }
7323 } catch (Exception $e) {
7324 //error_log('DeepSeek Exception: ' . $e->getMessage());
7325 return [
7326 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
7327 'error_code' => 'deepseek_exception',
7328 'provider' => 'deepseek'
7329 ];
7330 }
7331 }
7332 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
7333 // Get bot ID from session or request
7334 $bot_id = $this->get_current_bot_id($session_id);
7335
7336 // Get system prompt instructions using centralized function
7337 $system_prompt_instructions = $this->get_system_instructions($bot_id);
7338
7339 // Add system prompt to relevant content
7340 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
7341
7342 // Format messages for Gemini API
7343 $formatted_messages = [];
7344
7345 // Add system message as the first user message with role prefix
7346 // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
7347 $formatted_messages[] = [
7348 'role' => 'user',
7349 'parts' => [
7350 ['text' => "[System Instructions] " . $content_with_instructions]
7351 ]
7352 ];
7353
7354 // Add model response to acknowledge system instructions
7355 $formatted_messages[] = [
7356 'role' => 'model',
7357 'parts' => [
7358 ['text' => "I understand and will follow these instructions."]
7359 ]
7360 ];
7361
7362 // Process the rest of the conversation history
7363 $current_role = null;
7364 $current_parts = [];
7365
7366 foreach ($conversation_history as $message) {
7367 // Skip the first system message as we already handled it
7368 if ($message['role'] === 'system') {
7369 continue;
7370 }
7371
7372 // Map roles to Gemini format
7373 $gemini_role = '';
7374 if ($message['role'] === 'user') {
7375 $gemini_role = 'user';
7376 } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
7377 $gemini_role = 'model';
7378 } else {
7379 // Skip unsupported roles
7380 continue;
7381 }
7382
7383 // If we have a new role, add the previous message
7384 if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
7385 $formatted_messages[] = [
7386 'role' => $current_role,
7387 'parts' => $current_parts
7388 ];
7389 $current_parts = [];
7390 }
7391
7392 // Set current role and add text to parts
7393 $current_role = $gemini_role;
7394 $current_parts[] = ['text' => $message['content']];
7395 }
7396
7397 // Add the last message if there's content
7398 if ($current_role !== null && !empty($current_parts)) {
7399 $formatted_messages[] = [
7400 'role' => $current_role,
7401 'parts' => $current_parts
7402 ];
7403 }
7404
7405 // Build the request body
7406 $body = json_encode([
7407 'contents' => $formatted_messages,
7408 'generationConfig' => [
7409 'temperature' => 0.7,
7410 'topP' => 0.95,
7411 'topK' => 40,
7412 'maxOutputTokens' => 8192,
7413 ],
7414 'safetySettings' => [
7415 [
7416 'category' => 'HARM_CATEGORY_HARASSMENT',
7417 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
7418 ],
7419 [
7420 'category' => 'HARM_CATEGORY_HATE_SPEECH',
7421 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
7422 ],
7423 [
7424 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
7425 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
7426 ],
7427 [
7428 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
7429 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
7430 ]
7431 ]
7432 ]);
7433
7434 // Prepare the API endpoint
7435 $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
7436
7437 // Set up the API request
7438 $args = [
7439 'body' => $body,
7440 'headers' => [
7441 'Content-Type' => 'application/json',
7442 ],
7443 'timeout' => 60,
7444 'redirection' => 5,
7445 'blocking' => true,
7446 'httpversion' => '1.0',
7447 'sslverify' => true,
7448 ];
7449
7450 // Make the API request
7451 $response = wp_remote_post($api_endpoint, $args);
7452
7453 // Process the response
7454 if (is_wp_error($response)) {
7455 return "Sorry, there was an error processing your request: " . $response->get_error_message();
7456 }
7457
7458 $response_body = json_decode(wp_remote_retrieve_body($response), true);
7459
7460 // Handle potential errors in the response
7461 if (isset($response_body['error'])) {
7462 //error_log('Gemini API Error: ' . json_encode($response_body['error']));
7463 return "Sorry, there was an error with the Gemini API: " .
7464 (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
7465 }
7466
7467 // Extract the response text
7468 if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
7469 return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
7470 } else {
7471 //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
7472 return "Sorry, I couldn't process that request. The response format was unexpected.";
7473 }
7474 }
7475
7476
7477 public function test_streaming_request() {
7478 $options = get_option('mxchat_options', []);
7479 $model = $options['model'] ?? 'gpt-4o';
7480
7481 // Detect provider from model prefix
7482 $provider = strtolower(explode('-', $model)[0]);
7483
7484 $sample_prompt = 'Hello! Can you stream this response back to me?';
7485 $messages = [['role' => 'user', 'content' => $sample_prompt]];
7486 $headers = [];
7487 $body = [];
7488 $url = '';
7489 $api_key = '';
7490
7491 switch ($provider) {
7492 case 'gpt':
7493 case 'o1':
7494 $api_key = $options['api_key'] ?? '';
7495 if (empty($api_key)) return '❌ Missing API key for OpenAI';
7496 $url = 'https://api.openai.com/v1/chat/completions';
7497 $headers = [
7498 'Content-Type: application/json',
7499 'Authorization: Bearer ' . $api_key
7500 ];
7501 $body = [
7502 'model' => $model,
7503 'messages' => $messages,
7504 'stream' => true
7505 ];
7506 break;
7507
7508 case 'claude':
7509 $api_key = $options['claude_api_key'] ?? '';
7510 if (empty($api_key)) return '❌ Missing API key for Claude';
7511 $url = 'https://api.anthropic.com/v1/messages';
7512 $headers = [
7513 'Content-Type: application/json',
7514 'x-api-key: ' . $api_key,
7515 'anthropic-version: 2023-06-01'
7516 ];
7517 $body = [
7518 'model' => $model,
7519 'messages' => $messages,
7520 'max_tokens' => 100,
7521 'stream' => true
7522 ];
7523 break;
7524
7525 case 'grok':
7526 $api_key = $options['xai_api_key'] ?? '';
7527 if (empty($api_key)) return '❌ Missing API key for X.AI';
7528 $url = 'https://api.x.ai/v1/chat/completions';
7529 $headers = [
7530 'Content-Type: application/json',
7531 'Authorization: Bearer ' . $api_key
7532 ];
7533 $body = [
7534 'model' => $model,
7535 'messages' => $messages,
7536 'stream' => true
7537 ];
7538 break;
7539
7540 case 'deepseek':
7541 if (empty($deepseek_api_key)) {
7542 $error_response = [
7543 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
7544 'error_code' => 'missing_deepseek_api_key'
7545 ];
7546 if ($testing_data !== null) {
7547 $error_response['testing_data'] = $testing_data;
7548 }
7549 return $error_response;
7550 }
7551 if ($streaming) {
7552 return $this->mxchat_generate_response_deepseek_stream(
7553 $selected_model,
7554 $deepseek_api_key,
7555 $conversation_history,
7556 $relevant_content,
7557 $session_id,
7558 $testing_data // Pass testing data
7559 );
7560 } else {
7561 $response = $this->mxchat_generate_response_deepseek(
7562 $selected_model,
7563 $deepseek_api_key,
7564 $conversation_history,
7565 $relevant_content
7566 );
7567 }
7568 break;
7569
7570 case 'gemini':
7571 $api_key = $options['gemini_api_key'] ?? '';
7572 if (empty($api_key)) return '❌ Missing API key for Gemini';
7573 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
7574 $headers = ['Content-Type: application/json'];
7575 $body = [
7576 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
7577 'generationConfig' => ['temperature' => 0.7]
7578 ];
7579 break;
7580
7581 default:
7582 return '❌ Unsupported provider: ' . $provider;
7583 }
7584
7585 // Do the actual streaming test
7586 $ch = curl_init($url);
7587 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
7588 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
7589 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
7590 curl_setopt($ch, CURLOPT_TIMEOUT, 15);
7591 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7592
7593 $response = curl_exec($ch);
7594 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7595 $error = curl_error($ch);
7596 curl_close($ch);
7597
7598 if ($error) return "❌ cURL error: $error";
7599 if ($http_code !== 200) {
7600 $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
7601 return "❌ HTTP $http_code: $error_message";
7602 }
7603
7604 return true;
7605 }
7606
7607 public function mxchat_dismiss_pre_chat_message() {
7608 // Get and sanitize the user identifier
7609 $user_id = $this->mxchat_get_user_identifier();
7610 $user_id = sanitize_key($user_id);
7611
7612 // Set a transient to track that the user has dismissed the pre-chat message
7613 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
7614 set_transient($transient_key, true, DAY_IN_SECONDS);
7615
7616 wp_send_json_success();
7617 }
7618
7619 public function mxchat_check_pre_chat_message_status() {
7620 // Get and sanitize the user identifier
7621 $user_id = $this->mxchat_get_user_identifier();
7622 $user_id = sanitize_key($user_id);
7623
7624 // Check if the transient exists (i.e., if the message was dismissed)
7625 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
7626 $dismissed = get_transient($transient_key);
7627
7628 // Log the result to see if it's being set correctly
7629 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
7630
7631 if ($dismissed) {
7632 wp_send_json_success(['dismissed' => true]);
7633 } else {
7634 wp_send_json_success(['dismissed' => false]);
7635 }
7636
7637 wp_die();
7638 }
7639
7640 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
7641 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
7642 return 0;
7643 }
7644
7645 $dotProduct = array_sum(array_map(function ($a, $b) {
7646 return $a * $b;
7647 }, $vectorA, $vectorB));
7648 $normA = sqrt(array_sum(array_map(function ($a) {
7649 return $a * $a;
7650 }, $vectorA)));
7651 $normB = sqrt(array_sum(array_map(function ($b) {
7652 return $b * $b;
7653 }, $vectorB)));
7654
7655 if ($normA == 0 || $normB == 0) {
7656 return 0;
7657 }
7658
7659 return $dotProduct / ($normA * $normB);
7660 }
7661
7662
7663 public function mxchat_enqueue_scripts_styles() {
7664 // Enqueue the script
7665 wp_enqueue_script(
7666 'mxchat-chat-js',
7667 plugin_dir_url(__FILE__) . '../js/chat-script.js',
7668 array('jquery'),
7669 MXCHAT_VERSION,
7670 true
7671 );
7672 // Enqueue the CSS
7673 wp_enqueue_style(
7674 'mxchat-chat-css',
7675 plugin_dir_url(__FILE__) . '../css/chat-style.css',
7676 array(),
7677 MXCHAT_VERSION
7678 );
7679 // Fetch options from the database
7680 $this->options = get_option('mxchat_options');
7681 $prompts_options = get_option('mxchat_prompts_options', array());
7682
7683 // Prepare settings for JavaScript
7684 $style_settings = array(
7685 'ajax_url' => admin_url('admin-ajax.php'),
7686 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
7687 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
7688 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
7689 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
7690 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
7691 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
7692 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
7693 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
7694 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
7695 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
7696 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
7697 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
7698 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
7699 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
7700 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
7701 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
7702 'icon_color' => $this->options['icon_color'] ?? '#fff',
7703 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
7704 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
7705 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
7706 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
7707 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
7708 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
7709 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
7710 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
7711 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
7712 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
7713 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
7714 'initial_email_state' => null, // Also fixed this undefined variable
7715 'skip_email_check' => true,
7716 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
7717 );
7718 // Pass the settings to the script
7719 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
7720 }
7721
7722
7723 /**
7724 * Setup the cron jobs for rate limits with guard against multiple calls
7725 */
7726 public function setup_rate_limit_cron_jobs() {
7727 // Add a guard to prevent multiple rapid calls
7728 $last_setup = get_transient('mxchat_cron_setup_guard');
7729 if ($last_setup && (time() - $last_setup) < 60) {
7730 // Don't run again if we ran less than 60 seconds ago
7731 return;
7732 }
7733
7734 // Set the guard
7735 set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
7736
7737 try {
7738 // First, check if WordPress cron is disabled
7739 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
7740 //error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
7741 $this->setup_fallback_rate_limit_system();
7742 return;
7743 }
7744
7745 // Check if cron is already scheduled - if so, don't mess with it
7746 if (wp_next_scheduled('mxchat_reset_rate_limits')) {
7747 //error_log('MxChat: Rate limit cron already scheduled, skipping setup');
7748 return;
7749 }
7750
7751 // Clear any orphaned hooks (but don't loop indefinitely)
7752 $hooks_to_clear = [
7753 'mxchat_reset_rate_limits',
7754 'mxchat_reset_hourly_rate_limits',
7755 'mxchat_reset_daily_rate_limits',
7756 'mxchat_reset_weekly_rate_limits',
7757 'mxchat_reset_monthly_rate_limits'
7758 ];
7759
7760 foreach ($hooks_to_clear as $hook) {
7761 // Only clear a maximum of 3 instances to prevent infinite loops
7762 $cleared = 0;
7763 while (wp_next_scheduled($hook) && $cleared < 3) {
7764 wp_clear_scheduled_hook($hook);
7765 $cleared++;
7766 }
7767 }
7768
7769 // Small delay after clearing
7770 usleep(100000); // 0.1 seconds
7771
7772 // Try to schedule the event
7773 $initial_time = time() + 300; // Start in 5 minutes
7774 $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
7775
7776 if ($result === false) {
7777 //error_log('MxChat: Failed to schedule cron, using fallback system');
7778 $this->setup_fallback_rate_limit_system();
7779 } else {
7780 //error_log('MxChat: Successfully scheduled rate limit reset cron');
7781 }
7782
7783 } catch (Exception $e) {
7784 //error_log('MxChat: Cron setup exception: ' . $e->getMessage());
7785 $this->setup_fallback_rate_limit_system();
7786 }
7787 }
7788
7789 /**
7790 * Try alternative cron scheduling methods
7791 */
7792 private function try_alternative_cron_scheduling($initial_time) {
7793 try {
7794 // Method 1: Try with current time instead of future time
7795 $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
7796 if ($result1 !== false) {
7797 //error_log('MxChat: Alternative method 1 (current time) succeeded');
7798 return true;
7799 }
7800
7801 // Method 2: Try with a different interval
7802 $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
7803 if ($result2 !== false) {
7804 //error_log('MxChat: Alternative method 2 (daily interval) succeeded');
7805 return true;
7806 }
7807
7808 // Method 3: Try wp_schedule_single_event first, then recurring
7809 $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
7810 if ($result3 !== false) {
7811 //error_log('MxChat: Alternative method 3 (single event) succeeded');
7812 // Schedule the next one manually in the handler
7813 return true;
7814 }
7815
7816 return false;
7817
7818 } catch (Exception $e) {
7819 //error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
7820 return false;
7821 }
7822 }
7823
7824 /**
7825 * Enhanced fallback rate limit system
7826 */
7827 private function setup_fallback_rate_limit_system() {
7828 // Set a flag to use database-based rate limit cleanup
7829 update_option('mxchat_use_fallback_rate_limits', true);
7830
7831 // Schedule a one-time check to happen on the next plugin load
7832 update_option('mxchat_next_rate_limit_check', time() + 3600);
7833
7834 // Also set up a more frequent fallback check (every 4 hours)
7835 update_option('mxchat_fallback_check_interval', 4 * 3600);
7836
7837 //error_log('MxChat: Fallback rate limit system activated');
7838 }
7839
7840 /**
7841 * Enhanced fallback check method
7842 */
7843 public function check_fallback_rate_limits() {
7844 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
7845
7846 if (!$use_fallback) {
7847 return; // Regular cron is working
7848 }
7849
7850 $next_check = get_option('mxchat_next_rate_limit_check', 0);
7851 $check_interval = get_option('mxchat_fallback_check_interval', 3600);
7852
7853 if (time() >= $next_check) {
7854 //error_log('MxChat: Running fallback rate limit cleanup');
7855 $this->mxchat_reset_rate_limits();
7856
7857 // Schedule next check
7858 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
7859 }
7860 }
7861 /**
7862 * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
7863 */
7864 public function check_rate_limit() {
7865 // Check if we need to run fallback cleanup
7866 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
7867 $next_check = get_option('mxchat_next_rate_limit_check', 0);
7868
7869 if ($use_fallback && time() >= $next_check) {
7870 $this->mxchat_reset_rate_limits();
7871 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
7872 }
7873
7874 // Get bot ID from current request context
7875 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
7876
7877 // Get bot-specific options (includes rate limits if overridden)
7878 $bot_options = $this->get_bot_options($bot_id);
7879 $current_options = !empty($bot_options) ? $bot_options : $this->options;
7880
7881 // Use bot-specific rate limits if available, otherwise fall back to default
7882 $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
7883
7884 // Determine user role or if logged out
7885 if (is_user_logged_in()) {
7886 $user = wp_get_current_user();
7887 $user_id = $user->ID;
7888
7889 // Get the user's primary role using reset() to safely get the first element
7890 $user_roles = $user->roles;
7891
7892 // Safely get the first role regardless of array key structure
7893 if (!empty($user_roles) && is_array($user_roles)) {
7894 $role = reset($user_roles); // This safely gets the first element regardless of key
7895 } else {
7896 $role = 'subscriber'; // Default to subscriber if no role found
7897 }
7898 } else {
7899 $role = 'logged_out';
7900 // Use IP address for non-logged-in users
7901 $user_id = $this->get_client_ip();
7902 }
7903
7904 // Check if rate limits are configured for this role
7905 if (!isset($rate_limits_source[$role])) {
7906 return true; // No limit set for this role
7907 }
7908
7909 $limit = $rate_limits_source[$role]['limit'];
7910
7911 // If unlimited, return true immediately
7912 if ($limit === 'unlimited') {
7913 return true;
7914 }
7915
7916 // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
7917 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
7918 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
7919 $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
7920
7921 // Include bot_id in option name so each bot has separate rate limits
7922 $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
7923
7924 // Get the counter data
7925 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
7926
7927 // If first request or counter reset needed, set the initial timestamp
7928 if ($limit_data['count'] === 0) {
7929 $limit_data['timestamp'] = time();
7930 update_option($option_name, $limit_data);
7931 }
7932
7933 // Get the timeframe
7934 $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
7935 $rate_limits_source[$role]['timeframe'] : 'daily';
7936
7937 // Check if the counter needs to be reset based on timeframe
7938 $current_time = time();
7939 $timestamp = $limit_data['timestamp'];
7940 $should_reset = false;
7941
7942 switch ($timeframe) {
7943 case 'hourly':
7944 $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
7945 break;
7946 case 'daily':
7947 $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
7948 break;
7949 case 'weekly':
7950 $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
7951 break;
7952 case 'monthly':
7953 $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
7954 break;
7955 }
7956
7957 // Reset the counter if the timeframe has passed
7958 if ($should_reset) {
7959 $limit_data = ['count' => 0, 'timestamp' => $current_time];
7960 update_option($option_name, $limit_data);
7961 }
7962
7963 // Check if user has exceeded their limit
7964 if ($limit_data['count'] >= intval($limit)) {
7965 // Get the custom message for this role
7966 $message = !empty($rate_limits_source[$role]['message'])
7967 ? $rate_limits_source[$role]['message']
7968 : __('Rate limit exceeded. Please try again later.', 'mxchat');
7969
7970 // Add timeframe information to the message if placeholders exist
7971 $timeframe_label = '';
7972 switch ($timeframe) {
7973 case 'hourly':
7974 $timeframe_label = __('hour', 'mxchat');
7975 break;
7976 case 'daily':
7977 $timeframe_label = __('day', 'mxchat');
7978 break;
7979 case 'weekly':
7980 $timeframe_label = __('week', 'mxchat');
7981 break;
7982 case 'monthly':
7983 $timeframe_label = __('month', 'mxchat');
7984 break;
7985 }
7986
7987 // Replace placeholders in the message
7988 $message = str_replace(
7989 ['{limit}', '{count}', '{remaining}', '{timeframe}'],
7990 [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
7991 $message
7992 );
7993
7994 // Process HTML links in the message
7995 $message = $this->process_rate_limit_message_html($message);
7996
7997 // Return error with the processed message
7998 return [
7999 'error' => true,
8000 'message' => $message
8001 ];
8002 }
8003
8004 // Increment the counter
8005 $limit_data['count']++;
8006 update_option($option_name, $limit_data);
8007
8008 return true;
8009 }
8010
8011 /**
8012 * Enhanced rate limit reset with better error handling
8013 */
8014 public function mxchat_reset_rate_limits() {
8015 try {
8016 global $wpdb;
8017 $all_options = get_option('mxchat_options', []);
8018 $current_time = time();
8019
8020 // Get rate limit options with a safer query and limit
8021 $option_names = $wpdb->get_col(
8022 $wpdb->prepare(
8023 "SELECT option_name FROM {$wpdb->options}
8024 WHERE option_name LIKE %s
8025 LIMIT 1000",
8026 'mxchat_chat_limit_%'
8027 )
8028 );
8029
8030 if (empty($option_names)) {
8031 return;
8032 }
8033
8034 $processed_count = 0;
8035 $max_processing_time = 30; // Maximum 30 seconds
8036 $start_time = time();
8037
8038 foreach ($option_names as $option_name) {
8039 // Check processing time limit
8040 if ((time() - $start_time) > $max_processing_time) {
8041 //error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
8042 break;
8043 }
8044
8045 // Parse the option name more safely
8046 if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
8047 continue;
8048 }
8049
8050 $role_and_user = $matches[1] . '_' . $matches[2];
8051 $parts = explode('_', $role_and_user);
8052
8053 if (count($parts) < 2) {
8054 continue;
8055 }
8056
8057 // Extract role (everything except the last part which is user ID)
8058 $user_id_part = array_pop($parts);
8059 $role = implode('_', $parts);
8060
8061 // Skip if role doesn't exist in our settings
8062 if (!isset($all_options['rate_limits'][$role])) {
8063 // Clean up orphaned entries
8064 delete_option($option_name);
8065 continue;
8066 }
8067
8068 $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
8069 $limit_data = get_option($option_name);
8070
8071 if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
8072 // Clean up invalid entries
8073 delete_option($option_name);
8074 continue;
8075 }
8076
8077 $timestamp = $limit_data['timestamp'];
8078 $should_reset = false;
8079
8080 // Determine if we should reset based on the timeframe
8081 switch ($timeframe) {
8082 case 'hourly':
8083 $should_reset = ($current_time - $timestamp) >= 3600;
8084 break;
8085 case 'daily':
8086 $should_reset = ($current_time - $timestamp) >= 86400;
8087 break;
8088 case 'weekly':
8089 $should_reset = ($current_time - $timestamp) >= 604800;
8090 break;
8091 case 'monthly':
8092 $should_reset = ($current_time - $timestamp) >= 2592000;
8093 break;
8094 }
8095
8096 // Reset the counter if the timeframe has passed
8097 if ($should_reset) {
8098 delete_option($option_name);
8099 wp_cache_delete($option_name, 'options');
8100 $processed_count++;
8101 }
8102 }
8103
8104 // Clean up any orphaned cache entries
8105 wp_cache_delete('mxchat_all_chat_limits', 'options');
8106
8107 //error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
8108
8109 } catch (Exception $e) {
8110 //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
8111 }
8112 }
8113
8114
8115 /**
8116 * Process HTML links in rate limit messages
8117 *
8118 * @param string $message The rate limit message
8119 * @return string The processed message with safe HTML links
8120 */
8121 private function process_rate_limit_message_html($message) {
8122 // Return original message if empty
8123 if (empty($message)) {
8124 return $message;
8125 }
8126
8127 // First, convert markdown links to HTML
8128 $message = $this->convert_markdown_links($message);
8129
8130 // Then, auto-convert any remaining plain URLs to links
8131 $message = $this->auto_link_urls($message);
8132
8133 // Allow basic HTML tags for links and formatting
8134 $allowed_tags = [
8135 'a' => [
8136 'href' => true,
8137 'target' => true,
8138 'rel' => true,
8139 'title' => true,
8140 'class' => true
8141 ],
8142 'strong' => [],
8143 'em' => [],
8144 'br' => [],
8145 'b' => [],
8146 'i' => [],
8147 'span' => ['class' => true]
8148 ];
8149
8150 // Sanitize but allow the specified HTML tags
8151 $processed_message = wp_kses($message, $allowed_tags);
8152
8153 // If wp_kses stripped everything, return the original message as plain text
8154 if (empty($processed_message) && !empty($message)) {
8155 // Strip all HTML and return plain text as fallback
8156 return wp_strip_all_tags($message);
8157 }
8158
8159 return $processed_message;
8160 }
8161
8162 /**
8163 * Convert markdown links to HTML
8164 *
8165 * @param string $text The text to process
8166 * @return string The text with markdown links converted to HTML
8167 */
8168 private function convert_markdown_links($text) {
8169 // Return original text if empty
8170 if (empty($text)) {
8171 return $text;
8172 }
8173
8174 // Pattern to match markdown links: [text](url)
8175 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
8176
8177 $processed_text = preg_replace_callback($pattern, function($matches) {
8178 $link_text = $matches[1];
8179 $url = $matches[2];
8180
8181 // Clean up any trailing punctuation from the URL
8182 $url = rtrim($url, '.,;:!?');
8183
8184 // Sanitize the link text and URL
8185 $safe_text = esc_html($link_text);
8186 $safe_url = esc_url($url);
8187
8188 // Create the HTML link
8189 return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
8190 }, $text);
8191
8192 // If preg_replace_callback failed, return original text
8193 if ($processed_text === null) {
8194 return $text;
8195 }
8196
8197 return $processed_text;
8198 }
8199
8200 /**
8201 * Auto-convert plain URLs to clickable links
8202 *
8203 * @param string $text The text to process
8204 * @return string The text with URLs converted to links
8205 */
8206 private function auto_link_urls($text) {
8207 // Return original text if empty
8208 if (empty($text)) {
8209 return $text;
8210 }
8211
8212 // Simple pattern that avoids complex lookbehinds
8213 // This will match URLs that are not already inside href attributes or markdown links
8214 $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
8215
8216 $processed_text = preg_replace_callback($pattern, function($matches) {
8217 $url = $matches[0];
8218 // Clean up any trailing punctuation that might have been captured
8219 $url = rtrim($url, '.,;:!?');
8220
8221 // Add target="_blank" and rel="noopener noreferrer" for security
8222 return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
8223 }, $text);
8224
8225 // If preg_replace_callback failed, return original text
8226 if ($processed_text === null) {
8227 return $text;
8228 }
8229
8230 return $processed_text;
8231 }
8232
8233
8234 // Helper function to get client IP address
8235 private function get_client_ip() {
8236 // Check for shared internet/ISP IP
8237 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
8238 return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
8239 }
8240
8241 // Check for IPs passing through proxies
8242 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
8243 // Use the first value in the comma-separated list
8244 $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
8245 return trim($forwarded_for[0]);
8246 }
8247
8248 if (!empty($_SERVER['REMOTE_ADDR'])) {
8249 return sanitize_text_field($_SERVER['REMOTE_ADDR']);
8250 }
8251
8252 // Fallback
8253 return 'unknown';
8254 }
8255
8256 /**
8257 * AJAX handler to get system information for testing panel
8258 */
8259 /**
8260 * AJAX handler to get system information for testing panel
8261 */
8262 public function mxchat_get_system_info() {
8263 // Verify nonce for security
8264 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
8265 wp_send_json_error(['message' => 'Invalid nonce']);
8266 return;
8267 }
8268
8269 // Only allow admin users
8270 if (!current_user_can('administrator')) {
8271 wp_send_json_error(['message' => 'Unauthorized']);
8272 return;
8273 }
8274
8275 // Get system prompt from options
8276 $system_prompt = isset($this->options['system_prompt_instructions'])
8277 ? $this->options['system_prompt_instructions']
8278 : 'No system prompt configured';
8279
8280 // Get selected model
8281 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
8282
8283 // Check if OpenRouter is being used
8284 $is_openrouter = ($selected_model === 'openrouter');
8285 $openrouter_model = '';
8286
8287 if ($is_openrouter) {
8288 // Get the actual OpenRouter model that's selected
8289 $openrouter_model = isset($this->options['openrouter_selected_model'])
8290 ? $this->options['openrouter_selected_model']
8291 : 'No OpenRouter model selected';
8292
8293 // Update selected_model display to show both
8294 $selected_model = 'OpenRouter: ' . $openrouter_model;
8295 }
8296
8297 // Get API key status (just check if they exist, don't expose the keys)
8298 $api_status = [];
8299 $api_status['openai'] = !empty($this->options['api_key']);
8300 $api_status['claude'] = !empty($this->options['claude_api_key']);
8301 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
8302 $api_status['xai'] = !empty($this->options['xai_api_key']);
8303 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
8304 $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
8305
8306 wp_send_json_success([
8307 'system_prompt' => $system_prompt,
8308 'selected_model' => $selected_model,
8309 'is_openrouter' => $is_openrouter,
8310 'openrouter_model' => $openrouter_model,
8311 'api_status' => $api_status
8312 ]);
8313 }
8314
8315 /**
8316 * AJAX handler to get similarity threshold
8317 */
8318 public function mxchat_get_similarity_threshold() {
8319 // Verify nonce for security
8320 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
8321 wp_send_json_error(['message' => 'Invalid nonce']);
8322 return;
8323 }
8324
8325 // Only allow admin users
8326 if (!current_user_can('administrator')) {
8327 wp_send_json_error(['message' => 'Unauthorized']);
8328 return;
8329 }
8330
8331 // Get similarity threshold from main options (default 35%)
8332 $similarity_threshold = isset($this->options['similarity_threshold'])
8333 ? ((int) $this->options['similarity_threshold']) / 100
8334 : 0.35;
8335
8336 wp_send_json_success([
8337 'threshold' => $similarity_threshold,
8338 'threshold_percentage' => ($similarity_threshold * 100) . '%'
8339 ]);
8340 }
8341
8342 /**
8343 * AJAX handler to get knowledge base status
8344 */
8345 public function mxchat_get_kb_status() {
8346 // Verify nonce for security
8347 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
8348 wp_send_json_error(['message' => 'Invalid nonce']);
8349 return;
8350 }
8351
8352 // Only allow admin users
8353 if (!current_user_can('administrator')) {
8354 wp_send_json_error(['message' => 'Unauthorized']);
8355 return;
8356 }
8357
8358 // Check Pinecone vs WordPress
8359 $addon_options = get_option('mxchat_pinecone_addon_options', array());
8360 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
8361
8362 $kb_info = [
8363 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
8364 'status' => 'Active'
8365 ];
8366
8367 // Get document count
8368 if ($use_pinecone) {
8369 $kb_info['documents'] = 'Connected to Pinecone';
8370 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
8371 } else {
8372 // Count documents in WordPress database
8373 global $wpdb;
8374 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
8375 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
8376 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
8377 }
8378
8379 wp_send_json_success($kb_info);
8380 }
8381
8382 /**
8383 * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
8384 */
8385 public function mxchat_start_fresh_session() {
8386 // Verify nonce for security
8387 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
8388 wp_send_json_error(['message' => 'Invalid nonce']);
8389 return;
8390 }
8391
8392 // Only allow admin users
8393 if (!current_user_can('administrator')) {
8394 wp_send_json_error(['message' => 'Unauthorized']);
8395 return;
8396 }
8397
8398 $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
8399 $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
8400
8401 if (empty($old_session_id)) {
8402 wp_send_json_error(['message' => 'Old session ID required']);
8403 return;
8404 }
8405
8406 // If no new session ID provided, generate one
8407 if (empty($new_session_id)) {
8408 $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
8409 }
8410
8411 // Clear ALL data associated with the old session
8412 $this->clear_complete_session_data($old_session_id);
8413
8414 // Initialize the new session
8415 $this->initialize_fresh_session($new_session_id);
8416
8417 wp_send_json_success([
8418 'message' => 'Fresh session started successfully',
8419 'new_session_id' => $new_session_id,
8420 'old_session_id' => $old_session_id
8421 ]);
8422 }
8423
8424 /**
8425 * Clear ALL data associated with a session (ENHANCED)
8426 */
8427 private function clear_complete_session_data($session_id) {
8428 // Clear chat history
8429 delete_option("mxchat_history_{$session_id}");
8430
8431 // Clear chat mode
8432 delete_option("mxchat_mode_{$session_id}");
8433
8434 // Clear any PDF/Word transients
8435 $this->clear_pdf_transients($session_id);
8436 if (method_exists($this, 'clear_word_transients')) {
8437 $this->clear_word_transients($session_id);
8438 }
8439
8440 // Clear agent-related data
8441 delete_option("mxchat_channel_{$session_id}");
8442 delete_option("mxchat_agent_name_{$session_id}");
8443 delete_option("mxchat_email_{$session_id}");
8444
8445 // Clear any recommendation flow state
8446 delete_option("mxchat_sr_flow_state_{$session_id}");
8447
8448 // Clear any cached embeddings or context
8449 delete_transient("mxchat_context_{$session_id}");
8450 delete_transient("mxchat_last_query_{$session_id}");
8451
8452 // Clear any testing data
8453 delete_transient("mxchat_testing_data_{$session_id}");
8454
8455 // Clear any rate limiting data for this session
8456 delete_transient("mxchat_rate_limit_{$session_id}");
8457
8458 // Clear any other session-specific transients
8459 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
8460 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
8461 delete_transient("mxchat_include_word_in_context_{$session_id}");
8462
8463 // Clear form addon state (pending forms and submitted forms)
8464 delete_option("mxchat_pending_form_{$session_id}");
8465 delete_option("mxchat_submitted_forms_{$session_id}");
8466
8467 //error_log("MxChat: Cleared all data for session: {$session_id}");
8468 }
8469
8470 /**
8471 * Initialize a fresh session with default data
8472 */
8473 private function initialize_fresh_session($session_id) {
8474 // Set default chat mode
8475 update_option("mxchat_mode_{$session_id}", 'ai');
8476
8477 //error_log("MxChat: Initialized fresh session: {$session_id}");
8478 }
8479
8480 /**
8481 * Helper method to clear Word document transients (if you have Word support)
8482 */
8483 private function clear_word_transients($session_id) {
8484 delete_transient('mxchat_word_url_' . $session_id);
8485 delete_transient('mxchat_word_filename_' . $session_id);
8486 delete_transient('mxchat_word_embeddings_' . $session_id);
8487 delete_transient('mxchat_include_word_in_context_' . $session_id);
8488 }
8489
8490 /**
8491 * Simplified testing data capture method (CLEANED UP)
8492 */
8493 private function capture_testing_data($user_embedding, $message, $session_id) {
8494 // Only capture for admin users
8495 if (!current_user_can('administrator')) {
8496 return null;
8497 }
8498
8499 $testing_data = [
8500 'query' => $message,
8501 'timestamp' => time(),
8502 'top_matches' => [],
8503 'action_matches' => [] // Add action matches
8504 ];
8505
8506 // Get similarity threshold
8507 $similarity_threshold = isset($this->options['similarity_threshold'])
8508 ? ((int) $this->options['similarity_threshold']) / 100
8509 : 0.35;
8510
8511 $testing_data['similarity_threshold'] = $similarity_threshold;
8512
8513 // Use the real similarity analysis if available
8514 if ($this->last_similarity_analysis !== null) {
8515 $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
8516 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
8517 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8518 } else {
8519 // Fallback: determine knowledge base type
8520 $addon_options = get_option('mxchat_pinecone_addon_options', array());
8521 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
8522
8523 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
8524 }
8525
8526 // Include action analysis if available
8527 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
8528 $testing_data['action_matches'] = $this->last_action_analysis;
8529
8530 // Clear it after capturing to avoid stale data
8531 $this->last_action_analysis = null;
8532 }
8533
8534 return $testing_data;
8535 }
8536
8537
8538 /**
8539 * Track URL clicks from chatbot responses
8540 */
8541 public function mxchat_track_url_click() {
8542 // Verify nonce for security
8543 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
8544 wp_send_json_error(['message' => 'Invalid nonce']);
8545 wp_die();
8546 }
8547
8548 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
8549 $clicked_url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
8550 $message_context = isset($_POST['message_context']) ? sanitize_textarea_field($_POST['message_context']) : '';
8551
8552 if (empty($session_id) || empty($clicked_url)) {
8553 wp_send_json_error(['message' => 'Missing required data']);
8554 wp_die();
8555 }
8556
8557 global $wpdb;
8558 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
8559
8560 // Insert click tracking record
8561 $wpdb->insert(
8562 $table_name,
8563 [
8564 'session_id' => $session_id,
8565 'clicked_url' => $clicked_url,
8566 'message_context' => $message_context,
8567 'click_timestamp' => current_time('mysql', 1),
8568 'user_ip' => $_SERVER['REMOTE_ADDR'],
8569 'user_agent' => $_SERVER['HTTP_USER_AGENT']
8570 ]
8571 );
8572
8573 wp_send_json_success(['message' => 'Click tracked']);
8574 wp_die();
8575 }
8576
8577 /**
8578 * Get URL click analytics for a session
8579 */
8580 public function mxchat_get_url_clicks($session_id) {
8581 global $wpdb;
8582 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
8583
8584 $clicks = $wpdb->get_results($wpdb->prepare(
8585 "SELECT * FROM $table_name WHERE session_id = %s ORDER BY click_timestamp ASC",
8586 $session_id
8587 ));
8588
8589 return $clicks;
8590 }
8591 /**
8592 * Track the originating page where chat was started
8593 */
8594 public function mxchat_track_originating_page() {
8595 // Verify nonce
8596 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
8597 wp_send_json_error(['message' => 'Invalid nonce']);
8598 wp_die();
8599 }
8600
8601 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
8602 $page_url = isset($_POST['page_url']) ? esc_url_raw($_POST['page_url']) : '';
8603 $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
8604
8605 if (empty($session_id)) {
8606 wp_send_json_error(['message' => 'Missing session ID']);
8607 wp_die();
8608 }
8609
8610 global $wpdb;
8611 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
8612
8613 // Check if we've already tracked for this session
8614 $existing = $wpdb->get_var($wpdb->prepare(
8615 "SELECT COUNT(*) FROM $table_name
8616 WHERE session_id = %s
8617 AND originating_page_url IS NOT NULL",
8618 $session_id
8619 ));
8620
8621 if ($existing > 0) {
8622 wp_send_json_success(['message' => 'Already tracked']);
8623 wp_die();
8624 }
8625
8626 // Update the first message in this session with originating page info
8627 $wpdb->query($wpdb->prepare(
8628 "UPDATE $table_name
8629 SET originating_page_url = %s,
8630 originating_page_title = %s
8631 WHERE session_id = %s
8632 ORDER BY timestamp ASC
8633 LIMIT 1",
8634 $page_url,
8635 $page_title,
8636 $session_id
8637 ));
8638
8639 wp_send_json_success(['message' => 'Originating page tracked']);
8640 wp_die();
8641 }
8642
8643 /**
8644 * Validate and clean URLs from AI response
8645 * Removes any URLs that aren't in the knowledge base
8646 *
8647 * @param string $response_text The AI-generated response
8648 * @param array $valid_urls Array of URLs from the knowledge base
8649 * @return string Cleaned response with invalid URLs removed/flagged
8650 */
8651 private function validate_and_clean_urls($response_text, $valid_urls) {
8652 // DEBUG: Log what we're working with
8653 error_log("=== MxChat URL Validation Debug ===");
8654 error_log("Valid URLs count: " . count($valid_urls));
8655 error_log("Valid URLs: " . print_r($valid_urls, true));
8656 error_log("Response text length: " . strlen($response_text));
8657 error_log("Response text preview: " . substr($response_text, 0, 500));
8658
8659 // If no valid URLs provided or empty response, return as-is
8660 if (empty($valid_urls) || empty($response_text)) {
8661 error_log("Validation skipped - empty valid_urls or response");
8662 return $response_text;
8663 }
8664
8665 // Extract all URLs from the AI response
8666 // This regex matches http:// and https:// URLs
8667 preg_match_all(
8668 '#\bhttps?://[^\s<>"\')\]]+#i',
8669 $response_text,
8670 $matches
8671 );
8672
8673 // If no URLs found in response, return as-is
8674 if (empty($matches[0])) {
8675 error_log("No URLs found in response");
8676 return $response_text;
8677 }
8678
8679 $found_urls = $matches[0];
8680 $cleaned_response = $response_text;
8681 $removed_count = 0;
8682
8683 // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
8684 $normalized_valid_urls = array_map(function($url) {
8685 // Remove trailing slash
8686 $url = rtrim($url, '/');
8687 // Remove URL fragments (#section)
8688 $url = preg_replace('/#.*$/', '', $url);
8689 // Remove trailing punctuation that might have been captured
8690 $url = rtrim($url, '.,;:!?');
8691 return $url;
8692 }, $valid_urls);
8693
8694 error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
8695
8696 foreach ($found_urls as $found_url) {
8697 // Clean up the found URL (remove trailing punctuation that might have been captured)
8698 $clean_found_url = rtrim($found_url, '.,;:!?)');
8699
8700 // DEBUG: Log each URL being checked
8701 error_log("Checking found URL: " . $found_url);
8702
8703 // Normalize for comparison
8704 $normalized_found = rtrim($clean_found_url, '/');
8705 $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
8706
8707 error_log("Normalized found URL: " . $normalized_found);
8708
8709 // Check if this URL exists in our valid URLs list
8710 $is_valid = false;
8711
8712 error_log("Starting validation checks for: " . $normalized_found);
8713
8714 // First, try exact match
8715 if (in_array($normalized_found, $normalized_valid_urls)) {
8716 $is_valid = true;
8717 error_log("EXACT MATCH FOUND");
8718 } else {
8719 error_log("No exact match, checking variations...");
8720 // If no exact match, check if it's a variation (with query params, etc.)
8721 foreach ($normalized_valid_urls as $valid_url) {
8722 error_log(" Comparing against valid URL: " . $valid_url);
8723
8724 // Check if the found URL starts with a valid URL (handles query params)
8725 if (strpos($normalized_found, $valid_url) === 0) {
8726 // Check what comes after the valid URL
8727 $remainder = substr($normalized_found, strlen($valid_url));
8728
8729 // Only valid if:
8730 // 1. Exact match (remainder is empty)
8731 // 2. Query params (starts with ?)
8732 // 3. Fragment (starts with #)
8733 if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
8734 $is_valid = true;
8735 error_log(" MATCH: Found URL is valid variation of base URL");
8736 break;
8737 } else {
8738 error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
8739 }
8740 }
8741 // Also check the reverse (in case valid URL has query params)
8742 if (strpos($valid_url, $normalized_found) === 0) {
8743 $is_valid = true;
8744 error_log(" MATCH: Valid URL starts with found URL");
8745 break;
8746 }
8747 }
8748
8749 if (!$is_valid) {
8750 error_log("NO MATCH FOUND - URL should be removed");
8751 }
8752 }
8753
8754 // If URL is not valid, remove it from the response
8755 if (!$is_valid) {
8756 // Log the removal for debugging
8757 error_log("MxChat: Removed hallucinated URL: " . $found_url);
8758 error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
8759
8760 $removed_count++;
8761
8762 // Check if URL is part of a markdown link: [text](url)
8763 $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
8764 if (preg_match($markdown_pattern, $cleaned_response)) {
8765 error_log("Found markdown link, removing but keeping text");
8766 // Remove the markdown link but keep the text
8767 $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
8768 }
8769 // Check if URL is part of an HTML link: <a href="url">text</a>
8770 else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
8771 error_log("Found HTML link, removing but keeping text");
8772 // Remove the HTML link but keep the text
8773 $link_text = $link_match[1];
8774 $cleaned_response = preg_replace(
8775 '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
8776 $link_text,
8777 $cleaned_response
8778 );
8779 }
8780 // Otherwise just remove the bare URL
8781 else {
8782 error_log("Removing bare URL");
8783 $cleaned_response = str_replace($found_url, '', $cleaned_response);
8784 }
8785 }
8786 }
8787
8788 // Log summary if any URLs were removed
8789 if ($removed_count > 0) {
8790 error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
8791 } else {
8792 error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
8793 }
8794
8795 // Clean up any double spaces or awkward punctuation left behind
8796 // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
8797 $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
8798 $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
8799
8800 error_log("Final cleaned response: " . $cleaned_response);
8801
8802 return trim($cleaned_response);
8803 }
8804
8805 /**
8806 * AJAX handler to get current chat mode for a session
8807 */
8808 public function mxchat_get_current_chat_mode() {
8809 // Verify nonce for security
8810 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
8811 wp_send_json_error(['message' => 'Invalid nonce']);
8812 wp_die();
8813 }
8814
8815 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
8816
8817 if (empty($session_id)) {
8818 wp_send_json_error(['message' => 'Session ID missing']);
8819 wp_die();
8820 }
8821
8822 // Get the current chat mode for this session
8823 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
8824
8825 wp_send_json_success([
8826 'chat_mode' => $chat_mode
8827 ]);
8828 wp_die();
8829 }
8830
8831
8832
8833 }
8834 ?>
8835