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

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