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

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

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