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

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