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

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

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