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

7,270 lines 283.5 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 private $last_action_analysis = array();
15
16 /**
17 * Class constructor
18 */
19 public function __construct() {
20 $this->options = get_option('mxchat_options');
21 $this->prompts_options = get_option('mxchat_prompts_options', array());
22 $this->chat_count = get_option('mxchat_chat_count', 0);
23 $this->word_handler = new MXChat_Word_Handler($this->options);
24
25 // Add all action hooks
26 add_action('wp_enqueue_scripts', array($this, 'mxchat_enqueue_scripts_styles'));
27 add_action('wp_ajax_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
28 add_action('wp_ajax_nopriv_mxchat_handle_chat_request', array($this, 'mxchat_handle_chat_request'));
29 add_action('wp_ajax_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
30 add_action('wp_ajax_nopriv_mxchat_dismiss_pre_chat_message', array($this, 'mxchat_dismiss_pre_chat_message'));
31
32 // Add the AJAX actions for checking if the pre-chat message was dismissed
33 add_action('wp_ajax_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
34 add_action('wp_ajax_nopriv_mxchat_check_pre_chat_message_status', array($this, 'mxchat_check_pre_chat_message_status'));
35 add_action('wp_ajax_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
36 add_action('wp_ajax_nopriv_mxchat_fetch_conversation_history', [$this, 'mxchat_fetch_conversation_history']);
37 add_action('wp_ajax_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
38 add_action('wp_ajax_nopriv_mxchat_add_to_cart', [$this, 'mxchat_add_to_cart']);
39
40 // Add REST API routes registration
41 add_action('rest_api_init', array($this, 'register_routes'));
42 add_action('wp_ajax_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
43 add_action('wp_ajax_nopriv_mxchat_fetch_new_messages', array($this, 'mxchat_fetch_new_messages'));
44
45 // Rate limit action - notice we removed the old schedule setup
46 add_action('mxchat_reset_rate_limits', array($this, 'mxchat_reset_rate_limits'));
47
48 // File upload and handling actions
49 add_action('wp_ajax_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
50 add_action('wp_ajax_nopriv_mxchat_upload_pdf', [$this, 'handle_pdf_upload']);
51 add_action('wp_ajax_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
52 add_action('wp_ajax_nopriv_mxchat_remove_pdf', [$this, 'handle_pdf_remove']);
53
54 // Word document handling actions
55 add_action('wp_ajax_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
56 add_action('wp_ajax_nopriv_mxchat_upload_word', array($this, 'mxchat_handle_word_upload'));
57 add_action('wp_ajax_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
58 add_action('wp_ajax_nopriv_mxchat_remove_word', array($this, 'mxchat_handle_word_remove'));
59 add_action('wp_ajax_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
60 add_action('wp_ajax_nopriv_mxchat_check_word_status', array($this, 'mxchat_check_word_status'));
61
62 // Email handling actions
63 add_action('wp_ajax_nopriv_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
64 add_action('wp_ajax_mxchat_handle_save_email_and_response', [$this, 'mxchat_handle_save_email_and_response']);
65 add_action('wp_ajax_nopriv_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
66 add_action('wp_ajax_mxchat_check_email_provided', [$this, 'mxchat_check_email_provided']);
67
68 add_action('wp_ajax_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
69 add_action('wp_ajax_nopriv_mxchat_stream_chat', array($this, 'mxchat_handle_chat_request'));
70
71 // Testing panel AJAX actions
72 add_action('wp_ajax_mxchat_get_system_info', array($this, 'mxchat_get_system_info'));
73 add_action('wp_ajax_mxchat_get_similarity_threshold', array($this, 'mxchat_get_similarity_threshold'));
74 add_action('wp_ajax_mxchat_get_kb_status', array($this, 'mxchat_get_kb_status'));
75 add_action('wp_ajax_mxchat_start_fresh_session', array($this, 'mxchat_start_fresh_session'));
76 // Add to your existing constructor, in the section with other AJAX actions:
77 add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
78 add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
79 add_action('wp_ajax_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
80 add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
81 // Add chat mode checking actions
82 add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
83 add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
84
85 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
86
87
88 }
89
90 // In your core plugin's check_actions_for_addons method:
91 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
92 //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
93
94 $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
95
96 //error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
97
98 return $result;
99 }
100
101 private function mxchat_increment_chat_count() {
102 $chat_count = get_option('mxchat_chat_count', 0);
103 $chat_count++;
104 update_option('mxchat_chat_count', $chat_count);
105 }
106
107 function mxchat_fetch_conversation_history() {
108 if (empty($_POST['session_id'])) {
109 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
110 wp_die();
111 }
112
113 $session_id = sanitize_text_field($_POST['session_id']);
114 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
115 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
116
117 if (empty($history)) {
118 // Even if history is empty, return the chat mode
119 wp_send_json_success([
120 'conversation' => [],
121 'chat_mode' => $chat_mode
122 ]);
123 wp_die();
124 }
125
126 wp_send_json_success([
127 'conversation' => $history,
128 'chat_mode' => $chat_mode
129 ]);
130 wp_die();
131 }
132
133 private function mxchat_fetch_conversation_history_for_ai($session_id) {
134 $history = get_option("mxchat_history_{$session_id}", []);
135 $formatted_history = [];
136
137 // Adjusted for code-heavy conversations
138 $max_tokens = 120000; // Context window size
139 $reserved_tokens = 5000; // Space for system prompts + current query
140 $current_token_count = 0;
141
142 // Allowed HTML tags for content sanitization
143 $allowed_tags = [
144 'pre' => ['class' => true],
145 'code' => ['class' => true],
146 'span' => ['class' => true],
147 'div' => ['class' => true],
148 'strong' => [],
149 'em' => []
150 ];
151
152 foreach (array_reverse($history) as $entry) {
153 // Preserve code blocks while sanitizing other HTML
154 $clean_content = wp_kses($entry['content'], $allowed_tags);
155
156 // Detect code blocks in content
157 $has_code = false;
158 // Replace the HTML check with:
159 // Allow messages that contain code blocks or are plain text
160 if (strpos($clean_content, '<pre') === false &&
161 strpos($clean_content, '<code') === false &&
162 $clean_content !== strip_tags($entry['content'])) {
163 continue;
164 }
165
166 // Skip entries that lost significant content during sanitization
167 if (!$has_code && $clean_content !== strip_tags($entry['content'])) {
168 continue;
169 }
170
171 // More accurate token estimation (1 token ≈ 4 characters)
172 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
173
174 // Check token budget with the new estimate
175 if (($current_token_count + $token_estimate + $reserved_tokens) > $max_tokens) {
176 // Try to fit partial content if it's the first entry
177 if (empty($formatted_history)) {
178 $clean_content = mb_substr($clean_content, 0, ($max_tokens - $reserved_tokens) * 4);
179 $token_estimate = ceil(mb_strlen($clean_content, 'UTF-8') / 4);
180 } else {
181 break;
182 }
183 }
184
185 // Add to formatted history
186 $formatted_history[] = [
187 'role' => $entry['role'],
188 'content' => $clean_content
189 ];
190
191 $current_token_count += $token_estimate;
192 }
193
194 // Reverse back to maintain chronological order
195 $formatted_history = array_reverse($formatted_history);
196
197 // Add system message about code context
198 array_unshift($formatted_history, [
199 'role' => 'system',
200 'content' => 'Preserved code blocks are marked with [CODE BLOCK PRESERVED]. '
201 . 'Maintain formatting and syntax highlighting when referencing code.'
202 ]);
203
204 return $formatted_history;
205 }
206
207 public function register_routes() {
208 //error_log(esc_html__('Registering MxChat REST routes', 'mxchat'));
209
210 register_rest_route('mxchat/v1', '/stream', [
211 'methods' => 'GET',
212 'callback' => [$this, 'mxchat_stream_events'],
213 'permission_callback' => [$this, 'verify_chat_session'],
214 ]);
215
216 register_rest_route('mxchat/v1', '/agent-response', [
217 'methods' => 'POST',
218 'callback' => [$this, 'mxchat_handle_agent_response'],
219 'permission_callback' => [$this, 'verify_slack_request'],
220 ]);
221
222 register_rest_route('mxchat/v1', '/slack-interaction', [
223 'methods' => 'POST',
224 'callback' => [$this, 'handle_slack_interaction'],
225 'permission_callback' => [$this, 'verify_slack_request'],
226 ]);
227
228 register_rest_route('mxchat/v1', '/slack-messages', [
229 'methods' => 'POST',
230 'callback' => [$this, 'handle_slack_messages'],
231 'permission_callback' => [$this, 'verify_slack_request'],
232 ]);
233
234 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
235 }
236
237 /**
238 * Verify valid chat session
239 */
240 public function verify_chat_session($request) {
241 $session_id = $request->get_param('session_id');
242 if (empty($session_id)) {
243 //error_log(esc_html__('Empty session ID in chat request', 'mxchat'));
244 return false;
245 }
246
247 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
248 return $chat_mode === 'agent';
249 }
250
251 /**
252 * Verify request is coming from Slack.
253 *
254 * @param WP_REST_Request $request
255 * @return bool True if valid, false otherwise.
256 */
257 public function verify_slack_request($request) {
258 // Get the Slack signing secret from your plugin options
259 $valid_key = $this->options['live_agent_secret_key'] ?? '';
260
261 if (empty($valid_key)) {
262 //error_log(esc_html__('Slack signing secret not configured', 'mxchat'));
263 return false;
264 }
265
266 $timestamp = $request->get_header('X-Slack-Request-Timestamp');
267 $slack_signature = $request->get_header('X-Slack-Signature');
268
269 // Verify timestamp to prevent replay attacks
270 if (abs(time() - intval($timestamp)) > 300) {
271 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
272 return false;
273 }
274
275 // Get raw request body
276 $request_body = file_get_contents('php://input');
277
278 // Create the signature base string
279 $sig_basestring = "v0:{$timestamp}:{$request_body}";
280
281 // Calculate expected signature
282 $my_signature = 'v0=' . hash_hmac('sha256', $sig_basestring, $valid_key);
283
284 // Compare signatures
285 return hash_equals($my_signature, $slack_signature);
286 }
287 public function mxchat_stream_events(WP_REST_Request $request) {
288 header('Content-Type: text/event-stream');
289 header('Cache-Control: no-cache');
290 header('Connection: keep-alive');
291
292 $session_id = sanitize_text_field($request->get_param('session_id'));
293 $last_seen_id = sanitize_text_field($request->get_param('last_seen_id')) ?: '';
294
295 if (empty($session_id)) {
296 echo esc_html__("event: error\ndata: ", 'mxchat') . esc_html__('Missing session_id', 'mxchat') . "\n\n";
297 flush();
298 exit;
299 }
300
301 $history = get_option("mxchat_history_{$session_id}", []);
302
303 // Filter only new messages
304 $new_messages = array_filter($history, function ($message) use ($last_seen_id) {
305 return !empty($message['id']) && $message['id'] > $last_seen_id;
306 });
307
308 // Send new messages if available
309 if (!empty($new_messages)) {
310 echo esc_html__("event: newMessages\ndata: ", 'mxchat') . json_encode(array_values($new_messages)) . "\n\n";
311 } else {
312 // Keep the connection alive
313 echo esc_html__("event: keepAlive\ndata: ", 'mxchat') . "{}\n\n";
314 }
315 flush();
316 exit;
317 }
318
319
320
321
322 private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
323 global $wpdb;
324 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
325 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
326
327 // Check if this is the first message in a new session (before any other database operations)
328 $is_new_session = false;
329 if ($role === 'user') { // Only check for user messages, not bot responses
330 $existing_messages = $wpdb->get_var($wpdb->prepare(
331 "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
332 $session_id
333 ));
334 $is_new_session = ($existing_messages == 0);
335
336 // Log for debugging
337 if ($is_new_session) {
338 //error_log("[DEBUG] This is a NEW session - first message");
339 }
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
354 // 2) Generate unique message_id
355 $message_id = uniqid();
356 //error_log("[DEBUG] mxchat_save_chat_message -> Generated message_id: {$message_id}");
357
358 // 3) Determine user_id
359 $user_id = is_user_logged_in() ? get_current_user_id() : 0;
360
361 // 4) Determine user_identifier
362 $user_identifier = $agent_name
363 ? $agent_name
364 : MxChat_User::mxchat_get_user_identifier();
365
366 // 5) Determine displayed_name
367 $user_email = MxChat_User::mxchat_get_user_email();
368 $displayed_name = $agent_name ? $agent_name : ($user_email ?: $user_identifier);
369
370 // 6) Check for a saved email in wp_options
371 $email_option_key = "mxchat_email_{$session_id}";
372 $saved_email = get_option($email_option_key);
373 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
374
375 // Check for a saved name in wp_options
376 $name_option_key = "mxchat_name_{$session_id}";
377 $saved_name = get_option($name_option_key);
378 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
379
380 // If found, update DB user_email and user_name
381 if ($saved_email || $saved_name) {
382 $update_data = [];
383 if ($saved_email) {
384 $update_data['user_email'] = $saved_email;
385 }
386 if ($saved_name) {
387 $update_data['user_name'] = $saved_name;
388 }
389
390 if (!empty($update_data)) {
391 $update_res = $wpdb->update(
392 $table_name,
393 $update_data,
394 ['session_id' => $session_id],
395 array_fill(0, count($update_data), '%s'),
396 ['%s']
397 );
398 //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email/user_name update for session_id {$session_id}. update_res: {$update_res}");
399 }
400 }
401
402 // 7) Save to session history in wp_options
403 $history_key = "mxchat_history_{$session_id}";
404 $history = get_option($history_key, []);
405 $history[] = [
406 'id' => $message_id,
407 'role' => $role,
408 'content' => $message,
409 'timestamp' => round(microtime(true) * 1000),
410 'agent_name' => $displayed_name,
411 ];
412 update_option($history_key, $history, 'no');
413 //error_log("[DEBUG] mxchat_save_chat_message -> Updated session history in option: {$history_key}");
414
415 // 8) Save the message to DB (INSERT)
416 $insert_data = [
417 'user_id' => $user_id,
418 'user_identifier'=> $user_identifier,
419 'user_email' => $saved_email ?: $user_email,
420 'user_name' => $saved_name ?: '', // Add name to insert data
421 'session_id' => $session_id,
422 'role' => $role,
423 'message' => $message,
424 'timestamp' => current_time('mysql', 1),
425 ];
426
427 // IMPROVED: Handle originating page data
428 $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
429
430 if ($columns_exist) {
431 if ($is_new_session && $role === 'user') {
432 // For the first user message, set originating page data
433
434 // First check if we have it from the parameter
435 if ($originating_page && !empty($originating_page['url'])) {
436 $insert_data['originating_page_url'] = $originating_page['url'];
437 $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
438
439 //error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
440 }
441 // Otherwise check if it's stored in the instance property
442 else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
443 $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
444 $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
445
446 //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
447
448 // Clear after using
449 unset($this->pending_originating_page);
450 }
451 // Fallback to HTTP_REFERER if nothing else is available
452 else if (isset($_SERVER['HTTP_REFERER'])) {
453 $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
454 $insert_data['originating_page_url'] = $referer_url;
455
456 // Generate title from URL
457 $parsed_url = parse_url($referer_url);
458 $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
459
460 if (empty($path) || $path === 'index.php' || $path === 'index.html') {
461 $insert_data['originating_page_title'] = 'Homepage';
462 } else {
463 $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
464 $insert_data['originating_page_title'] = ucwords(trim($title));
465 }
466
467 //error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
468 }
469
470 // Store for this session so all messages have the same originating page
471 if (!empty($insert_data['originating_page_url'])) {
472 update_option("mxchat_originating_page_{$session_id}", [
473 'url' => $insert_data['originating_page_url'],
474 'title' => $insert_data['originating_page_title']
475 ], 'no');
476 }
477 } else {
478 // For subsequent messages in the session, use the stored originating page
479 $stored_originating = get_option("mxchat_originating_page_{$session_id}");
480 if ($stored_originating && !empty($stored_originating['url'])) {
481 $insert_data['originating_page_url'] = $stored_originating['url'];
482 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
483 }
484 }
485 }
486
487 $wpdb->insert($table_name, $insert_data);
488 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
489
490 // 9) Send notification email if this is the first user message in a new session
491 if ($wpdb->insert_id && $is_new_session && $role === 'user') {
492 $this->send_new_chat_notification($session_id, array(
493 'identifier' => $user_identifier,
494 'email' => $saved_email ?: $user_email,
495 'ip' => $_SERVER['REMOTE_ADDR']
496 ));
497 }
498
499 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
500 return $message_id;
501 }
502 private function send_new_chat_notification($session_id, $user_info = array()) {
503 $options = get_option('mxchat_transcripts_options');
504
505 // Check if notifications are enabled
506 if (empty($options['mxchat_enable_notifications'])) {
507 return false;
508 }
509
510 // Get notification email
511 $to = !empty($options['mxchat_notification_email']) ?
512 $options['mxchat_notification_email'] :
513 get_option('admin_email');
514
515 if (!is_email($to)) {
516 return false;
517 }
518
519 // Prepare email content
520 $subject = sprintf('[%s] New Chat Session Started', get_bloginfo('name'));
521
522 $user_identifier = isset($user_info['identifier']) ? $user_info['identifier'] : 'Guest';
523 $user_email = isset($user_info['email']) ? $user_info['email'] : 'Not provided';
524 $user_ip = isset($user_info['ip']) ? $user_info['ip'] : $_SERVER['REMOTE_ADDR'];
525
526 $message = sprintf(
527 "A new chat session has started on your website.\n\n" .
528 "Session ID: %s\n" .
529 "User: %s\n" .
530 "Email: %s\n" .
531 "IP Address: %s\n" .
532 "Time: %s\n\n" .
533 "View transcripts: %s",
534 $session_id,
535 $user_identifier,
536 $user_email,
537 $user_ip,
538 current_time('mysql'),
539 admin_url('admin.php?page=mxchat-transcripts')
540 );
541
542 // Send email
543 return wp_mail($to, $subject, $message);
544 }
545
546 public function mxchat_handle_save_email_and_response() {
547 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
548 //error_log('DEBUG: POST data: ' . print_r($_POST, true));
549
550 // Validate nonce
551 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
552 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
553 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
554 wp_die();
555 }
556
557 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
558 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
559 $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
560
561 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
562
563 if (empty($session_id) || empty($email)) {
564 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
565 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
566 wp_die();
567 }
568
569 // Validate name if provided (check if name field is enabled and name is required)
570 $options = get_option('mxchat_options', []);
571 $name_field_enabled = isset($options['enable_name_field']) &&
572 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
573
574 if ($name_field_enabled && (empty($name) || strlen(trim($name)) < 2 || strlen(trim($name)) > 100)) {
575 //error_log("[ERROR] Invalid name: {$name} (enabled: {$name_field_enabled})");
576 wp_send_json_error(['message' => esc_html__('Name must be between 2 and 100 characters.', 'mxchat')]);
577 wp_die();
578 }
579
580 // 1) Always store email in wp_options
581 $email_option_key = "mxchat_email_{$session_id}";
582 update_option($email_option_key, $email);
583 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
584
585 // Store name in wp_options if provided
586 if (!empty($name)) {
587 $name_option_key = "mxchat_name_{$session_id}";
588 update_option($name_option_key, $name);
589 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
590 }
591
592 // 2) (Optional) Also store in DB if a row already exists
593 global $wpdb;
594 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
595
596 // Make sure we have a valid placeholder in prepare
597 $sql = $wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE session_id = %s", $session_id);
598 $session_count = $wpdb->get_var($sql);
599
600 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
601
602 if ($session_count) {
603 // Update both user_email and user_name if row(s) exist
604 if (!empty($name)) {
605 $update_sql = $wpdb->prepare(
606 "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
607 $email,
608 $name,
609 $session_id
610 );
611 } else {
612 $update_sql = $wpdb->prepare(
613 "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
614 $email,
615 $session_id
616 );
617 }
618 $wpdb->query($update_sql);
619 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
620 } else {
621 //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email/name is only in wp_options.");
622 }
623
624 // Provide success response (same as original)
625 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
626 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
627 wp_send_json_success(['message' => $bot_message]);
628 wp_die();
629 }
630
631 public function mxchat_check_email_provided() {
632 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
633
634 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
635 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
636 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
637 }
638
639 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
640 if (empty($session_id)) {
641 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
642 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
643 }
644
645 // Check if the user is logged in
646 if (is_user_logged_in()) {
647 $current_user = wp_get_current_user();
648 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
649
650 // Get user's display name for logged in users
651 $user_name = !empty($current_user->display_name) ? $current_user->display_name :
652 (!empty($current_user->first_name) ? $current_user->first_name : '');
653
654 $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
655 if (!empty($user_name)) {
656 $response_data['name'] = $user_name;
657 }
658
659 wp_send_json_success($response_data);
660 }
661
662 // Check if name field is required
663 $options = get_option('mxchat_options', []);
664 $name_field_enabled = isset($options['enable_name_field']) &&
665 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
666
667 $email_option_key = "mxchat_email_{$session_id}";
668 $stored_email = get_option($email_option_key, '');
669
670 // Check for stored name
671 $name_option_key = "mxchat_name_{$session_id}";
672 $stored_name = get_option($name_option_key, '');
673
674 //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
675 //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
676
677 // Check if we have email and name (if name is required)
678 $has_required_info = !empty($stored_email);
679
680 if ($name_field_enabled) {
681 $has_required_info = $has_required_info && !empty($stored_name);
682 }
683
684 if ($has_required_info) {
685 //error_log("[DEBUG] mxchat_check_email_provided -> Required info found, returning success");
686
687 $response_data = ['email' => $stored_email];
688 if (!empty($stored_name)) {
689 $response_data['name'] = $stored_name;
690 }
691
692 wp_send_json_success($response_data);
693 } else {
694 //error_log("[DEBUG] mxchat_check_email_provided -> Required info missing, returning error");
695 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
696 }
697 }
698
699 public function mxchat_handle_chat_request() {
700 global $wpdb;
701
702 // Debug: Log incoming bot_id
703 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
704 error_log("=== MXCHAT DEBUG: Starting chat request ===");
705 error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
706
707 // Get bot-specific options
708 $bot_options = $this->get_bot_options($bot_id);
709 $current_options = !empty($bot_options) ? $bot_options : $this->options;
710
711 // Check if this is a streaming request
712 $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
713
714 // Set streaming headers if needed
715 if ($is_streaming) {
716 // Disable output buffering
717 while (ob_get_level()) {
718 ob_end_flush(); // Changed from ob_end_clean()
719 }
720
721 // Set headers for SSE
722 header('Content-Type: text/event-stream');
723 header('Cache-Control: no-cache');
724 header('Connection: keep-alive');
725 header('X-Accel-Buffering: no');
726
727 // Add these new lines:
728 ob_implicit_flush(true);
729 flush();
730 }
731
732 // Check if MX Chat Moderation is active
733 if (class_exists('MX_Chat_Moderation')) {
734 // Get user email and IP
735 $user_email = '';
736 $user_ip = $_SERVER['REMOTE_ADDR'];
737
738 // If user is logged in, get their email
739 if (is_user_logged_in()) {
740 $current_user = wp_get_current_user();
741 $user_email = $current_user->user_email;
742 }
743
744 // Create ban handler instance
745 $ban_handler = new MX_Chat_Ban_Handler();
746
747 // Check if user is banned by IP
748 if ($ban_handler->check_ban($user_ip, 'ip')) {
749 wp_send_json([
750 'success' => false,
751 'message' => esc_html__('Access denied. Your IP address has been banned.', 'mxchat'),
752 'status' => 'banned'
753 ]);
754 wp_die();
755 }
756
757 // If user is logged in, also check email
758 if (!empty($user_email) && $ban_handler->check_ban($user_email, 'email')) {
759 wp_send_json([
760 'success' => false,
761 'message' => esc_html__('Access denied. Your email address has been banned.', 'mxchat'),
762 'status' => 'banned'
763 ]);
764 wp_die();
765 }
766 }
767
768 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
769 $this->productCardHtml = '';
770
771 // Get the actual WordPress user ID if logged in
772 $is_logged_in = is_user_logged_in();
773 if ($is_logged_in) {
774 $user_id = get_current_user_id(); // This will get the actual WordPress user ID
775 } else {
776 // For logged-out users, use your existing identifier method
777 $user_id = $this->mxchat_get_user_identifier();
778 }
779
780 // Get and sanitize the user identifier
781 $user_id = sanitize_key($user_id);
782
783 // Check rate limit using new settings structure
784 $rate_limit_result = $this->check_rate_limit();
785
786 if ($rate_limit_result !== true) {
787 wp_send_json([
788 'success' => false,
789 'message' => $rate_limit_result['message'],
790 'status' => 'rate_limit_exceeded'
791 ]);
792 wp_die();
793 }
794
795 // Rest of your existing code...
796 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
797
798 if (empty($session_id)) {
799 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
800 wp_die();
801 }
802
803 // Validate and sanitize the incoming message
804 if (empty($_POST['message'])) {
805 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
806 wp_die();
807 }
808
809
810 // Track originating page for first message in session
811 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
812
813 // Check if originating page columns exist
814 $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
815
816 if ($columns_exist) {
817 // Check if this session already has messages
818 $message_count = $wpdb->get_var($wpdb->prepare(
819 "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
820 $session_id
821 ));
822
823 // If this is the first message in the session
824 if ($message_count == 0) {
825 // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
826 $originating_url = '';
827 $originating_title = '';
828
829 // Try to get from POST data first (sent by JavaScript)
830 if (isset($_POST['current_page_url'])) {
831 $originating_url = esc_url_raw($_POST['current_page_url']);
832 $originating_title = isset($_POST['current_page_title'])
833 ? sanitize_text_field($_POST['current_page_title'])
834 : '';
835 }
836 // Fallback to HTTP_REFERER if not provided by JavaScript
837 else if (isset($_SERVER['HTTP_REFERER'])) {
838 $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
839 }
840
841 // Generate title if we have URL but no title
842 if ($originating_url && empty($originating_title)) {
843 $parsed_url = parse_url($originating_url);
844 $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
845
846 if (empty($path) || $path === 'index.php' || $path === 'index.html') {
847 $originating_title = 'Homepage';
848 } else {
849 // Clean up the path to make a readable title
850 $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
851 $originating_title = ucwords(trim($originating_title));
852 }
853 }
854
855 // Store for later use when saving the message
856 $this->pending_originating_page = [
857 'url' => $originating_url,
858 'title' => $originating_title
859 ];
860 }
861 }
862
863
864
865 // Get page context if provided
866 $page_context = null;
867 if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
868 $page_context_raw = stripslashes($_POST['page_context']);
869 $page_context = json_decode($page_context_raw, true);
870
871 // Validate page context structure
872 if (is_array($page_context) &&
873 isset($page_context['url']) &&
874 isset($page_context['title']) &&
875 isset($page_context['content'])) {
876
877 // Sanitize page context
878 $page_context['url'] = esc_url_raw($page_context['url']);
879 $page_context['title'] = sanitize_text_field($page_context['title']);
880 $page_context['content'] = wp_kses_post($page_context['content']);
881 } else {
882 $page_context = null;
883 }
884 }
885
886 // Modify the message sanitization to preserve PHP tags in code blocks
887 $allowed_tags = [
888 'pre' => [],
889 'code' => ['class' => true],
890 'span' => ['class' => true],
891 'div' => ['class' => true],
892 ];
893
894 // First preserve code blocks
895 $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
896 return htmlspecialchars_decode($matches[0]);
897 }, $_POST['message']);
898
899 // Then apply sanitization
900 $message = wp_kses($message, $allowed_tags);
901
902 // Preserve code blocks from markdown conversion
903 $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
904 $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
905
906 // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
907 // Always initialize testing data for admins (no toggle needed)
908 $testing_data = null;
909 if (current_user_can('administrator')) {
910 // For vision messages, use the original user message for the query display
911 $query_for_testing = $message;
912 if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
913 $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
914 }
915
916 $testing_data = [
917 'query' => $query_for_testing,
918 'timestamp' => time(),
919 'top_matches' => [],
920 'action_matches' => [], // Initialize action matches array
921 'page_context' => $page_context, // Include page context in testing data
922 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
923 'bot_id' => $bot_id // Include bot ID in testing data
924 ];
925
926 // Get similarity threshold from bot options or default options
927 $similarity_threshold = isset($current_options['similarity_threshold'])
928 ? ((int) $current_options['similarity_threshold']) / 100
929 : 0.75;
930
931 $testing_data['similarity_threshold'] = $similarity_threshold;
932
933 // Determine knowledge base type using bot-specific config
934 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
935 $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
936 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
937 }
938 // ===== END SIMPLIFIED TESTING INITIALIZATION =====
939
940 // Add debug before and after:
941 //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
942 $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
943 //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
944
945
946 // If the pre-processing returned a result (not the original message), use it directly
947 if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
948 // Save the AI response
949 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
950
951 // Save HTML content if provided
952 if (!empty($pre_processed_result['html'])) {
953 $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
954 }
955
956 // Add testing data if admin
957 $response_data = [
958 'text' => $pre_processed_result['text'],
959 'html' => $pre_processed_result['html'] ?? '',
960 'session_id' => $session_id
961 ];
962
963 if ($testing_data !== null) {
964 $response_data['testing_data'] = $testing_data;
965 }
966
967 wp_send_json($response_data);
968 wp_die();
969 }
970
971 // Save the user's message - handle vision processed messages differently
972 if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
973 // For vision messages, save the original user message with image indicator
974 $original_message = sanitize_textarea_field($_POST['original_user_message']);
975 if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
976 $image_count = intval($_POST['vision_images_count']);
977 $original_message .= " [{$image_count} image(s)]";
978 }
979 $this->mxchat_save_chat_message($session_id, 'user', $original_message);
980 } else {
981 // Regular message - save as normal
982 $this->mxchat_save_chat_message($session_id, 'user', $message);
983 }
984
985
986 if (is_email($message)) {
987 // Add the email to Loops
988 $this->add_email_to_loops($message);
989
990 // Get the user's success message instruction using current_options
991 $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
992
993 // Set instruction for AI using the user's success message
994 $this->current_action_instruction = $user_success_message;
995
996 // Clear the email capture transient since we got the email
997 delete_transient('mxchat_email_capture_' . $user_id);
998 }
999
1000 // Check if we're in an email capture flow but user hasn't provided email yet
1001 elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1002 // Check if the message contains an email (not the whole message being an email)
1003 if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1004 $extracted_email = $matches[0];
1005
1006 // Add the extracted email to Loops
1007 $this->add_email_to_loops($extracted_email);
1008
1009 // Get the user's success message instruction using current_options
1010 $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1011
1012 // Set instruction for AI using the user's success message
1013 $this->current_action_instruction = $user_success_message;
1014
1015 // Clear the email capture transient since we got the email
1016 delete_transient('mxchat_email_capture_' . $user_id);
1017 }
1018 // If no email found but we're in capture mode, remind them
1019 else {
1020 // Get the original instruction to remind them using current_options
1021 $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1022 $this->current_action_instruction = $original_instruction;
1023 }
1024 }
1025
1026 $intent_info = '';
1027
1028 // Check chat mode
1029 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1030
1031 // Handle agent mode
1032 // Handle agent mode
1033 if ($chat_mode === 'agent') {
1034 // First, check for switch intent before doing anything else
1035 $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1036
1037 // Capture action analysis for testing panel after intent check
1038 if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1039 $testing_data['action_matches'] = $this->last_action_analysis;
1040 }
1041
1042 // Around line 506, in the agent mode handling section:
1043 if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1044 // Update chat mode first
1045 update_option("mxchat_mode_{$session_id}", 'ai');
1046
1047 // Clear any existing PDF context to start fresh
1048 $this->clear_pdf_transients($session_id);
1049
1050 // Prepare clean switch response with explicit chat_mode
1051 $response_data = [
1052 'text' => $this->fallbackResponse['text'],
1053 'html' => $this->fallbackResponse['html'] ?? '',
1054 'session_id' => $session_id,
1055 'chat_mode' => 'ai' // EXPLICITLY SET THIS
1056 ];
1057
1058 if ($testing_data !== null) {
1059 $response_data['testing_data'] = $testing_data;
1060 }
1061
1062 // Save the mode switch message
1063 $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1064 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1065
1066 // Send response and exit
1067 wp_send_json($response_data);
1068 wp_die();
1069 } elseif (!$intent_matched) {
1070 // No intent matched, handle live agent message
1071 try {
1072 $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1073
1074 $agent_response = [
1075 'status' => 'waiting_for_agent',
1076 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1077 ];
1078
1079 if ($testing_data !== null) {
1080 $agent_response['testing_data'] = $testing_data;
1081 }
1082
1083 wp_send_json_success($agent_response);
1084 } catch (\Exception $e) {
1085 wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1086 }
1087 wp_die();
1088 }
1089 }
1090
1091 // Step 1: Check for new PDF URL in the message
1092 if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1093 $new_pdf_url = $matches[0];
1094
1095 // Check if this is likely a PDF-related request
1096 $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1097 $is_pdf_request = false;
1098
1099 foreach ($pdf_keywords as $keyword) {
1100 if (stripos($message, $keyword) !== false) {
1101 $is_pdf_request = true;
1102 break;
1103 }
1104 }
1105
1106 // If it looks like a PDF request or we're waiting for a PDF URL
1107 if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1108 // Validate HTTPS
1109 if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1110 // Extract filename from URL
1111 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1112
1113 // Clear previous PDF transients
1114 $this->clear_pdf_transients($session_id);
1115
1116 // Process new PDF using current_options
1117 $max_pages = $current_options['pdf_max_pages'] ?? 69;
1118 $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1119
1120 if ($embeddings === 'too_many_pages') {
1121 $error_text = sprintf(
1122 $current_options['pdf_intent_error_text'] ??
1123 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1124 $max_pages
1125 );
1126 $this->fallbackResponse['text'] = $error_text;
1127 } elseif ($embeddings) {
1128 // Store new PDF information
1129 $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1130
1131 // If the filename is generic, create a more descriptive one
1132 if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1133 strpos($pdf_filename, '.php') !== false) {
1134 $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1135 }
1136
1137 set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1138 set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1139 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1140 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1141
1142 $success_text = $current_options['pdf_intent_success_text'] ??
1143 esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1144
1145 $pdf_response = [
1146 'success' => true,
1147 'message' => $success_text,
1148 'data' => [
1149 'filename' => $pdf_filename
1150 ]
1151 ];
1152
1153 if ($testing_data !== null) {
1154 $pdf_response['testing_data'] = $testing_data;
1155 }
1156
1157 wp_send_json($pdf_response);
1158 wp_die();
1159 } else {
1160 $error_text = $current_options['pdf_intent_error_text'] ??
1161 esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1162 $this->fallbackResponse['text'] = $error_text;
1163 }
1164
1165 $pdf_error_response = [
1166 'success' => false,
1167 'message' => $this->fallbackResponse['text']
1168 ];
1169
1170 if ($testing_data !== null) {
1171 $pdf_error_response['testing_data'] = $testing_data;
1172 }
1173
1174 wp_send_json($pdf_error_response);
1175 wp_die();
1176 }
1177 }
1178 }
1179
1180 // Check if there's an active recommendation flow session
1181 $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1182 if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1183 // Create a dummy intent object that matches the original intent
1184 $dummy_intent = new stdClass();
1185 $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1186 $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1187
1188 // Call the recommendation flow handler directly
1189 $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1190
1191 // If the handler returned a response, send it
1192 if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1193 // Save the bot's response to the chat history
1194 if (!empty($response_data['text'])) {
1195 $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
1196 }
1197 if (!empty($response_data['html'])) {
1198 $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1199 }
1200
1201 if ($testing_data !== null) {
1202 $response_data['testing_data'] = $testing_data;
1203 }
1204
1205 // Send the response
1206 wp_send_json($response_data);
1207 wp_die();
1208 }
1209 }
1210
1211 // Step 2: Detect intent and handle intent-based responses
1212 $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1213
1214 // Capture action analysis for testing panel after intent check
1215 if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1216 $testing_data['action_matches'] = $this->last_action_analysis;
1217 }
1218
1219 // Step 3: Handle the intent result appropriately
1220 if ($intent_result !== false) {
1221 // Intent was matched - ALWAYS send as JSON response, never streaming
1222
1223 if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1224 // Intent returned a direct response array
1225 $response_data = [
1226 'text' => $intent_result['text'] ?? '',
1227 'html' => $intent_result['html'] ?? '',
1228 'session_id' => $session_id
1229 ];
1230
1231 if ($testing_data !== null) {
1232 $response_data['testing_data'] = $testing_data;
1233 }
1234
1235 // Clear streaming headers if they were set
1236 if ($is_streaming) {
1237 header_remove('Content-Type');
1238 header_remove('Cache-Control');
1239 header_remove('Connection');
1240 header_remove('X-Accel-Buffering');
1241 header('Content-Type: application/json');
1242 }
1243
1244 wp_send_json($response_data);
1245 wp_die();
1246 } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1247 // Intent returned true and set fallbackResponse
1248
1249 // SAVE TO TRANSCRIPT FIRST
1250 if (!empty($this->fallbackResponse['text'])) {
1251 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1252 }
1253 if (!empty($this->fallbackResponse['html'])) {
1254 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1255 }
1256
1257 $response_data = [
1258 'text' => $this->fallbackResponse['text'] ?? '',
1259 'html' => $this->fallbackResponse['html'] ?? '',
1260 'session_id' => $session_id
1261 ];
1262
1263 if (isset($this->fallbackResponse['chat_mode'])) {
1264 $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1265 }
1266
1267 if ($testing_data !== null) {
1268 $response_data['testing_data'] = $testing_data;
1269 }
1270
1271 // Clear streaming headers if they were set
1272 if ($is_streaming) {
1273 header_remove('Content-Type');
1274 header_remove('Cache-Control');
1275 header_remove('Connection');
1276 header_remove('X-Accel-Buffering');
1277 header('Content-Type: application/json');
1278 }
1279
1280 wp_send_json($response_data);
1281 wp_die();
1282 }
1283 }
1284
1285 // If we get here, no intent matched OR the intent didn't provide a usable response
1286
1287 // Step 4: Generate AI response
1288 $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1289 $this->mxchat_increment_chat_count();
1290
1291 // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1292 $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1293 $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1294
1295 // Check if the embedding generation returned an error
1296 if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1297 $error_message = $user_message_embedding['error'];
1298 $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1299
1300 wp_send_json_error([
1301 'error_message' => $error_message,
1302 'error_code' => $error_code
1303 ]);
1304 wp_die();
1305 }
1306
1307 // Check if the embedding is valid
1308 if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1309 wp_send_json_error([
1310 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1311 'error_code' => 'invalid_embedding'
1312 ]);
1313 wp_die();
1314 }
1315
1316 // Build context with both knowledge base and PDF content if available
1317 $context_content = "User asked: '{$message}'\n\n";
1318
1319 // Add action instruction if present (add this right after the above line)
1320 if (!empty($this->current_action_instruction)) {
1321 $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1322 $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1323 $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1324 $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1325
1326 // Clear the instruction after using it
1327 $this->current_action_instruction = null;
1328 }
1329
1330
1331 // Add page context if available and contextual awareness is enabled using current_options
1332 if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
1333 $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1334 $context_content .= "Page URL: " . $page_context['url'] . "\n";
1335 $context_content .= "Page Title: " . $page_context['title'] . "\n";
1336 $context_content .= "Page Content: " . $page_context['content'] . "\n";
1337 $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1338 }
1339
1340 // Get relevant content from knowledge base - PASS BOT_ID
1341 $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id);
1342
1343 // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1344 if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1345 // Update testing data with the REAL similarity analysis
1346 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1347 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1348 $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1349 }
1350 // ===== END SIMILARITY DATA CAPTURE =====
1351
1352 if (!empty($relevant_content)) {
1353 $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1354 } else {
1355 $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1356 }
1357
1358 // Check for and include PDF content
1359 $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1360 $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1361 $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1362 if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1363 $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1364 if (!empty($relevant_pdf_pages)) {
1365 $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1366 foreach ($relevant_pdf_pages as $page_data) {
1367 $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
1368 }
1369 $context_content .= "\n";
1370 }
1371 }
1372
1373 // Check for and include Word content
1374 $word_url = get_transient('mxchat_word_url_' . $session_id);
1375 $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1376 $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1377 if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1378 $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1379 if (!empty($relevant_word_chunks)) {
1380 $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1381 foreach ($relevant_word_chunks as $chunk_data) {
1382 $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1383 }
1384 $context_content .= "\n";
1385 }
1386 }
1387
1388 $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1389
1390 // Generate response - USE BOT-SPECIFIC API KEYS
1391 $response = $this->mxchat_generate_response(
1392 $context_content,
1393 $current_options['api_key'] ?? $this->options['api_key'],
1394 $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
1395 $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
1396 $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
1397 $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
1398 $conversation_history,
1399 $is_streaming,
1400 $session_id,
1401 $testing_data
1402 );
1403
1404 // Handle streaming vs non-streaming responses
1405 if ($is_streaming) {
1406 // Check if streaming actually happened or if it fell back to regular response
1407 if ($response === true) {
1408 wp_die();
1409 }
1410 // If we get here, streaming fell back to regular response, continue
1411 }
1412
1413 // Check if the response is an error array
1414 if (is_array($response) && isset($response['error'])) {
1415 wp_send_json_error([
1416 'error_message' => $response['error'],
1417 'error_code' => $response['error_code'] ?? 'api_error'
1418 ]);
1419 wp_die();
1420 }
1421
1422 // If we get here, the response is valid text
1423 $this->mxchat_save_chat_message($session_id, 'bot', $response);
1424
1425 // Step 5: Save additional content if available
1426 if (!empty($this->productCardHtml)) {
1427 $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1428 }
1429
1430 if (!empty($this->fallbackResponse['html'])) {
1431 $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1432 }
1433
1434 // Step 6: Return the response
1435 $response_data = [
1436 'text' => $response,
1437 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1438 'session_id' => $session_id
1439 ];
1440
1441 // Always add testing data for admins (no toggle needed)
1442 if ($testing_data !== null) {
1443 $response_data['testing_data'] = $testing_data;
1444 }
1445
1446 wp_send_json($response_data);
1447 wp_die();
1448 }
1449
1450
1451 /**
1452 * Get bot-specific options for multi-bot functionality
1453 * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
1454 */
1455 // Also debug the bot options retrieval
1456 private function get_bot_options($bot_id = 'default') {
1457 error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
1458
1459 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1460 error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
1461 return array();
1462 }
1463
1464 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
1465
1466 if (!empty($bot_options)) {
1467 error_log("MXCHAT DEBUG: Got bot-specific options from filter");
1468 if (isset($bot_options['similarity_threshold'])) {
1469 error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
1470 }
1471 }
1472
1473 return is_array($bot_options) ? $bot_options : array();
1474 }
1475
1476 /**
1477 * Get bot-specific Pinecone configuration
1478 * Used in the knowledge retrieval functions
1479 */
1480 // Also add debugging to your get_bot_pinecone_config function
1481 private function get_bot_pinecone_config($bot_id = 'default') {
1482 error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
1483
1484 // If default bot or multi-bot add-on not active, use default Pinecone config
1485 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
1486 error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
1487 $addon_options = get_option('mxchat_pinecone_addon_options', array());
1488 $config = array(
1489 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
1490 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
1491 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
1492 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
1493 );
1494 error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
1495 return $config;
1496 }
1497
1498 error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
1499
1500 // Hook for multi-bot add-on to provide bot-specific Pinecone config
1501 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
1502
1503 if (!empty($bot_pinecone_config)) {
1504 error_log("MXCHAT DEBUG: Got bot-specific config from filter");
1505 error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
1506 error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
1507 error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
1508 } else {
1509 error_log("MXCHAT DEBUG: Filter returned empty config!");
1510 }
1511
1512 return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1513 }
1514
1515
1516 // Updated function to check intents and invoke the callback function
1517 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
1518 global $wpdb;
1519 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1520
1521 // NEW: Get the current bot_id
1522 $current_bot_id = $this->get_current_bot_id($session_id);
1523
1524 // Generate the user embedding
1525 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1526
1527 // Check if embedding generation returned an error
1528 if (is_array($user_embedding) && isset($user_embedding['error'])) {
1529 $error_message = $user_embedding['error'];
1530 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
1531
1532 wp_send_json_error([
1533 'error_message' => $error_message,
1534 'error_code' => $error_code
1535 ]);
1536 wp_die();
1537 }
1538
1539 // Check if embedding is valid
1540 if (!is_array($user_embedding) || empty($user_embedding)) {
1541 wp_send_json_error([
1542 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1543 'error_code' => 'invalid_embedding'
1544 ]);
1545 wp_die();
1546 }
1547
1548 // Fetch intents from the database
1549 $table_name = $wpdb->prefix . 'mxchat_intents';
1550 if ($chat_mode === 'agent') {
1551 $query = $wpdb->prepare(
1552 "SELECT * FROM $table_name WHERE callback_function = %s AND (enabled = 1 OR enabled IS NULL)",
1553 'mxchat_handle_switch_to_chatbot_intent'
1554 );
1555 $intents = $wpdb->get_results($query);
1556 } else {
1557 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
1558 }
1559
1560 if (empty($intents)) {
1561 return false;
1562 }
1563
1564 $highest_similarity = -INF;
1565 $matched_intent = null;
1566
1567 // Array to store action analysis for testing panel
1568 $action_analysis = [];
1569
1570 foreach ($intents as $intent) {
1571 // Additional check for enabled state
1572 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
1573 if (!$is_enabled) {
1574 continue;
1575 }
1576
1577 // NEW: Check if this action is enabled for the current bot
1578 if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
1579 continue;
1580 }
1581
1582 $intent_embedding_serialized = $intent->embedding_vector;
1583 $intent_embedding = $intent_embedding_serialized
1584 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
1585 : null;
1586
1587 if (!is_array($intent_embedding)) {
1588 continue;
1589 }
1590
1591 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
1592 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
1593
1594 // Store action analysis data for testing panel
1595 $action_analysis[] = [
1596 'intent_label' => $intent->intent_label,
1597 'callback_function' => $intent->callback_function,
1598 'similarity' => round($similarity, 4),
1599 'similarity_percentage' => round($similarity * 100, 2),
1600 'threshold' => $intent_threshold,
1601 'threshold_percentage' => round($intent_threshold * 100, 2),
1602 'above_threshold' => $similarity >= $intent_threshold,
1603 'triggered' => false // Will be updated below if this intent is triggered
1604 ];
1605
1606 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
1607 $highest_similarity = $similarity;
1608 $matched_intent = $intent;
1609 }
1610 }
1611
1612 // Mark the triggered action if any
1613 if ($matched_intent) {
1614 foreach ($action_analysis as &$action) {
1615 if ($action['intent_label'] === $matched_intent->intent_label) {
1616 $action['triggered'] = true;
1617 break;
1618 }
1619 }
1620 }
1621
1622 // Sort actions by similarity (highest first) and store for testing panel
1623 usort($action_analysis, function($a, $b) {
1624 return $b['similarity'] <=> $a['similarity'];
1625 });
1626
1627 // Store action analysis for testing panel capture
1628 $this->last_action_analysis = $action_analysis;
1629
1630 // Around line 715 in your mxchat_check_intent_and_invoke_callback function
1631 if ($matched_intent) {
1632 // If the callback is a method on this instance (core callback), call it directly
1633 if (method_exists($this, $matched_intent->callback_function)) {
1634 $callback_result = call_user_func(
1635 [$this, $matched_intent->callback_function],
1636 $message,
1637 $user_id,
1638 $session_id,
1639 $matched_intent,
1640 $user_context ?? null
1641 );
1642 } else {
1643 // Otherwise, use apply_filters for add-on callbacks
1644 $callback_result = apply_filters(
1645 $matched_intent->callback_function,
1646 false,
1647 $message,
1648 $user_id,
1649 $session_id,
1650 $matched_intent
1651 );
1652 }
1653
1654 // Handle the callback result properly
1655 if ($callback_result !== false) {
1656 // If callback returned an array with chat_mode, use it directly
1657 if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
1658 $this->fallbackResponse = $callback_result;
1659 return $callback_result; // Return the full array
1660 } else {
1661 $this->fallbackResponse = $callback_result;
1662 return true;
1663 }
1664 }
1665 }
1666
1667 return false;
1668 }
1669
1670 /**
1671 * Check if an action is enabled for a specific bot
1672 */
1673 private function is_action_enabled_for_bot($intent, $bot_id) {
1674 // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
1675 if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
1676 return true;
1677 }
1678
1679 $enabled_bots = json_decode($intent->enabled_bots, true);
1680
1681 // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
1682 if (!is_array($enabled_bots) || empty($enabled_bots)) {
1683 return true;
1684 }
1685
1686 // Check if the current bot is in the enabled bots list
1687 return in_array($bot_id, $enabled_bots);
1688 }
1689
1690 // Helper function to clear PDF and Word document related transients
1691 private function clear_pdf_transients($session_id) {
1692 // PDF transients
1693 delete_transient('mxchat_pdf_url_' . $session_id);
1694 delete_transient('mxchat_pdf_embeddings_' . $session_id);
1695 delete_transient('mxchat_include_pdf_in_context_' . $session_id);
1696 delete_transient('mxchat_waiting_for_pdf_url_' . $session_id);
1697
1698 // Word document transients
1699 delete_transient('mxchat_word_url_' . $session_id);
1700 delete_transient('mxchat_word_filename_' . $session_id);
1701 delete_transient('mxchat_word_embeddings_' . $session_id);
1702 delete_transient('mxchat_include_word_in_context_' . $session_id);
1703 delete_transient('mxchat_waiting_for_word_' . $session_id);
1704 }
1705
1706
1707
1708 //verified good
1709 public function mxchat_handle_email_capture($message, $user_id, $session_id) {
1710 // Get the user's original instruction/message
1711 $user_instruction = esc_html($this->options['triggered_phrase_response'] ?? esc_html__("Please provide your email address.", 'mxchat'));
1712
1713 // Set instruction for AI - just pass along what the user wanted to say
1714 $this->current_action_instruction = $user_instruction;
1715
1716 // Set the transient to track email capture flow
1717 set_transient('mxchat_email_capture_' . $user_id, true, 5 * MINUTE_IN_SECONDS);
1718
1719 // Return false to let the AI generate the response
1720 return false;
1721 }
1722
1723 public function mxchat_generate_image($message, $user_id, $session_id) {
1724 //error_log("Starting image generation for message: " . $message);
1725
1726 // Prepare a prompt for DALL-E
1727 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
1728
1729 // Use the existing OpenAI API key
1730 $openai_api_key = sanitize_text_field($this->options['api_key']);
1731
1732 // Call DALL-E to generate an image
1733 $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1734
1735 // Check if the response contains an image URL
1736 if (isset($image_response['imageUrl'])) {
1737 $image_url = esc_url_raw($image_response['imageUrl']);
1738
1739 // Construct the HTML with a CSS class instead of inline styles
1740 $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
1741 $response_text = esc_html__('Here is the image I generated:', 'mxchat');
1742
1743 // Save the bot message with both text and HTML
1744 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1745 $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
1746
1747 // Set the fallback response for the chat handler
1748 $this->fallbackResponse = [
1749 'text' => $response_text,
1750 'html' => $response_html,
1751 'images' => [$image_url]
1752 ];
1753
1754 // For debugging/verification - Use json_encode to verify what's being set
1755 //error_log("Image generation successful - fallbackResponse set: " . json_encode($this->fallbackResponse));
1756
1757 // Return the response directly instead of relying on the property
1758 return $this->fallbackResponse;
1759 } else {
1760 $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
1761
1762 // Save the error message
1763 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
1764
1765 // Set the fallback response for the chat handler
1766 $this->fallbackResponse = [
1767 'text' => $response_text,
1768 'html' => '',
1769 'images' => []
1770 ];
1771
1772 //error_log("DALL-E image generation error: " . esc_html($image_response['error'] ?? 'Unknown error.'));
1773 //error_log("Error fallbackResponse set: " . json_encode($this->fallbackResponse));
1774
1775 // Return the response directly instead of relying on the property
1776 return $this->fallbackResponse;
1777 }
1778 }
1779 private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
1780 $api_url = 'https://api.openai.com/v1/images/generations';
1781 $body = json_encode([
1782 'prompt' => sanitize_text_field($prompt),
1783 'n' => 1,
1784 'size' => '1024x1024',
1785 'model' => sanitize_text_field($model),
1786 ]);
1787
1788 $args = [
1789 'body' => $body,
1790 'headers' => [
1791 'Content-Type' => 'application/json',
1792 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
1793 ],
1794 'method' => 'POST',
1795 'timeout' => absint($timeout),
1796 ];
1797
1798 $response = wp_remote_post($api_url, $args);
1799
1800 if (is_wp_error($response)) {
1801 //error_log("DALL-E request failed: " . $response->get_error_message());
1802 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
1803 }
1804
1805 $response_body = json_decode(wp_remote_retrieve_body($response), true);
1806
1807 if (isset($response_body['data'][0]['url'])) {
1808 return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
1809 } else {
1810 //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
1811 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
1812 }
1813 }
1814
1815 /**
1816 * Handle web search requests.
1817 *
1818 * Sends the refined search query to the Brave Search API and uses the
1819 * results to generate a conversational response with the AI model.
1820 *
1821 * @since 1.0.0
1822 * @param string $message The user's search query.
1823 * @param string $user_id The user identifier.
1824 * @param string $session_id The current session ID.
1825 * @return array Response array containing text with embedded HTML links
1826 */
1827 public function mxchat_handle_search_request($message, $user_id, $session_id) {
1828 // Step 1: Interpret and refine the search query
1829 $refined_search_query = $this->mxchat_interpret_search_query($message);
1830 if (empty($refined_search_query)) {
1831 return array(
1832 'text' => esc_html__('I apologize, but could you please rephrase your search request?', 'mxchat'),
1833 'html' => ''
1834 );
1835 }
1836
1837 // Retrieve and validate API settings
1838 $options = get_option('mxchat_options');
1839 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1840 $results_count = isset($options['brave_results_count']) ? absint($options['brave_results_count']) : 5;
1841
1842 if (empty($api_key)) {
1843 return array(
1844 'text' => esc_html__('Search functionality is temporarily unavailable. Please try again later.', 'mxchat'),
1845 'html' => ''
1846 );
1847 }
1848
1849 // Build the API request URL
1850 $api_url = add_query_arg(
1851 array(
1852 'q' => rawurlencode($refined_search_query),
1853 'count' => $results_count,
1854 'text_decorations' => 'true',
1855 'rich_data' => 'true',
1856 ),
1857 'https://api.search.brave.com/res/v1/web/search'
1858 );
1859
1860 // Attempt to retrieve cached results first
1861 $transient_key = 'mxchat_search_' . md5($refined_search_query);
1862 $results = get_transient($transient_key);
1863
1864 if (false === $results) {
1865 // Fetch new results from the Brave Search API
1866 $response = wp_remote_get(
1867 $api_url,
1868 array(
1869 'headers' => array(
1870 'Accept' => 'application/json',
1871 'Accept-Encoding' => 'gzip',
1872 'X-Subscription-Token'=> $api_key,
1873 ),
1874 'timeout' => 10,
1875 )
1876 );
1877
1878 if (is_wp_error($response)) {
1879 return array(
1880 'text' => esc_html__('I encountered an error while searching. Please try again.', 'mxchat'),
1881 'html' => ''
1882 );
1883 }
1884
1885 $results = json_decode(wp_remote_retrieve_body($response), true);
1886
1887 if (json_last_error() !== JSON_ERROR_NONE) {
1888 return array(
1889 'text' => esc_html__('I received an invalid response from the search service.', 'mxchat'),
1890 'html' => ''
1891 );
1892 }
1893
1894 // Cache results for one hour
1895 set_transient($transient_key, $results, HOUR_IN_SECONDS);
1896 }
1897
1898 // Process results
1899 if (!empty($results['web']['results']) && is_array($results['web']['results'])) {
1900 // Create a more straightforward summary with HTML links
1901 $search_results_text = '';
1902
1903 // Add a simple intro
1904 $search_results_text .= sprintf(
1905 esc_html__("Here's what I found about '%s':", 'mxchat'),
1906 esc_html($refined_search_query)
1907 );
1908
1909 // Add the top results with HTML links
1910 foreach (array_slice($results['web']['results'], 0, 5) as $result) {
1911 $title = isset($result['title']) ? wp_strip_all_tags($result['title']) : '';
1912 $url = isset($result['url']) ? esc_url($result['url']) : '';
1913 $description = isset($result['description']) ? wp_strip_all_tags($result['description']) : '';
1914
1915 // Add a line break after the intro
1916 $search_results_text .= '<br><br>';
1917
1918 // Add title as a link
1919 $search_results_text .= sprintf(
1920 '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a><br>',
1921 $url,
1922 $title
1923 );
1924
1925 // Add a condensed description
1926 $search_results_text .= sprintf("%s", $description);
1927 }
1928
1929 // Save to chat history
1930 $this->mxchat_save_chat_message($session_id, 'bot', $search_results_text);
1931
1932 // Return the formatted text with embedded HTML links
1933 return array(
1934 'text' => $search_results_text,
1935 'html' => ''
1936 );
1937 } else {
1938 return array(
1939 'text' => sprintf(
1940 esc_html__('I searched for "%s" but couldn\'t find any relevant results. Would you like to try different search terms?', 'mxchat'),
1941 esc_html($refined_search_query)
1942 ),
1943 'html' => ''
1944 );
1945 }
1946 }
1947
1948 //very good
1949 /**
1950 * Handle image search requests from the chatbot
1951 *
1952 * @param string $message The user's search query
1953 * @param int $user_id The user's ID
1954 * @param string $session_id The chat session ID
1955 * @return array Response array with text and HTML content
1956 */
1957 public function mxchat_handle_image_search_request($message, $user_id, $session_id) {
1958 // Step 1: Interpret the search query using the user's selected AI model
1959 $refined_search_query = $this->mxchat_interpret_search_query($message);
1960
1961 // If no query was interpreted, return a fallback message
1962 if (empty($refined_search_query)) {
1963 return array(
1964 'text' => __("I'm sorry, I couldn't interpret your search query. Please specify what you'd like to see images of.", 'mxchat'),
1965 'html' => "",
1966 );
1967 }
1968
1969 // Brave API URL
1970 $api_url = 'https://api.search.brave.com/res/v1/images/search';
1971
1972 // Retrieve Brave API settings
1973 $options = get_option('mxchat_options');
1974 $api_key = isset($options['brave_api_key']) ? sanitize_text_field($options['brave_api_key']) : '';
1975
1976 if (empty($api_key)) {
1977 return array(
1978 'text' => __("API key is not configured. Please set it in the Brave Search Settings.", 'mxchat'),
1979 'html' => "",
1980 );
1981 }
1982
1983 $image_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
1984 $safe_search = isset($options['brave_safe_search']) ? sanitize_text_field($options['brave_safe_search']) : 'strict';
1985
1986 // Append query parameters based on settings
1987 $api_url = add_query_arg([
1988 'q' => rawurlencode($refined_search_query),
1989 'count' => $image_count,
1990 'safesearch' => $safe_search,
1991 ], $api_url);
1992
1993 // Implement caching
1994 $transient_key = 'mxchat_image_search_' . md5($refined_search_query);
1995 $body = get_transient($transient_key);
1996
1997 if (false === $body) {
1998 $args = [
1999 'headers' => [
2000 'Accept' => 'application/json',
2001 'Accept-Encoding' => 'gzip',
2002 'X-Subscription-Token' => $api_key,
2003 ],
2004 'timeout' => 10,
2005 ];
2006
2007 $response = wp_remote_get($api_url, $args);
2008
2009 if (is_wp_error($response)) {
2010 return array(
2011 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
2012 'html' => "",
2013 );
2014 }
2015
2016 $body = json_decode(wp_remote_retrieve_body($response), true);
2017 set_transient($transient_key, $body, HOUR_IN_SECONDS);
2018 }
2019
2020 // Process the API response
2021 if (isset($body['results']) && is_array($body['results']) && count($body['results']) > 0) {
2022 $html_output = '<div class="mxchat-image-gallery">';
2023
2024 // Get the configured image count (1-6)
2025 $display_count = isset($options['brave_image_count']) ? intval($options['brave_image_count']) : 4;
2026 $display_count = min($display_count, count($body['results'])); // Make sure we don't exceed available images
2027
2028 // Use only the requested number of images
2029 for ($i = 0; $i < $display_count; $i++) {
2030 $image = $body['results'][$i];
2031 $image_url = isset($image['url']) ? esc_url($image['url']) : '';
2032 $thumbnail_url = isset($image['thumbnail']['src']) ? esc_url($image['thumbnail']['src']) : '';
2033 $title = isset($image['title']) ? esc_html($image['title']) : esc_html__('Image', 'mxchat');
2034
2035 if ($image_url && $thumbnail_url) {
2036 $html_output .= '<div class="mxchat-image-item">';
2037 $html_output .= '<strong class="mxchat-image-title">' . $title . '</strong>';
2038 $html_output .= '<a href="' . $image_url . '" target="_blank" rel="noopener noreferrer" class="mxchat-image-link">';
2039 $html_output .= '<img src="' . $thumbnail_url . '" alt="' . $title . '" class="mxchat-image-thumbnail">';
2040 $html_output .= '</a></div>';
2041 }
2042 }
2043
2044 $html_output .= '</div>';
2045
2046 // Create response text
2047 $response_text = sprintf(__("Here are some images of %s:", 'mxchat'), $refined_search_query);
2048
2049 // Save both response text and HTML to chat history
2050 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2051 $this->mxchat_save_chat_message($session_id, 'bot', $html_output);
2052
2053 // Return the combined response
2054 return array(
2055 'text' => $response_text,
2056 'html' => $html_output,
2057 );
2058 } else {
2059 $response_text = __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat');
2060
2061 // Save the error message to chat history
2062 $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2063
2064 return array(
2065 'text' => $response_text,
2066 'html' => "",
2067 );
2068 }
2069 }
2070
2071 /**
2072 * Interpret the search query using the user's selected AI model
2073 *
2074 * @param string $user_query The original query from the user
2075 * @return string The refined search query
2076 */
2077 public function mxchat_interpret_search_query($user_query) {
2078 $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');
2079
2080 // Get options and determine the selected model
2081 $options = $this->options ?? get_option('mxchat_options');
2082 $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
2083
2084 // Extract model prefix to determine the provider
2085 $model_parts = explode('-', $selected_model);
2086 $provider = strtolower($model_parts[0]);
2087
2088 // Determine which API key to use based on the provider
2089 switch ($provider) {
2090 case 'gemini':
2091 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
2092 if (empty($api_key)) {
2093 return sanitize_text_field($user_query); // Default to original query if API key missing
2094 }
2095 return $this->interpret_query_with_gemini($user_query, $system_prompt, $api_key, $selected_model);
2096
2097 case 'claude':
2098 $api_key = isset($options['claude_api_key']) ? sanitize_text_field($options['claude_api_key']) : '';
2099 if (empty($api_key)) {
2100 return sanitize_text_field($user_query);
2101 }
2102 return $this->interpret_query_with_claude($user_query, $system_prompt, $api_key, $selected_model);
2103
2104 case 'grok':
2105 $api_key = isset($options['xai_api_key']) ? sanitize_text_field($options['xai_api_key']) : '';
2106 if (empty($api_key)) {
2107 return sanitize_text_field($user_query);
2108 }
2109 return $this->interpret_query_with_xai($user_query, $system_prompt, $api_key, $selected_model);
2110
2111 case 'deepseek':
2112 $api_key = isset($options['deepseek_api_key']) ? sanitize_text_field($options['deepseek_api_key']) : '';
2113 if (empty($api_key)) {
2114 return sanitize_text_field($user_query);
2115 }
2116 return $this->interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $selected_model);
2117
2118 case 'gpt':
2119 default:
2120 // Default to OpenAI for custom models or unrecognized prefixes
2121 $api_key = isset($options['api_key']) ? sanitize_text_field($options['api_key']) : '';
2122 if (empty($api_key)) {
2123 return sanitize_text_field($user_query);
2124 }
2125 return $this->interpret_query_with_openai($user_query, $system_prompt, $api_key, $selected_model);
2126 }
2127 }
2128
2129 /**
2130 * Interpret query using OpenAI models
2131 */
2132 private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
2133 $url = 'https://api.openai.com/v1/chat/completions';
2134 $args = [
2135 'headers' => [
2136 'Authorization' => 'Bearer ' . $api_key,
2137 'Content-Type' => 'application/json',
2138 ],
2139 'body' => wp_json_encode([
2140 'model' => $model,
2141 'messages' => [
2142 ['role' => 'system', 'content' => $system_prompt],
2143 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2144 ],
2145 'temperature' => 0.2,
2146 'max_tokens' => 20,
2147 ]),
2148 'method' => 'POST',
2149 'timeout' => 15,
2150 ];
2151
2152 $response = wp_remote_post($url, $args);
2153 if (is_wp_error($response)) {
2154 return sanitize_text_field($user_query);
2155 }
2156
2157 $body = json_decode(wp_remote_retrieve_body($response), true);
2158 return isset($body['choices'][0]['message']['content'])
2159 ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
2160 : sanitize_text_field($user_query);
2161 }
2162
2163 /**
2164 * Interpret query using Claude models
2165 */
2166 private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
2167 $url = 'https://api.anthropic.com/v1/messages';
2168
2169 $args = [
2170 'headers' => [
2171 'Content-Type' => 'application/json',
2172 'x-api-key' => $api_key,
2173 'anthropic-version' => '2023-06-01',
2174 ],
2175 'body' => wp_json_encode([
2176 'model' => $model,
2177 'system' => $system_prompt,
2178 'messages' => [
2179 ['role' => 'user', 'content' => sanitize_text_field($user_query)]
2180 ],
2181 'max_tokens' => 20,
2182 'temperature' => 0.2,
2183 ]),
2184 'method' => 'POST',
2185 'timeout' => 15,
2186 ];
2187
2188 $response = wp_remote_post($url, $args);
2189 if (is_wp_error($response)) {
2190 return sanitize_text_field($user_query);
2191 }
2192
2193 $body = json_decode(wp_remote_retrieve_body($response), true);
2194 if (!empty($body['content'][0]['text'])) {
2195 return sanitize_text_field(trim($body['content'][0]['text']));
2196 }
2197
2198 return sanitize_text_field($user_query);
2199 }
2200
2201 /**
2202 * Interpret query using Gemini models
2203 */
2204 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
2205 // Strip "gemini-" prefix for the API
2206 $model_version = str_replace('gemini-', '', $model);
2207
2208 $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2209
2210 $args = [
2211 'headers' => [
2212 'Content-Type' => 'application/json',
2213 ],
2214 'body' => wp_json_encode([
2215 'contents' => [
2216 [
2217 'role' => 'user',
2218 'parts' => [
2219 ['text' => $system_prompt . "\n\nQuery: " . sanitize_text_field($user_query)]
2220 ]
2221 ]
2222 ],
2223 'generationConfig' => [
2224 'temperature' => 0.2,
2225 'maxOutputTokens' => 20,
2226 ],
2227 ]),
2228 'method' => 'POST',
2229 'timeout' => 15,
2230 ];
2231
2232 $response = wp_remote_post($url, $args);
2233 if (is_wp_error($response)) {
2234 return sanitize_text_field($user_query);
2235 }
2236
2237 $body = json_decode(wp_remote_retrieve_body($response), true);
2238 if (!empty($body['candidates'][0]['content']['parts'][0]['text'])) {
2239 return sanitize_text_field(trim($body['candidates'][0]['content']['parts'][0]['text']));
2240 }
2241
2242 return sanitize_text_field($user_query);
2243 }
2244
2245 /**
2246 * Interpret query using X.AI (Grok) models
2247 */
2248 private function interpret_query_with_xai($user_query, $system_prompt, $api_key, $model) {
2249 $url = 'https://api.xai.com/v1/chat/completions';
2250
2251 $args = [
2252 'headers' => [
2253 'Content-Type' => 'application/json',
2254 'Authorization' => 'Bearer ' . $api_key,
2255 ],
2256 'body' => wp_json_encode([
2257 'model' => $model,
2258 'messages' => [
2259 ['role' => 'system', 'content' => $system_prompt],
2260 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2261 ],
2262 'temperature' => 0.2,
2263 'max_tokens' => 20,
2264 ]),
2265 'method' => 'POST',
2266 'timeout' => 15,
2267 ];
2268
2269 $response = wp_remote_post($url, $args);
2270 if (is_wp_error($response)) {
2271 return sanitize_text_field($user_query);
2272 }
2273
2274 $body = json_decode(wp_remote_retrieve_body($response), true);
2275 if (isset($body['choices'][0]['message']['content'])) {
2276 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
2277 }
2278
2279 return sanitize_text_field($user_query);
2280 }
2281
2282 /**
2283 * Interpret query using DeepSeek models
2284 */
2285 private function interpret_query_with_deepseek($user_query, $system_prompt, $api_key, $model) {
2286 $url = 'https://api.deepseek.com/v1/chat/completions';
2287
2288 $args = [
2289 'headers' => [
2290 'Content-Type' => 'application/json',
2291 'Authorization' => 'Bearer ' . $api_key,
2292 ],
2293 'body' => wp_json_encode([
2294 'model' => $model,
2295 'messages' => [
2296 ['role' => 'system', 'content' => $system_prompt],
2297 ['role' => 'user', 'content' => sanitize_text_field($user_query)],
2298 ],
2299 'temperature' => 0.2,
2300 'max_tokens' => 20,
2301 ]),
2302 'method' => 'POST',
2303 'timeout' => 15,
2304 ];
2305
2306 $response = wp_remote_post($url, $args);
2307 if (is_wp_error($response)) {
2308 return sanitize_text_field($user_query);
2309 }
2310
2311 $body = json_decode(wp_remote_retrieve_body($response), true);
2312 if (isset($body['choices'][0]['message']['content'])) {
2313 return sanitize_text_field(trim($body['choices'][0]['message']['content']));
2314 }
2315
2316 return sanitize_text_field($user_query);
2317 }
2318
2319 //very good
2320 private function add_email_to_loops($email) {
2321 // Sanitize the email
2322 $email = sanitize_email($email);
2323
2324 // Retrieve and sanitize options
2325 $api_key = isset($this->options['loops_api_key']) ? sanitize_text_field($this->options['loops_api_key']) : '';
2326 $mailing_list_id = isset($this->options['loops_mailing_list']) ? sanitize_text_field($this->options['loops_mailing_list']) : '';
2327
2328 // Check for missing API key or mailing list ID
2329 if (empty($api_key) || empty($mailing_list_id)) {
2330 //error_log(esc_html__('Loops API key or mailing list ID is missing.', 'mxchat'));
2331 return;
2332 }
2333
2334 $data = array(
2335 'email' => $email,
2336 'subscribed' => true,
2337 'source' => __('MxChat AI Chatbot', 'mxchat'),
2338 'mailingLists' => array($mailing_list_id => true),
2339 );
2340
2341 $url = 'https://app.loops.so/api/v1/contacts/create';
2342 $args = array(
2343 'body' => wp_json_encode($data),
2344 'headers' => array(
2345 'Authorization' => 'Bearer ' . $api_key,
2346 'Content-Type' => 'application/json',
2347 ),
2348 'method' => 'POST',
2349 'timeout' => 45,
2350 );
2351
2352 $response = wp_remote_post($url, $args);
2353
2354 // Handle errors in the API request
2355 if (is_wp_error($response)) {
2356 //error_log(esc_html__('Error adding email to Loops: ', 'mxchat') . $response->get_error_message());
2357 return;
2358 }
2359
2360 // Check for non-200 HTTP responses
2361 $response_code = wp_remote_retrieve_response_code($response);
2362 if ($response_code != 200) {
2363 $response_body = wp_remote_retrieve_body($response);
2364 //error_log(esc_html__('Loops API responded with code ', 'mxchat') . $response_code . ': ' . $response_body);
2365 }
2366 }
2367
2368 public function mxchat_handle_pdf_discussion($message, $user_id, $session_id) {
2369 // Get the maximum number of pages allowed from admin settings
2370 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2371
2372 // Retrieve options for dynamic texts
2373 $trigger_text = $this->options['pdf_intent_trigger_text'] ?? __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat');
2374 $success_text = $this->options['pdf_intent_success_text'] ?? __("I've processed the PDF. What questions do you have about it?", 'mxchat');
2375 $error_text = $this->options['pdf_intent_error_text'] ?? __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
2376
2377 // Check for explicit request for new PDF
2378 $new_pdf_requested = stripos($message, 'new') !== false ||
2379 stripos($message, 'another') !== false ||
2380 stripos($message, 'different') !== false;
2381
2382 // If user mentions adding/reading a PDF, set waiting flag
2383 if (stripos($message, 'pdf') !== false ||
2384 stripos($message, 'document') !== false ||
2385 stripos($message, 'read') !== false) {
2386 set_transient('mxchat_waiting_for_pdf_url_' . $session_id, true, HOUR_IN_SECONDS);
2387 $this->fallbackResponse['text'] = $trigger_text;
2388 return;
2389 }
2390
2391 // If we're waiting for a URL or user requested new PDF
2392 if ($new_pdf_requested || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
2393 if (preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
2394 // Process URL... (rest of your existing URL processing code)
2395 } else {
2396 $this->fallbackResponse['text'] = $trigger_text;
2397 }
2398 return;
2399 }
2400
2401 // Default to proceeding with conversation if no specific PDF action is needed
2402 $this->fallbackResponse['text'] = '';
2403 }
2404
2405
2406 /**
2407 * Enhanced fetch_and_split_pdf_pages with detailed debugging
2408 */
2409 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
2410 // CLEAR DEBUG LOGGING
2411 //error_log("=== MXCHAT PDF PROCESSING START ===");
2412 //error_log("PDF Source: " . $pdf_source);
2413 //error_log("Max Pages: " . $max_pages);
2414 //error_log("Session ID: " . ($this->session_id ?? 'not set'));
2415
2416 // Check if Advanced Claude Toolbar is available and enabled
2417 $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
2418 $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
2419
2420 //error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2421 //error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2422
2423 if ($claude_available && $claude_enabled) {
2424 //error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2425
2426 // Attempt Claude processing first
2427 $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
2428
2429 if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
2430 //error_log("�
2431 CLAUDE PROCESSING SUCCESSFUL!");
2432 //error_log("Claude returned " . count($claude_result) . " processed pages");
2433
2434 // Log first page details for verification
2435 if (isset($claude_result[0])) {
2436 $first_page = $claude_result[0];
2437 //error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2438 //error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2439 //error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2440 }
2441
2442 //error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2443 return $claude_result;
2444 } else {
2445 //error_log(" CLAUDE PROCESSING FAILED or returned invalid result");
2446 //error_log("Claude result type: " . gettype($claude_result));
2447 if (is_array($claude_result)) {
2448 //error_log("Claude result count: " . count($claude_result));
2449 }
2450 }
2451 }
2452
2453 // Fallback to basic processing
2454 //error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2455
2456 $upload_dir = wp_upload_dir();
2457 $temp_file = null;
2458
2459 try {
2460 // Your existing basic processing code here...
2461 // (I'll include the key parts with debug logging)
2462
2463 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
2464 //error_log("Downloading PDF from URL...");
2465 $temp_file = wp_tempnam($pdf_source);
2466 $response = wp_remote_get($pdf_source, [
2467 'timeout' => 60,
2468 'headers' => ['User-Agent' => 'MxChat PDF Processor']
2469 ]);
2470
2471 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
2472 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
2473 //error_log(" BASIC PROCESSING: Failed to download PDF: " . $error_message);
2474 return false;
2475 }
2476
2477 file_put_contents($temp_file, wp_remote_retrieve_body($response));
2478 //error_log("�
2479 PDF downloaded successfully");
2480 } else {
2481 $temp_file = $pdf_source;
2482 //error_log("Using local PDF file: " . $temp_file);
2483 }
2484
2485 // Parse PDF
2486 //error_log("Parsing PDF with basic parser...");
2487 $parser = new \Smalot\PdfParser\Parser();
2488 $pdf = $parser->parseFile($temp_file);
2489 $pages = $pdf->getPages();
2490
2491 //error_log("PDF contains " . count($pages) . " pages");
2492
2493 if (count($pages) > $max_pages) {
2494 //error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2495 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
2496 unlink($temp_file);
2497 }
2498 return 'too_many_pages';
2499 }
2500
2501 $embeddings = [];
2502 $processed_pages = 0;
2503
2504 foreach ($pages as $page_number => $page) {
2505 $text = $page->getText();
2506
2507 if (empty(trim($text))) {
2508 //error_log("Skipping empty page: " . ($page_number + 1));
2509 continue;
2510 }
2511
2512 $text = $this->mxchat_clean_text($text);
2513
2514 $embedding = $this->mxchat_generate_embedding(
2515 __("Page ", 'mxchat') . ($page_number + 1) . ": " . $text,
2516 $this->options['api_key']
2517 );
2518
2519 if ($embedding) {
2520 $embeddings[] = [
2521 'page_number' => $page_number + 1,
2522 'embedding' => $embedding,
2523 'text' => $text,
2524 'enhanced' => false, // CLEARLY MARK AS BASIC
2525 'processing_method' => 'basic_pdf_parser'
2526 ];
2527 $processed_pages++;
2528 }
2529 }
2530
2531 //error_log("�
2532 BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2533
2534 // Cleanup
2535 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2536 unlink($temp_file);
2537 }
2538
2539 //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
2540 return $embeddings;
2541
2542 } catch (\Exception $e) {
2543 //error_log(" BASIC PROCESSING ERROR: " . $e->getMessage());
2544 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
2545 unlink($temp_file);
2546 }
2547 //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
2548 return false;
2549 }
2550 }
2551
2552 private function mxchat_clean_text($text) {
2553 // Remove excessive whitespace
2554 $text = preg_replace('/\s+/', ' ', $text);
2555
2556 // Remove control characters except newlines and tabs
2557 $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
2558
2559 // Normalize line endings
2560 $text = str_replace(["\r\n", "\r"], "\n", $text);
2561
2562 // Trim whitespace
2563 $text = trim($text);
2564
2565 return $text;
2566 }
2567
2568 private function find_relevant_pdf_pages($query_embedding, $embeddings) {
2569 //error_log(esc_html__("find_relevant_pdf_pages called.", 'mxchat'));
2570
2571 $most_relevant = null;
2572 $highest_similarity = -INF;
2573
2574 foreach ($embeddings as $page_data) {
2575 $similarity = $this->mxchat_calculate_cosine_similarity($query_embedding, $page_data['embedding']);
2576
2577 if ($similarity > $highest_similarity) {
2578 $highest_similarity = $similarity;
2579 $most_relevant = $page_data['page_number'];
2580 }
2581 }
2582
2583 if (!is_null($most_relevant)) {
2584 $page_numbers = range(max(1, $most_relevant - 1), min(count($embeddings), $most_relevant + 1));
2585 return array_filter($embeddings, function ($page) use ($page_numbers) {
2586 return in_array($page['page_number'], $page_numbers);
2587 });
2588 }
2589
2590 return [];
2591 }
2592 // Add this to your class
2593 public function handle_pdf_upload() {
2594 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2595
2596 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
2597 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
2598 return;
2599 }
2600
2601 $file = $_FILES['pdf_file'];
2602 $session_id = sanitize_text_field($_POST['session_id']);
2603 $original_filename = sanitize_text_field($file['name']);
2604
2605 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
2606 if ($file_type['type'] !== 'application/pdf') {
2607 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
2608 return;
2609 }
2610
2611 $upload_dir = wp_upload_dir();
2612 $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
2613 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
2614
2615 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
2616 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
2617 return;
2618 }
2619
2620 $this->clear_pdf_transients($session_id);
2621
2622 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69;
2623 $embeddings = $this->fetch_and_split_pdf_pages($pdf_path, $max_pages);
2624
2625 if ($embeddings === 'too_many_pages') {
2626 unlink($pdf_path);
2627 $error_message = sprintf(
2628 $this->options['pdf_intent_error_text'] ??
2629 esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
2630 $max_pages
2631 );
2632 wp_send_json_error($error_message);
2633 return;
2634 }
2635
2636 if ($embeddings === false || empty($embeddings)) {
2637 unlink($pdf_path);
2638 $error_message = $this->options['pdf_intent_error_text'] ??
2639 esc_html__('The uploaded PDF appears to be empty or contains unsupported content.', 'mxchat');
2640 wp_send_json_error($error_message);
2641 return;
2642 }
2643
2644 if (!empty($embeddings)) {
2645 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
2646 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
2647 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
2648 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
2649
2650 $success_message = $this->options['pdf_intent_success_text'] ??
2651 esc_html__("I've processed the PDF. What questions do you have about it?", 'mxchat');
2652
2653 wp_send_json_success([
2654 'message' => $success_message,
2655 'filename' => $original_filename
2656 ]);
2657 return;
2658 }
2659
2660 unlink($pdf_path);
2661 $error_message = $this->options['pdf_intent_error_text'] ??
2662 esc_html__('Sorry, I couldn\'t process the PDF. Please ensure it\'s a valid file.', 'mxchat');
2663 wp_send_json_error($error_message);
2664 return;
2665 }
2666 public function handle_pdf_remove() {
2667 check_ajax_referer('mxchat_chat_nonce', 'nonce');
2668
2669 if (empty($_POST['session_id'])) {
2670 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
2671 wp_die();
2672 }
2673
2674 $session_id = sanitize_text_field($_POST['session_id']);
2675 $pdf_path = get_transient('mxchat_pdf_url_' . $session_id);
2676
2677 if ($pdf_path && file_exists($pdf_path)) {
2678 unlink($pdf_path);
2679 }
2680
2681 $this->clear_pdf_transients($session_id);
2682
2683 wp_send_json_success([
2684 'message' => esc_html__('PDF removed successfully.', 'mxchat')
2685 ]);
2686 wp_die();
2687 }
2688
2689
2690
2691
2692 function mxchat_fetch_new_messages() {
2693 $session_id = sanitize_text_field($_POST['session_id']);
2694 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
2695 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
2696 $initial_timestamp = isset($_POST['initial_timestamp']) ? intval($_POST['initial_timestamp']) : 0;
2697
2698 if (empty($session_id)) {
2699 //error_log(esc_html__('Fetch new messages error: Session ID missing.', 'mxchat'));
2700 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
2701 wp_die();
2702 }
2703
2704 $history = get_option("mxchat_history_{$session_id}", []);
2705
2706 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
2707 // If persistence is enabled, show all new messages
2708 if ($persistence_enabled) {
2709 return !empty($message['id']) &&
2710 strcmp($message['id'], $last_seen_id) > 0 &&
2711 $message['role'] === 'agent';
2712 }
2713
2714 // If persistence is disabled, only show messages after initial timestamp
2715 return !empty($message['id']) &&
2716 $message['role'] === 'agent' &&
2717 $message['timestamp'] > $initial_timestamp;
2718 });
2719
2720 //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
2721
2722 wp_send_json_success([
2723 'new_messages' => array_values($new_messages)
2724 ]);
2725 wp_die();
2726 }
2727 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
2728 // First check if live agents are available
2729 $live_agent_available = $this->options['live_agent_status'] ?? 'off';
2730 if ($live_agent_available !== 'on') {
2731 $away_message = $this->options['live_agent_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
2732 $this->fallbackResponse = [
2733 'text' => $away_message,
2734 'html' => '',
2735 'images' => [],
2736 'chat_mode' => 'ai'
2737 ];
2738 wp_send_json([
2739 'text' => $away_message,
2740 'html' => '',
2741 'chat_mode' => 'ai',
2742 'session_id' => $session_id
2743 ]);
2744 wp_die();
2745 }
2746
2747 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
2748
2749 if (empty($slack_bot_token)) {
2750 return false;
2751 }
2752
2753 // Check if channel already exists for this session
2754 $channel_id = get_option("mxchat_channel_{$session_id}", '');
2755
2756 if (empty($channel_id)) {
2757 // Create new channel with session ID as name
2758 $channel_name = $this->generate_channel_name($session_id);
2759
2760 //error_log("Attempting to create channel: $channel_name");
2761
2762 $response = wp_remote_post('https://slack.com/api/conversations.create', [
2763 'headers' => [
2764 'Content-Type' => 'application/json',
2765 'Authorization' => 'Bearer ' . $slack_bot_token
2766 ],
2767 'body' => json_encode([
2768 'name' => $channel_name,
2769 'is_private' => false // Public channel - anyone in workspace can join
2770 ])
2771 ]);
2772
2773 if (!is_wp_error($response)) {
2774 $response_body = wp_remote_retrieve_body($response);
2775 $response_data = json_decode($response_body, true);
2776
2777 //error_log("Channel creation response: " . $response_body);
2778
2779 if (isset($response_data['ok']) && $response_data['ok']) {
2780 $channel_id = $response_data['channel']['id'];
2781 $actual_channel_name = $response_data['channel']['name'] ?? 'unknown';
2782 //error_log("Channel created successfully: ID=$channel_id, Name=$actual_channel_name");
2783 update_option("mxchat_channel_{$session_id}", $channel_id);
2784
2785 // Auto-invite agents to the channel
2786 $agent_user_ids = $this->options['live_agent_user_ids'] ?? '';
2787
2788 if (!empty($agent_user_ids)) {
2789 // Parse user IDs (one per line)
2790 $user_ids = array_filter(array_map('trim', explode("\n", $agent_user_ids)));
2791
2792 foreach ($user_ids as $user_id_to_invite) {
2793 //error_log("Inviting user to channel: $user_id_to_invite");
2794
2795 $invite_response = wp_remote_post('https://slack.com/api/conversations.invite', [
2796 'headers' => [
2797 'Content-Type' => 'application/json',
2798 'Authorization' => 'Bearer ' . $slack_bot_token
2799 ],
2800 'body' => json_encode([
2801 'channel' => $channel_id,
2802 'users' => $user_id_to_invite
2803 ])
2804 ]);
2805
2806 if (!is_wp_error($invite_response)) {
2807 $invite_body = wp_remote_retrieve_body($invite_response);
2808 $invite_data = json_decode($invite_body, true);
2809 //error_log("Invite response for $user_id_to_invite: " . $invite_body);
2810
2811 if (isset($invite_data['ok']) && $invite_data['ok']) {
2812 //error_log("Successfully invited user $user_id_to_invite to channel");
2813 } else {
2814 //error_log("Failed to invite user $user_id_to_invite: " . ($invite_data['error'] ?? 'Unknown error'));
2815 }
2816 } else {
2817 //error_log("WP Error inviting user $user_id_to_invite: " . $invite_response->get_error_message());
2818 }
2819 }
2820 } else {
2821 //error_log("No agent user IDs configured for auto-invite");
2822 }
2823 } else {
2824 //error_log("Channel creation failed: " . ($response_data['error'] ?? 'Unknown error'));
2825 }
2826 } else {
2827 //error_log("WP Error creating channel: " . $response->get_error_message());
2828 }
2829
2830 if (empty($channel_id)) {
2831 return false; // Failed to create channel
2832 }
2833 }
2834
2835 // Get recent chat history
2836 $history = get_option("mxchat_history_{$session_id}", []);
2837 $recent_history = array_slice($history, -5);
2838
2839 // Format conversation context
2840 $conversation_context = "";
2841 if (!empty($recent_history)) {
2842 $conversation_context = "*Recent Conversation:*\n";
2843 foreach ($recent_history as $hist_message) {
2844 $role_display = $hist_message['role'] === 'user' ? 'User' : 'AI';
2845 $conversation_context .= ">{$role_display}: {$hist_message['content']}\n";
2846 }
2847 $conversation_context .= "\n";
2848 }
2849
2850 update_option("mxchat_mode_{$session_id}", 'agent');
2851
2852 // Send message to channel
2853 $channel_message = "🔔 *New Live Agent Request*\n\n";
2854 $channel_message .= "*Session ID:* `{$session_id}`\n";
2855 $channel_message .= "*User ID:* `{$user_id}`\n\n";
2856
2857 if (!empty($conversation_context)) {
2858 $channel_message .= $conversation_context;
2859 }
2860
2861 $channel_message .= "*Current Message:*\n{$message}\n\n";
2862 $channel_message .= "_Reply directly in this channel - all messages will go to the user_";
2863
2864 wp_remote_post('https://slack.com/api/chat.postMessage', [
2865 'headers' => [
2866 'Content-Type' => 'application/json',
2867 'Authorization' => 'Bearer ' . $slack_bot_token
2868 ],
2869 'body' => json_encode([
2870 'channel' => $channel_id,
2871 'text' => $channel_message,
2872 'mrkdwn' => true
2873 ])
2874 ]);
2875
2876 $success_message = $this->options['live_agent_notification_message'] ?? 'Live agent has been notified.';
2877 $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
2878
2879 $this->fallbackResponse = [
2880 'text' => $success_message,
2881 'html' => '',
2882 'images' => [],
2883 'chat_mode' => 'agent'
2884 ];
2885
2886 wp_send_json([
2887 'success' => true,
2888 'text' => $success_message,
2889 'html' => '',
2890 'chat_mode' => 'agent',
2891 'session_id' => $session_id,
2892 'fallbackResponse' => $this->fallbackResponse
2893 ]);
2894 wp_die();
2895 }
2896
2897 private function generate_channel_name($session_id) {
2898 $email = null;
2899 $name = null;
2900
2901 // 1. First priority: Check if user is logged in and get their info
2902 if (is_user_logged_in()) {
2903 $current_user = wp_get_current_user();
2904 if (!empty($current_user->user_email)) {
2905 $email = $current_user->user_email;
2906 //error_log("[DEBUG] Using logged-in user email for channel: {$email}");
2907 }
2908 if (!empty($current_user->display_name)) {
2909 $name = $current_user->display_name;
2910 //error_log("[DEBUG] Using logged-in user name for channel: {$name}");
2911 }
2912 }
2913
2914 // 2. Second priority: Check for saved email/name from "require email to chat" option
2915 if (empty($email)) {
2916 $email_option_key = "mxchat_email_{$session_id}";
2917 $saved_email = get_option($email_option_key);
2918 if (!empty($saved_email)) {
2919 $email = $saved_email;
2920 //error_log("[DEBUG] Using saved email from session for channel: {$email}");
2921 }
2922 }
2923
2924 if (empty($name)) {
2925 $name_option_key = "mxchat_name_{$session_id}";
2926 $saved_name = get_option($name_option_key);
2927 if (!empty($saved_name)) {
2928 $name = $saved_name;
2929 //error_log("[DEBUG] Using saved name from session for channel: {$name}");
2930 }
2931 }
2932
2933 // 3. Third priority: Check existing chat transcript for email/name
2934 if (empty($email) || empty($name)) {
2935 global $wpdb;
2936 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
2937 $existing_data = $wpdb->get_row($wpdb->prepare(
2938 "SELECT user_email, user_name FROM $table_name WHERE session_id = %s AND (user_email IS NOT NULL OR user_name IS NOT NULL) LIMIT 1",
2939 $session_id
2940 ));
2941
2942 if ($existing_data) {
2943 if (empty($email) && !empty($existing_data->user_email)) {
2944 $email = $existing_data->user_email;
2945 //error_log("[DEBUG] Using email from chat transcript for channel: {$email}");
2946 }
2947 if (empty($name) && !empty($existing_data->user_name)) {
2948 $name = $existing_data->user_name;
2949 //error_log("[DEBUG] Using name from chat transcript for channel: {$name}");
2950 }
2951 }
2952 }
2953
2954 // 4. Generate channel name based on priority: Name > Email > Session ID
2955 $channel_name = '';
2956
2957 if (!empty($name)) {
2958 // Convert name to valid Slack channel name
2959 $base_name = strtolower(trim($name));
2960 // Replace spaces and invalid characters
2961 $base_name = preg_replace('/[^a-z0-9\s]/', '', $base_name);
2962 $base_name = preg_replace('/\s+/', '-', $base_name);
2963 $base_name = trim($base_name, '-');
2964
2965 // Get last 4 characters of session ID for uniqueness
2966 $session_suffix = substr($session_id, -4);
2967 $channel_name = 'chat-' . $base_name . '-' . strtolower($session_suffix);
2968
2969 // Slack channel names have a 21 character limit
2970 if (strlen($channel_name) > 21) {
2971 // Calculate available space for name (21 - 'chat-' - '-' - session_suffix)
2972 $available_space = 21 - 5 - 1 - strlen($session_suffix); // 'chat-' = 5, '-' = 1
2973 $truncated_name = substr($base_name, 0, $available_space);
2974 $truncated_name = rtrim($truncated_name, '-'); // Remove trailing hyphen
2975 $channel_name = 'chat-' . $truncated_name . '-' . strtolower($session_suffix);
2976 }
2977
2978 //error_log("[DEBUG] Using name for channel: {$channel_name} (from name: {$name})");
2979
2980 } elseif (!empty($email)) {
2981 // Convert email to valid Slack channel name (your existing logic)
2982 $channel_name = 'chat-' . strtolower(str_replace(['@', '.', '+', '_'], ['-at-', '-', '-plus-', '-'], $email));
2983 // Remove any remaining invalid characters
2984 $channel_name = preg_replace('/[^a-z0-9\-]/', '', $channel_name);
2985 // Ensure it doesn't end with a hyphen
2986 $channel_name = rtrim($channel_name, '-');
2987 // Slack channel names have a 21 character limit, so truncate if needed
2988 if (strlen($channel_name) > 21) {
2989 $channel_name = substr($channel_name, 0, 21);
2990 $channel_name = rtrim($channel_name, '-'); // Remove trailing hyphen if truncation created one
2991 }
2992
2993 //error_log("[DEBUG] Using email for channel: {$channel_name} (from email: {$email})");
2994
2995 } else {
2996 // Fallback to session ID if no name or email found
2997 $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
2998 //error_log("[DEBUG] No name or email found, using session ID for channel: {$channel_name}");
2999 }
3000
3001 // Final validation - ensure channel name meets Slack requirements
3002 if (strlen($channel_name) > 21) {
3003 $channel_name = substr($channel_name, 0, 21);
3004 $channel_name = rtrim($channel_name, '-');
3005 }
3006
3007 //error_log("[DEBUG] Generated channel name: {$channel_name}");
3008 return $channel_name;
3009 }
3010 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
3011 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3012 $channel_id = get_option("mxchat_channel_{$session_id}", '');
3013
3014 if (empty($slack_bot_token) || empty($channel_id)) {
3015 return false;
3016 }
3017
3018 $user_message = "💬 *User:* {$message}";
3019
3020 $response = wp_remote_post('https://slack.com/api/chat.postMessage', [
3021 'headers' => [
3022 'Content-Type' => 'application/json',
3023 'Authorization' => 'Bearer ' . $slack_bot_token
3024 ],
3025 'body' => json_encode([
3026 'channel' => $channel_id,
3027 'text' => $user_message,
3028 'mrkdwn' => true
3029 ])
3030 ]);
3031
3032 return !is_wp_error($response);
3033 }
3034 public function handle_slack_interaction(WP_REST_Request $request) {
3035 //error_log('Received Slack interaction');
3036
3037 $payload = json_decode($request->get_param('payload'), true);
3038 //error_log('Payload: ' . print_r($payload, true));
3039
3040 // Handle button click
3041 if ($payload['type'] === 'block_actions' && $payload['actions'][0]['action_id'] === 'reply_to_user') {
3042 $session_id = $payload['actions'][0]['value'];
3043 $trigger_id = $payload['trigger_id'];
3044
3045 // Get Bot Token from settings
3046 $slack_token = $this->options['live_agent_bot_token'] ?? '';
3047
3048 if (empty($slack_token)) {
3049 //error_log('Slack Bot Token not configured');
3050 return new WP_REST_Response(['error' => esc_html__('Bot token not configured', 'mxchat')], 400);
3051 }
3052 $response = wp_remote_post('https://slack.com/api/views.open', [
3053 'headers' => [
3054 'Content-Type' => 'application/json',
3055 'Authorization' => 'Bearer ' . $slack_token
3056 ],
3057 'body' => json_encode([
3058 'trigger_id' => $trigger_id,
3059 'view' => [
3060 'type' => 'modal',
3061 'callback_id' => 'reply_modal',
3062 'title' => [
3063 'type' => 'plain_text',
3064 'text' => __('Reply to User', 'mxchat')
3065 ],
3066 'submit' => [
3067 'type' => 'plain_text',
3068 'text' => __('Send', 'mxchat')
3069 ],
3070 'close' => [
3071 'type' => 'plain_text',
3072 'text' => __('Cancel', 'mxchat')
3073 ],
3074 'blocks' => [
3075 [
3076 'type' => 'input',
3077 'block_id' => 'reply_block',
3078 'label' => [
3079 'type' => 'plain_text',
3080 'text' => sprintf(__('Reply to session: %s', 'mxchat'), $session_id)
3081 ],
3082 'element' => [
3083 'type' => 'plain_text_input',
3084 'action_id' => 'message',
3085 'multiline' => true,
3086 'placeholder' => [
3087 'type' => 'plain_text',
3088 'text' => __('Type your message here...', 'mxchat')
3089 ]
3090 ]
3091 ]
3092 ],
3093 'private_metadata' => $session_id
3094 ]
3095 ])
3096 ]);
3097
3098 //error_log('Views.open response: ' . print_r($response, true));
3099
3100 // Return immediate acknowledgment
3101 return new WP_REST_Response(['ok' => true]);
3102 }
3103
3104 // Handle modal submission
3105 // Handle modal submission
3106 if ($payload['type'] === 'view_submission') {
3107 $session_id = $payload['view']['private_metadata'];
3108 $message = $payload['view']['state']['values']['reply_block']['message']['value'];
3109
3110 // Save the message (keep the message_id but don't include in response)
3111 $this->mxchat_save_chat_message($session_id, 'agent', $message);
3112
3113 // Keep the original response format for Slack
3114 return new WP_REST_Response([
3115 'response_action' => 'clear'
3116 ]);
3117 }
3118
3119 // Default acknowledgment
3120 return new WP_REST_Response(['ok' => true]);
3121 }
3122 public function mxchat_handle_agent_response(WP_REST_Request $request) {
3123 //error_log('Received agent response request');
3124 //error_log('Request data: ' . print_r($request->get_params(), true));
3125 // //error_log('Raw body: ' . file_get_contents('php://input'));
3126
3127 // Get the data from Slack's slash command format
3128 $command_text = $request->get_param('text');
3129 // //error_log('Command text: ' . $command_text);
3130
3131 if (empty($command_text)) {
3132 //error_log(esc_html__('Agent response error: No command text received', 'mxchat'));
3133 return new WP_REST_Response([
3134 'error' => esc_html__('Command text is required. Format: /reply session_id message', 'mxchat')
3135 ], 400);
3136 }
3137
3138 // Split the command text into session_id and message
3139 $parts = explode(' ', $command_text, 2);
3140 if (count($parts) !== 2) {
3141 //error_log('Agent response error: Invalid command format');
3142 return new WP_REST_Response([
3143 'error' => esc_html__('Invalid format. Use: /reply session_id message', 'mxchat')
3144 ], 400);
3145 }
3146
3147 $session_id = sanitize_text_field($parts[0]);
3148 $message = sanitize_text_field($parts[1]);
3149
3150 //error_log("Processing agent response - Session ID: $session_id, Message: $message");
3151
3152 // Save the message
3153 $message_id = $this->mxchat_save_chat_message($session_id, 'agent', $message);
3154
3155 if (!$message_id) {
3156 // //error_log('Failed to save agent message');
3157 return new WP_REST_Response([
3158 'error' => esc_html__('Failed to save message', 'mxchat')
3159 ], 500);
3160 }
3161
3162 // Return success response in Slack's expected format
3163 return new WP_REST_Response([
3164 'response_type' => 'in_channel',
3165 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
3166 ], 200);
3167 }
3168 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
3169 // Update mode to AI
3170 update_option("mxchat_mode_{$session_id}", 'ai');
3171
3172 // Clear any existing PDF context to start fresh
3173 $this->clear_pdf_transients($session_id);
3174
3175 // Set the response with explicit chat_mode
3176 $this->fallbackResponse = [
3177 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
3178 'html' => '',
3179 'images' => [],
3180 'chat_mode' => 'ai' // Ensure this is set
3181 ];
3182
3183 // Return the complete response array instead of just true
3184 return $this->fallbackResponse;
3185 }
3186
3187 public function handle_slack_messages(WP_REST_Request $request) {
3188 // Log the incoming request for debugging
3189 //error_log('Slack events request received: ' . $request->get_body());
3190
3191 $body = $request->get_body();
3192 $data = json_decode($body, true);
3193
3194 // Handle Slack URL verification
3195 if (isset($data['type']) && $data['type'] === 'url_verification') {
3196 //error_log('Slack URL verification challenge: ' . $data['challenge']);
3197 return new WP_REST_Response($data['challenge'], 200, ['Content-Type' => 'text/plain']);
3198 }
3199
3200 // IMPORTANT: Handle Slack's event deduplication
3201 if (isset($data['event_id'])) {
3202 $event_id = $data['event_id'];
3203 $processed_events = get_transient('mxchat_slack_events') ?: [];
3204
3205 // Check if we've already processed this event
3206 if (in_array($event_id, $processed_events)) {
3207 //error_log("Duplicate event detected: $event_id");
3208 return new WP_REST_Response(['ok' => true]);
3209 }
3210
3211 // Add this event to processed list
3212 $processed_events[] = $event_id;
3213 // Keep only last 100 events to prevent memory issues
3214 if (count($processed_events) > 100) {
3215 $processed_events = array_slice($processed_events, -100);
3216 }
3217 // Store for 1 hour
3218 set_transient('mxchat_slack_events', $processed_events, HOUR_IN_SECONDS);
3219 }
3220
3221 // Handle message events
3222 if (isset($data['event']) && $data['event']['type'] === 'message') {
3223 $event = $data['event'];
3224
3225 // Skip bot messages and messages with subtypes (like bot_message)
3226 if (isset($event['bot_id']) || isset($event['subtype'])) {
3227 return new WP_REST_Response(['ok' => true]);
3228 }
3229
3230 // Additional check: Skip if this is a threaded reply to our confirmation
3231 if (isset($event['thread_ts']) && $event['thread_ts'] !== $event['ts']) {
3232 return new WP_REST_Response(['ok' => true]);
3233 }
3234
3235 $channel_id = $event['channel'];
3236 $message_text = $event['text'] ?? '';
3237 $message_ts = $event['ts'] ?? '';
3238
3239 // Find session ID by looking for matching channel
3240 global $wpdb;
3241 $session_option = $wpdb->get_var(
3242 $wpdb->prepare(
3243 "SELECT option_name FROM {$wpdb->options}
3244 WHERE option_name LIKE 'mxchat_channel_%'
3245 AND option_value = %s",
3246 $channel_id
3247 )
3248 );
3249
3250 if ($session_option) {
3251 $session_id = str_replace('mxchat_channel_', '', $session_option);
3252
3253 // Create a unique key for this specific message
3254 $message_key = md5($session_id . $message_ts . $message_text);
3255 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
3256
3257 // Check if we've already processed this exact message
3258 if (in_array($message_key, $processed_messages)) {
3259 //error_log("Duplicate message detected for session $session_id");
3260 return new WP_REST_Response(['ok' => true]);
3261 }
3262
3263 // Add to processed messages
3264 $processed_messages[] = $message_key;
3265 // Keep only last 50 messages per session
3266 if (count($processed_messages) > 50) {
3267 $processed_messages = array_slice($processed_messages, -50);
3268 }
3269 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
3270
3271 // Save the agent message
3272 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
3273
3274 // Send confirmation back to Slack (only once)
3275 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
3276 if (!empty($slack_bot_token)) {
3277 // Use a transient to prevent duplicate confirmations
3278 $confirm_key = 'mxchat_confirm_' . $message_key;
3279 if (!get_transient($confirm_key)) {
3280 wp_remote_post('https://slack.com/api/chat.postMessage', [
3281 'headers' => [
3282 'Content-Type' => 'application/json',
3283 'Authorization' => 'Bearer ' . $slack_bot_token
3284 ],
3285 'body' => json_encode([
3286 'channel' => $channel_id,
3287 'text' => "✅ _Message sent to user_",
3288 'thread_ts' => $event['ts'] // Reply in thread
3289 ])
3290 ]);
3291 // Set transient to prevent duplicate confirmations
3292 set_transient($confirm_key, true, 300); // 5 minutes
3293 }
3294 }
3295 }
3296 }
3297
3298 return new WP_REST_Response(['ok' => true]);
3299 }
3300
3301 // For the word upload handler
3302 public function mxchat_handle_word_upload() {
3303 // Delegate to word handler
3304 $this->word_handler->mxchat_handle_word_upload();
3305 }
3306
3307 // For the word removal handler
3308 public function mxchat_handle_word_remove() {
3309 // Delegate to word handler
3310 $this->word_handler->mxchat_handle_word_remove();
3311 }
3312
3313 // For the word status check
3314 public function mxchat_check_word_status() {
3315 // Delegate to word handler
3316 $this->word_handler->mxchat_check_word_status();
3317 }
3318
3319
3320 private function mxchat_get_user_identifier() {
3321 return MxChat_User::mxchat_get_user_identifier();
3322 }
3323
3324 private function mxchat_generate_embedding($text, $api_key) {
3325 try {
3326 // Get options and selected model
3327 $options = get_option('mxchat_options');
3328 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
3329
3330 // Determine endpoint and API key based on model
3331 if (strpos($selected_model, 'voyage') === 0) {
3332 $endpoint = 'https://api.voyageai.com/v1/embeddings';
3333 $api_key = $options['voyage_api_key'] ?? '';
3334
3335 // Check if Voyage API key is missing
3336 if (empty($api_key)) {
3337 //error_log('Voyage API key is missing');
3338 return [
3339 'error' => esc_html__('Voyage AI API key is not configured', 'mxchat'),
3340 'error_code' => 'missing_voyage_api_key'
3341 ];
3342 }
3343 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
3344 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
3345 $api_key = $options['gemini_api_key'] ?? '';
3346
3347 // Check if Gemini API key is missing
3348 if (empty($api_key)) {
3349 //error_log('Gemini API key is missing');
3350 return [
3351 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
3352 'error_code' => 'missing_gemini_api_key'
3353 ];
3354 }
3355 } else {
3356 $endpoint = 'https://api.openai.com/v1/embeddings';
3357 // Use the passed API key for OpenAI
3358
3359 // Check if OpenAI API key is missing
3360 if (empty($api_key)) {
3361 //error_log('OpenAI API key is missing');
3362 return [
3363 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
3364 'error_code' => 'missing_openai_api_key'
3365 ];
3366 }
3367 }
3368
3369 // Check if text is empty
3370 if (empty($text)) {
3371 //error_log('Empty text provided for embedding generation');
3372 return [
3373 'error' => esc_html__('No text provided for embedding generation', 'mxchat'),
3374 'error_code' => 'empty_embedding_text'
3375 ];
3376 }
3377
3378 // Prepare request body based on provider
3379 if (strpos($selected_model, 'gemini-embedding') === 0) {
3380 // Gemini API format
3381 $request_body = [
3382 'model' => 'models/' . $selected_model,
3383 'content' => [
3384 'parts' => [
3385 ['text' => $text]
3386 ]
3387 ],
3388 'outputDimensionality' => 1536
3389 ];
3390
3391 // Prepare headers for Gemini (API key as query parameter)
3392 $endpoint .= '?key=' . $api_key;
3393 $headers = [
3394 'Content-Type' => 'application/json'
3395 ];
3396 } else {
3397 // OpenAI/Voyage API format
3398 $request_body = [
3399 'input' => $text,
3400 'model' => $selected_model
3401 ];
3402
3403 // Add output_dimension for voyage-3-large
3404 if ($selected_model === 'voyage-3-large') {
3405 $request_body['output_dimension'] = 2048;
3406 }
3407
3408 // Prepare headers for OpenAI/Voyage
3409 $headers = [
3410 'Content-Type' => 'application/json',
3411 'Authorization' => 'Bearer ' . $api_key
3412 ];
3413 }
3414
3415 // Prepare request arguments
3416 $args = [
3417 'body' => wp_json_encode($request_body),
3418 'headers' => $headers,
3419 'timeout' => 60,
3420 'redirection' => 5,
3421 'blocking' => true,
3422 'httpversion' => '1.0',
3423 'sslverify' => true,
3424 ];
3425
3426 // Make the request
3427 $response = wp_remote_post($endpoint, $args);
3428
3429 // Handle WordPress errors
3430 if (is_wp_error($response)) {
3431 $error_message = $response->get_error_message();
3432 //error_log('Embedding Generation Error: ' . $error_message);
3433 return [
3434 'error' => esc_html__('Connection error when generating embeddings: ', 'mxchat') . esc_html($error_message),
3435 'error_code' => 'embedding_connection_error'
3436 ];
3437 }
3438
3439 // Check HTTP status code
3440 $status_code = wp_remote_retrieve_response_code($response);
3441 if ($status_code !== 200) {
3442 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3443
3444 $error_message = isset($response_body['error']['message'])
3445 ? $response_body['error']['message']
3446 : 'HTTP Error ' . $status_code;
3447
3448 $error_type = isset($response_body['error']['type'])
3449 ? $response_body['error']['type']
3450 : 'unknown';
3451
3452 //error_log('Embedding API HTTP Error: ' . $status_code . ' - ' . $error_message);
3453
3454 // Handle specific error types
3455 switch ($error_type) {
3456 case 'invalid_request_error':
3457 if (strpos($error_message, 'API key') !== false) {
3458 return [
3459 'error' => esc_html__('Invalid API key for embedding generation. Please check your API key configuration.', 'mxchat'),
3460 'error_code' => 'embedding_invalid_api_key'
3461 ];
3462 }
3463 break;
3464
3465 case 'authentication_error':
3466 return [
3467 'error' => esc_html__('Authentication failed for embedding generation. Please check your API key.', 'mxchat'),
3468 'error_code' => 'embedding_auth_error'
3469 ];
3470
3471 case 'rate_limit_exceeded':
3472 return [
3473 'error' => esc_html__('Rate limit exceeded for embedding generation. Please try again later.', 'mxchat'),
3474 'error_code' => 'embedding_rate_limit'
3475 ];
3476
3477 case 'quota_exceeded':
3478 return [
3479 'error' => esc_html__('API quota exceeded for embedding generation. Please check your billing details.', 'mxchat'),
3480 'error_code' => 'embedding_quota_exceeded'
3481 ];
3482 }
3483
3484 // Generic error fallback
3485 return [
3486 'error' => esc_html__('Embedding API error - check embedding API key.: ', 'mxchat') . esc_html($error_message),
3487 'error_code' => 'embedding_api_error',
3488 'status_code' => $status_code
3489 ];
3490 }
3491
3492 $response_body = json_decode(wp_remote_retrieve_body($response), true);
3493
3494 // Handle different response formats based on provider
3495 if (strpos($selected_model, 'gemini-embedding') === 0) {
3496 // Gemini API response format
3497 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
3498 return $response_body['embedding']['values'];
3499 } else {
3500 //error_log('Invalid Gemini embedding response: ' . wp_json_encode($response_body));
3501 return [
3502 'error' => esc_html__('Received invalid embedding data from the Gemini API.', 'mxchat'),
3503 'error_code' => 'invalid_gemini_embedding_response'
3504 ];
3505 }
3506 } else {
3507 // OpenAI/Voyage API response format
3508 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
3509 return $response_body['data'][0]['embedding'];
3510 } else {
3511 //error_log('Invalid embedding response: ' . wp_json_encode($response_body));
3512 return [
3513 'error' => esc_html__('Received invalid embedding data from the API.', 'mxchat'),
3514 'error_code' => 'invalid_embedding_response'
3515 ];
3516 }
3517 }
3518 } catch (Exception $e) {
3519 //error_log('Embedding Exception: ' . $e->getMessage());
3520 return [
3521 'error' => esc_html__('System error when generating embeddings: ', 'mxchat') . esc_html($e->getMessage()),
3522 'error_code' => 'embedding_exception'
3523 ];
3524 }
3525 }
3526
3527
3528 private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default') {
3529 error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
3530
3531 // Get bot-specific Pinecone configuration
3532 $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
3533
3534 // Debug: Log the Pinecone configuration
3535 error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
3536 error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
3537 error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
3538 error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
3539 error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
3540
3541 // Determine whether to use Pinecone based on bot configuration
3542 $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
3543
3544 error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
3545
3546 if ($use_pinecone) {
3547 return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
3548 } else {
3549 return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
3550 }
3551 }
3552
3553 private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
3554 global $wpdb;
3555 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3556 $cache_key = 'mxchat_system_prompt_embeddings_' . $bot_id; // Bot-specific cache key
3557 $batch_size = 500;
3558
3559 // Initialize similarity analysis storage
3560 $this->last_similarity_analysis = [
3561 'knowledge_base_type' => 'WordPress Database',
3562 'bot_id' => $bot_id, // Track which bot is being used
3563 'top_matches' => [],
3564 'threshold_used' => 0,
3565 'total_checked' => 0
3566 ];
3567
3568 // Get bot-specific options for similarity threshold
3569 $bot_options = $this->get_bot_options($bot_id);
3570 $current_options = !empty($bot_options) ? $bot_options : $this->options;
3571
3572 // Retrieve embeddings from cache or database
3573 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3574 if ($embeddings === false) {
3575 // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
3576 $embeddings = [];
3577 $offset = 0;
3578
3579 do {
3580 // Add bot_id filter if not default and if bot_metadata column exists
3581 $bot_filter = '';
3582 if ($bot_id !== 'default') {
3583 // Check if bot_metadata column exists
3584 $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
3585 if ($column_exists) {
3586 $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3587 }
3588 }
3589
3590 $query = $wpdb->prepare(
3591 "SELECT id, embedding_vector, article_content, source_url, role_restriction
3592 FROM {$system_prompt_table}
3593 WHERE 1=1 {$bot_filter}
3594 LIMIT %d OFFSET %d",
3595 $batch_size,
3596 $offset
3597 );
3598
3599 $batch = $wpdb->get_results($query);
3600 if (empty($batch)) {
3601 break;
3602 }
3603
3604 $embeddings = array_merge($embeddings, $batch);
3605 $offset += $batch_size;
3606 unset($batch);
3607 } while (true);
3608
3609 if (empty($embeddings)) {
3610 return '';
3611 }
3612
3613 // Cache embeddings for future use (but note: this now includes content and role restrictions)
3614 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3615 }
3616
3617 // Get knowledge manager instance for role checking
3618 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3619
3620 // Get base similarity threshold from bot options or default options
3621 $similarity_threshold = isset($current_options['similarity_threshold'])
3622 ? ((int) $current_options['similarity_threshold']) / 100
3623 : 0.75;
3624
3625 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3626
3627 // Calculate similarities and build results array
3628 $all_similarities = [];
3629 $relevant_results = [];
3630
3631 foreach ($embeddings as $embedding) {
3632 $database_embedding = $embedding->embedding_vector
3633 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3634 : null;
3635
3636 if (is_array($database_embedding) && is_array($user_embedding)) {
3637 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
3638
3639 // Check role access
3640 $role_restriction = $embedding->role_restriction ?? 'public';
3641 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3642
3643 // Store ALL similarities for testing (top 10)
3644 $source_display = '';
3645 if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3646 $source_display = $embedding->source_url;
3647 } else {
3648 $content_preview = strip_tags($embedding->article_content ?? '');
3649 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3650 $source_display = substr(trim($content_preview), 0, 50) . '...';
3651 }
3652
3653 $all_similarities[] = [
3654 'document_id' => $embedding->id,
3655 'similarity' => $similarity,
3656 'similarity_percentage' => round($similarity * 100, 2),
3657 'above_threshold' => $similarity >= $similarity_threshold,
3658 'source_display' => $source_display,
3659 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3660 'used_for_context' => false, // Initialize as false, we'll update this later
3661 'role_restriction' => $role_restriction, // Include role info for testing
3662 'has_access' => $has_access, // Include access info for testing
3663 'filtered_out' => !$has_access // Mark if filtered out by role
3664 ];
3665
3666 // Only consider results above threshold AND with access for actual content retrieval
3667 if ($similarity >= $similarity_threshold && $has_access) {
3668 $relevant_results[] = [
3669 'id' => $embedding->id,
3670 'similarity' => $similarity
3671 ];
3672 }
3673 }
3674
3675 unset($database_embedding);
3676 }
3677
3678 // Sort ALL similarities for testing display (highest first)
3679 usort($all_similarities, function ($a, $b) {
3680 return $b['similarity'] <=> $a['similarity'];
3681 });
3682
3683 // Sort relevant results by similarity (highest first)
3684 usort($relevant_results, function ($a, $b) {
3685 return $b['similarity'] <=> $a['similarity'];
3686 });
3687
3688 // Get top 5 results for actual content (standard approach)
3689 $top_results = array_slice($relevant_results, 0, 5);
3690
3691 // NOW mark which documents are actually used for context
3692 $used_document_ids = [];
3693 foreach ($top_results as $result) {
3694 $used_document_ids[] = $result['id'];
3695 }
3696
3697 // Update the all_similarities array to mark which were actually used
3698 foreach ($all_similarities as &$similarity_item) {
3699 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
3700 }
3701
3702 // Store top 10 for testing panel (now with correct used_for_context flags and role info)
3703 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
3704 $this->last_similarity_analysis['total_checked'] = count($embeddings);
3705
3706 //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3707
3708 // Initialize final content
3709 $content = '';
3710
3711 // Track document IDs to avoid duplicates
3712 $added_document_ids = [];
3713
3714 // Fetch and format content for each selected result
3715 foreach ($top_results as $index => $result) {
3716 if (in_array($result['id'], $added_document_ids)) {
3717 continue;
3718 }
3719
3720 $chunk_content = $this->fetch_content_with_product_links($result['id']);
3721 $added_document_ids[] = $result['id'];
3722
3723 $content .= "## Reference " . ($index + 1) . " ##\n";
3724 $content .= $chunk_content . "\n\n";
3725
3726 // PDF surrounding pages logic (unchanged)
3727 if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3728 $surrounding_content = $wpdb->get_results($wpdb->prepare(
3729 "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
3730 WHERE id IN (
3731 (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3732 (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3733 )",
3734 $result['id'],
3735 $result['id']
3736 ));
3737
3738 // Check role access for surrounding content too
3739 if (!empty($surrounding_content[0])) {
3740 $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public';
3741 if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3742 $content .= "## Related Content ##\n";
3743 $content .= $surrounding_content[0]->article_content . "\n\n";
3744 $added_document_ids[] = $surrounding_content[0]->id;
3745 }
3746 }
3747
3748 if (!empty($surrounding_content[1])) {
3749 $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public';
3750 if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3751 $content .= "## Related Content ##\n";
3752 $content .= $surrounding_content[1]->article_content . "\n\n";
3753 $added_document_ids[] = $surrounding_content[1]->id;
3754 }
3755 }
3756 }
3757 }
3758
3759 // Add response guidelines
3760 if (empty($top_results)) {
3761 $content = "No reference information was found for this query.\n\n";
3762 } else {
3763 $content .= "\n## Response Guidelines ##\n" .
3764 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3765 "Be conversational and friendly, but never mention your knowledge base or training data. " .
3766 "If you don't have specific information or are uncertain about any details, it's always " .
3767 "better to honestly say you don't know rather than making up or guessing at answers. " .
3768 "When information is incomplete, let them know you are unsure.";
3769 }
3770
3771 return trim($content);
3772 }
3773
3774 private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
3775 global $wpdb;
3776
3777 error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
3778 error_log(" - bot_id: " . $bot_id);
3779 error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
3780 error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
3781
3782 // Use bot-specific config or fall back to default
3783 if ($bot_config === null) {
3784 $bot_config = $this->get_bot_pinecone_config($bot_id);
3785 }
3786
3787 $api_key = $bot_config['api_key'] ?? '';
3788 $host = $bot_config['host'] ?? '';
3789 $namespace = $bot_config['namespace'] ?? '';
3790
3791 error_log("MXCHAT DEBUG: Pinecone query parameters:");
3792 error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
3793 error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
3794 error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
3795
3796 // Initialize similarity analysis storage
3797 $this->last_similarity_analysis = [
3798 'knowledge_base_type' => 'Pinecone',
3799 'bot_id' => $bot_id,
3800 'namespace' => $namespace,
3801 'top_matches' => [],
3802 'threshold_used' => 0,
3803 'total_checked' => 0
3804 ];
3805
3806 if (empty($host) || empty($api_key)) {
3807 error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
3808 error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
3809 error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
3810 return '';
3811 }
3812
3813 // Get knowledge manager instance for role checking
3814 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3815
3816 // Get the similarity threshold from the bot options or main options
3817 $bot_options = $this->get_bot_options($bot_id);
3818 $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
3819
3820 $similarity_threshold = isset($current_options['similarity_threshold'])
3821 ? ((int) $current_options['similarity_threshold']) / 100
3822 : 0.75;
3823
3824 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3825
3826 // Prepare the query request for Pinecone
3827 $api_endpoint = "https://{$host}/query";
3828
3829 $request_body = array(
3830 'vector' => $user_embedding,
3831 'topK' => 20, // Request more to get good testing data
3832 'includeMetadata' => true,
3833 'includeValues' => true
3834 );
3835
3836 // Add namespace if specified for this bot
3837 if (!empty($namespace)) {
3838 $request_body['namespace'] = $namespace;
3839 }
3840
3841 error_log("MXCHAT DEBUG: About to call Pinecone API");
3842 error_log(" - Endpoint: " . $api_endpoint);
3843 error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
3844
3845 $response = wp_remote_post($api_endpoint, array(
3846 'headers' => array(
3847 'Api-Key' => $api_key,
3848 'accept' => 'application/json',
3849 'content-type' => 'application/json'
3850 ),
3851 'body' => wp_json_encode($request_body),
3852 'timeout' => 30
3853 ));
3854
3855 if (is_wp_error($response)) {
3856 error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
3857 return '';
3858 }
3859
3860 $response_code = wp_remote_retrieve_response_code($response);
3861 error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
3862
3863 if ($response_code !== 200) {
3864 $response_body = wp_remote_retrieve_body($response);
3865 error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
3866 return '';
3867 }
3868
3869 // ADD DETAILED DEBUG SECTION HERE
3870 $response_body = wp_remote_retrieve_body($response);
3871 error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
3872
3873 $results = json_decode($response_body, true);
3874
3875 if (json_last_error() !== JSON_ERROR_NONE) {
3876 error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
3877 error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
3878 return '';
3879 }
3880
3881 error_log("MXCHAT DEBUG: Pinecone response structure:");
3882 error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
3883 error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
3884
3885 if (empty($results['matches'])) {
3886 error_log("MXCHAT DEBUG: No matches found in Pinecone response");
3887 error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
3888 return '';
3889 }
3890
3891 error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
3892
3893 // Log first match details for debugging
3894 if (!empty($results['matches'][0])) {
3895 $first_match = $results['matches'][0];
3896 error_log("MXCHAT DEBUG: First match details:");
3897 error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
3898 error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
3899 if (isset($first_match['metadata'])) {
3900 error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
3901 }
3902 }
3903
3904 // Initialize the final content
3905 $content = '';
3906 $matches_used = 0;
3907 $matches_used_for_context = [];
3908
3909 // Process each match for actual content generation (lazy role checking)
3910 foreach ($results['matches'] as $index => $match) {
3911 // Skip if similarity is below threshold
3912 if ($match['score'] < $similarity_threshold) {
3913 continue;
3914 }
3915
3916 // Limit to top 5 matches above threshold
3917 if ($matches_used >= 5) {
3918 break;
3919 }
3920
3921 if (!empty($match['metadata']['text'])) {
3922 // LAZY ROLE CHECK: Only check role for content we're actually considering
3923 $match_id = $match['id'] ?? '';
3924 $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
3925 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3926
3927 // Skip if user doesn't have access
3928 if (!$has_access) {
3929 continue;
3930 }
3931
3932 // User has access - add to content
3933 $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3934 $content .= $match['metadata']['text'] . "\n\n";
3935
3936 if (!empty($match['metadata']['source_url'])) {
3937 $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
3938 }
3939
3940 $matches_used_for_context[] = $match['id'] ?? $index;
3941 $matches_used++;
3942 }
3943 }
3944
3945 // Process ALL matches for testing data (top 10) - with role checking for testing display
3946 $all_matches = [];
3947 foreach ($results['matches'] as $index => $match) {
3948 if ($index >= 10) break; // Limit to top 10 for testing
3949
3950 $match_id = $match['id'] ?? '';
3951
3952 // Check role access for testing display (use cache if available)
3953 $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
3954 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3955
3956 $source_display = '';
3957 if (!empty($match['metadata']['source_url'])) {
3958 $source_display = $match['metadata']['source_url'];
3959 } else {
3960 $content_preview = strip_tags($match['metadata']['text'] ?? '');
3961 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3962 $source_display = substr(trim($content_preview), 0, 50) . '...';
3963 }
3964
3965 $match_id_for_display = $match['id'] ?? $index;
3966
3967 $all_matches[] = [
3968 'document_id' => $match_id_for_display,
3969 'similarity' => $match['score'],
3970 'similarity_percentage' => round($match['score'] * 100, 2),
3971 'above_threshold' => $match['score'] >= $similarity_threshold,
3972 'source_display' => $source_display,
3973 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
3974 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
3975 'role_restriction' => $role_restriction,
3976 'has_access' => $has_access,
3977 'filtered_out' => !$has_access
3978 ];
3979 }
3980
3981 // Store for testing panel
3982 $this->last_similarity_analysis['top_matches'] = $all_matches;
3983 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
3984
3985 // Add response guidelines
3986 if ($matches_used === 0) {
3987 $content = "No reference information was found for this query.\n\n";
3988 } else {
3989 $content .= "\n## Response Guidelines ##\n" .
3990 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3991 "Be conversational and friendly, but never mention your knowledge base or training data. " .
3992 "If you don't have specific information or are uncertain about any details, it's always " .
3993 "better to honestly say you don't know rather than making up or guessing at answers. " .
3994 "When information is incomplete, let them know you are unsure.";
3995 }
3996
3997 return trim($content);
3998 }
3999 /**
4000 * Get role restriction for a single vector (with caching)
4001 */
4002 private function get_single_vector_role($vector_id, $metadata = array()) {
4003 global $wpdb;
4004
4005 if (empty($vector_id)) {
4006 return 'public';
4007 }
4008
4009 // Check cache first
4010 $cache_key = 'mxchat_vector_role_' . $vector_id;
4011 $cached_role = wp_cache_get($cache_key, 'mxchat_vector_roles');
4012
4013 if ($cached_role !== false) {
4014 return $cached_role;
4015 }
4016
4017 $role_restriction = 'public';
4018
4019 // First try Pinecone metadata
4020 if (!empty($metadata['role_restriction'])) {
4021 $role_restriction = $metadata['role_restriction'];
4022 } else {
4023 // Check WordPress table for user-modified roles
4024 $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
4025 $stored_role = $wpdb->get_var($wpdb->prepare(
4026 "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
4027 $vector_id
4028 ));
4029
4030 if ($stored_role) {
4031 $role_restriction = $stored_role;
4032 }
4033 }
4034
4035 // Cache individual role for 1 hour
4036 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
4037
4038 return $role_restriction;
4039 }
4040
4041 private function mxchat_find_relevant_products($user_embedding) {
4042 //error_log('MXChat Vector Search: Starting product search...');
4043
4044 // Retrieve the add-on settings from the database
4045 $addon_options = get_option('mxchat_pinecone_addon_options', array());
4046
4047 // Determine whether Pinecone is enabled
4048 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
4049
4050 //error_log('Pinecone enabled flag: ' . $use_pinecone);
4051
4052 if ($use_pinecone === 1) {
4053 //error_log('MXChat Vector Search: Using Pinecone database for products');
4054 return $this->find_relevant_products_pinecone($user_embedding);
4055 } else {
4056 //error_log('MXChat Vector Search: Using WordPress database for products');
4057 return $this->find_relevant_products_wordpress($user_embedding);
4058 }
4059 }
4060 private function find_relevant_products_wordpress($user_embedding) {
4061 global $wpdb;
4062 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4063 $cache_key = 'mxchat_system_prompt_embeddings';
4064 $batch_size = 500;
4065
4066 // Original WordPress database search logic
4067 // [Previous implementation remains the same]
4068 $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
4069 if ($embeddings === false) {
4070 $embeddings = [];
4071 $offset = 0;
4072
4073 do {
4074 $query = $wpdb->prepare(
4075 "SELECT id, embedding_vector
4076 FROM {$system_prompt_table}
4077 LIMIT %d OFFSET %d",
4078 $batch_size,
4079 $offset
4080 );
4081
4082 $batch = $wpdb->get_results($query);
4083 if (empty($batch)) {
4084 break;
4085 }
4086
4087 $embeddings = array_merge($embeddings, $batch);
4088 $offset += $batch_size;
4089
4090 unset($batch);
4091
4092 } while (true);
4093
4094 if (empty($embeddings)) {
4095 return '';
4096 }
4097 wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
4098 }
4099
4100 $relevant_results = [];
4101 foreach ($embeddings as $embedding) {
4102 $database_embedding = $embedding->embedding_vector
4103 ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
4104 : null;
4105 if (is_array($database_embedding) && is_array($user_embedding)) {
4106 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
4107 $relevant_results[] = [
4108 'id' => $embedding->id,
4109 'similarity' => $similarity
4110 ];
4111 }
4112 unset($database_embedding);
4113 }
4114
4115 // Use fixed threshold for products
4116 $similarity_threshold = 0.85;
4117
4118 $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
4119 return $result['similarity'] >= $similarity_threshold;
4120 });
4121 usort($relevant_results, function ($a, $b) {
4122 return $b['similarity'] <=> $a['similarity'];
4123 });
4124
4125 $top_results = array_slice($relevant_results, 0, 5);
4126 $content = '';
4127
4128 foreach ($top_results as $result) {
4129 $chunk_content = $this->fetch_content_with_product_links($result['id']);
4130 $content .= $chunk_content . "\n\n";
4131 }
4132
4133 return trim($content);
4134 }
4135
4136
4137 private function find_relevant_products_pinecone($user_embedding) {
4138 //error_log('Starting Pinecone product search...');
4139
4140 $options = get_option('mxchat_pinecone_addon_options', array());
4141 $api_key = $options['mxchat_pinecone_api_key'] ?? '';
4142 $host = $options['mxchat_pinecone_host'] ?? '';
4143
4144 if (empty($host) || empty($api_key)) {
4145 //error_log('Pinecone credentials not properly configured for product search');
4146 return '';
4147 }
4148
4149 $similarity_threshold = 0.85;
4150 $api_endpoint = "https://{$host}/query";
4151
4152 $request_body = array(
4153 'vector' => $user_embedding,
4154 'topK' => 5,
4155 'includeMetadata' => true,
4156 'includeValues' => true,
4157 'filter' => array(
4158 'type' => 'product'
4159 )
4160 );
4161
4162 //error_log('Sending request to Pinecone with body: ' . wp_json_encode($request_body));
4163
4164 $response = wp_remote_post($api_endpoint, array(
4165 'headers' => array(
4166 'Api-Key' => $api_key,
4167 'accept' => 'application/json',
4168 'content-type' => 'application/json'
4169 ),
4170 'body' => wp_json_encode($request_body),
4171 'timeout' => 30
4172 ));
4173
4174 if (is_wp_error($response)) {
4175 //error_log('Pinecone product query error: ' . $response->get_error_message());
4176 return '';
4177 }
4178
4179 $response_code = wp_remote_retrieve_response_code($response);
4180 //error_log('Pinecone response code: ' . $response_code);
4181
4182 if ($response_code !== 200) {
4183 //error_log('Pinecone API error during product search: ' . wp_remote_retrieve_body($response));
4184 return '';
4185 }
4186
4187 $results = json_decode(wp_remote_retrieve_body($response), true);
4188 //error_log('Pinecone raw response: ' . wp_remote_retrieve_body($response));
4189
4190 if (empty($results['matches'])) {
4191 //error_log('No matches found in Pinecone response');
4192 return '';
4193 }
4194
4195 $content = '';
4196 foreach ($results['matches'] as $match) {
4197 if ($match['score'] < $similarity_threshold) {
4198 //error_log("Match below threshold: " . $match['score']);
4199 continue;
4200 }
4201
4202 if (!empty($match['metadata']['text'])) {
4203 $content .= $match['metadata']['text'];
4204 if (!empty($match['metadata']['source_url'])) {
4205 $content .= "\n\nFor more details, check out this product: " . esc_url($match['metadata']['source_url']);
4206 }
4207 $content .= "\n\n";
4208 }
4209 }
4210
4211 return trim($content);
4212 }
4213
4214
4215 private function fetch_content_with_product_links($most_relevant_id) {
4216 global $wpdb;
4217 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
4218
4219 // Fetch the article content and associated product URL
4220 $query = $wpdb->prepare("SELECT article_content, source_url FROM {$system_prompt_table} WHERE id = %d", $most_relevant_id);
4221 $result = $wpdb->get_row($query);
4222
4223 if ($result) {
4224 // Append the product link to the content if available
4225 $content = $result->article_content;
4226 if (!empty($result->source_url)) {
4227 $content .= "\n\nFor more details, check out this product: " . esc_url($result->source_url);
4228 }
4229 return $content;
4230 }
4231
4232 return null;
4233 }
4234
4235 /**
4236 * Get system instructions for a specific bot or default
4237 * Checks for multi-bot add-on and uses bot-specific instructions if available
4238 */
4239 private function get_system_instructions($bot_id = 'default') {
4240 // Check if multi-bot add-on is active
4241 if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
4242 // Get bot-specific options from multi-bot add-on
4243 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
4244
4245 // If bot has custom system instructions, use those
4246 if (!empty($bot_options['system_prompt_instructions'])) {
4247 return $bot_options['system_prompt_instructions'];
4248 }
4249 }
4250
4251 // Fall back to default system instructions
4252 return isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
4253 }
4254 /**
4255 * Get the current bot ID from session or request context
4256 */
4257 private function get_current_bot_id($session_id = '') {
4258 // First, check if bot_id is passed in the current request
4259 if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
4260 return sanitize_key($_POST['bot_id']);
4261 }
4262
4263 // If not in POST, try to get it from session data
4264 if (!empty($session_id)) {
4265 $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
4266 if (!empty($bot_id)) {
4267 return $bot_id;
4268 }
4269 }
4270
4271 // Fall back to default
4272 return 'default';
4273 }
4274 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) {
4275 try {
4276 if (!$relevant_content) {
4277 $error_response = [
4278 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
4279 'error_code' => 'no_relevant_content'
4280 ];
4281
4282 // Add testing data to error response if available
4283 if ($testing_data !== null) {
4284 $error_response['testing_data'] = $testing_data;
4285 //error_log("MxChat Testing: Added testing data to no_relevant_content error");
4286 }
4287
4288 return $error_response;
4289 }
4290
4291 // Ensure conversation_history is an array
4292 if (!is_array($conversation_history)) {
4293 $conversation_history = array();
4294 }
4295
4296 // Get selected model with default fallback
4297 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
4298
4299 // Extract model prefix to determine the provider
4300 $model_parts = explode('-', $selected_model);
4301 $provider = strtolower($model_parts[0]);
4302
4303 // Handle model selection based on provider prefix
4304 switch ($provider) {
4305 case 'gemini':
4306 if (empty($gemini_api_key)) {
4307 $error_response = [
4308 'error' => esc_html__('Google Gemini API key is not configured', 'mxchat'),
4309 'error_code' => 'missing_gemini_api_key'
4310 ];
4311 if ($testing_data !== null) {
4312 $error_response['testing_data'] = $testing_data;
4313 }
4314 return $error_response;
4315 }
4316 $response = $this->mxchat_generate_response_gemini(
4317 $selected_model,
4318 $gemini_api_key,
4319 $conversation_history,
4320 $relevant_content
4321 );
4322 break;
4323
4324 case 'claude':
4325 if (empty($claude_api_key)) {
4326 $error_response = [
4327 'error' => esc_html__('Claude API key is not configured', 'mxchat'),
4328 'error_code' => 'missing_claude_api_key'
4329 ];
4330 if ($testing_data !== null) {
4331 $error_response['testing_data'] = $testing_data;
4332 }
4333 return $error_response;
4334 }
4335 if ($streaming) {
4336 return $this->mxchat_generate_response_claude_stream(
4337 $selected_model,
4338 $claude_api_key,
4339 $conversation_history,
4340 $relevant_content,
4341 $session_id,
4342 $testing_data // Pass testing data
4343 );
4344 } else {
4345 $response = $this->mxchat_generate_response_claude(
4346 $selected_model,
4347 $claude_api_key,
4348 $conversation_history,
4349 $relevant_content
4350 );
4351 }
4352 break;
4353
4354 case 'grok':
4355 if (empty($xai_api_key)) {
4356 $error_response = [
4357 'error' => esc_html__('X.AI API key is not configured', 'mxchat'),
4358 'error_code' => 'missing_xai_api_key'
4359 ];
4360 if ($testing_data !== null) {
4361 $error_response['testing_data'] = $testing_data;
4362 }
4363 return $error_response;
4364 }
4365 if ($streaming) {
4366 return $this->mxchat_generate_response_xai_stream(
4367 $selected_model,
4368 $xai_api_key,
4369 $conversation_history,
4370 $relevant_content,
4371 $session_id,
4372 $testing_data // Pass testing data
4373 );
4374 } else {
4375 $response = $this->mxchat_generate_response_xai(
4376 $selected_model,
4377 $xai_api_key,
4378 $conversation_history,
4379 $relevant_content
4380 );
4381 }
4382 break;
4383
4384 case 'deepseek':
4385 if (empty($deepseek_api_key)) {
4386 $error_response = [
4387 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
4388 'error_code' => 'missing_deepseek_api_key'
4389 ];
4390 if ($testing_data !== null) {
4391 $error_response['testing_data'] = $testing_data;
4392 }
4393 return $error_response;
4394 }
4395 if ($streaming) {
4396 return $this->mxchat_generate_response_deepseek_stream(
4397 $selected_model,
4398 $deepseek_api_key,
4399 $conversation_history,
4400 $relevant_content,
4401 $session_id,
4402 $testing_data // Pass testing data
4403 );
4404 } else {
4405 $response = $this->mxchat_generate_response_deepseek(
4406 $selected_model,
4407 $deepseek_api_key,
4408 $conversation_history,
4409 $relevant_content
4410 );
4411 }
4412 break;
4413
4414 case 'gpt':
4415 case 'o1':
4416 if (empty($api_key)) {
4417 $error_response = [
4418 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
4419 'error_code' => 'missing_openai_api_key'
4420 ];
4421 if ($testing_data !== null) {
4422 $error_response['testing_data'] = $testing_data;
4423 }
4424 return $error_response;
4425 }
4426 if ($streaming) {
4427 return $this->mxchat_generate_response_openai_stream(
4428 $selected_model,
4429 $api_key,
4430 $conversation_history,
4431 $relevant_content,
4432 $session_id,
4433 $testing_data // Pass testing data
4434 );
4435 } else {
4436 $response = $this->mxchat_generate_response_openai(
4437 $selected_model,
4438 $api_key,
4439 $conversation_history,
4440 $relevant_content
4441 );
4442 }
4443 break;
4444
4445 default:
4446 // Default to OpenAI for custom models or unrecognized prefixes
4447 if (empty($api_key)) {
4448 $error_response = [
4449 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
4450 'error_code' => 'missing_openai_api_key'
4451 ];
4452 if ($testing_data !== null) {
4453 $error_response['testing_data'] = $testing_data;
4454 }
4455 return $error_response;
4456 }
4457 if ($streaming) {
4458 return $this->mxchat_generate_response_openai_stream(
4459 $selected_model,
4460 $api_key,
4461 $conversation_history,
4462 $relevant_content,
4463 $session_id,
4464 $testing_data // Pass testing data
4465 );
4466 } else {
4467 $response = $this->mxchat_generate_response_openai(
4468 $selected_model,
4469 $api_key,
4470 $conversation_history,
4471 $relevant_content
4472 );
4473 }
4474 break;
4475 }
4476
4477 // Check if the response is an error array from the provider-specific function
4478 if (is_array($response) && isset($response['error'])) {
4479 // Add testing data to error response if available
4480 if ($testing_data !== null) {
4481 $response['testing_data'] = $testing_data;
4482 //error_log("MxChat Testing: Added testing data to provider error response");
4483 }
4484 return $response; // Pass through the error with testing data
4485 }
4486
4487 // For successful non-streaming responses, we don't add testing data here
4488 // because it will be added in the main handler
4489 return $response;
4490
4491 } catch (Exception $e) {
4492 //error_log('MXChat Error: ' . $e->getMessage());
4493 $error_response = [
4494 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
4495 'error_code' => 'system_exception',
4496 'exception_details' => $e->getMessage()
4497 ];
4498
4499 // Add testing data to exception response if available
4500 if ($testing_data !== null) {
4501 $error_response['testing_data'] = $testing_data;
4502 //error_log("MxChat Testing: Added testing data to exception response");
4503 }
4504
4505 return $error_response;
4506 }
4507 }
4508 private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4509 try {
4510 $bot_id = $this->get_current_bot_id($session_id);
4511
4512 // Get system prompt instructions using centralized function
4513 $system_prompt_instructions = $this->get_system_instructions($bot_id);
4514
4515 // Ensure conversation_history is an array
4516 if (!is_array($conversation_history)) {
4517 $conversation_history = array();
4518 }
4519
4520 // Format conversation history for OpenAI
4521 $formatted_conversation = array();
4522
4523 $formatted_conversation[] = array(
4524 'role' => 'system',
4525 'content' => $system_prompt_instructions . " " . $relevant_content
4526 );
4527
4528 foreach ($conversation_history as $message) {
4529 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4530 $role = $message['role'];
4531 if ($role === 'bot' || $role === 'agent') {
4532 $role = 'assistant';
4533 }
4534 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4535 $role = 'user';
4536 }
4537 $formatted_conversation[] = array(
4538 'role' => $role,
4539 'content' => $message['content']
4540 );
4541 }
4542 }
4543
4544 // Check if we can actually stream
4545 if (headers_sent() || !function_exists('curl_init')) {
4546 // Fallback to regular response with testing data
4547 //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4548 $regular_response = $this->mxchat_generate_response_openai(
4549 $selected_model,
4550 $api_key,
4551 $conversation_history,
4552 $relevant_content
4553 );
4554
4555 $response_data = [
4556 'text' => $regular_response,
4557 'html' => '',
4558 'session_id' => $session_id
4559 ];
4560
4561 if ($testing_data !== null) {
4562 $response_data['testing_data'] = $testing_data;
4563 //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
4564 }
4565
4566 header('Content-Type: application/json');
4567 echo json_encode($response_data);
4568 return true;
4569 }
4570
4571 // Prepare the request body with stream: true
4572 $body = json_encode([
4573 'model' => $selected_model,
4574 'messages' => $formatted_conversation,
4575 'temperature' => 1,
4576 'stream' => true
4577 ]);
4578
4579 // Use cURL for streaming support
4580 $ch = curl_init();
4581 curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4582 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4583 curl_setopt($ch, CURLOPT_POST, true);
4584 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4585 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4586 'Content-Type: application/json',
4587 'Authorization: Bearer ' . $api_key
4588 ));
4589 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4590 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4591
4592 $full_response = ''; // Accumulate full response for saving
4593 $stream_started = false;
4594
4595 // Buffer control for real-time streaming
4596 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4597 // Send testing data as the first event if available
4598 if (!$stream_started && $testing_data !== null) {
4599 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4600 flush();
4601 $stream_started = true;
4602 //error_log("MxChat Testing: Sent testing data in OpenAI stream");
4603 }
4604
4605 // Process each chunk of data
4606 $lines = explode("\n", $data);
4607
4608 foreach ($lines as $line) {
4609 if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4610 continue;
4611 }
4612
4613 $json_str = substr($line, 6); // Remove 'data: ' prefix
4614
4615 if ($json_str === '[DONE]') {
4616 echo "data: [DONE]\n\n";
4617 flush();
4618 continue;
4619 }
4620
4621 $json = json_decode($json_str, true);
4622 if (isset($json['choices'][0]['delta']['content'])) {
4623 $content = $json['choices'][0]['delta']['content'];
4624 $full_response .= $content; // Accumulate
4625 // Send as SSE format
4626 echo "data: " . json_encode(['content' => $content]) . "\n\n";
4627 flush();
4628 }
4629 }
4630
4631 return strlen($data);
4632 });
4633
4634 $response = curl_exec($ch);
4635 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4636
4637 if (curl_errno($ch) || $http_code !== 200) {
4638 curl_close($ch);
4639
4640 // Fallback to regular response
4641 //error_log("MxChat: OpenAI streaming failed, falling back");
4642 $regular_response = $this->mxchat_generate_response_openai(
4643 $selected_model,
4644 $api_key,
4645 $conversation_history,
4646 $relevant_content
4647 );
4648
4649 $response_data = [
4650 'text' => $regular_response,
4651 'html' => '',
4652 'session_id' => $session_id
4653 ];
4654
4655 if ($testing_data !== null) {
4656 $response_data['testing_data'] = $testing_data;
4657 //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
4658 }
4659
4660 header('Content-Type: application/json');
4661 echo json_encode($response_data);
4662 return true;
4663 }
4664
4665 curl_close($ch);
4666
4667 // Save the complete response to maintain chat persistence
4668 if (!empty($full_response) && !empty($session_id)) {
4669 $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4670 }
4671
4672 return true; // Indicate streaming completed successfully
4673
4674 } catch (Exception $e) {
4675 //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4676
4677 // Fallback to regular response
4678 $regular_response = $this->mxchat_generate_response_openai(
4679 $selected_model,
4680 $api_key,
4681 $conversation_history,
4682 $relevant_content
4683 );
4684
4685 $response_data = [
4686 'text' => $regular_response,
4687 'html' => '',
4688 'session_id' => $session_id
4689 ];
4690
4691 if ($testing_data !== null) {
4692 $response_data['testing_data'] = $testing_data;
4693 //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
4694 }
4695
4696 header('Content-Type: application/json');
4697 echo json_encode($response_data);
4698 return true;
4699 }
4700 }
4701 private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4702 try {
4703 // Get bot ID from session or request
4704 $bot_id = $this->get_current_bot_id($session_id);
4705
4706 // Get system prompt instructions using centralized function
4707 $system_prompt_instructions = $this->get_system_instructions($bot_id);
4708 // Ensure conversation_history is an array
4709 if (!is_array($conversation_history)) {
4710 $conversation_history = array();
4711 }
4712
4713 // Clean and validate conversation history
4714 foreach ($conversation_history as &$message) {
4715 // Convert bot and agent roles to assistant
4716 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
4717 $message['role'] = 'assistant';
4718 }
4719
4720 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
4721 if (!in_array($message['role'], ['assistant', 'user'])) {
4722 $message['role'] = 'user';
4723 }
4724
4725 // Ensure content field exists
4726 if (!isset($message['content']) || empty($message['content'])) {
4727 $message['content'] = '';
4728 }
4729
4730 // Remove any unsupported fields
4731 $message = array_intersect_key($message, array_flip(['role', 'content']));
4732 }
4733
4734 // Add relevant content as the latest user message
4735 $conversation_history[] = [
4736 'role' => 'user',
4737 'content' => $relevant_content
4738 ];
4739
4740 // Prepare the request body with stream: true
4741 $body = json_encode([
4742 'model' => $selected_model,
4743 'messages' => $conversation_history,
4744 'max_tokens' => 1000,
4745 'temperature' => 0.8,
4746 'system' => $system_prompt_instructions,
4747 'stream' => true
4748 ]);
4749
4750 // Check if we can actually stream (headers not sent, etc.)
4751 if (headers_sent() || !function_exists('curl_init')) {
4752 // Fallback to regular response with testing data
4753 //error_log("MxChat: Streaming not possible, falling back to regular response");
4754 $regular_response = $this->mxchat_generate_response_claude(
4755 $selected_model,
4756 $claude_api_key,
4757 array_slice($conversation_history, 0, -1), // Remove the added content
4758 $relevant_content
4759 );
4760
4761 // Return as JSON with testing data
4762 $response_data = [
4763 'text' => $regular_response,
4764 'html' => '',
4765 'session_id' => $session_id
4766 ];
4767
4768 if ($testing_data !== null) {
4769 $response_data['testing_data'] = $testing_data;
4770 //error_log("MxChat Testing: Added testing data to Claude fallback response");
4771 }
4772
4773 // Clear any streaming headers and send JSON
4774 if (headers_sent() === false) {
4775 header('Content-Type: application/json');
4776 }
4777 echo json_encode($response_data);
4778 return true; // Indicate we handled the response
4779 }
4780
4781 // Use cURL for streaming support
4782 $ch = curl_init();
4783 curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
4784 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4785 curl_setopt($ch, CURLOPT_POST, true);
4786 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4787 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4788 'Content-Type: application/json',
4789 'x-api-key: ' . $claude_api_key,
4790 'anthropic-version: 2023-06-01'
4791 ));
4792 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4793 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4794
4795 $full_response = ''; // Accumulate full response for saving
4796 $stream_started = false;
4797
4798 // Buffer control for real-time streaming
4799 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4800 // Send testing data as the first event if available
4801 if (!$stream_started && $testing_data !== null) {
4802 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4803 flush();
4804 $stream_started = true;
4805 //error_log("MxChat Testing: Sent testing data in Claude stream");
4806 }
4807
4808 // Process each chunk of data
4809 $lines = explode("\n", $data);
4810
4811 foreach ($lines as $line) {
4812 if (trim($line) === '') {
4813 continue;
4814 }
4815
4816 // Claude uses event: and data: format
4817 if (strpos($line, 'event: ') === 0) {
4818 // Store the event type for the next data line
4819 continue;
4820 }
4821
4822 if (strpos($line, 'data: ') === 0) {
4823 $json_str = substr($line, 6); // Remove 'data: ' prefix
4824
4825 $json = json_decode($json_str, true);
4826 if (json_last_error() !== JSON_ERROR_NONE) {
4827 continue;
4828 }
4829
4830 // Handle different event types
4831 if (isset($json['type'])) {
4832 switch ($json['type']) {
4833 case 'content_block_delta':
4834 if (isset($json['delta']['text'])) {
4835 $content = $json['delta']['text'];
4836 $full_response .= $content; // Accumulate
4837 // Send as SSE format compatible with your frontend
4838 echo "data: " . json_encode(['content' => $content]) . "\n\n";
4839 flush();
4840 }
4841 break;
4842
4843 case 'message_stop':
4844 echo "data: [DONE]\n\n";
4845 flush();
4846 break;
4847
4848 case 'error':
4849 echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
4850 flush();
4851 break;
4852 }
4853 }
4854 }
4855 }
4856
4857 return strlen($data);
4858 });
4859
4860 $response = curl_exec($ch);
4861 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4862
4863 if (curl_errno($ch)) {
4864 curl_close($ch);
4865 throw new Exception('cURL Error: ' . curl_error($ch));
4866 }
4867
4868 curl_close($ch);
4869
4870 if ($http_code !== 200) {
4871 // Fallback to regular response
4872 //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
4873 $regular_response = $this->mxchat_generate_response_claude(
4874 $selected_model,
4875 $claude_api_key,
4876 array_slice($conversation_history, 0, -1), // Remove the added content
4877 $relevant_content
4878 );
4879
4880 $response_data = [
4881 'text' => $regular_response,
4882 'html' => '',
4883 'session_id' => $session_id
4884 ];
4885
4886 if ($testing_data !== null) {
4887 $response_data['testing_data'] = $testing_data;
4888 //error_log("MxChat Testing: Added testing data to Claude error fallback");
4889 }
4890
4891 header('Content-Type: application/json');
4892 echo json_encode($response_data);
4893 return true;
4894 }
4895
4896 // Save the complete response to maintain chat persistence
4897 if (!empty($full_response) && !empty($session_id)) {
4898 $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
4899 }
4900
4901 return true; // Indicate streaming completed successfully
4902
4903 } catch (Exception $e) {
4904 //error_log("MxChat Claude streaming exception: " . $e->getMessage());
4905
4906 // Fallback to regular response on exception
4907 $regular_response = $this->mxchat_generate_response_claude(
4908 $selected_model,
4909 $claude_api_key,
4910 $conversation_history,
4911 $relevant_content
4912 );
4913
4914 $response_data = [
4915 'text' => $regular_response,
4916 'html' => '',
4917 'session_id' => $session_id
4918 ];
4919
4920 if ($testing_data !== null) {
4921 $response_data['testing_data'] = $testing_data;
4922 //error_log("MxChat Testing: Added testing data to Claude exception fallback");
4923 }
4924
4925 header('Content-Type: application/json');
4926 echo json_encode($response_data);
4927 return true;
4928 }
4929 }
4930 private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4931 try {
4932 // Get bot ID from session or request
4933 $bot_id = $this->get_current_bot_id($session_id);
4934
4935 // Get system prompt instructions using centralized function
4936 $system_prompt_instructions = $this->get_system_instructions($bot_id);
4937
4938 // Ensure conversation_history is an array
4939 if (!is_array($conversation_history)) {
4940 $conversation_history = array();
4941 }
4942
4943 // Format conversation history for X.AI (same as OpenAI format)
4944 $formatted_conversation = array();
4945
4946 $formatted_conversation[] = array(
4947 'role' => 'system',
4948 'content' => $system_prompt_instructions . " " . $relevant_content
4949 );
4950
4951 foreach ($conversation_history as $message) {
4952 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
4953 $role = $message['role'];
4954 if ($role === 'bot' || $role === 'agent') {
4955 $role = 'assistant';
4956 }
4957 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
4958 $role = 'user';
4959 }
4960 $formatted_conversation[] = array(
4961 'role' => $role,
4962 'content' => $message['content']
4963 );
4964 }
4965 }
4966
4967 // Check if we can actually stream
4968 if (headers_sent() || !function_exists('curl_init')) {
4969 // Fallback to regular response with testing data
4970 //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
4971 $regular_response = $this->mxchat_generate_response_xai(
4972 $selected_model,
4973 $xai_api_key,
4974 $conversation_history,
4975 $relevant_content
4976 );
4977
4978 $response_data = [
4979 'text' => $regular_response,
4980 'html' => '',
4981 'session_id' => $session_id
4982 ];
4983
4984 if ($testing_data !== null) {
4985 $response_data['testing_data'] = $testing_data;
4986 //error_log("MxChat Testing: Added testing data to X.AI fallback response");
4987 }
4988
4989 header('Content-Type: application/json');
4990 echo json_encode($response_data);
4991 return true;
4992 }
4993
4994 // Prepare the request body with stream: true
4995 $body = json_encode([
4996 'model' => $selected_model,
4997 'messages' => $formatted_conversation,
4998 'temperature' => 0.8,
4999 'stream' => true
5000 ]);
5001
5002 // Use cURL for streaming support
5003 $ch = curl_init();
5004 curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
5005 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5006 curl_setopt($ch, CURLOPT_POST, true);
5007 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5008 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5009 'Content-Type: application/json',
5010 'Authorization: Bearer ' . $xai_api_key
5011 ));
5012 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5013 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5014
5015 $full_response = ''; // Accumulate full response for saving
5016 $stream_started = false;
5017
5018 // Buffer control for real-time streaming
5019 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
5020 // Send testing data as the first event if available
5021 if (!$stream_started && $testing_data !== null) {
5022 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5023 flush();
5024 $stream_started = true;
5025 //error_log("MxChat Testing: Sent testing data in X.AI stream");
5026 }
5027
5028 // Process each chunk of data
5029 $lines = explode("\n", $data);
5030
5031 foreach ($lines as $line) {
5032 if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
5033 continue;
5034 }
5035
5036 $json_str = substr($line, 6); // Remove 'data: ' prefix
5037
5038 if ($json_str === '[DONE]') {
5039 echo "data: [DONE]\n\n";
5040 flush();
5041 continue;
5042 }
5043
5044 $json = json_decode($json_str, true);
5045 if (isset($json['choices'][0]['delta']['content'])) {
5046 $content = $json['choices'][0]['delta']['content'];
5047 $full_response .= $content; // Accumulate
5048 // Send as SSE format
5049 echo "data: " . json_encode(['content' => $content]) . "\n\n";
5050 flush();
5051 }
5052 }
5053
5054 return strlen($data);
5055 });
5056
5057 $response = curl_exec($ch);
5058 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5059
5060 if (curl_errno($ch) || $http_code !== 200) {
5061 curl_close($ch);
5062
5063 // Fallback to regular response
5064 //error_log("MxChat: X.AI streaming failed, falling back");
5065 $regular_response = $this->mxchat_generate_response_xai(
5066 $selected_model,
5067 $xai_api_key,
5068 $conversation_history,
5069 $relevant_content
5070 );
5071
5072 $response_data = [
5073 'text' => $regular_response,
5074 'html' => '',
5075 'session_id' => $session_id
5076 ];
5077
5078 if ($testing_data !== null) {
5079 $response_data['testing_data'] = $testing_data;
5080 //error_log("MxChat Testing: Added testing data to X.AI error fallback");
5081 }
5082
5083 header('Content-Type: application/json');
5084 echo json_encode($response_data);
5085 return true;
5086 }
5087
5088 curl_close($ch);
5089
5090 // Save the complete response to maintain chat persistence
5091 if (!empty($full_response) && !empty($session_id)) {
5092 $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
5093 }
5094
5095 return true; // Indicate streaming completed successfully
5096
5097 } catch (Exception $e) {
5098 //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
5099
5100 // Fallback to regular response
5101 $regular_response = $this->mxchat_generate_response_xai(
5102 $selected_model,
5103 $xai_api_key,
5104 $conversation_history,
5105 $relevant_content
5106 );
5107
5108 $response_data = [
5109 'text' => $regular_response,
5110 'html' => '',
5111 'session_id' => $session_id
5112 ];
5113
5114 if ($testing_data !== null) {
5115 $response_data['testing_data'] = $testing_data;
5116 //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
5117 }
5118
5119 header('Content-Type: application/json');
5120 echo json_encode($response_data);
5121 return true;
5122 }
5123 }
5124 private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
5125 try {
5126 // Get bot ID from session or request
5127 $bot_id = $this->get_current_bot_id($session_id);
5128
5129 // Get system prompt instructions using centralized function
5130 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5131
5132 // Ensure conversation_history is an array
5133 if (!is_array($conversation_history)) {
5134 $conversation_history = array();
5135 }
5136
5137 // Format conversation history for DeepSeek
5138 $formatted_conversation = array();
5139
5140 $formatted_conversation[] = array(
5141 'role' => 'system',
5142 'content' => $system_prompt_instructions . " " . $relevant_content
5143 );
5144
5145 foreach ($conversation_history as $message) {
5146 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5147 $role = $message['role'];
5148 if ($role === 'bot' || $role === 'agent') {
5149 $role = 'assistant';
5150 }
5151 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5152 $role = 'user';
5153 }
5154 $formatted_conversation[] = array(
5155 'role' => $role,
5156 'content' => $message['content']
5157 );
5158 }
5159 }
5160
5161 // Check if we can actually stream
5162 if (headers_sent() || !function_exists('curl_init')) {
5163 // Fallback to regular response with testing data
5164 //error_log("MxChat: DeepSeek streaming not possible, falling back to regular response");
5165 $regular_response = $this->mxchat_generate_response_deepseek(
5166 $selected_model,
5167 $deepseek_api_key,
5168 $conversation_history,
5169 $relevant_content
5170 );
5171
5172 $response_data = [
5173 'text' => $regular_response,
5174 'html' => '',
5175 'session_id' => $session_id
5176 ];
5177
5178 if ($testing_data !== null) {
5179 $response_data['testing_data'] = $testing_data;
5180 //error_log("MxChat Testing: Added testing data to DeepSeek fallback response");
5181 }
5182
5183 header('Content-Type: application/json');
5184 echo json_encode($response_data);
5185 return true;
5186 }
5187
5188 // Prepare the request body with stream: true
5189 $body = json_encode([
5190 'model' => $selected_model,
5191 'messages' => $formatted_conversation,
5192 'temperature' => 0.8,
5193 'stream' => true
5194 ]);
5195
5196 // Use cURL for streaming support
5197 $ch = curl_init();
5198 curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
5199 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
5200 curl_setopt($ch, CURLOPT_POST, true);
5201 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
5202 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
5203 'Content-Type: application/json',
5204 'Authorization: Bearer ' . $deepseek_api_key
5205 ));
5206 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
5207 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
5208
5209 $full_response = ''; // Accumulate full response for saving
5210 $stream_started = false;
5211
5212 // Buffer control for real-time streaming
5213 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
5214 // Send testing data as the first event if available
5215 if (!$stream_started && $testing_data !== null) {
5216 echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
5217 flush();
5218 $stream_started = true;
5219 //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
5220 }
5221
5222 // Process each chunk of data
5223 $lines = explode("\n", $data);
5224
5225 foreach ($lines as $line) {
5226 if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
5227 continue;
5228 }
5229
5230 $json_str = substr($line, 6); // Remove 'data: ' prefix
5231
5232 if ($json_str === '[DONE]') {
5233 echo "data: [DONE]\n\n";
5234 flush();
5235 continue;
5236 }
5237
5238 $json = json_decode($json_str, true);
5239 if (isset($json['choices'][0]['delta']['content'])) {
5240 $content = $json['choices'][0]['delta']['content'];
5241 $full_response .= $content; // Accumulate
5242 // Send as SSE format
5243 echo "data: " . json_encode(['content' => $content]) . "\n\n";
5244 flush();
5245 }
5246 }
5247
5248 return strlen($data);
5249 });
5250
5251 $response = curl_exec($ch);
5252 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
5253
5254 if (curl_errno($ch) || $http_code !== 200) {
5255 $curl_error = curl_error($ch);
5256 curl_close($ch);
5257
5258 // Log the specific error for debugging
5259 //error_log("MxChat: DeepSeek streaming failed - HTTP: $http_code, cURL: $curl_error");
5260
5261 // Fallback to regular response
5262 $regular_response = $this->mxchat_generate_response_deepseek(
5263 $selected_model,
5264 $deepseek_api_key,
5265 $conversation_history,
5266 $relevant_content
5267 );
5268
5269 // Handle error response from regular function
5270 if (is_array($regular_response) && isset($regular_response['error'])) {
5271 if ($testing_data !== null) {
5272 $regular_response['testing_data'] = $testing_data;
5273 }
5274 header('Content-Type: application/json');
5275 echo json_encode($regular_response);
5276 return true;
5277 }
5278
5279 $response_data = [
5280 'text' => $regular_response,
5281 'html' => '',
5282 'session_id' => $session_id
5283 ];
5284
5285 if ($testing_data !== null) {
5286 $response_data['testing_data'] = $testing_data;
5287 //error_log("MxChat Testing: Added testing data to DeepSeek error fallback");
5288 }
5289
5290 header('Content-Type: application/json');
5291 echo json_encode($response_data);
5292 return true;
5293 }
5294
5295 curl_close($ch);
5296
5297 // Save the complete response to maintain chat persistence
5298 if (!empty($full_response) && !empty($session_id)) {
5299 $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
5300 }
5301
5302 return true; // Indicate streaming completed successfully
5303
5304 } catch (Exception $e) {
5305 //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
5306
5307 // Fallback to regular response
5308 $regular_response = $this->mxchat_generate_response_deepseek(
5309 $selected_model,
5310 $deepseek_api_key,
5311 $conversation_history,
5312 $relevant_content
5313 );
5314
5315 // Handle error response from regular function
5316 if (is_array($regular_response) && isset($regular_response['error'])) {
5317 if ($testing_data !== null) {
5318 $regular_response['testing_data'] = $testing_data;
5319 }
5320 header('Content-Type: application/json');
5321 echo json_encode($regular_response);
5322 return true;
5323 }
5324
5325 $response_data = [
5326 'text' => $regular_response,
5327 'html' => '',
5328 'session_id' => $session_id
5329 ];
5330
5331 if ($testing_data !== null) {
5332 $response_data['testing_data'] = $testing_data;
5333 //error_log("MxChat Testing: Added testing data to DeepSeek exception fallback");
5334 }
5335
5336 header('Content-Type: application/json');
5337 echo json_encode($response_data);
5338 return true;
5339 }
5340 }
5341
5342 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
5343
5344 // Get bot ID from session or request
5345 $bot_id = $this->get_current_bot_id($session_id);
5346
5347 // Get system prompt instructions using centralized function
5348 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5349
5350 // Clean and validate conversation history
5351 foreach ($conversation_history as &$message) {
5352 // Convert bot and agent roles to assistant
5353 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
5354 $message['role'] = 'assistant';
5355 }
5356
5357 // Remove unsupported roles - Claude only supports 'assistant' and 'user'
5358 if (!in_array($message['role'], ['assistant', 'user'])) {
5359 $message['role'] = 'user';
5360 }
5361
5362 // Ensure content field exists
5363 if (!isset($message['content']) || empty($message['content'])) {
5364 $message['content'] = '';
5365 }
5366
5367 // Remove any unsupported fields
5368 $message = array_intersect_key($message, array_flip(['role', 'content']));
5369 }
5370
5371 // Add relevant content as the latest user message
5372 $conversation_history[] = [
5373 'role' => 'user',
5374 'content' => $relevant_content
5375 ];
5376
5377 // Build request body
5378 $body = json_encode([
5379 'model' => $selected_model,
5380 'max_tokens' => 1000,
5381 'temperature' => 0.8,
5382 'messages' => $conversation_history,
5383 'system' => $system_prompt_instructions
5384 ]);
5385
5386 // Set up API request
5387 $args = [
5388 'body' => $body,
5389 'headers' => [
5390 'Content-Type' => 'application/json',
5391 'x-api-key' => $claude_api_key,
5392 'anthropic-version' => '2023-06-01'
5393 ],
5394 'timeout' => 60,
5395 'redirection' => 5,
5396 'blocking' => true,
5397 'httpversion' => '1.0',
5398 'sslverify' => true,
5399 ];
5400
5401 // Make API request
5402 $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
5403
5404 // Check for WordPress errors
5405 if (is_wp_error($response)) {
5406 //error_log("Claude API request error: " . $response->get_error_message());
5407 return "Sorry, there was an error connecting to the API.";
5408 }
5409
5410 // Check HTTP response code
5411 $http_code = wp_remote_retrieve_response_code($response);
5412 if ($http_code !== 200) {
5413 $error_body = wp_remote_retrieve_body($response);
5414 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
5415
5416 // Try to extract error message from response
5417 $error_data = json_decode($error_body, true);
5418 $error_message = isset($error_data['error']['message']) ?
5419 $error_data['error']['message'] :
5420 "HTTP error " . $http_code;
5421
5422 return "Sorry, the API returned an error: " . $error_message;
5423 }
5424
5425 // Parse response
5426 $response_body = json_decode(wp_remote_retrieve_body($response), true);
5427
5428 // Check for JSON decode errors
5429 if (json_last_error() !== JSON_ERROR_NONE) {
5430 //error_log("Claude API JSON decode error: " . json_last_error_msg());
5431 return "Sorry, there was an error processing the API response.";
5432 }
5433
5434 // Extract and validate response content
5435 if (isset($response_body['content']) &&
5436 is_array($response_body['content']) &&
5437 !empty($response_body['content']) &&
5438 isset($response_body['content'][0]['text'])) {
5439 return trim($response_body['content'][0]['text']);
5440 }
5441
5442 // Log unexpected response format
5443 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
5444 return "Sorry, I received an unexpected response format from the API.";
5445 }
5446 private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
5447 try {
5448 // Ensure conversation_history is an array
5449 if (!is_array($conversation_history)) {
5450 $conversation_history = array();
5451 }
5452
5453 // Get bot ID from session or request
5454 $bot_id = $this->get_current_bot_id($session_id);
5455
5456 // Get system prompt instructions using centralized function
5457 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5458
5459 // Create a new array for the formatted conversation
5460 $formatted_conversation = array();
5461
5462 // Add system message first
5463 $formatted_conversation[] = array(
5464 'role' => 'system',
5465 'content' => $system_prompt_instructions . " " . $relevant_content
5466 );
5467
5468 // Add the rest of the conversation history
5469 foreach ($conversation_history as $message) {
5470 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5471 $role = $message['role'];
5472
5473 // Convert roles to supported format
5474 if ($role === 'bot' || $role === 'agent') {
5475 $role = 'assistant';
5476 }
5477 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5478 $role = 'user';
5479 }
5480
5481 $formatted_conversation[] = array(
5482 'role' => $role,
5483 'content' => $message['content']
5484 );
5485 }
5486 }
5487
5488 $body = json_encode([
5489 'model' => $selected_model,
5490 'messages' => $formatted_conversation,
5491 'temperature' => 1,
5492 'stream' => false
5493 ]);
5494
5495 $args = [
5496 'body' => $body,
5497 'headers' => [
5498 'Content-Type' => 'application/json',
5499 'Authorization' => 'Bearer ' . $api_key,
5500 ],
5501 'timeout' => 60,
5502 'redirection' => 5,
5503 'blocking' => true,
5504 'httpversion' => '1.0',
5505 'sslverify' => true,
5506 ];
5507
5508 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
5509
5510 if (is_wp_error($response)) {
5511 $error_message = $response->get_error_message();
5512 //error_log('OpenAI API Error: ' . $error_message);
5513 return [
5514 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
5515 'error_code' => 'openai_connection_error',
5516 'provider' => 'openai'
5517 ];
5518 }
5519
5520 $status_code = wp_remote_retrieve_response_code($response);
5521 if ($status_code !== 200) {
5522 $response_body = wp_remote_retrieve_body($response);
5523 $decoded_response = json_decode($response_body, true);
5524
5525 $error_message = isset($decoded_response['error']['message'])
5526 ? $decoded_response['error']['message']
5527 : 'HTTP Error ' . $status_code;
5528
5529 $error_type = isset($decoded_response['error']['type'])
5530 ? $decoded_response['error']['type']
5531 : 'unknown';
5532
5533 //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5534
5535 // Handle specific error types
5536 switch ($error_type) {
5537 case 'invalid_request_error':
5538 if (strpos($error_message, 'API key') !== false) {
5539 return [
5540 'error' => esc_html__('Invalid OpenAI API key. Please check your API key configuration.', 'mxchat'),
5541 'error_code' => 'openai_invalid_api_key',
5542 'provider' => 'openai'
5543 ];
5544 }
5545 break;
5546
5547 case 'authentication_error':
5548 return [
5549 'error' => esc_html__('Authentication failed with OpenAI. Please check your API key.', 'mxchat'),
5550 'error_code' => 'openai_auth_error',
5551 'provider' => 'openai'
5552 ];
5553
5554 case 'rate_limit_exceeded':
5555 return [
5556 'error' => esc_html__('OpenAI rate limit exceeded. Please try again later.', 'mxchat'),
5557 'error_code' => 'openai_rate_limit',
5558 'provider' => 'openai'
5559 ];
5560
5561 case 'quota_exceeded':
5562 return [
5563 'error' => esc_html__('OpenAI API quota exceeded. Please check your billing details.', 'mxchat'),
5564 'error_code' => 'openai_quota_exceeded',
5565 'provider' => 'openai'
5566 ];
5567 }
5568
5569 // Generic error fallback
5570 return [
5571 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
5572 'error_code' => 'openai_api_error',
5573 'provider' => 'openai',
5574 'status_code' => $status_code
5575 ];
5576 }
5577
5578 $response_body = wp_remote_retrieve_body($response);
5579 $decoded_response = json_decode($response_body, true);
5580
5581 if (isset($decoded_response['choices'][0]['message']['content'])) {
5582 return trim($decoded_response['choices'][0]['message']['content']);
5583 } else {
5584 //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
5585 return [
5586 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
5587 'error_code' => 'openai_response_format_error',
5588 'provider' => 'openai'
5589 ];
5590 }
5591 } catch (Exception $e) {
5592 //error_log('OpenAI Exception: ' . $e->getMessage());
5593 return [
5594 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
5595 'error_code' => 'openai_exception',
5596 'provider' => 'openai'
5597 ];
5598 }
5599 }
5600 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
5601 try {
5602 // Get bot ID from session or request
5603 $bot_id = $this->get_current_bot_id($session_id);
5604
5605 // Get system prompt instructions using centralized function
5606 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5607
5608 // Add system prompt to relevant content
5609 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5610
5611 // Prepend system instructions to the conversation history
5612 array_unshift($conversation_history, [
5613 'role' => 'system',
5614 'content' => "Here are your instructions: " . $content_with_instructions
5615 ]);
5616
5617 // Ensure consistency: Replace 'bot' and 'agent' roles with supported values
5618 foreach ($conversation_history as &$message) {
5619 if ($message['role'] === 'bot') {
5620 $message['role'] = 'assistant';
5621 } elseif ($message['role'] === 'agent') {
5622 // Tag the message as coming from a live agent
5623 $message['role'] = 'assistant';
5624 if (!isset($message['metadata'])) {
5625 $message['metadata'] = ['source' => 'live_agent'];
5626 }
5627 }
5628
5629 // Ensure all roles are valid
5630 if (!in_array($message['role'], ['system', 'assistant', 'user', 'function', 'tool'])) {
5631 $message['role'] = 'user'; // Default to 'user'
5632 }
5633 }
5634
5635 // Build the request body
5636 $body = json_encode([
5637 'model' => $selected_model,
5638 'messages' => $conversation_history,
5639 'temperature' => 0.8,
5640 'stream' => false
5641 ]);
5642
5643 // Set up the API request
5644 $args = [
5645 'body' => $body,
5646 'headers' => [
5647 'Content-Type' => 'application/json',
5648 'Authorization' => 'Bearer ' . $xai_api_key,
5649 ],
5650 'timeout' => 60,
5651 'redirection' => 5,
5652 'blocking' => true,
5653 'httpversion' => '1.0',
5654 'sslverify' => true,
5655 ];
5656
5657 // Make the API request
5658 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
5659
5660 // Process the response
5661 if (is_wp_error($response)) {
5662 $error_message = $response->get_error_message();
5663 //error_log('X.AI API Error: ' . $error_message);
5664 return [
5665 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
5666 'error_code' => 'xai_connection_error',
5667 'provider' => 'xai'
5668 ];
5669 }
5670
5671 $status_code = wp_remote_retrieve_response_code($response);
5672 if ($status_code !== 200) {
5673 $response_body = wp_remote_retrieve_body($response);
5674 $decoded_response = json_decode($response_body, true);
5675
5676 // Log the full response for debugging
5677 //error_log('X.AI Error Response: ' . print_r($decoded_response, true));
5678
5679 // Extract error message from X.AI's specific format
5680 $error_message = '';
5681
5682 // Check for direct error string (as seen in your logs)
5683 if (isset($decoded_response['error']) && is_string($decoded_response['error'])) {
5684 $error_message = $decoded_response['error'];
5685 }
5686 // Check for nested error object (OpenAI style)
5687 elseif (isset($decoded_response['error']['message'])) {
5688 $error_message = $decoded_response['error']['message'];
5689 }
5690 // Check for top-level message
5691 elseif (isset($decoded_response['message'])) {
5692 $error_message = $decoded_response['message'];
5693 }
5694 // Fallback
5695 else {
5696 $error_message = 'HTTP Error ' . $status_code;
5697 }
5698
5699 //error_log('X.AI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5700
5701 // Check for API key errors using string matching
5702 if (stripos($error_message, 'api key') !== false ||
5703 stripos($error_message, 'incorrect api key') !== false ||
5704 stripos($error_message, 'invalid api key') !== false) {
5705 return [
5706 'error' => esc_html__('Invalid X.AI API key. Please check your API key configuration.', 'mxchat'),
5707 'error_code' => 'xai_invalid_api_key',
5708 'provider' => 'xai'
5709 ];
5710 }
5711
5712 // Authentication errors
5713 if ($status_code === 401 || $status_code === 403 ||
5714 stripos($error_message, 'auth') !== false) {
5715 return [
5716 'error' => esc_html__('Authentication failed with X.AI. Please check your API key.', 'mxchat'),
5717 'error_code' => 'xai_auth_error',
5718 'provider' => 'xai'
5719 ];
5720 }
5721
5722 // Model errors
5723 if (stripos($error_message, 'model') !== false) {
5724 return [
5725 'error' => esc_html__('Invalid model specified for X.AI. Please check your model configuration.', 'mxchat'),
5726 'error_code' => 'xai_invalid_model',
5727 'provider' => 'xai'
5728 ];
5729 }
5730
5731 // Rate limit errors
5732 if ($status_code === 429 ||
5733 stripos($error_message, 'rate') !== false ||
5734 stripos($error_message, 'limit') !== false) {
5735 return [
5736 'error' => esc_html__('X.AI rate limit exceeded. Please try again later.', 'mxchat'),
5737 'error_code' => 'xai_rate_limit',
5738 'provider' => 'xai'
5739 ];
5740 }
5741
5742 // Quota errors
5743 if (stripos($error_message, 'quota') !== false ||
5744 stripos($error_message, 'billing') !== false) {
5745 return [
5746 'error' => esc_html__('X.AI API quota exceeded. Please check your billing details.', 'mxchat'),
5747 'error_code' => 'xai_quota_exceeded',
5748 'provider' => 'xai'
5749 ];
5750 }
5751
5752 // Server errors
5753 if ($status_code >= 500) {
5754 return [
5755 'error' => esc_html__('X.AI service is currently unavailable. Please try again later.', 'mxchat'),
5756 'error_code' => 'xai_service_unavailable',
5757 'provider' => 'xai'
5758 ];
5759 }
5760
5761 // Generic error fallback with the actual error message
5762 return [
5763 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
5764 'error_code' => 'xai_api_error',
5765 'provider' => 'xai',
5766 'status_code' => $status_code
5767 ];
5768 }
5769
5770 $response_body = wp_remote_retrieve_body($response);
5771 $decoded_response = json_decode($response_body, true);
5772
5773 if (isset($decoded_response['choices'][0]['message']['content'])) {
5774 return trim($decoded_response['choices'][0]['message']['content']);
5775 } else {
5776 //error_log('X.AI API Response Format Error: ' . print_r($decoded_response, true));
5777 return [
5778 'error' => esc_html__('Unexpected response format from X.AI.', 'mxchat'),
5779 'error_code' => 'xai_response_format_error',
5780 'provider' => 'xai'
5781 ];
5782 }
5783 } catch (Exception $e) {
5784 //error_log('X.AI Exception: ' . $e->getMessage());
5785 return [
5786 'error' => esc_html__('System error when processing X.AI request: ', 'mxchat') . esc_html($e->getMessage()),
5787 'error_code' => 'xai_exception',
5788 'provider' => 'xai'
5789 ];
5790 }
5791
5792
5793 }
5794 private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
5795 try {
5796 // Ensure conversation_history is an array
5797 if (!is_array($conversation_history)) {
5798 $conversation_history = array();
5799 }
5800
5801 // Get bot ID from session or request
5802 $bot_id = $this->get_current_bot_id($session_id);
5803
5804 // Get system prompt instructions using centralized function
5805 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5806
5807 // Create a new array for the formatted conversation
5808 $formatted_conversation = array();
5809
5810 // Add system message first
5811 $formatted_conversation[] = array(
5812 'role' => 'system',
5813 'content' => $system_prompt_instructions . " " . $relevant_content
5814 );
5815
5816 // Add the rest of the conversation history
5817 foreach ($conversation_history as $message) {
5818 if (is_array($message) && isset($message['role']) && isset($message['content'])) {
5819 $role = $message['role'];
5820
5821 // Convert roles to supported format
5822 if ($role === 'bot' || $role === 'agent') {
5823 $role = 'assistant';
5824 }
5825 if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
5826 $role = 'user';
5827 }
5828
5829 $formatted_conversation[] = array(
5830 'role' => $role,
5831 'content' => $message['content']
5832 );
5833 }
5834 }
5835
5836 $body = json_encode([
5837 'model' => $selected_model,
5838 'messages' => $formatted_conversation,
5839 'temperature' => 0.8,
5840 'stream' => false
5841 ]);
5842
5843 $args = [
5844 'body' => $body,
5845 'headers' => [
5846 'Content-Type' => 'application/json',
5847 'Authorization' => 'Bearer ' . $deepseek_api_key,
5848 ],
5849 'timeout' => 60,
5850 'redirection' => 5,
5851 'blocking' => true,
5852 'httpversion' => '1.0',
5853 'sslverify' => true,
5854 ];
5855
5856 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
5857
5858 if (is_wp_error($response)) {
5859 $error_message = $response->get_error_message();
5860 //error_log('DeepSeek API Error: ' . $error_message);
5861 return [
5862 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
5863 'error_code' => 'deepseek_connection_error',
5864 'provider' => 'deepseek'
5865 ];
5866 }
5867
5868 $status_code = wp_remote_retrieve_response_code($response);
5869 if ($status_code !== 200) {
5870 $response_body = wp_remote_retrieve_body($response);
5871 $decoded_response = json_decode($response_body, true);
5872
5873 $error_message = isset($decoded_response['error']['message'])
5874 ? $decoded_response['error']['message']
5875 : 'HTTP Error ' . $status_code;
5876
5877 $error_type = isset($decoded_response['error']['type'])
5878 ? $decoded_response['error']['type']
5879 : 'unknown';
5880
5881 //error_log('DeepSeek API HTTP Error: ' . $status_code . ' - ' . $error_message);
5882
5883 // Handle specific error types
5884 switch ($status_code) {
5885 case 401:
5886 return [
5887 'error' => esc_html__('Authentication failed with DeepSeek. Please check your API key.', 'mxchat'),
5888 'error_code' => 'deepseek_auth_error',
5889 'provider' => 'deepseek'
5890 ];
5891
5892 case 400:
5893 if (strpos($error_message, 'API key') !== false) {
5894 return [
5895 'error' => esc_html__('Invalid DeepSeek API key. Please check your API key configuration.', 'mxchat'),
5896 'error_code' => 'deepseek_invalid_api_key',
5897 'provider' => 'deepseek'
5898 ];
5899 }
5900 break;
5901
5902 case 429:
5903 if (strpos($error_message, 'quota') !== false) {
5904 return [
5905 'error' => esc_html__('DeepSeek API quota exceeded. Please check your billing details.', 'mxchat'),
5906 'error_code' => 'deepseek_quota_exceeded',
5907 'provider' => 'deepseek'
5908 ];
5909 } else {
5910 return [
5911 'error' => esc_html__('DeepSeek rate limit exceeded. Please try again later.', 'mxchat'),
5912 'error_code' => 'deepseek_rate_limit',
5913 'provider' => 'deepseek'
5914 ];
5915 }
5916
5917 case 500:
5918 case 502:
5919 case 503:
5920 case 504:
5921 return [
5922 'error' => esc_html__('DeepSeek service is currently unavailable. Please try again later.', 'mxchat'),
5923 'error_code' => 'deepseek_service_unavailable',
5924 'provider' => 'deepseek'
5925 ];
5926 }
5927
5928 // Generic error fallback
5929 return [
5930 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
5931 'error_code' => 'deepseek_api_error',
5932 'provider' => 'deepseek',
5933 'status_code' => $status_code
5934 ];
5935 }
5936
5937 $response_body = wp_remote_retrieve_body($response);
5938 $decoded_response = json_decode($response_body, true);
5939
5940 if (isset($decoded_response['choices'][0]['message']['content'])) {
5941 return trim($decoded_response['choices'][0]['message']['content']);
5942 } else {
5943 //error_log('DeepSeek API Response Format Error: ' . print_r($decoded_response, true));
5944 return [
5945 'error' => esc_html__('Unexpected response format from DeepSeek.', 'mxchat'),
5946 'error_code' => 'deepseek_response_format_error',
5947 'provider' => 'deepseek'
5948 ];
5949 }
5950 } catch (Exception $e) {
5951 //error_log('DeepSeek Exception: ' . $e->getMessage());
5952 return [
5953 'error' => esc_html__('System error when processing DeepSeek request: ', 'mxchat') . esc_html($e->getMessage()),
5954 'error_code' => 'deepseek_exception',
5955 'provider' => 'deepseek'
5956 ];
5957 }
5958 }
5959 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
5960 // Get bot ID from session or request
5961 $bot_id = $this->get_current_bot_id($session_id);
5962
5963 // Get system prompt instructions using centralized function
5964 $system_prompt_instructions = $this->get_system_instructions($bot_id);
5965
5966 // Add system prompt to relevant content
5967 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
5968
5969 // Format messages for Gemini API
5970 $formatted_messages = [];
5971
5972 // Add system message as the first user message with role prefix
5973 // Note: Gemini doesn't have a dedicated system role, so we use a prefixed user message
5974 $formatted_messages[] = [
5975 'role' => 'user',
5976 'parts' => [
5977 ['text' => "[System Instructions] " . $content_with_instructions]
5978 ]
5979 ];
5980
5981 // Add model response to acknowledge system instructions
5982 $formatted_messages[] = [
5983 'role' => 'model',
5984 'parts' => [
5985 ['text' => "I understand and will follow these instructions."]
5986 ]
5987 ];
5988
5989 // Process the rest of the conversation history
5990 $current_role = null;
5991 $current_parts = [];
5992
5993 foreach ($conversation_history as $message) {
5994 // Skip the first system message as we already handled it
5995 if ($message['role'] === 'system') {
5996 continue;
5997 }
5998
5999 // Map roles to Gemini format
6000 $gemini_role = '';
6001 if ($message['role'] === 'user') {
6002 $gemini_role = 'user';
6003 } else if (in_array($message['role'], ['assistant', 'bot', 'agent'])) {
6004 $gemini_role = 'model';
6005 } else {
6006 // Skip unsupported roles
6007 continue;
6008 }
6009
6010 // If we have a new role, add the previous message
6011 if ($current_role !== null && $current_role !== $gemini_role && !empty($current_parts)) {
6012 $formatted_messages[] = [
6013 'role' => $current_role,
6014 'parts' => $current_parts
6015 ];
6016 $current_parts = [];
6017 }
6018
6019 // Set current role and add text to parts
6020 $current_role = $gemini_role;
6021 $current_parts[] = ['text' => $message['content']];
6022 }
6023
6024 // Add the last message if there's content
6025 if ($current_role !== null && !empty($current_parts)) {
6026 $formatted_messages[] = [
6027 'role' => $current_role,
6028 'parts' => $current_parts
6029 ];
6030 }
6031
6032 // Build the request body
6033 $body = json_encode([
6034 'contents' => $formatted_messages,
6035 'generationConfig' => [
6036 'temperature' => 0.7,
6037 'topP' => 0.95,
6038 'topK' => 40,
6039 'maxOutputTokens' => 8192,
6040 ],
6041 'safetySettings' => [
6042 [
6043 'category' => 'HARM_CATEGORY_HARASSMENT',
6044 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6045 ],
6046 [
6047 'category' => 'HARM_CATEGORY_HATE_SPEECH',
6048 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6049 ],
6050 [
6051 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
6052 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6053 ],
6054 [
6055 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT',
6056 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'
6057 ]
6058 ]
6059 ]);
6060
6061 // Prepare the API endpoint
6062 $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
6063
6064 // Set up the API request
6065 $args = [
6066 'body' => $body,
6067 'headers' => [
6068 'Content-Type' => 'application/json',
6069 ],
6070 'timeout' => 60,
6071 'redirection' => 5,
6072 'blocking' => true,
6073 'httpversion' => '1.0',
6074 'sslverify' => true,
6075 ];
6076
6077 // Make the API request
6078 $response = wp_remote_post($api_endpoint, $args);
6079
6080 // Process the response
6081 if (is_wp_error($response)) {
6082 return "Sorry, there was an error processing your request: " . $response->get_error_message();
6083 }
6084
6085 $response_body = json_decode(wp_remote_retrieve_body($response), true);
6086
6087 // Handle potential errors in the response
6088 if (isset($response_body['error'])) {
6089 //error_log('Gemini API Error: ' . json_encode($response_body['error']));
6090 return "Sorry, there was an error with the Gemini API: " .
6091 (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
6092 }
6093
6094 // Extract the response text
6095 if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
6096 return trim($response_body['candidates'][0]['content']['parts'][0]['text']);
6097 } else {
6098 //error_log('Unexpected Gemini API response format: ' . json_encode($response_body));
6099 return "Sorry, I couldn't process that request. The response format was unexpected.";
6100 }
6101 }
6102
6103
6104 public function test_streaming_request() {
6105 $options = get_option('mxchat_options', []);
6106 $model = $options['model'] ?? 'gpt-4o';
6107
6108 // Detect provider from model prefix
6109 $provider = strtolower(explode('-', $model)[0]);
6110
6111 $sample_prompt = 'Hello! Can you stream this response back to me?';
6112 $messages = [['role' => 'user', 'content' => $sample_prompt]];
6113 $headers = [];
6114 $body = [];
6115 $url = '';
6116 $api_key = '';
6117
6118 switch ($provider) {
6119 case 'gpt':
6120 case 'o1':
6121 $api_key = $options['api_key'] ?? '';
6122 if (empty($api_key)) return '❌ Missing API key for OpenAI';
6123 $url = 'https://api.openai.com/v1/chat/completions';
6124 $headers = [
6125 'Content-Type: application/json',
6126 'Authorization: Bearer ' . $api_key
6127 ];
6128 $body = [
6129 'model' => $model,
6130 'messages' => $messages,
6131 'stream' => true
6132 ];
6133 break;
6134
6135 case 'claude':
6136 $api_key = $options['claude_api_key'] ?? '';
6137 if (empty($api_key)) return '❌ Missing API key for Claude';
6138 $url = 'https://api.anthropic.com/v1/messages';
6139 $headers = [
6140 'Content-Type: application/json',
6141 'x-api-key: ' . $api_key,
6142 'anthropic-version: 2023-06-01'
6143 ];
6144 $body = [
6145 'model' => $model,
6146 'messages' => $messages,
6147 'max_tokens' => 100,
6148 'stream' => true
6149 ];
6150 break;
6151
6152 case 'grok':
6153 $api_key = $options['xai_api_key'] ?? '';
6154 if (empty($api_key)) return '❌ Missing API key for X.AI';
6155 $url = 'https://api.x.ai/v1/chat/completions';
6156 $headers = [
6157 'Content-Type: application/json',
6158 'Authorization: Bearer ' . $api_key
6159 ];
6160 $body = [
6161 'model' => $model,
6162 'messages' => $messages,
6163 'stream' => true
6164 ];
6165 break;
6166
6167 case 'deepseek':
6168 if (empty($deepseek_api_key)) {
6169 $error_response = [
6170 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
6171 'error_code' => 'missing_deepseek_api_key'
6172 ];
6173 if ($testing_data !== null) {
6174 $error_response['testing_data'] = $testing_data;
6175 }
6176 return $error_response;
6177 }
6178 if ($streaming) {
6179 return $this->mxchat_generate_response_deepseek_stream(
6180 $selected_model,
6181 $deepseek_api_key,
6182 $conversation_history,
6183 $relevant_content,
6184 $session_id,
6185 $testing_data // Pass testing data
6186 );
6187 } else {
6188 $response = $this->mxchat_generate_response_deepseek(
6189 $selected_model,
6190 $deepseek_api_key,
6191 $conversation_history,
6192 $relevant_content
6193 );
6194 }
6195 break;
6196
6197 case 'gemini':
6198 $api_key = $options['gemini_api_key'] ?? '';
6199 if (empty($api_key)) return '❌ Missing API key for Gemini';
6200 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
6201 $headers = ['Content-Type: application/json'];
6202 $body = [
6203 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
6204 'generationConfig' => ['temperature' => 0.7]
6205 ];
6206 break;
6207
6208 default:
6209 return '❌ Unsupported provider: ' . $provider;
6210 }
6211
6212 // Do the actual streaming test
6213 $ch = curl_init($url);
6214 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
6215 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
6216 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
6217 curl_setopt($ch, CURLOPT_TIMEOUT, 15);
6218 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6219
6220 $response = curl_exec($ch);
6221 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
6222 $error = curl_error($ch);
6223 curl_close($ch);
6224
6225 if ($error) return "❌ cURL error: $error";
6226 if ($http_code !== 200) {
6227 $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
6228 return "❌ HTTP $http_code: $error_message";
6229 }
6230
6231 return true;
6232 }
6233
6234 public function mxchat_dismiss_pre_chat_message() {
6235 // Get and sanitize the user identifier
6236 $user_id = $this->mxchat_get_user_identifier();
6237 $user_id = sanitize_key($user_id);
6238
6239 // Set a transient to track that the user has dismissed the pre-chat message
6240 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
6241 set_transient($transient_key, true, DAY_IN_SECONDS);
6242
6243 wp_send_json_success();
6244 }
6245
6246 public function mxchat_check_pre_chat_message_status() {
6247 // Get and sanitize the user identifier
6248 $user_id = $this->mxchat_get_user_identifier();
6249 $user_id = sanitize_key($user_id);
6250
6251 // Check if the transient exists (i.e., if the message was dismissed)
6252 $transient_key = 'mxchat_pre_chat_message_dismissed_' . $user_id;
6253 $dismissed = get_transient($transient_key);
6254
6255 // Log the result to see if it's being set correctly
6256 //error_log("Check pre-chat message dismissed for $user_id: " . ($dismissed ? 'Yes' : 'No'));
6257
6258 if ($dismissed) {
6259 wp_send_json_success(['dismissed' => true]);
6260 } else {
6261 wp_send_json_success(['dismissed' => false]);
6262 }
6263
6264 wp_die();
6265 }
6266
6267 private function mxchat_calculate_cosine_similarity($vectorA, $vectorB) {
6268 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
6269 return 0;
6270 }
6271
6272 $dotProduct = array_sum(array_map(function ($a, $b) {
6273 return $a * $b;
6274 }, $vectorA, $vectorB));
6275 $normA = sqrt(array_sum(array_map(function ($a) {
6276 return $a * $a;
6277 }, $vectorA)));
6278 $normB = sqrt(array_sum(array_map(function ($b) {
6279 return $b * $b;
6280 }, $vectorB)));
6281
6282 if ($normA == 0 || $normB == 0) {
6283 return 0;
6284 }
6285
6286 return $dotProduct / ($normA * $normB);
6287 }
6288
6289
6290 public function mxchat_enqueue_scripts_styles() {
6291 // Define version numbers for the styles and scripts
6292 $chat_style_version = '2.4.2';
6293 $chat_script_version = '2.4.2';
6294 // Enqueue the script
6295 wp_enqueue_script(
6296 'mxchat-chat-js',
6297 plugin_dir_url(__FILE__) . '../js/chat-script.js',
6298 array('jquery'),
6299 $chat_script_version,
6300 true
6301 );
6302 // Enqueue the CSS
6303 wp_enqueue_style(
6304 'mxchat-chat-css',
6305 plugin_dir_url(__FILE__) . '../css/chat-style.css',
6306 array(),
6307 $chat_style_version
6308 );
6309 // Fetch options from the database
6310 $this->options = get_option('mxchat_options');
6311 $prompts_options = get_option('mxchat_prompts_options', array());
6312
6313 // Prepare settings for JavaScript
6314 $style_settings = array(
6315 'ajax_url' => admin_url('admin-ajax.php'),
6316 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
6317 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
6318 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
6319 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
6320 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
6321 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
6322 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
6323 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
6324 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
6325 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
6326 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
6327 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
6328 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
6329 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
6330 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
6331 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
6332 'icon_color' => $this->options['icon_color'] ?? '#fff',
6333 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
6334 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
6335 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
6336 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
6337 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
6338 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
6339 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
6340 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
6341 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
6342 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
6343 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
6344 'initial_email_state' => null, // Also fixed this undefined variable
6345 'skip_email_check' => true,
6346 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
6347 );
6348 // Pass the settings to the script
6349 wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
6350 }
6351
6352
6353 /**
6354 * Setup the cron jobs for rate limits with guard against multiple calls
6355 */
6356 public function setup_rate_limit_cron_jobs() {
6357 // Add a guard to prevent multiple rapid calls
6358 $last_setup = get_transient('mxchat_cron_setup_guard');
6359 if ($last_setup && (time() - $last_setup) < 60) {
6360 // Don't run again if we ran less than 60 seconds ago
6361 return;
6362 }
6363
6364 // Set the guard
6365 set_transient('mxchat_cron_setup_guard', time(), 300); // 5 minutes
6366
6367 try {
6368 // First, check if WordPress cron is disabled
6369 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
6370 //error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
6371 $this->setup_fallback_rate_limit_system();
6372 return;
6373 }
6374
6375 // Check if cron is already scheduled - if so, don't mess with it
6376 if (wp_next_scheduled('mxchat_reset_rate_limits')) {
6377 //error_log('MxChat: Rate limit cron already scheduled, skipping setup');
6378 return;
6379 }
6380
6381 // Clear any orphaned hooks (but don't loop indefinitely)
6382 $hooks_to_clear = [
6383 'mxchat_reset_rate_limits',
6384 'mxchat_reset_hourly_rate_limits',
6385 'mxchat_reset_daily_rate_limits',
6386 'mxchat_reset_weekly_rate_limits',
6387 'mxchat_reset_monthly_rate_limits'
6388 ];
6389
6390 foreach ($hooks_to_clear as $hook) {
6391 // Only clear a maximum of 3 instances to prevent infinite loops
6392 $cleared = 0;
6393 while (wp_next_scheduled($hook) && $cleared < 3) {
6394 wp_clear_scheduled_hook($hook);
6395 $cleared++;
6396 }
6397 }
6398
6399 // Small delay after clearing
6400 usleep(100000); // 0.1 seconds
6401
6402 // Try to schedule the event
6403 $initial_time = time() + 300; // Start in 5 minutes
6404 $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
6405
6406 if ($result === false) {
6407 //error_log('MxChat: Failed to schedule cron, using fallback system');
6408 $this->setup_fallback_rate_limit_system();
6409 } else {
6410 //error_log('MxChat: Successfully scheduled rate limit reset cron');
6411 }
6412
6413 } catch (Exception $e) {
6414 //error_log('MxChat: Cron setup exception: ' . $e->getMessage());
6415 $this->setup_fallback_rate_limit_system();
6416 }
6417 }
6418
6419 /**
6420 * Try alternative cron scheduling methods
6421 */
6422 private function try_alternative_cron_scheduling($initial_time) {
6423 try {
6424 // Method 1: Try with current time instead of future time
6425 $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
6426 if ($result1 !== false) {
6427 //error_log('MxChat: Alternative method 1 (current time) succeeded');
6428 return true;
6429 }
6430
6431 // Method 2: Try with a different interval
6432 $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
6433 if ($result2 !== false) {
6434 //error_log('MxChat: Alternative method 2 (daily interval) succeeded');
6435 return true;
6436 }
6437
6438 // Method 3: Try wp_schedule_single_event first, then recurring
6439 $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
6440 if ($result3 !== false) {
6441 //error_log('MxChat: Alternative method 3 (single event) succeeded');
6442 // Schedule the next one manually in the handler
6443 return true;
6444 }
6445
6446 return false;
6447
6448 } catch (Exception $e) {
6449 //error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
6450 return false;
6451 }
6452 }
6453
6454 /**
6455 * Enhanced fallback rate limit system
6456 */
6457 private function setup_fallback_rate_limit_system() {
6458 // Set a flag to use database-based rate limit cleanup
6459 update_option('mxchat_use_fallback_rate_limits', true);
6460
6461 // Schedule a one-time check to happen on the next plugin load
6462 update_option('mxchat_next_rate_limit_check', time() + 3600);
6463
6464 // Also set up a more frequent fallback check (every 4 hours)
6465 update_option('mxchat_fallback_check_interval', 4 * 3600);
6466
6467 //error_log('MxChat: Fallback rate limit system activated');
6468 }
6469
6470 /**
6471 * Enhanced fallback check method
6472 */
6473 public function check_fallback_rate_limits() {
6474 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
6475
6476 if (!$use_fallback) {
6477 return; // Regular cron is working
6478 }
6479
6480 $next_check = get_option('mxchat_next_rate_limit_check', 0);
6481 $check_interval = get_option('mxchat_fallback_check_interval', 3600);
6482
6483 if (time() >= $next_check) {
6484 //error_log('MxChat: Running fallback rate limit cleanup');
6485 $this->mxchat_reset_rate_limits();
6486
6487 // Schedule next check
6488 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
6489 }
6490 }
6491 /**
6492 * Enhanced rate limit check that includes fallback cleanup
6493 */
6494 public function check_rate_limit() {
6495 // Check if we need to run fallback cleanup
6496 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
6497 $next_check = get_option('mxchat_next_rate_limit_check', 0);
6498
6499 if ($use_fallback && time() >= $next_check) {
6500 $this->mxchat_reset_rate_limits();
6501 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
6502 }
6503
6504 // Continue with your existing rate limit logic...
6505 $all_options = get_option('mxchat_options', []);
6506
6507 // Determine user role or if logged out
6508 if (is_user_logged_in()) {
6509 $user = wp_get_current_user();
6510 $user_id = $user->ID;
6511
6512 // Get the user's primary role using reset() to safely get the first element
6513 $user_roles = $user->roles;
6514
6515 // Safely get the first role regardless of array key structure
6516 if (!empty($user_roles) && is_array($user_roles)) {
6517 $role = reset($user_roles); // This safely gets the first element regardless of key
6518 } else {
6519 $role = 'subscriber'; // Default to subscriber if no role found
6520 }
6521 } else {
6522 $role = 'logged_out';
6523 // Use IP address for non-logged-in users
6524 $user_id = $this->get_client_ip();
6525 }
6526
6527 // Check if rate limits are configured for this role
6528 if (!isset($all_options['rate_limits'][$role])) {
6529 return true; // No limit set for this role
6530 }
6531
6532 $limit = $all_options['rate_limits'][$role]['limit'];
6533
6534 // If unlimited, return true immediately
6535 if ($limit === 'unlimited') {
6536 return true;
6537 }
6538
6539 // Get the option name for this user/role with safer naming
6540 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
6541 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
6542 $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
6543
6544 // Get the counter data
6545 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
6546
6547 // If first request or counter reset needed, set the initial timestamp
6548 if ($limit_data['count'] === 0) {
6549 $limit_data['timestamp'] = time();
6550 update_option($option_name, $limit_data);
6551 }
6552
6553 // Get the timeframe
6554 $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
6555 $all_options['rate_limits'][$role]['timeframe'] : 'daily';
6556
6557 // Check if the counter needs to be reset based on timeframe
6558 $current_time = time();
6559 $timestamp = $limit_data['timestamp'];
6560 $should_reset = false;
6561
6562 switch ($timeframe) {
6563 case 'hourly':
6564 $should_reset = ($current_time - $timestamp) >= 3600; // 1 hour
6565 break;
6566 case 'daily':
6567 $should_reset = ($current_time - $timestamp) >= 86400; // 24 hours
6568 break;
6569 case 'weekly':
6570 $should_reset = ($current_time - $timestamp) >= 604800; // 7 days
6571 break;
6572 case 'monthly':
6573 $should_reset = ($current_time - $timestamp) >= 2592000; // 30 days
6574 break;
6575 }
6576
6577 // Reset the counter if the timeframe has passed
6578 if ($should_reset) {
6579 $limit_data = ['count' => 0, 'timestamp' => $current_time];
6580 update_option($option_name, $limit_data);
6581 }
6582
6583 // Check if user has exceeded their limit
6584 if ($limit_data['count'] >= intval($limit)) {
6585 // Get the custom message for this role
6586 $message = !empty($all_options['rate_limits'][$role]['message'])
6587 ? $all_options['rate_limits'][$role]['message']
6588 : __('Rate limit exceeded. Please try again later.', 'mxchat');
6589
6590 // Add timeframe information to the message if placeholders exist
6591 $timeframe_label = '';
6592 switch ($timeframe) {
6593 case 'hourly':
6594 $timeframe_label = __('hour', 'mxchat');
6595 break;
6596 case 'daily':
6597 $timeframe_label = __('day', 'mxchat');
6598 break;
6599 case 'weekly':
6600 $timeframe_label = __('week', 'mxchat');
6601 break;
6602 case 'monthly':
6603 $timeframe_label = __('month', 'mxchat');
6604 break;
6605 }
6606
6607 // Replace placeholders in the message
6608 $message = str_replace(
6609 ['{limit}', '{count}', '{remaining}', '{timeframe}'],
6610 [intval($limit), $limit_data['count'], max(0, intval($limit) - $limit_data['count']), $timeframe_label],
6611 $message
6612 );
6613
6614 // Process HTML links in the message
6615 $message = $this->process_rate_limit_message_html($message);
6616
6617 // Return error with the processed message
6618 return [
6619 'error' => true,
6620 'message' => $message
6621 ];
6622 }
6623
6624 // Increment the counter
6625 $limit_data['count']++;
6626 update_option($option_name, $limit_data);
6627
6628 return true;
6629 }
6630
6631 /**
6632 * Enhanced rate limit reset with better error handling
6633 */
6634 public function mxchat_reset_rate_limits() {
6635 try {
6636 global $wpdb;
6637 $all_options = get_option('mxchat_options', []);
6638 $current_time = time();
6639
6640 // Get rate limit options with a safer query and limit
6641 $option_names = $wpdb->get_col(
6642 $wpdb->prepare(
6643 "SELECT option_name FROM {$wpdb->options}
6644 WHERE option_name LIKE %s
6645 LIMIT 1000",
6646 'mxchat_chat_limit_%'
6647 )
6648 );
6649
6650 if (empty($option_names)) {
6651 return;
6652 }
6653
6654 $processed_count = 0;
6655 $max_processing_time = 30; // Maximum 30 seconds
6656 $start_time = time();
6657
6658 foreach ($option_names as $option_name) {
6659 // Check processing time limit
6660 if ((time() - $start_time) > $max_processing_time) {
6661 //error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
6662 break;
6663 }
6664
6665 // Parse the option name more safely
6666 if (!preg_match('/^mxchat_chat_limit_(.+)_(.+)$/', $option_name, $matches)) {
6667 continue;
6668 }
6669
6670 $role_and_user = $matches[1] . '_' . $matches[2];
6671 $parts = explode('_', $role_and_user);
6672
6673 if (count($parts) < 2) {
6674 continue;
6675 }
6676
6677 // Extract role (everything except the last part which is user ID)
6678 $user_id_part = array_pop($parts);
6679 $role = implode('_', $parts);
6680
6681 // Skip if role doesn't exist in our settings
6682 if (!isset($all_options['rate_limits'][$role])) {
6683 // Clean up orphaned entries
6684 delete_option($option_name);
6685 continue;
6686 }
6687
6688 $timeframe = $all_options['rate_limits'][$role]['timeframe'] ?? 'daily';
6689 $limit_data = get_option($option_name);
6690
6691 if (!$limit_data || !is_array($limit_data) || !isset($limit_data['timestamp'])) {
6692 // Clean up invalid entries
6693 delete_option($option_name);
6694 continue;
6695 }
6696
6697 $timestamp = $limit_data['timestamp'];
6698 $should_reset = false;
6699
6700 // Determine if we should reset based on the timeframe
6701 switch ($timeframe) {
6702 case 'hourly':
6703 $should_reset = ($current_time - $timestamp) >= 3600;
6704 break;
6705 case 'daily':
6706 $should_reset = ($current_time - $timestamp) >= 86400;
6707 break;
6708 case 'weekly':
6709 $should_reset = ($current_time - $timestamp) >= 604800;
6710 break;
6711 case 'monthly':
6712 $should_reset = ($current_time - $timestamp) >= 2592000;
6713 break;
6714 }
6715
6716 // Reset the counter if the timeframe has passed
6717 if ($should_reset) {
6718 delete_option($option_name);
6719 wp_cache_delete($option_name, 'options');
6720 $processed_count++;
6721 }
6722 }
6723
6724 // Clean up any orphaned cache entries
6725 wp_cache_delete('mxchat_all_chat_limits', 'options');
6726
6727 //error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
6728
6729 } catch (Exception $e) {
6730 //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
6731 }
6732 }
6733
6734
6735 /**
6736 * Process HTML links in rate limit messages
6737 *
6738 * @param string $message The rate limit message
6739 * @return string The processed message with safe HTML links
6740 */
6741 private function process_rate_limit_message_html($message) {
6742 // Return original message if empty
6743 if (empty($message)) {
6744 return $message;
6745 }
6746
6747 // First, convert markdown links to HTML
6748 $message = $this->convert_markdown_links($message);
6749
6750 // Then, auto-convert any remaining plain URLs to links
6751 $message = $this->auto_link_urls($message);
6752
6753 // Allow basic HTML tags for links and formatting
6754 $allowed_tags = [
6755 'a' => [
6756 'href' => true,
6757 'target' => true,
6758 'rel' => true,
6759 'title' => true,
6760 'class' => true
6761 ],
6762 'strong' => [],
6763 'em' => [],
6764 'br' => [],
6765 'b' => [],
6766 'i' => [],
6767 'span' => ['class' => true]
6768 ];
6769
6770 // Sanitize but allow the specified HTML tags
6771 $processed_message = wp_kses($message, $allowed_tags);
6772
6773 // If wp_kses stripped everything, return the original message as plain text
6774 if (empty($processed_message) && !empty($message)) {
6775 // Strip all HTML and return plain text as fallback
6776 return wp_strip_all_tags($message);
6777 }
6778
6779 return $processed_message;
6780 }
6781
6782 /**
6783 * Convert markdown links to HTML
6784 *
6785 * @param string $text The text to process
6786 * @return string The text with markdown links converted to HTML
6787 */
6788 private function convert_markdown_links($text) {
6789 // Return original text if empty
6790 if (empty($text)) {
6791 return $text;
6792 }
6793
6794 // Pattern to match markdown links: [text](url)
6795 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
6796
6797 $processed_text = preg_replace_callback($pattern, function($matches) {
6798 $link_text = $matches[1];
6799 $url = $matches[2];
6800
6801 // Clean up any trailing punctuation from the URL
6802 $url = rtrim($url, '.,;:!?');
6803
6804 // Sanitize the link text and URL
6805 $safe_text = esc_html($link_text);
6806 $safe_url = esc_url($url);
6807
6808 // Create the HTML link
6809 return '<a href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">' . $safe_text . '</a>';
6810 }, $text);
6811
6812 // If preg_replace_callback failed, return original text
6813 if ($processed_text === null) {
6814 return $text;
6815 }
6816
6817 return $processed_text;
6818 }
6819
6820 /**
6821 * Auto-convert plain URLs to clickable links
6822 *
6823 * @param string $text The text to process
6824 * @return string The text with URLs converted to links
6825 */
6826 private function auto_link_urls($text) {
6827 // Return original text if empty
6828 if (empty($text)) {
6829 return $text;
6830 }
6831
6832 // Simple pattern that avoids complex lookbehinds
6833 // This will match URLs that are not already inside href attributes or markdown links
6834 $pattern = '/(?<!href=["\'])(?<!\]\()https?:\/\/[^\s<>"\')\]]+/i';
6835
6836 $processed_text = preg_replace_callback($pattern, function($matches) {
6837 $url = $matches[0];
6838 // Clean up any trailing punctuation that might have been captured
6839 $url = rtrim($url, '.,;:!?');
6840
6841 // Add target="_blank" and rel="noopener noreferrer" for security
6842 return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';
6843 }, $text);
6844
6845 // If preg_replace_callback failed, return original text
6846 if ($processed_text === null) {
6847 return $text;
6848 }
6849
6850 return $processed_text;
6851 }
6852
6853
6854 // Helper function to get client IP address
6855 private function get_client_ip() {
6856 // Check for shared internet/ISP IP
6857 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
6858 return sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
6859 }
6860
6861 // Check for IPs passing through proxies
6862 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
6863 // Use the first value in the comma-separated list
6864 $forwarded_for = explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']));
6865 return trim($forwarded_for[0]);
6866 }
6867
6868 if (!empty($_SERVER['REMOTE_ADDR'])) {
6869 return sanitize_text_field($_SERVER['REMOTE_ADDR']);
6870 }
6871
6872 // Fallback
6873 return 'unknown';
6874 }
6875
6876 /**
6877 * AJAX handler to get system information for testing panel
6878 */
6879 public function mxchat_get_system_info() {
6880 // Verify nonce for security
6881 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6882 wp_send_json_error(['message' => 'Invalid nonce']);
6883 return;
6884 }
6885
6886 // Only allow admin users
6887 if (!current_user_can('administrator')) {
6888 wp_send_json_error(['message' => 'Unauthorized']);
6889 return;
6890 }
6891
6892 // Get system prompt from options
6893 $system_prompt = isset($this->options['system_prompt_instructions'])
6894 ? $this->options['system_prompt_instructions']
6895 : 'No system prompt configured';
6896
6897 // Get selected model
6898 $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
6899
6900 // Get API key status (just check if they exist, don't expose the keys)
6901 $api_status = [];
6902 $api_status['openai'] = !empty($this->options['api_key']);
6903 $api_status['claude'] = !empty($this->options['claude_api_key']);
6904 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
6905 $api_status['xai'] = !empty($this->options['xai_api_key']);
6906 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
6907
6908 wp_send_json_success([
6909 'system_prompt' => $system_prompt,
6910 'selected_model' => $selected_model,
6911 'api_status' => $api_status
6912 ]);
6913 }
6914
6915 /**
6916 * AJAX handler to get similarity threshold
6917 */
6918 public function mxchat_get_similarity_threshold() {
6919 // Verify nonce for security
6920 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6921 wp_send_json_error(['message' => 'Invalid nonce']);
6922 return;
6923 }
6924
6925 // Only allow admin users
6926 if (!current_user_can('administrator')) {
6927 wp_send_json_error(['message' => 'Unauthorized']);
6928 return;
6929 }
6930
6931 // Get similarity threshold from main options (default 75%)
6932 $similarity_threshold = isset($this->options['similarity_threshold'])
6933 ? ((int) $this->options['similarity_threshold']) / 100
6934 : 0.75;
6935
6936 wp_send_json_success([
6937 'threshold' => $similarity_threshold,
6938 'threshold_percentage' => ($similarity_threshold * 100) . '%'
6939 ]);
6940 }
6941
6942 /**
6943 * AJAX handler to get knowledge base status
6944 */
6945 public function mxchat_get_kb_status() {
6946 // Verify nonce for security
6947 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6948 wp_send_json_error(['message' => 'Invalid nonce']);
6949 return;
6950 }
6951
6952 // Only allow admin users
6953 if (!current_user_can('administrator')) {
6954 wp_send_json_error(['message' => 'Unauthorized']);
6955 return;
6956 }
6957
6958 // Check Pinecone vs WordPress
6959 $addon_options = get_option('mxchat_pinecone_addon_options', array());
6960 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
6961
6962 $kb_info = [
6963 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
6964 'status' => 'Active'
6965 ];
6966
6967 // Get document count
6968 if ($use_pinecone) {
6969 $kb_info['documents'] = 'Connected to Pinecone';
6970 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
6971 } else {
6972 // Count documents in WordPress database
6973 global $wpdb;
6974 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
6975 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
6976 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
6977 }
6978
6979 wp_send_json_success($kb_info);
6980 }
6981
6982 /**
6983 * AJAX handler to start a completely fresh session (NEW - replaces old clear session)
6984 */
6985 public function mxchat_start_fresh_session() {
6986 // Verify nonce for security
6987 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
6988 wp_send_json_error(['message' => 'Invalid nonce']);
6989 return;
6990 }
6991
6992 // Only allow admin users
6993 if (!current_user_can('administrator')) {
6994 wp_send_json_error(['message' => 'Unauthorized']);
6995 return;
6996 }
6997
6998 $old_session_id = isset($_POST['old_session_id']) ? sanitize_text_field($_POST['old_session_id']) : '';
6999 $new_session_id = isset($_POST['new_session_id']) ? sanitize_text_field($_POST['new_session_id']) : '';
7000
7001 if (empty($old_session_id)) {
7002 wp_send_json_error(['message' => 'Old session ID required']);
7003 return;
7004 }
7005
7006 // If no new session ID provided, generate one
7007 if (empty($new_session_id)) {
7008 $new_session_id = 'mxchat_chat_' . substr(md5(uniqid()), 0, 9);
7009 }
7010
7011 // Clear ALL data associated with the old session
7012 $this->clear_complete_session_data($old_session_id);
7013
7014 // Initialize the new session
7015 $this->initialize_fresh_session($new_session_id);
7016
7017 wp_send_json_success([
7018 'message' => 'Fresh session started successfully',
7019 'new_session_id' => $new_session_id,
7020 'old_session_id' => $old_session_id
7021 ]);
7022 }
7023
7024 /**
7025 * Clear ALL data associated with a session (ENHANCED)
7026 */
7027 private function clear_complete_session_data($session_id) {
7028 // Clear chat history
7029 delete_option("mxchat_history_{$session_id}");
7030
7031 // Clear chat mode
7032 delete_option("mxchat_mode_{$session_id}");
7033
7034 // Clear any PDF/Word transients
7035 $this->clear_pdf_transients($session_id);
7036 if (method_exists($this, 'clear_word_transients')) {
7037 $this->clear_word_transients($session_id);
7038 }
7039
7040 // Clear agent-related data
7041 delete_option("mxchat_channel_{$session_id}");
7042 delete_option("mxchat_agent_name_{$session_id}");
7043 delete_option("mxchat_email_{$session_id}");
7044
7045 // Clear any recommendation flow state
7046 delete_option("mxchat_sr_flow_state_{$session_id}");
7047
7048 // Clear any cached embeddings or context
7049 delete_transient("mxchat_context_{$session_id}");
7050 delete_transient("mxchat_last_query_{$session_id}");
7051
7052 // Clear any testing data
7053 delete_transient("mxchat_testing_data_{$session_id}");
7054
7055 // Clear any rate limiting data for this session
7056 delete_transient("mxchat_rate_limit_{$session_id}");
7057
7058 // Clear any other session-specific transients
7059 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
7060 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
7061 delete_transient("mxchat_include_word_in_context_{$session_id}");
7062
7063 //error_log("MxChat: Cleared all data for session: {$session_id}");
7064 }
7065
7066 /**
7067 * Initialize a fresh session with default data
7068 */
7069 private function initialize_fresh_session($session_id) {
7070 // Set default chat mode
7071 update_option("mxchat_mode_{$session_id}", 'ai');
7072
7073 //error_log("MxChat: Initialized fresh session: {$session_id}");
7074 }
7075
7076 /**
7077 * Helper method to clear Word document transients (if you have Word support)
7078 */
7079 private function clear_word_transients($session_id) {
7080 delete_transient('mxchat_word_url_' . $session_id);
7081 delete_transient('mxchat_word_filename_' . $session_id);
7082 delete_transient('mxchat_word_embeddings_' . $session_id);
7083 delete_transient('mxchat_include_word_in_context_' . $session_id);
7084 }
7085
7086 /**
7087 * Simplified testing data capture method (CLEANED UP)
7088 */
7089 private function capture_testing_data($user_embedding, $message, $session_id) {
7090 // Only capture for admin users
7091 if (!current_user_can('administrator')) {
7092 return null;
7093 }
7094
7095 $testing_data = [
7096 'query' => $message,
7097 'timestamp' => time(),
7098 'top_matches' => [],
7099 'action_matches' => [] // Add action matches
7100 ];
7101
7102 // Get similarity threshold
7103 $similarity_threshold = isset($this->options['similarity_threshold'])
7104 ? ((int) $this->options['similarity_threshold']) / 100
7105 : 0.75;
7106
7107 $testing_data['similarity_threshold'] = $similarity_threshold;
7108
7109 // Use the real similarity analysis if available
7110 if ($this->last_similarity_analysis !== null) {
7111 $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
7112 $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
7113 $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7114 } else {
7115 // Fallback: determine knowledge base type
7116 $addon_options = get_option('mxchat_pinecone_addon_options', array());
7117 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
7118
7119 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
7120 }
7121
7122 // Include action analysis if available
7123 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
7124 $testing_data['action_matches'] = $this->last_action_analysis;
7125
7126 // Clear it after capturing to avoid stale data
7127 $this->last_action_analysis = null;
7128 }
7129
7130 return $testing_data;
7131 }
7132
7133
7134 /**
7135 * Track URL clicks from chatbot responses
7136 */
7137 public function mxchat_track_url_click() {
7138 // Verify nonce for security
7139 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7140 wp_send_json_error(['message' => 'Invalid nonce']);
7141 wp_die();
7142 }
7143
7144 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7145 $clicked_url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
7146 $message_context = isset($_POST['message_context']) ? sanitize_textarea_field($_POST['message_context']) : '';
7147
7148 if (empty($session_id) || empty($clicked_url)) {
7149 wp_send_json_error(['message' => 'Missing required data']);
7150 wp_die();
7151 }
7152
7153 global $wpdb;
7154 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
7155
7156 // Insert click tracking record
7157 $wpdb->insert(
7158 $table_name,
7159 [
7160 'session_id' => $session_id,
7161 'clicked_url' => $clicked_url,
7162 'message_context' => $message_context,
7163 'click_timestamp' => current_time('mysql', 1),
7164 'user_ip' => $_SERVER['REMOTE_ADDR'],
7165 'user_agent' => $_SERVER['HTTP_USER_AGENT']
7166 ]
7167 );
7168
7169 wp_send_json_success(['message' => 'Click tracked']);
7170 wp_die();
7171 }
7172
7173 /**
7174 * Get URL click analytics for a session
7175 */
7176 public function mxchat_get_url_clicks($session_id) {
7177 global $wpdb;
7178 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
7179
7180 $clicks = $wpdb->get_results($wpdb->prepare(
7181 "SELECT * FROM $table_name WHERE session_id = %s ORDER BY click_timestamp ASC",
7182 $session_id
7183 ));
7184
7185 return $clicks;
7186 }
7187 /**
7188 * Track the originating page where chat was started
7189 */
7190 public function mxchat_track_originating_page() {
7191 // Verify nonce
7192 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7193 wp_send_json_error(['message' => 'Invalid nonce']);
7194 wp_die();
7195 }
7196
7197 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7198 $page_url = isset($_POST['page_url']) ? esc_url_raw($_POST['page_url']) : '';
7199 $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';
7200
7201 if (empty($session_id)) {
7202 wp_send_json_error(['message' => 'Missing session ID']);
7203 wp_die();
7204 }
7205
7206 global $wpdb;
7207 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
7208
7209 // Check if we've already tracked for this session
7210 $existing = $wpdb->get_var($wpdb->prepare(
7211 "SELECT COUNT(*) FROM $table_name
7212 WHERE session_id = %s
7213 AND originating_page_url IS NOT NULL",
7214 $session_id
7215 ));
7216
7217 if ($existing > 0) {
7218 wp_send_json_success(['message' => 'Already tracked']);
7219 wp_die();
7220 }
7221
7222 // Update the first message in this session with originating page info
7223 $wpdb->query($wpdb->prepare(
7224 "UPDATE $table_name
7225 SET originating_page_url = %s,
7226 originating_page_title = %s
7227 WHERE session_id = %s
7228 ORDER BY timestamp ASC
7229 LIMIT 1",
7230 $page_url,
7231 $page_title,
7232 $session_id
7233 ));
7234
7235 wp_send_json_success(['message' => 'Originating page tracked']);
7236 wp_die();
7237 }
7238
7239
7240 /**
7241 * AJAX handler to get current chat mode for a session
7242 */
7243 public function mxchat_get_current_chat_mode() {
7244 // Verify nonce for security
7245 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
7246 wp_send_json_error(['message' => 'Invalid nonce']);
7247 wp_die();
7248 }
7249
7250 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
7251
7252 if (empty($session_id)) {
7253 wp_send_json_error(['message' => 'Session ID missing']);
7254 wp_die();
7255 }
7256
7257 // Get the current chat mode for this session
7258 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
7259
7260 wp_send_json_success([
7261 'chat_mode' => $chat_mode
7262 ]);
7263 wp_die();
7264 }
7265
7266
7267
7268 }
7269 ?>
7270