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

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

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