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

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

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