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

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