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

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