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

6,478 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' => 1,
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 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
4612 // Get system prompt instructions from options
4613 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4614
4615 // Clean and validate conversation history
4616 foreach ($conversation_history as &$message) {
4617 // Convert bot and agent roles to assistant
4618 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
4619 $message['role'] = 'assistant';
4620 }
4621
4622 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
4623 if (!in_array($message['role'], ['assistant', 'user'])) {
4624 $message['role'] = 'user';
4625 }
4626
4627 // Ensure content field exists
4628 if (!isset($message['content']) || empty($message['content'])) {
4629 $message['content'] = '';
4630 }
4631
4632 // Remove any unsupported fields
4633 $message = array_intersect_key($message, array_flip(['role', 'content']));
4634 }
4635
4636 // Add relevant content as the latest user message
4637 $conversation_history[] = [
4638 'role' => 'user',
4639 'content' => $relevant_content
4640 ];
4641
4642 // Build request body
4643 $body = json_encode([
4644 'model' => $selected_model,
4645 'max_tokens' => 1000,
4646 'temperature' => 0.8,
4647 'messages' => $conversation_history,
4648 'system' => $system_prompt_instructions
4649 ]);
4650
4651 // Set up API request
4652 $args = [
4653 'body' => $body,
4654 'headers' => [
4655 'Content-Type' => 'application/json',
4656 'x-api-key' => $claude_api_key,
4657 'anthropic-version' => '2023-06-01'
4658 ],
4659 'timeout' => 60,
4660 'redirection' => 5,
4661 'blocking' => true,
4662 'httpversion' => '1.0',
4663 'sslverify' => true,
4664 ];
4665
4666 // Make API request
4667 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
4668
4669 // Check for WordPress errors
4670 if (is_wp_error($response)) {
4671 //error_log("Claude API request error: " . $response->get_error_message());
4672 return "Sorry, there was an error connecting to the API.";
4673 }
4674
4675 // Check HTTP response code
4676 $http_code = wp_remote_retrieve_response_code($response);
4677 if ($http_code !== 200) {
4678 $error_body = wp_remote_retrieve_body($response);
4679 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
4680
4681 // Try to extract error message from response
4682 $error_data = json_decode($error_body, true);
4683 $error_message = isset($error_data['error']['message']) ?
4684 $error_data['error']['message'] :
4685 "HTTP error " . $http_code;
4686
4687 return "Sorry, the API returned an error: " . $error_message;
4688 }
4689
4690 // Parse response
4691 $response_body = json_decode(wp_remote_retrieve_body($response), true);
4692
4693 // Check for JSON decode errors
4694 if (json_last_error() !== JSON_ERROR_NONE) {
4695 //error_log("Claude API JSON decode error: " . json_last_error_msg());
4696 return "Sorry, there was an error processing the API response.";
4697 }
4698
4699 // Extract and validate response content
4700 if (isset($response_body['content']) &&
4701 is_array($response_body['content']) &&
4702 !empty($response_body['content']) &&
4703 isset($response_body['content'][0]['text'])) {
4704 return trim($response_body['content'][0]['text']);
4705 }
4706
4707 // Log unexpected response format
4708 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
4709 return "Sorry, I received an unexpected response format from the API.";
4710 }
4711 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
4712 try {
4713 // Ensure conversation_history is an array
4714 if (!is_array($conversation_history)) {
4715 $conversation_history = array();
4716 }
4717
4718 // Get system prompt instructions from options
4719 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4720
4721 // Create a new array for the formatted conversation
4722 $formatted_conversation = array();
4723
4724 // Add system message first
4725 $formatted_conversation[] = array(
4726 'role' => 'system',
4727 'content' => $system_prompt_instructions . " " . $relevant_content
4728 );
4729
4730 // Add the rest of the conversation history
4731 foreach ($conversation_history as $message) {
4732 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4733 $role = $message['role'];
4734
4735 // Convert roles to supported format
4736 if ($role === 'bot' || $role === 'agent') {
4737 $role = 'assistant';
4738 }
4739 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4740 $role = 'user';
4741 }
4742
4743 $formatted_conversation[] = array(
4744 'role' => $role,
4745 'content' => $message['content']
4746 );
4747 }
4748 }
4749
4750 $body = json_encode([
4751 'model' => $selected_model,
4752 'messages' => $formatted_conversation,
4753 'temperature' => 1,
4754 'stream' => false
4755 ]);
4756
4757 $args = [
4758 'body' => $body,
4759 'headers' => [
4760 'Content-Type' => 'application/json',
4761 'Authorization' => 'Bearer ' . $api_key,
4762 ],
4763 'timeout' => 60,
4764 'redirection' => 5,
4765 'blocking' => true,
4766 'httpversion' => '1.0',
4767 'sslverify' => true,
4768 ];
4769
4770 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
4771
4772 if (is_wp_error($response)) {
4773 $error_message = $response->get_error_message();
4774 //error_log('OpenAI API Error: ' . $error_message);
4775 return [
4776 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
4777 'error_code' => 'openai_connection_error',
4778 'provider' => 'openai'
4779 ];
4780 }
4781
4782 $status_code = wp_remote_retrieve_response_code($response);
4783 if ($status_code !== 200) {
4784 $response_body = wp_remote_retrieve_body($response);
4785 $decoded_response = json_decode($response_body, true);
4786
4787 $error_message = isset($decoded_response['error']['message'])
4788 ? $decoded_response['error']['message']
4789 : 'HTTP Error ' . $status_code;
4790
4791 $error_type = isset($decoded_response['error']['type'])
4792 ? $decoded_response['error']['type']
4793 : 'unknown';
4794
4795 //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4796
4797 // Handle specific error types
4798 switch ($error_type) {
4799 case 'invalid_request_error':
4800 if (strpos($error_message, 'API key') !== false) {
4801 return [
4802 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
4803 'error_code' => 'openai_invalid_api_key',
4804 'provider' => 'openai'
4805 ];
4806 }
4807 break;
4808
4809 case 'authentication_error':
4810 return [
4811 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
4812 'error_code' => 'openai_auth_error',
4813 'provider' => 'openai'
4814 ];
4815
4816 case 'rate_limit_exceeded':
4817 return [
4818 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
4819 'error_code' => 'openai_rate_limit',
4820 'provider' => 'openai'
4821 ];
4822
4823 case 'quota_exceeded':
4824 return [
4825 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
4826 'error_code' => 'openai_quota_exceeded',
4827 'provider' => 'openai'
4828 ];
4829 }
4830
4831 // Generic error fallback
4832 return [
4833 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
4834 'error_code' => 'openai_api_error',
4835 'provider' => 'openai',
4836 'status_code' => $status_code
4837 ];
4838 }
4839
4840 $response_body = wp_remote_retrieve_body($response);
4841 $decoded_response = json_decode($response_body, true);
4842
4843 if (isset($decoded_response['choices'][0]['message']['content'])) {
4844 return trim($decoded_response['choices'][0]['message']['content']);
4845 } else {
4846 //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
4847 return [
4848 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
4849 'error_code' => 'openai_response_format_error',
4850 'provider' => 'openai'
4851 ];
4852 }
4853 } catch (Exception $e) {
4854 //error_log('OpenAI Exception: ' . $e->getMessage());
4855 return [
4856 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
4857 'error_code' => 'openai_exception',
4858 'provider' => 'openai'
4859 ];
4860 }
4861 }
4862 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
4863 try {
4864 // Get system prompt instructions from options
4865 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4866
4867 // Add system prompt to relevant content
4868 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
4869
4870 // Prepend system instructions to the conversation history
4871 array_unshift($conversation_history, [
4872 'role' => 'system',
4873 'content' => "Here are your instructions: " . $content_with_instructions
4874 ]);
4875
4876 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
4877 foreach ($conversation_history as &$message) {
4878 if ($message['role'] === 'bot') {
4879 $message['role'] = 'assistant';
4880 } elseif ($message['role'] === 'agent') {
4881 // Tag the message as coming from a live agent
4882 $message['role'] = 'assistant';
4883 if (!isset($message['metadata'])) {
4884 $message['metadata'] = ['source' => 'live_agent'];
4885 }
4886 }
4887
4888 // Ensure all roles are valid
4889 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
4890 $message['role'] = 'user'; // Default to 'user'
4891 }
4892 }
4893
4894 // Build the request body
4895 $body = json_encode([
4896 'model' => $selected_model,
4897 'messages' => $conversation_history,
4898 'temperature' => 0.8,
4899 'stream' => false
4900 ]);
4901
4902 // Set up the API request
4903 $args = [
4904 'body' => $body,
4905 'headers' => [
4906 'Content-Type' => 'application/json',
4907 'Authorization' => 'Bearer ' . $xai_api_key,
4908 ],
4909 'timeout' => 60,
4910 'redirection' => 5,
4911 'blocking' => true,
4912 'httpversion' => '1.0',
4913 'sslverify' => true,
4914 ];
4915
4916 // Make the API request
4917 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
4918
4919 // Process the response
4920 if (is_wp_error($response)) {
4921 $error_message = $response->get_error_message();
4922 //error_log('X.AI API Error: ' . $error_message);
4923 return [
4924 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
4925 'error_code' => 'xai_connection_error',
4926 'provider' => 'xai'
4927 ];
4928 }
4929
4930 $status_code = wp_remote_retrieve_response_code($response);
4931 if ($status_code !== 200) {
4932 $response_body = wp_remote_retrieve_body($response);
4933 $decoded_response = json_decode($response_body, true);
4934
4935 // Log the full response for debugging
4936 //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
4937
4938 // Extract error message from X.AI's specific format
4939 $error_message = '';
4940
4941 // Check for direct error string (as seen in your logs)
4942 if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
4943 $error_message = $decoded_response['error'];
4944 }
4945 // Check for nested error object (OpenAI style)
4946 elseif (isset($decoded_response['error']['message'])) {
4947 $error_message = $decoded_response['error']['message'];
4948 }
4949 // Check for top-level message
4950 elseif (isset($decoded_response['message'])) {
4951 $error_message = $decoded_response['message'];
4952 }
4953 // Fallback
4954 else {
4955 $error_message = 'HTTP Error ' . $status_code;
4956 }
4957
4958 //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4959
4960 // Check for API key errors using string matching
4961 if (stripos($error_message, 'api key') !== false ||
4962 stripos($error_message, 'incorrect api key') !== false ||
4963 stripos($error_message, 'invalid api key') !== false) {
4964 return [
4965 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
4966 'error_code' => 'xai_invalid_api_key',
4967 'provider' => 'xai'
4968 ];
4969 }
4970
4971 // Authentication errors
4972 if ($status_code === 401 || $status_code === 403 ||
4973 stripos($error_message, 'auth') !== false) {
4974 return [
4975 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
4976 'error_code' => 'xai_auth_error',
4977 'provider' => 'xai'
4978 ];
4979 }
4980
4981 // Model errors
4982 if (stripos($error_message, 'model') !== false) {
4983 return [
4984 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
4985 'error_code' => 'xai_invalid_model',
4986 'provider' => 'xai'
4987 ];
4988 }
4989
4990 // Rate limit errors
4991 if ($status_code === 429 ||
4992 stripos($error_message, 'rate') !== false ||
4993 stripos($error_message, 'limit') !== false) {
4994 return [
4995 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
4996 'error_code' => 'xai_rate_limit',
4997 'provider' => 'xai'
4998 ];
4999 }
5000
5001 // Quota errors
5002 if (stripos($error_message, 'quota') !== false ||
5003 stripos($error_message, 'billing') !== false) {
5004 return [
5005 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
5006 'error_code' => 'xai_quota_exceeded',
5007 'provider' => 'xai'
5008 ];
5009 }
5010
5011 // Server errors
5012 if ($status_code >= 500) {
5013 return [
5014 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
5015 'error_code' => 'xai_service_unavailable',
5016 'provider' => 'xai'
5017 ];
5018 }
5019
5020 // Generic error fallback with the actual error message
5021 return [
5022 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
5023 'error_code' => 'xai_api_error',
5024 'provider' => 'xai',
5025 'status_code' => $status_code
5026 ];
5027 }
5028
5029 $response_body = wp_remote_retrieve_body($response);
5030 $decoded_response = json_decode($response_body, true);
5031
5032 if (isset($decoded_response['choices'][0]['message']['content'])) {
5033 return trim($decoded_response['choices'][0]['message']['content']);
5034 } else {
5035 //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
5036 return [
5037 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
5038 'error_code' => 'xai_response_format_error',
5039 'provider' => 'xai'
5040 ];
5041 }
5042 } catch (Exception $e) {
5043 //error_log('X.AI Exception: ' . $e->getMessage());
5044 return [
5045 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
5046 'error_code' => 'xai_exception',
5047 'provider' => 'xai'
5048 ];
5049 }
5050
5051
5052 }
5053 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
5054 try {
5055 // Ensure conversation_history is an array
5056 if (!is_array($conversation_history)) {
5057 $conversation_history = array();
5058 }
5059
5060 // Get system prompt instructions from options
5061 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5062
5063 // Create a new array for the formatted conversation
5064 $formatted_conversation = array();
5065
5066 // Add system message first
5067 $formatted_conversation[] = array(
5068 'role' => 'system',
5069 'content' => $system_prompt_instructions . " " . $relevant_content
5070 );
5071
5072 // Add the rest of the conversation history
5073 foreach ($conversation_history as $message) {
5074 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5075 $role = $message['role'];
5076
5077 // Convert roles to supported format
5078 if ($role === 'bot' || $role === 'agent') {
5079 $role = 'assistant';
5080 }
5081 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5082 $role = 'user';
5083 }
5084
5085 $formatted_conversation[] = array(
5086 'role' => $role,
5087 'content' => $message['content']
5088 );
5089 }
5090 }
5091
5092 $body = json_encode([
5093 'model' => $selected_model,
5094 'messages' => $formatted_conversation,
5095 'temperature' => 0.8,
5096 'stream' => false
5097 ]);
5098
5099 $args = [
5100 'body' => $body,
5101 'headers' => [
5102 'Content-Type' => 'application/json',
5103 'Authorization' => 'Bearer ' . $deepseek_api_key,
5104 ],
5105 'timeout' => 60,
5106 'redirection' => 5,
5107 'blocking' => true,
5108 'httpversion' => '1.0',
5109 'sslverify' => true,
5110 ];
5111
5112 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
5113
5114 if (is_wp_error($response)) {
5115 $error_message = $response->get_error_message();
5116 //error_log('DeepSeek API Error: ' . $error_message);
5117 return [
5118 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
5119 'error_code' => 'deepseek_connection_error',
5120 'provider' => 'deepseek'
5121 ];
5122 }
5123
5124 $status_code = wp_remote_retrieve_response_code($response);
5125 if ($status_code !== 200) {
5126 $response_body = wp_remote_retrieve_body($response);
5127 $decoded_response = json_decode($response_body, true);
5128
5129 $error_message = isset($decoded_response['error']['message'])
5130 ? $decoded_response['error']['message']
5131 : 'HTTP Error ' . $status_code;
5132
5133 $error_type = isset($decoded_response['error']['type'])
5134 ? $decoded_response['error']['type']
5135 : 'unknown';
5136
5137 //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
5138
5139 // Handle specific error types
5140 switch ($status_code) {
5141 case 401:
5142 return [
5143 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
5144 'error_code' => 'deepseek_auth_error',
5145 'provider' => 'deepseek'
5146 ];
5147
5148 case 400:
5149 if (strpos($error_message, 'API key') !== false) {
5150 return [
5151 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
5152 'error_code' => 'deepseek_invalid_api_key',
5153 'provider' => 'deepseek'
5154 ];
5155 }
5156 break;
5157
5158 case 429:
5159 if (strpos($error_message, 'quota') !== false) {
5160 return [
5161 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
5162 'error_code' => 'deepseek_quota_exceeded',
5163 'provider' => 'deepseek'
5164 ];
5165 } else {
5166 return [
5167 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
5168 'error_code' => 'deepseek_rate_limit',
5169 'provider' => 'deepseek'
5170 ];
5171 }
5172
5173 case 500:
5174 case 502:
5175 case 503:
5176 case 504:
5177 return [
5178 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
5179 'error_code' => 'deepseek_service_unavailable',
5180 'provider' => 'deepseek'
5181 ];
5182 }
5183
5184 // Generic error fallback
5185 return [
5186 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
5187 'error_code' => 'deepseek_api_error',
5188 'provider' => 'deepseek',
5189 'status_code' => $status_code
5190 ];
5191 }
5192
5193 $response_body = wp_remote_retrieve_body($response);
5194 $decoded_response = json_decode($response_body, true);
5195
5196 if (isset($decoded_response['choices'][0]['message']['content'])) {
5197 return trim($decoded_response['choices'][0]['message']['content']);
5198 } else {
5199 //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
5200 return [
5201 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
5202 'error_code' => 'deepseek_response_format_error',
5203 'provider' => 'deepseek'
5204 ];
5205 }
5206 } catch (Exception $e) {
5207 //error_log('DeepSeek Exception: ' . $e->getMessage());
5208 return [
5209 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
5210 'error_code' => 'deepseek_exception',
5211 'provider' => 'deepseek'
5212 ];
5213 }
5214 }
5215 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
5216 // Get system prompt instructions from options
5217 $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5218
5219 // Add system prompt to relevant content
5220 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5221
5222 // Format messages for Gemini API
5223 $formatted_messages = [];
5224
5225 // Add system message as the first user message with role prefix
5226 // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
5227 $formatted_messages[] = [
5228 'role' => 'user',
5229 'parts' => [
5230 ['text' => "[System Instructions] " . $content_with_instructions]
5231 ]
5232 ];
5233
5234 // Add model response to acknowledge system instructions
5235 $formatted_messages[] = [
5236 'role' => 'model',
5237 'parts' => [
5238 ['text' => "I understand and will follow these instructions."]
5239 ]
5240 ];
5241
5242 // Process the rest of the conversation history
5243 $current_role = null;
5244 $current_parts = [];
5245
5246 foreach ($conversation_history as $message) {
5247 // Skip the first system message as we already handled it
5248 if ($message['role'] === 'system') {
5249 continue;
5250 }
5251
5252 // Map roles to Gemini format
5253 $gemini_role = '';
5254 if ($message['role'] === 'user') {
5255 $gemini_role = 'user';
5256 } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
5257 $gemini_role = 'model';
5258 } else {
5259 // Skip unsupported roles
5260 continue;
5261 }
5262
5263 // If we have a new role, add the previous message
5264 if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
5265 $formatted_messages[] = [
5266 'role' => $current_role,
5267 'parts' => $current_parts
5268 ];
5269 $current_parts = [];
5270 }
5271
5272 // Set current role and add text to parts
5273 $current_role = $gemini_role;
5274 $current_parts[] = ['text' => $message['content']];
5275 }
5276
5277 // Add the last message if there's content
5278 if ($current_role !== null && !empty($current_parts)) {
5279 $formatted_messages[] = [
5280 'role' => $current_role,
5281 'parts' => $current_parts
5282 ];
5283 }
5284
5285 // Build the request body
5286 $body = json_encode([
5287 'contents' => $formatted_messages,
5288 'generationConfig' => [
5289 'temperature' => 0.7,
5290 'topP' => 0.95,
5291 'topK' => 40,
5292 'maxOutputTokens' => 8192,
5293 ],
5294 'safetySettings' => [
5295 [
5296 'category' => 'HARM_CATEGORY_HARASSMENT',
5297 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5298 ],
5299 [
5300 'category' => 'HARM_CATEGORY_HATE_SPEECH',
5301 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5302 ],
5303 [
5304 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
5305 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5306 ],
5307 [
5308 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
5309 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
5310 ]
5311 ]
5312 ]);
5313
5314 // Prepare the API endpoint
5315 $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5316
5317 // Set up the API request
5318 $args = [
5319 'body' => $body,
5320 'headers' => [
5321 'Content-Type' => 'application/json',
5322 ],
5323 'timeout' => 60,
5324 'redirection' => 5,
5325 'blocking' => true,
5326 'httpversion' => '1.0',
5327 'sslverify' => true,
5328 ];
5329
5330 // Make the API request
5331 $response = wp_remote_post($api_endpoint, $args);
5332
5333 // Process the response
5334 if (is_wp_error($response)) {
5335 return "Sorry, there was an error processing your request: " . $response->get_error_message();
5336 }
5337
5338 $response_body = json_decode(wp_remote_retrieve_body($response), true);
5339
5340 // Handle potential errors in the response
5341 if (isset($response_body['error'])) {
5342 //error_log('Gemini API Error: ' . json_encode($response_body['error']));
5343 return "Sorry, there was an error with the Gemini API: " .
5344 (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
5345 }
5346
5347 // Extract the response text
5348 if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
5349 return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
5350 } else {
5351 //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
5352 return "Sorry, I couldn't process that request. The response format was unexpected.";
5353 }
5354 }
5355
5356 public function test_streaming_request() {
5357 $options = get_option('mxchat_options', []);
5358 $model = $options['model'] ?? 'gpt-4o';
5359
5360 // Detect provider from model prefix
5361 $provider = strtolower(explode('-', $model)[0]);
5362
5363 $sample_prompt = 'Hello! Can you stream this response back to me?';
5364 $messages = [['role' => 'user', 'content' => $sample_prompt]];
5365 $headers = [];
5366 $body = [];
5367 $url = '';
5368 $api_key = '';
5369
5370 switch ($provider) {
5371 case 'gpt':
5372 case 'o1':
5373 $api_key = $options['api_key'] ?? '';
5374 if (empty($api_key)) return '❌ Missing API key for OpenAI';
5375 $url = 'https://api.openai.com/v1/chat/completions';
5376 $headers = [
5377 'Content-Type: application/json',
5378 'Authorization: Bearer ' . $api_key
5379 ];
5380 $body = [
5381 'model' => $model,
5382 'messages' => $messages,
5383 'stream' => true
5384 ];
5385 break;
5386
5387 case 'claude':
5388 $api_key = $options['claude_api_key'] ?? '';
5389 if (empty($api_key)) return '❌ Missing API key for Claude';
5390 $url = 'https://api.anthropic.com/v1/messages';
5391 $headers = [
5392 'Content-Type: application/json',
5393 'x-api-key: ' . $api_key,
5394 'anthropic-version: 2023-06-01'
5395 ];
5396 $body = [
5397 'model' => $model,
5398 'messages' => $messages,
5399 'max_tokens' => 100,
5400 'stream' => true
5401 ];
5402 break;
5403
5404 case 'grok':
5405 $api_key = $options['xai_api_key'] ?? '';
5406 if (empty($api_key)) return '❌ Missing API key for X.AI';
5407 $url = 'https://api.x.ai/v1/chat/completions';
5408 $headers = [
5409 'Content-Type: application/json',
5410 'Authorization: Bearer ' . $api_key
5411 ];
5412 $body = [
5413 'model' => $model,
5414 'messages' => $messages,
5415 'stream' => true
5416 ];
5417 break;
5418
5419 case 'deepseek':
5420 $api_key = $options['deepseek_api_key'] ?? '';
5421 if (empty($api_key)) return '❌ Missing API key for DeepSeek';
5422 $url = 'https://api.deepseek.com/v1/chat/completions';
5423 $headers = [
5424 'Content-Type: application/json',
5425 'Authorization: Bearer ' . $api_key
5426 ];
5427 $body = [
5428 'model' => $model,
5429 'messages' => $messages,
5430 'stream' => true
5431 ];
5432 break;
5433
5434 case 'gemini':
5435 $api_key = $options['gemini_api_key'] ?? '';
5436 if (empty($api_key)) return '❌ Missing API key for Gemini';
5437 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
5438 $headers = ['Content-Type: application/json'];
5439 $body = [
5440 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
5441 'generationConfig' => ['temperature' => 0.7]
5442 ];
5443 break;
5444
5445 default:
5446 return '❌ Unsupported provider: ' . $provider;
5447 }
5448
5449 // Do the actual streaming test
5450 $ch = curl_init($url);
5451 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
5452 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
5453 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
5454 curl_setopt($ch, CURLOPT_TIMEOUT, 15);
5455 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5456
5457 $response = curl_exec($ch);
5458 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5459 $error = curl_error($ch);
5460 curl_close($ch);
5461
5462 if ($error) return "❌ cURL error: $error";
5463 if ($http_code !== 200) {
5464 $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
5465 return "❌ HTTP $http_code: $error_message";
5466 }
5467
5468 return true;
5469 }
5470
5471
5472 public function mxchat_dismiss_pre_chat_message() {
5473 // Get and sanitize the user identifier
5474 $user_id = $this->mxchat_get_user_identifier();
5475 $user_id = sanitize_key($user_id);
5476
5477 // Set a transient to track that the user has dismissed the pre-chat message
5478 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
5479 set_transient($transient_key, true, DAY_IN_SECONDS);
5480
5481 wp_send_json_success();
5482 }
5483
5484 public function mxchat_check_pre_chat_message_status() {
5485 // Get and sanitize the user identifier
5486 $user_id = $this->mxchat_get_user_identifier();
5487 $user_id = sanitize_key($user_id);
5488
5489 // Check if the transient exists (i.e., if the message was dismissed)
5490 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
5491 $dismissed = get_transient($transient_key);
5492
5493 // Log the result to see if it's being set correctly
5494 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
5495
5496 if ($dismissed) {
5497 wp_send_json_success(['dismissed' => true]);
5498 } else {
5499 wp_send_json_success(['dismissed' => false]);
5500 }
5501
5502 wp_die();
5503 }
5504
5505 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
5506 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
5507 return 0;
5508 }
5509
5510 $dotProduct = array_sum(array_map(function ($a, $b) {
5511 return $a * $b;
5512 }, $vectorA, $vectorB));
5513 $normA = sqrt(array_sum(array_map(function ($a) {
5514 return $a * $a;
5515 }, $vectorA)));
5516 $normB = sqrt(array_sum(array_map(function ($b) {
5517 return $b * $b;
5518 }, $vectorB)));
5519
5520 if ($normA == 0 || $normB == 0) {
5521 return 0;
5522 }
5523
5524 return $dotProduct / ($normA * $normB);
5525 }
5526
5527 public function mxchat_enqueue_scripts_styles() {
5528 // Define version numbers for the styles and scripts
5529 $chat_style_version = '2.3.7';
5530 $chat_script_version = '2.3.7';
5531 // Enqueue the script
5532 wp_enqueue_script(
5533 'mxchat-chat-js',
5534 plugin_dir_url(__FILE__) . '../js/chat-script.js',
5535 array('jquery'),
5536 $chat_script_version,
5537 true
5538 );
5539 // Enqueue the CSS
5540 wp_enqueue_style(
5541 'mxchat-chat-css',
5542 plugin_dir_url(__FILE__) . '../css/chat-style.css',
5543 array(),
5544 $chat_style_version
5545 );
5546 // Fetch options from the database
5547 $this->options = get_option('mxchat_options');
5548 $prompts_options = get_option('mxchat_prompts_options', array());
5549
5550 // Prepare settings for JavaScript
5551 $style_settings = array(
5552 'ajax_url' => admin_url('admin-ajax.php'),
5553 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
5554 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
5555 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
5556 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off', // ADD THIS LINE
5557 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
5558 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
5559 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
5560 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
5561 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
5562 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
5563 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
5564 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
5565 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
5566 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
5567 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
5568 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
5569 'icon_color' => $this->options['icon_color'] ?? '#fff',
5570 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
5571 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
5572 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
5573 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
5574 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
5575 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
5576 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
5577 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
5578 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
5579 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
5580 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
5581 );
5582 // Pass the settings to the script
5583 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
5584 }
5585
5586
5587 /**
5588 * Setup the cron jobs for rate limits with guard against multiple calls
5589 */
5590 public function setup_rate_limit_cron_jobs() {
5591 // Add a guard to prevent multiple rapid calls
5592 $last_setup = get_transient('mxchat_cron_setup_guard');
5593 if ($last_setup && (time() - $last_setup) < 60) {
5594 // Don't run again if we ran less than 60 seconds ago
5595 return;
5596 }
5597
5598 // Set the guard
5599 set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
5600
5601 try {
5602 // First, check if WordPress cron is disabled
5603 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
5604 error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
5605 $this->setup_fallback_rate_limit_system();
5606 return;
5607 }
5608
5609 // Check if cron is already scheduled - if so, don't mess with it
5610 if (wp_next_scheduled('mxchat_reset_rate_limits')) {
5611 error_log('MxChat: Rate limit cron already scheduled, skipping setup');
5612 return;
5613 }
5614
5615 // Clear any orphaned hooks (but don't loop indefinitely)
5616 $hooks_to_clear = [
5617 'mxchat_reset_rate_limits',
5618 'mxchat_reset_hourly_rate_limits',
5619 'mxchat_reset_daily_rate_limits',
5620 'mxchat_reset_weekly_rate_limits',
5621 'mxchat_reset_monthly_rate_limits'
5622 ];
5623
5624 foreach ($hooks_to_clear as $hook) {
5625 // Only clear a maximum of 3 instances to prevent infinite loops
5626 $cleared = 0;
5627 while (wp_next_scheduled($hook) && $cleared < 3) {
5628 wp_clear_scheduled_hook($hook);
5629 $cleared++;
5630 }
5631 }
5632
5633 // Small delay after clearing
5634 usleep(100000); // 0.1 seconds
5635
5636 // Try to schedule the event
5637 $initial_time = time() + 300; // Start in 5 minutes
5638 $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
5639
5640 if ($result === false) {
5641 error_log('MxChat: Failed to schedule cron, using fallback system');
5642 $this->setup_fallback_rate_limit_system();
5643 } else {
5644 error_log('MxChat: Successfully scheduled rate limit reset cron');
5645 }
5646
5647 } catch (Exception $e) {
5648 error_log('MxChat: Cron setup exception: ' . $e->getMessage());
5649 $this->setup_fallback_rate_limit_system();
5650 }
5651 }
5652
5653 /**
5654 * Try alternative cron scheduling methods
5655 */
5656 private function try_alternative_cron_scheduling($initial_time) {
5657 try {
5658 // Method 1: Try with current time instead of future time
5659 $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
5660 if ($result1 !== false) {
5661 error_log('MxChat: Alternative method 1 (current time) succeeded');
5662 return true;
5663 }
5664
5665 // Method 2: Try with a different interval
5666 $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
5667 if ($result2 !== false) {
5668 error_log('MxChat: Alternative method 2 (daily interval) succeeded');
5669 return true;
5670 }
5671
5672 // Method 3: Try wp_schedule_single_event first, then recurring
5673 $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
5674 if ($result3 !== false) {
5675 error_log('MxChat: Alternative method 3 (single event) succeeded');
5676 // Schedule the next one manually in the handler
5677 return true;
5678 }
5679
5680 return false;
5681
5682 } catch (Exception $e) {
5683 error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
5684 return false;
5685 }
5686 }
5687
5688 /**
5689 * Enhanced fallback rate limit system
5690 */
5691 private function setup_fallback_rate_limit_system() {
5692 // Set a flag to use database-based rate limit cleanup
5693 update_option('mxchat_use_fallback_rate_limits', true);
5694
5695 // Schedule a one-time check to happen on the next plugin load
5696 update_option('mxchat_next_rate_limit_check', time() + 3600);
5697
5698 // Also set up a more frequent fallback check (every 4 hours)
5699 update_option('mxchat_fallback_check_interval', 4 * 3600);
5700
5701 error_log('MxChat: Fallback rate limit system activated');
5702 }
5703
5704 /**
5705 * Enhanced fallback check method
5706 */
5707 public function check_fallback_rate_limits() {
5708 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5709
5710 if (!$use_fallback) {
5711 return; // Regular cron is working
5712 }
5713
5714 $next_check = get_option('mxchat_next_rate_limit_check', 0);
5715 $check_interval = get_option('mxchat_fallback_check_interval', 3600);
5716
5717 if (time() >= $next_check) {
5718 error_log('MxChat: Running fallback rate limit cleanup');
5719 $this->mxchat_reset_rate_limits();
5720
5721 // Schedule next check
5722 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
5723 }
5724 }
5725 /**
5726 * Enhanced rate limit check that includes fallback cleanup
5727 */
5728 public function check_rate_limit() {
5729 // Check if we need to run fallback cleanup
5730 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
5731 $next_check = get_option('mxchat_next_rate_limit_check', 0);
5732
5733 if ($use_fallback && time() >= $next_check) {
5734 $this->mxchat_reset_rate_limits();
5735 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
5736 }
5737
5738 // Continue with your existing rate limit logic...
5739 $all_options = get_option('mxchat_options', []);
5740
5741 // Determine user role or if logged out
5742 if (is_user_logged_in()) {
5743 $user = wp_get_current_user();
5744 $user_id = $user->ID;
5745
5746 // Get the user's primary role using reset() to safely get the first element
5747 $user_roles = $user->roles;
5748
5749 // Safely get the first role regardless of array key structure
5750 if (!empty($user_roles) && is_array($user_roles)) {
5751 $role = reset($user_roles); // This safely gets the first element regardless of key
5752 } else {
5753 $role = 'subscriber'; // Default to subscriber if no role found
5754 }
5755 } else {
5756 $role = 'logged_out';
5757 // Use IP address for non-logged-in users
5758 $user_id = $this->get_client_ip();
5759 }
5760
5761 // Check if rate limits are configured for this role
5762 if (!isset($all_options['rate_limits'][$role])) {
5763 return true; // No limit set for this role
5764 }
5765
5766 $limit = $all_options['rate_limits'][$role]['limit'];
5767
5768 // If unlimited, return true immediately
5769 if ($limit === 'unlimited') {
5770 return true;
5771 }
5772
5773 // Get the option name for this user/role with safer naming
5774 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
5775 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
5776 $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
5777
5778 // Get the counter data
5779 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
5780
5781 // If first request or counter reset needed, set the initial timestamp
5782 if ($limit_data['count'] === 0) {
5783 $limit_data['timestamp'] = time();
5784 update_option($option_name, $limit_data);
5785 }
5786
5787 // Get the timeframe
5788 $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
5789 $all_options['rate_limits'][$role]['timeframe'] : 'daily';
5790
5791 // Check if the counter needs to be reset based on timeframe
5792 $current_time = time();
5793 $timestamp = $limit_data['timestamp'];
5794 $should_reset = false;
5795
5796 switch ($timeframe) {
5797 case 'hourly':
5798 $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
5799 break;
5800 case 'daily':
5801 $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
5802 break;
5803 case 'weekly':
5804 $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
5805 break;
5806 case 'monthly':
5807 $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
5808 break;
5809 }
5810
5811 // Reset the counter if the timeframe has passed
5812 if ($should_reset) {
5813 $limit_data = ['count' => 0, 'timestamp' => $current_time];
5814 update_option($option_name, $limit_data);
5815 }
5816
5817 // Check if user has exceeded their limit
5818 if ($limit_data['count'] >= intval($limit)) {
5819 // Get the custom message for this role
5820 $message = !empty($all_options['rate_limits'][$role]['message'])
5821 ? $all_options['rate_limits'][$role]['message']
5822 : __('Rate limit exceeded. Please try again later.', 'mxchat');
5823
5824 // Add timeframe information to the message if placeholders exist
5825 $timeframe_label = '';
5826 switch ($timeframe) {
5827 case 'hourly':
5828 $timeframe_label = __('hour', 'mxchat');
5829 break;
5830 case 'daily':
5831 $timeframe_label = __('day', 'mxchat');
5832 break;
5833 case 'weekly':
5834 $timeframe_label = __('week', 'mxchat');
5835 break;
5836 case 'monthly':
5837 $timeframe_label = __('month', 'mxchat');
5838 break;
5839 }
5840
5841 // Replace placeholders in the message
5842 $message = str_replace(
5843 ['{limit}', '{count}', '{remaining}', '{timeframe}'],
5844 [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
5845 $message
5846 );
5847
5848 // Process HTML links in the message
5849 $message = $this->process_rate_limit_message_html($message);
5850
5851 // Return error with the processed message
5852 return [
5853 'error' => true,
5854 'message' => $message
5855 ];
5856 }
5857
5858 // Increment the counter
5859 $limit_data['count']++;
5860 update_option($option_name, $limit_data);
5861
5862 return true;
5863 }
5864
5865 /**
5866 * Enhanced rate limit reset with better error handling
5867 */
5868 public function mxchat_reset_rate_limits() {
5869 try {
5870 global $wpdb;
5871 $all_options = get_option('mxchat_options', []);
5872 $current_time = time();
5873
5874 // Get rate limit options with a safer query and limit
5875 $option_names = $wpdb->get_col(
5876 $wpdb->prepare(
5877 "SELECT option_name FROM {$wpdb->options}
5878 WHERE option_name LIKE %s
5879 LIMIT 1000",
5880 'mxchat_chat_limit_%'
5881 )
5882 );
5883
5884 if (empty($option_names)) {
5885 return;
5886 }
5887
5888 $processed_count = 0;
5889 $max_processing_time = 30; // Maximum 30 seconds
5890 $start_time = time();
5891
5892 foreach ($option_names as $option_name) {
5893 // Check processing time limit
5894 if ((time() - $start_time) > $max_processing_time) {
5895 error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
5896 break;
5897 }
5898
5899 // Parse the option name more safely
5900 if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
5901 continue;
5902 }
5903
5904 $role_and_user = $matches[1] . '_' . $matches[2];
5905 $parts = explode('_', $role_and_user);
5906
5907 if (count($parts) < 2) {
5908 continue;
5909 }
5910
5911 // Extract role (everything except the last part which is user ID)
5912 $user_id_part = array_pop($parts);
5913 $role = implode('_', $parts);
5914
5915 // Skip if role doesn't exist in our settings
5916 if (!isset($all_options['rate_limits'][$role])) {
5917 // Clean up orphaned entries
5918 delete_option($option_name);
5919 continue;
5920 }
5921
5922 $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
5923 $limit_data = get_option($option_name);
5924
5925 if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
5926 // Clean up invalid entries
5927 delete_option($option_name);
5928 continue;
5929 }
5930
5931 $timestamp = $limit_data['timestamp'];
5932 $should_reset = false;
5933
5934 // Determine if we should reset based on the timeframe
5935 switch ($timeframe) {
5936 case 'hourly':
5937 $should_reset = ($current_time - $timestamp) >= 3600;
5938 break;
5939 case 'daily':
5940 $should_reset = ($current_time - $timestamp) >= 86400;
5941 break;
5942 case 'weekly':
5943 $should_reset = ($current_time - $timestamp) >= 604800;
5944 break;
5945 case 'monthly':
5946 $should_reset = ($current_time - $timestamp) >= 2592000;
5947 break;
5948 }
5949
5950 // Reset the counter if the timeframe has passed
5951 if ($should_reset) {
5952 delete_option($option_name);
5953 wp_cache_delete($option_name, 'options');
5954 $processed_count++;
5955 }
5956 }
5957
5958 // Clean up any orphaned cache entries
5959 wp_cache_delete('mxchat_all_chat_limits', 'options');
5960
5961 error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
5962
5963 } catch (Exception $e) {
5964 error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
5965 }
5966 }
5967
5968
5969 /**
5970 * Process HTML links in rate limit messages
5971 *
5972 * @param string $message The rate limit message
5973 * @return string The processed message with safe HTML links
5974 */
5975 private function process_rate_limit_message_html($message) {
5976 // Return original message if empty
5977 if (empty($message)) {
5978 return $message;
5979 }
5980
5981 // First, convert markdown links to HTML
5982 $message = $this->convert_markdown_links($message);
5983
5984 // Then, auto-convert any remaining plain URLs to links
5985 $message = $this->auto_link_urls($message);
5986
5987 // Allow basic HTML tags for links and formatting
5988 $allowed_tags = [
5989 'a' => [
5990 'href' => true,
5991 'target' => true,
5992 'rel' => true,
5993 'title' => true,
5994 'class' => true
5995 ],
5996 'strong' => [],
5997 'em' => [],
5998 'br' => [],
5999 'b' => [],
6000 'i' => [],
6001 'span' => ['class' => true]
6002 ];
6003
6004 // Sanitize but allow the specified HTML tags
6005 $processed_message = wp_kses($message, $allowed_tags);
6006
6007 // If wp_kses stripped everything, return the original message as plain text
6008 if (empty($processed_message) && !empty($message)) {
6009 // Strip all HTML and return plain text as fallback
6010 return wp_strip_all_tags($message);
6011 }
6012
6013 return $processed_message;
6014 }
6015
6016 /**
6017 * Convert markdown links to HTML
6018 *
6019 * @param string $text The text to process
6020 * @return string The text with markdown links converted to HTML
6021 */
6022 private function convert_markdown_links($text) {
6023 // Return original text if empty
6024 if (empty($text)) {
6025 return $text;
6026 }
6027
6028 // Pattern to match markdown links: [text](url)
6029 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
6030
6031 $processed_text = preg_replace_callback($pattern, function($matches) {
6032 $link_text = $matches[1];
6033 $url = $matches[2];
6034
6035 // Clean up any trailing punctuation from the URL
6036 $url = rtrim($url, '.,;:!?');
6037
6038 // Sanitize the link text and URL
6039 $safe_text = esc_html($link_text);
6040 $safe_url = esc_url($url);
6041
6042 // Create the HTML link
6043 return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
6044 }, $text);
6045
6046 // If preg_replace_callback failed, return original text
6047 if ($processed_text === null) {
6048 return $text;
6049 }
6050
6051 return $processed_text;
6052 }
6053
6054 /**
6055 * Auto-convert plain URLs to clickable links
6056 *
6057 * @param string $text The text to process
6058 * @return string The text with URLs converted to links
6059 */
6060 private function auto_link_urls($text) {
6061 // Return original text if empty
6062 if (empty($text)) {
6063 return $text;
6064 }
6065
6066 // Simple pattern that avoids complex lookbehinds
6067 // This will match URLs that are not already inside href attributes or markdown links
6068 $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
6069
6070 $processed_text = preg_replace_callback($pattern, function($matches) {
6071 $url = $matches[0];
6072 // Clean up any trailing punctuation that might have been captured
6073 $url = rtrim($url, '.,;:!?');
6074
6075 // Add target="_blank" and rel="noopener noreferrer" for security
6076 return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
6077 }, $text);
6078
6079 // If preg_replace_callback failed, return original text
6080 if ($processed_text === null) {
6081 return $text;
6082 }
6083
6084 return $processed_text;
6085 }
6086
6087
6088 // Helper function to get client IP address
6089 private function get_client_ip() {
6090 // Check for shared internet/ISP IP
6091 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
6092 return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
6093 }
6094
6095 // Check for IPs passing through proxies
6096 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
6097 // Use the first value in the comma-separated list
6098 $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
6099 return trim($forwarded_for[0]);
6100 }
6101
6102 if (!empty($_SERVER['REMOTE_ADDR'])) {
6103 return sanitize_text_field($_SERVER['REMOTE_ADDR']);
6104 }
6105
6106 // Fallback
6107 return 'unknown';
6108 }
6109
6110 /**
6111 * AJAX handler to get system information for testing panel
6112 */
6113 public function mxchat_get_system_info() {
6114 // Verify nonce for security
6115 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6116 wp_send_json_error(['message' => 'Invalid nonce']);
6117 return;
6118 }
6119
6120 // Only allow admin users
6121 if (!current_user_can('administrator')) {
6122 wp_send_json_error(['message' => 'Unauthorized']);
6123 return;
6124 }
6125
6126 // Get system prompt from options
6127 $system_prompt = isset($this->options['system_prompt_instructions'])
6128 ? $this->options['system_prompt_instructions']
6129 : 'No system prompt configured';
6130
6131 // Get selected model
6132 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
6133
6134 // Get API key status (just check if they exist, don't expose the keys)
6135 $api_status = [];
6136 $api_status['openai'] = !empty($this->options['api_key']);
6137 $api_status['claude'] = !empty($this->options['claude_api_key']);
6138 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
6139 $api_status['xai'] = !empty($this->options['xai_api_key']);
6140 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
6141
6142 wp_send_json_success([
6143 'system_prompt' => $system_prompt,
6144 'selected_model' => $selected_model,
6145 'api_status' => $api_status
6146 ]);
6147 }
6148
6149 /**
6150 * AJAX handler to get similarity threshold
6151 */
6152 public function mxchat_get_similarity_threshold() {
6153 // Verify nonce for security
6154 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6155 wp_send_json_error(['message' => 'Invalid nonce']);
6156 return;
6157 }
6158
6159 // Only allow admin users
6160 if (!current_user_can('administrator')) {
6161 wp_send_json_error(['message' => 'Unauthorized']);
6162 return;
6163 }
6164
6165 // Get similarity threshold from main options (default 75%)
6166 $similarity_threshold = isset($this->options['similarity_threshold'])
6167 ? ((int) $this->options['similarity_threshold']) / 100
6168 : 0.75;
6169
6170 wp_send_json_success([
6171 'threshold' => $similarity_threshold,
6172 'threshold_percentage' => ($similarity_threshold * 100) . '%'
6173 ]);
6174 }
6175
6176 /**
6177 * AJAX handler to get knowledge base status
6178 */
6179 public function mxchat_get_kb_status() {
6180 // Verify nonce for security
6181 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6182 wp_send_json_error(['message' => 'Invalid nonce']);
6183 return;
6184 }
6185
6186 // Only allow admin users
6187 if (!current_user_can('administrator')) {
6188 wp_send_json_error(['message' => 'Unauthorized']);
6189 return;
6190 }
6191
6192 // Check Pinecone vs WordPress
6193 $addon_options = get_option('mxchat_pinecone_addon_options', array());
6194 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
6195
6196 $kb_info = [
6197 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
6198 'status' => 'Active'
6199 ];
6200
6201 // Get document count
6202 if ($use_pinecone) {
6203 $kb_info['documents'] = 'Connected to Pinecone';
6204 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
6205 } else {
6206 // Count documents in WordPress database
6207 global $wpdb;
6208 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6209 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
6210 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
6211 }
6212
6213 wp_send_json_success($kb_info);
6214 }
6215
6216 /**
6217 * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
6218 */
6219 public function mxchat_start_fresh_session() {
6220 // Verify nonce for security
6221 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6222 wp_send_json_error(['message' => 'Invalid nonce']);
6223 return;
6224 }
6225
6226 // Only allow admin users
6227 if (!current_user_can('administrator')) {
6228 wp_send_json_error(['message' => 'Unauthorized']);
6229 return;
6230 }
6231
6232 $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
6233 $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
6234
6235 if (empty($old_session_id)) {
6236 wp_send_json_error(['message' => 'Old session ID required']);
6237 return;
6238 }
6239
6240 // If no new session ID provided, generate one
6241 if (empty($new_session_id)) {
6242 $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
6243 }
6244
6245 // Clear ALL data associated with the old session
6246 $this->clear_complete_session_data($old_session_id);
6247
6248 // Initialize the new session
6249 $this->initialize_fresh_session($new_session_id);
6250
6251 wp_send_json_success([
6252 'message' => 'Fresh session started successfully',
6253 'new_session_id' => $new_session_id,
6254 'old_session_id' => $old_session_id
6255 ]);
6256 }
6257
6258 /**
6259 * Clear ALL data associated with a session (ENHANCED)
6260 */
6261 private function clear_complete_session_data($session_id) {
6262 // Clear chat history
6263 delete_option("mxchat_history_{$session_id}");
6264
6265 // Clear chat mode
6266 delete_option("mxchat_mode_{$session_id}");
6267
6268 // Clear any PDF/Word transients
6269 $this->clear_pdf_transients($session_id);
6270 if (method_exists($this, 'clear_word_transients')) {
6271 $this->clear_word_transients($session_id);
6272 }
6273
6274 // Clear agent-related data
6275 delete_option("mxchat_channel_{$session_id}");
6276 delete_option("mxchat_agent_name_{$session_id}");
6277 delete_option("mxchat_email_{$session_id}");
6278
6279 // Clear any recommendation flow state
6280 delete_option("mxchat_sr_flow_state_{$session_id}");
6281
6282 // Clear any cached embeddings or context
6283 delete_transient("mxchat_context_{$session_id}");
6284 delete_transient("mxchat_last_query_{$session_id}");
6285
6286 // Clear any testing data
6287 delete_transient("mxchat_testing_data_{$session_id}");
6288
6289 // Clear any rate limiting data for this session
6290 delete_transient("mxchat_rate_limit_{$session_id}");
6291
6292 // Clear any other session-specific transients
6293 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
6294 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
6295 delete_transient("mxchat_include_word_in_context_{$session_id}");
6296
6297 //error_log("MxChat: Cleared all data for session: {$session_id}");
6298 }
6299
6300 /**
6301 * Initialize a fresh session with default data
6302 */
6303 private function initialize_fresh_session($session_id) {
6304 // Set default chat mode
6305 update_option("mxchat_mode_{$session_id}", 'ai');
6306
6307 //error_log("MxChat: Initialized fresh session: {$session_id}");
6308 }
6309
6310 /**
6311 * Helper method to clear Word document transients (if you have Word support)
6312 */
6313 private function clear_word_transients($session_id) {
6314 delete_transient('mxchat_word_url_' . $session_id);
6315 delete_transient('mxchat_word_filename_' . $session_id);
6316 delete_transient('mxchat_word_embeddings_' . $session_id);
6317 delete_transient('mxchat_include_word_in_context_' . $session_id);
6318 }
6319
6320 /**
6321 * Simplified testing data capture method (CLEANED UP)
6322 */
6323 private function capture_testing_data($user_embedding, $message, $session_id) {
6324 // Only capture for admin users
6325 if (!current_user_can('administrator')) {
6326 return null;
6327 }
6328
6329 $testing_data = [
6330 'query' => $message,
6331 'timestamp' => time(),
6332 'top_matches' => [],
6333 'action_matches' => [] // NEW: Add action matches
6334 ];
6335
6336 // Get similarity threshold
6337 $similarity_threshold = isset($this->options['similarity_threshold'])
6338 ? ((int) $this->options['similarity_threshold']) / 100
6339 : 0.75;
6340
6341 $testing_data['similarity_threshold'] = $similarity_threshold;
6342
6343 // Use the real similarity analysis if available
6344 if ($this->last_similarity_analysis !== null) {
6345 $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
6346 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
6347 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
6348 } else {
6349 // Fallback: determine knowledge base type
6350 $addon_options = get_option('mxchat_pinecone_addon_options', array());
6351 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
6352
6353 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
6354 }
6355
6356 // NEW: Include action analysis if available
6357 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
6358 $testing_data['action_matches'] = $this->last_action_analysis;
6359
6360 // Clear it after capturing to avoid stale data
6361 $this->last_action_analysis = null;
6362 }
6363
6364 return $testing_data;
6365 }
6366
6367
6368 /**
6369 * NEW: Track URL clicks from chatbot responses
6370 */
6371 public function mxchat_track_url_click() {
6372 // Verify nonce for security
6373 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
6374 wp_send_json_error(['message' => 'Invalid nonce']);
6375 wp_die();
6376 }
6377
6378 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
6379 $clicked_url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
6380 $message_context = isset($_POST['message_context']) ? sanitize_textarea_field($_POST['message_context']) : '';
6381
6382 if (empty($session_id) || empty($clicked_url)) {
6383 wp_send_json_error(['message' => 'Missing required data']);
6384 wp_die();
6385 }
6386
6387 global $wpdb;
6388 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
6389
6390 // Insert click tracking record
6391 $wpdb->insert(
6392 $table_name,
6393 [
6394 'session_id' => $session_id,
6395 'clicked_url' => $clicked_url,
6396 'message_context' => $message_context,
6397 'click_timestamp' => current_time('mysql', 1),
6398 'user_ip' => $_SERVER['REMOTE_ADDR'],
6399 'user_agent' => $_SERVER['HTTP_USER_AGENT']
6400 ]
6401 );
6402
6403 wp_send_json_success(['message' => 'Click tracked']);
6404 wp_die();
6405 }
6406
6407 /**
6408 * NEW: Get URL click analytics for a session
6409 */
6410 public function mxchat_get_url_clicks($session_id) {
6411 global $wpdb;
6412 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
6413
6414 $clicks = $wpdb->get_results($wpdb->prepare(
6415 "SELECT * FROM $table_name WHERE session_id = %s ORDER BY click_timestamp ASC",
6416 $session_id
6417 ));
6418
6419 return $clicks;
6420 }
6421 /**
6422 * NEW: Track the originating page where chat was started
6423 */
6424 public function mxchat_track_originating_page() {
6425 // Verify nonce
6426 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
6427 wp_send_json_error(['message' => 'Invalid nonce']);
6428 wp_die();
6429 }
6430
6431 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
6432 $page_url = isset($_POST['page_url']) ? esc_url_raw($_POST['page_url']) : '';
6433 $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
6434
6435 if (empty($session_id)) {
6436 wp_send_json_error(['message' => 'Missing session ID']);
6437 wp_die();
6438 }
6439
6440 global $wpdb;
6441 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
6442
6443 // Check if we've already tracked for this session
6444 $existing = $wpdb->get_var($wpdb->prepare(
6445 "SELECT COUNT(*) FROM $table_name
6446 WHERE session_id = %s
6447 AND originating_page_url IS NOT NULL",
6448 $session_id
6449 ));
6450
6451 if ($existing > 0) {
6452 wp_send_json_success(['message' => 'Already tracked']);
6453 wp_die();
6454 }
6455
6456 // Update the first message in this session with originating page info
6457 $wpdb->query($wpdb->prepare(
6458 "UPDATE $table_name
6459 SET originating_page_url = %s,
6460 originating_page_title = %s
6461 WHERE session_id = %s
6462 ORDER BY timestamp ASC
6463 LIMIT 1",
6464 $page_url,
6465 $page_title,
6466 $session_id
6467 ));
6468
6469 wp_send_json_success(['message' => 'Originating page tracked']);
6470 wp_die();
6471 }
6472
6473
6474
6475
6476 }
6477 ?>
6478