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

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

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