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

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

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