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

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

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