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

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

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