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

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