PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.9
MxChat – AI Chatbot & Content Generation for WordPress v2.3.9
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-integrator.php +1506 -6296 3.2.92.3.9 View file →
@@ -10,247 +10,11 @@
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 12 private $word_handler;
13 13 private $last_similarity_analysis = null;
14 - private $current_valid_urls = [];
15 - private $last_vectorstore_error = null;
16 - private $is_streaming = false; // ADDED: Track if current request is streaming
17 - private $streaming_headers_sent = false; // Track if streaming headers have been sent
18 - private $pending_originating_page = null; // Originating page captured at session start, consumed on row insert
19 - private $current_action_instruction = null; // Success-message instruction injected into the next system context
20 - private $last_action_analysis = null; // Last action-match analysis for testing_data payloads
21 14
22 -/**
23 - * Setup streaming headers - call this right before actually streaming
24 - * This delays header setup to allow actions/forms to return JSON responses
25 - */
26 -/**
27 - * Auto-retry wrapper around wp_remote_post for chat-send provider calls.
28 - *
29 - * Retries up to twice (750ms then 2000ms backoff) when the upstream provider
30 - * returns a TRANSIENT error: WP timeout, 429, 502, 503, 504, or a provider-
31 - * specific "overloaded" / "rate limit" body string. Returns immediately on
32 - * permanent errors (401/403/404/422) so misconfiguration surfaces fast.
33 - *
34 - * Drop-in replacement for wp_remote_post — returns the same shape
35 - * (WP_Error or response array) so the caller's existing error-handling
36 - * code path is unchanged.
37 - *
38 - * STREAMING PATH NOTE: this helper is ONLY for non-streaming chat-send
39 - * paths (the *_response_openai / *_response_claude / etc functions).
40 - * For the *_stream variants, the cURL initial-connect happens inside a
41 - * read-chunks loop — retrying there safely (without re-emitting partial
42 - * stream chunks to the client) is a separate problem. Streaming paths
43 - * are NOT wrapped in this build; tracked as a follow-on.
44 - *
45 - * Honors the `mxchat_options['auto_retry_on_transient_error']` toggle
46 - * (default true). When false, behavior is identical to plain wp_remote_post.
47 - */
48 -private function mxchat_provider_call_with_retry($url, $args, $provider_hint = '') {
49 - $opts = is_array($this->options ?? null) ? $this->options : array();
50 - $enabled = !isset($opts['auto_retry_on_transient_error']) ||
51 - (string) $opts['auto_retry_on_transient_error'] !== '0';
52 15
53 - if (!$enabled) {
54 - return wp_remote_post($url, $args);
55 - }
56 -
57 - $backoffs = array(0, 750, 2000); // ms — first attempt 0, then retry waits
58 - $last_response = null;
59 -
60 - foreach ($backoffs as $i => $delay_ms) {
61 - if ($delay_ms > 0) {
62 - usleep($delay_ms * 1000);
63 - }
64 - $response = wp_remote_post($url, $args);
65 - $last_response = $response;
66 -
67 - if (!$this->mxchat_is_transient_provider_error($response, $provider_hint)) {
68 - return $response;
69 - }
70 -
71 - if (defined('WP_DEBUG') && WP_DEBUG) {
72 - $code_for_log = is_wp_error($response) ? 'wp_error:' . $response->get_error_code()
73 - : (int) wp_remote_retrieve_response_code($response);
74 - error_log(sprintf(
75 - '[MxChat] Transient provider error (provider=%s, attempt=%d/3, status=%s). %s',
76 - $provider_hint ?: 'unknown',
77 - $i + 1,
78 - $code_for_log,
79 - ($i + 1) < count($backoffs) ? 'Retrying.' : 'Giving up.'
80 - ));
81 - }
82 - }
83 -
84 - return $last_response;
85 -}
86 -
87 16 /**
88 - * Returns true if a wp_remote_post response represents a TRANSIENT
89 - * provider error worth retrying. Conservative — only retries on signals
90 - * that are very likely to clear within a few seconds.
91 - *
92 - * Transient signals:
93 - * - WP_Error with timeout / connection / dns / ssl
94 - * - HTTP 429, 502, 503, 504
95 - * - Provider-specific overload bodies (gemini "overloaded", openai
96 - * "server_error", anthropic "overloaded_error", xai/grok "Rate limit")
97 - *
98 - * NOT transient (return false — fail-fast):
99 - * - 200/2xx (success)
100 - * - 401, 403, 404, 422 (auth / config errors — retrying wastes the
101 - * budget; the user needs to fix something)
102 - * - Any other 4xx (assume permanent unless explicitly listed above)
103 - * - 5xx other than the four listed above (e.g. 500 generic server error
104 - * is often a malformed request on our side, not a transient outage)
105 - */
106 -private function mxchat_is_transient_provider_error($response, $provider_hint = '') {
107 - if (is_wp_error($response)) {
108 - $code = $response->get_error_code();
109 - return in_array($code, array('http_request_failed', 'connection_failed', 'connection_timeout'), true)
110 - || stripos((string) $response->get_error_message(), 'timed out') !== false
111 - || stripos((string) $response->get_error_message(), 'timeout') !== false;
112 - }
113 -
114 - $status = (int) wp_remote_retrieve_response_code($response);
115 - if (in_array($status, array(429, 502, 503, 504), true)) {
116 - return true;
117 - }
118 - if ($status >= 200 && $status < 300) {
119 - return false;
120 - }
121 - // Permanent 4xx that should fail fast — even with no body.
122 - if (in_array($status, array(401, 403, 404, 405, 422), true)) {
123 - return false;
124 - }
125 -
126 - // Provider-specific body inspection for the cases where the upstream
127 - // returns 200 with an error envelope (gemini does this for overload).
128 - $body = (string) wp_remote_retrieve_body($response);
129 - if ($body === '') {
130 - return false;
131 - }
132 - $lower = strtolower($body);
133 - $hint = strtolower((string) $provider_hint);
134 -
135 - if ($hint === 'gemini' && (strpos($lower, 'overloaded') !== false
136 - || strpos($lower, 'high demand') !== false
137 - || strpos($lower, 'model is overloaded') !== false)) {
138 - return true;
139 - }
140 - if ($hint === 'openai' && (strpos($lower, 'rate limit reached') !== false
141 - || strpos($lower, '"type":"server_error"') !== false
142 - || strpos($lower, '"code":"server_error"') !== false)) {
143 - return true;
144 - }
145 - if ($hint === 'anthropic' && (strpos($lower, '"type":"overloaded_error"') !== false
146 - || strpos($lower, 'overloaded_error') !== false)) {
147 - return true;
148 - }
149 - if (($hint === 'xai' || $hint === 'grok') && strpos($lower, 'rate limit') !== false) {
150 - return true;
151 - }
152 -
153 - return false;
154 -}
155 -
156 -/**
157 - * Streaming-path classifier: same rules as mxchat_is_transient_provider_error
158 - * but takes a raw (http_code, body, provider_hint, curl_errno) tuple as
159 - * captured during a cURL streaming exec. cURL's WRITEFUNCTION/HEADERFUNCTION
160 - * collect status separately from a plain wp_remote_post array shape, so the
161 - * non-streaming helper above can't be called directly. This delegate keeps
162 - * the classification rules identical across both paths.
163 - */
164 -private function mxchat_is_transient_provider_error_raw($http_code, $body, $provider_hint = '', $curl_errno = 0) {
165 - if ($curl_errno) {
166 - // cURL transport-level error (timeout, connection failure, DNS, etc.)
167 - // Match the same WP_Error timeout/connection signals the array variant treats as transient.
168 - return in_array($curl_errno, array(
169 - CURLE_OPERATION_TIMEDOUT,
170 - CURLE_COULDNT_CONNECT,
171 - CURLE_COULDNT_RESOLVE_HOST,
172 - CURLE_SSL_CONNECT_ERROR,
173 - CURLE_GOT_NOTHING,
174 - CURLE_SEND_ERROR,
175 - CURLE_RECV_ERROR,
176 - ), true);
177 - }
178 -
179 - $status = (int) $http_code;
180 - if (in_array($status, array(429, 502, 503, 504), true)) {
181 - return true;
182 - }
183 - if ($status >= 200 && $status < 300) {
184 - return false;
185 - }
186 - if (in_array($status, array(401, 403, 404, 405, 422), true)) {
187 - return false;
188 - }
189 -
190 - $body = (string) $body;
191 - if ($body === '') {
192 - return false;
193 - }
194 - $lower = strtolower($body);
195 - $hint = strtolower((string) $provider_hint);
196 -
197 - if ($hint === 'gemini' && (strpos($lower, 'overloaded') !== false
198 - || strpos($lower, 'high demand') !== false
199 - || strpos($lower, 'model is overloaded') !== false)) {
200 - return true;
201 - }
202 - if ($hint === 'openai' && (strpos($lower, 'rate limit reached') !== false
203 - || strpos($lower, '"type":"server_error"') !== false
204 - || strpos($lower, '"code":"server_error"') !== false)) {
205 - return true;
206 - }
207 - if ($hint === 'anthropic' && (strpos($lower, '"type":"overloaded_error"') !== false
208 - || strpos($lower, 'overloaded_error') !== false)) {
209 - return true;
210 - }
211 - if (($hint === 'xai' || $hint === 'grok') && strpos($lower, 'rate limit') !== false) {
212 - return true;
213 - }
214 -
215 - return false;
216 -}
217 -
218 -/**
219 - * Whether transient-error auto-retry is enabled in admin settings.
220 - * Default true unless explicitly set to '0'. Used by both wp_remote_post
221 - * (mxchat_provider_call_with_retry) and cURL streaming paths.
222 - */
223 -private function mxchat_retry_enabled() {
224 - $opts = is_array($this->options ?? null) ? $this->options : array();
225 - return !isset($opts['auto_retry_on_transient_error']) ||
226 - (string) $opts['auto_retry_on_transient_error'] !== '0';
227 -}
228 -
229 -private function setup_streaming_headers() {
230 - if ($this->streaming_headers_sent || headers_sent()) {
231 - return false;
232 - }
233 -
234 - // Disable output buffering
235 - while (ob_get_level()) {
236 - ob_end_flush();
237 - }
238 -
239 - // Set headers for SSE
240 - header('Content-Type: text/event-stream');
241 - header('Cache-Control: no-cache');
242 - header('Connection: keep-alive');
243 - header('X-Accel-Buffering: no');
244 -
245 - ob_implicit_flush(true);
246 - flush();
247 -
248 - $this->streaming_headers_sent = true;
249 - return true;
250 -}
251 -
252 -/**
253 17 * Class constructor
254 18 */
255 19 public function __construct() {
256 20 $this->options = get_option('mxchat_options');
@@ -312,98 +76,16 @@
312 76 // Add to your existing constructor, in the section with other AJAX actions:
313 77 add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
314 78 add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
315 79 add_action('wp_ajax_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
316 - add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
317 - // Add chat mode checking actions
318 - add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
319 - add_action('wp_ajax_nopriv_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
80 +add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
81 +
320 82
321 - // Nonce refresh for page-cache compatibility (WP Rocket, LiteSpeed, etc.)
322 - add_action('wp_ajax_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
323 - add_action('wp_ajax_nopriv_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
324 -
325 - // Auto-email transcript action
326 - add_action('mxchat_send_delayed_transcript', array($this, 'mxchat_send_delayed_transcript'), 10, 1);
327 -
328 83 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
329 84
330 85
331 86 }
332 87
333 -/**
334 - * Return a fresh nonce so cached pages can replace the stale one.
335 - * With `with_settings`, also returns the current behavior-gate settings so
336 - * the widget can correct stale inline-localized values (plan-32db95).
337 - */
338 -public function mxchat_refresh_nonce() {
339 - nocache_headers();
340 - $payload = array('nonce' => wp_create_nonce('mxchat_chat_nonce'));
341 - if (!empty($_REQUEST['with_settings'])) {
342 - $payload['settings'] = $this->get_dynamic_widget_settings(true);
343 - }
344 - wp_send_json_success($payload);
345 -}
346 -
347 -/**
348 - * Behavior-gate settings the widget may re-fetch at runtime (plan-32db95).
349 - *
350 - * Every widget setting ships inline in page HTML via wp_localize_script, so
351 - * full-page caches (host caches, WP Rocket, LiteSpeed, W3TC, FlyingPress,
352 - * WP Super Cache, Cloudflare APO, the browser itself) keep serving a stale
353 - * snapshot after an admin changes a setting. MxChat_Cache_Purge clears the
354 - * caches PHP can reach; this payload covers the rest — the widget requests
355 - * it on first open (via the nonce-refresh endpoints) and merges it over
356 - * `mxchatChat`, the same distrust-cached-HTML pattern the 3.2.7 per-request
357 - * nonce uses.
358 - *
359 - * Behavior gates + labels ONLY — colors stay inline because they're also
360 - * server-inline-styled, and a runtime swap would visibly flash.
361 - *
362 - * Both wp_localize_script blocks merge this exact array, so the inline and
363 - * refreshed payloads cannot drift.
364 - *
365 - * @param bool $fresh Re-read mxchat_options from the DB (endpoint paths)
366 - * instead of trusting the instance copy.
367 - * @return array
368 - */
369 -public function get_dynamic_widget_settings($fresh = false) {
370 - $options = $fresh ? get_option('mxchat_options', array()) : $this->options;
371 - if (!is_array($options)) {
372 - $options = array();
373 - }
374 - return array(
375 - 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
376 - 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
377 - 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
378 - 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
379 - 'print_button_enabled' => $options['print_button_enabled'] ?? 'on',
380 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
381 - 'stop_button_label' => esc_html__('Stop response', 'mxchat'),
382 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
383 - // Emit 'on'/'off' STRINGS, never booleans: wp_localize_script casts
384 - // scalars to string, and (string) false === '' — which the widget's
385 - // old gate read as enabled (plan-4bba64). The filter keeps its
386 - // boolean contract; only the emitted value is stringified.
387 - 'satisfaction_rating_enabled' => apply_filters(
388 - 'mxchat_satisfaction_rating_enabled',
389 - ($options['satisfaction_rating_enabled'] ?? 'off') === 'on'
390 - ) ? 'on' : 'off',
391 - 'satisfaction_rating_idle_seconds' => max(5, min(600, intval($options['satisfaction_rating_idle_seconds'] ?? 60))),
392 - 'satisfaction_rating_copy' => array(
393 - 'question' => !empty($options['satisfaction_rating_question']) ? esc_html($options['satisfaction_rating_question']) : esc_html__('Was this helpful?', 'mxchat'),
394 - 'helpful' => esc_html__('Helpful', 'mxchat'),
395 - 'not_helpful' => esc_html__('Not helpful', 'mxchat'),
396 - 'dismiss' => esc_html__('Dismiss', 'mxchat'),
397 - 'thanks' => !empty($options['satisfaction_rating_thanks']) ? esc_html($options['satisfaction_rating_thanks']) : esc_html__('Thanks! Anything we should improve? (optional)', 'mxchat'),
398 - 'placeholder' => !empty($options['satisfaction_rating_placeholder']) ? esc_html($options['satisfaction_rating_placeholder']) : esc_html__('Tell us what could be better…', 'mxchat'),
399 - 'send' => esc_html__('Send', 'mxchat'),
400 - 'skip' => esc_html__('Skip', 'mxchat'),
401 - 'saved' => !empty($options['satisfaction_rating_saved']) ? esc_html($options['satisfaction_rating_saved']) : esc_html__('Thanks for the feedback.', 'mxchat'),
402 - ),
403 - );
404 -}
405 -
406 88 // In your core plugin's check_actions_for_addons method:
407 89 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
408 90 //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
409 91
@@ -426,22 +108,8 @@
426 108 wp_die();
427 109 }
428 110
429 111 $session_id = sanitize_text_field($_POST['session_id']);
430 -
431 - // SECURITY FIX: Verify session ownership before retrieving data
432 - // If IP/user changed, signal frontend to reset session instead of blocking
433 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
434 -
435 - // Check if this session has an owner recorded
436 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
437 -
438 - // Update session owner if it changed (e.g. IP changed due to network switch)
439 - // The session ID itself is the authentication — if the client has it, they own it
440 - if (!$session_owner || $session_owner !== $current_user_identifier) {
441 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
442 - }
443 -
444 112 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
445 113 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
446 114
447 115 if (empty($history)) {
@@ -458,25 +126,11 @@
458 126 'chat_mode' => $chat_mode
459 127 ]);
460 128 wp_die();
461 129 }
462 -private function mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp = 0) {
130 +
131 +private function mxchat_fetch_conversation_history_for_ai($session_id) {
463 132 $history = get_option("mxchat_history_{$session_id}", []);
464 -
465 - // Check persistence setting - when OFF, only include messages from current page load
466 - $options = get_option('mxchat_options', []);
467 - $persistence_enabled = isset($options['chat_persistence_toggle']) && $options['chat_persistence_toggle'] === 'on';
468 -
469 - // Filter history when persistence is OFF to match what the user sees
470 - if (!$persistence_enabled && $session_start_timestamp > 0) {
471 - $history = array_filter($history, function($entry) use ($session_start_timestamp) {
472 - // Include messages from this page load onwards
473 - return isset($entry['timestamp']) && $entry['timestamp'] >= $session_start_timestamp;
474 - });
475 - // Re-index array after filtering
476 - $history = array_values($history);
477 - }
478 -
479 133 $formatted_history = [];
480 134
481 135 // Adjusted for code-heavy conversations
482 136 $max_tokens = 120000; // Context window size
@@ -550,17 +204,8 @@
550 204
551 205 public function register_routes() {
552 206 //error_log(esc_html__('Registering MxChat REST routes', 'mxchat'));
553 207
554 - // Per-request chat-send nonce endpoint — issues a fresh nonce on demand
555 - // so the chat widget never depends on a stale nonce embedded in cached HTML.
556 - // Public (no auth), rate-limited (1 call / IP / second via a transient).
557 - register_rest_route('mxchat/v1', '/nonce', [
558 - 'methods' => 'GET',
559 - 'callback' => [$this, 'mxchat_issue_chat_send_nonce'],
560 - 'permission_callback' => '__return_true',
561 - ]);
562 -
563 208 register_rest_route('mxchat/v1', '/stream', [
564 209 'methods' => 'GET',
565 210 'callback' => [$this, 'mxchat_stream_events'],
566 211 'permission_callback' => [$this, 'verify_chat_session'],
@@ -583,105 +228,12 @@
583 228 'callback' => [$this, 'handle_slack_messages'],
584 229 'permission_callback' => [$this, 'verify_slack_request'],
585 230 ]);
586 231
587 - // Telegram webhook endpoint
588 - register_rest_route('mxchat/v1', '/telegram-webhook', [
589 - 'methods' => 'POST',
590 - 'callback' => [$this, 'handle_telegram_webhook'],
591 - 'permission_callback' => [$this, 'verify_telegram_request'],
592 - ]);
593 -
594 232 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
595 233 }
596 234
597 235 /**
598 - * Issue a fresh per-request nonce for chat-send. Returned to the widget which
599 - * caches it for the session and includes it on every chat-send / stream-send /
600 - * upload call. By moving the nonce out of inline `window.mxchatChat = {...}` HTML
601 - * we eliminate the entire class of "first-message Access denied" failures that
602 - * plague WP installs behind a full-page cache (WP Rocket, LiteSpeed, FlyingPress,
603 - * W3 Total Cache, Cloudflare APO) — the nonce is never cached because it never
604 - * lives in the HTML body.
605 - *
606 - * Public endpoint. Rate-limited to 1 call / IP / 1s via a transient so a single
607 - * client browser can't be used to flood the nonce-issuance path.
608 - *
609 - * Nonce action: `mxchat_chat_send` (new). The chat-send AJAX handlers accept
610 - * BOTH this action AND the legacy `mxchat_chat_nonce` action for a 30-day
611 - * backwards-compat window so cached pages still in users' browsers don't break
612 - * mid-session.
613 - *
614 - * @since 3.2.7
615 - */
616 -public function mxchat_issue_chat_send_nonce(WP_REST_Request $request) {
617 - $ip = '';
618 - if (!empty($_SERVER['REMOTE_ADDR'])) {
619 - $ip = preg_replace('#[^0-9a-fA-F:\.]#', '', wp_unslash((string) $_SERVER['REMOTE_ADDR']));
620 - }
621 - if ($ip !== '') {
622 - // Best-effort rate limit. WP transients with sub-second TTL are racy
623 - // (parallel bursts can squeak through before set_transient completes);
624 - // we use 2s to make the gate slightly more reliable. Real production
625 - // rate-limiting at sub-second granularity needs Redis or DB row locks
626 - // — out of scope for this endpoint, which is already cheap.
627 - $key = 'mxchat_nonce_rl_' . md5($ip);
628 - if (get_transient($key)) {
629 - return new WP_REST_Response(array(
630 - 'error' => 'rate_limited',
631 - 'message' => __('Too many nonce requests. Try again shortly.', 'mxchat'),
632 - ), 429);
633 - }
634 - set_transient($key, 1, 2);
635 - }
636 -
637 - // The widget calls this endpoint without an X-WP-Nonce header, so WordPress does not
638 - // honor the auth cookie and the request runs as uid=0 even for logged-in users. That
639 - // makes wp_create_nonce() bind the nonce to uid=0, which then fails wp_verify_nonce()
640 - // at admin-ajax (which runs as the real uid) -> logged-in users get a 403 on upload.
641 - // Resolve the real user from the logged_in cookie so the nonce binds to the correct uid.
642 - if ( ! is_user_logged_in() ) {
643 - $maybe_uid = wp_validate_auth_cookie( '', 'logged_in' );
644 - if ( $maybe_uid ) {
645 - wp_set_current_user( $maybe_uid );
646 - }
647 - }
648 -
649 - $payload = array(
650 - 'nonce' => wp_create_nonce('mxchat_chat_send'),
651 - 'expires_in' => 86400, // WP nonces live 24h; widget caches for 12h conservatively.
652 - );
653 -
654 - // plan-32db95: the widget's first-open refresh asks for current behavior
655 - // settings in the same round-trip, so stale inline-localized values on
656 - // cached pages get corrected without a second request. All values in
657 - // this payload already ship in public page HTML — nothing sensitive.
658 - if ($request->get_param('with_settings')) {
659 - $payload['settings'] = $this->get_dynamic_widget_settings(true);
660 - }
661 -
662 - return new WP_REST_Response($payload, 200);
663 -}
664 -
665 -/**
666 - * Verify a chat-send nonce. Accepts BOTH the new `mxchat_chat_send` action
667 - * (issued by /wp-json/mxchat/v1/nonce) AND the legacy `mxchat_chat_nonce`
668 - * action (inline-localized in older cached HTML). The legacy acceptance is
669 - * a 30-day backwards-compat window — to be removed in a follow-up release
670 - * after 2026-06-27.
671 - *
672 - * @param string $posted_nonce
673 - * @return bool
674 - */
675 -public static function mxchat_verify_chat_send_nonce($posted_nonce) {
676 - if (!is_string($posted_nonce) || $posted_nonce === '') {
677 - return false;
678 - }
679 - return (bool) wp_verify_nonce($posted_nonce, 'mxchat_chat_send')
680 - || (bool) wp_verify_nonce($posted_nonce, 'mxchat_chat_nonce');
681 -}
682 -
683 -/**
684 236 * Verify valid chat session
685 237 */
686 238 public function verify_chat_session($request) {
687 239 $session_id = $request->get_param('session_id');
@@ -717,11 +269,10 @@
717 269 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
718 270 return false;
719 271 }
720 272
721 - // Get raw request body from the WP_REST_Request object
722 - // (php://input may already be consumed by WordPress at this point)
723 - $request_body = $request->get_body();
273 + // Get raw request body
274 + $request_body = file_get_contents('php://input');
724 275
725 276 // Create the signature base string
726 277 $sig_basestring = "v0:{$timestamp}:{$request_body}";
727 278
@@ -730,43 +281,8 @@
730 281
731 282 // Compare signatures
732 283 return hash_equals($my_signature, $slack_signature);
733 284 }
734 -
735 -/**
736 - * Verify request is coming from Telegram.
737 - *
738 - * @param WP_REST_Request $request
739 - * @return bool True if valid, false otherwise.
740 - */
741 -public function verify_telegram_request($request) {
742 - $secret_token = $this->options['telegram_webhook_secret'] ?? '';
743 -
744 - //error_log('[MxChat Telegram DEBUG] verify_telegram_request called');
745 - //error_log('[MxChat Telegram DEBUG] Stored secret: ' . (empty($secret_token) ? 'EMPTY' : substr($secret_token, 0, 10) . '...'));
746 -
747 - if (empty($secret_token)) {
748 - // If no secret is configured, allow the request (for initial setup)
749 - //error_log('[MxChat Telegram DEBUG] No secret configured, allowing request');
750 - return true;
751 - }
752 -
753 - // Telegram sends the secret token in the X-Telegram-Bot-Api-Secret-Token header
754 - $request_token = $request->get_header('X-Telegram-Bot-Api-Secret-Token');
755 -
756 - //error_log('[MxChat Telegram DEBUG] Request token: ' . (empty($request_token) ? 'EMPTY' : substr($request_token, 0, 10) . '...'));
757 -
758 - if (empty($request_token)) {
759 - //error_log('[MxChat Telegram DEBUG] Request rejected: No token in header');
760 - return false;
761 - }
762 -
763 - // Timing-safe comparison
764 - $result = hash_equals($secret_token, $request_token);
765 - //error_log('[MxChat Telegram DEBUG] Token comparison result: ' . ($result ? 'MATCH' : 'MISMATCH'));
766 - return $result;
767 -}
768 -
769 285 public function mxchat_stream_events(WP_REST_Request $request) {
770 286 header('Content-Type: text/event-stream');
771 287 header('Cache-Control: no-cache');
772 288 header('Connection: keep-alive');
@@ -800,9 +316,9 @@
800 316
801 317
802 318
803 319
804 -private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
320 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
805 321 global $wpdb;
806 322 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
807 323 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
808 324
@@ -814,26 +330,14 @@
814 330 $session_id
815 331 ));
816 332 $is_new_session = ($existing_messages == 0);
817 333
818 - // Log for debugging
334 + // NEW: Log for debugging
819 335 if ($is_new_session) {
820 336 //error_log("[DEBUG] This is a NEW session - first message");
821 337 }
822 338 }
823 339
824 - // SECURITY FIX: Set session ownership for new sessions
825 - if ($is_new_session && $role === 'user') {
826 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
827 - $session_owner_key = "mxchat_session_owner_{$session_id}";
828 -
829 - // Only set ownership if not already set
830 - if (!get_option($session_owner_key)) {
831 - update_option($session_owner_key, $current_user_identifier, 'no');
832 - //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
833 - }
834 - }
835 -
836 340 // 1) Extract agent name if present
837 341 $agent_name = '';
838 342 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
839 343 $agent_name = $matches[1];
@@ -865,9 +369,9 @@
865 369 $email_option_key = "mxchat_email_{$session_id}";
866 370 $saved_email = get_option($email_option_key);
867 371 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
868 372
869 - // Check for a saved name in wp_options
373 + // NEW: Check for a saved name in wp_options
870 374 $name_option_key = "mxchat_name_{$session_id}";
871 375 $saved_name = get_option($name_option_key);
872 376 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
873 377
@@ -910,9 +414,9 @@
910 414 $insert_data = [
911 415 'user_id' => $user_id,
912 416 'user_identifier'=> $user_identifier,
913 417 'user_email' => $saved_email ?: $user_email,
914 - 'user_name' => $saved_name ?: '', // Add name to insert data
418 + 'user_name' => $saved_name ?: '', // NEW: Add name to insert data
915 419 'session_id' => $session_id,
916 420 'role' => $role,
917 421 'message' => $message,
918 422 'timestamp' => current_time('mysql', 1),
@@ -938,11 +442,10 @@
938 442 $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
939 443
940 444 //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
941 445
942 - // Clear after using (= null, not unset(): unset() undeclares the property
943 - // and the next assignment recreates it dynamic, re-triggering the PHP 8.2 deprecation)
944 - $this->pending_originating_page = null;
446 + // Clear after using
447 + unset($this->pending_originating_page);
945 448 }
946 449 // Fallback to HTTP_REFERER if nothing else is available
947 450 else if (isset($_SERVER['HTTP_REFERER'])) {
948 451 $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
@@ -977,17 +480,9 @@
977 480 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
978 481 }
979 482 }
980 483 }
981 -
982 - // Add RAG context if provided (for bot messages)
983 - if ($rag_context !== null && $role === 'bot') {
984 - $rag_context_column_exists = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'rag_context'");
985 - if ($rag_context_column_exists) {
986 - $insert_data['rag_context'] = is_array($rag_context) ? wp_json_encode($rag_context) : $rag_context;
987 - }
988 - }
989 -
484 +
990 485 $wpdb->insert($table_name, $insert_data);
991 486 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
992 487
993 488 // 9) Send notification email if this is the first user message in a new session
@@ -998,17 +493,11 @@
998 493 'ip' => $_SERVER['REMOTE_ADDR']
999 494 ));
1000 495 }
1001 496
1002 - // 10) Schedule delayed transcript email if enabled and message is from user
1003 - if ($wpdb->insert_id && $role === 'user') {
1004 - $this->schedule_delayed_transcript_email($session_id);
1005 - }
1006 -
1007 497 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
1008 498 return $message_id;
1009 499 }
1010 -
1011 500 private function send_new_chat_notification($session_id, $user_info = array()) {
1012 501 $options = get_option('mxchat_transcripts_options');
1013 502
1014 503 // Check if notifications are enabled
@@ -1051,202 +540,14 @@
1051 540 // Send email
1052 541 return wp_mail($to, $subject, $message);
1053 542 }
1054 543
1055 -/**
1056 - * Schedule delayed transcript email for a session
1057 - * Reschedules if a new user message is received
1058 - */
1059 -private function schedule_delayed_transcript_email($session_id) {
1060 - $options = get_option('mxchat_transcripts_options');
1061 -
1062 - // Check if auto-email is enabled
1063 - if (empty($options['mxchat_auto_email_transcript_enabled'])) {
1064 - return;
1065 - }
1066 -
1067 - // Get notification email
1068 - $email = !empty($options['mxchat_notification_email']) ?
1069 - $options['mxchat_notification_email'] :
1070 - get_option('admin_email');
1071 -
1072 - if (!is_email($email)) {
1073 - return;
1074 - }
1075 -
1076 - // Get delay in minutes (default 30)
1077 - $delay_minutes = isset($options['mxchat_auto_email_transcript_delay']) ?
1078 - intval($options['mxchat_auto_email_transcript_delay']) : 30;
1079 -
1080 - // Clear any existing scheduled event for this session
1081 - $hook = 'mxchat_send_delayed_transcript';
1082 - $args = array($session_id);
1083 - $timestamp = wp_next_scheduled($hook, $args);
1084 -
1085 - if ($timestamp) {
1086 - wp_unschedule_event($timestamp, $hook, $args);
1087 - }
1088 -
1089 - // Schedule new event
1090 - $schedule_time = time() + ($delay_minutes * 60);
1091 - wp_schedule_single_event($schedule_time, $hook, $args);
1092 -}
1093 -
1094 -/**
1095 - * Check if chat messages contain contact information (email or phone number)
1096 - *
1097 - * @param array $messages Array of message objects with 'message' property
1098 - * @param object|null $session_data Session data object with user_email property
1099 - * @return bool True if contact info found, false otherwise
1100 - */
1101 -private function chat_contains_contact_info($messages, $session_data = null) {
1102 - // Check if session already has a stored email
1103 - if ($session_data && !empty($session_data->user_email)) {
1104 - return true;
1105 - }
1106 -
1107 - // Email regex pattern
1108 - $email_pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
1109 -
1110 - // Phone number patterns (covers various formats including international, WhatsApp style)
1111 - // Matches: +1234567890, (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 234 567 8900, etc.
1112 - $phone_pattern = '/(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{2,4}[-.\s]?\d{2,4}(?:[-.\s]?\d{1,4})?/';
1113 -
1114 - // Only check user messages (not assistant responses)
1115 - foreach ($messages as $msg) {
1116 - if ($msg->role !== 'user') {
1117 - continue;
1118 - }
1119 -
1120 - $message_text = $msg->message;
1121 -
1122 - // Check for email
1123 - if (preg_match($email_pattern, $message_text)) {
1124 - return true;
1125 - }
1126 -
1127 - // Check for phone number (must be at least 7 digits total to avoid false positives)
1128 - if (preg_match($phone_pattern, $message_text, $matches)) {
1129 - // Count actual digits to avoid matching short numbers
1130 - $digits_only = preg_replace('/\D/', '', $matches[0]);
1131 - if (strlen($digits_only) >= 7) {
1132 - return true;
1133 - }
1134 - }
1135 - }
1136 -
1137 - return false;
1138 -}
1139 -
1140 -/**
1141 - * Send the delayed transcript email with .txt attachment
1142 - */
1143 -public function mxchat_send_delayed_transcript($session_id) {
1144 - global $wpdb;
1145 -
1146 - $options = get_option('mxchat_transcripts_options');
1147 -
1148 - // Get notification email
1149 - $to = !empty($options['mxchat_notification_email']) ?
1150 - $options['mxchat_notification_email'] :
1151 - get_option('admin_email');
1152 -
1153 - if (!is_email($to)) {
1154 - return false;
1155 - }
1156 -
1157 - // Get all messages for this session
1158 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1159 - $messages = $wpdb->get_results($wpdb->prepare(
1160 - "SELECT role, message, timestamp FROM {$table_name}
1161 - WHERE session_id = %s
1162 - ORDER BY timestamp ASC",
1163 - $session_id
1164 - ));
1165 -
1166 - if (empty($messages)) {
1167 - return false;
1168 - }
1169 -
1170 - // Get session metadata
1171 - $sessions_table = $wpdb->prefix . 'mxchat_sessions';
1172 - $session_data = $wpdb->get_row($wpdb->prepare(
1173 - "SELECT * FROM {$sessions_table} WHERE session_id = %s",
1174 - $session_id
1175 - ));
1176 -
1177 - // Check if contact info is required and if it's present
1178 - $require_contact = !empty($options['mxchat_auto_email_transcript_require_contact']);
1179 - if ($require_contact && !$this->chat_contains_contact_info($messages, $session_data)) {
1180 - // Contact info required but not found - skip sending
1181 - return false;
1182 - }
1183 -
1184 - // Build transcript content
1185 - $transcript_content = "Chat Transcript\n";
1186 - $transcript_content .= "================\n\n";
1187 - $transcript_content .= "Session ID: " . $session_id . "\n";
1188 -
1189 - if ($session_data) {
1190 - $transcript_content .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
1191 - $transcript_content .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
1192 - $transcript_content .= "Started: " . $session_data->created_at . "\n";
1193 - }
1194 -
1195 - $transcript_content .= "\n" . str_repeat("=", 50) . "\n\n";
1196 -
1197 - // Add messages
1198 - foreach ($messages as $msg) {
1199 - $role_label = ($msg->role === 'user') ? 'User' : 'Assistant';
1200 - $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
1201 - $transcript_content .= $msg->message . "\n\n";
1202 - }
1203 -
1204 - // Create temporary file for attachment using WP_Filesystem
1205 - $upload_dir = wp_upload_dir();
1206 - $temp_file = $upload_dir['basedir'] . '/mxchat-transcript-' . $session_id . '.txt';
1207 - global $wp_filesystem;
1208 - if (empty($wp_filesystem)) {
1209 - require_once ABSPATH . 'wp-admin/includes/file.php';
1210 - WP_Filesystem();
1211 - }
1212 - $wp_filesystem->put_contents($temp_file, $transcript_content, FS_CHMOD_FILE);
1213 -
1214 - // Prepare email
1215 - $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
1216 -
1217 - $message = "Please find attached the full chat transcript.\n\n";
1218 - $message .= "Session ID: {$session_id}\n";
1219 -
1220 - if ($session_data) {
1221 - $message .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
1222 - $message .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
1223 - }
1224 -
1225 - $message .= "\nView online: " . admin_url('admin.php?page=mxchat-transcripts');
1226 -
1227 - // Send email with attachment
1228 - $attachments = array($temp_file);
1229 - $result = wp_mail($to, $subject, $message, '', $attachments);
1230 -
1231 - // Clean up temporary file
1232 - if (file_exists($temp_file)) {
1233 - unlink($temp_file);
1234 - }
1235 -
1236 - return $result;
1237 -}
1238 -
1239 -
1240 -
1241 544 public function mxchat_handle_save_email_and_response() {
1242 545 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
1243 546 //error_log('DEBUG: POST data: ' . print_r($_POST, true));
1244 547
1245 - nocache_headers();
1246 -
1247 548 // Validate nonce
1248 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
549 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
1249 550 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
1250 551 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
1251 552 wp_die();
1252 553 }
@@ -1256,15 +557,15 @@
1256 557 $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
1257 558
1258 559 //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
1259 560
1260 - if (empty($session_id) || $session_id === 'null' || empty($email)) {
561 + if (empty($session_id) || empty($email)) {
1261 562 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
1262 563 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
1263 564 wp_die();
1264 565 }
1265 566
1266 - // Validate name if provided (check if name field is enabled and name is required)
567 + // NEW: Validate name if provided (check if name field is enabled and name is required)
1267 568 $options = get_option('mxchat_options', []);
1268 569 $name_field_enabled = isset($options['enable_name_field']) &&
1269 570 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
1270 571
@@ -1275,15 +576,15 @@
1275 576 }
1276 577
1277 578 // 1) Always store email in wp_options
1278 579 $email_option_key = "mxchat_email_{$session_id}";
1279 - update_option($email_option_key, $email, 'no');
580 + update_option($email_option_key, $email);
1280 581 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
1281 582
1282 - // Store name in wp_options if provided
583 + // NEW: Store name in wp_options if provided
1283 584 if (!empty($name)) {
1284 585 $name_option_key = "mxchat_name_{$session_id}";
1285 - update_option($name_option_key, $name, 'no');
586 + update_option($name_option_key, $name);
1286 587 //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
1287 588 }
1288 589
1289 590 // 2) (Optional) Also store in DB if a row already exists
@@ -1296,9 +597,9 @@
1296 597
1297 598 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
1298 599
1299 600 if ($session_count) {
1300 - // Update both user_email and user_name if row(s) exist
601 + // NEW: Update both user_email and user_name if row(s) exist
1301 602 if (!empty($name)) {
1302 603 $update_sql = $wpdb->prepare(
1303 604 "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
1304 605 $email,
@@ -1327,17 +628,15 @@
1327 628
1328 629 public function mxchat_check_email_provided() {
1329 630 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
1330 631
1331 - nocache_headers();
1332 -
1333 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
632 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
1334 633 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
1335 634 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
1336 635 }
1337 636
1338 637 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1339 - if (empty($session_id) || $session_id === 'null') {
638 + if (empty($session_id)) {
1340 639 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
1341 640 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
1342 641 }
1343 642
@@ -1345,9 +644,9 @@
1345 644 if (is_user_logged_in()) {
1346 645 $current_user = wp_get_current_user();
1347 646 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
1348 647
1349 - // Get user's display name for logged in users
648 + // NEW: Get user's display name for logged in users
1350 649 $user_name = !empty($current_user->display_name) ? $current_user->display_name :
1351 650 (!empty($current_user->first_name) ? $current_user->first_name : '');
1352 651
1353 652 $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
@@ -1357,9 +656,9 @@
1357 656
1358 657 wp_send_json_success($response_data);
1359 658 }
1360 659
1361 - // Check if name field is required
660 + // NEW: Check if name field is required
1362 661 $options = get_option('mxchat_options', []);
1363 662 $name_field_enabled = isset($options['enable_name_field']) &&
1364 663 ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
1365 664
@@ -1365,9 +664,9 @@
1365 664
1366 665 $email_option_key = "mxchat_email_{$session_id}";
1367 666 $stored_email = get_option($email_option_key, '');
1368 667
1369 - // Check for stored name
668 + // NEW: Check for stored name
1370 669 $name_option_key = "mxchat_name_{$session_id}";
1371 670 $stored_name = get_option($name_option_key, '');
1372 671
1373 672 //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
@@ -1372,9 +671,9 @@
1372 671
1373 672 //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
1374 673 //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
1375 674
1376 - // Check if we have email and name (if name is required)
675 + // NEW: Check if we have email and name (if name is required)
1377 676 $has_required_info = !empty($stored_email);
1378 677
1379 678 if ($name_field_enabled) {
1380 679 $has_required_info = $has_required_info && !empty($stored_name);
@@ -1394,59 +693,32 @@
1394 693 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
1395 694 }
1396 695 }
1397 696
1398 -/**
1399 - * Send error response in appropriate format based on streaming mode
1400 - * ADDED: Helper method to consistently handle errors in both streaming and non-streaming modes
1401 - *
1402 - * @param string $error_message The error message to display
1403 - * @param string $error_code Optional error code for debugging
1404 - */
1405 -private function send_error_response($error_message, $error_code = 'api_error') {
1406 - if ($this->is_streaming) {
1407 - echo "data: " . json_encode([
1408 - 'error' => true,
1409 - 'error_message' => $error_message,
1410 - 'error_code' => $error_code,
1411 - 'text' => $error_message,
1412 - 'message' => $error_message
1413 - ]) . "\n\n";
1414 - echo "data: [DONE]\n\n";
1415 - flush();
1416 - } else {
1417 - wp_send_json_error([
1418 - 'error_message' => $error_message,
1419 - 'error_code' => $error_code
1420 - ]);
1421 - }
1422 - wp_die();
1423 -}
1424 -
1425 697 public function mxchat_handle_chat_request() {
1426 698 global $wpdb;
1427 699
1428 - // Debug: Log incoming bot_id
1429 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1430 - //error_log("=== MXCHAT DEBUG: Starting chat request ===");
1431 - //error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
700 + // NEW: Check if this is a streaming request
701 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
1432 702
1433 - // Get bot-specific options
1434 - $bot_options = $this->get_bot_options($bot_id);
1435 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
703 + // NEW: Set streaming headers if needed
704 + if ($is_streaming) {
705 + // Disable output buffering
706 + while (ob_get_level()) {
707 + ob_end_flush(); // Changed from ob_end_clean()
708 + }
709 +
710 + // Set headers for SSE
711 + header('Content-Type: text/event-stream');
712 + header('Cache-Control: no-cache');
713 + header('Connection: keep-alive');
714 + header('X-Accel-Buffering: no');
715 +
716 + // Add these new lines:
717 + ob_implicit_flush(true);
718 + flush();
719 + }
1436 720
1437 - // Check if this is a streaming request
1438 - // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing)
1439 - $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator');
1440 - $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
1441 - ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on'));
1442 -
1443 - // ADDED: Store streaming state in class property for use in private methods
1444 - $this->is_streaming = $is_streaming;
1445 -
1446 - // NOTE: Streaming headers are now set later via setup_streaming_headers()
1447 - // This allows actions/forms to return JSON responses without header conflicts
1448 -
1449 721 // Check if MX Chat Moderation is active
1450 722 if (class_exists('MX_Chat_Moderation')) {
1451 723 // Get user email and IP
1452 724 $user_email = '';
@@ -1511,31 +783,13 @@
1511 783
1512 784 // Rest of your existing code...
1513 785 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1514 786
1515 - // Treat the literal strings 'null' / 'undefined' as missing too. Browser edge cases
1516 - // (Safari ITP, private mode, cross-origin iframes with partitioned storage) can cause
1517 - // the frontend FormData.append() to stringify a null session_id into the literal
1518 - // "null", which would otherwise pass empty() and pollute the transcripts table with
1519 - // ghost sessions that group every visitor's first message under one row.
1520 - if ($session_id === 'null' || $session_id === 'undefined') {
1521 - $session_id = '';
1522 - }
1523 -
1524 787 if (empty($session_id)) {
1525 788 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1526 789 wp_die();
1527 790 }
1528 791
1529 - // Update session owner if it changed (e.g. IP changed due to network switch)
1530 - // The session ID itself is the authentication — if the client has it, they own it
1531 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1532 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
1533 -
1534 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1535 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1536 - }
1537 -
1538 792 // Validate and sanitize the incoming message
1539 793 if (empty($_POST['message'])) {
1540 794 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1541 795 wp_die();
@@ -1541,191 +795,209 @@
1541 795 wp_die();
1542 796 }
1543 797
1544 798
1545 - // Track originating page for first message in session
1546 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
799 + // NEW: Track originating page for first message in session
800 +$table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1547 801
1548 - // Check if originating page columns exist
1549 - $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
802 +// Check if originating page columns exist
803 +$columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
1550 804
1551 - if ($columns_exist) {
1552 - // Check if this session already has messages
1553 - $message_count = $wpdb->get_var($wpdb->prepare(
1554 - "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
1555 - $session_id
1556 - ));
805 +if ($columns_exist) {
806 + // Check if this session already has messages
807 + $message_count = $wpdb->get_var($wpdb->prepare(
808 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
809 + $session_id
810 + ));
811 +
812 + // If this is the first message in the session
813 + if ($message_count == 0) {
814 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
815 + $originating_url = '';
816 + $originating_title = '';
1557 817
1558 - // If this is the first message in the session
1559 - if ($message_count == 0) {
1560 - // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
1561 - $originating_url = '';
1562 - $originating_title = '';
818 + // Try to get from POST data first (sent by JavaScript)
819 + if (isset($_POST['current_page_url'])) {
820 + $originating_url = esc_url_raw($_POST['current_page_url']);
821 + $originating_title = isset($_POST['current_page_title'])
822 + ? sanitize_text_field($_POST['current_page_title'])
823 + : '';
824 + }
825 + // Fallback to HTTP_REFERER if not provided by JavaScript
826 + else if (isset($_SERVER['HTTP_REFERER'])) {
827 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
828 + }
829 +
830 + // Generate title if we have URL but no title
831 + if ($originating_url && empty($originating_title)) {
832 + $parsed_url = parse_url($originating_url);
833 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1563 834
1564 - // Try to get from POST data first (sent by JavaScript)
1565 - if (isset($_POST['current_page_url'])) {
1566 - $originating_url = esc_url_raw($_POST['current_page_url']);
1567 - $originating_title = isset($_POST['current_page_title'])
1568 - ? sanitize_text_field($_POST['current_page_title'])
1569 - : '';
835 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
836 + $originating_title = 'Homepage';
837 + } else {
838 + // Clean up the path to make a readable title
839 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
840 + $originating_title = ucwords(trim($originating_title));
1570 841 }
1571 - // Fallback to HTTP_REFERER if not provided by JavaScript
1572 - else if (isset($_SERVER['HTTP_REFERER'])) {
1573 - $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
1574 - }
1575 -
1576 - // Generate title if we have URL but no title
1577 - if ($originating_url && empty($originating_title)) {
1578 - $parsed_url = parse_url($originating_url);
1579 - $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1580 -
1581 - if (empty($path) || $path === 'index.php' || $path === 'index.html') {
1582 - $originating_title = 'Homepage';
1583 - } else {
1584 - // Clean up the path to make a readable title
1585 - $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
1586 - $originating_title = ucwords(trim($originating_title));
1587 - }
1588 - }
1589 -
1590 - // Store for later use when saving the message
1591 - $this->pending_originating_page = [
1592 - 'url' => $originating_url,
1593 - 'title' => $originating_title
1594 - ];
1595 842 }
843 +
844 + // Store for later use when saving the message
845 + $this->pending_originating_page = [
846 + 'url' => $originating_url,
847 + 'title' => $originating_title
848 + ];
1596 849 }
850 +}
851 +
852 +
853 +
854 + // NEW: Get page context if provided
855 + $page_context = null;
856 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
857 + $page_context_raw = stripslashes($_POST['page_context']);
858 + $page_context = json_decode($page_context_raw, true);
1597 859
1598 -
1599 -
1600 - // Get page context if provided
1601 - $page_context = null;
1602 - if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
1603 - $page_context_raw = stripslashes($_POST['page_context']);
1604 - $page_context = json_decode($page_context_raw, true);
860 + // Validate page context structure
861 + if (is_array($page_context) &&
862 + isset($page_context['url']) &&
863 + isset($page_context['title']) &&
864 + isset($page_context['content'])) {
1605 865
1606 - // Validate page context structure
1607 - if (is_array($page_context) &&
1608 - isset($page_context['url']) &&
1609 - isset($page_context['title']) &&
1610 - isset($page_context['content'])) {
1611 -
1612 - // Sanitize page context
1613 - $page_context['url'] = esc_url_raw($page_context['url']);
1614 - $page_context['title'] = sanitize_text_field($page_context['title']);
1615 - $page_context['content'] = wp_kses_post($page_context['content']);
1616 - } else {
1617 - $page_context = null;
1618 - }
866 + // Sanitize page context
867 + $page_context['url'] = esc_url_raw($page_context['url']);
868 + $page_context['title'] = sanitize_text_field($page_context['title']);
869 + $page_context['content'] = wp_kses_post($page_context['content']);
870 + } else {
871 + $page_context = null;
1619 872 }
873 + }
1620 874
1621 - // Modify the message sanitization to preserve PHP tags in code blocks
1622 - $allowed_tags = [
1623 - 'pre' => [],
1624 - 'code' => ['class' => true],
1625 - 'span' => ['class' => true],
1626 - 'div' => ['class' => true],
1627 - ];
875 + // Modify the message sanitization to preserve PHP tags in code blocks
876 + $allowed_tags = [
877 + 'pre' => [],
878 + 'code' => ['class' => true],
879 + 'span' => ['class' => true],
880 + 'div' => ['class' => true],
881 + ];
1628 882
1629 - // First preserve code blocks
1630 - $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
1631 - return htmlspecialchars_decode($matches[0]);
1632 - }, $_POST['message']);
883 + // First preserve code blocks
884 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
885 + return htmlspecialchars_decode($matches[0]);
886 + }, $_POST['message']);
1633 887
1634 - // Then apply sanitization
1635 - $message = wp_kses($message, $allowed_tags);
888 + // Then apply sanitization
889 + $message = wp_kses($message, $allowed_tags);
1636 890
1637 - // Preserve code blocks from markdown conversion
1638 - $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
1639 - $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
891 + // Preserve code blocks from markdown conversion
892 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
893 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
1640 894
1641 - // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
1642 - // Always initialize testing data for admins (no toggle needed)
1643 - $testing_data = null;
1644 - if (current_user_can('administrator')) {
1645 - // For vision messages, use the original user message for the query display
1646 - $query_for_testing = $message;
1647 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1648 - $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1649 - }
1650 -
1651 - $testing_data = [
1652 - 'query' => $query_for_testing,
1653 - 'timestamp' => time(),
1654 - 'top_matches' => [],
1655 - 'action_matches' => [], // Initialize action matches array
1656 - 'page_context' => $page_context, // Include page context in testing data
1657 - 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
1658 - 'bot_id' => $bot_id // Include bot ID in testing data
1659 - ];
1660 -
1661 - // Get similarity threshold from bot options or default options
1662 - $similarity_threshold = isset($current_options['similarity_threshold'])
1663 - ? ((int) $current_options['similarity_threshold']) / 100
1664 - : 0.35;
1665 -
1666 - $testing_data['similarity_threshold'] = $similarity_threshold;
1667 -
1668 - // Determine knowledge base type using bot-specific config
1669 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
1670 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
1671 - $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
895 +// ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
896 + // Always initialize testing data for admins (no toggle needed)
897 + $testing_data = null;
898 + if (current_user_can('administrator')) {
899 + // For vision messages, use the original user message for the query display
900 + $query_for_testing = $message;
901 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
902 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1672 903 }
1673 - // ===== END SIMPLIFIED TESTING INITIALIZATION =====
904 +
905 + $testing_data = [
906 + 'query' => $query_for_testing,
907 + 'timestamp' => time(),
908 + 'top_matches' => [],
909 + 'action_matches' => [], // NEW: Initialize action matches array
910 + 'page_context' => $page_context, // NEW: Include page context in testing data
911 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed']
912 + ];
913 +
914 + // Get similarity threshold
915 + $similarity_threshold = isset($this->options['similarity_threshold'])
916 + ? ((int) $this->options['similarity_threshold']) / 100
917 + : 0.75;
918 +
919 + $testing_data['similarity_threshold'] = $similarity_threshold;
920 +
921 + // Determine knowledge base type
922 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
923 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
924 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
925 + }
926 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
1674 927
1675 - // Add debug before and after:
1676 - //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
1677 - $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
1678 - //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
928 +// Add debug before and after:
929 +//error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
930 +$pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
931 +//error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
1679 932
1680 933
1681 - // If the pre-processing returned a result (not the original message), use it directly
1682 - if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
1683 - // Save the AI response
1684 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
1685 -
1686 - // Save HTML content if provided
1687 - if (!empty($pre_processed_result['html'])) {
1688 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1689 - }
1690 -
1691 - // Add testing data if admin
1692 - $response_data = [
1693 - 'text' => $pre_processed_result['text'],
1694 - 'html' => $pre_processed_result['html'] ?? '',
1695 - 'session_id' => $session_id
1696 - ];
1697 -
1698 - if ($testing_data !== null) {
1699 - $response_data['testing_data'] = $testing_data;
1700 - }
1701 -
1702 - wp_send_json($response_data);
1703 - wp_die();
934 + // If the pre-processing returned a result (not the original message), use it directly
935 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
936 + // Save the AI response
937 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
938 +
939 + // Save HTML content if provided
940 + if (!empty($pre_processed_result['html'])) {
941 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1704 942 }
943 +
944 + // Add testing data if admin
945 + $response_data = [
946 + 'text' => $pre_processed_result['text'],
947 + 'html' => $pre_processed_result['html'] ?? '',
948 + 'session_id' => $session_id
949 + ];
950 +
951 + if ($testing_data !== null) {
952 + $response_data['testing_data'] = $testing_data;
953 + }
954 +
955 + wp_send_json($response_data);
956 + wp_die();
957 + }
1705 958
1706 - // Save the user's message - handle vision processed messages differently
1707 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1708 - // For vision messages, save the original user message with image indicator
1709 - $original_message = sanitize_textarea_field($_POST['original_user_message']);
1710 - if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
1711 - $image_count = intval($_POST['vision_images_count']);
1712 - $original_message .= " [{$image_count} image(s)]";
1713 - }
1714 - $this->mxchat_save_chat_message($session_id, 'user', $original_message);
1715 - } else {
1716 - // Regular message - save as normal
1717 - $this->mxchat_save_chat_message($session_id, 'user', $message);
959 + // Save the user's message - handle vision processed messages differently
960 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
961 + // For vision messages, save the original user message with image indicator
962 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
963 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
964 + $image_count = intval($_POST['vision_images_count']);
965 + $original_message .= " [{$image_count} image(s)]";
1718 966 }
967 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
968 + } else {
969 + // Regular message - save as normal
970 + $this->mxchat_save_chat_message($session_id, 'user', $message);
971 + }
1719 972
973 +
974 +if (is_email($message)) {
975 + // Add the email to Loops
976 + $this->add_email_to_loops($message);
1720 977
1721 - if (is_email($message)) {
1722 - // Add the email to Loops
1723 - $this->add_email_to_loops($message);
978 + // Get the user's success message instruction
979 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
980 +
981 + // Set instruction for AI using the user's success message
982 + $this->current_action_instruction = $user_success_message;
983 +
984 + // Clear the email capture transient since we got the email
985 + delete_transient('mxchat_email_capture_' . $user_id);
986 + }
987 +
988 + // NEW: Check if we're in an email capture flow but user hasn't provided email yet
989 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
990 + // Check if the message contains an email (not the whole message being an email)
991 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
992 + $extracted_email = $matches[0];
1724 993
1725 - // Get the user's success message instruction using current_options
1726 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
994 + // Add the extracted email to Loops
995 + $this->add_email_to_loops($extracted_email);
1727 996
997 + // Get the user's success message instruction
998 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
999 +
1728 1000 // Set instruction for AI using the user's success message
1729 1001 $this->current_action_instruction = $user_success_message;
1730 1002
1731 1003 // Clear the email capture transient since we got the email
@@ -1730,692 +1002,464 @@
1730 1002
1731 1003 // Clear the email capture transient since we got the email
1732 1004 delete_transient('mxchat_email_capture_' . $user_id);
1733 1005 }
1734 -
1735 - // Check if we're in an email capture flow but user hasn't provided email yet
1736 - elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1737 - // Check if the message contains an email (not the whole message being an email)
1738 - if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1739 - $extracted_email = $matches[0];
1740 -
1741 - // Add the extracted email to Loops
1742 - $this->add_email_to_loops($extracted_email);
1743 -
1744 - // Get the user's success message instruction using current_options
1745 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1746 -
1747 - // Set instruction for AI using the user's success message
1748 - $this->current_action_instruction = $user_success_message;
1749 -
1750 - // Clear the email capture transient since we got the email
1751 - delete_transient('mxchat_email_capture_' . $user_id);
1752 - }
1753 - // If no email found but we're in capture mode, remind them
1754 - else {
1755 - // Get the original instruction to remind them using current_options
1756 - $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1757 - $this->current_action_instruction = $original_instruction;
1758 - }
1006 + // If no email found but we're in capture mode, remind them
1007 + else {
1008 + // Get the original instruction to remind them
1009 + $original_instruction = $this->options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1010 + $this->current_action_instruction = $original_instruction;
1759 1011 }
1012 + }
1760 1013
1761 - $intent_info = '';
1014 + $intent_info = '';
1762 1015
1763 - // Check chat mode
1764 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1016 + // Check chat mode
1017 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1765 1018
1766 - // Handle agent mode
1767 1019 // Handle agent mode
1768 - if ($chat_mode === 'agent') {
1769 - // First, check for switch intent before doing anything else
1770 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1020 +// Handle agent mode
1021 + if ($chat_mode === 'agent') {
1022 + // First, check for switch intent before doing anything else
1023 + $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1771 1024
1772 - // Capture action analysis for testing panel after intent check
1773 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1774 - $testing_data['action_matches'] = $this->last_action_analysis;
1025 + // NEW: Capture action analysis for testing panel after intent check
1026 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1027 + $testing_data['action_matches'] = $this->last_action_analysis;
1028 + }
1029 +
1030 + // If we matched an intent and it's the switch intent, handle it
1031 + if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1032 + // Update chat mode first
1033 + update_option("mxchat_mode_{$session_id}", 'ai');
1034 +
1035 + // Clear any existing PDF context to start fresh
1036 + $this->clear_pdf_transients($session_id);
1037 +
1038 + // Prepare clean switch response
1039 + $response_data = [
1040 + 'text' => $this->fallbackResponse['text'],
1041 + 'html' => '',
1042 + 'session_id' => $session_id,
1043 + 'chat_mode' => 'ai'
1044 + ];
1045 +
1046 + if ($testing_data !== null) {
1047 + $response_data['testing_data'] = $testing_data;
1775 1048 }
1776 -
1777 - // Around line 506, in the agent mode handling section:
1778 - if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1779 - // Update chat mode first
1780 - update_option("mxchat_mode_{$session_id}", 'ai');
1781 -
1782 - // Clear any existing PDF context to start fresh
1783 - $this->clear_pdf_transients($session_id);
1784 -
1785 - // Prepare clean switch response with explicit chat_mode
1786 - $response_data = [
1787 - 'text' => $this->fallbackResponse['text'],
1788 - 'html' => $this->fallbackResponse['html'] ?? '',
1789 - 'session_id' => $session_id,
1790 - 'chat_mode' => 'ai' // EXPLICITLY SET THIS
1049 +
1050 + // Save the mode switch message
1051 + $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1052 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1053 +
1054 + // Send response and exit
1055 + wp_send_json($response_data);
1056 + wp_die();
1057 + } elseif (!$intent_matched) {
1058 + // No intent matched, handle live agent message
1059 + try {
1060 + $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1061 +
1062 + $agent_response = [
1063 + 'status' => 'waiting_for_agent',
1064 + 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1791 1065 ];
1792 -
1066 +
1793 1067 if ($testing_data !== null) {
1794 - $response_data['testing_data'] = $testing_data;
1068 + $agent_response['testing_data'] = $testing_data;
1795 1069 }
1796 -
1797 - // Save the mode switch message
1798 - $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1799 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1800 -
1801 - // Send response and exit
1802 - wp_send_json($response_data);
1803 - wp_die();
1804 - } elseif (!$intent_matched) {
1805 - // No intent matched, handle live agent message
1806 - try {
1807 - $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1808 1070
1809 - $agent_response = [
1810 - 'status' => 'waiting_for_agent',
1811 - 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1812 - ];
1813 -
1814 - if ($testing_data !== null) {
1815 - $agent_response['testing_data'] = $testing_data;
1816 - }
1817 -
1818 - wp_send_json_success($agent_response);
1819 - } catch (\Exception $e) {
1820 - wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1821 - }
1822 - wp_die();
1071 + wp_send_json_success($agent_response);
1072 + } catch (\Exception $e) {
1073 + wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1823 1074 }
1075 + wp_die();
1824 1076 }
1077 + }
1825 1078
1826 - // Step 1: Check for new PDF URL in the message
1827 - if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1828 - $new_pdf_url = $matches[0];
1079 + // Step 1: Check for new PDF URL in the message
1080 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1081 + $new_pdf_url = $matches[0];
1829 1082
1830 - // Check if this is likely a PDF-related request
1831 - $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1832 - $is_pdf_request = false;
1083 + // Check if this is likely a PDF-related request
1084 + $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1085 + $is_pdf_request = false;
1833 1086
1834 - foreach ($pdf_keywords as $keyword) {
1835 - if (stripos($message, $keyword) !== false) {
1836 - $is_pdf_request = true;
1837 - break;
1838 - }
1087 + foreach ($pdf_keywords as $keyword) {
1088 + if (stripos($message, $keyword) !== false) {
1089 + $is_pdf_request = true;
1090 + break;
1839 1091 }
1092 + }
1840 1093
1841 - // If it looks like a PDF request or we're waiting for a PDF URL
1842 - if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1843 - // Validate HTTPS
1844 - if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1845 - // Extract filename from URL
1846 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1094 + // If it looks like a PDF request or we're waiting for a PDF URL
1095 + if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1096 + // Validate HTTPS
1097 + if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1098 + // Extract filename from URL
1099 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1847 1100
1848 - // Clear previous PDF transients
1849 - $this->clear_pdf_transients($session_id);
1101 + // Clear previous PDF transients
1102 + $this->clear_pdf_transients($session_id);
1850 1103
1851 - // Process new PDF using current_options
1852 - $max_pages = $current_options['pdf_max_pages'] ?? 69;
1853 - $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1104 + // Process new PDF
1105 + $max_pages = $this->options['pdf_max_pages'] ?? 69;
1106 + $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1854 1107
1855 - if ($embeddings === 'too_many_pages') {
1856 - $error_text = sprintf(
1857 - $current_options['pdf_intent_error_text'] ??
1858 - esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1859 - $max_pages
1860 - );
1861 - $this->fallbackResponse['text'] = $error_text;
1862 - } elseif ($embeddings) {
1863 - // Store new PDF information
1864 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1108 + if ($embeddings === 'too_many_pages') {
1109 + $error_text = sprintf(
1110 + $this->options['pdf_intent_error_text'] ??
1111 + esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1112 + $max_pages
1113 + );
1114 + $this->fallbackResponse['text'] = $error_text;
1115 + } elseif ($embeddings) {
1116 + // Store new PDF information
1117 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1865 1118
1866 - // If the filename is generic, create a more descriptive one
1867 - if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1868 - strpos($pdf_filename, '.php') !== false) {
1869 - $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1870 - }
1119 + // If the filename is generic, create a more descriptive one
1120 + if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1121 + strpos($pdf_filename, '.php') !== false) {
1122 + $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1123 + }
1871 1124
1872 - set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1873 - set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1874 - set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1875 - set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1125 + set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1126 + set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1127 + set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1128 + set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1876 1129
1877 - $success_text = $current_options['pdf_intent_success_text'] ??
1878 - esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1130 + $success_text = $this->options['pdf_intent_success_text'] ??
1131 + esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1879 1132
1880 - $pdf_response = [
1881 - 'success' => true,
1882 - 'message' => $success_text,
1883 - 'data' => [
1884 - 'filename' => $pdf_filename
1885 - ]
1886 - ];
1887 -
1888 - if ($testing_data !== null) {
1889 - $pdf_response['testing_data'] = $testing_data;
1890 - }
1891 -
1892 - wp_send_json($pdf_response);
1893 - wp_die();
1894 - } else {
1895 - $error_text = $current_options['pdf_intent_error_text'] ??
1896 - esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1897 - $this->fallbackResponse['text'] = $error_text;
1898 - }
1899 -
1900 - $pdf_error_response = [
1901 - 'success' => false,
1902 - 'message' => $this->fallbackResponse['text']
1133 + $pdf_response = [
1134 + 'success' => true,
1135 + 'message' => $success_text,
1136 + 'data' => [
1137 + 'filename' => $pdf_filename
1138 + ]
1903 1139 ];
1904 1140
1905 1141 if ($testing_data !== null) {
1906 - $pdf_error_response['testing_data'] = $testing_data;
1142 + $pdf_response['testing_data'] = $testing_data;
1907 1143 }
1908 1144
1909 - wp_send_json($pdf_error_response);
1145 + wp_send_json($pdf_response);
1910 1146 wp_die();
1147 + } else {
1148 + $error_text = $this->options['pdf_intent_error_text'] ??
1149 + esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1150 + $this->fallbackResponse['text'] = $error_text;
1911 1151 }
1912 - }
1913 - }
1914 1152
1915 -
1916 - // Step 2: Detect intent and handle intent-based responses
1917 - $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1918 -
1919 - // Capture action analysis for testing panel after intent check
1920 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1921 - $testing_data['action_matches'] = $this->last_action_analysis;
1922 - }
1923 -
1924 - // Step 3: Handle the intent result appropriately
1925 - if ($intent_result !== false) {
1926 - // Intent was matched - ALWAYS send as JSON response, never streaming
1927 -
1928 - if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1929 - // Intent returned a direct response array
1930 - $response_data = [
1931 - 'text' => $intent_result['text'] ?? '',
1932 - 'html' => $intent_result['html'] ?? '',
1933 - 'session_id' => $session_id
1153 + $pdf_error_response = [
1154 + 'success' => false,
1155 + 'message' => $this->fallbackResponse['text']
1934 1156 ];
1935 -
1936 - // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1937 - if (isset($intent_result['chat_mode'])) {
1938 - $response_data['chat_mode'] = $intent_result['chat_mode'];
1939 - }
1940 -
1157 +
1941 1158 if ($testing_data !== null) {
1942 - $response_data['testing_data'] = $testing_data;
1159 + $pdf_error_response['testing_data'] = $testing_data;
1943 1160 }
1944 1161
1945 - wp_send_json($response_data);
1162 + wp_send_json($pdf_error_response);
1946 1163 wp_die();
1947 - } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1948 - // Intent returned true and set fallbackResponse
1949 -
1950 - // SAVE TO TRANSCRIPT
1951 - if (!empty($this->fallbackResponse['text'])) {
1952 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1953 - }
1954 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1955 - if (!empty($this->fallbackResponse['html'])) {
1956 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1957 - }
1958 -
1959 - $response_data = [
1960 - 'text' => $this->fallbackResponse['text'] ?? '',
1961 - 'html' => $this->fallbackResponse['html'] ?? '',
1962 - 'session_id' => $session_id
1963 - ];
1964 -
1965 - if (isset($this->fallbackResponse['chat_mode'])) {
1966 - $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1967 - }
1968 -
1969 - if ($testing_data !== null) {
1970 - $response_data['testing_data'] = $testing_data;
1971 - }
1972 -
1973 - wp_send_json($response_data);
1974 - wp_die();
1975 1164 }
1976 1165 }
1166 + }
1977 1167
1978 - // If we get here, no intent matched OR the intent didn't provide a usable response
1979 -
1980 - // Step 4: Generate AI response
1981 - // Get session start timestamp - when persistence is OFF, only include messages from this page load
1982 - $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
1983 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp);
1984 - $this->mxchat_increment_chat_count();
1168 + // Check if there's an active recommendation flow session
1169 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1170 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1171 + // Create a dummy intent object that matches the original intent
1172 + $dummy_intent = new stdClass();
1173 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1174 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
1985 1175
1986 - // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
1987 - $api_key = $current_options['api_key'] ?? $this->options['api_key'];
1988 - $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1176 + // Call the recommendation flow handler directly
1177 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
1989 1178
1990 - // Check if the embedding generation returned an error
1991 - if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1992 - $error_message = $user_message_embedding['error'];
1993 - $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
1994 -
1995 - // FIXED: Send error in appropriate format based on streaming mode
1996 - if ($is_streaming) {
1997 - echo "data: " . json_encode([
1998 - 'error' => true,
1999 - 'error_message' => $error_message,
2000 - 'error_code' => $error_code,
2001 - 'text' => $error_message,
2002 - 'message' => $error_message
2003 - ]) . "\n\n";
2004 - echo "data: [DONE]\n\n";
2005 - flush();
2006 - } else {
2007 - wp_send_json_error([
2008 - 'error_message' => $error_message,
2009 - 'error_code' => $error_code
2010 - ]);
1179 + // If the handler returned a response, send it
1180 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1181 + // Save the bot's response to the chat history
1182 + if (!empty($response_data['text'])) {
1183 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
2011 1184 }
1185 + if (!empty($response_data['html'])) {
1186 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1187 + }
1188 +
1189 + if ($testing_data !== null) {
1190 + $response_data['testing_data'] = $testing_data;
1191 + }
1192 +
1193 + // Send the response
1194 + wp_send_json($response_data);
2012 1195 wp_die();
2013 1196 }
1197 + }
2014 1198
2015 - // Check if the embedding is valid
2016 - if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
2017 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1199 + // Step 2: Detect intent and handle intent-based responses
1200 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
2018 1201
2019 - // FIXED: Send error in appropriate format based on streaming mode
1202 + // NEW: Capture action analysis for testing panel after intent check
1203 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1204 + $testing_data['action_matches'] = $this->last_action_analysis;
1205 + }
1206 +
1207 + // Step 3: Handle the intent result appropriately
1208 + if ($intent_result !== false) {
1209 + // Intent was matched - ALWAYS send as JSON response, never streaming
1210 +
1211 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1212 + // Intent returned a direct response array
1213 + $response_data = [
1214 + 'text' => $intent_result['text'] ?? '',
1215 + 'html' => $intent_result['html'] ?? '',
1216 + 'session_id' => $session_id
1217 + ];
1218 +
1219 + if ($testing_data !== null) {
1220 + $response_data['testing_data'] = $testing_data;
1221 + }
1222 +
1223 + // Clear streaming headers if they were set
2020 1224 if ($is_streaming) {
2021 - echo "data: " . json_encode([
2022 - 'error' => true,
2023 - 'error_message' => $error_message,
2024 - 'error_code' => 'invalid_embedding',
2025 - 'text' => $error_message,
2026 - 'message' => $error_message
2027 - ]) . "\n\n";
2028 - echo "data: [DONE]\n\n";
2029 - flush();
2030 - } else {
2031 - wp_send_json_error([
2032 - 'error_message' => $error_message,
2033 - 'error_code' => 'invalid_embedding'
2034 - ]);
1225 + header_remove('Content-Type');
1226 + header_remove('Cache-Control');
1227 + header_remove('Connection');
1228 + header_remove('X-Accel-Buffering');
1229 + header('Content-Type: application/json');
2035 1230 }
1231 +
1232 + wp_send_json($response_data);
2036 1233 wp_die();
2037 - }
2038 -
2039 - // Build context with both knowledge base and PDF content if available
2040 - $context_content = "User asked: '{$message}'\n\n";
2041 -
2042 - // Add action instruction if present (add this right after the above line)
2043 - if (!empty($this->current_action_instruction)) {
2044 - $context_content .= "===== SPECIAL INSTRUCTION =====\n";
2045 - $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
2046 - $context_content .= "Respond naturally and conversationally while following this instruction.\n";
2047 - $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1234 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1235 + // Intent returned true and set fallbackResponse
2048 1236
2049 - // Clear the instruction after using it
2050 - $this->current_action_instruction = null;
2051 - }
2052 -
2053 -
2054 - // Add page context if available and contextual awareness is enabled using current_options
2055 - if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
2056 - $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
2057 - $context_content .= "Page URL: " . $page_context['url'] . "\n";
2058 - $context_content .= "Page Title: " . $page_context['title'] . "\n";
2059 - $context_content .= "Page Content: " . $page_context['content'] . "\n";
2060 - $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
2061 - }
2062 -
2063 - // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
2064 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
2065 -
2066 - // NEW: Also extract URLs from system instructions (only if citation links enabled)
2067 - // Use fresh options to ensure we get the latest setting value
2068 - $fresh_options = get_option('mxchat_options', []);
2069 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
2070 -
2071 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
2072 - if ($citation_links_enabled && !empty($system_instructions)) {
2073 - preg_match_all(
2074 - '#\bhttps?://[^\s<>"\']+#i',
2075 - $system_instructions,
2076 - $system_instruction_urls
2077 - );
2078 -
2079 - if (!empty($system_instruction_urls[0])) {
2080 - // Merge with existing valid URLs
2081 - $this->current_valid_urls = array_merge(
2082 - $this->current_valid_urls,
2083 - $system_instruction_urls[0]
2084 - );
2085 - // Remove duplicates
2086 - $this->current_valid_urls = array_unique($this->current_valid_urls);
2087 -
2088 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1237 + // SAVE TO TRANSCRIPT FIRST
1238 + if (!empty($this->fallbackResponse['text'])) {
1239 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
2089 1240 }
2090 - }
2091 -
2092 -// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
2093 -if ($testing_data !== null && $this->last_similarity_analysis !== null) {
2094 - // Update testing data with the REAL similarity analysis
2095 - $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
2096 - $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
2097 - $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
2098 - $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
2099 - $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
2100 -}
2101 -// ===== END SIMILARITY DATA CAPTURE =====
2102 -
2103 -// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
2104 -if ($testing_data !== null && !empty($this->current_valid_urls)) {
2105 - $testing_data['approved_urls'] = array_values($this->current_valid_urls);
2106 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
2107 -}
2108 -
2109 - if (!empty($relevant_content)) {
2110 - $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
2111 - } else {
2112 - $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
2113 - }
2114 -
2115 - // NEW: Add approved URLs list to context for AI (only if citation links enabled)
2116 - if ($citation_links_enabled && !empty($this->current_valid_urls)) {
2117 - $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
2118 - $context_content .= "You may ONLY use these exact URLs in your response:\n";
2119 - foreach ($this->current_valid_urls as $url) {
2120 - $context_content .= "- " . $url . "\n";
1241 + if (!empty($this->fallbackResponse['html'])) {
1242 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
2121 1243 }
2122 - $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
2123 - $context_content .= "===== END APPROVED URLS =====\n\n";
2124 - }
2125 -
2126 - // Check for and include PDF content
2127 - $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
2128 - $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
2129 - $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
2130 - if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
2131 - $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
2132 - if (!empty($relevant_pdf_pages)) {
2133 - $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
2134 - foreach ($relevant_pdf_pages as $page_data) {
2135 - $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
2136 - }
2137 - $context_content .= "\n";
1244 +
1245 + $response_data = [
1246 + 'text' => $this->fallbackResponse['text'] ?? '',
1247 + 'html' => $this->fallbackResponse['html'] ?? '',
1248 + 'session_id' => $session_id
1249 + ];
1250 +
1251 + if ($testing_data !== null) {
1252 + $response_data['testing_data'] = $testing_data;
2138 1253 }
2139 - }
2140 -
2141 - // Check for and include Word content
2142 - $word_url = get_transient('mxchat_word_url_' . $session_id);
2143 - $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
2144 - $word_filename = get_transient('mxchat_word_filename_' . $session_id);
2145 - if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
2146 - $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
2147 - if (!empty($relevant_word_chunks)) {
2148 - $context_content .= "Relevant content from Word document '{$word_filename}':\n";
2149 - foreach ($relevant_word_chunks as $chunk_data) {
2150 - $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
2151 - }
2152 - $context_content .= "\n";
1254 +
1255 + // Clear streaming headers if they were set
1256 + if ($is_streaming) {
1257 + header_remove('Content-Type');
1258 + header_remove('Cache-Control');
1259 + header_remove('Connection');
1260 + header_remove('X-Accel-Buffering');
1261 + header('Content-Type: application/json');
2153 1262 }
1263 +
1264 + wp_send_json($response_data);
1265 + wp_die();
2154 1266 }
2155 -
2156 - $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1267 + }
2157 1268
2158 - // Extract model from current options for bot-specific model support
2159 - $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-5.1-chat-latest';
1269 + // If we get here, no intent matched OR the intent didn't provide a usable response
1270 +
1271 + // Step 4: Generate AI response
1272 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1273 + $this->mxchat_increment_chat_count();
1274 +
1275 + // Generate embedding for the user's query
1276 + $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1277 +
1278 + // Check if the embedding generation returned an error
1279 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1280 + $error_message = $user_message_embedding['error'];
1281 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
2160 1282
2161 - $response = $this->mxchat_generate_response(
2162 - $context_content,
2163 - $current_options['api_key'] ?? $this->options['api_key'],
2164 - $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
2165 - $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
2166 - $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
2167 - $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
2168 - $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
2169 - $conversation_history,
2170 - $is_streaming,
2171 - $session_id,
2172 - $testing_data,
2173 - $selected_model
2174 - );
2175 -
2176 - // Handle streaming vs non-streaming responses
2177 - if ($is_streaming) {
2178 - // Check if streaming actually happened or if it fell back to regular response
2179 - if ($response === true) {
2180 - wp_die();
2181 - }
2182 - // If we get here, streaming fell back to regular response, continue
2183 - // But if there's an error, we need to send it as SSE format since headers are already set
2184 - if (is_array($response) && isset($response['error'])) {
2185 - $error_message = $response['error'];
2186 - $error_code = $response['error_code'] ?? 'api_error';
2187 - // Send error in SSE format that the client JS can handle
2188 - echo "data: " . json_encode([
2189 - 'error' => true,
2190 - 'error_message' => $error_message,
2191 - 'error_code' => $error_code,
2192 - 'text' => $error_message, // Also include as text for fallback handling
2193 - 'message' => $error_message
2194 - ]) . "\n\n";
2195 - echo "data: [DONE]\n\n";
2196 - flush();
2197 - wp_die();
2198 - }
2199 - }
1283 + wp_send_json_error([
1284 + 'error_message' => $error_message,
1285 + 'error_code' => $error_code
1286 + ]);
1287 + wp_die();
1288 + }
1289 +
1290 + // Check if the embedding is valid
1291 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1292 + wp_send_json_error([
1293 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1294 + 'error_code' => 'invalid_embedding'
1295 + ]);
1296 + wp_die();
1297 + }
2200 1298
2201 - // Check if the response is an error array (non-streaming mode)
2202 - if (is_array($response) && isset($response['error'])) {
2203 - wp_send_json_error([
2204 - 'error_message' => $response['error'],
2205 - 'error_code' => $response['error_code'] ?? 'api_error'
2206 - ]);
2207 - wp_die();
2208 - }
1299 + // Build context with both knowledge base and PDF content if available
1300 + $context_content = "User asked: '{$message}'\n\n";
1301 +
1302 + // NEW: Add action instruction if present (add this right after the above line)
1303 + if (!empty($this->current_action_instruction)) {
1304 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1305 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1306 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1307 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
2209 1308
2210 - // DEBUG: Check what we have
2211 - //error_log("=== BEFORE URL VALIDATION ===");
2212 - //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
2213 - //error_log("current_valid_urls count: " . count($this->current_valid_urls));
2214 - //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
2215 -
2216 - // If we get here, the response is valid text - now validate URLs
2217 - if (!empty($this->current_valid_urls)) {
2218 - //error_log("CALLING validate_and_clean_urls");
2219 - $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
2220 - } else {
2221 - //error_log("SKIPPING validation - current_valid_urls is empty");
2222 - }
2223 - // ===== END URL VALIDATION =====
1309 + // Clear the instruction after using it
1310 + $this->current_action_instruction = null;
1311 + }
2224 1312
2225 - // Prepare RAG context data for storage (only include documents used for context)
2226 - $rag_context_for_storage = null;
2227 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
2228 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
2229 1313
2230 - if ($has_rag_data || $has_action_data) {
2231 - $rag_context_for_storage = [];
1314 + // NEW: Add page context if available and contextual awareness is enabled
1315 + if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1316 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1317 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1318 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1319 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1320 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1321 + }
2232 1322
2233 - // Add RAG/source data if available
2234 - if ($has_rag_data) {
2235 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
2236 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
2237 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
2238 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
2239 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
2240 - $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
2241 - $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
2242 - }
1323 + // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
1324 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1325 +
1326 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1327 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1328 + // Update testing data with the REAL similarity analysis
1329 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1330 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1331 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1332 + }
1333 + // ===== END SIMILARITY DATA CAPTURE =====
1334 +
1335 + if (!empty($relevant_content)) {
1336 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1337 + } else {
1338 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1339 + }
2243 1340
2244 - // Add action analysis data if available
2245 - if ($has_action_data) {
2246 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
1341 + // Check for and include PDF content
1342 + $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1343 + $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1344 + $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1345 + if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1346 + $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1347 + if (!empty($relevant_pdf_pages)) {
1348 + $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1349 + foreach ($relevant_pdf_pages as $page_data) {
1350 + $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
2247 1351 }
1352 + $context_content .= "\n";
2248 1353 }
1354 + }
2249 1355
2250 - // Save the cleaned response with RAG context
2251 - $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
2252 -
2253 - // Step 5: Save additional content if available
2254 - if (!empty($this->productCardHtml)) {
2255 - $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
1356 + // Check for and include Word content
1357 + $word_url = get_transient('mxchat_word_url_' . $session_id);
1358 + $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1359 + $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1360 + if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1361 + $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1362 + if (!empty($relevant_word_chunks)) {
1363 + $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1364 + foreach ($relevant_word_chunks as $chunk_data) {
1365 + $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
1366 + }
1367 + $context_content .= "\n";
2256 1368 }
1369 + }
1370 +
1371 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
2257 1372
2258 - if (!empty($this->fallbackResponse['html'])) {
2259 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1373 + // Generate response
1374 + $response = $this->mxchat_generate_response(
1375 + $context_content,
1376 + $this->options['api_key'],
1377 + $this->options['xai_api_key'],
1378 + $this->options['claude_api_key'],
1379 + $this->options['deepseek_api_key'],
1380 + $this->options['gemini_api_key'],
1381 + $conversation_history,
1382 + $is_streaming,
1383 + $session_id,
1384 + $testing_data
1385 + );
1386 +
1387 + // Handle streaming vs non-streaming responses
1388 + if ($is_streaming) {
1389 + // Check if streaming actually happened or if it fell back to regular response
1390 + if ($response === true) {
1391 + wp_die();
2260 1392 }
2261 -
2262 - // Step 6: Return the response
2263 - // DEBUG: Check if newlines exist in the response
2264 - //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
2265 - //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
2266 - //error_log("Response first 500 chars: " . substr($response, 0, 500));
2267 -
2268 - $response_data = [
2269 - 'text' => $response,
2270 - 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
2271 - 'session_id' => $session_id
2272 - ];
2273 -
2274 - // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
2275 - if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
2276 - $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
2277 - }
2278 -
2279 - // Also pass it as a top-level field so JS can show a better error message to admins
2280 - if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
2281 - $response_data['vectorstore_error'] = $this->last_vectorstore_error;
2282 - }
2283 -
2284 - // Always add testing data for admins (no toggle needed)
2285 - if ($testing_data !== null) {
2286 - $response_data['testing_data'] = $testing_data;
2287 - }
2288 -
2289 - wp_send_json($response_data);
1393 + // If we get here, streaming fell back to regular response, continue
1394 + }
1395 +
1396 + // Check if the response is an error array
1397 + if (is_array($response) && isset($response['error'])) {
1398 + wp_send_json_error([
1399 + 'error_message' => $response['error'],
1400 + 'error_code' => $response['error_code'] ?? 'api_error'
1401 + ]);
2290 1402 wp_die();
2291 -}
2292 -
2293 -/**
2294 - * Get bot-specific options for multi-bot functionality
2295 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
2296 - */
2297 -// Also debug the bot options retrieval
2298 -private function get_bot_options($bot_id = 'default') {
2299 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
2300 -
2301 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
2302 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
2303 - return array();
2304 1403 }
2305 1404
2306 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
2307 -
2308 - if (!empty($bot_options)) {
2309 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
2310 - if (isset($bot_options['similarity_threshold'])) {
2311 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
2312 - }
1405 + // If we get here, the response is valid text
1406 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1407 +
1408 + // Step 5: Save additional content if available
1409 + if (!empty($this->productCardHtml)) {
1410 + $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
2313 1411 }
2314 -
2315 - return is_array($bot_options) ? $bot_options : array();
2316 -}
2317 1412
2318 -/**
2319 - * Get bot-specific Pinecone configuration
2320 - * Used in the knowledge retrieval functions
2321 - */
2322 -// Also add debugging to your get_bot_pinecone_config function
2323 -private function get_bot_pinecone_config($bot_id = 'default') {
2324 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
2325 -
2326 - // If default bot or multi-bot add-on not active, use default Pinecone config
2327 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
2328 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
2329 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
2330 - $config = array(
2331 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
2332 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
2333 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
2334 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
2335 - );
2336 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
2337 - return $config;
1413 + if (!empty($this->fallbackResponse['html'])) {
1414 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
2338 1415 }
2339 -
2340 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
2341 -
2342 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
2343 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
2344 -
2345 - if (!empty($bot_pinecone_config)) {
2346 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
2347 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
2348 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
2349 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
2350 - } else {
2351 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1416 +
1417 + // Step 6: Return the response
1418 + $response_data = [
1419 + 'text' => $response,
1420 + 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1421 + 'session_id' => $session_id
1422 + ];
1423 +
1424 + // Always add testing data for admins (no toggle needed)
1425 + if ($testing_data !== null) {
1426 + $response_data['testing_data'] = $testing_data;
2352 1427 }
2353 -
2354 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1428 +
1429 + wp_send_json($response_data);
1430 + wp_die();
2355 1431 }
2356 1432
2357 -
2358 1433 // Updated function to check intents and invoke the callback function
2359 1434 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
2360 1435 global $wpdb;
2361 1436 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
2362 1437
2363 - // Get the current bot_id
2364 - $current_bot_id = $this->get_current_bot_id($session_id);
2365 -
2366 1438 // Generate the user embedding
2367 1439 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
2368 -
1440 +
2369 1441 // Check if embedding generation returned an error
2370 1442 if (is_array($user_embedding) && isset($user_embedding['error'])) {
2371 1443 $error_message = $user_embedding['error'];
2372 1444 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
2373 -
2374 - // FIXED: Send error in appropriate format based on streaming mode
2375 - if ($this->is_streaming) {
2376 - echo "data: " . json_encode([
2377 - 'error' => true,
2378 - 'error_message' => $error_message,
2379 - 'error_code' => $error_code,
2380 - 'text' => $error_message,
2381 - 'message' => $error_message
2382 - ]) . "\n\n";
2383 - echo "data: [DONE]\n\n";
2384 - flush();
2385 - } else {
2386 - wp_send_json_error([
2387 - 'error_message' => $error_message,
2388 - 'error_code' => $error_code
2389 - ]);
2390 - }
1445 +
1446 + wp_send_json_error([
1447 + 'error_message' => $error_message,
1448 + 'error_code' => $error_code
1449 + ]);
2391 1450 wp_die();
2392 1451 }
2393 -
1452 +
2394 1453 // Check if embedding is valid
2395 1454 if (!is_array($user_embedding) || empty($user_embedding)) {
2396 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2397 -
2398 - // FIXED: Send error in appropriate format based on streaming mode
2399 - if ($this->is_streaming) {
2400 - echo "data: " . json_encode([
2401 - 'error' => true,
2402 - 'error_message' => $error_message,
2403 - 'error_code' => 'invalid_embedding',
2404 - 'text' => $error_message,
2405 - 'message' => $error_message
2406 - ]) . "\n\n";
2407 - echo "data: [DONE]\n\n";
2408 - flush();
2409 - } else {
2410 - wp_send_json_error([
2411 - 'error_message' => $error_message,
2412 - 'error_code' => 'invalid_embedding'
2413 - ]);
2414 - }
1455 + wp_send_json_error([
1456 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1457 + 'error_code' => 'invalid_embedding'
1458 + ]);
2415 1459 wp_die();
2416 1460 }
2417 -
1461 +
2418 1462 // Fetch intents from the database
2419 1463 $table_name = $wpdb->prefix . 'mxchat_intents';
2420 1464 if ($chat_mode === 'agent') {
2421 1465 $query = $wpdb->prepare(
@@ -2425,29 +1469,19 @@
2425 1469 $intents = $wpdb->get_results($query);
2426 1470 } else {
2427 1471 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2428 1472 }
2429 -
1473 +
2430 1474 if (empty($intents)) {
2431 1475 return false;
2432 1476 }
2433 -
2434 - // Prefetch individual phrase vectors from wp_mxchat_intent_phrases (grouped by intent_id)
2435 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
2436 - $phrases_by_intent = [];
2437 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
2438 - $all_phrases = $wpdb->get_results("SELECT intent_id, phrase, embedding_vector FROM $phrases_table");
2439 - foreach ($all_phrases as $p) {
2440 - $phrases_by_intent[$p->intent_id][] = $p;
2441 - }
2442 - }
2443 -
1477 +
2444 1478 $highest_similarity = -INF;
2445 1479 $matched_intent = null;
2446 -
2447 - // Array to store action analysis for testing panel
1480 +
1481 + // NEW: Array to store action analysis for testing panel
2448 1482 $action_analysis = [];
2449 -
1483 +
2450 1484 foreach ($intents as $intent) {
2451 1485 // Additional check for enabled state
2452 1486 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2453 1487 if (!$is_enabled) {
@@ -2452,57 +1486,22 @@
2452 1486 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2453 1487 if (!$is_enabled) {
2454 1488 continue;
2455 1489 }
2456 -
2457 - // Check if this action is enabled for the current bot
2458 - if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2459 - continue;
2460 - }
2461 -
2462 - $best_similarity = -INF;
2463 - $matched_phrase_text = '';
2464 -
2465 - // Check legacy embedding vector (existing behavior)
1490 +
2466 1491 $intent_embedding_serialized = $intent->embedding_vector;
2467 1492 $intent_embedding = $intent_embedding_serialized
2468 1493 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2469 1494 : null;
2470 -
2471 - if (is_array($intent_embedding) && !empty($intent_embedding)) {
2472 - $legacy_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2473 - if ($legacy_similarity > $best_similarity) {
2474 - $best_similarity = $legacy_similarity;
2475 - $matched_phrase_text = 'legacy';
2476 - }
2477 - }
2478 -
2479 - // Check individual phrase vectors
2480 - if (isset($phrases_by_intent[$intent->id])) {
2481 - foreach ($phrases_by_intent[$intent->id] as $phrase_row) {
2482 - $phrase_embedding = $phrase_row->embedding_vector
2483 - ? unserialize($phrase_row->embedding_vector, ['allowed_classes' => false])
2484 - : null;
2485 - if (!is_array($phrase_embedding)) {
2486 - continue;
2487 - }
2488 - $phrase_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $phrase_embedding);
2489 - if ($phrase_similarity > $best_similarity) {
2490 - $best_similarity = $phrase_similarity;
2491 - $matched_phrase_text = $phrase_row->phrase;
2492 - }
2493 - }
2494 - }
2495 -
2496 - // Skip if no valid embedding was found at all
2497 - if ($best_similarity === -INF) {
1495 +
1496 + if (!is_array($intent_embedding)) {
2498 1497 continue;
2499 1498 }
2500 -
2501 - $similarity = $best_similarity;
1499 +
1500 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2502 1501 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2503 -
2504 - // Store action analysis data for testing panel
1502 +
1503 + // NEW: Store action analysis data for testing panel
2505 1504 $action_analysis[] = [
2506 1505 'intent_label' => $intent->intent_label,
2507 1506 'callback_function' => $intent->callback_function,
2508 1507 'similarity' => round($similarity, 4),
@@ -2509,12 +1508,11 @@
2509 1508 'similarity_percentage' => round($similarity * 100, 2),
2510 1509 'threshold' => $intent_threshold,
2511 1510 'threshold_percentage' => round($intent_threshold * 100, 2),
2512 1511 'above_threshold' => $similarity >= $intent_threshold,
2513 - 'matched_phrase' => $matched_phrase_text,
2514 1512 'triggered' => false // Will be updated below if this intent is triggered
2515 1513 ];
2516 -
1514 +
2517 1515 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2518 1516 $highest_similarity = $similarity;
2519 1517 $matched_intent = $intent;
2520 1518 }
@@ -2519,9 +1517,9 @@
2519 1517 $matched_intent = $intent;
2520 1518 }
2521 1519 }
2522 1520
2523 - // Mark the triggered action if any
1521 + // NEW: Mark the triggered action if any
2524 1522 if ($matched_intent) {
2525 1523 foreach ($action_analysis as &$action) {
2526 1524 if ($action['intent_label'] === $matched_intent->intent_label) {
2527 1525 $action['triggered'] = true;
@@ -2529,9 +1527,9 @@
2529 1527 }
2530 1528 }
2531 1529 }
2532 1530
2533 - // Sort actions by similarity (highest first) and store for testing panel
1531 + // NEW: Sort actions by similarity (highest first) and store for testing panel
2534 1532 usort($action_analysis, function($a, $b) {
2535 1533 return $b['similarity'] <=> $a['similarity'];
2536 1534 });
2537 1535
@@ -2537,9 +1535,8 @@
2537 1535
2538 1536 // Store action analysis for testing panel capture
2539 1537 $this->last_action_analysis = $action_analysis;
2540 1538
2541 - // Around line 715 in your mxchat_check_intent_and_invoke_callback function
2542 1539 if ($matched_intent) {
2543 1540 // If the callback is a method on this instance (core callback), call it directly
2544 1541 if (method_exists($this, $matched_intent->callback_function)) {
2545 1542 $callback_result = call_user_func(
@@ -2553,9 +1550,9 @@
2553 1550 } else {
2554 1551 // Otherwise, use apply_filters for add-on callbacks
2555 1552 $callback_result = apply_filters(
2556 1553 $matched_intent->callback_function,
2557 - false,
1554 + false, // default return value
2558 1555 $message,
2559 1556 $user_id,
2560 1557 $session_id,
2561 1558 $matched_intent
@@ -2561,18 +1558,11 @@
2561 1558 $matched_intent
2562 1559 );
2563 1560 }
2564 1561
2565 - // Handle the callback result properly
2566 1562 if ($callback_result !== false) {
2567 - // If callback returned an array with chat_mode, use it directly
2568 - if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
2569 - $this->fallbackResponse = $callback_result;
2570 - return $callback_result; // Return the full array
2571 - } else {
2572 - $this->fallbackResponse = $callback_result;
2573 - return true;
2574 - }
1563 + $this->fallbackResponse = $callback_result;
1564 + return true;
2575 1565 }
2576 1566 }
2577 1567
2578 1568 return false;
@@ -2577,34 +1567,8 @@
2577 1567
2578 1568 return false;
2579 1569 }
2580 1570
2581 -/**
2582 - * Check if an action is enabled for a specific bot
2583 - */
2584 -private function is_action_enabled_for_bot($intent, $bot_id) {
2585 - // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2586 - if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2587 - return true;
2588 - }
2589 -
2590 - $enabled_bots = json_decode($intent->enabled_bots, true);
2591 -
2592 - // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2593 - if (!is_array($enabled_bots) || empty($enabled_bots)) {
2594 - return true;
2595 - }
2596 -
2597 - // Admin testing tab uses bot_id "testing" — treat it as "default" so all
2598 - // default-bot actions are testable from the admin panel
2599 - if ($bot_id === 'testing') {
2600 - $bot_id = 'default';
2601 - }
2602 -
2603 - // Check if the current bot is in the enabled bots list
2604 - return in_array($bot_id, $enabled_bots);
2605 -}
2606 -
2607 1571 // Helper function to clear PDF and Word document related transients
2608 1572 private function clear_pdf_transients($session_id) {
2609 1573 // PDF transients
2610 1574 delete_transient('mxchat_pdf_url_' . $session_id);
@@ -2638,23 +1602,18 @@
2638 1602 }
2639 1603
2640 1604 public function mxchat_generate_image($message, $user_id, $session_id) {
2641 1605 //error_log("Starting image generation for message: " . $message);
2642 -
2643 - // Prepare a prompt for OpenAI image generation
1606 +
1607 + // Prepare a prompt for DALL-E
2644 1608 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2645 -
2646 - // Opt-in routing: when 'custom_provider_for_images' is on, route image gen
2647 - // through the configured Custom (OpenAI-compatible) /images/generations route.
2648 - if (!empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on') {
2649 - $image_response = $this->mxchat_generate_custom_image($prompt);
2650 - } else {
2651 - // Use the existing OpenAI API key
2652 - $openai_api_key = sanitize_text_field($this->options['api_key']);
2653 - // Call OpenAI GPT Image to generate an image
2654 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2655 - }
2656 1609
1610 + // Use the existing OpenAI API key
1611 + $openai_api_key = sanitize_text_field($this->options['api_key']);
1612 +
1613 + // Call DALL-E to generate an image
1614 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1615 +
2657 1616 // Check if the response contains an image URL
2658 1617 if (isset($image_response['imageUrl'])) {
2659 1618 $image_url = esc_url_raw($image_response['imageUrl']);
2660 1619
@@ -2697,103 +1656,24 @@
2697 1656 // Return the response directly instead of relying on the property
2698 1657 return $this->fallbackResponse;
2699 1658 }
2700 1659 }
2701 -
2702 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2703 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2704 -
2705 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2706 - if (empty($gemini_api_key)) {
2707 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2708 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2709 - return ['text' => $response_text, 'html' => '', 'images' => []];
2710 - }
2711 -
2712 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2713 -
2714 - if (isset($image_response['imageUrl'])) {
2715 - $image_url = esc_url_raw($image_response['imageUrl']);
2716 -
2717 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2718 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2719 -
2720 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2721 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2722 -
2723 - $this->fallbackResponse = [
2724 - 'text' => $response_text,
2725 - 'html' => $response_html,
2726 - 'images' => [$image_url]
2727 - ];
2728 -
2729 - return $this->fallbackResponse;
2730 - } else {
2731 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2732 -
2733 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2734 -
2735 - $this->fallbackResponse = [
2736 - 'text' => $response_text,
2737 - 'html' => '',
2738 - 'images' => []
2739 - ];
2740 -
2741 - return $this->fallbackResponse;
2742 - }
2743 -}
2744 -
2745 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2746 - $extension = ($mime_type === 'image/jpeg') ? 'jpg' : 'png';
2747 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2748 - $decoded = base64_decode($base64_data);
2749 -
2750 - if ($decoded === false) {
2751 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2752 - }
2753 -
2754 - $upload = wp_upload_bits($filename, null, $decoded);
2755 -
2756 - if (!empty($upload['error'])) {
2757 - return new \WP_Error('upload_failed', $upload['error']);
2758 - }
2759 -
2760 - $attach_id = wp_insert_attachment([
2761 - 'post_mime_type' => $mime_type,
2762 - 'post_title' => $prefix,
2763 - 'post_content' => '',
2764 - 'post_status' => 'inherit',
2765 - ], $upload['file']);
2766 -
2767 - if (is_wp_error($attach_id)) {
2768 - return $attach_id;
2769 - }
2770 -
2771 - require_once ABSPATH . 'wp-admin/includes/image.php';
2772 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2773 - wp_update_attachment_metadata($attach_id, $metadata);
2774 -
2775 - return esc_url_raw(wp_get_attachment_url($attach_id));
2776 -}
2777 -
2778 -private function mxchat_generate_openai_image($prompt, $api_key, $model = 'gpt-image-1', $timeout = 60) {
1660 +private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
2779 1661 $api_url = 'https://api.openai.com/v1/images/generations';
2780 1662 $body = json_encode([
2781 - 'prompt' => sanitize_text_field($prompt),
2782 - 'n' => 1,
2783 - 'size' => '1024x1024',
2784 - 'quality' => 'medium',
2785 - 'output_format' => 'png',
2786 - 'model' => sanitize_text_field($model),
1663 + 'prompt' => sanitize_text_field($prompt),
1664 + 'n' => 1,
1665 + 'size' => '1024x1024',
1666 + 'model' => sanitize_text_field($model),
2787 1667 ]);
2788 1668
2789 1669 $args = [
2790 - 'body' => $body,
1670 + 'body' => $body,
2791 1671 'headers' => [
2792 - 'Content-Type' => 'application/json',
1672 + 'Content-Type' => 'application/json',
2793 1673 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2794 1674 ],
2795 - 'method' => 'POST',
1675 + 'method' => 'POST',
2796 1676 'timeout' => absint($timeout),
2797 1677 ];
2798 1678
2799 1679 $response = wp_remote_post($api_url, $args);
@@ -2798,114 +1678,23 @@
2798 1678
2799 1679 $response = wp_remote_post($api_url, $args);
2800 1680
2801 1681 if (is_wp_error($response)) {
1682 + //error_log("DALL-E request failed: " . $response->get_error_message());
2802 1683 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2803 1684 }
2804 1685
2805 1686 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2806 1687
2807 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2808 - if ($b64) {
2809 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2810 - if (is_wp_error($saved_url)) {
2811 - return ['error' => $saved_url->get_error_message()];
2812 - }
2813 - return ['imageUrl' => $saved_url];
1688 + if (isset($response_body['data'][0]['url'])) {
1689 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2814 1690 } else {
1691 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2815 1692 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2816 1693 }
2817 1694 }
2818 1695
2819 1696 /**
2820 - * Generate an image via a Custom (OpenAI-compatible) provider's /images/generations route.
2821 - * Only called when the opt-in 'custom_provider_for_images' setting is on.
2822 - */
2823 -private function mxchat_generate_custom_image($prompt, $timeout = 90) {
2824 - $cfg = $this->mxchat_resolve_custom_provider();
2825 - if (empty($cfg['base_url'])) {
2826 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat')];
2827 - }
2828 - $url = $cfg['base_url'] . '/images/generations';
2829 - if (!empty($cfg['api_version'])) {
2830 - $url .= (strpos($url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
2831 - }
2832 - $body = wp_json_encode([
2833 - 'prompt' => sanitize_text_field($prompt),
2834 - 'n' => 1,
2835 - 'size' => '1024x1024',
2836 - 'model' => $cfg['model'],
2837 - ]);
2838 - $response = wp_remote_post($url, [
2839 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
2840 - 'body' => $body,
2841 - 'method' => 'POST',
2842 - 'timeout' => absint($timeout),
2843 - ]);
2844 - if (is_wp_error($response)) {
2845 - return ['error' => esc_html__('Error generating image (custom provider): ', 'mxchat') . $response->get_error_message()];
2846 - }
2847 - $resp = json_decode(wp_remote_retrieve_body($response), true);
2848 - // Try b64 first (matches OpenAI shape), then url-based fallback.
2849 - $b64 = $resp['data'][0]['b64_json'] ?? $resp['data'][0]['b64'] ?? null;
2850 - if ($b64) {
2851 - $saved = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-custom');
2852 - if (is_wp_error($saved)) {
2853 - return ['error' => $saved->get_error_message()];
2854 - }
2855 - return ['imageUrl' => $saved];
2856 - }
2857 - $remote_url = $resp['data'][0]['url'] ?? null;
2858 - if ($remote_url) {
2859 - return ['imageUrl' => esc_url_raw($remote_url)];
2860 - }
2861 - $err_msg = $resp['error']['message'] ?? esc_html__('Custom provider did not return an image.', 'mxchat');
2862 - return ['error' => esc_html($err_msg)];
2863 -}
2864 -
2865 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2866 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2867 -
2868 - $body = json_encode([
2869 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2870 - 'parameters' => [
2871 - 'sampleCount' => 1,
2872 - 'aspectRatio' => '1:1',
2873 - ],
2874 - ]);
2875 -
2876 - $args = [
2877 - 'body' => $body,
2878 - 'headers' => [
2879 - 'Content-Type' => 'application/json',
2880 - 'x-goog-api-key' => sanitize_text_field($api_key),
2881 - ],
2882 - 'method' => 'POST',
2883 - 'timeout' => absint($timeout),
2884 - ];
2885 -
2886 - $response = wp_remote_post($api_url, $args);
2887 -
2888 - if (is_wp_error($response)) {
2889 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2890 - }
2891 -
2892 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2893 -
2894 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2895 - if ($b64) {
2896 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2897 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2898 - if (is_wp_error($saved_url)) {
2899 - return ['error' => $saved_url->get_error_message()];
2900 - }
2901 - return ['imageUrl' => $saved_url];
2902 - } else {
2903 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2904 - }
2905 -}
2906 -
2907 -/**
2908 1697 * Handle web search requests.
2909 1698 *
2910 1699 * Sends the refined search query to the Brave Search API and uses the
2911 1700 * results to generate a conversational response with the AI model.
@@ -2953,10 +1742,10 @@
2953 1742 $transient_key = 'mxchat_search_' . md5($refined_search_query);
2954 1743 $results = get_transient($transient_key);
2955 1744
2956 1745 if (false === $results) {
2957 - // SECURITY FIX: Changed to wp_safe_remote_get
2958 - $response = wp_safe_remote_get(
1746 + // Fetch new results from the Brave Search API
1747 + $response = wp_remote_get(
2959 1748 $api_url,
2960 1749 array(
2961 1750 'headers' => array(
2962 1751 'Accept' => 'application/json',
@@ -3095,10 +1884,9 @@
3095 1884 ],
3096 1885 'timeout' => 10,
3097 1886 ];
3098 1887
3099 - // SECURITY FIX: Changed to wp_safe_remote_get
3100 - $response = wp_safe_remote_get($api_url, $args);
1888 + $response = wp_remote_get($api_url, $args);
3101 1889
3102 1890 if (is_wp_error($response)) {
3103 1891 return array(
3104 1892 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
@@ -3168,22 +1956,17 @@
3168 1956 * @return string The refined search query
3169 1957 */
3170 1958 public function mxchat_interpret_search_query($user_query) {
3171 1959 $system_prompt = esc_html__("Interpret the user's request to provide only the essential keywords or phrases for image searching. Remove conversational language, politeness, or extra context. Return a concise search query that doesn't lose any of the original meaning.", 'mxchat');
3172 -
1960 +
3173 1961 // Get options and determine the selected model
3174 1962 $options = $this->options ?? get_option('mxchat_options');
3175 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
3176 -
3177 - // Custom (OpenAI-compatible) provider routes by model id, not prefix.
3178 - if ($selected_model === 'custom-provider') {
3179 - return $this->interpret_query_with_custom($user_query, $system_prompt);
3180 - }
3181 -
1963 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1964 +
3182 1965 // Extract model prefix to determine the provider
3183 1966 $model_parts = explode('-', $selected_model);
3184 1967 $provider = strtolower($model_parts[0]);
3185 -
1968 +
3186 1969 // Determine which API key to use based on the provider
3187 1970 switch ($provider) {
3188 1971 case 'gemini':
3189 1972 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
@@ -3224,60 +2007,11 @@
3224 2007 }
3225 2008 }
3226 2009
3227 2010 /**
3228 - * Interpret query against the configured Custom (OpenAI-compatible) provider.
3229 - * Uses the same base URL + auth scheme as the chat dispatcher.
3230 - */
3231 -private function interpret_query_with_custom($user_query, $system_prompt) {
3232 - $cfg = $this->mxchat_resolve_custom_provider();
3233 - if (empty($cfg['base_url'])) {
3234 - return sanitize_text_field($user_query);
3235 - }
3236 - $args = [
3237 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
3238 - 'body' => wp_json_encode([
3239 - 'model' => $cfg['model'],
3240 - 'messages' => [
3241 - ['role' => 'system', 'content' => $system_prompt],
3242 - ['role' => 'user', 'content' => sanitize_text_field($user_query)],
3243 - ],
3244 - 'temperature' => 0.2,
3245 - 'max_tokens' => 20,
3246 - ]),
3247 - 'method' => 'POST',
3248 - 'timeout' => 15,
3249 - ];
3250 - $response = wp_remote_post($cfg['chat_url'], $args);
3251 - if (is_wp_error($response)) {
3252 - return sanitize_text_field($user_query);
3253 - }
3254 - $body = json_decode(wp_remote_retrieve_body($response), true);
3255 - return isset($body['choices'][0]['message']['content'])
3256 - ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
3257 - : sanitize_text_field($user_query);
3258 -}
3259 -
3260 -/**
3261 - * Convert the colon-style header list returned by mxchat_resolve_custom_provider
3262 - * into the assoc-array form wp_remote_post expects.
3263 - */
3264 -private function mxchat_custom_provider_assoc_headers($cfg) {
3265 - $headers = ['Content-Type' => 'application/json'];
3266 - if (!empty($cfg['api_key'])) {
3267 - if (($cfg['auth_scheme'] ?? 'bearer') === 'api-key') {
3268 - $headers['api-key'] = $cfg['api_key'];
3269 - } else {
3270 - $headers['Authorization'] = 'Bearer ' . $cfg['api_key'];
3271 - }
3272 - }
3273 - return $headers;
3274 -}
3275 -
3276 -/**
3277 2011 * Interpret query using OpenAI models
3278 2012 */
3279 -private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-5.1-chat-latest') {
2013 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
3280 2014 $url = 'https://api.openai.com/v1/chat/completions';
3281 2015 $args = [
3282 2016 'headers' => [
3283 2017 'Authorization' => 'Bearer ' . $api_key,
@@ -3307,36 +2041,13 @@
3307 2041 : sanitize_text_field($user_query);
3308 2042 }
3309 2043
3310 2044 /**
3311 - * Anthropic removed temperature/top_p/top_k starting with Opus 4.7 (the API
3312 - * returns 400 if sent) — add new flagship model ids here. (We don't send
3313 - * top_p/top_k in any Claude body, so the list only needs to gate temperature
3314 - * stripping. We never send a `thinking` param either, which is required for
3315 - * claude-fable-5: it rejects an explicit thinking "disabled" — omit only.)
3316 - */
3317 -private function mxchat_claude_omits_temperature($model) {
3318 - $no_temp = array('claude-opus-4-7', 'claude-opus-4-8', 'claude-fable-5');
3319 - return in_array($model, $no_temp, true);
3320 -}
3321 -
3322 -/**
3323 2045 * Interpret query using Claude models
3324 2046 */
3325 2047 private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
3326 2048 $url = 'https://api.anthropic.com/v1/messages';
3327 -
3328 - $payload = [
3329 - 'model' => $model,
3330 - 'system' => $system_prompt,
3331 - 'messages' => [
3332 - ['role' => 'user', 'content' => sanitize_text_field($user_query)]
3333 - ],
3334 - 'max_tokens' => 20,
3335 - 'temperature' => 0.2,
3336 - ];
3337 - if ($this->mxchat_claude_omits_temperature($model)) { unset($payload['temperature']); }
3338 -
2049 +
3339 2050 $args = [
3340 2051 'headers' => [
3341 2052 'Content-Type' => 'application/json',
3342 2053 'x-api-key' => $api_key,
@@ -3341,9 +2052,17 @@
3341 2052 'Content-Type' => 'application/json',
3342 2053 'x-api-key' => $api_key,
3343 2054 'anthropic-version' => '2023-06-01',
3344 2055 ],
3345 - 'body' => wp_json_encode($payload),
2056 + 'body' => wp_json_encode([
2057 + 'model' => $model,
2058 + 'system' => $system_prompt,
2059 + 'messages' => [
2060 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
2061 + ],
2062 + 'max_tokens' => 20,
2063 + 'temperature' => 0.2,
2064 + ]),
3346 2065 'method' => 'POST',
3347 2066 'timeout' => 15,
3348 2067 ];
3349 2068
@@ -3352,16 +2071,12 @@
3352 2071 return sanitize_text_field($user_query);
3353 2072 }
3354 2073
3355 2074 $body = json_decode(wp_remote_retrieve_body($response), true);
3356 - // claude-fable-5 prepends a thinking block to content — take the first
3357 - // TEXT block, not content[0].
3358 - foreach ((array) ($body['content'] ?? array()) as $block) {
3359 - if (isset($block['type'], $block['text']) && $block['type'] === 'text' && trim($block['text']) !== '') {
3360 - return sanitize_text_field(trim($block['text']));
3361 - }
2075 + if (!empty($body['content'][0]['text'])) {
2076 + return sanitize_text_field(trim($body['content'][0]['text']));
3362 2077 }
3363 -
2078 +
3364 2079 return sanitize_text_field($user_query);
3365 2080 }
3366 2081
3367 2082 /**
@@ -3367,16 +2082,13 @@
3367 2082 /**
3368 2083 * Interpret query using Gemini models
3369 2084 */
3370 2085 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
3371 - if ($model === 'gemini-3-pro-preview') {
3372 - $model = 'gemini-3.1-pro-preview';
3373 - }
3374 - // Use v1beta for preview models, v1 for stable models
3375 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
3376 -
3377 - $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key);
2086 + // Strip "gemini-" prefix for the API
2087 + $model_version = str_replace('gemini-', '', $model);
3378 2088
2089 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2090 +
3379 2091 $args = [
3380 2092 'headers' => [
3381 2093 'Content-Type' => 'application/json',
3382 2094 ],
@@ -3572,9 +2284,9 @@
3572 2284 }
3573 2285
3574 2286
3575 2287 /**
3576 - * Enhanced fetch_and_split_pdf_pages with SSRF protection
2288 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
3577 2289 */
3578 2290 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3579 2291 // CLEAR DEBUG LOGGING
3580 2292 //error_log("=== MXCHAT PDF PROCESSING START ===");
@@ -3629,19 +2341,10 @@
3629 2341 // (I'll include the key parts with debug logging)
3630 2342
3631 2343 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3632 2344 //error_log("Downloading PDF from URL...");
3633 -
3634 - // SECURITY FIX: Validate URL before processing
3635 - if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3636 - //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3637 - return false;
3638 - }
3639 -
3640 2345 $temp_file = wp_tempnam($pdf_source);
3641 -
3642 - // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3643 - $response = wp_safe_remote_get($pdf_source, [
2346 + $response = wp_remote_get($pdf_source, [
3644 2347 'timeout' => 60,
3645 2348 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3646 2349 ]);
3647 2350
@@ -3650,14 +2353,9 @@
3650 2353 //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3651 2354 return false;
3652 2355 }
3653 2356
3654 - global $wp_filesystem;
3655 - if (empty($wp_filesystem)) {
3656 - require_once ABSPATH . 'wp-admin/includes/file.php';
3657 - WP_Filesystem();
3658 - }
3659 - $wp_filesystem->put_contents($temp_file, wp_remote_retrieve_body($response), FS_CHMOD_FILE);
2357 + file_put_contents($temp_file, wp_remote_retrieve_body($response));
3660 2358 //error_log("✅ PDF downloaded successfully");
3661 2359 } else {
3662 2360 $temp_file = $pdf_source;
3663 2361 //error_log("Using local PDF file: " . $temp_file);
@@ -3664,9 +2362,8 @@
3664 2362 }
3665 2363
3666 2364 // Parse PDF
3667 2365 //error_log("Parsing PDF with basic parser...");
3668 - mxchat_load_pdf_parser();
3669 2366 $parser = new \Smalot\PdfParser\Parser();
3670 2367 $pdf = $parser->parseFile($temp_file);
3671 2368 $pages = $pdf->getPages();
3672 2369
@@ -3729,33 +2426,8 @@
3729 2426 return false;
3730 2427 }
3731 2428 }
3732 2429
3733 -
3734 -/**
3735 - * Validate PDF URL for security
3736 - * Prevents SSRF attacks by blocking dangerous URLs
3737 - */
3738 -
3739 -private function mxchat_is_safe_pdf_url($url) {
3740 - // Use WordPress core function for comprehensive validation
3741 - // This blocks localhost, private IPs, and reserved IP ranges
3742 - $validated_url = wp_http_validate_url($url);
3743 -
3744 - if ($validated_url === false) {
3745 - return false;
3746 - }
3747 -
3748 - // Additional check: only allow HTTP/HTTPS schemes
3749 - $parsed = parse_url($url);
3750 - if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3751 - return false;
3752 - }
3753 -
3754 - return true;
3755 -}
3756 -
3757 -
3758 2430 private function mxchat_clean_text($text) {
3759 2431 // Remove excessive whitespace
3760 2432 $text = preg_replace('/\s+/', ' ', $text);
3761 2433
@@ -3794,14 +2466,11 @@
3794 2466 }
3795 2467
3796 2468 return [];
3797 2469 }
3798 -
3799 -
2470 +// Add this to your class
3800 2471 public function handle_pdf_upload() {
3801 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce(wp_unslash((string) $_POST['nonce']))) {
3802 - wp_send_json_error(array('message' => esc_html__('Invalid nonce.', 'mxchat')), 403);
3803 - }
2472 + check_ajax_referer('mxchat_chat_nonce', 'nonce');
3804 2473
3805 2474 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
3806 2475 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3807 2476 return;
@@ -3806,29 +2475,12 @@
3806 2475 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3807 2476 return;
3808 2477 }
3809 2478
3810 - // SECURITY FIX: Check if PDF uploads are enabled in settings
3811 - $options = get_option('mxchat_options', array());
3812 - $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3813 -
3814 - if ($show_pdf_button !== 'on') {
3815 - wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3816 - return;
3817 - }
3818 -
3819 2479 $file = $_FILES['pdf_file'];
3820 2480 $session_id = sanitize_text_field($_POST['session_id']);
3821 2481 $original_filename = sanitize_text_field($file['name']);
3822 2482
3823 - // Update session owner if it changed (e.g. IP changed due to network switch)
3824 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3825 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
3826 -
3827 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3828 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3829 - }
3830 -
3831 2483 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3832 2484 if ($file_type['type'] !== 'application/pdf') {
3833 2485 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3834 2486 return;
@@ -3834,12 +2486,9 @@
3834 2486 return;
3835 2487 }
3836 2488
3837 2489 $upload_dir = wp_upload_dir();
3838 -
3839 - // SECURITY FIX: Generate random filename without exposing session_id
3840 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3841 - $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2490 + $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3842 2491 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3843 2492
3844 2493 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3845 2494 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -3870,9 +2519,8 @@
3870 2519 return;
3871 2520 }
3872 2521
3873 2522 if (!empty($embeddings)) {
3874 - // Store the mapping between session and the random filename
3875 2523 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3876 2524 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3877 2525 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3878 2526 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -3893,11 +2541,9 @@
3893 2541 wp_send_json_error($error_message);
3894 2542 return;
3895 2543 }
3896 2544 public function handle_pdf_remove() {
3897 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce(wp_unslash((string) $_POST['nonce']))) {
3898 - wp_send_json_error(array('message' => esc_html__('Invalid nonce.', 'mxchat')), 403);
3899 - }
2545 + check_ajax_referer('mxchat_chat_nonce', 'nonce');
3900 2546
3901 2547 if (empty($_POST['session_id'])) {
3902 2548 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
3903 2549 wp_die();
@@ -3918,8 +2564,10 @@
3918 2564 wp_die();
3919 2565 }
3920 2566
3921 2567
2568 +
2569 +
3922 2570 function mxchat_fetch_new_messages() {
3923 2571 $session_id = sanitize_text_field($_POST['session_id']);
3924 2572 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
3925 2573 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -3932,31 +2580,14 @@
3932 2580 }
3933 2581
3934 2582 $history = get_option("mxchat_history_{$session_id}", []);
3935 2583
3936 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
3937 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
3938 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
3939 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
3940 -
3941 2584 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
3942 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
3943 -
3944 2585 // If persistence is enabled, show all new messages
3945 2586 if ($persistence_enabled) {
3946 - $has_id = !empty($message['id']);
3947 - $is_agent = $message['role'] === 'agent';
3948 -
3949 - // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
3950 - if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
3951 - $is_newer = true;
3952 - } else {
3953 - $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
3954 - }
3955 -
3956 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
3957 -
3958 - return $has_id && $is_newer && $is_agent;
2587 + return !empty($message['id']) &&
2588 + strcmp($message['id'], $last_seen_id) > 0 &&
2589 + $message['role'] === 'agent';
3959 2590 }
3960 2591
3961 2592 // If persistence is disabled, only show messages after initial timestamp
3962 2593 return !empty($message['id']) &&
@@ -3963,16 +2594,12 @@
3963 2594 $message['role'] === 'agent' &&
3964 2595 $message['timestamp'] > $initial_timestamp;
3965 2596 });
3966 2597
3967 - //error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
2598 + //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
3968 2599
3969 - // Include current chat mode so frontend can detect agent→AI transitions
3970 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
3971 -
3972 2600 wp_send_json_success([
3973 - 'new_messages' => array_values($new_messages),
3974 - 'chat_mode' => $chat_mode
2601 + 'new_messages' => array_values($new_messages)
3975 2602 ]);
3976 2603 wp_die();
3977 2604 }
3978 2605 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -4257,393 +2884,9 @@
4257 2884
4258 2885 //error_log("[DEBUG] Generated channel name: {$channel_name}");
4259 2886 return $channel_name;
4260 2887 }
4261 -
4262 -/**
4263 - * Telegram Live Agent Handover
4264 - * Creates a forum topic in the Telegram group and notifies agents
4265 - */
4266 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
4267 - // Check if Telegram agents are available
4268 - $telegram_available = $this->options['telegram_status'] ?? 'off';
4269 - if ($telegram_available !== 'on') {
4270 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
4271 - $this->fallbackResponse = [
4272 - 'text' => $away_message,
4273 - 'html' => '',
4274 - 'images' => [],
4275 - 'chat_mode' => 'ai'
4276 - ];
4277 - wp_send_json([
4278 - 'text' => $away_message,
4279 - 'html' => '',
4280 - 'chat_mode' => 'ai',
4281 - 'session_id' => $session_id
4282 - ]);
4283 - wp_die();
4284 - }
4285 -
4286 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4287 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
4288 -
4289 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
4290 - return false;
4291 - }
4292 -
4293 - // Check if topic already exists for this session
4294 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4295 -
4296 - if (empty($topic_id)) {
4297 - // Generate topic name
4298 - $topic_name = $this->generate_telegram_topic_name($session_id);
4299 -
4300 - // Random icon color (Telegram forum topic colors)
4301 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
4302 - $icon_color = $icon_colors[array_rand($icon_colors)];
4303 -
4304 - // Create forum topic
4305 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
4306 - 'headers' => ['Content-Type' => 'application/json'],
4307 - 'body' => json_encode([
4308 - 'chat_id' => $telegram_group_id,
4309 - 'name' => $topic_name,
4310 - 'icon_color' => $icon_color
4311 - ])
4312 - ]);
4313 -
4314 - if (!is_wp_error($response)) {
4315 - $response_body = wp_remote_retrieve_body($response);
4316 - $response_data = json_decode($response_body, true);
4317 -
4318 - if (isset($response_data['ok']) && $response_data['ok']) {
4319 - $topic_id = $response_data['result']['message_thread_id'];
4320 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
4321 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
4322 - }
4323 - }
4324 -
4325 - if (empty($topic_id)) {
4326 - return false; // Failed to create topic
4327 - }
4328 - }
4329 -
4330 - // Get recent chat history
4331 - $history = get_option("mxchat_history_{$session_id}", []);
4332 - $recent_history = array_slice($history, -5);
4333 -
4334 - // Format conversation context for Telegram (HTML format)
4335 - $conversation_context = "";
4336 - if (!empty($recent_history)) {
4337 - $conversation_context = "<b>Recent Conversation:</b>\n";
4338 - foreach ($recent_history as $hist_message) {
4339 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
4340 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
4341 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
4342 - }
4343 - $conversation_context .= "\n";
4344 - }
4345 -
4346 - // Get user info
4347 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
4348 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
4349 -
4350 - // Update session mode
4351 - update_option("mxchat_mode_{$session_id}", 'agent');
4352 -
4353 - // Send initial message to topic
4354 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
4355 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
4356 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
4357 - $topic_message .= "<b>User:</b> {$user_name}\n";
4358 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
4359 -
4360 - if (!empty($conversation_context)) {
4361 - $topic_message .= $conversation_context;
4362 - }
4363 -
4364 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
4365 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
4366 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
4367 -
4368 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4369 - 'headers' => ['Content-Type' => 'application/json'],
4370 - 'body' => json_encode([
4371 - 'chat_id' => $telegram_group_id,
4372 - 'message_thread_id' => $topic_id,
4373 - 'text' => $topic_message,
4374 - 'parse_mode' => 'HTML'
4375 - ])
4376 - ]);
4377 -
4378 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
4379 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
4380 -
4381 - $this->fallbackResponse = [
4382 - 'text' => $success_message,
4383 - 'html' => '',
4384 - 'images' => [],
4385 - 'chat_mode' => 'agent'
4386 - ];
4387 -
4388 - wp_send_json([
4389 - 'success' => true,
4390 - 'text' => $success_message,
4391 - 'html' => '',
4392 - 'chat_mode' => 'agent',
4393 - 'session_id' => $session_id,
4394 - 'fallbackResponse' => $this->fallbackResponse
4395 - ]);
4396 - wp_die();
4397 -}
4398 -
4399 -/**
4400 - * Generate topic name for Telegram forum
4401 - */
4402 -private function generate_telegram_topic_name($session_id) {
4403 - $name = null;
4404 - $email = null;
4405 -
4406 - // Check logged in user
4407 - if (is_user_logged_in()) {
4408 - $current_user = wp_get_current_user();
4409 - if (!empty($current_user->display_name)) {
4410 - $name = $current_user->display_name;
4411 - }
4412 - if (!empty($current_user->user_email)) {
4413 - $email = $current_user->user_email;
4414 - }
4415 - }
4416 -
4417 - // Check session data
4418 - if (empty($name)) {
4419 - $name = get_option("mxchat_name_{$session_id}");
4420 - }
4421 - if (empty($email)) {
4422 - $email = get_option("mxchat_email_{$session_id}");
4423 - }
4424 -
4425 - // Generate topic name
4426 - $session_suffix = substr($session_id, -6);
4427 -
4428 - if (!empty($name)) {
4429 - // Clean name for topic (max 128 chars in Telegram)
4430 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
4431 - $clean_name = trim($clean_name);
4432 - if (strlen($clean_name) > 50) {
4433 - $clean_name = substr($clean_name, 0, 50);
4434 - }
4435 - return "Chat - {$clean_name} ({$session_suffix})";
4436 - } elseif (!empty($email)) {
4437 - // Use email prefix
4438 - $email_prefix = explode('@', $email)[0];
4439 - if (strlen($email_prefix) > 30) {
4440 - $email_prefix = substr($email_prefix, 0, 30);
4441 - }
4442 - return "Chat - {$email_prefix} ({$session_suffix})";
4443 - }
4444 -
4445 - return "Chat - {$session_suffix}";
4446 -}
4447 -
4448 -/**
4449 - * Send user message to Telegram agent
4450 - */
4451 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
4452 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4453 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4454 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4455 -
4456 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
4457 - return false;
4458 - }
4459 -
4460 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
4461 - $user_message = "👤 <b>User:</b> {$escaped_message}";
4462 -
4463 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4464 - 'headers' => ['Content-Type' => 'application/json'],
4465 - 'body' => json_encode([
4466 - 'chat_id' => $group_id,
4467 - 'message_thread_id' => $topic_id,
4468 - 'text' => $user_message,
4469 - 'parse_mode' => 'HTML'
4470 - ])
4471 - ]);
4472 -
4473 - return !is_wp_error($response);
4474 -}
4475 -
4476 -/**
4477 - * Handle incoming Telegram webhook
4478 - */
4479 -public function handle_telegram_webhook(WP_REST_Request $request) {
4480 - $body = $request->get_body();
4481 - $data = json_decode($body, true);
4482 -
4483 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
4484 -
4485 - // Handle message events from forum topics
4486 - if (isset($data['message'])) {
4487 - $message_data = $data['message'];
4488 -
4489 - // Skip if not from a forum topic
4490 - if (!isset($message_data['message_thread_id'])) {
4491 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
4492 - return new WP_REST_Response(['ok' => true]);
4493 - }
4494 -
4495 - // Skip bot messages
4496 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
4497 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
4498 - return new WP_REST_Response(['ok' => true]);
4499 - }
4500 -
4501 - $chat_id = $message_data['chat']['id'] ?? '';
4502 - $topic_id = $message_data['message_thread_id'];
4503 - $message_text = $message_data['text'] ?? '';
4504 - $message_id = $message_data['message_id'] ?? '';
4505 - $from = $message_data['from'] ?? [];
4506 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4507 - if (empty($agent_name)) {
4508 - $agent_name = $from['username'] ?? 'Agent';
4509 - }
4510 -
4511 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4512 -
4513 - // Skip empty messages
4514 - if (empty($message_text)) {
4515 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4516 - return new WP_REST_Response(['ok' => true]);
4517 - }
4518 -
4519 - // Find session ID by topic ID - cast to string for comparison
4520 - global $wpdb;
4521 - $topic_id_str = strval($topic_id);
4522 - $session_option = $wpdb->get_var(
4523 - $wpdb->prepare(
4524 - "SELECT option_name FROM {$wpdb->options}
4525 - WHERE option_name LIKE %s
4526 - AND option_value = %s",
4527 - 'mxchat_telegram_topic_%',
4528 - $topic_id_str
4529 - )
4530 - );
4531 -
4532 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4533 -
4534 - if ($session_option) {
4535 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4536 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4537 -
4538 - // Verify the group ID matches
4539 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4540 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4541 -
4542 - if (strval($stored_group_id) != strval($chat_id)) {
4543 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4544 - return new WP_REST_Response(['ok' => true]);
4545 - }
4546 -
4547 - // Check for closure commands
4548 - $lower_text = strtolower(trim($message_text));
4549 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4550 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4551 - // End the live agent session
4552 - update_option("mxchat_mode_{$session_id}", 'ai');
4553 -
4554 - // Save disconnect message
4555 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4556 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4557 -
4558 - // Notify in Telegram
4559 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4560 - if (!empty($telegram_bot_token)) {
4561 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4562 - 'headers' => ['Content-Type' => 'application/json'],
4563 - 'body' => json_encode([
4564 - 'chat_id' => $chat_id,
4565 - 'message_thread_id' => $topic_id,
4566 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4567 - 'parse_mode' => 'HTML'
4568 - ])
4569 - ]);
4570 -
4571 - // Optionally close the topic
4572 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4573 - 'headers' => ['Content-Type' => 'application/json'],
4574 - 'body' => json_encode([
4575 - 'chat_id' => $chat_id,
4576 - 'message_thread_id' => $topic_id
4577 - ])
4578 - ]);
4579 - }
4580 -
4581 - return new WP_REST_Response(['ok' => true]);
4582 - }
4583 -
4584 - // Deduplicate messages
4585 - $message_key = md5($session_id . $message_id . $message_text);
4586 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4587 -
4588 - if (in_array($message_key, $processed_messages)) {
4589 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4590 - return new WP_REST_Response(['ok' => true]);
4591 - }
4592 -
4593 - $processed_messages[] = $message_key;
4594 - if (count($processed_messages) > 50) {
4595 - $processed_messages = array_slice($processed_messages, -50);
4596 - }
4597 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4598 -
4599 - // Save the agent message - format with agent name prefix for proper parsing
4600 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4601 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4602 -
4603 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4604 -
4605 - // Verify the message was saved to history
4606 - $history = get_option("mxchat_history_{$session_id}", []);
4607 - $last_message = end($history);
4608 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4609 -
4610 - // Send confirmation back to Telegram
4611 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4612 - if (!empty($telegram_bot_token)) {
4613 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4614 - if (!get_transient($confirm_key)) {
4615 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4616 - 'headers' => ['Content-Type' => 'application/json'],
4617 - 'body' => json_encode([
4618 - 'chat_id' => $chat_id,
4619 - 'message_thread_id' => $topic_id,
4620 - 'text' => "✅ <i>Message sent to user</i>",
4621 - 'parse_mode' => 'HTML',
4622 - 'reply_to_message_id' => $message_id
4623 - ])
4624 - ]);
4625 - set_transient($confirm_key, true, 300);
4626 - }
4627 - }
4628 - } else {
4629 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4630 - }
4631 - } else {
4632 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4633 - }
4634 -
4635 - return new WP_REST_Response(['ok' => true]);
4636 -}
4637 -
4638 2888 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4639 - // Check if this is a Telegram agent session
4640 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4641 - if (!empty($telegram_topic_id)) {
4642 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4643 - }
4644 -
4645 - // Otherwise, try Slack
4646 2889 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4647 2890 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4648 2891
4649 2892 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4800,26 +3043,22 @@
4800 3043 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
4801 3044 ], 200);
4802 3045 }
4803 3046 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
4804 - // Update mode to AI
3047 + //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
3048 +
3049 + // Just update mode to AI
4805 3050 update_option("mxchat_mode_{$session_id}", 'ai');
4806 -
4807 - // Clear any existing PDF context to start fresh
4808 - $this->clear_pdf_transients($session_id);
4809 -
4810 - // Set the response with explicit chat_mode
4811 - $this->fallbackResponse = [
4812 - 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
4813 - 'html' => '',
4814 - 'images' => [],
4815 - 'chat_mode' => 'ai' // Ensure this is set
4816 - ];
4817 -
4818 - // Return the complete response array instead of just true
4819 - return $this->fallbackResponse;
3051 +
3052 + // Initialize states
3053 + $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
3054 + $this->productCardHtml = '';
3055 +
3056 + // Set the response message
3057 + $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
3058 +
3059 + return true; // Intent was handled
4820 3060 }
4821 -
4822 3061 public function handle_slack_messages(WP_REST_Request $request) {
4823 3062 // Log the incoming request for debugging
4824 3063 //error_log('Slack events request received: ' . $request->get_body());
4825 3064
@@ -4869,33 +3108,33 @@
4869 3108
4870 3109 $channel_id = $event['channel'];
4871 3110 $message_text = $event['text'] ?? '';
4872 3111 $message_ts = $event['ts'] ?? '';
4873 -
3112 +
4874 3113 // Find session ID by looking for matching channel
4875 3114 global $wpdb;
4876 3115 $session_option = $wpdb->get_var(
4877 3116 $wpdb->prepare(
4878 - "SELECT option_name FROM {$wpdb->options}
4879 - WHERE option_name LIKE 'mxchat_channel_%'
3117 + "SELECT option_name FROM {$wpdb->options}
3118 + WHERE option_name LIKE 'mxchat_channel_%'
4880 3119 AND option_value = %s",
4881 3120 $channel_id
4882 3121 )
4883 3122 );
4884 -
3123 +
4885 3124 if ($session_option) {
4886 3125 $session_id = str_replace('mxchat_channel_', '', $session_option);
4887 -
3126 +
4888 3127 // Create a unique key for this specific message
4889 3128 $message_key = md5($session_id . $message_ts . $message_text);
4890 3129 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4891 -
3130 +
4892 3131 // Check if we've already processed this exact message
4893 3132 if (in_array($message_key, $processed_messages)) {
4894 3133 //error_log("Duplicate message detected for session $session_id");
4895 3134 return new WP_REST_Response(['ok' => true]);
4896 3135 }
4897 -
3136 +
4898 3137 // Add to processed messages
4899 3138 $processed_messages[] = $message_key;
4900 3139 // Keep only last 50 messages per session
4901 3140 if (count($processed_messages) > 50) {
@@ -4901,46 +3140,14 @@
4901 3140 if (count($processed_messages) > 50) {
4902 3141 $processed_messages = array_slice($processed_messages, -50);
4903 3142 }
4904 3143 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4905 -
4906 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4907 -
4908 - // Handle agent ending the chat — transfer back to AI
4909 - // Format: "!endchat" or "!endchat <custom message to user>"
4910 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
4911 - update_option("mxchat_mode_{$session_id}", 'ai');
4912 -
4913 - // Extract custom message after !endchat, or use empty string
4914 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
4915 -
4916 - // Send the agent's custom farewell message if provided
4917 - if (!empty($custom_message)) {
4918 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
4919 - }
4920 -
4921 - // Confirm in Slack channel
4922 - if (!empty($slack_bot_token)) {
4923 - wp_remote_post('https://slack.com/api/chat.postMessage', [
4924 - 'headers' => [
4925 - 'Content-Type' => 'application/json',
4926 - 'Authorization' => 'Bearer ' . $slack_bot_token
4927 - ],
4928 - 'body' => json_encode([
4929 - 'channel' => $channel_id,
4930 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
4931 - 'mrkdwn' => true
4932 - ])
4933 - ]);
4934 - }
4935 -
4936 - return new WP_REST_Response(['ok' => true]);
4937 - }
4938 -
3144 +
4939 3145 // Save the agent message
4940 3146 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
4941 -
3147 +
4942 3148 // Send confirmation back to Slack (only once)
3149 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4943 3150 if (!empty($slack_bot_token)) {
4944 3151 // Use a transient to prevent duplicate confirmations
4945 3152 $confirm_key = 'mxchat_confirm_' . $message_key;
4946 3153 if (!get_transient($confirm_key)) {
@@ -4950,9 +3157,9 @@
4950 3157 'Authorization' => 'Bearer ' . $slack_bot_token
4951 3158 ],
4952 3159 'body' => json_encode([
4953 3160 'channel' => $channel_id,
4954 - 'text' => "✅ _Message sent to user_",
3161 + 'text' => "✅ _Message sent to user_",
4955 3162 'thread_ts' => $event['ts'] // Reply in thread
4956 3163 ])
4957 3164 ]);
4958 3165 // Set transient to prevent duplicate confirmations
@@ -4992,15 +3199,9 @@
4992 3199 try {
4993 3200 // Get options and selected model
4994 3201 $options = get_option('mxchat_options');
4995 3202 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4996 -
4997 - // Opt-in: route embeddings through the Custom (OpenAI-compatible) provider.
4998 - // Off by default so existing sites see byte-identical behavior.
4999 - if (!empty($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
5000 - return $this->mxchat_generate_embedding_custom($text);
5001 - }
5002 -
3203 +
5003 3204 // Determine endpoint and API key based on model
5004 3205 if (strpos($selected_model, 'voyage') === 0) {
5005 3206 $endpoint = 'https://api.voyageai.com/v1/embeddings';
5006 3207 $api_key = $options['voyage_api_key'] ?? '';
@@ -5196,617 +3397,251 @@
5196 3397 ];
5197 3398 }
5198 3399 }
5199 3400
3401 +private function mxchat_find_relevant_content($user_embedding) {
3402 + //error_log('MXChat Vector Search: Starting content search...');
5200 3403
5201 -/**
5202 - * Generate embedding via a Custom (OpenAI-compatible) provider's /embeddings route.
5203 - * Only called when the opt-in 'custom_provider_for_embeddings' setting is on.
5204 - * Returns a numeric array (the embedding vector) on success, or ['error','error_code'] on failure.
5205 - */
5206 -private function mxchat_generate_embedding_custom($text) {
5207 - if (empty($text)) {
5208 - return ['error' => esc_html__('No text provided for embedding generation', 'mxchat'), 'error_code' => 'empty_embedding_text'];
5209 - }
5210 - $cfg = $this->mxchat_resolve_custom_provider();
5211 - if (empty($cfg['base_url'])) {
5212 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url'];
5213 - }
3404 + // Retrieve the add-on settings from the database.
3405 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
5214 3406
5215 - $options = get_option('mxchat_options');
5216 - $embed_url = $cfg['base_url'] . '/embeddings';
5217 - if (!empty($cfg['api_version'])) {
5218 - $embed_url .= (strpos($embed_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
5219 - }
5220 - $model = isset($options['custom_provider_embedding_model']) && trim((string) $options['custom_provider_embedding_model']) !== ''
5221 - ? trim((string) $options['custom_provider_embedding_model'])
5222 - : $cfg['model'];
3407 + // Determine whether Pinecone is enabled.
3408 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
5223 3409
5224 - $response = wp_remote_post($embed_url, [
5225 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
5226 - 'body' => wp_json_encode(['input' => $text, 'model' => $model]),
5227 - 'timeout' => 60,
5228 - ]);
5229 - if (is_wp_error($response)) {
5230 - return [
5231 - 'error' => esc_html__('Connection error when generating embeddings (custom provider): ', 'mxchat') . esc_html($response->get_error_message()),
5232 - 'error_code' => 'embedding_custom_connection_error',
5233 - ];
5234 - }
5235 - $status = wp_remote_retrieve_response_code($response);
5236 - $body = json_decode(wp_remote_retrieve_body($response), true);
5237 - if ($status !== 200) {
5238 - $msg = isset($body['error']['message']) ? $body['error']['message'] : 'HTTP ' . $status;
5239 - return [
5240 - 'error' => esc_html__('Custom embedding endpoint error: ', 'mxchat') . esc_html($msg),
5241 - 'error_code' => 'embedding_custom_api_error',
5242 - 'status_code' => $status,
5243 - ];
5244 - }
5245 - if (isset($body['data'][0]['embedding']) && is_array($body['data'][0]['embedding'])) {
5246 - return $body['data'][0]['embedding'];
5247 - }
5248 - return [
5249 - 'error' => esc_html__('Invalid embedding response from custom provider.', 'mxchat'),
5250 - 'error_code' => 'embedding_custom_invalid_response',
5251 - ];
5252 -}
3410 + //error_log('Pinecone enabled flag: ' . $use_pinecone);
5253 3411
5254 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
5255 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
5256 -
5257 - // Check for OpenAI Vector Store first (takes priority when enabled)
5258 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5259 -
5260 - if ($bot_vectorstore_config['use_vectorstore']) {
5261 - // Get current model to verify it's an OpenAI model
5262 - $bot_options = $this->get_bot_options($bot_id);
5263 - $mxchat_options = get_option('mxchat_options', array());
5264 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
5265 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
5266 -
5267 - if ($this->is_openai_chat_model($selected_model)) {
5268 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
5269 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
5270 - } else {
5271 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
5272 - }
5273 - }
5274 -
5275 - // Get bot-specific Pinecone configuration
5276 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
5277 -
5278 - // Debug: Log the Pinecone configuration
5279 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
5280 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
5281 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
5282 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
5283 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
5284 -
5285 - // Determine whether to use Pinecone based on bot configuration
5286 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
5287 -
5288 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
5289 -
5290 - if ($use_pinecone) {
5291 - return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
3412 + if ($use_pinecone === 1) {
3413 + //error_log('MXChat Vector Search: Using Pinecone database');
3414 + return $this->find_relevant_content_pinecone($user_embedding);
5292 3415 } else {
5293 - return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
3416 + //error_log('MXChat Vector Search: Using WordPress database');
3417 + return $this->find_relevant_content_wordpress($user_embedding);
5294 3418 }
5295 3419 }
5296 3420
5297 -private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
3421 +private function find_relevant_content_wordpress($user_embedding) {
5298 3422 global $wpdb;
5299 3423 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3424 + $cache_key = 'mxchat_system_prompt_embeddings';
3425 + $batch_size = 500;
3426 +
5300 3427 // Initialize similarity analysis storage
5301 3428 $this->last_similarity_analysis = [
5302 3429 'knowledge_base_type' => 'WordPress Database',
5303 - 'bot_id' => $bot_id,
5304 3430 'top_matches' => [],
5305 3431 'threshold_used' => 0,
5306 3432 'total_checked' => 0
5307 3433 ];
5308 3434
5309 - // NEW: Initialize valid URLs array
5310 - $valid_urls = [];
3435 + // Retrieve embeddings from cache or database
3436 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3437 + if ($embeddings === false) {
3438 + // Cache miss - load embeddings from database WITH CONTENT and ROLE RESTRICTION for testing
3439 + $embeddings = [];
3440 + $offset = 0;
5311 3441
5312 - // Get bot-specific options for similarity threshold
5313 - $bot_options = $this->get_bot_options($bot_id);
5314 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
3442 + do {
3443 + $query = $wpdb->prepare(
3444 + "SELECT id, embedding_vector, article_content, source_url, role_restriction
3445 + FROM {$system_prompt_table}
3446 + LIMIT %d OFFSET %d",
3447 + $batch_size,
3448 + $offset
3449 + );
5315 3450
5316 - // Get knowledge manager instance for role checking
5317 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3451 + $batch = $wpdb->get_results($query);
3452 + if (empty($batch)) {
3453 + break;
3454 + }
5318 3455
5319 - // Get base similarity threshold from bot options or default options
5320 - $similarity_threshold = isset($current_options['similarity_threshold'])
5321 - ? ((int) $current_options['similarity_threshold']) / 100
5322 - : 0.35;
5323 - $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3456 + $embeddings = array_merge($embeddings, $batch);
3457 + $offset += $batch_size;
3458 + unset($batch);
3459 + } while (true);
5324 3460
5325 - // Precompute bot_filter once, outside the streaming loop
5326 - $bot_filter = '';
5327 - if ($bot_id !== 'default') {
5328 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
5329 - if ($column_exists) {
5330 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3461 + if (empty($embeddings)) {
3462 + return '';
5331 3463 }
3464 +
3465 + // Cache embeddings for future use (but note: this now includes content and role restrictions)
3466 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
5332 3467 }
5333 3468
5334 - // ===== STREAMING TOP-K PASS =====
5335 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
5336 - // - top 10 by raw similarity (for the testing/debug display panel)
5337 - // - candidates above threshold with access (capped) for context assembly
5338 - // This bounds peak memory regardless of knowledge base size and avoids loading
5339 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
5340 - $batch_size = 250;
5341 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
5342 - $top_display = [];
5343 - $candidates = [];
5344 - $total_checked = 0;
5345 - $offset = 0;
3469 + // NEW: Get knowledge manager instance for role checking
3470 + $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
5346 3471
5347 - do {
5348 - $batch = $wpdb->get_results($wpdb->prepare(
5349 - "SELECT id, embedding_vector, source_url, role_restriction
5350 - FROM {$system_prompt_table}
5351 - WHERE 1=1 {$bot_filter}
5352 - LIMIT %d OFFSET %d",
5353 - $batch_size,
5354 - $offset
5355 - ));
5356 -
5357 - if (empty($batch)) {
5358 - break;
5359 - }
5360 -
5361 - foreach ($batch as $row) {
5362 - $database_embedding = $row->embedding_vector
5363 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
5364 - : null;
5365 -
5366 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
5367 - unset($database_embedding);
5368 - continue;
5369 - }
5370 -
3472 + // Get configuration options
3473 + $main_options = get_option('mxchat_options', []);
3474 +
3475 + // Get base similarity threshold (default 75%)
3476 + $similarity_threshold = isset($main_options['similarity_threshold'])
3477 + ? ((int) $main_options['similarity_threshold']) / 100
3478 + : 0.75;
3479 +
3480 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3481 +
3482 + // Calculate similarities and build results array
3483 + $all_similarities = [];
3484 + $relevant_results = [];
3485 +
3486 + foreach ($embeddings as $embedding) {
3487 + $database_embedding = $embedding->embedding_vector
3488 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3489 + : null;
3490 +
3491 + if (is_array($database_embedding) && is_array($user_embedding)) {
5371 3492 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
5372 - unset($database_embedding);
5373 -
5374 - $role_restriction = $row->role_restriction ?? 'public';
3493 +
3494 + // NEW: Check role access
3495 + $role_restriction = $embedding->role_restriction ?? 'public';
5375 3496 $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5376 - $source_url = $row->source_url ?? '';
5377 -
5378 - // Maintain top 10 display buffer (insert-if-beats-worst)
5379 - if (count($top_display) < 10) {
5380 - $top_display[] = [
5381 - 'id' => $row->id,
5382 - 'similarity' => $similarity,
5383 - 'source_url' => $source_url,
5384 - 'role_restriction' => $role_restriction,
5385 - 'has_access' => $has_access,
5386 - ];
5387 - usort($top_display, function ($a, $b) {
5388 - return $b['similarity'] <=> $a['similarity'];
5389 - });
5390 - } elseif ($similarity > $top_display[9]['similarity']) {
5391 - $top_display[9] = [
5392 - 'id' => $row->id,
5393 - 'similarity' => $similarity,
5394 - 'source_url' => $source_url,
5395 - 'role_restriction' => $role_restriction,
5396 - 'has_access' => $has_access,
5397 - ];
5398 - usort($top_display, function ($a, $b) {
5399 - return $b['similarity'] <=> $a['similarity'];
5400 - });
3497 +
3498 + // Store ALL similarities for testing (top 10)
3499 + $source_display = '';
3500 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3501 + $source_display = $embedding->source_url;
3502 + } else {
3503 + $content_preview = strip_tags($embedding->article_content ?? '');
3504 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3505 + $source_display = substr(trim($content_preview), 0, 50) . '...';
5401 3506 }
5402 -
5403 - // Track candidates for context assembly (above threshold + has access)
3507 +
3508 + $all_similarities[] = [
3509 + 'document_id' => $embedding->id,
3510 + 'similarity' => $similarity,
3511 + 'similarity_percentage' => round($similarity * 100, 2),
3512 + 'above_threshold' => $similarity >= $similarity_threshold,
3513 + 'source_display' => $source_display,
3514 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3515 + 'used_for_context' => false, // Initialize as false, we'll update this later
3516 + 'role_restriction' => $role_restriction, // NEW: Include role info for testing
3517 + 'has_access' => $has_access, // NEW: Include access info for testing
3518 + 'filtered_out' => !$has_access // NEW: Mark if filtered out by role
3519 + ];
3520 +
3521 + // Only consider results above threshold AND with access for actual content retrieval
5404 3522 if ($similarity >= $similarity_threshold && $has_access) {
5405 - $candidates[] = [
5406 - 'id' => $row->id,
5407 - 'similarity' => $similarity,
5408 - 'source_url' => $source_url,
3523 + $relevant_results[] = [
3524 + 'id' => $embedding->id,
3525 + 'similarity' => $similarity
5409 3526 ];
5410 3527 }
5411 -
5412 - $total_checked++;
5413 3528 }
5414 -
5415 - unset($batch);
5416 -
5417 - // Trim candidates periodically to cap memory during long scans
5418 - if (count($candidates) > $max_candidates) {
5419 - usort($candidates, function ($a, $b) {
5420 - return $b['similarity'] <=> $a['similarity'];
5421 - });
5422 - $candidates = array_slice($candidates, 0, $max_candidates);
5423 - }
5424 -
5425 - $offset += $batch_size;
5426 - } while (true);
5427 -
5428 - if ($total_checked === 0) {
5429 - $this->current_valid_urls = [];
5430 - return '';
3529 +
3530 + unset($database_embedding);
5431 3531 }
5432 3532
5433 - // Final candidates sort (best first)
5434 - if (count($candidates) > 1) {
5435 - usort($candidates, function ($a, $b) {
5436 - return $b['similarity'] <=> $a['similarity'];
5437 - });
5438 - }
5439 -
5440 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
5441 - // Gather unique IDs we actually need (top_display + candidates) and pull
5442 - // article_content in bounded IN() batches. This avoids loading content for
5443 - // every row during the similarity scan.
5444 - $needed_ids = [];
5445 - foreach ($top_display as $item) {
5446 - $needed_ids[$item['id']] = true;
5447 - }
5448 - foreach ($candidates as $item) {
5449 - $needed_ids[$item['id']] = true;
5450 - }
5451 - $needed_ids = array_keys($needed_ids);
5452 -
5453 - $content_map = [];
5454 - if (!empty($needed_ids)) {
5455 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
5456 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
5457 - $rows = $wpdb->get_results($wpdb->prepare(
5458 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
5459 - ...$chunk_ids
5460 - ));
5461 - foreach ($rows as $r) {
5462 - $content_map[$r->id] = $r->article_content;
5463 - }
5464 - unset($rows);
5465 - }
5466 - }
5467 -
5468 - // Build the all_similarities display array from the top 10
5469 - $all_similarities = [];
5470 - foreach ($top_display as $item) {
5471 - $article_content_for_parse = $content_map[$item['id']] ?? '';
5472 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
5473 - $is_chunk = $parsed_for_display['is_chunked'];
5474 - $chunk_meta = $parsed_for_display['metadata'];
5475 -
5476 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
5477 - $source_display = $item['source_url'];
5478 - } else {
5479 - $content_preview = strip_tags($article_content_for_parse);
5480 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
5481 - $source_display = substr(trim($content_preview), 0, 50) . '...';
5482 - }
5483 -
5484 - $all_similarities[] = [
5485 - 'document_id' => $item['id'],
5486 - 'similarity' => $item['similarity'],
5487 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
5488 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
5489 - 'source_display' => $source_display,
5490 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
5491 - 'used_for_context' => false,
5492 - 'role_restriction' => $item['role_restriction'],
5493 - 'has_access' => $item['has_access'],
5494 - 'filtered_out' => !$item['has_access'],
5495 - 'is_chunk' => $is_chunk,
5496 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
5497 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
5498 - ];
5499 - }
5500 -
5501 - // Build url_groups from candidates for chunk reassembly
5502 - $url_groups = array();
5503 - foreach ($candidates as $cand) {
5504 - $article_content = $content_map[$cand['id']] ?? '';
5505 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
5506 - $is_chunked = $parsed['is_chunked'];
5507 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5508 - $text_content = $parsed['text'];
5509 -
5510 - $source_url = $cand['source_url'];
5511 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
5512 -
5513 - if (!isset($url_groups[$group_key])) {
5514 - $url_groups[$group_key] = array(
5515 - 'source_url' => $source_url,
5516 - 'best_score' => 0,
5517 - 'is_chunked' => $is_chunked,
5518 - 'chunks' => array(),
5519 - 'single_text' => '',
5520 - 'single_id' => null
5521 - );
5522 - }
5523 -
5524 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
5525 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
5526 - }
5527 -
5528 - if ($is_chunked) {
5529 - $url_groups[$group_key]['is_chunked'] = true;
5530 - $url_groups[$group_key]['chunks'][] = array(
5531 - 'id' => $cand['id'],
5532 - 'score' => $cand['similarity'],
5533 - 'chunk_index' => $chunk_index,
5534 - 'text' => $text_content
5535 - );
5536 - } else {
5537 - $url_groups[$group_key]['single_text'] = $text_content;
5538 - $url_groups[$group_key]['single_id'] = $cand['id'];
5539 - }
5540 - }
5541 -
5542 3533 // Sort ALL similarities for testing display (highest first)
5543 3534 usort($all_similarities, function ($a, $b) {
5544 3535 return $b['similarity'] <=> $a['similarity'];
5545 3536 });
5546 -
5547 - // Sort URL groups by best score (highest first)
5548 - uasort($url_groups, function($a, $b) {
5549 - return $b['best_score'] <=> $a['best_score'];
3537 +
3538 + // Sort relevant results by similarity (highest first)
3539 + usort($relevant_results, function ($a, $b) {
3540 + return $b['similarity'] <=> $a['similarity'];
5550 3541 });
5551 -
5552 - // Get RAG sources limit from options (default 6, min 3, max 10)
5553 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5554 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5555 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5556 -
5557 - // Take top N unique URLs based on user setting
5558 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5559 -
5560 - // Track which document IDs are used for context
3542 +
3543 + // Get top 5 results for actual content (standard approach)
3544 + $top_results = array_slice($relevant_results, 0, 5);
3545 +
3546 + // NOW mark which documents are actually used for context
5561 3547 $used_document_ids = [];
5562 - foreach ($top_urls as $group) {
5563 - if ($group['is_chunked']) {
5564 - foreach ($group['chunks'] as $chunk) {
5565 - $used_document_ids[] = $chunk['id'];
5566 - }
5567 - } elseif ($group['single_id']) {
5568 - $used_document_ids[] = $group['single_id'];
5569 - }
3548 + foreach ($top_results as $result) {
3549 + $used_document_ids[] = $result['id'];
5570 3550 }
5571 -
3551 +
5572 3552 // Update the all_similarities array to mark which were actually used
5573 3553 foreach ($all_similarities as &$similarity_item) {
5574 3554 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5575 3555 }
5576 -
5577 - // Store top 10 for testing panel
3556 +
3557 + // Store top 10 for testing panel (now with correct used_for_context flags and role info)
5578 3558 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5579 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5580 -
3559 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3560 +
3561 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3562 +
5581 3563 // Initialize final content
5582 3564 $content = '';
5583 - $matches_used = 0;
5584 - $total_chunks_used = 0;
5585 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5586 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5587 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5588 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5589 -
5590 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5591 - // Use fresh options to ensure we get the latest setting value
5592 - $fresh_options = get_option('mxchat_options', []);
5593 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5594 -
5595 - // Build content from top sources
5596 - foreach ($top_urls as $group_key => $group) {
5597 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5598 -
5599 - // Stop if we've hit the total chunk limit
5600 - if ($total_chunks_used >= $max_total_chunks) {
5601 - break;
3565 +
3566 + // Track document IDs to avoid duplicates
3567 + $added_document_ids = [];
3568 +
3569 + // Fetch and format content for each selected result
3570 + foreach ($top_results as $index => $result) {
3571 + if (in_array($result['id'], $added_document_ids)) {
3572 + continue;
5602 3573 }
5603 -
5604 - $full_text = '';
5605 - $chunks_in_this_source = 1; // Default for non-chunked content
5606 -
5607 - if ($group['is_chunked']) {
5608 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5609 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5610 -
5611 - // Fetch chunks for this URL with limit
5612 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5613 -
5614 - // If fetching all chunks fails, fall back to matched chunks
5615 - if (empty($full_text)) {
5616 - // Sort matched chunks by index and concatenate
5617 - usort($group['chunks'], function($a, $b) {
5618 - return $a['chunk_index'] <=> $b['chunk_index'];
5619 - });
5620 -
5621 - $chunk_texts = array();
5622 - $chunks_in_this_source = 0;
5623 - foreach ($group['chunks'] as $chunk) {
5624 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5625 - break;
5626 - }
5627 - $chunk_texts[] = $chunk['text'];
5628 - $chunks_in_this_source++;
3574 +
3575 + $chunk_content = $this->fetch_content_with_product_links($result['id']);
3576 + $added_document_ids[] = $result['id'];
3577 +
3578 + $content .= "## Reference " . ($index + 1) . " ##\n";
3579 + $content .= $chunk_content . "\n\n";
3580 +
3581 + // PDF surrounding pages logic (unchanged)
3582 + if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3583 + $surrounding_content = $wpdb->get_results($wpdb->prepare(
3584 + "SELECT id, article_content, role_restriction FROM {$system_prompt_table}
3585 + WHERE id IN (
3586 + (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3587 + (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3588 + )",
3589 + $result['id'],
3590 + $result['id']
3591 + ));
3592 +
3593 + // NEW: Check role access for surrounding content too
3594 + if (!empty($surrounding_content[0])) {
3595 + $surrounding_role = $surrounding_content[0]->role_restriction ?? 'public';
3596 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3597 + $content .= "## Related Content ##\n";
3598 + $content .= $surrounding_content[0]->article_content . "\n\n";
3599 + $added_document_ids[] = $surrounding_content[0]->id;
5629 3600 }
5630 - $full_text = implode("\n\n", $chunk_texts);
5631 3601 }
5632 - } else {
5633 - $full_text = $group['single_text'];
5634 - $chunks_in_this_source = 1;
5635 - }
5636 -
5637 - if (!empty($full_text)) {
5638 - // Strip URLs from content if citation links are disabled
5639 - if (!$citation_links_enabled) {
5640 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5641 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
5642 - }
5643 -
5644 - // Use numbered reference for URL-based entries, plain info label for manual entries
5645 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5646 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5647 - $matches_used++;
5648 - $content .= "## Reference " . $matches_used . " ##\n";
5649 - $content .= $full_text . "\n\n";
5650 -
5651 - // Only include citation URLs if citation links are enabled
5652 - if ($citation_links_enabled) {
5653 - $valid_urls[] = $source_url;
5654 - $content .= "URL: " . $source_url . "\n\n";
3602 +
3603 + if (!empty($surrounding_content[1])) {
3604 + $surrounding_role = $surrounding_content[1]->role_restriction ?? 'public';
3605 + if ($knowledge_manager->mxchat_user_has_content_access($surrounding_role)) {
3606 + $content .= "## Related Content ##\n";
3607 + $content .= $surrounding_content[1]->article_content . "\n\n";
3608 + $added_document_ids[] = $surrounding_content[1]->id;
5655 3609 }
5656 - } else {
5657 - // Manual entry — no reference number, no citation
5658 - $content .= "## Information ##\n";
5659 - $content .= $full_text . "\n\n";
5660 3610 }
5661 -
5662 - // Extract any URLs from the text content itself (only if citation links enabled)
5663 - if ($citation_links_enabled) {
5664 - preg_match_all(
5665 - '#\bhttps?://[^\s<>"\']+#i',
5666 - $full_text,
5667 - $content_urls
5668 - );
5669 - if (!empty($content_urls[0])) {
5670 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5671 - }
5672 - }
5673 -
5674 - $total_chunks_used += $chunks_in_this_source;
5675 3611 }
5676 3612 }
5677 -
5678 - // NEW: Store unique valid URLs for validation
5679 - $this->current_valid_urls = array_unique($valid_urls);
5680 -
5681 - // Store sources and chunks counts for testing/transcript display
5682 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5683 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5684 -
5685 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5686 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5687 -
3613 +
5688 3614 // Add response guidelines
5689 - if (empty($top_urls)) {
3615 + if (empty($top_results)) {
5690 3616 $content = "No reference information was found for this query.\n\n";
5691 3617 } else {
5692 - // Build response guidelines based on citation links setting
5693 3618 $content .= "\n## Response Guidelines ##\n" .
5694 3619 "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5695 3620 "Be conversational and friendly, but never mention your knowledge base or training data. " .
5696 3621 "If you don't have specific information or are uncertain about any details, it's always " .
5697 3622 "better to honestly say you don't know rather than making up or guessing at answers. " .
5698 - "When information is incomplete, let them know you are unsure.\n\n";
5699 -
5700 - // Only add hyperlink instructions if citation links are enabled
5701 - if ($citation_links_enabled) {
5702 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5703 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5704 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
5705 - } else {
5706 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5707 - "Simply provide helpful answers based on the reference information without citing sources.";
5708 - }
3623 + "When information is incomplete, let them know you are unsure.";
5709 3624 }
5710 3625
5711 3626 return trim($content);
5712 3627 }
5713 3628
5714 -/**
5715 - * Fetch and reassemble chunks for a URL from WordPress database
5716 - *
5717 - * @param string $source_url The source URL to fetch chunks for
5718 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5719 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5720 - * @return string Reassembled content from chunks
5721 - */
5722 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5723 - global $wpdb;
5724 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5725 -
5726 - // Fetch all rows with this source_url
5727 - $rows = $wpdb->get_results($wpdb->prepare(
5728 - "SELECT article_content FROM {$table}
5729 - WHERE source_url = %s
5730 - ORDER BY id ASC",
5731 - $source_url
5732 - ));
5733 -
5734 - if (empty($rows)) {
5735 - $chunk_count = 0;
5736 - return '';
5737 - }
5738 -
5739 - // Parse and sort chunks by index
5740 - $chunks = array();
5741 - foreach ($rows as $row) {
5742 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5743 -
5744 - if ($parsed['is_chunked']) {
5745 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5746 - $chunks[$chunk_index] = $parsed['text'];
5747 - } else {
5748 - // Non-chunked content - just return it
5749 - $chunks[] = $parsed['text'];
5750 - }
5751 - }
5752 -
5753 - // Sort by chunk index
5754 - ksort($chunks);
5755 -
5756 - // Apply chunk limit if specified
5757 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5758 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5759 - }
5760 -
5761 - // Store actual chunk count
5762 - $chunk_count = count($chunks);
5763 -
5764 - // Reassemble content
5765 - return implode("\n\n", $chunks);
5766 -}
5767 -
5768 -private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5769 - global $wpdb;
3629 +private function find_relevant_content_pinecone($user_embedding) {
3630 + global $wpdb; // For single role lookups
3631 + $options = get_option('mxchat_pinecone_addon_options', array());
3632 + $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3633 + $host = $options['mxchat_pinecone_host'] ?? '';
5770 3634
5771 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5772 - //error_log(" - bot_id: " . $bot_id);
5773 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5774 - //error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
5775 -
5776 - // Use bot-specific config or fall back to default
5777 - if ($bot_config === null) {
5778 - $bot_config = $this->get_bot_pinecone_config($bot_id);
5779 - }
5780 -
5781 - $api_key = $bot_config['api_key'] ?? '';
5782 - $host = $bot_config['host'] ?? '';
5783 - $namespace = $bot_config['namespace'] ?? '';
5784 -
5785 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5786 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5787 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5788 - //error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
5789 -
5790 3635 // Initialize similarity analysis storage
5791 3636 $this->last_similarity_analysis = [
5792 3637 'knowledge_base_type' => 'Pinecone',
5793 - 'bot_id' => $bot_id,
5794 - 'namespace' => $namespace,
5795 3638 'top_matches' => [],
5796 3639 'threshold_used' => 0,
5797 3640 'total_checked' => 0
5798 3641 ];
5799 3642
5800 - // NEW: Initialize valid URLs array
5801 - $valid_urls = [];
5802 -
5803 3643 if (empty($host) || empty($api_key)) {
5804 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5805 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5806 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5807 - // Store empty array for valid URLs since we can't proceed
5808 - $this->current_valid_urls = [];
5809 3644 return '';
5810 3645 }
5811 3646
5812 3647 // Get knowledge manager instance for role checking
@@ -5811,37 +3646,26 @@
5811 3646
5812 3647 // Get knowledge manager instance for role checking
5813 3648 $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
5814 3649
5815 - // Get the similarity threshold from the bot options or main options
5816 - $bot_options = $this->get_bot_options($bot_id);
5817 - $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
3650 + // Get the similarity threshold from the main options
3651 + $main_options = get_option('mxchat_options', []);
3652 + $similarity_threshold = isset($main_options['similarity_threshold'])
3653 + ? ((int) $main_options['similarity_threshold']) / 100
3654 + : 0.75;
5818 3655
5819 - $similarity_threshold = isset($current_options['similarity_threshold'])
5820 - ? ((int) $current_options['similarity_threshold']) / 100
5821 - : 0.35;
5822 -
5823 3656 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
5824 3657
5825 - // Prepare the query request for Pinecone
3658 + // Prepare the query request for Pinecone (request more for testing)
5826 3659 $api_endpoint = "https://{$host}/query";
5827 3660
5828 3661 $request_body = array(
5829 3662 'vector' => $user_embedding,
5830 - 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top N unique URLs
3663 + 'topK' => 20, // Request more to get good testing data
5831 3664 'includeMetadata' => true,
5832 3665 'includeValues' => true
5833 3666 );
5834 3667
5835 - // Add namespace if specified for this bot
5836 - if (!empty($namespace)) {
5837 - $request_body['namespace'] = $namespace;
5838 - }
5839 -
5840 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5841 - //error_log(" - Endpoint: " . $api_endpoint);
5842 - //error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
5843 -
5844 3668 $response = wp_remote_post($api_endpoint, array(
5845 3669 'headers' => array(
5846 3670 'Api-Key' => $api_key,
5847 3671 'accept' => 'application/json',
@@ -5851,242 +3675,62 @@
5851 3675 'timeout' => 30
5852 3676 ));
5853 3677
5854 3678 if (is_wp_error($response)) {
5855 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5856 - // Store empty array for valid URLs
5857 - $this->current_valid_urls = [];
5858 3679 return '';
5859 3680 }
5860 3681
5861 3682 $response_code = wp_remote_retrieve_response_code($response);
5862 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5863 -
5864 3683 if ($response_code !== 200) {
5865 - $response_body = wp_remote_retrieve_body($response);
5866 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5867 - // Store empty array for valid URLs
5868 - $this->current_valid_urls = [];
5869 3684 return '';
5870 3685 }
5871 3686
5872 - // ADD DETAILED DEBUG SECTION HERE
5873 - $response_body = wp_remote_retrieve_body($response);
5874 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5875 -
5876 - $results = json_decode($response_body, true);
5877 -
5878 - if (json_last_error() !== JSON_ERROR_NONE) {
5879 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5880 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5881 - // Store empty array for valid URLs
5882 - $this->current_valid_urls = [];
5883 - return '';
5884 - }
5885 -
5886 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5887 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5888 - //error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
5889 -
3687 + $results = json_decode(wp_remote_retrieve_body($response), true);
5890 3688 if (empty($results['matches'])) {
5891 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5892 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5893 - // Store empty array for valid URLs
5894 - $this->current_valid_urls = [];
5895 3689 return '';
5896 3690 }
5897 3691
5898 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
5899 -
5900 - // Log first match details for debugging
5901 - if (!empty($results['matches'][0])) {
5902 - $first_match = $results['matches'][0];
5903 - //error_log("MXCHAT DEBUG: First match details:");
5904 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
5905 - //error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
5906 - if (isset($first_match['metadata'])) {
5907 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
5908 - }
5909 - }
5910 -
5911 3692 // Initialize the final content
5912 3693 $content = '';
5913 3694 $matches_used = 0;
5914 3695 $matches_used_for_context = [];
5915 - $total_chunks_used = 0;
5916 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5917 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5918 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5919 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5920 -
5921 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5922 - // Use fresh options to ensure we get the latest setting value
5923 - $fresh_options = get_option('mxchat_options', []);
5924 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5925 -
5926 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
5927 - $url_groups = array();
5928 -
3696 +
3697 + // Process each match for actual content generation (lazy role checking)
5929 3698 foreach ($results['matches'] as $index => $match) {
5930 3699 // Skip if similarity is below threshold
5931 3700 if ($match['score'] < $similarity_threshold) {
5932 3701 continue;
5933 3702 }
5934 -
5935 - $metadata = $match['metadata'] ?? array();
5936 - $source_url = $metadata['source_url'] ?? '';
5937 - $match_id = $match['id'] ?? '';
5938 -
5939 - // LAZY ROLE CHECK: Only check role for content we're actually considering
5940 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
5941 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5942 -
5943 - // Skip if user doesn't have access
5944 - if (!$has_access) {
5945 - continue;
5946 - }
5947 -
5948 - // Use a unique key for manual entries without a source URL
5949 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
5950 -
5951 - // Group by source URL (or unique key for manual entries)
5952 - if (!isset($url_groups[$group_key])) {
5953 - $url_groups[$group_key] = array(
5954 - 'source_url' => $source_url,
5955 - 'best_score' => 0,
5956 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
5957 - 'chunks' => array(),
5958 - 'single_text' => ''
5959 - );
5960 - }
5961 -
5962 - // Track best score for this group
5963 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
5964 - $url_groups[$group_key]['best_score'] = $match['score'];
5965 - }
5966 -
5967 - // Store chunk info or single text
5968 - if ($url_groups[$group_key]['is_chunked']) {
5969 - $url_groups[$group_key]['chunks'][] = array(
5970 - 'id' => $match_id,
5971 - 'score' => $match['score'],
5972 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
5973 - 'text' => $metadata['text'] ?? ''
5974 - );
5975 - } else {
5976 - // Non-chunked content - just store the text
5977 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
5978 - $url_groups[$group_key]['single_id'] = $match_id;
5979 - }
5980 - }
5981 -
5982 - // Sort URL groups by best score (highest first)
5983 - uasort($url_groups, function($a, $b) {
5984 - return $b['best_score'] <=> $a['best_score'];
5985 - });
5986 -
5987 - // Get RAG sources limit from options (default 6, min 3, max 10)
5988 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5989 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5990 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5991 -
5992 - // Take top N unique URLs based on user setting
5993 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5994 -
5995 - // Track which match IDs are actually used for context
5996 - foreach ($top_urls as $group) {
5997 - if ($group['is_chunked']) {
5998 - foreach ($group['chunks'] as $chunk) {
5999 - $matches_used_for_context[] = $chunk['id'];
6000 - }
6001 - } elseif (!empty($group['single_id'])) {
6002 - $matches_used_for_context[] = $group['single_id'];
6003 - }
6004 - }
6005 -
6006 - // Build content from top sources
6007 - foreach ($top_urls as $group_key => $group) {
6008 - $source_url = $group['source_url']; // Use actual source_url, not the group key
6009 -
6010 - // Stop if we've hit the total chunk limit
6011 - if ($total_chunks_used >= $max_total_chunks) {
3703 +
3704 + // Limit to top 5 matches above threshold
3705 + if ($matches_used >= 5) {
6012 3706 break;
6013 3707 }
6014 -
6015 - $full_text = '';
6016 - $chunks_in_this_source = 1; // Default for non-chunked content
6017 -
6018 - if ($group['is_chunked']) {
6019 - // Calculate how many chunks we can still use (respect both total and per-source caps)
6020 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
6021 -
6022 - // Fetch chunks for this URL with limit
6023 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
6024 -
6025 - // If fetching all chunks fails, fall back to matched chunks
6026 - if (empty($full_text)) {
6027 - // Sort matched chunks by index and concatenate
6028 - usort($group['chunks'], function($a, $b) {
6029 - return $a['chunk_index'] <=> $b['chunk_index'];
6030 - });
6031 -
6032 - $chunk_texts = array();
6033 - $chunks_in_this_source = 0;
6034 - foreach ($group['chunks'] as $chunk) {
6035 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
6036 - break;
6037 - }
6038 - $chunk_texts[] = $chunk['text'];
6039 - $chunks_in_this_source++;
6040 - }
6041 - $full_text = implode("\n\n", $chunk_texts);
3708 +
3709 + if (!empty($match['metadata']['text'])) {
3710 + // LAZY ROLE CHECK: Only check role for content we're actually considering
3711 + $match_id = $match['id'] ?? '';
3712 + $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
3713 + $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
3714 +
3715 + // Skip if user doesn't have access
3716 + if (!$has_access) {
3717 + continue;
6042 3718 }
6043 - } else {
6044 - $full_text = $group['single_text'];
6045 - $chunks_in_this_source = 1;
6046 - }
6047 -
6048 - if (!empty($full_text)) {
6049 - // Strip URLs from content if citation links are disabled
6050 - if (!$citation_links_enabled) {
6051 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
6052 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
3719 +
3720 + // User has access - add to content
3721 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3722 + $content .= $match['metadata']['text'] . "\n\n";
3723 +
3724 + if (!empty($match['metadata']['source_url'])) {
3725 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
6053 3726 }
6054 -
6055 - // Use numbered reference for URL-based entries, plain info label for manual entries
6056 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
6057 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
6058 - $matches_used++;
6059 - $content .= "## Reference " . $matches_used . " ##\n";
6060 - $content .= $full_text . "\n\n";
6061 -
6062 - // Only include citation URLs if citation links are enabled
6063 - if ($citation_links_enabled) {
6064 - $valid_urls[] = $source_url;
6065 - $content .= "URL: " . $source_url . "\n\n";
6066 - }
6067 - } else {
6068 - // Manual entry — no reference number, no citation
6069 - $content .= "## Information ##\n";
6070 - $content .= $full_text . "\n\n";
6071 - }
6072 -
6073 - // Extract any URLs from the text content itself (only if citation links enabled)
6074 - if ($citation_links_enabled) {
6075 - preg_match_all(
6076 - '#\bhttps?://[^\s<>"\']+#i',
6077 - $full_text,
6078 - $content_urls
6079 - );
6080 - if (!empty($content_urls[0])) {
6081 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6082 - }
6083 - }
6084 -
6085 - $total_chunks_used += $chunks_in_this_source;
3727 +
3728 + $matches_used_for_context[] = $match['id'] ?? $index;
3729 + $matches_used++;
6086 3730 }
6087 3731 }
6088 -
3732 +
6089 3733 // Process ALL matches for testing data (top 10) - with role checking for testing display
6090 3734 $all_matches = [];
6091 3735 foreach ($results['matches'] as $index => $match) {
6092 3736 if ($index >= 10) break; // Limit to top 10 for testing
@@ -6106,19 +3750,9 @@
6106 3750 $source_display = substr(trim($content_preview), 0, 50) . '...';
6107 3751 }
6108 3752
6109 3753 $match_id_for_display = $match['id'] ?? $index;
6110 -
6111 - // Check for chunk metadata in Pinecone
6112 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
6113 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
6114 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
6115 -
6116 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
6117 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
6118 - $is_chunk = true;
6119 - }
6120 -
3754 +
6121 3755 $all_matches[] = [
6122 3756 'document_id' => $match_id_for_display,
6123 3757 'similarity' => $match['score'],
6124 3758 'similarity_percentage' => round($match['score'] * 100, 2),
@@ -6127,12 +3761,9 @@
6127 3761 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
6128 3762 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
6129 3763 'role_restriction' => $role_restriction,
6130 3764 'has_access' => $has_access,
6131 - 'filtered_out' => !$has_access,
6132 - 'is_chunk' => $is_chunk,
6133 - 'chunk_index' => $chunk_index,
6134 - 'total_chunks' => $total_chunks
3765 + 'filtered_out' => !$has_access
6135 3766 ];
6136 3767 }
6137 3768
6138 3769 // Store for testing panel
@@ -6137,40 +3768,23 @@
6137 3768
6138 3769 // Store for testing panel
6139 3770 $this->last_similarity_analysis['top_matches'] = $all_matches;
6140 3771 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
6141 - $this->last_similarity_analysis['sources_used'] = $matches_used;
6142 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
6143 -
6144 - // NEW: Store unique valid URLs for validation
6145 - $this->current_valid_urls = array_unique($valid_urls);
6146 -
6147 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6148 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6149 -
3772 +
3773 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
3774 +
6150 3775 // Add response guidelines
6151 3776 if ($matches_used === 0) {
6152 3777 $content = "No reference information was found for this query.\n\n";
6153 3778 } else {
6154 - // Build response guidelines based on citation links setting
6155 - $content .= "\n## Response Guidelines ##\n" .
6156 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6157 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6158 - "If you don't have specific information or are uncertain about any details, it's always " .
6159 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6160 - "When information is incomplete, let them know you are unsure.\n\n";
6161 -
6162 - // Only add hyperlink instructions if citation links are enabled
6163 - if ($citation_links_enabled) {
6164 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6165 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
6166 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
6167 - } else {
6168 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6169 - "Simply provide helpful answers based on the reference information without citing sources.";
6170 - }
3779 + $content .= "\n## Response Guidelines ##\n" .
3780 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3781 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3782 + "If you don't have specific information or are uncertain about any details, it's always " .
3783 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3784 + "When information is incomplete, let them know you are unsure.";
6171 3785 }
6172 -
3786 +
6173 3787 return trim($content);
6174 3788 }
6175 3789
6176 3790 /**
@@ -6210,518 +3824,12 @@
6210 3824 }
6211 3825
6212 3826 // Cache individual role for 1 hour
6213 3827 wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
6214 -
3828 +
6215 3829 return $role_restriction;
6216 3830 }
6217 3831
6218 -/**
6219 - * Fetch and reassemble all chunks for a URL from Pinecone
6220 - *
6221 - * @param string $source_url The source URL to fetch chunks for
6222 - * @param array $bot_config Bot-specific Pinecone configuration
6223 - * @return string Reassembled content from all chunks
6224 - */
6225 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
6226 - $api_key = $bot_config['api_key'] ?? '';
6227 - $host = $bot_config['host'] ?? '';
6228 - $namespace = $bot_config['namespace'] ?? '';
6229 -
6230 - if (empty($host) || empty($api_key)) {
6231 - $chunk_count = 0;
6232 - return '';
6233 - }
6234 -
6235 - $base_hash = md5($source_url);
6236 -
6237 - // Use Pinecone list API to find all chunk vectors with this prefix
6238 - $list_url = "https://{$host}/vectors/list";
6239 -
6240 - // Limit to max_chunks if specified, otherwise fetch up to 100
6241 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
6242 -
6243 - $list_body = array(
6244 - 'prefix' => $base_hash . '_chunk_',
6245 - 'limit' => $fetch_limit
6246 - );
6247 -
6248 - if (!empty($namespace)) {
6249 - $list_body['namespace'] = $namespace;
6250 - }
6251 -
6252 - $list_response = wp_remote_post($list_url, array(
6253 - 'headers' => array(
6254 - 'Api-Key' => $api_key,
6255 - 'accept' => 'application/json',
6256 - 'content-type' => 'application/json'
6257 - ),
6258 - 'body' => wp_json_encode($list_body),
6259 - 'timeout' => 30
6260 - ));
6261 -
6262 - if (is_wp_error($list_response)) {
6263 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
6264 - return '';
6265 - }
6266 -
6267 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
6268 -
6269 - if (empty($list_data['vectors'])) {
6270 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
6271 - return '';
6272 - }
6273 -
6274 - // Extract vector IDs
6275 - $vector_ids = array();
6276 - foreach ($list_data['vectors'] as $vector) {
6277 - if (isset($vector['id'])) {
6278 - $vector_ids[] = $vector['id'];
6279 - }
6280 - }
6281 -
6282 - if (empty($vector_ids)) {
6283 - return '';
6284 - }
6285 -
6286 - // Fetch all chunk content
6287 - $fetch_url = "https://{$host}/vectors/fetch";
6288 -
6289 - $fetch_body = array(
6290 - 'ids' => $vector_ids
6291 - );
6292 -
6293 - if (!empty($namespace)) {
6294 - $fetch_body['namespace'] = $namespace;
6295 - }
6296 -
6297 - $fetch_response = wp_remote_post($fetch_url, array(
6298 - 'headers' => array(
6299 - 'Api-Key' => $api_key,
6300 - 'accept' => 'application/json',
6301 - 'content-type' => 'application/json'
6302 - ),
6303 - 'body' => wp_json_encode($fetch_body),
6304 - 'timeout' => 30
6305 - ));
6306 -
6307 - if (is_wp_error($fetch_response)) {
6308 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
6309 - return '';
6310 - }
6311 -
6312 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
6313 -
6314 - if (empty($fetch_data['vectors'])) {
6315 - return '';
6316 - }
6317 -
6318 - // Sort chunks by index and reassemble
6319 - $chunks = array();
6320 - foreach ($fetch_data['vectors'] as $id => $vector) {
6321 - $metadata = $vector['metadata'] ?? array();
6322 - $chunk_index = $metadata['chunk_index'] ?? 0;
6323 - $text = $metadata['text'] ?? '';
6324 -
6325 - // Store chunk with its index
6326 - $chunks[$chunk_index] = $text;
6327 - }
6328 -
6329 - // Sort by chunk index
6330 - ksort($chunks);
6331 -
6332 - // Apply chunk limit if specified
6333 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
6334 - $chunks = array_slice($chunks, 0, $max_chunks, true);
6335 - }
6336 -
6337 - // Store actual chunk count
6338 - $chunk_count = count($chunks);
6339 -
6340 - // Reassemble content
6341 - return implode("\n\n", $chunks);
6342 -}
6343 -
6344 -/**
6345 - * Search for relevant content using OpenAI Vector Store (File Search)
6346 - *
6347 - * @param string $user_query The user's query text
6348 - * @param string $bot_id The bot ID
6349 - * @param array $vectorstore_config Vector Store configuration
6350 - * @return string Formatted context string with references
6351 - */
6352 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
6353 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
6354 - //error_log(" - bot_id: " . $bot_id);
6355 - //error_log(" - user_query length: " . strlen($user_query));
6356 -
6357 - // Get OpenAI API key
6358 - $mxchat_options = get_option('mxchat_options', array());
6359 - $api_key = $mxchat_options['api_key'] ?? '';
6360 -
6361 - // Reset vectorstore error tracking
6362 - $this->last_vectorstore_error = null;
6363 -
6364 - if (empty($api_key)) {
6365 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
6366 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
6367 - $this->current_valid_urls = [];
6368 - return '';
6369 - }
6370 -
6371 - // Get Vector Store configuration
6372 - if (empty($vectorstore_config)) {
6373 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
6374 - }
6375 -
6376 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
6377 - $max_results = $vectorstore_config['max_results'] ?? 5;
6378 -
6379 - if (empty($vectorstore_ids_string)) {
6380 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
6381 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
6382 - $this->current_valid_urls = [];
6383 - return '';
6384 - }
6385 -
6386 - // Parse Vector Store IDs
6387 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
6388 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
6389 -
6390 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
6391 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
6392 -
6393 - // Initialize similarity analysis storage
6394 - $this->last_similarity_analysis = [
6395 - 'knowledge_base_type' => 'OpenAI Vector Store',
6396 - 'bot_id' => $bot_id,
6397 - 'vectorstore_ids' => $vectorstore_ids,
6398 - 'top_matches' => [],
6399 - 'threshold_used' => 0,
6400 - 'total_checked' => 0
6401 - ];
6402 -
6403 - $valid_urls = [];
6404 -
6405 - // Get the selected model
6406 - $bot_options = $this->get_bot_options($bot_id);
6407 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
6408 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
6409 -
6410 - // Verify it's an OpenAI model
6411 - if (!$this->is_openai_chat_model($selected_model)) {
6412 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
6413 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
6414 - $this->current_valid_urls = [];
6415 - return '';
6416 - }
6417 -
6418 - // Use OpenAI Responses API with file_search tool
6419 - $request_body = array(
6420 - 'model' => $selected_model,
6421 - 'input' => $user_query,
6422 - 'tools' => array(
6423 - array(
6424 - 'type' => 'file_search',
6425 - 'vector_store_ids' => $vectorstore_ids,
6426 - 'max_num_results' => intval($max_results)
6427 - )
6428 - ),
6429 - 'include' => array('output[*].file_search_call.search_results')
6430 - );
6431 -
6432 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
6433 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
6434 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
6435 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
6436 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
6437 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
6438 -
6439 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
6440 - 'headers' => array(
6441 - 'Authorization' => 'Bearer ' . $api_key,
6442 - 'Content-Type' => 'application/json'
6443 - ),
6444 - 'body' => wp_json_encode($request_body),
6445 - 'timeout' => 60
6446 - ));
6447 -
6448 - if (is_wp_error($response)) {
6449 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
6450 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
6451 - $this->current_valid_urls = [];
6452 - return '';
6453 - }
6454 -
6455 - $response_code = wp_remote_retrieve_response_code($response);
6456 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
6457 -
6458 - $response_body = wp_remote_retrieve_body($response);
6459 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
6460 -
6461 - if ($response_code !== 200) {
6462 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
6463 - $api_error_detail = '';
6464 - $decoded_error = json_decode($response_body, true);
6465 - if (isset($decoded_error['error']['message'])) {
6466 - $api_error_detail = $decoded_error['error']['message'];
6467 - }
6468 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
6469 - $this->current_valid_urls = [];
6470 - return '';
6471 - }
6472 - $result = json_decode($response_body, true);
6473 -
6474 - if (json_last_error() !== JSON_ERROR_NONE) {
6475 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
6476 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
6477 - $this->current_valid_urls = [];
6478 - return '';
6479 - }
6480 -
6481 - // Debug: Log the structure of the result
6482 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
6483 - if (isset($result['output'])) {
6484 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
6485 - foreach ($result['output'] as $idx => $out) {
6486 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
6487 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
6488 - }
6489 - } else {
6490 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
6491 - }
6492 -
6493 - // Extract file search results from the response
6494 - $content = '';
6495 - $matches_used = 0;
6496 - $all_matches = [];
6497 -
6498 - // The Responses API returns output array with tool results
6499 - if (isset($result['output']) && is_array($result['output'])) {
6500 - foreach ($result['output'] as $output_item) {
6501 - // Look for file_search_call results
6502 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
6503 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
6504 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
6505 -
6506 - // Check for search_results in the output item directly
6507 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
6508 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
6509 -
6510 - if (empty($search_results)) {
6511 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
6512 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
6513 - }
6514 -
6515 - foreach ($search_results as $index => $search_result) {
6516 - $filename = $search_result['filename'] ?? '';
6517 - $score = $search_result['score'] ?? 0;
6518 - $text_content = '';
6519 -
6520 - // Extract text content from the result
6521 - // The text can be directly on the result OR nested under content array
6522 - if (isset($search_result['text']) && !empty($search_result['text'])) {
6523 - // Direct text field (OpenAI's actual format)
6524 - $text_content = $search_result['text'];
6525 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
6526 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
6527 - // Nested content array format
6528 - foreach ($search_result['content'] as $content_item) {
6529 - if (isset($content_item['text'])) {
6530 - $text_content .= $content_item['text'] . "\n";
6531 - }
6532 - }
6533 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
6534 - } else {
6535 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
6536 - }
6537 -
6538 - if (!empty($text_content)) {
6539 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6540 - $content .= trim($text_content) . "\n\n";
6541 -
6542 - if (!empty($filename)) {
6543 - $content .= "Source: " . $filename . "\n\n";
6544 - }
6545 -
6546 - // Extract URLs from content
6547 - preg_match_all(
6548 - '#\bhttps?://[^\s<>"\']+#i',
6549 - $text_content,
6550 - $content_urls
6551 - );
6552 - if (!empty($content_urls[0])) {
6553 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6554 - }
6555 -
6556 - $matches_used++;
6557 - }
6558 -
6559 - // Store for similarity analysis
6560 - $all_matches[] = [
6561 - 'document_id' => $filename ?: ('result_' . $index),
6562 - 'similarity' => $score,
6563 - 'similarity_percentage' => round($score * 100, 2),
6564 - 'above_threshold' => true,
6565 - 'source_display' => $filename,
6566 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6567 - 'used_for_context' => true,
6568 - 'role_restriction' => 'public',
6569 - 'has_access' => true,
6570 - 'filtered_out' => false
6571 - ];
6572 - }
6573 - }
6574 -
6575 - // Also check for message content with annotations (citations)
6576 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6577 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6578 - foreach ($output_item['content'] as $content_block) {
6579 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6580 - foreach ($content_block['annotations'] as $annotation) {
6581 - if (isset($annotation['filename'])) {
6582 - $filename = $annotation['filename'];
6583 - $score = $annotation['score'] ?? 0;
6584 - $text_content = '';
6585 -
6586 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6587 - foreach ($annotation['content'] as $ann_content) {
6588 - if (isset($ann_content['text'])) {
6589 - $text_content .= $ann_content['text'] . "\n";
6590 - }
6591 - }
6592 - }
6593 -
6594 - if (!empty($text_content) && $matches_used < $max_results) {
6595 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6596 - $content .= trim($text_content) . "\n\n";
6597 - $content .= "Source: " . $filename . "\n\n";
6598 -
6599 - preg_match_all(
6600 - '#\bhttps?://[^\s<>"\']+#i',
6601 - $text_content,
6602 - $content_urls
6603 - );
6604 - if (!empty($content_urls[0])) {
6605 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6606 - }
6607 -
6608 - $matches_used++;
6609 -
6610 - $all_matches[] = [
6611 - 'document_id' => $filename,
6612 - 'similarity' => $score,
6613 - 'similarity_percentage' => round($score * 100, 2),
6614 - 'above_threshold' => true,
6615 - 'source_display' => $filename,
6616 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6617 - 'used_for_context' => true,
6618 - 'role_restriction' => 'public',
6619 - 'has_access' => true,
6620 - 'filtered_out' => false
6621 - ];
6622 - }
6623 - }
6624 - }
6625 - }
6626 - }
6627 - }
6628 - }
6629 - }
6630 - }
6631 -
6632 - // Store for testing panel
6633 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6634 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6635 -
6636 - // Store unique valid URLs for validation
6637 - $this->current_valid_urls = array_unique($valid_urls);
6638 -
6639 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6640 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6641 -
6642 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6643 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6644 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6645 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6646 - if ($matches_used > 0) {
6647 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6648 - }
6649 -
6650 - // Check if citation links are enabled
6651 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6652 -
6653 - // Add response guidelines
6654 - if ($matches_used === 0) {
6655 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6656 - $content = "No reference information was found for this query.\n\n";
6657 - } else {
6658 - // Build response guidelines based on citation links setting
6659 - $content .= "\n## Response Guidelines ##\n" .
6660 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6661 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6662 - "If you don't have specific information or are uncertain about any details, it's always " .
6663 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6664 - "When information is incomplete, let them know you are unsure.\n\n";
6665 -
6666 - // Only add hyperlink instructions if citation links are enabled
6667 - if ($citation_links_enabled) {
6668 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6669 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6670 - } else {
6671 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6672 - "Simply provide helpful answers based on the reference information without citing sources.";
6673 - }
6674 - }
6675 -
6676 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6677 -
6678 - return trim($content);
6679 -}
6680 -
6681 -/**
6682 - * Check if the given model is an OpenAI chat model
6683 - *
6684 - * @param string $model The model ID
6685 - * @return bool True if it's an OpenAI model
6686 - */
6687 -private function is_openai_chat_model($model) {
6688 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6689 - foreach ($openai_prefixes as $prefix) {
6690 - if (strpos($model, $prefix) === 0) {
6691 - return true;
6692 - }
6693 - }
6694 - return false;
6695 -}
6696 -
6697 -/**
6698 - * Get bot-specific Vector Store configuration
6699 - *
6700 - * @param string $bot_id The bot ID
6701 - * @return array Configuration array
6702 - */
6703 -private function get_bot_vectorstore_config($bot_id = 'default') {
6704 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6705 -
6706 - // Default global settings
6707 - $default_config = array(
6708 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6709 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6710 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6711 - );
6712 -
6713 - // Allow multi-bot plugin to override with bot-specific settings
6714 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6715 -
6716 - // Preserve max_results from global settings if not set in bot config
6717 - if (!isset($bot_config['max_results'])) {
6718 - $bot_config['max_results'] = $default_config['max_results'];
6719 - }
6720 -
6721 - return $bot_config;
6722 -}
6723 -
6724 3832 private function mxchat_find_relevant_products($user_embedding) {
6725 3833 //error_log('MXChat Vector Search: Starting product search...');
6726 3834
6727 3835 // Retrieve the add-on settings from the database
@@ -6742,75 +3850,73 @@
6742 3850 }
6743 3851 private function find_relevant_products_wordpress($user_embedding) {
6744 3852 global $wpdb;
6745 3853 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3854 + $cache_key = 'mxchat_system_prompt_embeddings';
3855 + $batch_size = 500;
6746 3856
6747 - if (!is_array($user_embedding)) {
6748 - return '';
6749 - }
3857 + // Original WordPress database search logic
3858 + // [Previous implementation remains the same]
3859 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3860 + if ($embeddings === false) {
3861 + $embeddings = [];
3862 + $offset = 0;
6750 3863
6751 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6752 - // results above the similarity threshold. Peak memory is bounded by
6753 - // $batch_size embedding rows plus a 3-element top list.
6754 - $batch_size = 250;
6755 - $similarity_threshold = 0.85;
6756 - $top_k = 3;
6757 - $top_results = [];
6758 - $offset = 0;
3864 + do {
3865 + $query = $wpdb->prepare(
3866 + "SELECT id, embedding_vector
3867 + FROM {$system_prompt_table}
3868 + LIMIT %d OFFSET %d",
3869 + $batch_size,
3870 + $offset
3871 + );
6759 3872
6760 - do {
6761 - $batch = $wpdb->get_results($wpdb->prepare(
6762 - "SELECT id, embedding_vector
6763 - FROM {$system_prompt_table}
6764 - LIMIT %d OFFSET %d",
6765 - $batch_size,
6766 - $offset
6767 - ));
3873 + $batch = $wpdb->get_results($query);
3874 + if (empty($batch)) {
3875 + break;
3876 + }
6768 3877
6769 - if (empty($batch)) {
6770 - break;
6771 - }
3878 + $embeddings = array_merge($embeddings, $batch);
3879 + $offset += $batch_size;
6772 3880
6773 - foreach ($batch as $row) {
6774 - $database_embedding = $row->embedding_vector
6775 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6776 - : null;
3881 + unset($batch);
6777 3882
6778 - if (!is_array($database_embedding)) {
6779 - unset($database_embedding);
6780 - continue;
6781 - }
3883 + } while (true);
6782 3884
3885 + if (empty($embeddings)) {
3886 + return '';
3887 + }
3888 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3889 + }
3890 +
3891 + $relevant_results = [];
3892 + foreach ($embeddings as $embedding) {
3893 + $database_embedding = $embedding->embedding_vector
3894 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3895 + : null;
3896 + if (is_array($database_embedding) && is_array($user_embedding)) {
6783 3897 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6784 - unset($database_embedding);
6785 -
6786 - if ($similarity < $similarity_threshold) {
6787 - continue;
6788 - }
6789 -
6790 - // Insert into bounded top-K (kept sorted descending)
6791 - if (count($top_results) < $top_k) {
6792 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6793 - usort($top_results, function ($a, $b) {
6794 - return $b['similarity'] <=> $a['similarity'];
6795 - });
6796 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6797 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6798 - usort($top_results, function ($a, $b) {
6799 - return $b['similarity'] <=> $a['similarity'];
6800 - });
6801 - }
3898 + $relevant_results[] = [
3899 + 'id' => $embedding->id,
3900 + 'similarity' => $similarity
3901 + ];
6802 3902 }
3903 + unset($database_embedding);
3904 + }
6803 3905
6804 - unset($batch);
6805 - $offset += $batch_size;
6806 - } while (true);
3906 + // Use fixed threshold for products
3907 + $similarity_threshold = 0.85;
6807 3908
6808 - if (empty($top_results)) {
6809 - return '';
6810 - }
3909 + $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3910 + return $result['similarity'] >= $similarity_threshold;
3911 + });
3912 + usort($relevant_results, function ($a, $b) {
3913 + return $b['similarity'] <=> $a['similarity'];
3914 + });
6811 3915
3916 + $top_results = array_slice($relevant_results, 0, 5);
6812 3917 $content = '';
3918 +
6813 3919 foreach ($top_results as $result) {
6814 3920 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6815 3921 $content .= $chunk_content . "\n\n";
6816 3922 }
@@ -6816,10 +3922,8 @@
6816 3922 }
6817 3923
6818 3924 return trim($content);
6819 3925 }
6820 -
6821 -
6822 3926 private function find_relevant_products_pinecone($user_embedding) {
6823 3927 //error_log('Starting Pinecone product search...');
6824 3928
6825 3929 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -6894,10 +3998,8 @@
6894 3998 }
6895 3999
6896 4000 return trim($content);
6897 4001 }
6898 -
6899 -
6900 4002 private function fetch_content_with_product_links($most_relevant_id) {
6901 4003 global $wpdb;
6902 4004 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
6903 4005
@@ -6917,85 +4019,12 @@
6917 4019 return null;
6918 4020 }
6919 4021
6920 4022 /**
6921 - * Get system instructions for a specific bot or default
6922 - * Checks for multi-bot add-on and uses bot-specific instructions if available
6923 - * Automatically strips URLs if citation links are disabled
6924 - * Replaces {visitor_name} placeholder with actual visitor name if available
6925 - *
6926 - * @param string $bot_id The bot ID to get instructions for
6927 - * @param string $session_id Optional session ID to lookup visitor name
4023 + * Modified streaming functions to include testing data
6928 4024 */
6929 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
6930 - $instructions = '';
6931 4025
6932 - // Check if multi-bot add-on is active
6933 - if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
6934 - // Get bot-specific options from multi-bot add-on
6935 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
6936 -
6937 - // If bot has custom system instructions, use those
6938 - if (!empty($bot_options['system_prompt_instructions'])) {
6939 - $instructions = $bot_options['system_prompt_instructions'];
6940 - }
6941 - }
6942 -
6943 - // Fall back to default system instructions
6944 - if (empty($instructions)) {
6945 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
6946 - }
6947 -
6948 - // Check if citation links are disabled - if so, strip URLs from instructions
6949 - $fresh_options = get_option('mxchat_options', []);
6950 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6951 -
6952 - if (!$citation_links_enabled && !empty($instructions)) {
6953 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
6954 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
6955 - }
6956 -
6957 - // Replace {visitor_name} placeholder with actual visitor name if available
6958 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
6959 - $name_option_key = "mxchat_name_{$session_id}";
6960 - $visitor_name = get_option($name_option_key, '');
6961 -
6962 - if (!empty($visitor_name)) {
6963 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
6964 - } else {
6965 - // Remove placeholder if no name is available
6966 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
6967 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
6968 - }
6969 - }
6970 -
6971 - // Allow developers to filter system instructions and process shortcodes
6972 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
6973 - $instructions = do_shortcode($instructions);
6974 -
6975 - return $instructions;
6976 -}
6977 -/**
6978 - * Get the current bot ID from session or request context
6979 - */
6980 -private function get_current_bot_id($session_id = '') {
6981 - // First, check if bot_id is passed in the current request
6982 - if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
6983 - return sanitize_key($_POST['bot_id']);
6984 - }
6985 -
6986 - // If not in POST, try to get it from session data
6987 - if (!empty($session_id)) {
6988 - $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
6989 - if (!empty($bot_id)) {
6990 - return $bot_id;
6991 - }
6992 - }
6993 -
6994 - // Fall back to default
6995 - return 'default';
6996 -}
6997 -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-5.1-chat-latest') {
4026 +private function mxchat_generate_response($relevant_content, $api_key, $xai_api_key, $claude_api_key, $deepseek_api_key, $gemini_api_key, $conversation_history, $streaming = false, $session_id = '', $testing_data = null) {
6998 4027 try {
6999 4028 if (!$relevant_content) {
7000 4029 $error_response = [
7001 4030 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
@@ -7001,74 +4030,25 @@
7001 4030 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
7002 4031 'error_code' => 'no_relevant_content'
7003 4032 ];
7004 4033
4034 + // Add testing data to error response if available
7005 4035 if ($testing_data !== null) {
7006 4036 $error_response['testing_data'] = $testing_data;
4037 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
7007 4038 }
7008 4039
7009 4040 return $error_response;
7010 4041 }
7011 4042
4043 + // Ensure conversation_history is an array
7012 4044 if (!is_array($conversation_history)) {
7013 4045 $conversation_history = array();
7014 4046 }
7015 4047
7016 - // Check if this is an OpenRouter model
7017 - if ($selected_model === 'openrouter') {
7018 - // Get the actual OpenRouter model from options
7019 - $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
7020 -
7021 - if (empty($openrouter_selected_model)) {
7022 - $error_response = [
7023 - 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
7024 - 'error_code' => 'no_openrouter_model_selected'
7025 - ];
7026 - if ($testing_data !== null) {
7027 - $error_response['testing_data'] = $testing_data;
7028 - }
7029 - return $error_response;
7030 - }
7031 -
7032 - if (empty($openrouter_api_key)) {
7033 - $error_response = [
7034 - 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
7035 - 'error_code' => 'missing_openrouter_api_key'
7036 - ];
7037 - if ($testing_data !== null) {
7038 - $error_response['testing_data'] = $testing_data;
7039 - }
7040 - return $error_response;
7041 - }
7042 -
7043 - if ($streaming) {
7044 - return $this->mxchat_generate_response_openrouter_stream(
7045 - $openrouter_selected_model,
7046 - $openrouter_api_key,
7047 - $conversation_history,
7048 - $relevant_content,
7049 - $session_id,
7050 - $testing_data
7051 - );
7052 - } else {
7053 - $response = $this->mxchat_generate_response_openrouter(
7054 - $openrouter_selected_model,
7055 - $openrouter_api_key,
7056 - $conversation_history,
7057 - $relevant_content
7058 - );
7059 - }
7060 -
7061 - if (is_array($response) && isset($response['error'])) {
7062 - if ($testing_data !== null) {
7063 - $response['testing_data'] = $testing_data;
7064 - }
7065 - return $response;
7066 - }
7067 -
7068 - return $response;
7069 - }
7070 -
4048 + // Get selected model with default fallback
4049 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
4050 +
7071 4051 // Extract model prefix to determine the provider
7072 4052 $model_parts = explode('-', $selected_model);
7073 4053 $provider = strtolower($model_parts[0]);
7074 4054
@@ -7110,9 +4090,9 @@
7110 4090 $claude_api_key,
7111 4091 $conversation_history,
7112 4092 $relevant_content,
7113 4093 $session_id,
7114 - $testing_data
4094 + $testing_data // Pass testing data
7115 4095 );
7116 4096 } else {
7117 4097 $response = $this->mxchat_generate_response_claude(
7118 4098 $selected_model,
@@ -7140,9 +4120,9 @@
7140 4120 $xai_api_key,
7141 4121 $conversation_history,
7142 4122 $relevant_content,
7143 4123 $session_id,
7144 - $testing_data
4124 + $testing_data // Pass testing data
7145 4125 );
7146 4126 } else {
7147 4127 $response = $this->mxchat_generate_response_xai(
7148 4128 $selected_model,
@@ -7170,9 +4150,9 @@
7170 4150 $deepseek_api_key,
7171 4151 $conversation_history,
7172 4152 $relevant_content,
7173 4153 $session_id,
7174 - $testing_data
4154 + $testing_data // Pass testing data
7175 4155 );
7176 4156 } else {
7177 4157 $response = $this->mxchat_generate_response_deepseek(
7178 4158 $selected_model,
@@ -7182,38 +4162,8 @@
7182 4162 );
7183 4163 }
7184 4164 break;
7185 4165
7186 - case 'custom':
7187 - // Custom (OpenAI-compatible) provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI
7188 - $cp_base_url = isset($this->options['custom_provider_base_url']) ? trim((string) $this->options['custom_provider_base_url']) : '';
7189 - if (empty($cp_base_url)) {
7190 - $error_response = [
7191 - 'error' => esc_html__('Custom provider is not configured. Set Base URL in MxChat → API Keys → Custom Provider.', 'mxchat'),
7192 - 'error_code' => 'missing_custom_provider_base_url'
7193 - ];
7194 - if ($testing_data !== null) {
7195 - $error_response['testing_data'] = $testing_data;
7196 - }
7197 - return $error_response;
7198 - }
7199 - if ($streaming) {
7200 - return $this->mxchat_generate_response_custom_stream(
7201 - $selected_model,
7202 - $conversation_history,
7203 - $relevant_content,
7204 - $session_id,
7205 - $testing_data
7206 - );
7207 - } else {
7208 - $response = $this->mxchat_generate_response_custom(
7209 - $selected_model,
7210 - $conversation_history,
7211 - $relevant_content
7212 - );
7213 - }
7214 - break;
7215 -
7216 4166 case 'gpt':
7217 4167 case 'o1':
7218 4168 if (empty($api_key)) {
7219 4169 $error_response = [
@@ -7224,27 +4174,9 @@
7224 4174 $error_response['testing_data'] = $testing_data;
7225 4175 }
7226 4176 return $error_response;
7227 4177 }
7228 -
7229 - // Check if web search is enabled for this OpenAI model
7230 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7231 - // Models that don't support web search
7232 - $unsupported_web_search_models = array('gpt-4.1-nano');
7233 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
7234 -
7235 - if ($web_search_enabled && $model_supports_web_search) {
7236 - // Use Responses API (required for some models, or when web search is enabled)
7237 - return $this->mxchat_generate_response_openai_web_search(
7238 - $selected_model,
7239 - $api_key,
7240 - $conversation_history,
7241 - $relevant_content,
7242 - $session_id,
7243 - $testing_data,
7244 - $streaming
7245 - );
7246 - } elseif ($streaming) {
4178 + if ($streaming) {
7247 4179 return $this->mxchat_generate_response_openai_stream(
7248 4180 $selected_model,
7249 4181 $api_key,
7250 4182 $conversation_history,
@@ -7249,9 +4181,9 @@
7249 4181 $api_key,
7250 4182 $conversation_history,
7251 4183 $relevant_content,
7252 4184 $session_id,
7253 - $testing_data
4185 + $testing_data // Pass testing data
7254 4186 );
7255 4187 } else {
7256 4188 $response = $this->mxchat_generate_response_openai(
7257 4189 $selected_model,
@@ -7262,8 +4194,9 @@
7262 4194 }
7263 4195 break;
7264 4196
7265 4197 default:
4198 + // Default to OpenAI for custom models or unrecognized prefixes
7266 4199 if (empty($api_key)) {
7267 4200 $error_response = [
7268 4201 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
7269 4202 'error_code' => 'missing_openai_api_key'
@@ -7272,25 +4205,9 @@
7272 4205 $error_response['testing_data'] = $testing_data;
7273 4206 }
7274 4207 return $error_response;
7275 4208 }
7276 -
7277 - // Check if web search is enabled (default case also handles OpenAI models)
7278 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7279 - $unsupported_web_search_models = array('gpt-4.1-nano');
7280 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
7281 -
7282 - if ($web_search_enabled && $model_supports_web_search) {
7283 - return $this->mxchat_generate_response_openai_web_search(
7284 - $selected_model,
7285 - $api_key,
7286 - $conversation_history,
7287 - $relevant_content,
7288 - $session_id,
7289 - $testing_data,
7290 - $streaming
7291 - );
7292 - } elseif ($streaming) {
4209 + if ($streaming) {
7293 4210 return $this->mxchat_generate_response_openai_stream(
7294 4211 $selected_model,
7295 4212 $api_key,
7296 4213 $conversation_history,
@@ -7295,9 +4212,9 @@
7295 4212 $api_key,
7296 4213 $conversation_history,
7297 4214 $relevant_content,
7298 4215 $session_id,
7299 - $testing_data
4216 + $testing_data // Pass testing data
7300 4217 );
7301 4218 } else {
7302 4219 $response = $this->mxchat_generate_response_openai(
7303 4220 $selected_model,
@@ -7308,18 +4225,24 @@
7308 4225 }
7309 4226 break;
7310 4227 }
7311 4228
4229 + // Check if the response is an error array from the provider-specific function
7312 4230 if (is_array($response) && isset($response['error'])) {
4231 + // Add testing data to error response if available
7313 4232 if ($testing_data !== null) {
7314 4233 $response['testing_data'] = $testing_data;
4234 + //error_log("MxChat Testing: Added testing data to provider error response");
7315 4235 }
7316 - return $response;
4236 + return $response; // Pass through the error with testing data
7317 4237 }
7318 4238
4239 + // For successful non-streaming responses, we don't add testing data here
4240 + // because it will be added in the main handler
7319 4241 return $response;
7320 4242
7321 4243 } catch (Exception $e) {
4244 + //error_log('MXChat Error: ' . $e->getMessage());
7322 4245 $error_response = [
7323 4246 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
7324 4247 'error_code' => 'system_exception',
7325 4248 'exception_details' => $e->getMessage()
@@ -7324,24 +4247,29 @@
7324 4247 'error_code' => 'system_exception',
7325 4248 'exception_details' => $e->getMessage()
7326 4249 ];
7327 4250
4251 + // Add testing data to exception response if available
7328 4252 if ($testing_data !== null) {
7329 4253 $error_response['testing_data'] = $testing_data;
4254 + //error_log("MxChat Testing: Added testing data to exception response");
7330 4255 }
7331 4256
7332 4257 return $error_response;
7333 4258 }
7334 4259 }
7335 -private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4260 +
4261 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7336 4262 try {
7337 - $bot_id = $this->get_current_bot_id($session_id);
7338 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4263 + // Get system prompt instructions from options
4264 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7339 4265
4266 + // Ensure conversation_history is an array
7340 4267 if (!is_array($conversation_history)) {
7341 4268 $conversation_history = array();
7342 4269 }
7343 4270
4271 + // Format conversation history for OpenAI
7344 4272 $formatted_conversation = array();
7345 4273
7346 4274 $formatted_conversation[] = array(
7347 4275 'role' => 'system',
@@ -7353,9 +4281,9 @@
7353 4281 $role = $message['role'];
7354 4282 if ($role === 'bot' || $role === 'agent') {
7355 4283 $role = 'assistant';
7356 4284 }
7357 - if (!in_array($role, ['system', 'assistant', 'user'])) {
4285 + if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
7358 4286 $role = 'user';
7359 4287 }
7360 4288 $formatted_conversation[] = array(
7361 4289 'role' => $role,
@@ -7363,21 +4291,19 @@
7363 4291 );
7364 4292 }
7365 4293 }
7366 4294
4295 + // Check if we can actually stream
7367 4296 if (headers_sent() || !function_exists('curl_init')) {
7368 - $regular_response = $this->mxchat_generate_response_openrouter(
4297 + // Fallback to regular response with testing data
4298 + //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4299 + $regular_response = $this->mxchat_generate_response_openai(
7369 4300 $selected_model,
7370 - $openrouter_api_key,
4301 + $api_key,
7371 4302 $conversation_history,
7372 4303 $relevant_content
7373 4304 );
7374 4305
7375 - // Save bot response to transcript
7376 - if (!empty($regular_response) && !empty($session_id)) {
7377 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7378 - }
7379 -
7380 4306 $response_data = [
7381 4307 'text' => $regular_response,
7382 4308 'html' => '',
7383 4309 'session_id' => $session_id
@@ -7384,8 +4310,9 @@
7384 4310 ];
7385 4311
7386 4312 if ($testing_data !== null) {
7387 4313 $response_data['testing_data'] = $testing_data;
4314 + //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
7388 4315 }
7389 4316
7390 4317 header('Content-Type: application/json');
7391 4318 echo json_encode($response_data);
@@ -7391,8 +4318,9 @@
7391 4318 echo json_encode($response_data);
7392 4319 return true;
7393 4320 }
7394 4321
4322 + // Prepare the request body with stream: true
7395 4323 $body = json_encode([
7396 4324 'model' => $selected_model,
7397 4325 'messages' => $formatted_conversation,
7398 4326 'temperature' => 1,
@@ -7398,213 +4326,71 @@
7398 4326 'temperature' => 1,
7399 4327 'stream' => true
7400 4328 ]);
7401 4329
7402 - // V2 retry-on-initial-connect: setup_streaming_headers is now lazy-fired
7403 - // inside WRITEFUNCTION on first byte of a successful upstream.
7404 -
7405 - $captured_status_code = 0;
7406 - $captured_body_pre_stream = '';
7407 - $full_response = '';
4330 + // Use cURL for streaming support
4331 + $ch = curl_init();
4332 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4333 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4334 + curl_setopt($ch, CURLOPT_POST, true);
4335 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4336 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4337 + 'Content-Type: application/json',
4338 + 'Authorization: Bearer ' . $api_key
4339 + ));
4340 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4341 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4342 +
4343 + $full_response = ''; // Accumulate full response for saving
7408 4344 $stream_started = false;
7409 - $buffer = '';
7410 - $errno = 0;
7411 - $last_curl_error = '';
7412 - $http_code = 0;
7413 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
7414 - $backoff_ms = array(0, 750, 2000);
7415 -
7416 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
7417 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
7418 - usleep($backoff_ms[$attempt] * 1000);
4345 +
4346 + // Buffer control for real-time streaming
4347 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4348 + // Send testing data as the first event if available
4349 + if (!$stream_started && $testing_data !== null) {
4350 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4351 + flush();
4352 + $stream_started = true;
4353 + //error_log("MxChat Testing: Sent testing data in OpenAI stream");
7419 4354 }
7420 -
7421 - $captured_status_code = 0;
7422 - $captured_body_pre_stream = '';
7423 - $full_response = '';
7424 - $stream_started = false;
7425 - $buffer = '';
7426 -
7427 - $ch = curl_init();
7428 - curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');
7429 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7430 - curl_setopt($ch, CURLOPT_POST, true);
7431 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7432 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7433 - 'Content-Type: application/json',
7434 - 'Authorization: Bearer ' . $openrouter_api_key,
7435 - 'HTTP-Referer: ' . home_url(),
7436 - 'X-Title: ' . get_bloginfo('name')
7437 - ));
7438 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7439 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7440 -
7441 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
7442 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
7443 - $captured_status_code = (int) $m[1];
4355 +
4356 + // Process each chunk of data
4357 + $lines = explode("\n", $data);
4358 +
4359 + foreach ($lines as $line) {
4360 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4361 + continue;
7444 4362 }
7445 - return strlen($header);
7446 - });
7447 -
7448 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
7449 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
7450 - $captured_body_pre_stream .= $data;
7451 - return strlen($data);
4363 +
4364 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4365 +
4366 + if ($json_str === '[DONE]') {
4367 + echo "data: [DONE]\n\n";
4368 + flush();
4369 + continue;
7452 4370 }
7453 -
7454 - if (!$this->streaming_headers_sent) {
7455 - $this->setup_streaming_headers();
7456 - }
7457 -
7458 - if (!$stream_started && $testing_data !== null) {
7459 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4371 +
4372 + $json = json_decode($json_str, true);
4373 + if (isset($json['choices'][0]['delta']['content'])) {
4374 + $content = $json['choices'][0]['delta']['content'];
4375 + $full_response .= $content; // Accumulate
4376 + // Send as SSE format
4377 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
7460 4378 flush();
7461 - $stream_started = true;
7462 4379 }
7463 -
7464 - $buffer .= $data;
7465 - $lines = explode("\n", $buffer);
7466 - $buffer = array_pop($lines);
7467 -
7468 - foreach ($lines as $line) {
7469 - if (trim($line) === '') {
7470 - continue;
7471 - }
7472 - if (strpos($line, 'data: ') !== 0) {
7473 - continue;
7474 - }
7475 -
7476 - $json_str = substr($line, 6);
7477 -
7478 - if (trim($json_str) === '[DONE]') {
7479 - echo "data: [DONE]\n\n";
7480 - flush();
7481 - continue;
7482 - }
7483 -
7484 - $json = json_decode(trim($json_str), true);
7485 - if ($json && isset($json['choices'][0]['delta']['content'])) {
7486 - $content = $json['choices'][0]['delta']['content'];
7487 - $full_response .= $content;
7488 -
7489 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7490 - flush();
7491 - }
7492 - }
7493 -
7494 - return strlen($data);
7495 - });
7496 -
7497 - $response = curl_exec($ch);
7498 - $errno = curl_errno($ch);
7499 - $last_curl_error = curl_error($ch);
7500 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
7501 - curl_close($ch);
7502 -
7503 - if (!$errno && $http_code === 200) {
7504 - break;
7505 4380 }
7506 -
7507 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
7508 - $can_retry = !$this->streaming_headers_sent
7509 - && ($attempt + 1) < $max_attempts
7510 - && $is_transient;
7511 -
7512 - if (defined('WP_DEBUG') && WP_DEBUG) {
7513 - error_log(sprintf(
7514 - '[MxChat] openrouter_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
7515 - $attempt + 1, $max_attempts, $http_code, $errno,
7516 - $is_transient ? 'yes' : 'no',
7517 - $can_retry ? 'Retrying.' : 'Giving up.'
7518 - ));
7519 - }
7520 -
7521 - if (!$can_retry) {
7522 - break;
7523 - }
7524 - }
7525 -
7526 - if (!$errno && $http_code === 200) {
7527 - if (!empty($full_response) && !empty($session_id)) {
7528 - $rag_context_for_storage = null;
7529 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7530 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7531 -
7532 - if ($has_rag_data || $has_action_data) {
7533 - $rag_context_for_storage = [];
7534 -
7535 - if ($has_rag_data) {
7536 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7537 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7538 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7539 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7540 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7541 - }
7542 -
7543 - if ($has_action_data) {
7544 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7545 - }
7546 - }
7547 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7548 - }
7549 - return true;
7550 - }
7551 -
7552 - return $this->mxchat_stream_emit_fallback(
7553 - 'openai',
7554 - $this->mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content),
7555 - $session_id,
7556 - $testing_data
7557 - );
7558 -
7559 - } catch (Exception $e) {
7560 - return $this->mxchat_stream_emit_fallback(
7561 - 'openai',
7562 - $this->mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content),
7563 - $session_id,
7564 - $testing_data
7565 - );
7566 - }
7567 -}
7568 -private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7569 - try {
7570 - $bot_id = $this->get_current_bot_id($session_id);
4381 +
4382 + return strlen($data);
4383 + });
7571 4384
7572 - // Get system prompt instructions using centralized function
7573 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
4385 + $response = curl_exec($ch);
4386 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
7574 4387
7575 - // Ensure conversation_history is an array
7576 - if (!is_array($conversation_history)) {
7577 - $conversation_history = array();
7578 - }
7579 -
7580 - // Format conversation history for OpenAI
7581 - $formatted_conversation = array();
7582 -
7583 - $formatted_conversation[] = array(
7584 - 'role' => 'system',
7585 - 'content' => $system_prompt_instructions . " " . $relevant_content
7586 - );
7587 -
7588 - foreach ($conversation_history as $message) {
7589 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7590 - $role = $message['role'];
7591 - if ($role === 'bot' || $role === 'agent') {
7592 - $role = 'assistant';
7593 - }
7594 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
7595 - $role = 'user';
7596 - }
7597 - $formatted_conversation[] = array(
7598 - 'role' => $role,
7599 - 'content' => $message['content']
7600 - );
7601 - }
7602 - }
7603 -
7604 - // Check if we can actually stream
7605 - if (headers_sent() || !function_exists('curl_init')) {
7606 - // Fallback to regular response with testing data
4388 + if (curl_errno($ch) || $http_code !== 200) {
4389 + curl_close($ch);
4390 +
4391 + // Fallback to regular response
4392 + //error_log("MxChat: OpenAI streaming failed, falling back");
7607 4393 $regular_response = $this->mxchat_generate_response_openai(
7608 4394 $selected_model,
7609 4395 $api_key,
7610 4396 $conversation_history,
@@ -7610,13 +4396,8 @@
7610 4396 $conversation_history,
7611 4397 $relevant_content
7612 4398 );
7613 4399
7614 - // Save bot response to transcript
7615 - if (!empty($regular_response) && !empty($session_id)) {
7616 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7617 - }
7618 -
7619 4400 $response_data = [
7620 4401 'text' => $regular_response,
7621 4402 'html' => '',
7622 4403 'session_id' => $session_id
@@ -7623,8 +4404,9 @@
7623 4404 ];
7624 4405
7625 4406 if ($testing_data !== null) {
7626 4407 $response_data['testing_data'] = $testing_data;
4408 + //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
7627 4409 }
7628 4410
7629 4411 header('Content-Type: application/json');
7630 4412 echo json_encode($response_data);
@@ -7629,912 +4411,50 @@
7629 4411 header('Content-Type: application/json');
7630 4412 echo json_encode($response_data);
7631 4413 return true;
7632 4414 }
7633 -
7634 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
7635 - $is_gpt5_model = (
7636 - strpos($selected_model, 'gpt-5') === 0 ||
7637 - $selected_model === 'gpt-5.2' ||
7638 - $selected_model === 'gpt-5.1-2025-11-13' ||
7639 - $selected_model === 'gpt-5' ||
7640 - $selected_model === 'gpt-5-mini' ||
7641 - $selected_model === 'gpt-5-nano'
7642 - );
7643 -
7644 - // Build request body with optimal settings for fast streaming
7645 - $request_body = [
7646 - 'model' => $selected_model,
7647 - 'messages' => $formatted_conversation,
7648 - 'temperature' => 1,
7649 - 'stream' => true
7650 - ];
7651 -
7652 - // Add reasoning_effort only for GPT-5 models that support it
7653 - // These chat models don't support reasoning_effort parameter
7654 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
7655 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
7656 - // GPT-5.1 uses 'low' instead of 'minimal'
7657 - if ($selected_model === 'gpt-5.1-2025-11-13') {
7658 - $request_body['reasoning_effort'] = 'low';
7659 - } elseif ($selected_model === 'gpt-5.5') {
7660 - $request_body['reasoning_effort'] = 'none';
7661 - } elseif ($selected_model === 'gpt-5.4') {
7662 - $request_body['reasoning_effort'] = 'none';
7663 - } else {
7664 - $request_body['reasoning_effort'] = 'minimal';
7665 - }
4415 +
4416 + curl_close($ch);
4417 +
4418 + // Save the complete response to maintain chat persistence
4419 + if (!empty($full_response) && !empty($session_id)) {
4420 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
7666 4421 }
7667 -
7668 - $body = json_encode($request_body);
7669 -
7670 - // V2 retry-on-initial-connect: do NOT call setup_streaming_headers() here.
7671 - // It is now lazy-fired inside the WRITEFUNCTION on the first byte of a
7672 - // SUCCESSFUL upstream response, gated by the captured HTTP status.
7673 -
7674 - $captured_status_code = 0;
7675 - $captured_body_pre_stream = '';
7676 - $full_response = '';
7677 - $stream_started = false;
7678 - $buffer = '';
7679 - $errno = 0;
7680 - $last_curl_error = '';
7681 - $http_code = 0;
7682 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
7683 - $backoff_ms = array(0, 750, 2000);
7684 -
7685 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
7686 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
7687 - usleep($backoff_ms[$attempt] * 1000);
7688 - }
7689 -
7690 - // Reset per-attempt capture state.
7691 - $captured_status_code = 0;
7692 - $captured_body_pre_stream = '';
7693 - $full_response = '';
7694 - $stream_started = false;
7695 - $buffer = '';
7696 -
7697 - $ch = curl_init();
7698 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
7699 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
7700 - curl_setopt($ch, CURLOPT_POST, true);
7701 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
7702 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
7703 - 'Content-Type: application/json',
7704 - 'Authorization: Bearer ' . $api_key
7705 - ));
7706 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7707 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
7708 -
7709 - // Capture HTTP status as soon as response headers arrive — fires before WRITEFUNCTION.
7710 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
7711 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
7712 - $captured_status_code = (int) $m[1];
7713 - }
7714 - return strlen($header);
7715 - });
7716 -
7717 - // Buffer control for real-time streaming
7718 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
7719 - // V2 guard: if upstream returned non-200, buffer body for transient
7720 - // classification and DO NOT emit to client. Stream channel must NOT open.
7721 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
7722 - $captured_body_pre_stream .= $data;
7723 - return strlen($data);
7724 - }
7725 -
7726 - // Lazy-fire streaming headers on first byte of a SUCCESSFUL upstream.
7727 - // After this point streaming_headers_sent === true → retry is structurally blocked.
7728 - if (!$this->streaming_headers_sent) {
7729 - $this->setup_streaming_headers();
7730 - }
7731 -
7732 - // Send testing data as the first event if available
7733 - if (!$stream_started && $testing_data !== null) {
7734 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
7735 - flush();
7736 - $stream_started = true;
7737 - }
7738 -
7739 - // CRITICAL FIX: Append new data to buffer
7740 - $buffer .= $data;
7741 -
7742 - // Process complete lines only
7743 - $lines = explode("\n", $buffer);
7744 -
7745 - // CRITICAL FIX: Keep the last incomplete line in the buffer
7746 - $buffer = array_pop($lines);
7747 -
7748 - foreach ($lines as $line) {
7749 - if (trim($line) === '') {
7750 - continue;
7751 - }
7752 - if (strpos($line, 'data: ') !== 0) {
7753 - continue;
7754 - }
7755 -
7756 - $json_str = substr($line, 6);
7757 -
7758 - if (trim($json_str) === '[DONE]') {
7759 - echo "data: [DONE]\n\n";
7760 - flush();
7761 - continue;
7762 - }
7763 -
7764 - $json = json_decode(trim($json_str), true);
7765 - if ($json && isset($json['choices'][0]['delta']['content'])) {
7766 - $content = $json['choices'][0]['delta']['content'];
7767 - $full_response .= $content;
7768 -
7769 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
7770 - flush();
7771 - }
7772 - }
7773 -
7774 - return strlen($data);
7775 - });
7776 -
7777 - $response = curl_exec($ch);
7778 - $errno = curl_errno($ch);
7779 - $last_curl_error = curl_error($ch);
7780 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
7781 - curl_close($ch);
7782 -
7783 - if (!$errno && $http_code === 200) {
7784 - break; // Happy path — WRITEFUNCTION already streamed everything.
7785 - }
7786 -
7787 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
7788 - $can_retry = !$this->streaming_headers_sent
7789 - && ($attempt + 1) < $max_attempts
7790 - && $is_transient;
7791 -
7792 - if (defined('WP_DEBUG') && WP_DEBUG) {
7793 - error_log(sprintf(
7794 - '[MxChat] openai_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
7795 - $attempt + 1, $max_attempts, $http_code, $errno,
7796 - $is_transient ? 'yes' : 'no',
7797 - $can_retry ? 'Retrying.' : 'Giving up.'
7798 - ));
7799 - }
7800 -
7801 - if (!$can_retry) {
7802 - break;
7803 - }
7804 - }
7805 -
7806 - // Post-loop branch.
7807 - if (!$errno && $http_code === 200) {
7808 - // Happy path — save the complete response to maintain chat persistence.
7809 - if (!empty($full_response) && !empty($session_id)) {
7810 - $rag_context_for_storage = null;
7811 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
7812 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
7813 -
7814 - if ($has_rag_data || $has_action_data) {
7815 - $rag_context_for_storage = [];
7816 -
7817 - if ($has_rag_data) {
7818 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
7819 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
7820 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
7821 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
7822 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
7823 - }
7824 -
7825 - if ($has_action_data) {
7826 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
7827 - }
7828 - }
7829 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
7830 - }
7831 -
7832 - return true;
7833 - }
7834 -
7835 - // Failure path — branch on whether SSE channel was opened.
7836 - return $this->mxchat_stream_emit_fallback(
7837 - 'openai',
7838 - $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content),
7839 - $session_id,
7840 - $testing_data
7841 - );
7842 -
4422 +
4423 + return true; // Indicate streaming completed successfully
4424 +
7843 4425 } catch (Exception $e) {
7844 - return $this->mxchat_stream_emit_fallback(
7845 - 'openai',
7846 - $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content),
7847 - $session_id,
7848 - $testing_data
4426 + //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4427 +
4428 + // Fallback to regular response
4429 + $regular_response = $this->mxchat_generate_response_openai(
4430 + $selected_model,
4431 + $api_key,
4432 + $conversation_history,
4433 + $relevant_content
7849 4434 );
7850 - }
7851 -}
7852 -
7853 -/**
7854 - * Shared fallback emitter for streaming chat functions. Two outcomes:
7855 - * - streaming_headers_sent === true: SSE channel is open. Emit fallback content
7856 - * as `data: {...}\n\n` + `data: [DONE]\n\n` so the widget renders it as a
7857 - * normal bot bubble. Transcript row is persisted.
7858 - * - streaming_headers_sent === false: SSE channel never opened (retries
7859 - * exhausted on initial connect). Emit a clean JSON response — the path
7860 - * the widget would normally hit if streaming wasn't even attempted.
7861 - *
7862 - * Used by all six *_stream functions after their per-attempt retry loop.
7863 - */
7864 -private function mxchat_stream_emit_fallback($provider_hint, $regular_response, $session_id, $testing_data = null) {
7865 - $is_error_array = is_array($regular_response) && isset($regular_response['error']);
7866 -
7867 - if ($this->streaming_headers_sent) {
7868 - if ($is_error_array) {
7869 - echo "data: " . json_encode([
7870 - 'error' => true,
7871 - 'error_message' => $regular_response['error'],
7872 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7873 - 'text' => $regular_response['error'],
7874 - 'message' => $regular_response['error']
7875 - ]) . "\n\n";
7876 - echo "data: [DONE]\n\n";
7877 - flush();
7878 - return true;
4435 +
4436 + $response_data = [
4437 + 'text' => $regular_response,
4438 + 'html' => '',
4439 + 'session_id' => $session_id
4440 + ];
4441 +
4442 + if ($testing_data !== null) {
4443 + $response_data['testing_data'] = $testing_data;
4444 + //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
7879 4445 }
7880 - $fallback_message = (string) $regular_response;
7881 - if (!empty($fallback_message) && !empty($session_id)) {
7882 - $this->mxchat_save_chat_message($session_id, 'bot', $fallback_message);
7883 - }
7884 - echo "data: " . json_encode(['content' => $fallback_message]) . "\n\n";
7885 - echo "data: [DONE]\n\n";
7886 - flush();
7887 - return true;
7888 - }
7889 -
7890 - // SSE channel never opened — clean JSON fallback.
7891 - if ($is_error_array) {
4446 +
7892 4447 header('Content-Type: application/json');
7893 - echo json_encode(array(
7894 - 'error' => true,
7895 - 'error_message' => $regular_response['error'],
7896 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
7897 - 'text' => $regular_response['error'],
7898 - 'message' => $regular_response['error'],
7899 - ));
4448 + echo json_encode($response_data);
7900 4449 return true;
7901 4450 }
7902 -
7903 - $fallback_message = (string) $regular_response;
7904 - if (!empty($fallback_message) && !empty($session_id)) {
7905 - $this->mxchat_save_chat_message($session_id, 'bot', $fallback_message);
7906 - }
7907 - $response_data = array(
7908 - 'text' => $fallback_message,
7909 - 'html' => '',
7910 - 'session_id' => $session_id,
7911 - );
7912 - if ($testing_data !== null) {
7913 - $response_data['testing_data'] = $testing_data;
7914 - }
7915 - header('Content-Type: application/json');
7916 - echo json_encode($response_data);
7917 - return true;
7918 4451 }
7919 -
7920 -/**
7921 - * Resolve custom (OpenAI-compatible) provider config from settings.
7922 - * Returns ['base_url','api_key','model','auth_scheme','api_version','chat_url','headers'].
7923 - */
7924 -private function mxchat_resolve_custom_provider() {
7925 - $base_url = isset($this->options['custom_provider_base_url']) ? rtrim(trim((string) $this->options['custom_provider_base_url']), '/') : '';
7926 - $api_key = isset($this->options['custom_provider_api_key']) ? trim((string) $this->options['custom_provider_api_key']) : '';
7927 - $model = isset($this->options['custom_provider_model']) ? trim((string) $this->options['custom_provider_model']) : '';
7928 - $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? $this->options['custom_provider_auth_scheme'] : 'bearer';
7929 - $api_version = isset($this->options['custom_provider_api_version']) ? trim((string) $this->options['custom_provider_api_version']) : '';
7930 -
7931 - $chat_url = $base_url . '/chat/completions';
7932 - if (!empty($api_version)) {
7933 - $chat_url .= (strpos($chat_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($api_version);
7934 - }
7935 -
7936 - $headers = array('Content-Type: application/json');
7937 - if (!empty($api_key)) {
7938 - if ($auth_scheme === 'api-key') {
7939 - $headers[] = 'api-key: ' . $api_key;
7940 - } else {
7941 - $headers[] = 'Authorization: Bearer ' . $api_key;
7942 - }
7943 - }
7944 -
7945 - return array(
7946 - 'base_url' => $base_url,
7947 - 'api_key' => $api_key,
7948 - 'model' => $model !== '' ? $model : 'default',
7949 - 'auth_scheme' => $auth_scheme,
7950 - 'api_version' => $api_version,
7951 - 'chat_url' => $chat_url,
7952 - 'headers' => $headers,
7953 - );
7954 -}
7955 -
7956 -/**
7957 - * Streaming chat completion against an OpenAI-compatible custom provider
7958 - * (Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.).
7959 - * Mirrors mxchat_generate_response_openai_stream but with parameterized URL/auth/model.
7960 - */
7961 -private function mxchat_generate_response_custom_stream($selected_model, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4452 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7962 4453 try {
7963 - $cfg = $this->mxchat_resolve_custom_provider();
7964 - if (empty($cfg['base_url'])) {
7965 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
7966 - }
4454 + // Get system prompt instructions from options
4455 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7967 4456
7968 - $bot_id = $this->get_current_bot_id($session_id);
7969 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7970 - if (!is_array($conversation_history)) {
7971 - $conversation_history = array();
7972 - }
7973 -
7974 - $formatted_conversation = array();
7975 - $formatted_conversation[] = array(
7976 - 'role' => 'system',
7977 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
7978 - );
7979 - foreach ($conversation_history as $message) {
7980 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7981 - $role = $message['role'];
7982 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
7983 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
7984 - $formatted_conversation[] = array('role' => $role, 'content' => $message['content']);
7985 - }
7986 - }
7987 -
7988 - if (headers_sent() || !function_exists('curl_init')) {
7989 - // No streaming capability — fall through to non-stream wrapper
7990 - $regular = $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content);
7991 - if (!empty($regular) && !empty($session_id) && is_string($regular)) {
7992 - $this->mxchat_save_chat_message($session_id, 'bot', $regular);
7993 - }
7994 - $response_data = array('text' => is_string($regular) ? $regular : '', 'html' => '', 'session_id' => $session_id);
7995 - if ($testing_data !== null) { $response_data['testing_data'] = $testing_data; }
7996 - header('Content-Type: application/json');
7997 - echo json_encode($response_data);
7998 - return true;
7999 - }
8000 -
8001 - $request_body = array(
8002 - 'model' => $cfg['model'],
8003 - 'messages' => $formatted_conversation,
8004 - 'stream' => true,
8005 - );
8006 - $body = json_encode($request_body);
8007 -
8008 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
8009 -
8010 - $captured_status_code = 0;
8011 - $captured_body_pre_stream = '';
8012 - $full_response = '';
8013 - $stream_started = false;
8014 - $buffer = '';
8015 - $errno = 0;
8016 - $http_code = 0;
8017 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
8018 - $backoff_ms = array(0, 750, 2000);
8019 -
8020 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
8021 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
8022 - usleep($backoff_ms[$attempt] * 1000);
8023 - }
8024 -
8025 - $captured_status_code = 0;
8026 - $captured_body_pre_stream = '';
8027 - $full_response = '';
8028 - $stream_started = false;
8029 - $buffer = '';
8030 -
8031 - $ch = curl_init();
8032 - curl_setopt($ch, CURLOPT_URL, $cfg['chat_url']);
8033 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8034 - curl_setopt($ch, CURLOPT_POST, true);
8035 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
8036 - curl_setopt($ch, CURLOPT_HTTPHEADER, $cfg['headers']);
8037 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8038 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
8039 -
8040 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
8041 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
8042 - $captured_status_code = (int) $m[1];
8043 - }
8044 - return strlen($header);
8045 - });
8046 -
8047 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
8048 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
8049 - $captured_body_pre_stream .= $data;
8050 - return strlen($data);
8051 - }
8052 -
8053 - if (!$this->streaming_headers_sent) {
8054 - $this->setup_streaming_headers();
8055 - }
8056 -
8057 - if (!$stream_started && $testing_data !== null) {
8058 - echo "data: " . json_encode(array('testing_data' => $testing_data)) . "\n\n";
8059 - flush();
8060 - $stream_started = true;
8061 - }
8062 - $buffer .= $data;
8063 - $lines = explode("\n", $buffer);
8064 - $buffer = array_pop($lines);
8065 - foreach ($lines as $line) {
8066 - if (trim($line) === '') { continue; }
8067 - if (strpos($line, 'data: ') !== 0) { continue; }
8068 - $json_str = substr($line, 6);
8069 - if (trim($json_str) === '[DONE]') {
8070 - echo "data: [DONE]\n\n";
8071 - flush();
8072 - continue;
8073 - }
8074 - $json = json_decode(trim($json_str), true);
8075 - if ($json && isset($json['choices'][0]['delta']['content'])) {
8076 - $content = $json['choices'][0]['delta']['content'];
8077 - $full_response .= $content;
8078 - echo "data: " . json_encode(array('content' => $content)) . "\n\n";
8079 - flush();
8080 - }
8081 - }
8082 - return strlen($data);
8083 - });
8084 -
8085 - $response = curl_exec($ch);
8086 - $errno = curl_errno($ch);
8087 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
8088 - curl_close($ch);
8089 -
8090 - if (!$errno && $http_code === 200) {
8091 - break;
8092 - }
8093 -
8094 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
8095 - $can_retry = !$this->streaming_headers_sent
8096 - && ($attempt + 1) < $max_attempts
8097 - && $is_transient;
8098 -
8099 - if (defined('WP_DEBUG') && WP_DEBUG) {
8100 - error_log(sprintf(
8101 - '[MxChat] custom_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
8102 - $attempt + 1, $max_attempts, $http_code, $errno,
8103 - $is_transient ? 'yes' : 'no',
8104 - $can_retry ? 'Retrying.' : 'Giving up.'
8105 - ));
8106 - }
8107 -
8108 - if (!$can_retry) {
8109 - break;
8110 - }
8111 - }
8112 -
8113 - if (!$errno && $http_code === 200) {
8114 - if (!empty($full_response) && !empty($session_id)) {
8115 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8116 - }
8117 - return true;
8118 - }
8119 -
8120 - return $this->mxchat_stream_emit_fallback(
8121 - 'openai',
8122 - $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content),
8123 - $session_id,
8124 - $testing_data
8125 - );
8126 -
8127 - } catch (Exception $e) {
8128 - return array('error' => sprintf(esc_html__('Custom provider error: %s', 'mxchat'), $e->getMessage()), 'error_code' => 'custom_provider_exception');
8129 - }
8130 -}
8131 -
8132 -/**
8133 - * Non-streaming chat completion against a custom OpenAI-compatible provider.
8134 - * Returns string content on success, array['error'=>...] on failure.
8135 - */
8136 -private function mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content) {
8137 - $cfg = $this->mxchat_resolve_custom_provider();
8138 - if (empty($cfg['base_url'])) {
8139 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
8140 - }
8141 -
8142 - $bot_id = $this->get_current_bot_id(null);
8143 - $system_prompt_instructions = $this->get_system_instructions($bot_id, null);
8144 - if (!is_array($conversation_history)) {
8145 - $conversation_history = array();
8146 - }
8147 -
8148 - $messages = array(array(
8149 - 'role' => 'system',
8150 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
8151 - ));
8152 - foreach ($conversation_history as $message) {
8153 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8154 - $role = $message['role'];
8155 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
8156 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
8157 - $messages[] = array('role' => $role, 'content' => $message['content']);
8158 - }
8159 - }
8160 -
8161 - $headers_assoc = array('Content-Type' => 'application/json');
8162 - if (!empty($cfg['api_key'])) {
8163 - if ($cfg['auth_scheme'] === 'api-key') {
8164 - $headers_assoc['api-key'] = $cfg['api_key'];
8165 - } else {
8166 - $headers_assoc['Authorization'] = 'Bearer ' . $cfg['api_key'];
8167 - }
8168 - }
8169 -
8170 - $response = $this->mxchat_provider_call_with_retry($cfg['chat_url'], array(
8171 - 'headers' => $headers_assoc,
8172 - 'body' => wp_json_encode(array(
8173 - 'model' => $cfg['model'],
8174 - 'messages' => $messages,
8175 - )),
8176 - 'timeout' => 120,
8177 - ), 'openai');
8178 -
8179 - if (is_wp_error($response)) {
8180 - return array('error' => sprintf(esc_html__('Custom provider request failed: %s', 'mxchat'), $response->get_error_message()), 'error_code' => 'custom_provider_network_error');
8181 - }
8182 - $code = (int) wp_remote_retrieve_response_code($response);
8183 - if ($code < 200 || $code >= 300) {
8184 - return array('error' => sprintf(esc_html__('Custom provider returned HTTP %d.', 'mxchat'), $code), 'error_code' => 'custom_provider_http_error');
8185 - }
8186 - $body = json_decode(wp_remote_retrieve_body($response), true);
8187 - if (isset($body['choices'][0]['message']['content'])) {
8188 - return (string) $body['choices'][0]['message']['content'];
8189 - }
8190 - return array('error' => esc_html__('Custom provider returned an unexpected response shape.', 'mxchat'), 'error_code' => 'custom_provider_response_shape');
8191 -}
8192 -
8193 -/**
8194 - * Generate response using OpenAI Responses API with web search tool
8195 - * This uses the newer Responses API which supports web search functionality
8196 - */
8197 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
8198 - try {
8199 - $bot_id = $this->get_current_bot_id($session_id);
8200 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8201 -
8202 - if (!is_array($conversation_history)) {
8203 - $conversation_history = array();
8204 - }
8205 -
8206 - // Build the input for Responses API
8207 - // The Responses API uses a different format - we need to construct the input properly
8208 - $input_parts = [];
8209 -
8210 - // Add system instructions as context
8211 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
8212 -
8213 - // Build conversation as input items for Responses API
8214 - foreach ($conversation_history as $message) {
8215 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8216 - $role = $message['role'];
8217 - if ($role === 'bot' || $role === 'agent') {
8218 - $role = 'assistant';
8219 - }
8220 - if (!in_array($role, ['assistant', 'user'])) {
8221 - $role = 'user';
8222 - }
8223 - $input_parts[] = [
8224 - 'type' => 'message',
8225 - 'role' => $role,
8226 - 'content' => $message['content']
8227 - ];
8228 - }
8229 - }
8230 -
8231 - // Build request body for Responses API
8232 - $request_body = [
8233 - 'model' => $selected_model,
8234 - 'input' => $input_parts,
8235 - 'instructions' => $system_context,
8236 - 'stream' => $streaming
8237 - ];
8238 -
8239 - // Only add web search tool if web search is enabled in settings
8240 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
8241 - if ($web_search_enabled) {
8242 - $request_body['tools'] = [
8243 - ['type' => 'web_search']
8244 - ];
8245 - }
8246 -
8247 - // Add reasoning effort for supported models
8248 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
8249 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
8250 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
8251 - if ($selected_model === 'gpt-5.1-2025-11-13') {
8252 - $request_body['reasoning'] = ['effort' => 'low'];
8253 - } elseif ($selected_model === 'gpt-5.5') {
8254 - $request_body['reasoning'] = ['effort' => 'low'];
8255 - } elseif ($selected_model === 'gpt-5.4') {
8256 - $request_body['reasoning'] = ['effort' => 'low'];
8257 - }
8258 - }
8259 -
8260 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
8261 -
8262 - if ($streaming) {
8263 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
8264 - } else {
8265 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
8266 - }
8267 -
8268 - } catch (Exception $e) {
8269 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
8270 - return [
8271 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
8272 - 'error_code' => 'web_search_exception'
8273 - ];
8274 - }
8275 -}
8276 -
8277 -/**
8278 - * Handle non-streaming web search response
8279 - */
8280 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
8281 - $request_body['stream'] = false;
8282 -
8283 - $response = $this->mxchat_provider_call_with_retry('https://api.openai.com/v1/responses', array(
8284 - 'headers' => array(
8285 - 'Authorization' => 'Bearer ' . $api_key,
8286 - 'Content-Type' => 'application/json'
8287 - ),
8288 - 'body' => json_encode($request_body),
8289 - 'timeout' => 90
8290 - ), 'openai');
8291 -
8292 - if (is_wp_error($response)) {
8293 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
8294 - return [
8295 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
8296 - 'error_code' => 'web_search_connection_error'
8297 - ];
8298 - }
8299 -
8300 - $response_code = wp_remote_retrieve_response_code($response);
8301 - $response_body = wp_remote_retrieve_body($response);
8302 -
8303 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
8304 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
8305 -
8306 - if ($response_code !== 200) {
8307 - $error_data = json_decode($response_body, true);
8308 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
8309 - return [
8310 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
8311 - 'error_code' => 'web_search_api_error'
8312 - ];
8313 - }
8314 -
8315 - $result = json_decode($response_body, true);
8316 -
8317 - if (json_last_error() !== JSON_ERROR_NONE) {
8318 - return [
8319 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
8320 - 'error_code' => 'web_search_json_error'
8321 - ];
8322 - }
8323 -
8324 - // Extract the response text and citations from Responses API format
8325 - $output_text = '';
8326 - $citations = [];
8327 -
8328 - if (isset($result['output'])) {
8329 - foreach ($result['output'] as $output_item) {
8330 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
8331 - foreach ($output_item['content'] as $content_item) {
8332 - if ($content_item['type'] === 'output_text') {
8333 - $output_text .= $content_item['text'];
8334 -
8335 - // Extract citations/annotations
8336 - if (isset($content_item['annotations'])) {
8337 - foreach ($content_item['annotations'] as $annotation) {
8338 - if ($annotation['type'] === 'url_citation') {
8339 - $citations[] = [
8340 - 'url' => $annotation['url'],
8341 - 'title' => $annotation['title'] ?? ''
8342 - ];
8343 - }
8344 - }
8345 - }
8346 - }
8347 - }
8348 - }
8349 - }
8350 - }
8351 -
8352 - // If we have citations, append them to the response
8353 - if (!empty($citations)) {
8354 - $output_text .= "\n\n**Sources:**\n";
8355 - $seen_urls = [];
8356 - foreach ($citations as $citation) {
8357 - if (!in_array($citation['url'], $seen_urls)) {
8358 - $seen_urls[] = $citation['url'];
8359 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
8360 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
8361 - }
8362 - }
8363 - }
8364 -
8365 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
8366 - // which includes rag_context for the "sources" link in transcripts.
8367 -
8368 - return $output_text;
8369 -}
8370 -
8371 -/**
8372 - * Handle streaming web search response using Responses API
8373 - */
8374 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
8375 - $request_body['stream'] = true;
8376 -
8377 - // Check if we can stream
8378 - if (headers_sent() || !function_exists('curl_init')) {
8379 - // Fallback to non-streaming
8380 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
8381 - }
8382 -
8383 - // Setup streaming headers
8384 - $this->setup_streaming_headers();
8385 -
8386 - $ch = curl_init();
8387 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
8388 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8389 - curl_setopt($ch, CURLOPT_POST, true);
8390 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
8391 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
8392 - 'Content-Type: application/json',
8393 - 'Authorization: Bearer ' . $api_key
8394 - ));
8395 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8396 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
8397 -
8398 - $full_response = '';
8399 - $stream_started = false;
8400 - $buffer = '';
8401 - $citations = [];
8402 -
8403 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
8404 - // Send testing data as first event if available
8405 - if (!$stream_started && $testing_data !== null) {
8406 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8407 - flush();
8408 - $stream_started = true;
8409 - }
8410 -
8411 - $buffer .= $data;
8412 - $lines = explode("\n", $buffer);
8413 - $buffer = array_pop($lines);
8414 -
8415 - foreach ($lines as $line) {
8416 - if (trim($line) === '') continue;
8417 - if (strpos($line, 'data: ') !== 0) continue;
8418 -
8419 - $json_str = substr($line, 6);
8420 -
8421 - if (trim($json_str) === '[DONE]') {
8422 - // Append citations if we have any
8423 - if (!empty($citations)) {
8424 - $citation_text = "\n\n**Sources:**\n";
8425 - $seen_urls = [];
8426 - foreach ($citations as $citation) {
8427 - if (!in_array($citation['url'], $seen_urls)) {
8428 - $seen_urls[] = $citation['url'];
8429 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
8430 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
8431 - }
8432 - }
8433 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
8434 - $full_response .= $citation_text;
8435 - flush();
8436 - }
8437 - echo "data: [DONE]\n\n";
8438 - flush();
8439 - continue;
8440 - }
8441 -
8442 - $json = json_decode(trim($json_str), true);
8443 - if (!$json) continue;
8444 -
8445 - // Handle Responses API streaming events
8446 - // The format is different from Chat Completions
8447 - if (isset($json['type'])) {
8448 - switch ($json['type']) {
8449 - case 'response.output_text.delta':
8450 - // Text content delta
8451 - if (isset($json['delta'])) {
8452 - $content = $json['delta'];
8453 - $full_response .= $content;
8454 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
8455 - flush();
8456 - }
8457 - break;
8458 -
8459 - case 'response.output_item.done':
8460 - // Check for citations in completed items
8461 - if (isset($json['item']['content'])) {
8462 - foreach ($json['item']['content'] as $content_item) {
8463 - if (isset($content_item['annotations'])) {
8464 - foreach ($content_item['annotations'] as $annotation) {
8465 - if ($annotation['type'] === 'url_citation') {
8466 - $citations[] = [
8467 - 'url' => $annotation['url'],
8468 - 'title' => $annotation['title'] ?? ''
8469 - ];
8470 - }
8471 - }
8472 - }
8473 - }
8474 - }
8475 - break;
8476 - }
8477 - }
8478 - }
8479 -
8480 - return strlen($data);
8481 - });
8482 -
8483 - $response = curl_exec($ch);
8484 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
8485 -
8486 - if (curl_errno($ch) || $http_code !== 200) {
8487 - $curl_error = curl_error($ch);
8488 - curl_close($ch);
8489 -
8490 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
8491 -
8492 - return $this->mxchat_stream_emit_fallback(
8493 - 'web_search',
8494 - $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data),
8495 - $session_id,
8496 - $testing_data
8497 - );
8498 - }
8499 -
8500 - curl_close($ch);
8501 -
8502 - // Save the complete response with RAG context so the "sources" link
8503 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
8504 - if (!empty($full_response) && !empty($session_id)) {
8505 - $rag_context_for_storage = null;
8506 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8507 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8508 -
8509 - if ($has_rag_data || $has_action_data) {
8510 - $rag_context_for_storage = [];
8511 -
8512 - if ($has_rag_data) {
8513 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8514 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8515 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8516 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8517 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8518 - }
8519 -
8520 - if ($has_action_data) {
8521 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8522 - }
8523 - }
8524 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
8525 - }
8526 -
8527 - return true;
8528 -}
8529 -
8530 -private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8531 - try {
8532 - // Get bot ID from session or request
8533 - $bot_id = $this->get_current_bot_id($session_id);
8534 -
8535 - // Get system prompt instructions using centralized function
8536 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8537 4457 // Ensure conversation_history is an array
8538 4458 if (!is_array($conversation_history)) {
8539 4459 $conversation_history = array();
8540 4460 }
@@ -8566,9 +4486,9 @@
8566 4486 'content' => $relevant_content
8567 4487 ];
8568 4488
8569 4489 // Prepare the request body with stream: true
8570 - $payload = [
4490 + $body = json_encode([
8571 4491 'model' => $selected_model,
8572 4492 'messages' => $conversation_history,
8573 4493 'max_tokens' => 1000,
8574 4494 'temperature' => 0.8,
@@ -8573,11 +4493,9 @@
8573 4493 'max_tokens' => 1000,
8574 4494 'temperature' => 0.8,
8575 4495 'system' => $system_prompt_instructions,
8576 4496 'stream' => true
8577 - ];
8578 - if ($this->mxchat_claude_omits_temperature($selected_model)) { unset($payload['temperature']); }
8579 - $body = json_encode($payload);
4497 + ]);
8580 4498
8581 4499 // Check if we can actually stream (headers not sent, etc.)
8582 4500 if (headers_sent() || !function_exists('curl_init')) {
8583 4501 // Fallback to regular response with testing data
@@ -8588,13 +4506,8 @@
8588 4506 array_slice($conversation_history, 0, -1), // Remove the added content
8589 4507 $relevant_content
8590 4508 );
8591 4509
8592 - // Save bot response to transcript
8593 - if (!empty($regular_response) && !empty($session_id)) {
8594 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8595 - }
8596 -
8597 4510 // Return as JSON with testing data
8598 4511 $response_data = [
8599 4512 'text' => $regular_response,
8600 4513 'html' => '',
@@ -8613,197 +4526,162 @@
8613 4526 echo json_encode($response_data);
8614 4527 return true; // Indicate we handled the response
8615 4528 }
8616 4529
8617 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
4530 + // Use cURL for streaming support
4531 + $ch = curl_init();
4532 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
4533 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4534 + curl_setopt($ch, CURLOPT_POST, true);
4535 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4536 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4537 + 'Content-Type: application/json',
4538 + 'x-api-key: ' . $claude_api_key,
4539 + 'anthropic-version: 2023-06-01'
4540 + ));
4541 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4542 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8618 4543
8619 - $captured_status_code = 0;
8620 - $captured_body_pre_stream = '';
8621 - $full_response = '';
4544 + $full_response = ''; // Accumulate full response for saving
8622 4545 $stream_started = false;
8623 - $buffer = '';
8624 - $errno = 0;
8625 - $http_code = 0;
8626 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
8627 - $backoff_ms = array(0, 750, 2000);
8628 4546
8629 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
8630 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
8631 - usleep($backoff_ms[$attempt] * 1000);
4547 + // Buffer control for real-time streaming
4548 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4549 + // Send testing data as the first event if available
4550 + if (!$stream_started && $testing_data !== null) {
4551 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4552 + flush();
4553 + $stream_started = true;
4554 + //error_log("MxChat Testing: Sent testing data in Claude stream");
8632 4555 }
4556 +
4557 + // Process each chunk of data
4558 + $lines = explode("\n", $data);
8633 4559
8634 - $captured_status_code = 0;
8635 - $captured_body_pre_stream = '';
8636 - $full_response = '';
8637 - $stream_started = false;
8638 - $buffer = '';
8639 -
8640 - $ch = curl_init();
8641 - curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
8642 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8643 - curl_setopt($ch, CURLOPT_POST, true);
8644 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
8645 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
8646 - 'Content-Type: application/json',
8647 - 'x-api-key: ' . $claude_api_key,
8648 - 'anthropic-version: 2023-06-01'
8649 - ));
8650 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8651 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8652 -
8653 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
8654 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
8655 - $captured_status_code = (int) $m[1];
4560 + foreach ($lines as $line) {
4561 + if (trim($line) === '') {
4562 + continue;
8656 4563 }
8657 - return strlen($header);
8658 - });
8659 4564
8660 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
8661 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
8662 - $captured_body_pre_stream .= $data;
8663 - return strlen($data);
4565 + // Claude uses event: and data: format
4566 + if (strpos($line, 'event: ') === 0) {
4567 + // Store the event type for the next data line
4568 + continue;
8664 4569 }
8665 4570
8666 - if (!$this->streaming_headers_sent) {
8667 - $this->setup_streaming_headers();
8668 - }
4571 + if (strpos($line, 'data: ') === 0) {
4572 + $json_str = substr($line, 6); // Remove 'data: ' prefix
8669 4573
8670 - if (!$stream_started && $testing_data !== null) {
8671 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8672 - flush();
8673 - $stream_started = true;
8674 - }
8675 -
8676 - $buffer .= $data;
8677 - $lines = explode("\n", $buffer);
8678 - $buffer = array_pop($lines);
8679 -
8680 - foreach ($lines as $line) {
8681 - if (trim($line) === '') {
4574 + $json = json_decode($json_str, true);
4575 + if (json_last_error() !== JSON_ERROR_NONE) {
8682 4576 continue;
8683 4577 }
8684 4578
8685 - if (strpos($line, 'event: ') === 0) {
8686 - continue;
8687 - }
4579 + // Handle different event types
4580 + if (isset($json['type'])) {
4581 + switch ($json['type']) {
4582 + case 'content_block_delta':
4583 + if (isset($json['delta']['text'])) {
4584 + $content = $json['delta']['text'];
4585 + $full_response .= $content; // Accumulate
4586 + // Send as SSE format compatible with your frontend
4587 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4588 + flush();
4589 + }
4590 + break;
8688 4591
8689 - if (strpos($line, 'data: ') === 0) {
8690 - $json_str = substr($line, 6);
4592 + case 'message_stop':
4593 + echo "data: [DONE]\n\n";
4594 + flush();
4595 + break;
8691 4596
8692 - $json = json_decode(trim($json_str), true);
8693 - if (json_last_error() !== JSON_ERROR_NONE) {
8694 - continue;
4597 + case 'error':
4598 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
4599 + flush();
4600 + break;
8695 4601 }
8696 -
8697 - if (isset($json['type'])) {
8698 - switch ($json['type']) {
8699 - case 'content_block_delta':
8700 - if (isset($json['delta']['text'])) {
8701 - $content = $json['delta']['text'];
8702 - $full_response .= $content;
8703 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
8704 - flush();
8705 - }
8706 - break;
8707 -
8708 - case 'message_stop':
8709 - echo "data: [DONE]\n\n";
8710 - flush();
8711 - break;
8712 -
8713 - case 'error':
8714 - echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
8715 - flush();
8716 - break;
8717 - }
8718 - }
8719 4602 }
8720 4603 }
8721 -
8722 - return strlen($data);
8723 - });
8724 -
8725 - $response = curl_exec($ch);
8726 - $errno = curl_errno($ch);
8727 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
8728 - curl_close($ch);
8729 -
8730 - if (!$errno && $http_code === 200) {
8731 - break;
8732 4604 }
8733 4605
8734 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'anthropic', $errno);
8735 - $can_retry = !$this->streaming_headers_sent
8736 - && ($attempt + 1) < $max_attempts
8737 - && $is_transient;
4606 + return strlen($data);
4607 + });
8738 4608
8739 - if (defined('WP_DEBUG') && WP_DEBUG) {
8740 - error_log(sprintf(
8741 - '[MxChat] claude_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
8742 - $attempt + 1, $max_attempts, $http_code, $errno,
8743 - $is_transient ? 'yes' : 'no',
8744 - $can_retry ? 'Retrying.' : 'Giving up.'
8745 - ));
8746 - }
4609 + $response = curl_exec($ch);
4610 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
8747 4611
8748 - if (!$can_retry) {
8749 - break;
8750 - }
4612 + if (curl_errno($ch)) {
4613 + curl_close($ch);
4614 + throw new Exception('cURL Error: ' . curl_error($ch));
8751 4615 }
8752 4616
8753 - if ($errno || $http_code !== 200) {
8754 - return $this->mxchat_stream_emit_fallback(
8755 - 'anthropic',
8756 - $this->mxchat_generate_response_claude($selected_model, $claude_api_key, array_slice($conversation_history, 0, -1), $relevant_content),
8757 - $session_id,
8758 - $testing_data
4617 + curl_close($ch);
4618 +
4619 + if ($http_code !== 200) {
4620 + // Fallback to regular response
4621 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
4622 + $regular_response = $this->mxchat_generate_response_claude(
4623 + $selected_model,
4624 + $claude_api_key,
4625 + array_slice($conversation_history, 0, -1), // Remove the added content
4626 + $relevant_content
8759 4627 );
4628 +
4629 + $response_data = [
4630 + 'text' => $regular_response,
4631 + 'html' => '',
4632 + 'session_id' => $session_id
4633 + ];
4634 +
4635 + if ($testing_data !== null) {
4636 + $response_data['testing_data'] = $testing_data;
4637 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
4638 + }
4639 +
4640 + header('Content-Type: application/json');
4641 + echo json_encode($response_data);
4642 + return true;
8760 4643 }
8761 4644
8762 4645 // Save the complete response to maintain chat persistence
8763 4646 if (!empty($full_response) && !empty($session_id)) {
8764 - // Prepare RAG context for streaming response
8765 - $rag_context_for_storage = null;
8766 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8767 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8768 -
8769 - if ($has_rag_data || $has_action_data) {
8770 - $rag_context_for_storage = [];
8771 -
8772 - if ($has_rag_data) {
8773 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8774 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8775 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8776 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8777 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8778 - }
8779 -
8780 - if ($has_action_data) {
8781 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8782 - }
8783 - }
8784 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4647 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8785 4648 }
8786 4649
8787 4650 return true; // Indicate streaming completed successfully
8788 4651
8789 4652 } catch (Exception $e) {
8790 - return $this->mxchat_stream_emit_fallback(
8791 - 'anthropic',
8792 - $this->mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content),
8793 - $session_id,
8794 - $testing_data
4653 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
4654 +
4655 + // Fallback to regular response on exception
4656 + $regular_response = $this->mxchat_generate_response_claude(
4657 + $selected_model,
4658 + $claude_api_key,
4659 + $conversation_history,
4660 + $relevant_content
8795 4661 );
4662 +
4663 + $response_data = [
4664 + 'text' => $regular_response,
4665 + 'html' => '',
4666 + 'session_id' => $session_id
4667 + ];
4668 +
4669 + if ($testing_data !== null) {
4670 + $response_data['testing_data'] = $testing_data;
4671 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
4672 + }
4673 +
4674 + header('Content-Type: application/json');
4675 + echo json_encode($response_data);
4676 + return true;
8796 4677 }
8797 4678 }
8798 4679 private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8799 4680 try {
8800 - // Get bot ID from session or request
8801 - $bot_id = $this->get_current_bot_id($session_id);
4681 + // Get system prompt instructions from options
4682 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
8802 4683
8803 - // Get system prompt instructions using centralized function
8804 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8805 -
8806 4684 // Ensure conversation_history is an array
8807 4685 if (!is_array($conversation_history)) {
8808 4686 $conversation_history = array();
8809 4687 }
@@ -8842,13 +4720,8 @@
8842 4720 $conversation_history,
8843 4721 $relevant_content
8844 4722 );
8845 4723
8846 - // Save bot response to transcript
8847 - if (!empty($regular_response) && !empty($session_id)) {
8848 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8849 - }
8850 -
8851 4724 $response_data = [
8852 4725 'text' => $regular_response,
8853 4726 'html' => '',
8854 4727 'session_id' => $session_id
@@ -8871,179 +4744,135 @@
8871 4744 'temperature' => 0.8,
8872 4745 'stream' => true
8873 4746 ]);
8874 4747
8875 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
8876 -
8877 - $captured_status_code = 0;
8878 - $captured_body_pre_stream = '';
8879 - $full_response = '';
4748 + // Use cURL for streaming support
4749 + $ch = curl_init();
4750 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
4751 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4752 + curl_setopt($ch, CURLOPT_POST, true);
4753 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4754 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4755 + 'Content-Type: application/json',
4756 + 'Authorization: Bearer ' . $xai_api_key
4757 + ));
4758 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4759 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4760 +
4761 + $full_response = ''; // Accumulate full response for saving
8880 4762 $stream_started = false;
8881 - $buffer = '';
8882 - $errno = 0;
8883 - $http_code = 0;
8884 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
8885 - $backoff_ms = array(0, 750, 2000);
8886 -
8887 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
8888 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
8889 - usleep($backoff_ms[$attempt] * 1000);
4763 +
4764 + // Buffer control for real-time streaming
4765 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4766 + // Send testing data as the first event if available
4767 + if (!$stream_started && $testing_data !== null) {
4768 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4769 + flush();
4770 + $stream_started = true;
4771 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
8890 4772 }
8891 -
8892 - $captured_status_code = 0;
8893 - $captured_body_pre_stream = '';
8894 - $full_response = '';
8895 - $stream_started = false;
8896 - $buffer = '';
8897 -
8898 - $ch = curl_init();
8899 - curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
8900 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8901 - curl_setopt($ch, CURLOPT_POST, true);
8902 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
8903 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
8904 - 'Content-Type: application/json',
8905 - 'Authorization: Bearer ' . $xai_api_key
8906 - ));
8907 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8908 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8909 -
8910 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
8911 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
8912 - $captured_status_code = (int) $m[1];
4773 +
4774 + // Process each chunk of data
4775 + $lines = explode("\n", $data);
4776 +
4777 + foreach ($lines as $line) {
4778 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4779 + continue;
8913 4780 }
8914 - return strlen($header);
8915 - });
8916 -
8917 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
8918 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
8919 - $captured_body_pre_stream .= $data;
8920 - return strlen($data);
4781 +
4782 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4783 +
4784 + if ($json_str === '[DONE]') {
4785 + echo "data: [DONE]\n\n";
4786 + flush();
4787 + continue;
8921 4788 }
8922 -
8923 - if (!$this->streaming_headers_sent) {
8924 - $this->setup_streaming_headers();
8925 - }
8926 -
8927 - if (!$stream_started && $testing_data !== null) {
8928 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4789 +
4790 + $json = json_decode($json_str, true);
4791 + if (isset($json['choices'][0]['delta']['content'])) {
4792 + $content = $json['choices'][0]['delta']['content'];
4793 + $full_response .= $content; // Accumulate
4794 + // Send as SSE format
4795 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
8929 4796 flush();
8930 - $stream_started = true;
8931 4797 }
8932 -
8933 - $buffer .= $data;
8934 - $lines = explode("\n", $buffer);
8935 - $buffer = array_pop($lines);
8936 -
8937 - foreach ($lines as $line) {
8938 - if (trim($line) === '') {
8939 - continue;
8940 - }
8941 - if (strpos($line, 'data: ') !== 0) {
8942 - continue;
8943 - }
8944 -
8945 - $json_str = substr($line, 6);
8946 -
8947 - if (trim($json_str) === '[DONE]') {
8948 - echo "data: [DONE]\n\n";
8949 - flush();
8950 - continue;
8951 - }
8952 -
8953 - $json = json_decode(trim($json_str), true);
8954 - if ($json && isset($json['choices'][0]['delta']['content'])) {
8955 - $content = $json['choices'][0]['delta']['content'];
8956 - $full_response .= $content;
8957 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
8958 - flush();
8959 - }
8960 - }
8961 -
8962 - return strlen($data);
8963 - });
8964 -
8965 - $response = curl_exec($ch);
8966 - $errno = curl_errno($ch);
8967 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
4798 + }
4799 +
4800 + return strlen($data);
4801 + });
4802 +
4803 + $response = curl_exec($ch);
4804 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4805 +
4806 + if (curl_errno($ch) || $http_code !== 200) {
8968 4807 curl_close($ch);
8969 -
8970 - if (!$errno && $http_code === 200) {
8971 - break;
4808 +
4809 + // Fallback to regular response
4810 + //error_log("MxChat: X.AI streaming failed, falling back");
4811 + $regular_response = $this->mxchat_generate_response_xai(
4812 + $selected_model,
4813 + $xai_api_key,
4814 + $conversation_history,
4815 + $relevant_content
4816 + );
4817 +
4818 + $response_data = [
4819 + 'text' => $regular_response,
4820 + 'html' => '',
4821 + 'session_id' => $session_id
4822 + ];
4823 +
4824 + if ($testing_data !== null) {
4825 + $response_data['testing_data'] = $testing_data;
4826 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
8972 4827 }
8973 -
8974 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'xai', $errno);
8975 - $can_retry = !$this->streaming_headers_sent
8976 - && ($attempt + 1) < $max_attempts
8977 - && $is_transient;
8978 -
8979 - if (defined('WP_DEBUG') && WP_DEBUG) {
8980 - error_log(sprintf(
8981 - '[MxChat] xai_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
8982 - $attempt + 1, $max_attempts, $http_code, $errno,
8983 - $is_transient ? 'yes' : 'no',
8984 - $can_retry ? 'Retrying.' : 'Giving up.'
8985 - ));
8986 - }
8987 -
8988 - if (!$can_retry) {
8989 - break;
8990 - }
4828 +
4829 + header('Content-Type: application/json');
4830 + echo json_encode($response_data);
4831 + return true;
8991 4832 }
8992 -
8993 - if ($errno || $http_code !== 200) {
8994 - return $this->mxchat_stream_emit_fallback(
8995 - 'xai',
8996 - $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content),
8997 - $session_id,
8998 - $testing_data
8999 - );
9000 - }
9001 -
4833 +
4834 + curl_close($ch);
4835 +
9002 4836 // Save the complete response to maintain chat persistence
9003 4837 if (!empty($full_response) && !empty($session_id)) {
9004 - // Prepare RAG context for streaming response
9005 - $rag_context_for_storage = null;
9006 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
9007 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
9008 -
9009 - if ($has_rag_data || $has_action_data) {
9010 - $rag_context_for_storage = [];
9011 -
9012 - if ($has_rag_data) {
9013 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
9014 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
9015 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
9016 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
9017 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
9018 - }
9019 -
9020 - if ($has_action_data) {
9021 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
9022 - }
9023 - }
9024 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4838 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
9025 4839 }
9026 -
4840 +
9027 4841 return true; // Indicate streaming completed successfully
9028 -
4842 +
9029 4843 } catch (Exception $e) {
9030 - return $this->mxchat_stream_emit_fallback(
9031 - 'xai',
9032 - $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content),
9033 - $session_id,
9034 - $testing_data
4844 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
4845 +
4846 + // Fallback to regular response
4847 + $regular_response = $this->mxchat_generate_response_xai(
4848 + $selected_model,
4849 + $xai_api_key,
4850 + $conversation_history,
4851 + $relevant_content
9035 4852 );
4853 +
4854 + $response_data = [
4855 + 'text' => $regular_response,
4856 + 'html' => '',
4857 + 'session_id' => $session_id
4858 + ];
4859 +
4860 + if ($testing_data !== null) {
4861 + $response_data['testing_data'] = $testing_data;
4862 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
4863 + }
4864 +
4865 + header('Content-Type: application/json');
4866 + echo json_encode($response_data);
4867 + return true;
9036 4868 }
9037 4869 }
9038 4870 private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
9039 4871 try {
9040 - // Get bot ID from session or request
9041 - $bot_id = $this->get_current_bot_id($session_id);
4872 + // Get system prompt instructions from options
4873 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
9042 4874
9043 - // Get system prompt instructions using centralized function
9044 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9045 -
9046 4875 // Ensure conversation_history is an array
9047 4876 if (!is_array($conversation_history)) {
9048 4877 $conversation_history = array();
9049 4878 }
@@ -9082,13 +4911,8 @@
9082 4911 $conversation_history,
9083 4912 $relevant_content
9084 4913 );
9085 4914
9086 - // Save bot response to transcript
9087 - if (!empty($regular_response) && !empty($session_id)) {
9088 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
9089 - }
9090 -
9091 4915 $response_data = [
9092 4916 'text' => $regular_response,
9093 4917 'html' => '',
9094 4918 'session_id' => $session_id
@@ -9111,284 +4935,158 @@
9111 4935 'temperature' => 0.8,
9112 4936 'stream' => true
9113 4937 ]);
9114 4938
9115 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
9116 -
9117 - $captured_status_code = 0;
9118 - $captured_body_pre_stream = '';
9119 - $full_response = '';
4939 + // Use cURL for streaming support
4940 + $ch = curl_init();
4941 + curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
4942 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4943 + curl_setopt($ch, CURLOPT_POST, true);
4944 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4945 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4946 + 'Content-Type: application/json',
4947 + 'Authorization: Bearer ' . $deepseek_api_key
4948 + ));
4949 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4950 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4951 +
4952 + $full_response = ''; // Accumulate full response for saving
9120 4953 $stream_started = false;
9121 - $buffer = '';
9122 - $errno = 0;
9123 - $http_code = 0;
9124 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
9125 - $backoff_ms = array(0, 750, 2000);
9126 -
9127 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
9128 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
9129 - usleep($backoff_ms[$attempt] * 1000);
4954 +
4955 + // Buffer control for real-time streaming
4956 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4957 + // Send testing data as the first event if available
4958 + if (!$stream_started && $testing_data !== null) {
4959 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4960 + flush();
4961 + $stream_started = true;
4962 + //error_log("MxChat Testing: Sent testing data in DeepSeek stream");
9130 4963 }
9131 -
9132 - $captured_status_code = 0;
9133 - $captured_body_pre_stream = '';
9134 - $full_response = '';
9135 - $stream_started = false;
9136 - $buffer = '';
9137 -
9138 - $ch = curl_init();
9139 - curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
9140 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
9141 - curl_setopt($ch, CURLOPT_POST, true);
9142 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
9143 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
9144 - 'Content-Type: application/json',
9145 - 'Authorization: Bearer ' . $deepseek_api_key
9146 - ));
9147 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
9148 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
9149 -
9150 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
9151 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
9152 - $captured_status_code = (int) $m[1];
4964 +
4965 + // Process each chunk of data
4966 + $lines = explode("\n", $data);
4967 +
4968 + foreach ($lines as $line) {
4969 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4970 + continue;
9153 4971 }
9154 - return strlen($header);
9155 - });
9156 -
9157 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
9158 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
9159 - $captured_body_pre_stream .= $data;
9160 - return strlen($data);
4972 +
4973 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4974 +
4975 + if ($json_str === '[DONE]') {
4976 + echo "data: [DONE]\n\n";
4977 + flush();
4978 + continue;
9161 4979 }
9162 -
9163 - if (!$this->streaming_headers_sent) {
9164 - $this->setup_streaming_headers();
9165 - }
9166 -
9167 - if (!$stream_started && $testing_data !== null) {
9168 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4980 +
4981 + $json = json_decode($json_str, true);
4982 + if (isset($json['choices'][0]['delta']['content'])) {
4983 + $content = $json['choices'][0]['delta']['content'];
4984 + $full_response .= $content; // Accumulate
4985 + // Send as SSE format
4986 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
9169 4987 flush();
9170 - $stream_started = true;
9171 4988 }
9172 -
9173 - $buffer .= $data;
9174 - $lines = explode("\n", $buffer);
9175 - $buffer = array_pop($lines);
9176 -
9177 - foreach ($lines as $line) {
9178 - if (trim($line) === '') {
9179 - continue;
9180 - }
9181 - if (strpos($line, 'data: ') !== 0) {
9182 - continue;
9183 - }
9184 -
9185 - $json_str = substr($line, 6);
9186 -
9187 - if (trim($json_str) === '[DONE]') {
9188 - echo "data: [DONE]\n\n";
9189 - flush();
9190 - continue;
9191 - }
9192 -
9193 - $json = json_decode(trim($json_str), true);
9194 - if ($json && isset($json['choices'][0]['delta']['content'])) {
9195 - $content = $json['choices'][0]['delta']['content'];
9196 - $full_response .= $content;
9197 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
9198 - flush();
9199 - }
4989 + }
4990 +
4991 + return strlen($data);
4992 + });
4993 +
4994 + $response = curl_exec($ch);
4995 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4996 +
4997 + if (curl_errno($ch) || $http_code !== 200) {
4998 + $curl_error = curl_error($ch);
4999 + curl_close($ch);
5000 +
5001 + // Log the specific error for debugging
5002 + //error_log("MxChat: DeepSeek streaming failed - HTTP: $http_code, cURL: $curl_error");
5003 +
5004 + // Fallback to regular response
5005 + $regular_response = $this->mxchat_generate_response_deepseek(
5006 + $selected_model,
5007 + $deepseek_api_key,
5008 + $conversation_history,
5009 + $relevant_content
5010 + );
5011 +
5012 + // Handle error response from regular function
5013 + if (is_array($regular_response) && isset($regular_response['error'])) {
5014 + if ($testing_data !== null) {
5015 + $regular_response['testing_data'] = $testing_data;
9200 5016 }
9201 -
9202 - return strlen($data);
9203 - });
9204 -
9205 - $response = curl_exec($ch);
9206 - $errno = curl_errno($ch);
9207 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
9208 - curl_close($ch);
9209 -
9210 - if (!$errno && $http_code === 200) {
9211 - break;
5017 + header('Content-Type: application/json');
5018 + echo json_encode($regular_response);
5019 + return true;
9212 5020 }
9213 -
9214 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
9215 - $can_retry = !$this->streaming_headers_sent
9216 - && ($attempt + 1) < $max_attempts
9217 - && $is_transient;
9218 -
9219 - if (defined('WP_DEBUG') && WP_DEBUG) {
9220 - error_log(sprintf(
9221 - '[MxChat] deepseek_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
9222 - $attempt + 1, $max_attempts, $http_code, $errno,
9223 - $is_transient ? 'yes' : 'no',
9224 - $can_retry ? 'Retrying.' : 'Giving up.'
9225 - ));
5021 +
5022 + $response_data = [
5023 + 'text' => $regular_response,
5024 + 'html' => '',
5025 + 'session_id' => $session_id
5026 + ];
5027 +
5028 + if ($testing_data !== null) {
5029 + $response_data['testing_data'] = $testing_data;
5030 + //error_log("MxChat Testing: Added testing data to DeepSeek error fallback");
9226 5031 }
9227 -
9228 - if (!$can_retry) {
9229 - break;
9230 - }
5032 +
5033 + header('Content-Type: application/json');
5034 + echo json_encode($response_data);
5035 + return true;
9231 5036 }
9232 -
9233 - if ($errno || $http_code !== 200) {
9234 - return $this->mxchat_stream_emit_fallback(
9235 - 'openai',
9236 - $this->mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content),
9237 - $session_id,
9238 - $testing_data
9239 - );
9240 - }
9241 -
5037 +
5038 + curl_close($ch);
5039 +
9242 5040 // Save the complete response to maintain chat persistence
9243 5041 if (!empty($full_response) && !empty($session_id)) {
9244 - // Prepare RAG context for streaming response
9245 - $rag_context_for_storage = null;
9246 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
9247 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
9248 -
9249 - if ($has_rag_data || $has_action_data) {
9250 - $rag_context_for_storage = [];
9251 -
9252 - if ($has_rag_data) {
9253 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
9254 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
9255 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
9256 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
9257 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
9258 - }
9259 -
9260 - if ($has_action_data) {
9261 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
9262 - }
9263 - }
9264 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
5042 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
9265 5043 }
9266 -
5044 +
9267 5045 return true; // Indicate streaming completed successfully
9268 -
5046 +
9269 5047 } catch (Exception $e) {
9270 - return $this->mxchat_stream_emit_fallback(
9271 - 'openai',
9272 - $this->mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content),
9273 - $session_id,
9274 - $testing_data
5048 + //error_log("MxChat DeepSeek streaming exception: " . $e->getMessage());
5049 +
5050 + // Fallback to regular response
5051 + $regular_response = $this->mxchat_generate_response_deepseek(
5052 + $selected_model,
5053 + $deepseek_api_key,
5054 + $conversation_history,
5055 + $relevant_content
9275 5056 );
9276 - }
9277 -}
9278 -
9279 -
9280 -private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content) {
9281 - try {
9282 - if (!is_array($conversation_history)) {
9283 - $conversation_history = array();
9284 - }
9285 -
9286 - $bot_id = $this->get_current_bot_id('');
9287 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9288 5057
9289 - $formatted_conversation = array();
9290 -
9291 - $formatted_conversation[] = array(
9292 - 'role' => 'system',
9293 - 'content' => $system_prompt_instructions . " " . $relevant_content
9294 - );
9295 -
9296 - foreach ($conversation_history as $message) {
9297 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
9298 - $role = $message['role'];
9299 -
9300 - if ($role === 'bot' || $role === 'agent') {
9301 - $role = 'assistant';
9302 - }
9303 - if (!in_array($role, ['system', 'assistant', 'user'])) {
9304 - $role = 'user';
9305 - }
9306 -
9307 - $formatted_conversation[] = array(
9308 - 'role' => $role,
9309 - 'content' => $message['content']
9310 - );
5058 + // Handle error response from regular function
5059 + if (is_array($regular_response) && isset($regular_response['error'])) {
5060 + if ($testing_data !== null) {
5061 + $regular_response['testing_data'] = $testing_data;
9311 5062 }
5063 + header('Content-Type: application/json');
5064 + echo json_encode($regular_response);
5065 + return true;
9312 5066 }
9313 -
9314 - $body = json_encode([
9315 - 'model' => $selected_model,
9316 - 'messages' => $formatted_conversation,
9317 - 'temperature' => 1,
9318 - ]);
9319 -
9320 - $args = [
9321 - 'body' => $body,
9322 - 'headers' => [
9323 - 'Content-Type' => 'application/json',
9324 - 'Authorization' => 'Bearer ' . $openrouter_api_key,
9325 - 'HTTP-Referer' => home_url(),
9326 - 'X-Title' => get_bloginfo('name'),
9327 - ],
9328 - 'timeout' => 60,
9329 - 'redirection' => 5,
9330 - 'blocking' => true,
9331 - 'httpversion' => '1.0',
9332 - 'sslverify' => true,
5067 +
5068 + $response_data = [
5069 + 'text' => $regular_response,
5070 + 'html' => '',
5071 + 'session_id' => $session_id
9333 5072 ];
9334 -
9335 - $response = $this->mxchat_provider_call_with_retry('https://openrouter.ai/api/v1/chat/completions', $args, 'openai');
9336 -
9337 - if (is_wp_error($response)) {
9338 - $error_message = $response->get_error_message();
9339 - return [
9340 - 'error' => esc_html__('Connection error when contacting OpenRouter: ', 'mxchat') . esc_html($error_message),
9341 - 'error_code' => 'openrouter_connection_error',
9342 - 'provider' => 'openrouter'
9343 - ];
5073 +
5074 + if ($testing_data !== null) {
5075 + $response_data['testing_data'] = $testing_data;
5076 + //error_log("MxChat Testing: Added testing data to DeepSeek exception fallback");
9344 5077 }
9345 -
9346 - $status_code = wp_remote_retrieve_response_code($response);
9347 - if ($status_code !== 200) {
9348 - $response_body = wp_remote_retrieve_body($response);
9349 - $decoded_response = json_decode($response_body, true);
9350 -
9351 - $error_message = isset($decoded_response['error']['message'])
9352 - ? $decoded_response['error']['message']
9353 - : 'HTTP Error ' . $status_code;
9354 -
9355 - return [
9356 - 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
9357 - 'error_code' => 'openrouter_api_error',
9358 - 'provider' => 'openrouter',
9359 - 'status_code' => $status_code
9360 - ];
9361 - }
9362 -
9363 - $response_body = wp_remote_retrieve_body($response);
9364 - $decoded_response = json_decode($response_body, true);
9365 -
9366 - if (isset($decoded_response['choices'][0]['message']['content'])) {
9367 - return trim($decoded_response['choices'][0]['message']['content']);
9368 - } else {
9369 - return [
9370 - 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
9371 - 'error_code' => 'openrouter_response_format_error',
9372 - 'provider' => 'openrouter'
9373 - ];
9374 - }
9375 - } catch (Exception $e) {
9376 - return [
9377 - 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
9378 - 'error_code' => 'openrouter_exception',
9379 - 'provider' => 'openrouter'
9380 - ];
5078 +
5079 + header('Content-Type: application/json');
5080 + echo json_encode($response_data);
5081 + return true;
9381 5082 }
9382 5083 }
5084 +
9383 5085 private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
9384 -
9385 - // Get bot ID from session or request
9386 - $bot_id = $this->get_current_bot_id($session_id);
9387 -
9388 - // Get system prompt instructions using centralized function
9389 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9390 -
5086 + // Get system prompt instructions from options
5087 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5088 +
9391 5089 // Clean and validate conversation history
9392 5090 foreach ($conversation_history as &$message) {
9393 5091 // Convert bot and agent roles to assistant
9394 5092 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -9415,17 +5113,15 @@
9415 5113 'content' => $relevant_content
9416 5114 ];
9417 5115
9418 5116 // Build request body
9419 - $payload = [
5117 + $body = json_encode([
9420 5118 'model' => $selected_model,
9421 5119 'max_tokens' => 1000,
9422 5120 'temperature' => 0.8,
9423 5121 'messages' => $conversation_history,
9424 5122 'system' => $system_prompt_instructions
9425 - ];
9426 - if ($this->mxchat_claude_omits_temperature($selected_model)) { unset($payload['temperature']); }
9427 - $body = json_encode($payload);
5123 + ]);
9428 5124
9429 5125 // Set up API request
9430 5126 $args = [
9431 5127 'body' => $body,
@@ -9441,9 +5137,9 @@
9441 5137 'sslverify' => true,
9442 5138 ];
9443 5139
9444 5140 // Make API request
9445 - $response = $this->mxchat_provider_call_with_retry('https://api.anthropic.com/v1/messages', $args, 'anthropic');
5141 + $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
9446 5142
9447 5143 // Check for WordPress errors
9448 5144 if (is_wp_error($response)) {
9449 5145 //error_log("Claude API request error: " . $response->get_error_message());
@@ -9473,17 +5169,14 @@
9473 5169 //error_log("Claude API JSON decode error: " . json_last_error_msg());
9474 5170 return "Sorry, there was an error processing the API response.";
9475 5171 }
9476 5172
9477 - // Extract and validate response content. claude-fable-5 prepends a
9478 - // thinking block to content even with no thinking param — take the first
9479 - // TEXT block rather than content[0].
9480 - if (isset($response_body['content']) && is_array($response_body['content'])) {
9481 - foreach ($response_body['content'] as $block) {
9482 - if (isset($block['type'], $block['text']) && $block['type'] === 'text') {
9483 - return trim($block['text']);
9484 - }
9485 - }
5173 + // Extract and validate response content
5174 + if (isset($response_body['content']) &&
5175 + is_array($response_body['content']) &&
5176 + !empty($response_body['content']) &&
5177 + isset($response_body['content'][0]['text'])) {
5178 + return trim($response_body['content'][0]['text']);
9486 5179 }
9487 5180
9488 5181 // Log unexpected response format
9489 5182 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
@@ -9495,14 +5188,11 @@
9495 5188 if (!is_array($conversation_history)) {
9496 5189 $conversation_history = array();
9497 5190 }
9498 5191
9499 - // Get bot ID from session or request
9500 - $bot_id = $this->get_current_bot_id('');
9501 -
9502 - // Get system prompt instructions using centralized function
9503 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9504 -
5192 + // Get system prompt instructions from options
5193 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5194 +
9505 5195 // Create a new array for the formatted conversation
9506 5196 $formatted_conversation = array();
9507 5197
9508 5198 // Add system message first
@@ -9530,44 +5220,15 @@
9530 5220 );
9531 5221 }
9532 5222 }
9533 5223
9534 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
9535 - $is_gpt5_model = (
9536 - strpos($selected_model, 'gpt-5') === 0 ||
9537 - $selected_model === 'gpt-5.2' ||
9538 - $selected_model === 'gpt-5.1-2025-11-13' ||
9539 - $selected_model === 'gpt-5' ||
9540 - $selected_model === 'gpt-5-mini' ||
9541 - $selected_model === 'gpt-5-nano'
9542 - );
9543 -
9544 - // Build request body with optimal settings for fast responses
9545 - $request_body = [
5224 + $body = json_encode([
9546 5225 'model' => $selected_model,
9547 5226 'messages' => $formatted_conversation,
9548 5227 'temperature' => 1,
9549 5228 'stream' => false
9550 - ];
5229 + ]);
9551 5230
9552 - // Add reasoning_effort only for GPT-5 models that support it
9553 - // These chat models don't support reasoning_effort parameter
9554 - $no_reasoning_models = array('gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
9555 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
9556 - // GPT-5.1 uses 'low' instead of 'minimal'
9557 - if ($selected_model === 'gpt-5.1-2025-11-13') {
9558 - $request_body['reasoning_effort'] = 'low';
9559 - } elseif ($selected_model === 'gpt-5.5') {
9560 - $request_body['reasoning_effort'] = 'none';
9561 - } elseif ($selected_model === 'gpt-5.4') {
9562 - $request_body['reasoning_effort'] = 'none';
9563 - } else {
9564 - $request_body['reasoning_effort'] = 'minimal';
9565 - }
9566 - }
9567 -
9568 - $body = json_encode($request_body);
9569 -
9570 5231 $args = [
9571 5232 'body' => $body,
9572 5233 'headers' => [
9573 5234 'Content-Type' => 'application/json',
@@ -9579,12 +5240,13 @@
9579 5240 'httpversion' => '1.0',
9580 5241 'sslverify' => true,
9581 5242 ];
9582 5243
9583 - $response = $this->mxchat_provider_call_with_retry('https://api.openai.com/v1/chat/completions', $args, 'openai');
5244 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
9584 5245
9585 5246 if (is_wp_error($response)) {
9586 5247 $error_message = $response->get_error_message();
5248 + //error_log('OpenAI API Error: ' . $error_message);
9587 5249 return [
9588 5250 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
9589 5251 'error_code' => 'openai_connection_error',
9590 5252 'provider' => 'openai'
@@ -9603,8 +5265,10 @@
9603 5265 $error_type = isset($decoded_response['error']['type'])
9604 5266 ? $decoded_response['error']['type']
9605 5267 : 'unknown';
9606 5268
5269 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
5270 +
9607 5271 // Handle specific error types
9608 5272 switch ($error_type) {
9609 5273 case 'invalid_request_error':
9610 5274 if (strpos($error_message, 'API key') !== false) {
@@ -9652,8 +5316,9 @@
9652 5316
9653 5317 if (isset($decoded_response['choices'][0]['message']['content'])) {
9654 5318 return trim($decoded_response['choices'][0]['message']['content']);
9655 5319 } else {
5320 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
9656 5321 return [
9657 5322 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
9658 5323 'error_code' => 'openai_response_format_error',
9659 5324 'provider' => 'openai'
@@ -9659,8 +5324,9 @@
9659 5324 'provider' => 'openai'
9660 5325 ];
9661 5326 }
9662 5327 } catch (Exception $e) {
5328 + //error_log('OpenAI Exception: ' . $e->getMessage());
9663 5329 return [
9664 5330 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
9665 5331 'error_code' => 'openai_exception',
9666 5332 'provider' => 'openai'
@@ -9666,17 +5332,13 @@
9666 5332 'provider' => 'openai'
9667 5333 ];
9668 5334 }
9669 5335 }
9670 -
9671 5336 private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
9672 5337 try {
9673 - // Get bot ID from session or request
9674 - $bot_id = $this->get_current_bot_id($session_id);
9675 -
9676 - // Get system prompt instructions using centralized function
9677 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9678 -
5338 + // Get system prompt instructions from options
5339 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5340 +
9679 5341 // Add system prompt to relevant content
9680 5342 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
9681 5343
9682 5344 // Prepend system instructions to the conversation history
@@ -9725,9 +5387,9 @@
9725 5387 'sslverify' => true,
9726 5388 ];
9727 5389
9728 5390 // Make the API request
9729 - $response = $this->mxchat_provider_call_with_retry('https://api.x.ai/v1/chat/completions', $args, 'xai');
5391 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
9730 5392
9731 5393 // Process the response
9732 5394 if (is_wp_error($response)) {
9733 5395 $error_message = $response->get_error_message();
@@ -9868,14 +5530,11 @@
9868 5530 if (!is_array($conversation_history)) {
9869 5531 $conversation_history = array();
9870 5532 }
9871 5533
9872 - // Get bot ID from session or request
9873 - $bot_id = $this->get_current_bot_id($session_id);
9874 -
9875 - // Get system prompt instructions using centralized function
9876 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9877 -
5534 + // Get system prompt instructions from options
5535 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5536 +
9878 5537 // Create a new array for the formatted conversation
9879 5538 $formatted_conversation = array();
9880 5539
9881 5540 // Add system message first
@@ -9923,9 +5582,9 @@
9923 5582 'httpversion' => '1.0',
9924 5583 'sslverify' => true,
9925 5584 ];
9926 5585
9927 - $response = $this->mxchat_provider_call_with_retry('https://api.deepseek.com/v1/chat/completions', $args, 'openai');
5586 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
9928 5587
9929 5588 if (is_wp_error($response)) {
9930 5589 $error_message = $response->get_error_message();
9931 5590 //error_log('DeepSeek API Error: ' . $error_message);
@@ -10027,19 +5686,11 @@
10027 5686 ];
10028 5687 }
10029 5688 }
10030 5689 private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
10031 - // Read-time remap: gemini-3-pro-preview was shut down March 9, 2026.
10032 - // Auto-rescue existing installs whose saved model is the dead ID.
10033 - if ($selected_model === 'gemini-3-pro-preview') {
10034 - $selected_model = 'gemini-3.1-pro-preview';
10035 - }
10036 - // Get bot ID from session or request
10037 - $bot_id = $this->get_current_bot_id($session_id);
10038 -
10039 - // Get system prompt instructions using centralized function
10040 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
10041 -
5690 + // Get system prompt instructions from options
5691 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5692 +
10042 5693 // Add system prompt to relevant content
10043 5694 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
10044 5695
10045 5696 // Format messages for Gemini API
@@ -10134,11 +5785,9 @@
10134 5785 ]
10135 5786 ]);
10136 5787
10137 5788 // Prepare the API endpoint
10138 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
10139 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
10140 - $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5789 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
10141 5790
10142 5791 // Set up the API request
10143 5792 $args = [
10144 5793 'body' => $body,
@@ -10152,10 +5801,10 @@
10152 5801 'sslverify' => true,
10153 5802 ];
10154 5803
10155 5804 // Make the API request
10156 - $response = $this->mxchat_provider_call_with_retry($api_endpoint, $args, 'gemini');
10157 -
5805 + $response = wp_remote_post($api_endpoint, $args);
5806 +
10158 5807 // Process the response
10159 5808 if (is_wp_error($response)) {
10160 5809 return "Sorry, there was an error processing your request: " . $response->get_error_message();
10161 5810 }
@@ -10177,12 +5826,11 @@
10177 5826 return "Sorry, I couldn't process that request. The response format was unexpected.";
10178 5827 }
10179 5828 }
10180 5829
10181 -
10182 5830 public function test_streaming_request() {
10183 5831 $options = get_option('mxchat_options', []);
10184 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
5832 + $model = $options['model'] ?? 'gpt-4o';
10185 5833
10186 5834 // Detect provider from model prefix
10187 5835 $provider = strtolower(explode('-', $model)[0]);
10188 5836
@@ -10308,8 +5956,9 @@
10308 5956
10309 5957 return true;
10310 5958 }
10311 5959
5960 +
10312 5961 public function mxchat_dismiss_pre_chat_message() {
10313 5962 // Get and sanitize the user identifier
10314 5963 $user_id = $this->mxchat_get_user_identifier();
10315 5964 $user_id = sanitize_key($user_id);
@@ -10363,63 +6012,40 @@
10363 6012
10364 6013 return $dotProduct / ($normA * $normB);
10365 6014 }
10366 6015
10367 -
10368 6016 public function mxchat_enqueue_scripts_styles() {
10369 - // Fetch options from the database first to check loading strategy
10370 - $this->options = get_option('mxchat_options');
10371 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
10372 -
10373 - // Always enqueue CSS immediately
6017 + // Define version numbers for the styles and scripts
6018 + $chat_style_version = '2.3.9';
6019 + $chat_script_version = '2.3.9';
6020 + // Enqueue the script
6021 + wp_enqueue_script(
6022 + 'mxchat-chat-js',
6023 + plugin_dir_url(__FILE__) . '../js/chat-script.js',
6024 + array('jquery'),
6025 + $chat_script_version,
6026 + true
6027 + );
6028 + // Enqueue the CSS
10374 6029 wp_enqueue_style(
10375 6030 'mxchat-chat-css',
10376 6031 plugin_dir_url(__FILE__) . '../css/chat-style.css',
10377 6032 array(),
10378 - MXCHAT_VERSION
6033 + $chat_style_version
10379 6034 );
10380 -
10381 - // Handle script loading based on strategy
10382 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
10383 - // Enqueue the script normally
10384 - wp_enqueue_script(
10385 - 'mxchat-chat-js',
10386 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
10387 - array('jquery'),
10388 - MXCHAT_VERSION,
10389 - true
10390 - );
10391 -
10392 - // Add defer attribute if strategy is 'defer'
10393 - if ($loading_strategy === 'defer') {
10394 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
10395 - }
10396 - } else {
10397 - // For delay or interaction-based loading, we'll use a custom loader
10398 - // Don't enqueue the main script - we'll load it dynamically
10399 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
10400 - }
10401 -
6035 + // Fetch options from the database
6036 + $this->options = get_option('mxchat_options');
10402 6037 $prompts_options = get_option('mxchat_prompts_options', array());
10403 -
10404 - // Check if AI theme is active - if so, skip inline colors in JavaScript
10405 - $theme_options = get_option('mxchat_theme_options', array());
10406 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
10407 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
10408 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
10409 -
6038 +
10410 6039 // Prepare settings for JavaScript
10411 6040 $style_settings = array(
10412 6041 'ajax_url' => admin_url('admin-ajax.php'),
10413 - // The chat-send nonce is now fetched per-request from /wp-json/mxchat/v1/nonce
10414 - // (plan-6a68c9) so it never sits in cached HTML. We still emit a nonce here
10415 - // as a one-shot fallback for the first interaction on a fresh page load
10416 - // (so the very first chat-send doesn't need to wait for a REST round-trip),
10417 - // but the widget refetches before each subsequent send.
10418 - 'nonce' => wp_create_nonce('mxchat_chat_send'),
10419 - 'rest_url' => esc_url_raw(trailingslashit(rest_url('mxchat/v1'))),
6042 + 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
6043 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
6044 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
10420 6045 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
10421 6046 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
6047 + 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
10422 6048 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
10423 6049 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
10424 6050 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
10425 6051 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
@@ -10434,8 +6060,9 @@
10434 6060 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
10435 6061 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
10436 6062 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
10437 6063 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
6064 + 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
10438 6065 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
10439 6066 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
10440 6067 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
10441 6068 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
@@ -10441,145 +6068,15 @@
10441 6068 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
10442 6069 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
10443 6070 'initial_email_state' => null, // Also fixed this undefined variable
10444 6071 'skip_email_check' => true,
10445 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
10446 - 'skip_inline_colors' => $skip_inline_colors,
10447 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
6072 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
10448 6073 );
10449 -
10450 - // Behavior gates + labels (model, streaming, rate-limit copy, toolbar,
10451 - // print/transcript, satisfaction rating) come from the shared
10452 - // dynamic-settings method so this inline payload and the first-open
10453 - // refresh endpoint can never drift (plan-32db95).
10454 - $style_settings = array_merge($style_settings, $this->get_dynamic_widget_settings());
10455 -
10456 - // For normal/defer loading, use wp_localize_script
10457 - // For delayed loading, we store settings in a transient to be output inline
10458 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
10459 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
10460 - } else {
10461 - // Store settings for the delayed loader to use
10462 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
10463 - }
6074 + // Pass the settings to the script
6075 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
10464 6076 }
10465 6077
10466 -/**
10467 - * Output the delayed script loader for performance optimization
10468 - */
10469 -public function mxchat_output_delayed_script_loader() {
10470 - $this->options = get_option('mxchat_options');
10471 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
10472 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
10473 6078
10474 - // Get the stored settings
10475 - $prompts_options = get_option('mxchat_prompts_options', array());
10476 - $theme_options = get_option('mxchat_theme_options', array());
10477 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
10478 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
10479 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
10480 -
10481 - $style_settings = array(
10482 - 'ajax_url' => admin_url('admin-ajax.php'),
10483 - // Per-request nonce — see plan-6a68c9; widget fetches via /wp-json/mxchat/v1/nonce
10484 - // before each send. This inline value is a one-shot fallback for the first interaction.
10485 - 'nonce' => wp_create_nonce('mxchat_chat_send'),
10486 - 'rest_url' => esc_url_raw(trailingslashit(rest_url('mxchat/v1'))),
10487 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
10488 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
10489 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
10490 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
10491 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
10492 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
10493 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
10494 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
10495 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
10496 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
10497 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
10498 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
10499 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
10500 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
10501 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
10502 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
10503 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
10504 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
10505 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
10506 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
10507 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
10508 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
10509 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
10510 - 'initial_email_state' => null,
10511 - 'skip_email_check' => true,
10512 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
10513 - 'skip_inline_colors' => $skip_inline_colors,
10514 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
10515 - );
10516 -
10517 - // Behavior gates + labels (model, streaming, rate-limit copy, toolbar,
10518 - // print/transcript, satisfaction rating) come from the shared
10519 - // dynamic-settings method so this inline payload and the first-open
10520 - // refresh endpoint can never drift (plan-32db95).
10521 - $style_settings = array_merge($style_settings, $this->get_dynamic_widget_settings());
10522 -
10523 - // Determine delay time based on strategy
10524 - $delay_ms = 0;
10525 - switch ($loading_strategy) {
10526 - case 'delay_1s':
10527 - $delay_ms = 1000;
10528 - break;
10529 - case 'delay_3s':
10530 - $delay_ms = 3000;
10531 - break;
10532 - case 'delay_5s':
10533 - $delay_ms = 5000;
10534 - break;
10535 - }
10536 -
10537 - ?>
10538 - <script type="text/javascript">
10539 - (function() {
10540 - var mxchatLoaded = false;
10541 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
10542 - window.mxchatChat = mxchatChat;
10543 -
10544 - function loadMxChatScript() {
10545 - if (mxchatLoaded) return;
10546 - mxchatLoaded = true;
10547 -
10548 - function appendChatScript() {
10549 - var script = document.createElement('script');
10550 - script.src = <?php echo wp_json_encode($script_url); ?>;
10551 - script.type = 'text/javascript';
10552 - document.body.appendChild(script);
10553 - }
10554 -
10555 - if (typeof jQuery !== 'undefined') {
10556 - appendChatScript();
10557 - } else {
10558 - var jq = document.createElement('script');
10559 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
10560 - jq.onload = appendChatScript;
10561 - document.body.appendChild(jq);
10562 - }
10563 - }
10564 -
10565 - <?php if ($loading_strategy === 'on_interaction'): ?>
10566 - // Load on user interaction
10567 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
10568 - events.forEach(function(evt) {
10569 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
10570 - });
10571 - // Fallback: load after 8 seconds if no interaction
10572 - setTimeout(loadMxChatScript, 8000);
10573 - <?php else: ?>
10574 - // Load after specified delay
10575 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
10576 - <?php endif; ?>
10577 - })();
10578 - </script>
10579 - <?php
10580 -}
10581 -
10582 6079 /**
10583 6080 * Setup the cron jobs for rate limits with guard against multiple calls
10584 6081 */
10585 6082 public function setup_rate_limit_cron_jobs() {
@@ -10717,9 +6214,9 @@
10717 6214 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
10718 6215 }
10719 6216 }
10720 6217 /**
10721 - * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
6218 + * Enhanced rate limit check that includes fallback cleanup
10722 6219 */
10723 6220 public function check_rate_limit() {
10724 6221 // Check if we need to run fallback cleanup
10725 6222 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
@@ -10729,66 +6226,11 @@
10729 6226 $this->mxchat_reset_rate_limits();
10730 6227 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
10731 6228 }
10732 6229
10733 - // Get bot ID from current request context
10734 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
6230 + // Continue with your existing rate limit logic...
6231 + $all_options = get_option('mxchat_options', []);
10735 6232
10736 - // Get bot-specific options (includes rate limits if overridden)
10737 - $bot_options = $this->get_bot_options($bot_id);
10738 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
10739 -
10740 - // Use bot-specific rate limits if available, otherwise fall back to default
10741 - $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
10742 -
10743 - // -------------------------------------------------------------------
10744 - // Whole-chatbot global cap (independent of role). Evaluated FIRST so
10745 - // it acts as a hard ceiling across all users + all roles. Default is
10746 - // 'unlimited' so existing installs are unchanged. Counter key drops
10747 - // both <role> and <user_id> segments — single pool per bot.
10748 - // -------------------------------------------------------------------
10749 - $global_cfg = isset($current_options['rate_limits_global']) && is_array($current_options['rate_limits_global'])
10750 - ? $current_options['rate_limits_global']
10751 - : (isset(get_option('mxchat_options', [])['rate_limits_global']) ? get_option('mxchat_options', [])['rate_limits_global'] : []);
10752 - $global_limit_raw = isset($global_cfg['limit']) ? (string) $global_cfg['limit'] : 'unlimited';
10753 - $global_timeframe = isset($global_cfg['timeframe']) ? (string) $global_cfg['timeframe'] : 'daily';
10754 - if ($global_limit_raw !== '' && $global_limit_raw !== 'unlimited' && (int) $global_limit_raw >= 1) {
10755 - $bot_id_for_global = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
10756 - $safe_bot_global = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id_for_global);
10757 - $global_option = 'mxchat_chat_limit_' . $safe_bot_global . '_global';
10758 - $global_data = get_option($global_option, ['count' => 0, 'timestamp' => time()]);
10759 - if ((int) $global_data['count'] === 0) {
10760 - $global_data['timestamp'] = time();
10761 - update_option($global_option, $global_data);
10762 - }
10763 - $now = time();
10764 - $ts = (int) $global_data['timestamp'];
10765 - $reset = false;
10766 - switch ($global_timeframe) {
10767 - case 'hourly': $reset = ($now - $ts) >= 3600; break;
10768 - case 'daily': $reset = ($now - $ts) >= 86400; break;
10769 - case 'weekly': $reset = ($now - $ts) >= 604800; break;
10770 - case 'monthly': $reset = ($now - $ts) >= 2592000; break;
10771 - }
10772 - if ($reset) {
10773 - $global_data = ['count' => 0, 'timestamp' => $now];
10774 - update_option($global_option, $global_data);
10775 - }
10776 - if ((int) $global_data['count'] >= (int) $global_limit_raw) {
10777 - $global_msg = !empty($global_cfg['message'])
10778 - ? $global_cfg['message']
10779 - : __('This chatbot has reached its message limit. Please try again later.', 'mxchat');
10780 - return [
10781 - 'error' => true,
10782 - 'message' => $this->process_rate_limit_message_html($global_msg),
10783 - ];
10784 - }
10785 - // Reserve the slot for this request. Per-role check below also increments
10786 - // its own counter — that is intentional, both ceilings apply independently.
10787 - $global_data['count']++;
10788 - update_option($global_option, $global_data);
10789 - }
10790 -
10791 6233 // Determine user role or if logged out
10792 6234 if (is_user_logged_in()) {
10793 6235 $user = wp_get_current_user();
10794 6236 $user_id = $user->ID;
@@ -10808,13 +6250,13 @@
10808 6250 $user_id = $this->get_client_ip();
10809 6251 }
10810 6252
10811 6253 // Check if rate limits are configured for this role
10812 - if (!isset($rate_limits_source[$role])) {
6254 + if (!isset($all_options['rate_limits'][$role])) {
10813 6255 return true; // No limit set for this role
10814 6256 }
10815 6257
10816 - $limit = $rate_limits_source[$role]['limit'];
6258 + $limit = $all_options['rate_limits'][$role]['limit'];
10817 6259
10818 6260 // If unlimited, return true immediately
10819 6261 if ($limit === 'unlimited') {
10820 6262 return true;
@@ -10819,16 +6261,13 @@
10819 6261 if ($limit === 'unlimited') {
10820 6262 return true;
10821 6263 }
10822 6264
10823 - // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
6265 + // Get the option name for this user/role with safer naming
10824 6266 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
10825 6267 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
10826 - $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
6268 + $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
10827 6269
10828 - // Include bot_id in option name so each bot has separate rate limits
10829 - $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
10830 -
10831 6270 // Get the counter data
10832 6271 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
10833 6272
10834 6273 // If first request or counter reset needed, set the initial timestamp
@@ -10837,10 +6276,10 @@
10837 6276 update_option($option_name, $limit_data);
10838 6277 }
10839 6278
10840 6279 // Get the timeframe
10841 - $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
10842 - $rate_limits_source[$role]['timeframe'] : 'daily';
6280 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
6281 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
10843 6282
10844 6283 // Check if the counter needs to be reset based on timeframe
10845 6284 $current_time = time();
10846 6285 $timestamp = $limit_data['timestamp'];
@@ -10869,10 +6308,10 @@
10869 6308
10870 6309 // Check if user has exceeded their limit
10871 6310 if ($limit_data['count'] >= intval($limit)) {
10872 6311 // Get the custom message for this role
10873 - $message = !empty($rate_limits_source[$role]['message'])
10874 - ? $rate_limits_source[$role]['message']
6312 + $message = !empty($all_options['rate_limits'][$role]['message'])
6313 + ? $all_options['rate_limits'][$role]['message']
10875 6314 : __('Rate limit exceeded. Please try again later.', 'mxchat');
10876 6315
10877 6316 // Add timeframe information to the message if placeholders exist
10878 6317 $timeframe_label = '';
@@ -11162,11 +6601,8 @@
11162 6601
11163 6602 /**
11164 6603 * AJAX handler to get system information for testing panel
11165 6604 */
11166 -/**
11167 - * AJAX handler to get system information for testing panel
11168 - */
11169 6605 public function mxchat_get_system_info() {
11170 6606 // Verify nonce for security
11171 6607 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
11172 6608 wp_send_json_error(['message' => 'Invalid nonce']);
@@ -11184,24 +6620,10 @@
11184 6620 ? $this->options['system_prompt_instructions']
11185 6621 : 'No system prompt configured';
11186 6622
11187 6623 // Get selected model
11188 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
6624 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
11189 6625
11190 - // Check if OpenRouter is being used
11191 - $is_openrouter = ($selected_model === 'openrouter');
11192 - $openrouter_model = '';
11193 -
11194 - if ($is_openrouter) {
11195 - // Get the actual OpenRouter model that's selected
11196 - $openrouter_model = isset($this->options['openrouter_selected_model'])
11197 - ? $this->options['openrouter_selected_model']
11198 - : 'No OpenRouter model selected';
11199 -
11200 - // Update selected_model display to show both
11201 - $selected_model = 'OpenRouter: ' . $openrouter_model;
11202 - }
11203 -
11204 6626 // Get API key status (just check if they exist, don't expose the keys)
11205 6627 $api_status = [];
11206 6628 $api_status['openai'] = !empty($this->options['api_key']);
11207 6629 $api_status['claude'] = !empty($this->options['claude_api_key']);
@@ -11207,15 +6629,12 @@
11207 6629 $api_status['claude'] = !empty($this->options['claude_api_key']);
11208 6630 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
11209 6631 $api_status['xai'] = !empty($this->options['xai_api_key']);
11210 6632 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
11211 - $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
11212 6633
11213 6634 wp_send_json_success([
11214 6635 'system_prompt' => $system_prompt,
11215 6636 'selected_model' => $selected_model,
11216 - 'is_openrouter' => $is_openrouter,
11217 - 'openrouter_model' => $openrouter_model,
11218 6637 'api_status' => $api_status
11219 6638 ]);
11220 6639 }
11221 6640
@@ -11234,12 +6653,12 @@
11234 6653 wp_send_json_error(['message' => 'Unauthorized']);
11235 6654 return;
11236 6655 }
11237 6656
11238 - // Get similarity threshold from main options (default 35%)
6657 + // Get similarity threshold from main options (default 75%)
11239 6658 $similarity_threshold = isset($this->options['similarity_threshold'])
11240 6659 ? ((int) $this->options['similarity_threshold']) / 100
11241 - : 0.35;
6660 + : 0.75;
11242 6661
11243 6662 wp_send_json_success([
11244 6663 'threshold' => $similarity_threshold,
11245 6664 'threshold_percentage' => ($similarity_threshold * 100) . '%'
@@ -11254,42 +6673,24 @@
11254 6673 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
11255 6674 wp_send_json_error(['message' => 'Invalid nonce']);
11256 6675 return;
11257 6676 }
11258 -
6677 +
11259 6678 // Only allow admin users
11260 6679 if (!current_user_can('administrator')) {
11261 6680 wp_send_json_error(['message' => 'Unauthorized']);
11262 6681 return;
11263 6682 }
11264 -
11265 - // Check OpenAI Vector Store first (takes priority)
11266 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
11267 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
11268 -
11269 - if ($use_vectorstore) {
11270 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
11271 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
11272 -
11273 - $kb_info = [
11274 - 'type' => 'OpenAI Vector Store',
11275 - 'status' => 'Active',
11276 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
11277 - ];
11278 -
11279 - wp_send_json_success($kb_info);
11280 - return;
11281 - }
11282 -
6683 +
11283 6684 // Check Pinecone vs WordPress
11284 6685 $addon_options = get_option('mxchat_pinecone_addon_options', array());
11285 6686 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
11286 -
6687 +
11287 6688 $kb_info = [
11288 6689 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
11289 6690 'status' => 'Active'
11290 6691 ];
11291 -
6692 +
11292 6693 // Get document count
11293 6694 if ($use_pinecone) {
11294 6695 $kb_info['documents'] = 'Connected to Pinecone';
11295 6696 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -11299,9 +6700,9 @@
11299 6700 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
11300 6701 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
11301 6702 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
11302 6703 }
11303 -
6704 +
11304 6705 wp_send_json_success($kb_info);
11305 6706 }
11306 6707
11307 6708 /**
@@ -11383,13 +6784,9 @@
11383 6784 // Clear any other session-specific transients
11384 6785 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
11385 6786 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
11386 6787 delete_transient("mxchat_include_word_in_context_{$session_id}");
11387 -
11388 - // Clear form addon state (pending forms and submitted forms)
11389 - delete_option("mxchat_pending_form_{$session_id}");
11390 - delete_option("mxchat_submitted_forms_{$session_id}");
11391 -
6788 +
11392 6789 //error_log("MxChat: Cleared all data for session: {$session_id}");
11393 6790 }
11394 6791
11395 6792 /**
@@ -11424,15 +6821,15 @@
11424 6821 $testing_data = [
11425 6822 'query' => $message,
11426 6823 'timestamp' => time(),
11427 6824 'top_matches' => [],
11428 - 'action_matches' => [] // Add action matches
6825 + 'action_matches' => [] // NEW: Add action matches
11429 6826 ];
11430 6827
11431 6828 // Get similarity threshold
11432 6829 $similarity_threshold = isset($this->options['similarity_threshold'])
11433 6830 ? ((int) $this->options['similarity_threshold']) / 100
11434 - : 0.35;
6831 + : 0.75;
11435 6832
11436 6833 $testing_data['similarity_threshold'] = $similarity_threshold;
11437 6834
11438 6835 // Use the real similarity analysis if available
@@ -11447,9 +6844,9 @@
11447 6844
11448 6845 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
11449 6846 }
11450 6847
11451 - // Include action analysis if available
6848 + // NEW: Include action analysis if available
11452 6849 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
11453 6850 $testing_data['action_matches'] = $this->last_action_analysis;
11454 6851
11455 6852 // Clear it after capturing to avoid stale data
@@ -11460,13 +6857,13 @@
11460 6857 }
11461 6858
11462 6859
11463 6860 /**
11464 - * Track URL clicks from chatbot responses
6861 + * NEW: Track URL clicks from chatbot responses
11465 6862 */
11466 6863 public function mxchat_track_url_click() {
11467 6864 // Verify nonce for security
11468 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
6865 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
11469 6866 wp_send_json_error(['message' => 'Invalid nonce']);
11470 6867 wp_die();
11471 6868 }
11472 6869
@@ -11499,9 +6896,9 @@
11499 6896 wp_die();
11500 6897 }
11501 6898
11502 6899 /**
11503 - * Get URL click analytics for a session
6900 + * NEW: Get URL click analytics for a session
11504 6901 */
11505 6902 public function mxchat_get_url_clicks($session_id) {
11506 6903 global $wpdb;
11507 6904 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
@@ -11513,13 +6910,13 @@
11513 6910
11514 6911 return $clicks;
11515 6912 }
11516 6913 /**
11517 - * Track the originating page where chat was started
6914 + * NEW: Track the originating page where chat was started
11518 6915 */
11519 6916 public function mxchat_track_originating_page() {
11520 6917 // Verify nonce
11521 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
6918 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
11522 6919 wp_send_json_error(['message' => 'Invalid nonce']);
11523 6920 wp_die();
11524 6921 }
11525 6922
@@ -11564,195 +6961,8 @@
11564 6961 wp_send_json_success(['message' => 'Originating page tracked']);
11565 6962 wp_die();
11566 6963 }
11567 6964
11568 -/**
11569 - * Validate and clean URLs from AI response
11570 - * Removes any URLs that aren't in the knowledge base
11571 - *
11572 - * @param string $response_text The AI-generated response
11573 - * @param array $valid_urls Array of URLs from the knowledge base
11574 - * @return string Cleaned response with invalid URLs removed/flagged
11575 - */
11576 -private function validate_and_clean_urls($response_text, $valid_urls) {
11577 - // DEBUG: Log what we're working with
11578 - //error_log("=== MxChat URL Validation Debug ===");
11579 - //error_log("Valid URLs count: " . count($valid_urls));
11580 - //error_log("Valid URLs: " . print_r($valid_urls, true));
11581 - //error_log("Response text length: " . strlen($response_text));
11582 - //error_log("Response text preview: " . substr($response_text, 0, 500));
11583 -
11584 - // If no valid URLs provided or empty response, return as-is
11585 - if (empty($valid_urls) || empty($response_text)) {
11586 - //error_log("Validation skipped - empty valid_urls or response");
11587 - return $response_text;
11588 - }
11589 -
11590 - // Extract all URLs from the AI response
11591 - // This regex matches http:// and https:// URLs
11592 - preg_match_all(
11593 - '#\bhttps?://[^\s<>"\')\]]+#i',
11594 - $response_text,
11595 - $matches
11596 - );
11597 -
11598 - // If no URLs found in response, return as-is
11599 - if (empty($matches[0])) {
11600 - //error_log("No URLs found in response");
11601 - return $response_text;
11602 - }
11603 -
11604 - $found_urls = $matches[0];
11605 - $cleaned_response = $response_text;
11606 - $removed_count = 0;
11607 -
11608 - // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
11609 - $normalized_valid_urls = array_map(function($url) {
11610 - // Remove trailing slash
11611 - $url = rtrim($url, '/');
11612 - // Remove URL fragments (#section)
11613 - $url = preg_replace('/#.*$/', '', $url);
11614 - // Remove trailing punctuation that might have been captured
11615 - $url = rtrim($url, '.,;:!?');
11616 - return $url;
11617 - }, $valid_urls);
11618 -
11619 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
11620 -
11621 - foreach ($found_urls as $found_url) {
11622 - // Clean up the found URL (remove trailing punctuation that might have been captured)
11623 - $clean_found_url = rtrim($found_url, '.,;:!?)');
11624 -
11625 - // DEBUG: Log each URL being checked
11626 - //error_log("Checking found URL: " . $found_url);
11627 -
11628 - // Normalize for comparison
11629 - $normalized_found = rtrim($clean_found_url, '/');
11630 - $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
11631 -
11632 - //error_log("Normalized found URL: " . $normalized_found);
11633 -
11634 - // Check if this URL exists in our valid URLs list
11635 - $is_valid = false;
11636 -
11637 - //error_log("Starting validation checks for: " . $normalized_found);
11638 -
11639 - // First, try exact match
11640 - if (in_array($normalized_found, $normalized_valid_urls)) {
11641 - $is_valid = true;
11642 - //error_log("EXACT MATCH FOUND");
11643 - } else {
11644 - //error_log("No exact match, checking variations...");
11645 - // If no exact match, check if it's a variation (with query params, etc.)
11646 - foreach ($normalized_valid_urls as $valid_url) {
11647 - //error_log(" Comparing against valid URL: " . $valid_url);
11648 -
11649 - // Check if the found URL starts with a valid URL (handles query params)
11650 - if (strpos($normalized_found, $valid_url) === 0) {
11651 - // Check what comes after the valid URL
11652 - $remainder = substr($normalized_found, strlen($valid_url));
11653 -
11654 - // Only valid if:
11655 - // 1. Exact match (remainder is empty)
11656 - // 2. Query params (starts with ?)
11657 - // 3. Fragment (starts with #)
11658 - if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
11659 - $is_valid = true;
11660 - //error_log(" MATCH: Found URL is valid variation of base URL");
11661 - break;
11662 - } else {
11663 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
11664 - }
11665 - }
11666 - // Also check the reverse (in case valid URL has query params)
11667 - if (strpos($valid_url, $normalized_found) === 0) {
11668 - $is_valid = true;
11669 - //error_log(" MATCH: Valid URL starts with found URL");
11670 - break;
11671 - }
11672 - }
11673 -
11674 - if (!$is_valid) {
11675 - //error_log("NO MATCH FOUND - URL should be removed");
11676 - }
11677 - }
11678 -
11679 - // If URL is not valid, remove it from the response
11680 - if (!$is_valid) {
11681 - // Log the removal for debugging
11682 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
11683 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
11684 -
11685 - $removed_count++;
11686 -
11687 - // Check if URL is part of a markdown link: [text](url)
11688 - $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
11689 - if (preg_match($markdown_pattern, $cleaned_response)) {
11690 - //error_log("Found markdown link, removing but keeping text");
11691 - // Remove the markdown link but keep the text
11692 - $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
11693 - }
11694 - // Check if URL is part of an HTML link: <a href="url">text</a>
11695 - else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
11696 - //error_log("Found HTML link, removing but keeping text");
11697 - // Remove the HTML link but keep the text
11698 - $link_text = $link_match[1];
11699 - $cleaned_response = preg_replace(
11700 - '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
11701 - $link_text,
11702 - $cleaned_response
11703 - );
11704 - }
11705 - // Otherwise just remove the bare URL
11706 - else {
11707 - //error_log("Removing bare URL");
11708 - $cleaned_response = str_replace($found_url, '', $cleaned_response);
11709 - }
11710 - }
11711 - }
11712 -
11713 - // Log summary if any URLs were removed
11714 - if ($removed_count > 0) {
11715 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
11716 - } else {
11717 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
11718 - }
11719 -
11720 - // Clean up any double spaces or awkward punctuation left behind
11721 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
11722 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
11723 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
11724 -
11725 - //error_log("Final cleaned response: " . $cleaned_response);
11726 -
11727 - return trim($cleaned_response);
11728 -}
11729 -
11730 -/**
11731 - * AJAX handler to get current chat mode for a session
11732 - */
11733 -public function mxchat_get_current_chat_mode() {
11734 - // Verify nonce for security
11735 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
11736 - wp_send_json_error(['message' => 'Invalid nonce']);
11737 - wp_die();
11738 - }
11739 -
11740 - $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
11741 -
11742 - if (empty($session_id)) {
11743 - wp_send_json_error(['message' => 'Session ID missing']);
11744 - wp_die();
11745 - }
11746 -
11747 - // Get the current chat mode for this session
11748 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
11749 -
11750 - wp_send_json_success([
11751 - 'chat_mode' => $chat_mode
11752 - ]);
11753 - wp_die();
11754 -}
11755 6965
11756 6966
11757 6967
11758 6968 }