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

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