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

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

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