PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.6
MxChat – AI Chatbot & Content Generation for WordPress v2.3.6
3.2.22 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 All 153 releases
← All changes | includes/class-mxchat-integrator.php +1608 -7586 3.2.102.3.6 View file →
@@ -8,258 +8,13 @@
8 8 private $prompts_options;
9 9 private $chat_count;
10 10 private $fallbackResponse;
11 11 private $productCardHtml;
12 - // plan-mxchat-20260617-48a57a — function-calling UI payload capture. When a
13 - // model-invoked tool yields a UI element (generated image, woo product card,
14 - // image-search gallery), the FC loop stashes its html here so the FC outcome
15 - // handler can SURFACE it to the frontend the same way the intent path does,
16 - // instead of stripping it to text for the model (the bug: UI-bearing actions
17 - // rendered nothing under function calling).
18 - private $fc_ui_html = '';
19 - private $fc_ui_images = array();
20 - private $fc_ui_captured = false;
21 12 private $word_handler;
22 13 private $last_similarity_analysis = null;
23 - private $current_valid_urls = [];
24 - private $last_vectorstore_error = null;
25 - private $is_streaming = false; // ADDED: Track if current request is streaming
26 - private $streaming_headers_sent = false; // Track if streaming headers have been sent
27 - private $pending_originating_page = null; // Originating page captured at session start, consumed on row insert
28 - private $current_action_instruction = null; // Success-message instruction injected into the next system context
29 - private $last_action_analysis = null; // Last action-match analysis for testing_data payloads
30 14
31 -/**
32 - * Setup streaming headers - call this right before actually streaming
33 - * This delays header setup to allow actions/forms to return JSON responses
34 - */
35 -/**
36 - * Auto-retry wrapper around wp_remote_post for chat-send provider calls.
37 - *
38 - * Retries up to twice (750ms then 2000ms backoff) when the upstream provider
39 - * returns a TRANSIENT error: WP timeout, 429, 502, 503, 504, or a provider-
40 - * specific "overloaded" / "rate limit" body string. Returns immediately on
41 - * permanent errors (401/403/404/422) so misconfiguration surfaces fast.
42 - *
43 - * Drop-in replacement for wp_remote_post — returns the same shape
44 - * (WP_Error or response array) so the caller's existing error-handling
45 - * code path is unchanged.
46 - *
47 - * STREAMING PATH NOTE: this helper is ONLY for non-streaming chat-send
48 - * paths (the *_response_openai / *_response_claude / etc functions).
49 - * For the *_stream variants, the cURL initial-connect happens inside a
50 - * read-chunks loop — retrying there safely (without re-emitting partial
51 - * stream chunks to the client) is a separate problem. Streaming paths
52 - * are NOT wrapped in this build; tracked as a follow-on.
53 - *
54 - * Honors the `mxchat_options['auto_retry_on_transient_error']` toggle
55 - * (default true). When false, behavior is identical to plain wp_remote_post.
56 - */
57 -private function mxchat_provider_call_with_retry($url, $args, $provider_hint = '') {
58 - $opts = is_array($this->options ?? null) ? $this->options : array();
59 - $enabled = !isset($opts['auto_retry_on_transient_error']) ||
60 - (string) $opts['auto_retry_on_transient_error'] !== '0';
61 15
62 - if (!$enabled) {
63 - return wp_remote_post($url, $args);
64 - }
65 -
66 - $backoffs = array(0, 750, 2000); // ms — first attempt 0, then retry waits
67 - $last_response = null;
68 -
69 - foreach ($backoffs as $i => $delay_ms) {
70 - if ($delay_ms > 0) {
71 - usleep($delay_ms * 1000);
72 - }
73 - $response = wp_remote_post($url, $args);
74 - $last_response = $response;
75 -
76 - if (!$this->mxchat_is_transient_provider_error($response, $provider_hint)) {
77 - return $response;
78 - }
79 -
80 - if (defined('WP_DEBUG') && WP_DEBUG) {
81 - $code_for_log = is_wp_error($response) ? 'wp_error:' . $response->get_error_code()
82 - : (int) wp_remote_retrieve_response_code($response);
83 - error_log(sprintf(
84 - '[MxChat] Transient provider error (provider=%s, attempt=%d/3, status=%s). %s',
85 - $provider_hint ?: 'unknown',
86 - $i + 1,
87 - $code_for_log,
88 - ($i + 1) < count($backoffs) ? 'Retrying.' : 'Giving up.'
89 - ));
90 - }
91 - }
92 -
93 - return $last_response;
94 -}
95 -
96 16 /**
97 - * Returns true if a wp_remote_post response represents a TRANSIENT
98 - * provider error worth retrying. Conservative — only retries on signals
99 - * that are very likely to clear within a few seconds.
100 - *
101 - * Transient signals:
102 - * - WP_Error with timeout / connection / dns / ssl
103 - * - HTTP 429, 502, 503, 504
104 - * - Provider-specific overload bodies (gemini "overloaded", openai
105 - * "server_error", anthropic "overloaded_error", xai/grok "Rate limit")
106 - *
107 - * NOT transient (return false — fail-fast):
108 - * - 200/2xx (success)
109 - * - 401, 403, 404, 422 (auth / config errors — retrying wastes the
110 - * budget; the user needs to fix something)
111 - * - Any other 4xx (assume permanent unless explicitly listed above)
112 - * - 5xx other than the four listed above (e.g. 500 generic server error
113 - * is often a malformed request on our side, not a transient outage)
114 - */
115 -private function mxchat_is_transient_provider_error($response, $provider_hint = '') {
116 - if (is_wp_error($response)) {
117 - $code = $response->get_error_code();
118 - return in_array($code, array('http_request_failed', 'connection_failed', 'connection_timeout'), true)
119 - || stripos((string) $response->get_error_message(), 'timed out') !== false
120 - || stripos((string) $response->get_error_message(), 'timeout') !== false;
121 - }
122 -
123 - $status = (int) wp_remote_retrieve_response_code($response);
124 - if (in_array($status, array(429, 502, 503, 504), true)) {
125 - return true;
126 - }
127 - if ($status >= 200 && $status < 300) {
128 - return false;
129 - }
130 - // Permanent 4xx that should fail fast — even with no body.
131 - if (in_array($status, array(401, 403, 404, 405, 422), true)) {
132 - return false;
133 - }
134 -
135 - // Provider-specific body inspection for the cases where the upstream
136 - // returns 200 with an error envelope (gemini does this for overload).
137 - $body = (string) wp_remote_retrieve_body($response);
138 - if ($body === '') {
139 - return false;
140 - }
141 - $lower = strtolower($body);
142 - $hint = strtolower((string) $provider_hint);
143 -
144 - if ($hint === 'gemini' && (strpos($lower, 'overloaded') !== false
145 - || strpos($lower, 'high demand') !== false
146 - || strpos($lower, 'model is overloaded') !== false)) {
147 - return true;
148 - }
149 - if ($hint === 'openai' && (strpos($lower, 'rate limit reached') !== false
150 - || strpos($lower, '"type":"server_error"') !== false
151 - || strpos($lower, '"code":"server_error"') !== false)) {
152 - return true;
153 - }
154 - if ($hint === 'anthropic' && (strpos($lower, '"type":"overloaded_error"') !== false
155 - || strpos($lower, 'overloaded_error') !== false)) {
156 - return true;
157 - }
158 - if (($hint === 'xai' || $hint === 'grok') && strpos($lower, 'rate limit') !== false) {
159 - return true;
160 - }
161 -
162 - return false;
163 -}
164 -
165 -/**
166 - * Streaming-path classifier: same rules as mxchat_is_transient_provider_error
167 - * but takes a raw (http_code, body, provider_hint, curl_errno) tuple as
168 - * captured during a cURL streaming exec. cURL's WRITEFUNCTION/HEADERFUNCTION
169 - * collect status separately from a plain wp_remote_post array shape, so the
170 - * non-streaming helper above can't be called directly. This delegate keeps
171 - * the classification rules identical across both paths.
172 - */
173 -private function mxchat_is_transient_provider_error_raw($http_code, $body, $provider_hint = '', $curl_errno = 0) {
174 - if ($curl_errno) {
175 - // cURL transport-level error (timeout, connection failure, DNS, etc.)
176 - // Match the same WP_Error timeout/connection signals the array variant treats as transient.
177 - return in_array($curl_errno, array(
178 - CURLE_OPERATION_TIMEDOUT,
179 - CURLE_COULDNT_CONNECT,
180 - CURLE_COULDNT_RESOLVE_HOST,
181 - CURLE_SSL_CONNECT_ERROR,
182 - CURLE_GOT_NOTHING,
183 - CURLE_SEND_ERROR,
184 - CURLE_RECV_ERROR,
185 - ), true);
186 - }
187 -
188 - $status = (int) $http_code;
189 - if (in_array($status, array(429, 502, 503, 504), true)) {
190 - return true;
191 - }
192 - if ($status >= 200 && $status < 300) {
193 - return false;
194 - }
195 - if (in_array($status, array(401, 403, 404, 405, 422), true)) {
196 - return false;
197 - }
198 -
199 - $body = (string) $body;
200 - if ($body === '') {
201 - return false;
202 - }
203 - $lower = strtolower($body);
204 - $hint = strtolower((string) $provider_hint);
205 -
206 - if ($hint === 'gemini' && (strpos($lower, 'overloaded') !== false
207 - || strpos($lower, 'high demand') !== false
208 - || strpos($lower, 'model is overloaded') !== false)) {
209 - return true;
210 - }
211 - if ($hint === 'openai' && (strpos($lower, 'rate limit reached') !== false
212 - || strpos($lower, '"type":"server_error"') !== false
213 - || strpos($lower, '"code":"server_error"') !== false)) {
214 - return true;
215 - }
216 - if ($hint === 'anthropic' && (strpos($lower, '"type":"overloaded_error"') !== false
217 - || strpos($lower, 'overloaded_error') !== false)) {
218 - return true;
219 - }
220 - if (($hint === 'xai' || $hint === 'grok') && strpos($lower, 'rate limit') !== false) {
221 - return true;
222 - }
223 -
224 - return false;
225 -}
226 -
227 -/**
228 - * Whether transient-error auto-retry is enabled in admin settings.
229 - * Default true unless explicitly set to '0'. Used by both wp_remote_post
230 - * (mxchat_provider_call_with_retry) and cURL streaming paths.
231 - */
232 -private function mxchat_retry_enabled() {
233 - $opts = is_array($this->options ?? null) ? $this->options : array();
234 - return !isset($opts['auto_retry_on_transient_error']) ||
235 - (string) $opts['auto_retry_on_transient_error'] !== '0';
236 -}
237 -
238 -private function setup_streaming_headers() {
239 - if ($this->streaming_headers_sent || headers_sent()) {
240 - return false;
241 - }
242 -
243 - // Disable output buffering
244 - while (ob_get_level()) {
245 - ob_end_flush();
246 - }
247 -
248 - // Set headers for SSE
249 - header('Content-Type: text/event-stream');
250 - header('Cache-Control: no-cache');
251 - header('Connection: keep-alive');
252 - header('X-Accel-Buffering: no');
253 -
254 - ob_implicit_flush(true);
255 - flush();
256 -
257 - $this->streaming_headers_sent = true;
258 - return true;
259 -}
260 -
261 -/**
262 17 * Class constructor
263 18 */
264 19 public function __construct() {
265 20 $this->options = get_option('mxchat_options');
@@ -321,109 +76,23 @@
321 76 // Add to your existing constructor, in the section with other AJAX actions:
322 77 add_action('wp_ajax_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
323 78 add_action('wp_ajax_nopriv_mxchat_track_url_click', array($this, 'mxchat_track_url_click'));
324 79 add_action('wp_ajax_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
325 - add_action('wp_ajax_nopriv_mxchat_track_originating_page', array($this, 'mxchat_track_originating_page'));
326 - // Add chat mode checking actions
327 - add_action('wp_ajax_mxchat_get_current_chat_mode', array($this, 'mxchat_get_current_chat_mode'));
328 - 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 +
329 82
330 - // Nonce refresh for page-cache compatibility (WP Rocket, LiteSpeed, etc.)
331 - add_action('wp_ajax_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
332 - add_action('wp_ajax_nopriv_mxchat_refresh_nonce', array($this, 'mxchat_refresh_nonce'));
333 -
334 - // Auto-email transcript action
335 - add_action('mxchat_send_delayed_transcript', array($this, 'mxchat_send_delayed_transcript'), 10, 1);
336 -
337 83 add_filter('mxchat_check_actions_only', array($this, 'check_actions_for_addons'), 10, 4);
338 84
339 85
340 86 }
341 87
342 -/**
343 - * Return a fresh nonce so cached pages can replace the stale one.
344 - * With `with_settings`, also returns the current behavior-gate settings so
345 - * the widget can correct stale inline-localized values (plan-32db95).
346 - */
347 -public function mxchat_refresh_nonce() {
348 - nocache_headers();
349 - $payload = array('nonce' => wp_create_nonce('mxchat_chat_nonce'));
350 - if (!empty($_REQUEST['with_settings'])) {
351 - $payload['settings'] = $this->get_dynamic_widget_settings(true);
352 - }
353 - wp_send_json_success($payload);
354 -}
355 -
356 -/**
357 - * Behavior-gate settings the widget may re-fetch at runtime (plan-32db95).
358 - *
359 - * Every widget setting ships inline in page HTML via wp_localize_script, so
360 - * full-page caches (host caches, WP Rocket, LiteSpeed, W3TC, FlyingPress,
361 - * WP Super Cache, Cloudflare APO, the browser itself) keep serving a stale
362 - * snapshot after an admin changes a setting. MxChat_Cache_Purge clears the
363 - * caches PHP can reach; this payload covers the rest — the widget requests
364 - * it on first open (via the nonce-refresh endpoints) and merges it over
365 - * `mxchatChat`, the same distrust-cached-HTML pattern the 3.2.7 per-request
366 - * nonce uses.
367 - *
368 - * Behavior gates + labels ONLY — colors stay inline because they're also
369 - * server-inline-styled, and a runtime swap would visibly flash.
370 - *
371 - * Both wp_localize_script blocks merge this exact array, so the inline and
372 - * refreshed payloads cannot drift.
373 - *
374 - * @param bool $fresh Re-read mxchat_options from the DB (endpoint paths)
375 - * instead of trusting the instance copy.
376 - * @return array
377 - */
378 -public function get_dynamic_widget_settings($fresh = false) {
379 - $options = $fresh ? get_option('mxchat_options', array()) : $this->options;
380 - if (!is_array($options)) {
381 - $options = array();
382 - }
383 - return array(
384 - 'model' => isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest',
385 - 'enable_streaming_toggle' => isset($options['enable_streaming_toggle']) ? $options['enable_streaming_toggle'] : 'on',
386 - 'rate_limit_message' => $options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
387 - 'chat_toolbar_toggle' => $options['chat_toolbar_toggle'] ?? 'off',
388 - 'print_button_enabled' => $options['print_button_enabled'] ?? 'on',
389 - 'print_button_label' => esc_html__('Download Transcript', 'mxchat'),
390 - // "Start new chat" header-menu item (plan ac2e81). Default OFF.
391 - 'reset_chat_enabled' => $options['reset_chat_enabled'] ?? 'off',
392 - 'reset_chat_label' => !empty($options['reset_chat_label']) ? esc_html($options['reset_chat_label']) : esc_html__('Start new chat', 'mxchat'),
393 - 'reset_chat_confirm' => esc_html__('Start a new chat? This clears the current conversation.', 'mxchat'),
394 - 'stop_button_label' => esc_html__('Stop response', 'mxchat'),
395 - 'print_header_title' => esc_html(get_bloginfo('name')) . ' — ' . esc_html__('Chat transcript', 'mxchat'),
396 - // Emit 'on'/'off' STRINGS, never booleans: wp_localize_script casts
397 - // scalars to string, and (string) false === '' — which the widget's
398 - // old gate read as enabled (plan-4bba64). The filter keeps its
399 - // boolean contract; only the emitted value is stringified.
400 - 'satisfaction_rating_enabled' => apply_filters(
401 - 'mxchat_satisfaction_rating_enabled',
402 - ($options['satisfaction_rating_enabled'] ?? 'off') === 'on'
403 - ) ? 'on' : 'off',
404 - 'satisfaction_rating_idle_seconds' => max(5, min(600, intval($options['satisfaction_rating_idle_seconds'] ?? 60))),
405 - 'satisfaction_rating_copy' => array(
406 - 'question' => !empty($options['satisfaction_rating_question']) ? esc_html($options['satisfaction_rating_question']) : esc_html__('Was this helpful?', 'mxchat'),
407 - 'helpful' => esc_html__('Helpful', 'mxchat'),
408 - 'not_helpful' => esc_html__('Not helpful', 'mxchat'),
409 - 'dismiss' => esc_html__('Dismiss', 'mxchat'),
410 - 'thanks' => !empty($options['satisfaction_rating_thanks']) ? esc_html($options['satisfaction_rating_thanks']) : esc_html__('Thanks! Anything we should improve? (optional)', 'mxchat'),
411 - 'placeholder' => !empty($options['satisfaction_rating_placeholder']) ? esc_html($options['satisfaction_rating_placeholder']) : esc_html__('Tell us what could be better…', 'mxchat'),
412 - 'send' => esc_html__('Send', 'mxchat'),
413 - 'skip' => esc_html__('Skip', 'mxchat'),
414 - 'saved' => !empty($options['satisfaction_rating_saved']) ? esc_html($options['satisfaction_rating_saved']) : esc_html__('Thanks for the feedback.', 'mxchat'),
415 - ),
416 - );
417 -}
418 -
419 88 // In your core plugin's check_actions_for_addons method:
420 89 public function check_actions_for_addons($default, $message, $user_id, $session_id) {
421 - //error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
90 + error_log('MxChat Core: check_actions_for_addons called with message: ' . $message);
422 91
423 92 $result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
424 93
425 - //error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
94 + error_log('MxChat Core: Intent check result = ' . ($result === false ? 'false' : 'true'));
426 95
427 96 return $result;
428 97 }
429 98
@@ -439,22 +108,8 @@
439 108 wp_die();
440 109 }
441 110
442 111 $session_id = sanitize_text_field($_POST['session_id']);
443 -
444 - // SECURITY FIX: Verify session ownership before retrieving data
445 - // If IP/user changed, signal frontend to reset session instead of blocking
446 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
447 -
448 - // Check if this session has an owner recorded
449 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
450 -
451 - // Update session owner if it changed (e.g. IP changed due to network switch)
452 - // The session ID itself is the authentication — if the client has it, they own it
453 - if (!$session_owner || $session_owner !== $current_user_identifier) {
454 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
455 - }
456 -
457 112 $history = get_option("mxchat_history_{$session_id}", []); // Retrieve stored history
458 113 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai'); // Get current chat mode
459 114
460 115 if (empty($history)) {
@@ -471,25 +126,11 @@
471 126 'chat_mode' => $chat_mode
472 127 ]);
473 128 wp_die();
474 129 }
475 -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) {
476 132 $history = get_option("mxchat_history_{$session_id}", []);
477 -
478 - // Check persistence setting - when OFF, only include messages from current page load
479 - $options = get_option('mxchat_options', []);
480 - $persistence_enabled = isset($options['chat_persistence_toggle']) && $options['chat_persistence_toggle'] === 'on';
481 -
482 - // Filter history when persistence is OFF to match what the user sees
483 - if (!$persistence_enabled && $session_start_timestamp > 0) {
484 - $history = array_filter($history, function($entry) use ($session_start_timestamp) {
485 - // Include messages from this page load onwards
486 - return isset($entry['timestamp']) && $entry['timestamp'] >= $session_start_timestamp;
487 - });
488 - // Re-index array after filtering
489 - $history = array_values($history);
490 - }
491 -
492 133 $formatted_history = [];
493 134
494 135 // Adjusted for code-heavy conversations
495 136 $max_tokens = 120000; // Context window size
@@ -563,17 +204,8 @@
563 204
564 205 public function register_routes() {
565 206 //error_log(esc_html__('Registering MxChat REST routes', 'mxchat'));
566 207
567 - // Per-request chat-send nonce endpoint — issues a fresh nonce on demand
568 - // so the chat widget never depends on a stale nonce embedded in cached HTML.
569 - // Public (no auth), rate-limited (1 call / IP / second via a transient).
570 - register_rest_route('mxchat/v1', '/nonce', [
571 - 'methods' => 'GET',
572 - 'callback' => [$this, 'mxchat_issue_chat_send_nonce'],
573 - 'permission_callback' => '__return_true',
574 - ]);
575 -
576 208 register_rest_route('mxchat/v1', '/stream', [
577 209 'methods' => 'GET',
578 210 'callback' => [$this, 'mxchat_stream_events'],
579 211 'permission_callback' => [$this, 'verify_chat_session'],
@@ -596,105 +228,12 @@
596 228 'callback' => [$this, 'handle_slack_messages'],
597 229 'permission_callback' => [$this, 'verify_slack_request'],
598 230 ]);
599 231
600 - // Telegram webhook endpoint
601 - register_rest_route('mxchat/v1', '/telegram-webhook', [
602 - 'methods' => 'POST',
603 - 'callback' => [$this, 'handle_telegram_webhook'],
604 - 'permission_callback' => [$this, 'verify_telegram_request'],
605 - ]);
606 -
607 232 //error_log(esc_html__('MxChat REST routes registered', 'mxchat'));
608 233 }
609 234
610 235 /**
611 - * Issue a fresh per-request nonce for chat-send. Returned to the widget which
612 - * caches it for the session and includes it on every chat-send / stream-send /
613 - * upload call. By moving the nonce out of inline `window.mxchatChat = {...}` HTML
614 - * we eliminate the entire class of "first-message Access denied" failures that
615 - * plague WP installs behind a full-page cache (WP Rocket, LiteSpeed, FlyingPress,
616 - * W3 Total Cache, Cloudflare APO) — the nonce is never cached because it never
617 - * lives in the HTML body.
618 - *
619 - * Public endpoint. Rate-limited to 1 call / IP / 1s via a transient so a single
620 - * client browser can't be used to flood the nonce-issuance path.
621 - *
622 - * Nonce action: `mxchat_chat_send` (new). The chat-send AJAX handlers accept
623 - * BOTH this action AND the legacy `mxchat_chat_nonce` action for a 30-day
624 - * backwards-compat window so cached pages still in users' browsers don't break
625 - * mid-session.
626 - *
627 - * @since 3.2.7
628 - */
629 -public function mxchat_issue_chat_send_nonce(WP_REST_Request $request) {
630 - $ip = '';
631 - if (!empty($_SERVER['REMOTE_ADDR'])) {
632 - $ip = preg_replace('#[^0-9a-fA-F:\.]#', '', wp_unslash((string) $_SERVER['REMOTE_ADDR']));
633 - }
634 - if ($ip !== '') {
635 - // Best-effort rate limit. WP transients with sub-second TTL are racy
636 - // (parallel bursts can squeak through before set_transient completes);
637 - // we use 2s to make the gate slightly more reliable. Real production
638 - // rate-limiting at sub-second granularity needs Redis or DB row locks
639 - // — out of scope for this endpoint, which is already cheap.
640 - $key = 'mxchat_nonce_rl_' . md5($ip);
641 - if (get_transient($key)) {
642 - return new WP_REST_Response(array(
643 - 'error' => 'rate_limited',
644 - 'message' => __('Too many nonce requests. Try again shortly.', 'mxchat'),
645 - ), 429);
646 - }
647 - set_transient($key, 1, 2);
648 - }
649 -
650 - // The widget calls this endpoint without an X-WP-Nonce header, so WordPress does not
651 - // honor the auth cookie and the request runs as uid=0 even for logged-in users. That
652 - // makes wp_create_nonce() bind the nonce to uid=0, which then fails wp_verify_nonce()
653 - // at admin-ajax (which runs as the real uid) -> logged-in users get a 403 on upload.
654 - // Resolve the real user from the logged_in cookie so the nonce binds to the correct uid.
655 - if ( ! is_user_logged_in() ) {
656 - $maybe_uid = wp_validate_auth_cookie( '', 'logged_in' );
657 - if ( $maybe_uid ) {
658 - wp_set_current_user( $maybe_uid );
659 - }
660 - }
661 -
662 - $payload = array(
663 - 'nonce' => wp_create_nonce('mxchat_chat_send'),
664 - 'expires_in' => 86400, // WP nonces live 24h; widget caches for 12h conservatively.
665 - );
666 -
667 - // plan-32db95: the widget's first-open refresh asks for current behavior
668 - // settings in the same round-trip, so stale inline-localized values on
669 - // cached pages get corrected without a second request. All values in
670 - // this payload already ship in public page HTML — nothing sensitive.
671 - if ($request->get_param('with_settings')) {
672 - $payload['settings'] = $this->get_dynamic_widget_settings(true);
673 - }
674 -
675 - return new WP_REST_Response($payload, 200);
676 -}
677 -
678 -/**
679 - * Verify a chat-send nonce. Accepts BOTH the new `mxchat_chat_send` action
680 - * (issued by /wp-json/mxchat/v1/nonce) AND the legacy `mxchat_chat_nonce`
681 - * action (inline-localized in older cached HTML). The legacy acceptance is
682 - * a 30-day backwards-compat window — to be removed in a follow-up release
683 - * after 2026-06-27.
684 - *
685 - * @param string $posted_nonce
686 - * @return bool
687 - */
688 -public static function mxchat_verify_chat_send_nonce($posted_nonce) {
689 - if (!is_string($posted_nonce) || $posted_nonce === '') {
690 - return false;
691 - }
692 - return (bool) wp_verify_nonce($posted_nonce, 'mxchat_chat_send')
693 - || (bool) wp_verify_nonce($posted_nonce, 'mxchat_chat_nonce');
694 -}
695 -
696 -/**
697 236 * Verify valid chat session
698 237 */
699 238 public function verify_chat_session($request) {
700 239 $session_id = $request->get_param('session_id');
@@ -730,11 +269,10 @@
730 269 //error_log(esc_html__('Slack request timestamp too old', 'mxchat'));
731 270 return false;
732 271 }
733 272
734 - // Get raw request body from the WP_REST_Request object
735 - // (php://input may already be consumed by WordPress at this point)
736 - $request_body = $request->get_body();
273 + // Get raw request body
274 + $request_body = file_get_contents('php://input');
737 275
738 276 // Create the signature base string
739 277 $sig_basestring = "v0:{$timestamp}:{$request_body}";
740 278
@@ -744,42 +282,8 @@
744 282 // Compare signatures
745 283 return hash_equals($my_signature, $slack_signature);
746 284 }
747 285
748 -/**
749 - * Verify request is coming from Telegram.
750 - *
751 - * @param WP_REST_Request $request
752 - * @return bool True if valid, false otherwise.
753 - */
754 -public function verify_telegram_request($request) {
755 - $secret_token = $this->options['telegram_webhook_secret'] ?? '';
756 -
757 - //error_log('[MxChat Telegram DEBUG] verify_telegram_request called');
758 - //error_log('[MxChat Telegram DEBUG] Stored secret: ' . (empty($secret_token) ? 'EMPTY' : substr($secret_token, 0, 10) . '...'));
759 -
760 - if (empty($secret_token)) {
761 - // If no secret is configured, allow the request (for initial setup)
762 - //error_log('[MxChat Telegram DEBUG] No secret configured, allowing request');
763 - return true;
764 - }
765 -
766 - // Telegram sends the secret token in the X-Telegram-Bot-Api-Secret-Token header
767 - $request_token = $request->get_header('X-Telegram-Bot-Api-Secret-Token');
768 -
769 - //error_log('[MxChat Telegram DEBUG] Request token: ' . (empty($request_token) ? 'EMPTY' : substr($request_token, 0, 10) . '...'));
770 -
771 - if (empty($request_token)) {
772 - //error_log('[MxChat Telegram DEBUG] Request rejected: No token in header');
773 - return false;
774 - }
775 -
776 - // Timing-safe comparison
777 - $result = hash_equals($secret_token, $request_token);
778 - //error_log('[MxChat Telegram DEBUG] Token comparison result: ' . ($result ? 'MATCH' : 'MISMATCH'));
779 - return $result;
780 -}
781 -
782 286 public function mxchat_stream_events(WP_REST_Request $request) {
783 287 header('Content-Type: text/event-stream');
784 288 header('Cache-Control: no-cache');
785 289 header('Connection: keep-alive');
@@ -813,9 +317,9 @@
813 317
814 318
815 319
816 320
817 -private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null, $rag_context = null) {
321 +private function mxchat_save_chat_message($session_id, $role, $message, $originating_page = null) {
818 322 global $wpdb;
819 323 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
820 324 //error_log("[DEBUG] mxchat_save_chat_message -> START for session_id: {$session_id}, role: {$role}");
821 325
@@ -827,26 +331,14 @@
827 331 $session_id
828 332 ));
829 333 $is_new_session = ($existing_messages == 0);
830 334
831 - // Log for debugging
335 + // NEW: Log for debugging
832 336 if ($is_new_session) {
833 - //error_log("[DEBUG] This is a NEW session - first message");
337 + error_log("[DEBUG] This is a NEW session - first message");
834 338 }
835 339 }
836 340
837 - // SECURITY FIX: Set session ownership for new sessions
838 - if ($is_new_session && $role === 'user') {
839 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
840 - $session_owner_key = "mxchat_session_owner_{$session_id}";
841 -
842 - // Only set ownership if not already set
843 - if (!get_option($session_owner_key)) {
844 - update_option($session_owner_key, $current_user_identifier, 'no');
845 - //error_log("[DEBUG] Set session ownership for {$session_id} to {$current_user_identifier}");
846 - }
847 - }
848 -
849 341 // 1) Extract agent name if present
850 342 $agent_name = '';
851 343 if (preg_match('/^Agent: (.*?) - /', $message, $matches)) {
852 344 $agent_name = $matches[1];
@@ -878,33 +370,18 @@
878 370 $email_option_key = "mxchat_email_{$session_id}";
879 371 $saved_email = get_option($email_option_key);
880 372 //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for email_option_key: {$email_option_key}, found: {$saved_email}");
881 373
882 - // Check for a saved name in wp_options
883 - $name_option_key = "mxchat_name_{$session_id}";
884 - $saved_name = get_option($name_option_key);
885 - //error_log("[DEBUG] mxchat_save_chat_message -> Checking wp_options for name_option_key: {$name_option_key}, found: {$saved_name}");
886 -
887 - // If found, update DB user_email and user_name
888 - if ($saved_email || $saved_name) {
889 - $update_data = [];
890 - if ($saved_email) {
891 - $update_data['user_email'] = $saved_email;
892 - }
893 - if ($saved_name) {
894 - $update_data['user_name'] = $saved_name;
895 - }
896 -
897 - if (!empty($update_data)) {
898 - $update_res = $wpdb->update(
899 - $table_name,
900 - $update_data,
901 - ['session_id' => $session_id],
902 - array_fill(0, count($update_data), '%s'),
903 - ['%s']
904 - );
905 - //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email/user_name update for session_id {$session_id}. update_res: {$update_res}");
906 - }
374 + // If found, update DB user_email
375 + if ($saved_email) {
376 + $update_res = $wpdb->update(
377 + $table_name,
378 + ['user_email' => $saved_email],
379 + ['session_id' => $session_id],
380 + ['%s'],
381 + ['%s']
382 + );
383 + //error_log("[DEBUG] mxchat_save_chat_message -> Attempted DB user_email update for session_id {$session_id}. update_res: {$update_res}");
907 384 }
908 385
909 386 // 7) Save to session history in wp_options
910 387 $history_key = "mxchat_history_{$session_id}";
@@ -923,9 +400,8 @@
923 400 $insert_data = [
924 401 'user_id' => $user_id,
925 402 'user_identifier'=> $user_identifier,
926 403 'user_email' => $saved_email ?: $user_email,
927 - 'user_name' => $saved_name ?: '', // Add name to insert data
928 404 'session_id' => $session_id,
929 405 'role' => $role,
930 406 'message' => $message,
931 407 'timestamp' => current_time('mysql', 1),
@@ -942,9 +418,9 @@
942 418 if ($originating_page && !empty($originating_page['url'])) {
943 419 $insert_data['originating_page_url'] = $originating_page['url'];
944 420 $insert_data['originating_page_title'] = $originating_page['title'] ?? '';
945 421
946 - //error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
422 + error_log("[DEBUG] Setting originating page from parameter: " . $originating_page['url']);
947 423 }
948 424 // Otherwise check if it's stored in the instance property
949 425 else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
950 426 $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
@@ -949,13 +425,12 @@
949 425 else if (isset($this->pending_originating_page) && !empty($this->pending_originating_page['url'])) {
950 426 $insert_data['originating_page_url'] = $this->pending_originating_page['url'];
951 427 $insert_data['originating_page_title'] = $this->pending_originating_page['title'] ?? '';
952 428
953 - //error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
429 + error_log("[DEBUG] Setting originating page from pending_originating_page: " . $this->pending_originating_page['url']);
954 430
955 - // Clear after using (= null, not unset(): unset() undeclares the property
956 - // and the next assignment recreates it dynamic, re-triggering the PHP 8.2 deprecation)
957 - $this->pending_originating_page = null;
431 + // Clear after using
432 + unset($this->pending_originating_page);
958 433 }
959 434 // Fallback to HTTP_REFERER if nothing else is available
960 435 else if (isset($_SERVER['HTTP_REFERER'])) {
961 436 $referer_url = esc_url_raw($_SERVER['HTTP_REFERER']);
@@ -971,9 +446,9 @@
971 446 $title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
972 447 $insert_data['originating_page_title'] = ucwords(trim($title));
973 448 }
974 449
975 - //error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
450 + error_log("[DEBUG] Setting originating page from HTTP_REFERER: " . $referer_url);
976 451 }
977 452
978 453 // Store for this session so all messages have the same originating page
979 454 if (!empty($insert_data['originating_page_url'])) {
@@ -990,17 +465,9 @@
990 465 $insert_data['originating_page_title'] = $stored_originating['title'] ?? '';
991 466 }
992 467 }
993 468 }
994 -
995 - // Add RAG context if provided (for bot messages)
996 - if ($rag_context !== null && $role === 'bot') {
997 - $rag_context_column_exists = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'rag_context'");
998 - if ($rag_context_column_exists) {
999 - $insert_data['rag_context'] = is_array($rag_context) ? wp_json_encode($rag_context) : $rag_context;
1000 - }
1001 - }
1002 -
469 +
1003 470 $wpdb->insert($table_name, $insert_data);
1004 471 //error_log("[DEBUG] mxchat_save_chat_message -> Inserted message into DB. row_id: {$wpdb->insert_id}, data: " . print_r($insert_data, true));
1005 472
1006 473 // 9) Send notification email if this is the first user message in a new session
@@ -1011,13 +478,8 @@
1011 478 'ip' => $_SERVER['REMOTE_ADDR']
1012 479 ));
1013 480 }
1014 481
1015 - // 10) Schedule delayed transcript email if enabled and message is from user
1016 - if ($wpdb->insert_id && $role === 'user') {
1017 - $this->schedule_delayed_transcript_email($session_id);
1018 - }
1019 -
1020 482 //error_log("[DEBUG] mxchat_save_chat_message -> END for session_id: {$session_id}");
1021 483 return $message_id;
1022 484 }
1023 485
@@ -1064,202 +526,13 @@
1064 526 // Send email
1065 527 return wp_mail($to, $subject, $message);
1066 528 }
1067 529
1068 -/**
1069 - * Schedule delayed transcript email for a session
1070 - * Reschedules if a new user message is received
1071 - */
1072 -private function schedule_delayed_transcript_email($session_id) {
1073 - $options = get_option('mxchat_transcripts_options');
1074 -
1075 - // Check if auto-email is enabled
1076 - if (empty($options['mxchat_auto_email_transcript_enabled'])) {
1077 - return;
1078 - }
1079 -
1080 - // Get notification email
1081 - $email = !empty($options['mxchat_notification_email']) ?
1082 - $options['mxchat_notification_email'] :
1083 - get_option('admin_email');
1084 -
1085 - if (!is_email($email)) {
1086 - return;
1087 - }
1088 -
1089 - // Get delay in minutes (default 30)
1090 - $delay_minutes = isset($options['mxchat_auto_email_transcript_delay']) ?
1091 - intval($options['mxchat_auto_email_transcript_delay']) : 30;
1092 -
1093 - // Clear any existing scheduled event for this session
1094 - $hook = 'mxchat_send_delayed_transcript';
1095 - $args = array($session_id);
1096 - $timestamp = wp_next_scheduled($hook, $args);
1097 -
1098 - if ($timestamp) {
1099 - wp_unschedule_event($timestamp, $hook, $args);
1100 - }
1101 -
1102 - // Schedule new event
1103 - $schedule_time = time() + ($delay_minutes * 60);
1104 - wp_schedule_single_event($schedule_time, $hook, $args);
1105 -}
1106 -
1107 -/**
1108 - * Check if chat messages contain contact information (email or phone number)
1109 - *
1110 - * @param array $messages Array of message objects with 'message' property
1111 - * @param object|null $session_data Session data object with user_email property
1112 - * @return bool True if contact info found, false otherwise
1113 - */
1114 -private function chat_contains_contact_info($messages, $session_data = null) {
1115 - // Check if session already has a stored email
1116 - if ($session_data && !empty($session_data->user_email)) {
1117 - return true;
1118 - }
1119 -
1120 - // Email regex pattern
1121 - $email_pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
1122 -
1123 - // Phone number patterns (covers various formats including international, WhatsApp style)
1124 - // Matches: +1234567890, (123) 456-7890, 123-456-7890, 123.456.7890, 1234567890, +1 234 567 8900, etc.
1125 - $phone_pattern = '/(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{2,4}[-.\s]?\d{2,4}(?:[-.\s]?\d{1,4})?/';
1126 -
1127 - // Only check user messages (not assistant responses)
1128 - foreach ($messages as $msg) {
1129 - if ($msg->role !== 'user') {
1130 - continue;
1131 - }
1132 -
1133 - $message_text = $msg->message;
1134 -
1135 - // Check for email
1136 - if (preg_match($email_pattern, $message_text)) {
1137 - return true;
1138 - }
1139 -
1140 - // Check for phone number (must be at least 7 digits total to avoid false positives)
1141 - if (preg_match($phone_pattern, $message_text, $matches)) {
1142 - // Count actual digits to avoid matching short numbers
1143 - $digits_only = preg_replace('/\D/', '', $matches[0]);
1144 - if (strlen($digits_only) >= 7) {
1145 - return true;
1146 - }
1147 - }
1148 - }
1149 -
1150 - return false;
1151 -}
1152 -
1153 -/**
1154 - * Send the delayed transcript email with .txt attachment
1155 - */
1156 -public function mxchat_send_delayed_transcript($session_id) {
1157 - global $wpdb;
1158 -
1159 - $options = get_option('mxchat_transcripts_options');
1160 -
1161 - // Get notification email
1162 - $to = !empty($options['mxchat_notification_email']) ?
1163 - $options['mxchat_notification_email'] :
1164 - get_option('admin_email');
1165 -
1166 - if (!is_email($to)) {
1167 - return false;
1168 - }
1169 -
1170 - // Get all messages for this session
1171 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1172 - $messages = $wpdb->get_results($wpdb->prepare(
1173 - "SELECT role, message, timestamp FROM {$table_name}
1174 - WHERE session_id = %s
1175 - ORDER BY timestamp ASC",
1176 - $session_id
1177 - ));
1178 -
1179 - if (empty($messages)) {
1180 - return false;
1181 - }
1182 -
1183 - // Get session metadata
1184 - $sessions_table = $wpdb->prefix . 'mxchat_sessions';
1185 - $session_data = $wpdb->get_row($wpdb->prepare(
1186 - "SELECT * FROM {$sessions_table} WHERE session_id = %s",
1187 - $session_id
1188 - ));
1189 -
1190 - // Check if contact info is required and if it's present
1191 - $require_contact = !empty($options['mxchat_auto_email_transcript_require_contact']);
1192 - if ($require_contact && !$this->chat_contains_contact_info($messages, $session_data)) {
1193 - // Contact info required but not found - skip sending
1194 - return false;
1195 - }
1196 -
1197 - // Build transcript content
1198 - $transcript_content = "Chat Transcript\n";
1199 - $transcript_content .= "================\n\n";
1200 - $transcript_content .= "Session ID: " . $session_id . "\n";
1201 -
1202 - if ($session_data) {
1203 - $transcript_content .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
1204 - $transcript_content .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
1205 - $transcript_content .= "Started: " . $session_data->created_at . "\n";
1206 - }
1207 -
1208 - $transcript_content .= "\n" . str_repeat("=", 50) . "\n\n";
1209 -
1210 - // Add messages
1211 - foreach ($messages as $msg) {
1212 - $role_label = ($msg->role === 'user') ? 'User' : 'Assistant';
1213 - $transcript_content .= "[{$msg->timestamp}] {$role_label}:\n";
1214 - $transcript_content .= $msg->message . "\n\n";
1215 - }
1216 -
1217 - // Create temporary file for attachment using WP_Filesystem
1218 - $upload_dir = wp_upload_dir();
1219 - $temp_file = $upload_dir['basedir'] . '/mxchat-transcript-' . $session_id . '.txt';
1220 - global $wp_filesystem;
1221 - if (empty($wp_filesystem)) {
1222 - require_once ABSPATH . 'wp-admin/includes/file.php';
1223 - WP_Filesystem();
1224 - }
1225 - $wp_filesystem->put_contents($temp_file, $transcript_content, FS_CHMOD_FILE);
1226 -
1227 - // Prepare email
1228 - $subject = sprintf('[%s] Chat Transcript - Session %s', get_bloginfo('name'), substr($session_id, 0, 8));
1229 -
1230 - $message = "Please find attached the full chat transcript.\n\n";
1231 - $message .= "Session ID: {$session_id}\n";
1232 -
1233 - if ($session_data) {
1234 - $message .= "User: " . ($session_data->user_identifier ?: 'Guest') . "\n";
1235 - $message .= "Email: " . ($session_data->user_email ?: 'Not provided') . "\n";
1236 - }
1237 -
1238 - $message .= "\nView online: " . admin_url('admin.php?page=mxchat-transcripts');
1239 -
1240 - // Send email with attachment
1241 - $attachments = array($temp_file);
1242 - $result = wp_mail($to, $subject, $message, '', $attachments);
1243 -
1244 - // Clean up temporary file
1245 - if (file_exists($temp_file)) {
1246 - unlink($temp_file);
1247 - }
1248 -
1249 - return $result;
1250 -}
1251 -
1252 -
1253 -
1254 530 public function mxchat_handle_save_email_and_response() {
1255 531 //error_log('[DEBUG] ---------- mxchat_handle_save_email_and_response START ----------');
1256 - //error_log('DEBUG: POST data: ' . print_r($_POST, true));
1257 532
1258 - nocache_headers();
1259 -
1260 533 // Validate nonce
1261 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
534 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
1262 535 //error_log(esc_html__('[ERROR] Invalid nonce in mxchat_handle_save_email_and_response', 'mxchat'));
1263 536 wp_send_json_error(['message' => esc_html__('Invalid nonce.', 'mxchat')]);
1264 537 wp_die();
1265 538 }
@@ -1265,41 +538,22 @@
1265 538 }
1266 539
1267 540 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1268 541 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
1269 - $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
1270 542
1271 - //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}, name: {$name}");
543 + //error_log("[DEBUG] handle_save_email_and_response -> session_id: {$session_id}, email: {$email}");
1272 544
1273 - if (empty($session_id) || $session_id === 'null' || empty($email)) {
545 + if (empty($session_id) || empty($email)) {
1274 546 //error_log("[ERROR] Missing session_id or email: session_id={$session_id}, email={$email}");
1275 547 wp_send_json_error(['message' => esc_html__('Session ID or email is missing.', 'mxchat')]);
1276 548 wp_die();
1277 549 }
1278 550
1279 - // Validate name if provided (check if name field is enabled and name is required)
1280 - $options = get_option('mxchat_options', []);
1281 - $name_field_enabled = isset($options['enable_name_field']) &&
1282 - ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
1283 -
1284 - if ($name_field_enabled && (empty($name) || strlen(trim($name)) < 2 || strlen(trim($name)) > 100)) {
1285 - //error_log("[ERROR] Invalid name: {$name} (enabled: {$name_field_enabled})");
1286 - wp_send_json_error(['message' => esc_html__('Name must be between 2 and 100 characters.', 'mxchat')]);
1287 - wp_die();
1288 - }
551 + // 1) Always store in wp_options
552 + $option_key = "mxchat_email_{$session_id}";
553 + update_option($option_key, $email);
554 + //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$option_key} => {$email}");
1289 555
1290 - // 1) Always store email in wp_options
1291 - $email_option_key = "mxchat_email_{$session_id}";
1292 - update_option($email_option_key, $email, 'no');
1293 - //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$email_option_key} => {$email}");
1294 -
1295 - // Store name in wp_options if provided
1296 - if (!empty($name)) {
1297 - $name_option_key = "mxchat_name_{$session_id}";
1298 - update_option($name_option_key, $name, 'no');
1299 - //error_log("[DEBUG] handle_save_email_and_response -> updated option: {$name_option_key} => {$name}");
1300 - }
1301 -
1302 556 // 2) (Optional) Also store in DB if a row already exists
1303 557 global $wpdb;
1304 558 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1305 559
@@ -1309,30 +563,21 @@
1309 563
1310 564 //error_log("[DEBUG] handle_save_email_and_response -> session_count for {$session_id}: {$session_count} (SQL: {$sql})");
1311 565
1312 566 if ($session_count) {
1313 - // Update both user_email and user_name if row(s) exist
1314 - if (!empty($name)) {
1315 - $update_sql = $wpdb->prepare(
1316 - "UPDATE {$table_name} SET user_email = %s, user_name = %s WHERE session_id = %s",
1317 - $email,
1318 - $name,
1319 - $session_id
1320 - );
1321 - } else {
1322 - $update_sql = $wpdb->prepare(
1323 - "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
1324 - $email,
1325 - $session_id
1326 - );
1327 - }
567 + // Update user_email if row(s) exist
568 + $update_sql = $wpdb->prepare(
569 + "UPDATE {$table_name} SET user_email = %s WHERE session_id = %s",
570 + $email,
571 + $session_id
572 + );
1328 573 $wpdb->query($update_sql);
1329 574 //error_log("[DEBUG] handle_save_email_and_response -> DB updated: {$update_sql}");
1330 575 } else {
1331 - //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email/name is only in wp_options.");
576 + //error_log("[INFO] handle_save_email_and_response -> No DB entry for {$session_id}, so email is only in wp_options.");
1332 577 }
1333 578
1334 - // Provide success response (same as original)
579 + // Provide success response
1335 580 $bot_message = __('Thanks for providing your email! You can continue chatting now.', 'mxchat');
1336 581 //error_log("[DEBUG] handle_save_email_and_response -> success, returning bot_message: {$bot_message}");
1337 582 wp_send_json_success(['message' => $bot_message]);
1338 583 wp_die();
@@ -1340,17 +585,15 @@
1340 585
1341 586 public function mxchat_check_email_provided() {
1342 587 //error_log('[DEBUG] ---------- mxchat_check_email_provided START ----------');
1343 588
1344 - nocache_headers();
1345 -
1346 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
589 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
1347 590 //error_log('[ERROR] Invalid nonce in mxchat_check_email_provided');
1348 591 wp_send_json_error(['message' => esc_html__('Invalid nonce', 'mxchat')]);
1349 592 }
1350 593
1351 594 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1352 - if (empty($session_id) || $session_id === 'null') {
595 + if (empty($session_id)) {
1353 596 //error_log('[ERROR] No session ID provided in mxchat_check_email_provided');
1354 597 wp_send_json_error(['message' => esc_html__('No session ID provided', 'mxchat')]);
1355 598 }
1356 599
@@ -1357,109 +600,49 @@
1357 600 // Check if the user is logged in
1358 601 if (is_user_logged_in()) {
1359 602 $current_user = wp_get_current_user();
1360 603 //error_log("[DEBUG] User is logged in as {$current_user->user_email}");
1361 -
1362 - // Get user's display name for logged in users
1363 - $user_name = !empty($current_user->display_name) ? $current_user->display_name :
1364 - (!empty($current_user->first_name) ? $current_user->first_name : '');
1365 -
1366 - $response_data = ['logged_in' => true, 'email' => $current_user->user_email];
1367 - if (!empty($user_name)) {
1368 - $response_data['name'] = $user_name;
1369 - }
1370 -
1371 - wp_send_json_success($response_data);
604 + wp_send_json_success(['logged_in' => true, 'email' => $current_user->user_email]);
1372 605 }
1373 606
1374 - // Check if name field is required
1375 - $options = get_option('mxchat_options', []);
1376 - $name_field_enabled = isset($options['enable_name_field']) &&
1377 - ($options['enable_name_field'] === '1' || $options['enable_name_field'] === 'on');
607 + $option_key = "mxchat_email_{$session_id}";
608 + $stored_email = get_option($option_key, '');
1378 609
1379 - $email_option_key = "mxchat_email_{$session_id}";
1380 - $stored_email = get_option($email_option_key, '');
1381 -
1382 - // Check for stored name
1383 - $name_option_key = "mxchat_name_{$session_id}";
1384 - $stored_name = get_option($name_option_key, '');
610 + //error_log("[DEBUG] mxchat_check_email_provided -> Checking option: {$option_key}, found: {$stored_email}");
1385 611
1386 - //error_log("[DEBUG] mxchat_check_email_provided -> Checking email option: {$email_option_key}, found: {$stored_email}");
1387 - //error_log("[DEBUG] mxchat_check_email_provided -> Checking name option: {$name_option_key}, found: {$stored_name}, required: " . ($name_field_enabled ? 'yes' : 'no'));
1388 -
1389 - // Check if we have email and name (if name is required)
1390 - $has_required_info = !empty($stored_email);
1391 -
1392 - if ($name_field_enabled) {
1393 - $has_required_info = $has_required_info && !empty($stored_name);
1394 - }
1395 -
1396 - if ($has_required_info) {
1397 - //error_log("[DEBUG] mxchat_check_email_provided -> Required info found, returning success");
1398 -
1399 - $response_data = ['email' => $stored_email];
1400 - if (!empty($stored_name)) {
1401 - $response_data['name'] = $stored_name;
1402 - }
1403 -
1404 - wp_send_json_success($response_data);
612 + if (!empty($stored_email)) {
613 + //error_log("[DEBUG] mxchat_check_email_provided -> Email found, returning success");
614 + wp_send_json_success(['email' => $stored_email]);
1405 615 } else {
1406 - //error_log("[DEBUG] mxchat_check_email_provided -> Required info missing, returning error");
616 + //error_log("[DEBUG] mxchat_check_email_provided -> No email found, returning error");
1407 617 wp_send_json_error(['message' => esc_html__('No email found', 'mxchat')]);
1408 618 }
1409 619 }
1410 620
1411 -/**
1412 - * Send error response in appropriate format based on streaming mode
1413 - * ADDED: Helper method to consistently handle errors in both streaming and non-streaming modes
1414 - *
1415 - * @param string $error_message The error message to display
1416 - * @param string $error_code Optional error code for debugging
1417 - */
1418 -private function send_error_response($error_message, $error_code = 'api_error') {
1419 - if ($this->is_streaming) {
1420 - echo "data: " . json_encode([
1421 - 'error' => true,
1422 - 'error_message' => $error_message,
1423 - 'error_code' => $error_code,
1424 - 'text' => $error_message,
1425 - 'message' => $error_message
1426 - ]) . "\n\n";
1427 - echo "data: [DONE]\n\n";
1428 - flush();
1429 - } else {
1430 - wp_send_json_error([
1431 - 'error_message' => $error_message,
1432 - 'error_code' => $error_code
1433 - ]);
1434 - }
1435 - wp_die();
1436 -}
1437 -
1438 621 public function mxchat_handle_chat_request() {
1439 622 global $wpdb;
1440 623
1441 - // Debug: Log incoming bot_id
1442 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
1443 - //error_log("=== MXCHAT DEBUG: Starting chat request ===");
1444 - //error_log("MXCHAT DEBUG: Bot ID received: " . $bot_id);
624 + // NEW: Check if this is a streaming request
625 + $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat';
1445 626
1446 - // Get bot-specific options
1447 - $bot_options = $this->get_bot_options($bot_id);
1448 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
627 + // NEW: Set streaming headers if needed
628 + if ($is_streaming) {
629 + // Disable output buffering
630 + while (ob_get_level()) {
631 + ob_end_flush(); // Changed from ob_end_clean()
632 + }
633 +
634 + // Set headers for SSE
635 + header('Content-Type: text/event-stream');
636 + header('Cache-Control: no-cache');
637 + header('Connection: keep-alive');
638 + header('X-Accel-Buffering: no');
639 +
640 + // Add these new lines:
641 + ob_implicit_flush(true);
642 + flush();
643 + }
1449 644
1450 - // Check if this is a streaming request
1451 - // Allow force_streaming_test parameter to bypass the setting check (for admin compatibility testing)
1452 - $force_streaming_test = isset($_POST['force_streaming_test']) && $_POST['force_streaming_test'] === '1' && current_user_can('administrator');
1453 - $is_streaming = isset($_POST['action']) && $_POST['action'] === 'mxchat_stream_chat' &&
1454 - ($force_streaming_test || (isset($current_options['enable_streaming_toggle']) && $current_options['enable_streaming_toggle'] === 'on'));
1455 -
1456 - // ADDED: Store streaming state in class property for use in private methods
1457 - $this->is_streaming = $is_streaming;
1458 -
1459 - // NOTE: Streaming headers are now set later via setup_streaming_headers()
1460 - // This allows actions/forms to return JSON responses without header conflicts
1461 -
1462 645 // Check if MX Chat Moderation is active
1463 646 if (class_exists('MX_Chat_Moderation')) {
1464 647 // Get user email and IP
1465 648 $user_email = '';
@@ -1496,12 +679,8 @@
1496 679 }
1497 680
1498 681 $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
1499 682 $this->productCardHtml = '';
1500 - // Reset the per-turn function-calling UI capture (plan 48a57a).
1501 - $this->fc_ui_html = '';
1502 - $this->fc_ui_images = array();
1503 - $this->fc_ui_captured = false;
1504 683
1505 684 // Get the actual WordPress user ID if logged in
1506 685 $is_logged_in = is_user_logged_in();
1507 686 if ($is_logged_in) {
@@ -1528,31 +707,13 @@
1528 707
1529 708 // Rest of your existing code...
1530 709 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
1531 710
1532 - // Treat the literal strings 'null' / 'undefined' as missing too. Browser edge cases
1533 - // (Safari ITP, private mode, cross-origin iframes with partitioned storage) can cause
1534 - // the frontend FormData.append() to stringify a null session_id into the literal
1535 - // "null", which would otherwise pass empty() and pollute the transcripts table with
1536 - // ghost sessions that group every visitor's first message under one row.
1537 - if ($session_id === 'null' || $session_id === 'undefined') {
1538 - $session_id = '';
1539 - }
1540 -
1541 711 if (empty($session_id)) {
1542 712 wp_send_json_error(esc_html__('Session ID is missing.', 'mxchat'));
1543 713 wp_die();
1544 714 }
1545 715
1546 - // Update session owner if it changed (e.g. IP changed due to network switch)
1547 - // The session ID itself is the authentication — if the client has it, they own it
1548 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
1549 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
1550 -
1551 - if (!$session_owner || $session_owner !== $current_user_identifier) {
1552 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
1553 - }
1554 -
1555 716 // Validate and sanitize the incoming message
1556 717 if (empty($_POST['message'])) {
1557 718 wp_send_json_error(esc_html__('No message received.', 'mxchat'));
1558 719 wp_die();
@@ -1558,191 +719,209 @@
1558 719 wp_die();
1559 720 }
1560 721
1561 722
1562 - // Track originating page for first message in session
1563 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
723 + // NEW: Track originating page for first message in session
724 +$table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1564 725
1565 - // Check if originating page columns exist
1566 - $columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
726 +// Check if originating page columns exist
727 +$columns_exist = $wpdb->get_var("SHOW COLUMNS FROM $table_name LIKE 'originating_page_url'");
1567 728
1568 - if ($columns_exist) {
1569 - // Check if this session already has messages
1570 - $message_count = $wpdb->get_var($wpdb->prepare(
1571 - "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
1572 - $session_id
1573 - ));
729 +if ($columns_exist) {
730 + // Check if this session already has messages
731 + $message_count = $wpdb->get_var($wpdb->prepare(
732 + "SELECT COUNT(*) FROM $table_name WHERE session_id = %s",
733 + $session_id
734 + ));
735 +
736 + // If this is the first message in the session
737 + if ($message_count == 0) {
738 + // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
739 + $originating_url = '';
740 + $originating_title = '';
1574 741
1575 - // If this is the first message in the session
1576 - if ($message_count == 0) {
1577 - // Get originating page from JavaScript (preferred) or HTTP_REFERER (fallback)
1578 - $originating_url = '';
1579 - $originating_title = '';
742 + // Try to get from POST data first (sent by JavaScript)
743 + if (isset($_POST['current_page_url'])) {
744 + $originating_url = esc_url_raw($_POST['current_page_url']);
745 + $originating_title = isset($_POST['current_page_title'])
746 + ? sanitize_text_field($_POST['current_page_title'])
747 + : '';
748 + }
749 + // Fallback to HTTP_REFERER if not provided by JavaScript
750 + else if (isset($_SERVER['HTTP_REFERER'])) {
751 + $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
752 + }
753 +
754 + // Generate title if we have URL but no title
755 + if ($originating_url && empty($originating_title)) {
756 + $parsed_url = parse_url($originating_url);
757 + $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1580 758
1581 - // Try to get from POST data first (sent by JavaScript)
1582 - if (isset($_POST['current_page_url'])) {
1583 - $originating_url = esc_url_raw($_POST['current_page_url']);
1584 - $originating_title = isset($_POST['current_page_title'])
1585 - ? sanitize_text_field($_POST['current_page_title'])
1586 - : '';
759 + if (empty($path) || $path === 'index.php' || $path === 'index.html') {
760 + $originating_title = 'Homepage';
761 + } else {
762 + // Clean up the path to make a readable title
763 + $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
764 + $originating_title = ucwords(trim($originating_title));
1587 765 }
1588 - // Fallback to HTTP_REFERER if not provided by JavaScript
1589 - else if (isset($_SERVER['HTTP_REFERER'])) {
1590 - $originating_url = esc_url_raw($_SERVER['HTTP_REFERER']);
1591 - }
1592 -
1593 - // Generate title if we have URL but no title
1594 - if ($originating_url && empty($originating_title)) {
1595 - $parsed_url = parse_url($originating_url);
1596 - $path = isset($parsed_url['path']) ? trim($parsed_url['path'], '/') : '';
1597 -
1598 - if (empty($path) || $path === 'index.php' || $path === 'index.html') {
1599 - $originating_title = 'Homepage';
1600 - } else {
1601 - // Clean up the path to make a readable title
1602 - $originating_title = str_replace(['-', '_', '/', '.php', '.html'], ' ', $path);
1603 - $originating_title = ucwords(trim($originating_title));
1604 - }
1605 - }
1606 -
1607 - // Store for later use when saving the message
1608 - $this->pending_originating_page = [
1609 - 'url' => $originating_url,
1610 - 'title' => $originating_title
1611 - ];
1612 766 }
767 +
768 + // Store for later use when saving the message
769 + $this->pending_originating_page = [
770 + 'url' => $originating_url,
771 + 'title' => $originating_title
772 + ];
1613 773 }
774 +}
775 +
776 +
777 +
778 + // NEW: Get page context if provided
779 + $page_context = null;
780 + if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
781 + $page_context_raw = stripslashes($_POST['page_context']);
782 + $page_context = json_decode($page_context_raw, true);
1614 783
1615 -
1616 -
1617 - // Get page context if provided
1618 - $page_context = null;
1619 - if (isset($_POST['page_context']) && !empty($_POST['page_context'])) {
1620 - $page_context_raw = stripslashes($_POST['page_context']);
1621 - $page_context = json_decode($page_context_raw, true);
784 + // Validate page context structure
785 + if (is_array($page_context) &&
786 + isset($page_context['url']) &&
787 + isset($page_context['title']) &&
788 + isset($page_context['content'])) {
1622 789
1623 - // Validate page context structure
1624 - if (is_array($page_context) &&
1625 - isset($page_context['url']) &&
1626 - isset($page_context['title']) &&
1627 - isset($page_context['content'])) {
1628 -
1629 - // Sanitize page context
1630 - $page_context['url'] = esc_url_raw($page_context['url']);
1631 - $page_context['title'] = sanitize_text_field($page_context['title']);
1632 - $page_context['content'] = wp_kses_post($page_context['content']);
1633 - } else {
1634 - $page_context = null;
1635 - }
790 + // Sanitize page context
791 + $page_context['url'] = esc_url_raw($page_context['url']);
792 + $page_context['title'] = sanitize_text_field($page_context['title']);
793 + $page_context['content'] = wp_kses_post($page_context['content']);
794 + } else {
795 + $page_context = null;
1636 796 }
797 + }
1637 798
1638 - // Modify the message sanitization to preserve PHP tags in code blocks
1639 - $allowed_tags = [
1640 - 'pre' => [],
1641 - 'code' => ['class' => true],
1642 - 'span' => ['class' => true],
1643 - 'div' => ['class' => true],
1644 - ];
799 + // Modify the message sanitization to preserve PHP tags in code blocks
800 + $allowed_tags = [
801 + 'pre' => [],
802 + 'code' => ['class' => true],
803 + 'span' => ['class' => true],
804 + 'div' => ['class' => true],
805 + ];
1645 806
1646 - // First preserve code blocks
1647 - $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
1648 - return htmlspecialchars_decode($matches[0]);
1649 - }, $_POST['message']);
807 + // First preserve code blocks
808 + $message = preg_replace_callback('/<pre><code.*?>.*?<\/code><\/pre>/s', function($matches) {
809 + return htmlspecialchars_decode($matches[0]);
810 + }, $_POST['message']);
1650 811
1651 - // Then apply sanitization
1652 - $message = wp_kses($message, $allowed_tags);
812 + // Then apply sanitization
813 + $message = wp_kses($message, $allowed_tags);
1653 814
1654 - // Preserve code blocks from markdown conversion
1655 - $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
1656 - $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
815 + // Preserve code blocks from markdown conversion
816 + $message = preg_replace('/```(\w+)?\s*([\s\S]+?)```/s', '<pre><code class="$1">$2</code></pre>', $message);
817 + $message = apply_filters('mxchat_filter_message', $message, 'prompt', $session_id);
1657 818
1658 - // ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
1659 - // Always initialize testing data for admins (no toggle needed)
1660 - $testing_data = null;
1661 - if (current_user_can('administrator')) {
1662 - // For vision messages, use the original user message for the query display
1663 - $query_for_testing = $message;
1664 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1665 - $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1666 - }
1667 -
1668 - $testing_data = [
1669 - 'query' => $query_for_testing,
1670 - 'timestamp' => time(),
1671 - 'top_matches' => [],
1672 - 'action_matches' => [], // Initialize action matches array
1673 - 'page_context' => $page_context, // Include page context in testing data
1674 - 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed'],
1675 - 'bot_id' => $bot_id // Include bot ID in testing data
1676 - ];
1677 -
1678 - // Get similarity threshold from bot options or default options
1679 - $similarity_threshold = isset($current_options['similarity_threshold'])
1680 - ? ((int) $current_options['similarity_threshold']) / 100
1681 - : 0.35;
1682 -
1683 - $testing_data['similarity_threshold'] = $similarity_threshold;
1684 -
1685 - // Determine knowledge base type using bot-specific config
1686 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
1687 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
1688 - $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
819 +// ===== SIMPLIFIED TESTING PANEL INITIALIZATION =====
820 + // Always initialize testing data for admins (no toggle needed)
821 + $testing_data = null;
822 + if (current_user_can('administrator')) {
823 + // For vision messages, use the original user message for the query display
824 + $query_for_testing = $message;
825 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
826 + $query_for_testing = sanitize_textarea_field($_POST['original_user_message']);
1689 827 }
1690 - // ===== END SIMPLIFIED TESTING INITIALIZATION =====
828 +
829 + $testing_data = [
830 + 'query' => $query_for_testing,
831 + 'timestamp' => time(),
832 + 'top_matches' => [],
833 + 'action_matches' => [], // NEW: Initialize action matches array
834 + 'page_context' => $page_context, // NEW: Include page context in testing data
835 + 'is_vision' => isset($_POST['vision_processed']) && $_POST['vision_processed']
836 + ];
837 +
838 + // Get similarity threshold
839 + $similarity_threshold = isset($this->options['similarity_threshold'])
840 + ? ((int) $this->options['similarity_threshold']) / 100
841 + : 0.75;
842 +
843 + $testing_data['similarity_threshold'] = $similarity_threshold;
844 +
845 + // Determine knowledge base type
846 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
847 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
848 + $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
849 + }
850 + // ===== END SIMPLIFIED TESTING INITIALIZATION =====
1691 851
1692 - // Add debug before and after:
1693 - //error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
1694 - $pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
1695 - //error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
852 +// Add debug before and after:
853 +error_log('MxChat Core: About to call mxchat_pre_process_message filter with message: ' . $message);
854 +$pre_processed_result = apply_filters('mxchat_pre_process_message', $message, $user_id, $session_id);
855 +error_log('MxChat Core: Filter returned: ' . (is_array($pre_processed_result) ? 'array' : $pre_processed_result));
1696 856
1697 857
1698 - // If the pre-processing returned a result (not the original message), use it directly
1699 - if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
1700 - // Save the AI response
1701 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
1702 -
1703 - // Save HTML content if provided
1704 - if (!empty($pre_processed_result['html'])) {
1705 - $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1706 - }
1707 -
1708 - // Add testing data if admin
1709 - $response_data = [
1710 - 'text' => $pre_processed_result['text'],
1711 - 'html' => $pre_processed_result['html'] ?? '',
1712 - 'session_id' => $session_id
1713 - ];
1714 -
1715 - if ($testing_data !== null) {
1716 - $response_data['testing_data'] = $testing_data;
1717 - }
1718 -
1719 - wp_send_json($response_data);
1720 - wp_die();
858 + // If the pre-processing returned a result (not the original message), use it directly
859 + if (is_array($pre_processed_result) && isset($pre_processed_result['text'])) {
860 + // Save the AI response
861 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['text']);
862 +
863 + // Save HTML content if provided
864 + if (!empty($pre_processed_result['html'])) {
865 + $this->mxchat_save_chat_message($session_id, 'bot', $pre_processed_result['html']);
1721 866 }
867 +
868 + // Add testing data if admin
869 + $response_data = [
870 + 'text' => $pre_processed_result['text'],
871 + 'html' => $pre_processed_result['html'] ?? '',
872 + 'session_id' => $session_id
873 + ];
874 +
875 + if ($testing_data !== null) {
876 + $response_data['testing_data'] = $testing_data;
877 + }
878 +
879 + wp_send_json($response_data);
880 + wp_die();
881 + }
1722 882
1723 - // Save the user's message - handle vision processed messages differently
1724 - if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
1725 - // For vision messages, save the original user message with image indicator
1726 - $original_message = sanitize_textarea_field($_POST['original_user_message']);
1727 - if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
1728 - $image_count = intval($_POST['vision_images_count']);
1729 - $original_message .= " [{$image_count} image(s)]";
1730 - }
1731 - $this->mxchat_save_chat_message($session_id, 'user', $original_message);
1732 - } else {
1733 - // Regular message - save as normal
1734 - $this->mxchat_save_chat_message($session_id, 'user', $message);
883 + // Save the user's message - handle vision processed messages differently
884 + if (isset($_POST['vision_processed']) && $_POST['vision_processed'] && isset($_POST['original_user_message'])) {
885 + // For vision messages, save the original user message with image indicator
886 + $original_message = sanitize_textarea_field($_POST['original_user_message']);
887 + if (isset($_POST['vision_images_count']) && $_POST['vision_images_count'] > 0) {
888 + $image_count = intval($_POST['vision_images_count']);
889 + $original_message .= " [{$image_count} image(s)]";
1735 890 }
891 + $this->mxchat_save_chat_message($session_id, 'user', $original_message);
892 + } else {
893 + // Regular message - save as normal
894 + $this->mxchat_save_chat_message($session_id, 'user', $message);
895 + }
1736 896
897 +
898 +if (is_email($message)) {
899 + // Add the email to Loops
900 + $this->add_email_to_loops($message);
1737 901
1738 - if (is_email($message)) {
1739 - // Add the email to Loops
1740 - $this->add_email_to_loops($message);
902 + // Get the user's success message instruction
903 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
904 +
905 + // Set instruction for AI using the user's success message
906 + $this->current_action_instruction = $user_success_message;
907 +
908 + // Clear the email capture transient since we got the email
909 + delete_transient('mxchat_email_capture_' . $user_id);
910 + }
911 +
912 + // NEW: Check if we're in an email capture flow but user hasn't provided email yet
913 + elseif (get_transient('mxchat_email_capture_' . $user_id)) {
914 + // Check if the message contains an email (not the whole message being an email)
915 + if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
916 + $extracted_email = $matches[0];
1741 917
1742 - // Get the user's success message instruction using current_options
1743 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
918 + // Add the extracted email to Loops
919 + $this->add_email_to_loops($extracted_email);
1744 920
921 + // Get the user's success message instruction
922 + $user_success_message = $this->options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
923 +
1745 924 // Set instruction for AI using the user's success message
1746 925 $this->current_action_instruction = $user_success_message;
1747 926
1748 927 // Clear the email capture transient since we got the email
@@ -1747,750 +926,464 @@
1747 926
1748 927 // Clear the email capture transient since we got the email
1749 928 delete_transient('mxchat_email_capture_' . $user_id);
1750 929 }
1751 -
1752 - // Check if we're in an email capture flow but user hasn't provided email yet
1753 - elseif (get_transient('mxchat_email_capture_' . $user_id)) {
1754 - // Check if the message contains an email (not the whole message being an email)
1755 - if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message, $matches)) {
1756 - $extracted_email = $matches[0];
1757 -
1758 - // Add the extracted email to Loops
1759 - $this->add_email_to_loops($extracted_email);
1760 -
1761 - // Get the user's success message instruction using current_options
1762 - $user_success_message = $current_options['email_capture_response'] ?? __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat');
1763 -
1764 - // Set instruction for AI using the user's success message
1765 - $this->current_action_instruction = $user_success_message;
1766 -
1767 - // Clear the email capture transient since we got the email
1768 - delete_transient('mxchat_email_capture_' . $user_id);
1769 - }
1770 - // If no email found but we're in capture mode, remind them
1771 - else {
1772 - // Get the original instruction to remind them using current_options
1773 - $original_instruction = $current_options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
1774 - $this->current_action_instruction = $original_instruction;
1775 - }
930 + // If no email found but we're in capture mode, remind them
931 + else {
932 + // Get the original instruction to remind them
933 + $original_instruction = $this->options['triggered_phrase_response'] ?? __("Please provide your email address.", 'mxchat');
934 + $this->current_action_instruction = $original_instruction;
1776 935 }
936 + }
1777 937
1778 - $intent_info = '';
938 + $intent_info = '';
1779 939
1780 - // Check chat mode
1781 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
940 + // Check chat mode
941 + $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
1782 942
1783 - // Handle agent mode
1784 943 // Handle agent mode
1785 - if ($chat_mode === 'agent') {
1786 - // First, check for switch intent before doing anything else
1787 - $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
944 +// Handle agent mode
945 + if ($chat_mode === 'agent') {
946 + // First, check for switch intent before doing anything else
947 + $intent_matched = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1788 948
1789 - // Capture action analysis for testing panel after intent check
1790 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1791 - $testing_data['action_matches'] = $this->last_action_analysis;
949 + // NEW: Capture action analysis for testing panel after intent check
950 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
951 + $testing_data['action_matches'] = $this->last_action_analysis;
952 + }
953 +
954 + // If we matched an intent and it's the switch intent, handle it
955 + if ($intent_matched && !empty($this->fallbackResponse['text'])) {
956 + // Update chat mode first
957 + update_option("mxchat_mode_{$session_id}", 'ai');
958 +
959 + // Clear any existing PDF context to start fresh
960 + $this->clear_pdf_transients($session_id);
961 +
962 + // Prepare clean switch response
963 + $response_data = [
964 + 'text' => $this->fallbackResponse['text'],
965 + 'html' => '',
966 + 'session_id' => $session_id,
967 + 'chat_mode' => 'ai'
968 + ];
969 +
970 + if ($testing_data !== null) {
971 + $response_data['testing_data'] = $testing_data;
1792 972 }
1793 -
1794 - // Around line 506, in the agent mode handling section:
1795 - if ($intent_matched && !empty($this->fallbackResponse['text'])) {
1796 - // Update chat mode first
1797 - update_option("mxchat_mode_{$session_id}", 'ai');
1798 -
1799 - // Clear any existing PDF context to start fresh
1800 - $this->clear_pdf_transients($session_id);
1801 -
1802 - // Prepare clean switch response with explicit chat_mode
1803 - $response_data = [
1804 - 'text' => $this->fallbackResponse['text'],
1805 - 'html' => $this->fallbackResponse['html'] ?? '',
1806 - 'session_id' => $session_id,
1807 - 'chat_mode' => 'ai' // EXPLICITLY SET THIS
973 +
974 + // Save the mode switch message
975 + $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
976 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
977 +
978 + // Send response and exit
979 + wp_send_json($response_data);
980 + wp_die();
981 + } elseif (!$intent_matched) {
982 + // No intent matched, handle live agent message
983 + try {
984 + $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
985 +
986 + $agent_response = [
987 + 'status' => 'waiting_for_agent',
988 + 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1808 989 ];
1809 -
990 +
1810 991 if ($testing_data !== null) {
1811 - $response_data['testing_data'] = $testing_data;
992 + $agent_response['testing_data'] = $testing_data;
1812 993 }
1813 -
1814 - // Save the mode switch message
1815 - $this->mxchat_save_chat_message($session_id, 'system', esc_html__('Switched to AI chat mode', 'mxchat'));
1816 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1817 -
1818 - // Send response and exit
1819 - wp_send_json($response_data);
1820 - wp_die();
1821 - } elseif (!$intent_matched) {
1822 - // No intent matched, handle live agent message
1823 - try {
1824 - $this->mxchat_send_user_message_to_agent($message, $user_id, $session_id);
1825 994
1826 - $agent_response = [
1827 - 'status' => 'waiting_for_agent',
1828 - 'message' => esc_html__('Message sent to live agent.', 'mxchat')
1829 - ];
1830 -
1831 - if ($testing_data !== null) {
1832 - $agent_response['testing_data'] = $testing_data;
1833 - }
1834 -
1835 - wp_send_json_success($agent_response);
1836 - } catch (\Exception $e) {
1837 - wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1838 - }
1839 - wp_die();
995 + wp_send_json_success($agent_response);
996 + } catch (\Exception $e) {
997 + wp_send_json_error(esc_html__('Failed to send message to agent', 'mxchat'));
1840 998 }
999 + wp_die();
1841 1000 }
1001 + }
1842 1002
1843 - // Step 1: Check for new PDF URL in the message
1844 - if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1845 - $new_pdf_url = $matches[0];
1003 + // Step 1: Check for new PDF URL in the message
1004 + if (!isset($_POST['vision_processed']) && preg_match('/https?:\/\/[^\s"]+/i', $message, $matches)) {
1005 + $new_pdf_url = $matches[0];
1846 1006
1847 - // Check if this is likely a PDF-related request
1848 - $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1849 - $is_pdf_request = false;
1007 + // Check if this is likely a PDF-related request
1008 + $pdf_keywords = ['pdf', 'document', 'read', 'analyze'];
1009 + $is_pdf_request = false;
1850 1010
1851 - foreach ($pdf_keywords as $keyword) {
1852 - if (stripos($message, $keyword) !== false) {
1853 - $is_pdf_request = true;
1854 - break;
1855 - }
1011 + foreach ($pdf_keywords as $keyword) {
1012 + if (stripos($message, $keyword) !== false) {
1013 + $is_pdf_request = true;
1014 + break;
1856 1015 }
1016 + }
1857 1017
1858 - // If it looks like a PDF request or we're waiting for a PDF URL
1859 - if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1860 - // Validate HTTPS
1861 - if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1862 - // Extract filename from URL
1863 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1018 + // If it looks like a PDF request or we're waiting for a PDF URL
1019 + if ($is_pdf_request || get_transient('mxchat_waiting_for_pdf_url_' . $session_id)) {
1020 + // Validate HTTPS
1021 + if (wp_http_validate_url($new_pdf_url) && parse_url($new_pdf_url, PHP_URL_SCHEME) === 'https') {
1022 + // Extract filename from URL
1023 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1864 1024
1865 - // Clear previous PDF transients
1866 - $this->clear_pdf_transients($session_id);
1025 + // Clear previous PDF transients
1026 + $this->clear_pdf_transients($session_id);
1867 1027
1868 - // Process new PDF using current_options
1869 - $max_pages = $current_options['pdf_max_pages'] ?? 69;
1870 - $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1028 + // Process new PDF
1029 + $max_pages = $this->options['pdf_max_pages'] ?? 69;
1030 + $embeddings = $this->fetch_and_split_pdf_pages($new_pdf_url, $max_pages);
1871 1031
1872 - if ($embeddings === 'too_many_pages') {
1873 - $error_text = sprintf(
1874 - $current_options['pdf_intent_error_text'] ??
1875 - esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1876 - $max_pages
1877 - );
1878 - $this->fallbackResponse['text'] = $error_text;
1879 - } elseif ($embeddings) {
1880 - // Store new PDF information
1881 - $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1032 + if ($embeddings === 'too_many_pages') {
1033 + $error_text = sprintf(
1034 + $this->options['pdf_intent_error_text'] ??
1035 + esc_html__("The provided PDF exceeds the maximum allowed limit of %d pages. Please provide a smaller document.", 'mxchat'),
1036 + $max_pages
1037 + );
1038 + $this->fallbackResponse['text'] = $error_text;
1039 + } elseif ($embeddings) {
1040 + // Store new PDF information
1041 + $pdf_filename = basename(parse_url($new_pdf_url, PHP_URL_PATH));
1882 1042
1883 - // If the filename is generic, create a more descriptive one
1884 - if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1885 - strpos($pdf_filename, '.php') !== false) {
1886 - $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1887 - }
1043 + // If the filename is generic, create a more descriptive one
1044 + if (in_array($pdf_filename, ['results_download.php', 'download.php', 'view.php', 'pdf.php']) ||
1045 + strpos($pdf_filename, '.php') !== false) {
1046 + $pdf_filename = 'Document_' . date('Y-m-d_H-i') . '.pdf';
1047 + }
1888 1048
1889 - set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1890 - set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1891 - set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1892 - set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1049 + set_transient('mxchat_pdf_url_' . $session_id, $new_pdf_url, HOUR_IN_SECONDS);
1050 + set_transient('mxchat_pdf_filename_' . $session_id, $pdf_filename, HOUR_IN_SECONDS);
1051 + set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
1052 + set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
1893 1053
1894 - $success_text = $current_options['pdf_intent_success_text'] ??
1895 - esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1054 + $success_text = $this->options['pdf_intent_success_text'] ??
1055 + esc_html__("I've processed the new PDF '{$pdf_filename}'. What questions do you have about it?", 'mxchat');
1896 1056
1897 - $pdf_response = [
1898 - 'success' => true,
1899 - 'message' => $success_text,
1900 - 'data' => [
1901 - 'filename' => $pdf_filename
1902 - ]
1903 - ];
1904 -
1905 - if ($testing_data !== null) {
1906 - $pdf_response['testing_data'] = $testing_data;
1907 - }
1908 -
1909 - wp_send_json($pdf_response);
1910 - wp_die();
1911 - } else {
1912 - $error_text = $current_options['pdf_intent_error_text'] ??
1913 - esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1914 - $this->fallbackResponse['text'] = $error_text;
1915 - }
1916 -
1917 - $pdf_error_response = [
1918 - 'success' => false,
1919 - 'message' => $this->fallbackResponse['text']
1057 + $pdf_response = [
1058 + 'success' => true,
1059 + 'message' => $success_text,
1060 + 'data' => [
1061 + 'filename' => $pdf_filename
1062 + ]
1920 1063 ];
1921 1064
1922 1065 if ($testing_data !== null) {
1923 - $pdf_error_response['testing_data'] = $testing_data;
1066 + $pdf_response['testing_data'] = $testing_data;
1924 1067 }
1925 1068
1926 - wp_send_json($pdf_error_response);
1069 + wp_send_json($pdf_response);
1927 1070 wp_die();
1071 + } else {
1072 + $error_text = $this->options['pdf_intent_error_text'] ??
1073 + esc_html__("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat');
1074 + $this->fallbackResponse['text'] = $error_text;
1928 1075 }
1929 - }
1930 - }
1931 1076
1932 -
1933 - // Step 2: Detect intent and handle intent-based responses
1934 - $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
1935 -
1936 - // Capture action analysis for testing panel after intent check
1937 - if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1938 - $testing_data['action_matches'] = $this->last_action_analysis;
1939 - }
1940 -
1941 - // Step 3: Handle the intent result appropriately
1942 - if ($intent_result !== false) {
1943 - // Intent was matched - ALWAYS send as JSON response, never streaming
1944 -
1945 - if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1946 - // Intent returned a direct response array
1947 - $response_data = [
1948 - 'text' => $intent_result['text'] ?? '',
1949 - 'html' => $intent_result['html'] ?? '',
1950 - 'session_id' => $session_id
1077 + $pdf_error_response = [
1078 + 'success' => false,
1079 + 'message' => $this->fallbackResponse['text']
1951 1080 ];
1952 -
1953 - // IMPORTANT: Include chat_mode if present (for WhatsApp, Slack, etc.)
1954 - if (isset($intent_result['chat_mode'])) {
1955 - $response_data['chat_mode'] = $intent_result['chat_mode'];
1956 - }
1957 -
1081 +
1958 1082 if ($testing_data !== null) {
1959 - $response_data['testing_data'] = $testing_data;
1083 + $pdf_error_response['testing_data'] = $testing_data;
1960 1084 }
1961 1085
1962 - wp_send_json($response_data);
1086 + wp_send_json($pdf_error_response);
1963 1087 wp_die();
1964 - } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1965 - // Intent returned true and set fallbackResponse
1966 -
1967 - // SAVE TO TRANSCRIPT
1968 - if (!empty($this->fallbackResponse['text'])) {
1969 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
1970 - }
1971 - // Save action HTML (product cards, featured products, etc.) so it renders in transcripts
1972 - if (!empty($this->fallbackResponse['html'])) {
1973 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1974 - }
1975 -
1976 - $response_data = [
1977 - 'text' => $this->fallbackResponse['text'] ?? '',
1978 - 'html' => $this->fallbackResponse['html'] ?? '',
1979 - 'session_id' => $session_id
1980 - ];
1981 -
1982 - if (isset($this->fallbackResponse['chat_mode'])) {
1983 - $response_data['chat_mode'] = $this->fallbackResponse['chat_mode'];
1984 - }
1985 -
1986 - if ($testing_data !== null) {
1987 - $response_data['testing_data'] = $testing_data;
1988 - }
1989 -
1990 - wp_send_json($response_data);
1991 - wp_die();
1992 1088 }
1993 1089 }
1090 + }
1994 1091
1995 - // If we get here, no intent matched OR the intent didn't provide a usable response
1996 -
1997 - // Step 4: Generate AI response
1998 - // Get session start timestamp - when persistence is OFF, only include messages from this page load
1999 - $session_start_timestamp = isset($_POST['session_start_timestamp']) ? intval($_POST['session_start_timestamp']) : 0;
2000 - $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id, $session_start_timestamp);
2001 - $this->mxchat_increment_chat_count();
1092 + // Check if there's an active recommendation flow session
1093 + $flow_state = get_option("mxchat_sr_flow_state_{$session_id}", array());
1094 + if (!empty($flow_state) && isset($flow_state['flow_id'])) {
1095 + // Create a dummy intent object that matches the original intent
1096 + $dummy_intent = new stdClass();
1097 + $dummy_intent->intent_label = 'Recommendation Flow ' . $flow_state['flow_id'];
1098 + $dummy_intent->phrases = ''; // Empty phrases to avoid matching the original trigger
2002 1099
2003 - // Generate embedding for the user's query - USE BOT-SPECIFIC API KEY
2004 - $api_key = $current_options['api_key'] ?? $this->options['api_key'];
2005 - $user_message_embedding = $this->mxchat_generate_embedding($message, $api_key);
1100 + // Call the recommendation flow handler directly
1101 + $response_data = apply_filters('mxchat_sr_recommendation_flow', false, $message, $user_id, $session_id, $dummy_intent);
2006 1102
2007 - // Check if the embedding generation returned an error
2008 - if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
2009 - $error_message = $user_message_embedding['error'];
2010 - $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
2011 -
2012 - // FIXED: Send error in appropriate format based on streaming mode
2013 - if ($is_streaming) {
2014 - echo "data: " . json_encode([
2015 - 'error' => true,
2016 - 'error_message' => $error_message,
2017 - 'error_code' => $error_code,
2018 - 'text' => $error_message,
2019 - 'message' => $error_message
2020 - ]) . "\n\n";
2021 - echo "data: [DONE]\n\n";
2022 - flush();
2023 - } else {
2024 - wp_send_json_error([
2025 - 'error_message' => $error_message,
2026 - 'error_code' => $error_code
2027 - ]);
1103 + // If the handler returned a response, send it
1104 + if (is_array($response_data) && (isset($response_data['text']) || isset($response_data['html']))) {
1105 + // Save the bot's response to the chat history
1106 + if (!empty($response_data['text'])) {
1107 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['text']);
2028 1108 }
1109 + if (!empty($response_data['html'])) {
1110 + $this->mxchat_save_chat_message($session_id, 'bot', $response_data['html']);
1111 + }
1112 +
1113 + if ($testing_data !== null) {
1114 + $response_data['testing_data'] = $testing_data;
1115 + }
1116 +
1117 + // Send the response
1118 + wp_send_json($response_data);
2029 1119 wp_die();
2030 1120 }
1121 + }
2031 1122
2032 - // Check if the embedding is valid
2033 - if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
2034 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
1123 + // Step 2: Detect intent and handle intent-based responses
1124 + $intent_result = $this->mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id);
2035 1125
2036 - // FIXED: Send error in appropriate format based on streaming mode
1126 + // NEW: Capture action analysis for testing panel after intent check
1127 + if ($testing_data !== null && isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
1128 + $testing_data['action_matches'] = $this->last_action_analysis;
1129 + }
1130 +
1131 + // Step 3: Handle the intent result appropriately
1132 + if ($intent_result !== false) {
1133 + // Intent was matched - ALWAYS send as JSON response, never streaming
1134 +
1135 + if (is_array($intent_result) && (isset($intent_result['text']) || isset($intent_result['html']))) {
1136 + // Intent returned a direct response array
1137 + $response_data = [
1138 + 'text' => $intent_result['text'] ?? '',
1139 + 'html' => $intent_result['html'] ?? '',
1140 + 'session_id' => $session_id
1141 + ];
1142 +
1143 + if ($testing_data !== null) {
1144 + $response_data['testing_data'] = $testing_data;
1145 + }
1146 +
1147 + // Clear streaming headers if they were set
2037 1148 if ($is_streaming) {
2038 - echo "data: " . json_encode([
2039 - 'error' => true,
2040 - 'error_message' => $error_message,
2041 - 'error_code' => 'invalid_embedding',
2042 - 'text' => $error_message,
2043 - 'message' => $error_message
2044 - ]) . "\n\n";
2045 - echo "data: [DONE]\n\n";
2046 - flush();
2047 - } else {
2048 - wp_send_json_error([
2049 - 'error_message' => $error_message,
2050 - 'error_code' => 'invalid_embedding'
2051 - ]);
1149 + header_remove('Content-Type');
1150 + header_remove('Cache-Control');
1151 + header_remove('Connection');
1152 + header_remove('X-Accel-Buffering');
1153 + header('Content-Type: application/json');
2052 1154 }
1155 +
1156 + wp_send_json($response_data);
2053 1157 wp_die();
2054 - }
2055 -
2056 - // Build context with both knowledge base and PDF content if available
2057 - $context_content = "User asked: '{$message}'\n\n";
2058 -
2059 - // Add action instruction if present (add this right after the above line)
2060 - if (!empty($this->current_action_instruction)) {
2061 - $context_content .= "===== SPECIAL INSTRUCTION =====\n";
2062 - $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
2063 - $context_content .= "Respond naturally and conversationally while following this instruction.\n";
2064 - $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
1158 + } else if ($intent_result === true && (!empty($this->fallbackResponse['text']) || !empty($this->fallbackResponse['html']))) {
1159 + // Intent returned true and set fallbackResponse
2065 1160
2066 - // Clear the instruction after using it
2067 - $this->current_action_instruction = null;
2068 - }
2069 -
2070 -
2071 - // Add page context if available and contextual awareness is enabled using current_options
2072 - if ($page_context && isset($current_options['contextual_awareness_toggle']) && $current_options['contextual_awareness_toggle'] === 'on') {
2073 - $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
2074 - $context_content .= "Page URL: " . $page_context['url'] . "\n";
2075 - $context_content .= "Page Title: " . $page_context['title'] . "\n";
2076 - $context_content .= "Page Content: " . $page_context['content'] . "\n";
2077 - $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
2078 - }
2079 -
2080 - // Get relevant content from knowledge base - PASS BOT_ID and MESSAGE for Vector Store
2081 - $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding, $bot_id, $message);
2082 -
2083 - // NEW: Also extract URLs from system instructions (only if citation links enabled)
2084 - // Use fresh options to ensure we get the latest setting value
2085 - $fresh_options = get_option('mxchat_options', []);
2086 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
2087 -
2088 - $system_instructions = $this->get_system_instructions($bot_id, $session_id);
2089 - if ($citation_links_enabled && !empty($system_instructions)) {
2090 - preg_match_all(
2091 - '#\bhttps?://[^\s<>"\']+#i',
2092 - $system_instructions,
2093 - $system_instruction_urls
2094 - );
2095 -
2096 - if (!empty($system_instruction_urls[0])) {
2097 - // Merge with existing valid URLs
2098 - $this->current_valid_urls = array_merge(
2099 - $this->current_valid_urls,
2100 - $system_instruction_urls[0]
2101 - );
2102 - // Remove duplicates
2103 - $this->current_valid_urls = array_unique($this->current_valid_urls);
2104 -
2105 - //error_log("Added " . count($system_instruction_urls[0]) . " URLs from system instructions");
1161 + // SAVE TO TRANSCRIPT FIRST
1162 + if (!empty($this->fallbackResponse['text'])) {
1163 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['text']);
2106 1164 }
1165 + if (!empty($this->fallbackResponse['html'])) {
1166 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
1167 + }
1168 +
1169 + $response_data = [
1170 + 'text' => $this->fallbackResponse['text'] ?? '',
1171 + 'html' => $this->fallbackResponse['html'] ?? '',
1172 + 'session_id' => $session_id
1173 + ];
1174 +
1175 + if ($testing_data !== null) {
1176 + $response_data['testing_data'] = $testing_data;
1177 + }
1178 +
1179 + // Clear streaming headers if they were set
1180 + if ($is_streaming) {
1181 + header_remove('Content-Type');
1182 + header_remove('Cache-Control');
1183 + header_remove('Connection');
1184 + header_remove('X-Accel-Buffering');
1185 + header('Content-Type: application/json');
1186 + }
1187 +
1188 + wp_send_json($response_data);
1189 + wp_die();
2107 1190 }
2108 -
2109 -// ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
2110 -if ($testing_data !== null && $this->last_similarity_analysis !== null) {
2111 - // Update testing data with the REAL similarity analysis
2112 - $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
2113 - $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
2114 - $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
2115 - $testing_data['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
2116 - $testing_data['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
2117 -}
2118 -// ===== END SIMILARITY DATA CAPTURE =====
1191 + }
2119 1192
2120 -// NEW: Add valid URLs to testing data for admin panel display (AFTER similarity data)
2121 -if ($testing_data !== null && !empty($this->current_valid_urls)) {
2122 - $testing_data['approved_urls'] = array_values($this->current_valid_urls);
2123 - //error_log("Added " . count($this->current_valid_urls) . " approved URLs to testing data");
2124 -}
1193 + // If we get here, no intent matched OR the intent didn't provide a usable response
1194 +
1195 + // Step 4: Generate AI response
1196 + $conversation_history = $this->mxchat_fetch_conversation_history_for_ai($session_id);
1197 + $this->mxchat_increment_chat_count();
1198 +
1199 + // Generate embedding for the user's query
1200 + $user_message_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
1201 +
1202 + // Check if the embedding generation returned an error
1203 + if (is_array($user_message_embedding) && isset($user_message_embedding['error'])) {
1204 + $error_message = $user_message_embedding['error'];
1205 + $error_code = $user_message_embedding['error_code'] ?? 'embedding_error';
2125 1206
2126 - if (!empty($relevant_content)) {
2127 - $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
2128 - } else {
2129 - $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
2130 - }
2131 -
2132 - // NEW: Add approved URLs list to context for AI (only if citation links enabled)
2133 - if ($citation_links_enabled && !empty($this->current_valid_urls)) {
2134 - $context_content .= "===== APPROVED URLS FOR CITATIONS =====\n";
2135 - $context_content .= "You may ONLY use these exact URLs in your response:\n";
2136 - foreach ($this->current_valid_urls as $url) {
2137 - $context_content .= "- " . $url . "\n";
2138 - }
2139 - $context_content .= "\nCRITICAL: Do NOT create, modify, extend, or invent any other URLs. ";
2140 - $context_content .= "===== END APPROVED URLS =====\n\n";
2141 - }
2142 -
2143 - // Check for and include PDF content
2144 - $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
2145 - $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
2146 - $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
2147 - if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
2148 - $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
2149 - if (!empty($relevant_pdf_pages)) {
2150 - $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
2151 - foreach ($relevant_pdf_pages as $page_data) {
2152 - $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
2153 - }
2154 - $context_content .= "\n";
2155 - }
2156 - }
1207 + wp_send_json_error([
1208 + 'error_message' => $error_message,
1209 + 'error_code' => $error_code
1210 + ]);
1211 + wp_die();
1212 + }
1213 +
1214 + // Check if the embedding is valid
1215 + if (!is_array($user_message_embedding) || empty($user_message_embedding)) {
1216 + wp_send_json_error([
1217 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1218 + 'error_code' => 'invalid_embedding'
1219 + ]);
1220 + wp_die();
1221 + }
2157 1222
2158 - // Check for and include Word content
2159 - $word_url = get_transient('mxchat_word_url_' . $session_id);
2160 - $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
2161 - $word_filename = get_transient('mxchat_word_filename_' . $session_id);
2162 - if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
2163 - $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
2164 - if (!empty($relevant_word_chunks)) {
2165 - $context_content .= "Relevant content from Word document '{$word_filename}':\n";
2166 - foreach ($relevant_word_chunks as $chunk_data) {
2167 - $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
2168 - }
2169 - $context_content .= "\n";
2170 - }
2171 - }
1223 + // Build context with both knowledge base and PDF content if available
1224 + $context_content = "User asked: '{$message}'\n\n";
1225 +
1226 + // NEW: Add action instruction if present (add this right after the above line)
1227 + if (!empty($this->current_action_instruction)) {
1228 + $context_content .= "===== SPECIAL INSTRUCTION =====\n";
1229 + $context_content .= "IMPORTANT: " . $this->current_action_instruction . "\n";
1230 + $context_content .= "Respond naturally and conversationally while following this instruction.\n";
1231 + $context_content .= "===== END SPECIAL INSTRUCTION =====\n\n";
2172 1232
2173 - $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
1233 + // Clear the instruction after using it
1234 + $this->current_action_instruction = null;
1235 + }
2174 1236
2175 - // Extract model from current options for bot-specific model support
2176 - $selected_model = isset($current_options['model']) ? $current_options['model'] : 'gpt-5.1-chat-latest';
2177 1237
2178 - // ===== Native function-calling fallback (plan-mxchat-20260617-a41dee) =====
2179 - // Intents already missed (we're past the intent router). If function
2180 - // calling is enabled and the active model is tool-capable, let the model
2181 - // SELECT and run registered callbacks as tools — independent of intents,
2182 - // works with zero Actions. The tool round is buffered; the final answer is
2183 - // emitted via the SAME envelopes the normal path uses. Default-off, so
2184 - // existing installs never enter this branch.
2185 - if ($this->mxchat_fc_should_run($selected_model)) {
2186 - $fc_outcome = $this->mxchat_fc_attempt(
2187 - $message,
2188 - $context_content,
2189 - $conversation_history,
2190 - $selected_model,
2191 - $current_options,
2192 - $session_id,
2193 - $user_id
2194 - );
2195 - if (is_array($fc_outcome) && !empty($fc_outcome['handled'])) {
2196 - $fc_text = isset($fc_outcome['text']) ? $fc_outcome['text'] : '';
2197 - if (!empty($this->current_valid_urls)) {
2198 - $fc_text = $this->validate_and_clean_urls($fc_text, $this->current_valid_urls);
2199 - }
2200 - // plan-mxchat-20260617-48a57a — surface any UI element a tool
2201 - // produced (generated image / product card / image gallery) so the
2202 - // widget RENDERS it, instead of emitting only the model's text.
2203 - // The html was already saved to the transcript in
2204 - // mxchat_fc_execute_tool (or by the callback itself for self-saving
2205 - // core tools), so we persist ONLY the model's caption text here.
2206 - $fc_html = isset($this->fc_ui_html) ? $this->fc_ui_html : '';
1238 + // NEW: Add page context if available and contextual awareness is enabled
1239 + if ($page_context && isset($this->options['contextual_awareness_toggle']) && $this->options['contextual_awareness_toggle'] === 'on') {
1240 + $context_content .= "===== CURRENT PAGE CONTEXT =====\n";
1241 + $context_content .= "Page URL: " . $page_context['url'] . "\n";
1242 + $context_content .= "Page Title: " . $page_context['title'] . "\n";
1243 + $context_content .= "Page Content: " . $page_context['content'] . "\n";
1244 + $context_content .= "===== END CURRENT PAGE CONTEXT =====\n\n";
1245 + }
2207 1246
2208 - if ($fc_text !== '') {
2209 - $this->mxchat_save_chat_message($session_id, 'bot', $fc_text, null, null);
2210 - }
1247 + // Get relevant content from knowledge base - THIS IS WHERE THE SIMILARITY ANALYSIS HAPPENS
1248 + $relevant_content = $this->mxchat_find_relevant_content($user_message_embedding);
1249 +
1250 + // ===== CAPTURE REAL SIMILARITY DATA FOR ADMINS =====
1251 + if ($testing_data !== null && $this->last_similarity_analysis !== null) {
1252 + // Update testing data with the REAL similarity analysis
1253 + $testing_data['top_matches'] = $this->last_similarity_analysis['top_matches'];
1254 + $testing_data['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
1255 + $testing_data['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'];
1256 + }
1257 + // ===== END SIMILARITY DATA CAPTURE =====
1258 +
1259 + if (!empty($relevant_content)) {
1260 + $context_content .= "===== OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n" . $relevant_content . "\n===== END OF OFFICIAL KNOWLEDGE DATABASE CONTENT =====\n\n";
1261 + } else {
1262 + $context_content .= "===== NO RELEVANT CONTENT FOUND IN KNOWLEDGE DATABASE =====\n";
1263 + }
2211 1264
2212 - if ($is_streaming) {
2213 - // The frontend SSE reader routes any event carrying text/html
2214 - // to handleNonStreamResponse(), which renders text + html in a
2215 - // single bot message — so emit one complete event (mirrors the
2216 - // intent path's text/html envelope).
2217 - $sse = array('session_id' => $session_id);
2218 - if ($fc_text !== '') $sse['text'] = $fc_text;
2219 - if ($fc_html !== '') $sse['html'] = $fc_html;
2220 - if ($fc_text === '' && $fc_html === '') $sse['text'] = $this->mxchat_fc_giveup_text();
2221 - echo "data: " . wp_json_encode($sse) . "\n\n";
2222 - echo "data: [DONE]\n\n";
2223 - flush();
2224 - } else {
2225 - $fc_response_data = array('text' => $fc_text, 'html' => $fc_html, 'session_id' => $session_id);
2226 - if ($testing_data !== null) {
2227 - $fc_response_data['testing_data'] = $testing_data;
2228 - }
2229 - wp_send_json($fc_response_data);
2230 - }
2231 - wp_die();
1265 + // Check for and include PDF content
1266 + $pdf_url = get_transient('mxchat_pdf_url_' . $session_id);
1267 + $pdf_embeddings = get_transient('mxchat_pdf_embeddings_' . $session_id);
1268 + $pdf_filename = get_transient('mxchat_pdf_filename_' . $session_id);
1269 + if ($pdf_url && $pdf_embeddings && get_transient('mxchat_include_pdf_in_context_' . $session_id)) {
1270 + $relevant_pdf_pages = $this->find_relevant_pdf_pages($user_message_embedding, $pdf_embeddings);
1271 + if (!empty($relevant_pdf_pages)) {
1272 + $context_content .= "Relevant content from PDF document '{$pdf_filename}':\n";
1273 + foreach ($relevant_pdf_pages as $page_data) {
1274 + $context_content .= "Page {$page_data['page_number']} of '{$pdf_filename}': {$page_data['text']}\n";
2232 1275 }
1276 + $context_content .= "\n";
2233 1277 }
2234 - // ===== end function-calling fallback =====
1278 + }
2235 1279
2236 - $response = $this->mxchat_generate_response(
2237 - $context_content,
2238 - $current_options['api_key'] ?? $this->options['api_key'],
2239 - $current_options['xai_api_key'] ?? $this->options['xai_api_key'],
2240 - $current_options['claude_api_key'] ?? $this->options['claude_api_key'],
2241 - $current_options['deepseek_api_key'] ?? $this->options['deepseek_api_key'],
2242 - $current_options['gemini_api_key'] ?? $this->options['gemini_api_key'],
2243 - $current_options['openrouter_api_key'] ?? $this->options['openrouter_api_key'],
2244 - $conversation_history,
2245 - $is_streaming,
2246 - $session_id,
2247 - $testing_data,
2248 - $selected_model
2249 - );
2250 -
2251 - // Handle streaming vs non-streaming responses
2252 - if ($is_streaming) {
2253 - // Check if streaming actually happened or if it fell back to regular response
2254 - if ($response === true) {
2255 - wp_die();
1280 + // Check for and include Word content
1281 + $word_url = get_transient('mxchat_word_url_' . $session_id);
1282 + $word_embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
1283 + $word_filename = get_transient('mxchat_word_filename_' . $session_id);
1284 + if ($word_url && $word_embeddings && get_transient('mxchat_include_word_in_context_' . $session_id)) {
1285 + $relevant_word_chunks = $this->word_handler->mxchat_find_relevant_word_chunks($user_message_embedding, $word_embeddings);
1286 + if (!empty($relevant_word_chunks)) {
1287 + $context_content .= "Relevant content from Word document '{$word_filename}':\n";
1288 + foreach ($relevant_word_chunks as $chunk_data) {
1289 + $context_content .= "Section {$chunk_data['chunk_number']} of '{$word_filename}': {$chunk_data['text']}\n";
2256 1290 }
2257 - // If we get here, streaming fell back to regular response, continue
2258 - // But if there's an error, we need to send it as SSE format since headers are already set
2259 - if (is_array($response) && isset($response['error'])) {
2260 - $error_message = $response['error'];
2261 - $error_code = $response['error_code'] ?? 'api_error';
2262 - // Send error in SSE format that the client JS can handle
2263 - echo "data: " . json_encode([
2264 - 'error' => true,
2265 - 'error_message' => $error_message,
2266 - 'error_code' => $error_code,
2267 - 'text' => $error_message, // Also include as text for fallback handling
2268 - 'message' => $error_message
2269 - ]) . "\n\n";
2270 - echo "data: [DONE]\n\n";
2271 - flush();
2272 - wp_die();
2273 - }
1291 + $context_content .= "\n";
2274 1292 }
1293 + }
1294 +
1295 + $context_content = apply_filters('mxchat_prepare_context', $context_content, $session_id);
2275 1296
2276 - // Check if the response is an error array (non-streaming mode)
2277 - if (is_array($response) && isset($response['error'])) {
2278 - wp_send_json_error([
2279 - 'error_message' => $response['error'],
2280 - 'error_code' => $response['error_code'] ?? 'api_error'
2281 - ]);
1297 + // Generate response
1298 + $response = $this->mxchat_generate_response(
1299 + $context_content,
1300 + $this->options['api_key'],
1301 + $this->options['xai_api_key'],
1302 + $this->options['claude_api_key'],
1303 + $this->options['deepseek_api_key'],
1304 + $this->options['gemini_api_key'],
1305 + $conversation_history,
1306 + $is_streaming,
1307 + $session_id,
1308 + $testing_data
1309 + );
1310 +
1311 + // Handle streaming vs non-streaming responses
1312 + if ($is_streaming) {
1313 + // Check if streaming actually happened or if it fell back to regular response
1314 + if ($response === true) {
2282 1315 wp_die();
2283 1316 }
1317 + // If we get here, streaming fell back to regular response, continue
1318 + }
2284 1319
2285 - // DEBUG: Check what we have
2286 - //error_log("=== BEFORE URL VALIDATION ===");
2287 - //error_log("current_valid_urls is empty? " . (empty($this->current_valid_urls) ? 'YES' : 'NO'));
2288 - //error_log("current_valid_urls count: " . count($this->current_valid_urls));
2289 - //error_log("current_valid_urls content: " . print_r($this->current_valid_urls, true));
2290 -
2291 - // If we get here, the response is valid text - now validate URLs
2292 - if (!empty($this->current_valid_urls)) {
2293 - //error_log("CALLING validate_and_clean_urls");
2294 - $response = $this->validate_and_clean_urls($response, $this->current_valid_urls);
2295 - } else {
2296 - //error_log("SKIPPING validation - current_valid_urls is empty");
2297 - }
2298 - // ===== END URL VALIDATION =====
2299 -
2300 - // Prepare RAG context data for storage (only include documents used for context)
2301 - $rag_context_for_storage = null;
2302 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
2303 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
2304 -
2305 - if ($has_rag_data || $has_action_data) {
2306 - $rag_context_for_storage = [];
2307 -
2308 - // Add RAG/source data if available
2309 - if ($has_rag_data) {
2310 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
2311 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
2312 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
2313 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
2314 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
2315 - $rag_context_for_storage['sources_used'] = $this->last_similarity_analysis['sources_used'] ?? 0;
2316 - $rag_context_for_storage['total_chunks_used'] = $this->last_similarity_analysis['total_chunks_used'] ?? 0;
2317 - }
2318 -
2319 - // Add action analysis data if available
2320 - if ($has_action_data) {
2321 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
2322 - }
2323 - }
2324 -
2325 - // Save the cleaned response with RAG context
2326 - $this->mxchat_save_chat_message($session_id, 'bot', $response, null, $rag_context_for_storage);
2327 -
2328 - // Step 5: Save additional content if available
2329 - if (!empty($this->productCardHtml)) {
2330 - $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
2331 - }
2332 -
2333 - if (!empty($this->fallbackResponse['html'])) {
2334 - $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
2335 - }
2336 -
2337 - // Step 6: Return the response
2338 - // DEBUG: Check if newlines exist in the response
2339 - //error_log("=== MXCHAT NON-STREAMING RESPONSE DEBUG ===");
2340 - //error_log("Response has newlines: " . (strpos($response, "\n") !== false ? 'YES' : 'NO'));
2341 - //error_log("Response first 500 chars: " . substr($response, 0, 500));
2342 -
2343 - $response_data = [
2344 - 'text' => $response,
2345 - 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
2346 - 'session_id' => $session_id
2347 - ];
2348 -
2349 - // Include vectorstore error info for admin debugging (only visible to admins via testing_data)
2350 - if (!empty($this->last_vectorstore_error) && $testing_data !== null) {
2351 - $testing_data['vectorstore_error'] = $this->last_vectorstore_error;
2352 - }
2353 -
2354 - // Also pass it as a top-level field so JS can show a better error message to admins
2355 - if (!empty($this->last_vectorstore_error) && current_user_can('manage_options')) {
2356 - $response_data['vectorstore_error'] = $this->last_vectorstore_error;
2357 - }
2358 -
2359 - // Always add testing data for admins (no toggle needed)
2360 - if ($testing_data !== null) {
2361 - $response_data['testing_data'] = $testing_data;
2362 - }
2363 -
2364 - wp_send_json($response_data);
1320 + // Check if the response is an error array
1321 + if (is_array($response) && isset($response['error'])) {
1322 + wp_send_json_error([
1323 + 'error_message' => $response['error'],
1324 + 'error_code' => $response['error_code'] ?? 'api_error'
1325 + ]);
2365 1326 wp_die();
2366 -}
2367 -
2368 -/**
2369 - * Get bot-specific options for multi-bot functionality
2370 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
2371 - */
2372 -// Also debug the bot options retrieval
2373 -private function get_bot_options($bot_id = 'default') {
2374 - //error_log("MXCHAT DEBUG: get_bot_options called for bot: " . $bot_id);
2375 -
2376 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
2377 - //error_log("MXCHAT DEBUG: Using default options (no multi-bot or bot is 'default')");
2378 - return array();
2379 1327 }
2380 1328
2381 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
2382 -
2383 - if (!empty($bot_options)) {
2384 - //error_log("MXCHAT DEBUG: Got bot-specific options from filter");
2385 - if (isset($bot_options['similarity_threshold'])) {
2386 - //error_log(" - similarity_threshold: " . $bot_options['similarity_threshold']);
2387 - }
1329 + // If we get here, the response is valid text
1330 + $this->mxchat_save_chat_message($session_id, 'bot', $response);
1331 +
1332 + // Step 5: Save additional content if available
1333 + if (!empty($this->productCardHtml)) {
1334 + $this->mxchat_save_chat_message($session_id, 'bot', $this->productCardHtml);
2388 1335 }
2389 -
2390 - return is_array($bot_options) ? $bot_options : array();
2391 -}
2392 1336
2393 -/**
2394 - * Get bot-specific Pinecone configuration
2395 - * Used in the knowledge retrieval functions
2396 - */
2397 -// Also add debugging to your get_bot_pinecone_config function
2398 -private function get_bot_pinecone_config($bot_id = 'default') {
2399 - //error_log("MXCHAT DEBUG: get_bot_pinecone_config called for bot: " . $bot_id);
2400 -
2401 - // If default bot or multi-bot add-on not active, use default Pinecone config
2402 - if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
2403 - //error_log("MXCHAT DEBUG: Using default Pinecone config (no multi-bot or bot is 'default')");
2404 - $addon_options = get_option('mxchat_pinecone_addon_options', array());
2405 - $config = array(
2406 - 'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
2407 - 'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
2408 - 'host' => $addon_options['mxchat_pinecone_host'] ?? '',
2409 - 'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
2410 - );
2411 - //error_log("MXCHAT DEBUG: Default config - use_pinecone: " . ($config['use_pinecone'] ? 'true' : 'false'));
2412 - return $config;
1337 + if (!empty($this->fallbackResponse['html'])) {
1338 + $this->mxchat_save_chat_message($session_id, 'bot', $this->fallbackResponse['html']);
2413 1339 }
2414 -
2415 - //error_log("MXCHAT DEBUG: Calling filter 'mxchat_get_bot_pinecone_config' for bot: " . $bot_id);
2416 -
2417 - // Hook for multi-bot add-on to provide bot-specific Pinecone config
2418 - $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
2419 -
2420 - if (!empty($bot_pinecone_config)) {
2421 - //error_log("MXCHAT DEBUG: Got bot-specific config from filter");
2422 - //error_log(" - use_pinecone: " . (isset($bot_pinecone_config['use_pinecone']) ? ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false') : 'not set'));
2423 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'not set'));
2424 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'not set'));
2425 - } else {
2426 - //error_log("MXCHAT DEBUG: Filter returned empty config!");
1340 +
1341 + // Step 6: Return the response
1342 + $response_data = [
1343 + 'text' => $response,
1344 + 'html' => !empty($this->productCardHtml) ? $this->productCardHtml : ($this->fallbackResponse['html'] ?? ''),
1345 + 'session_id' => $session_id
1346 + ];
1347 +
1348 + // Always add testing data for admins (no toggle needed)
1349 + if ($testing_data !== null) {
1350 + $response_data['testing_data'] = $testing_data;
2427 1351 }
2428 -
2429 - return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
1352 +
1353 + wp_send_json($response_data);
1354 + wp_die();
2430 1355 }
2431 1356
2432 -
2433 1357 // Updated function to check intents and invoke the callback function
2434 1358 private function mxchat_check_intent_and_invoke_callback($message, $user_id, $session_id) {
2435 1359 global $wpdb;
2436 1360 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
2437 1361
2438 - // Get the current bot_id
2439 - $current_bot_id = $this->get_current_bot_id($session_id);
2440 -
2441 1362 // Generate the user embedding
2442 1363 $user_embedding = $this->mxchat_generate_embedding($message, $this->options['api_key']);
2443 -
1364 +
2444 1365 // Check if embedding generation returned an error
2445 1366 if (is_array($user_embedding) && isset($user_embedding['error'])) {
2446 1367 $error_message = $user_embedding['error'];
2447 1368 $error_code = $user_embedding['error_code'] ?? 'embedding_error';
2448 -
2449 - // FIXED: Send error in appropriate format based on streaming mode
2450 - if ($this->is_streaming) {
2451 - echo "data: " . json_encode([
2452 - 'error' => true,
2453 - 'error_message' => $error_message,
2454 - 'error_code' => $error_code,
2455 - 'text' => $error_message,
2456 - 'message' => $error_message
2457 - ]) . "\n\n";
2458 - echo "data: [DONE]\n\n";
2459 - flush();
2460 - } else {
2461 - wp_send_json_error([
2462 - 'error_message' => $error_message,
2463 - 'error_code' => $error_code
2464 - ]);
2465 - }
1369 +
1370 + wp_send_json_error([
1371 + 'error_message' => $error_message,
1372 + 'error_code' => $error_code
1373 + ]);
2466 1374 wp_die();
2467 1375 }
2468 -
1376 +
2469 1377 // Check if embedding is valid
2470 1378 if (!is_array($user_embedding) || empty($user_embedding)) {
2471 - $error_message = esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat');
2472 -
2473 - // FIXED: Send error in appropriate format based on streaming mode
2474 - if ($this->is_streaming) {
2475 - echo "data: " . json_encode([
2476 - 'error' => true,
2477 - 'error_message' => $error_message,
2478 - 'error_code' => 'invalid_embedding',
2479 - 'text' => $error_message,
2480 - 'message' => $error_message
2481 - ]) . "\n\n";
2482 - echo "data: [DONE]\n\n";
2483 - flush();
2484 - } else {
2485 - wp_send_json_error([
2486 - 'error_message' => $error_message,
2487 - 'error_code' => 'invalid_embedding'
2488 - ]);
2489 - }
1379 + wp_send_json_error([
1380 + 'error_message' => esc_html__('Unable to process your message. The embedding service is not responding correctly.', 'mxchat'),
1381 + 'error_code' => 'invalid_embedding'
1382 + ]);
2490 1383 wp_die();
2491 1384 }
2492 -
1385 +
2493 1386 // Fetch intents from the database
2494 1387 $table_name = $wpdb->prefix . 'mxchat_intents';
2495 1388 if ($chat_mode === 'agent') {
2496 1389 $query = $wpdb->prepare(
@@ -2500,29 +1393,19 @@
2500 1393 $intents = $wpdb->get_results($query);
2501 1394 } else {
2502 1395 $intents = $wpdb->get_results("SELECT * FROM $table_name WHERE enabled = 1 OR enabled IS NULL");
2503 1396 }
2504 -
1397 +
2505 1398 if (empty($intents)) {
2506 1399 return false;
2507 1400 }
2508 -
2509 - // Prefetch individual phrase vectors from wp_mxchat_intent_phrases (grouped by intent_id)
2510 - $phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
2511 - $phrases_by_intent = [];
2512 - if ($wpdb->get_var("SHOW TABLES LIKE '$phrases_table'") === $phrases_table) {
2513 - $all_phrases = $wpdb->get_results("SELECT intent_id, phrase, embedding_vector FROM $phrases_table");
2514 - foreach ($all_phrases as $p) {
2515 - $phrases_by_intent[$p->intent_id][] = $p;
2516 - }
2517 - }
2518 -
1401 +
2519 1402 $highest_similarity = -INF;
2520 1403 $matched_intent = null;
2521 -
2522 - // Array to store action analysis for testing panel
1404 +
1405 + // NEW: Array to store action analysis for testing panel
2523 1406 $action_analysis = [];
2524 -
1407 +
2525 1408 foreach ($intents as $intent) {
2526 1409 // Additional check for enabled state
2527 1410 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2528 1411 if (!$is_enabled) {
@@ -2527,57 +1410,22 @@
2527 1410 $is_enabled = isset($intent->enabled) ? (bool)$intent->enabled : true;
2528 1411 if (!$is_enabled) {
2529 1412 continue;
2530 1413 }
2531 -
2532 - // Check if this action is enabled for the current bot
2533 - if (!$this->is_action_enabled_for_bot($intent, $current_bot_id)) {
2534 - continue;
2535 - }
2536 -
2537 - $best_similarity = -INF;
2538 - $matched_phrase_text = '';
2539 -
2540 - // Check legacy embedding vector (existing behavior)
1414 +
2541 1415 $intent_embedding_serialized = $intent->embedding_vector;
2542 1416 $intent_embedding = $intent_embedding_serialized
2543 1417 ? unserialize($intent_embedding_serialized, ['allowed_classes' => false])
2544 1418 : null;
2545 -
2546 - if (is_array($intent_embedding) && !empty($intent_embedding)) {
2547 - $legacy_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2548 - if ($legacy_similarity > $best_similarity) {
2549 - $best_similarity = $legacy_similarity;
2550 - $matched_phrase_text = 'legacy';
2551 - }
2552 - }
2553 -
2554 - // Check individual phrase vectors
2555 - if (isset($phrases_by_intent[$intent->id])) {
2556 - foreach ($phrases_by_intent[$intent->id] as $phrase_row) {
2557 - $phrase_embedding = $phrase_row->embedding_vector
2558 - ? unserialize($phrase_row->embedding_vector, ['allowed_classes' => false])
2559 - : null;
2560 - if (!is_array($phrase_embedding)) {
2561 - continue;
2562 - }
2563 - $phrase_similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $phrase_embedding);
2564 - if ($phrase_similarity > $best_similarity) {
2565 - $best_similarity = $phrase_similarity;
2566 - $matched_phrase_text = $phrase_row->phrase;
2567 - }
2568 - }
2569 - }
2570 -
2571 - // Skip if no valid embedding was found at all
2572 - if ($best_similarity === -INF) {
1419 +
1420 + if (!is_array($intent_embedding)) {
2573 1421 continue;
2574 1422 }
2575 -
2576 - $similarity = $best_similarity;
1423 +
1424 + $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $intent_embedding);
2577 1425 $intent_threshold = isset($intent->similarity_threshold) ? $intent->similarity_threshold : 0.85;
2578 -
2579 - // Store action analysis data for testing panel
1426 +
1427 + // NEW: Store action analysis data for testing panel
2580 1428 $action_analysis[] = [
2581 1429 'intent_label' => $intent->intent_label,
2582 1430 'callback_function' => $intent->callback_function,
2583 1431 'similarity' => round($similarity, 4),
@@ -2584,12 +1432,11 @@
2584 1432 'similarity_percentage' => round($similarity * 100, 2),
2585 1433 'threshold' => $intent_threshold,
2586 1434 'threshold_percentage' => round($intent_threshold * 100, 2),
2587 1435 'above_threshold' => $similarity >= $intent_threshold,
2588 - 'matched_phrase' => $matched_phrase_text,
2589 1436 'triggered' => false // Will be updated below if this intent is triggered
2590 1437 ];
2591 -
1438 +
2592 1439 if ($similarity >= $intent_threshold && $similarity > $highest_similarity) {
2593 1440 $highest_similarity = $similarity;
2594 1441 $matched_intent = $intent;
2595 1442 }
@@ -2594,9 +1441,9 @@
2594 1441 $matched_intent = $intent;
2595 1442 }
2596 1443 }
2597 1444
2598 - // Mark the triggered action if any
1445 + // NEW: Mark the triggered action if any
2599 1446 if ($matched_intent) {
2600 1447 foreach ($action_analysis as &$action) {
2601 1448 if ($action['intent_label'] === $matched_intent->intent_label) {
2602 1449 $action['triggered'] = true;
@@ -2604,9 +1451,9 @@
2604 1451 }
2605 1452 }
2606 1453 }
2607 1454
2608 - // Sort actions by similarity (highest first) and store for testing panel
1455 + // NEW: Sort actions by similarity (highest first) and store for testing panel
2609 1456 usort($action_analysis, function($a, $b) {
2610 1457 return $b['similarity'] <=> $a['similarity'];
2611 1458 });
2612 1459
@@ -2612,9 +1459,8 @@
2612 1459
2613 1460 // Store action analysis for testing panel capture
2614 1461 $this->last_action_analysis = $action_analysis;
2615 1462
2616 - // Around line 715 in your mxchat_check_intent_and_invoke_callback function
2617 1463 if ($matched_intent) {
2618 1464 // If the callback is a method on this instance (core callback), call it directly
2619 1465 if (method_exists($this, $matched_intent->callback_function)) {
2620 1466 $callback_result = call_user_func(
@@ -2628,9 +1474,9 @@
2628 1474 } else {
2629 1475 // Otherwise, use apply_filters for add-on callbacks
2630 1476 $callback_result = apply_filters(
2631 1477 $matched_intent->callback_function,
2632 - false,
1478 + false, // default return value
2633 1479 $message,
2634 1480 $user_id,
2635 1481 $session_id,
2636 1482 $matched_intent
@@ -2636,18 +1482,11 @@
2636 1482 $matched_intent
2637 1483 );
2638 1484 }
2639 1485
2640 - // Handle the callback result properly
2641 1486 if ($callback_result !== false) {
2642 - // If callback returned an array with chat_mode, use it directly
2643 - if (is_array($callback_result) && isset($callback_result['chat_mode'])) {
2644 - $this->fallbackResponse = $callback_result;
2645 - return $callback_result; // Return the full array
2646 - } else {
2647 - $this->fallbackResponse = $callback_result;
2648 - return true;
2649 - }
1487 + $this->fallbackResponse = $callback_result;
1488 + return true;
2650 1489 }
2651 1490 }
2652 1491
2653 1492 return false;
@@ -2652,34 +1491,8 @@
2652 1491
2653 1492 return false;
2654 1493 }
2655 1494
2656 -/**
2657 - * Check if an action is enabled for a specific bot
2658 - */
2659 -private function is_action_enabled_for_bot($intent, $bot_id) {
2660 - // If enabled_bots column doesn't exist or is null, assume it's enabled for all bots (backward compatibility)
2661 - if (!isset($intent->enabled_bots) || empty($intent->enabled_bots)) {
2662 - return true;
2663 - }
2664 -
2665 - $enabled_bots = json_decode($intent->enabled_bots, true);
2666 -
2667 - // If JSON decode fails or returns empty array, assume enabled for all (backward compatibility)
2668 - if (!is_array($enabled_bots) || empty($enabled_bots)) {
2669 - return true;
2670 - }
2671 -
2672 - // Admin testing tab uses bot_id "testing" — treat it as "default" so all
2673 - // default-bot actions are testable from the admin panel
2674 - if ($bot_id === 'testing') {
2675 - $bot_id = 'default';
2676 - }
2677 -
2678 - // Check if the current bot is in the enabled bots list
2679 - return in_array($bot_id, $enabled_bots);
2680 -}
2681 -
2682 1495 // Helper function to clear PDF and Word document related transients
2683 1496 private function clear_pdf_transients($session_id) {
2684 1497 // PDF transients
2685 1498 delete_transient('mxchat_pdf_url_' . $session_id);
@@ -2713,23 +1526,18 @@
2713 1526 }
2714 1527
2715 1528 public function mxchat_generate_image($message, $user_id, $session_id) {
2716 1529 //error_log("Starting image generation for message: " . $message);
2717 -
2718 - // Prepare a prompt for OpenAI image generation
1530 +
1531 + // Prepare a prompt for DALL-E
2719 1532 $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2720 -
2721 - // Opt-in routing: when 'custom_provider_for_images' is on, route image gen
2722 - // through the configured Custom (OpenAI-compatible) /images/generations route.
2723 - if (!empty($this->options['custom_provider_for_images']) && $this->options['custom_provider_for_images'] === 'on') {
2724 - $image_response = $this->mxchat_generate_custom_image($prompt);
2725 - } else {
2726 - // Use the existing OpenAI API key
2727 - $openai_api_key = sanitize_text_field($this->options['api_key']);
2728 - // Call OpenAI GPT Image to generate an image
2729 - $image_response = $this->mxchat_generate_openai_image($prompt, $openai_api_key);
2730 - }
2731 1533
1534 + // Use the existing OpenAI API key
1535 + $openai_api_key = sanitize_text_field($this->options['api_key']);
1536 +
1537 + // Call DALL-E to generate an image
1538 + $image_response = $this->mxchat_generate_dalle_image($prompt, $openai_api_key);
1539 +
2732 1540 // Check if the response contains an image URL
2733 1541 if (isset($image_response['imageUrl'])) {
2734 1542 $image_url = esc_url_raw($image_response['imageUrl']);
2735 1543
@@ -2772,125 +1580,24 @@
2772 1580 // Return the response directly instead of relying on the property
2773 1581 return $this->fallbackResponse;
2774 1582 }
2775 1583 }
2776 -
2777 -public function mxchat_generate_gemini_image($message, $user_id, $session_id) {
2778 - $prompt = esc_html__('Create an image of ', 'mxchat') . sanitize_text_field($message);
2779 -
2780 - $gemini_api_key = sanitize_text_field($this->options['gemini_api_key'] ?? '');
2781 - if (empty($gemini_api_key)) {
2782 - $response_text = esc_html__("Gemini API key is not configured.", 'mxchat');
2783 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2784 - return ['text' => $response_text, 'html' => '', 'images' => []];
2785 - }
2786 -
2787 - $image_response = $this->mxchat_generate_imagen_image($prompt, $gemini_api_key);
2788 -
2789 - if (isset($image_response['imageUrl'])) {
2790 - $image_url = esc_url_raw($image_response['imageUrl']);
2791 -
2792 - $response_html = '<img src="' . esc_url($image_url) . '" alt="' . esc_attr__('Generated Image', 'mxchat') . '" class="mxchat-generated-image" />';
2793 - $response_text = esc_html__('Here is the image I generated:', 'mxchat');
2794 -
2795 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2796 - $this->mxchat_save_chat_message($session_id, 'bot', $response_html);
2797 -
2798 - $this->fallbackResponse = [
2799 - 'text' => $response_text,
2800 - 'html' => $response_html,
2801 - 'images' => [$image_url]
2802 - ];
2803 -
2804 - return $this->fallbackResponse;
2805 - } else {
2806 - $response_text = esc_html__("I'm sorry, but I couldn't generate an image based on your request.", 'mxchat');
2807 -
2808 - $this->mxchat_save_chat_message($session_id, 'bot', $response_text);
2809 -
2810 - $this->fallbackResponse = [
2811 - 'text' => $response_text,
2812 - 'html' => '',
2813 - 'images' => []
2814 - ];
2815 -
2816 - return $this->fallbackResponse;
2817 - }
2818 -}
2819 -
2820 -private function mxchat_save_generated_image($base64_data, $mime_type = 'image/png', $prefix = 'mxchat-generated') {
2821 - // Map the real mime type to a matching file extension so the saved file's
2822 - // extension always agrees with its bytes. A mismatch (e.g. Imagen returning
2823 - // webp bytes that were written into a ".png" file) makes the browser refuse
2824 - // to render the image even though the file saved successfully and the bot
2825 - // reported success — that was the Gemini/Imagen "image never renders" bug.
2826 - // OpenAI + custom-provider paths pass 'image/png' explicitly, so they are
2827 - // unaffected; this only matters for providers that return another type.
2828 - $mime_to_ext = [
2829 - 'image/jpeg' => 'jpg',
2830 - 'image/jpg' => 'jpg',
2831 - 'image/png' => 'png',
2832 - 'image/webp' => 'webp',
2833 - 'image/gif' => 'gif',
2834 - ];
2835 - $mime_type = strtolower(trim((string) $mime_type));
2836 - if (isset($mime_to_ext[$mime_type])) {
2837 - $extension = $mime_to_ext[$mime_type];
2838 - } else {
2839 - // Unknown/unsupported type: fall back to png and normalize the stored
2840 - // mime so the attachment record and the file extension stay consistent.
2841 - $extension = 'png';
2842 - $mime_type = 'image/png';
2843 - }
2844 - $filename = sanitize_file_name($prefix . '-' . wp_generate_uuid4() . '.' . $extension);
2845 - $decoded = base64_decode($base64_data);
2846 -
2847 - if ($decoded === false) {
2848 - return new \WP_Error('decode_failed', esc_html__('Failed to decode image data.', 'mxchat'));
2849 - }
2850 -
2851 - $upload = wp_upload_bits($filename, null, $decoded);
2852 -
2853 - if (!empty($upload['error'])) {
2854 - return new \WP_Error('upload_failed', $upload['error']);
2855 - }
2856 -
2857 - $attach_id = wp_insert_attachment([
2858 - 'post_mime_type' => $mime_type,
2859 - 'post_title' => $prefix,
2860 - 'post_content' => '',
2861 - 'post_status' => 'inherit',
2862 - ], $upload['file']);
2863 -
2864 - if (is_wp_error($attach_id)) {
2865 - return $attach_id;
2866 - }
2867 -
2868 - require_once ABSPATH . 'wp-admin/includes/image.php';
2869 - $metadata = wp_generate_attachment_metadata($attach_id, $upload['file']);
2870 - wp_update_attachment_metadata($attach_id, $metadata);
2871 -
2872 - return esc_url_raw(wp_get_attachment_url($attach_id));
2873 -}
2874 -
2875 -private function mxchat_generate_openai_image($prompt, $api_key, $model = 'gpt-image-1', $timeout = 60) {
1584 +private function mxchat_generate_dalle_image($prompt, $api_key, $model = 'dall-e-3', $timeout = 60) {
2876 1585 $api_url = 'https://api.openai.com/v1/images/generations';
2877 1586 $body = json_encode([
2878 - 'prompt' => sanitize_text_field($prompt),
2879 - 'n' => 1,
2880 - 'size' => '1024x1024',
2881 - 'quality' => 'medium',
2882 - 'output_format' => 'png',
2883 - 'model' => sanitize_text_field($model),
1587 + 'prompt' => sanitize_text_field($prompt),
1588 + 'n' => 1,
1589 + 'size' => '1024x1024',
1590 + 'model' => sanitize_text_field($model),
2884 1591 ]);
2885 1592
2886 1593 $args = [
2887 - 'body' => $body,
1594 + 'body' => $body,
2888 1595 'headers' => [
2889 - 'Content-Type' => 'application/json',
1596 + 'Content-Type' => 'application/json',
2890 1597 'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
2891 1598 ],
2892 - 'method' => 'POST',
1599 + 'method' => 'POST',
2893 1600 'timeout' => absint($timeout),
2894 1601 ];
2895 1602
2896 1603 $response = wp_remote_post($api_url, $args);
@@ -2895,114 +1602,23 @@
2895 1602
2896 1603 $response = wp_remote_post($api_url, $args);
2897 1604
2898 1605 if (is_wp_error($response)) {
1606 + //error_log("DALL-E request failed: " . $response->get_error_message());
2899 1607 return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2900 1608 }
2901 1609
2902 1610 $response_body = json_decode(wp_remote_retrieve_body($response), true);
2903 1611
2904 - $b64 = $response_body['data'][0]['b64_json'] ?? $response_body['data'][0]['b64'] ?? null;
2905 - if ($b64) {
2906 - $saved_url = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-openai');
2907 - if (is_wp_error($saved_url)) {
2908 - return ['error' => $saved_url->get_error_message()];
2909 - }
2910 - return ['imageUrl' => $saved_url];
1612 + if (isset($response_body['data'][0]['url'])) {
1613 + return ['imageUrl' => esc_url_raw($response_body['data'][0]['url'])];
2911 1614 } else {
1615 + //error_log("DALL-E response error: " . wp_remote_retrieve_body($response));
2912 1616 return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
2913 1617 }
2914 1618 }
2915 1619
2916 1620 /**
2917 - * Generate an image via a Custom (OpenAI-compatible) provider's /images/generations route.
2918 - * Only called when the opt-in 'custom_provider_for_images' setting is on.
2919 - */
2920 -private function mxchat_generate_custom_image($prompt, $timeout = 90) {
2921 - $cfg = $this->mxchat_resolve_custom_provider();
2922 - if (empty($cfg['base_url'])) {
2923 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat')];
2924 - }
2925 - $url = $cfg['base_url'] . '/images/generations';
2926 - if (!empty($cfg['api_version'])) {
2927 - $url .= (strpos($url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
2928 - }
2929 - $body = wp_json_encode([
2930 - 'prompt' => sanitize_text_field($prompt),
2931 - 'n' => 1,
2932 - 'size' => '1024x1024',
2933 - 'model' => $cfg['model'],
2934 - ]);
2935 - $response = wp_remote_post($url, [
2936 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
2937 - 'body' => $body,
2938 - 'method' => 'POST',
2939 - 'timeout' => absint($timeout),
2940 - ]);
2941 - if (is_wp_error($response)) {
2942 - return ['error' => esc_html__('Error generating image (custom provider): ', 'mxchat') . $response->get_error_message()];
2943 - }
2944 - $resp = json_decode(wp_remote_retrieve_body($response), true);
2945 - // Try b64 first (matches OpenAI shape), then url-based fallback.
2946 - $b64 = $resp['data'][0]['b64_json'] ?? $resp['data'][0]['b64'] ?? null;
2947 - if ($b64) {
2948 - $saved = $this->mxchat_save_generated_image($b64, 'image/png', 'mxchat-custom');
2949 - if (is_wp_error($saved)) {
2950 - return ['error' => $saved->get_error_message()];
2951 - }
2952 - return ['imageUrl' => $saved];
2953 - }
2954 - $remote_url = $resp['data'][0]['url'] ?? null;
2955 - if ($remote_url) {
2956 - return ['imageUrl' => esc_url_raw($remote_url)];
2957 - }
2958 - $err_msg = $resp['error']['message'] ?? esc_html__('Custom provider did not return an image.', 'mxchat');
2959 - return ['error' => esc_html($err_msg)];
2960 -}
2961 -
2962 -private function mxchat_generate_imagen_image($prompt, $api_key, $timeout = 60) {
2963 - $api_url = 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict';
2964 -
2965 - $body = json_encode([
2966 - 'instances' => [['prompt' => sanitize_text_field($prompt)]],
2967 - 'parameters' => [
2968 - 'sampleCount' => 1,
2969 - 'aspectRatio' => '1:1',
2970 - ],
2971 - ]);
2972 -
2973 - $args = [
2974 - 'body' => $body,
2975 - 'headers' => [
2976 - 'Content-Type' => 'application/json',
2977 - 'x-goog-api-key' => sanitize_text_field($api_key),
2978 - ],
2979 - 'method' => 'POST',
2980 - 'timeout' => absint($timeout),
2981 - ];
2982 -
2983 - $response = wp_remote_post($api_url, $args);
2984 -
2985 - if (is_wp_error($response)) {
2986 - return ['error' => esc_html__('Error generating image: ', 'mxchat') . $response->get_error_message()];
2987 - }
2988 -
2989 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
2990 -
2991 - $b64 = $response_body['predictions'][0]['bytesBase64Encoded'] ?? $response_body['predictions'][0]['imageBytes'] ?? null;
2992 - if ($b64) {
2993 - $mime = $response_body['predictions'][0]['mimeType'] ?? 'image/png';
2994 - $saved_url = $this->mxchat_save_generated_image($b64, $mime, 'mxchat-gemini');
2995 - if (is_wp_error($saved_url)) {
2996 - return ['error' => $saved_url->get_error_message()];
2997 - }
2998 - return ['imageUrl' => $saved_url];
2999 - } else {
3000 - return ['error' => esc_html__('Failed to generate image.', 'mxchat')];
3001 - }
3002 -}
3003 -
3004 -/**
3005 1621 * Handle web search requests.
3006 1622 *
3007 1623 * Sends the refined search query to the Brave Search API and uses the
3008 1624 * results to generate a conversational response with the AI model.
@@ -3050,10 +1666,10 @@
3050 1666 $transient_key = 'mxchat_search_' . md5($refined_search_query);
3051 1667 $results = get_transient($transient_key);
3052 1668
3053 1669 if (false === $results) {
3054 - // SECURITY FIX: Changed to wp_safe_remote_get
3055 - $response = wp_safe_remote_get(
1670 + // Fetch new results from the Brave Search API
1671 + $response = wp_remote_get(
3056 1672 $api_url,
3057 1673 array(
3058 1674 'headers' => array(
3059 1675 'Accept' => 'application/json',
@@ -3192,10 +1808,9 @@
3192 1808 ],
3193 1809 'timeout' => 10,
3194 1810 ];
3195 1811
3196 - // SECURITY FIX: Changed to wp_safe_remote_get
3197 - $response = wp_safe_remote_get($api_url, $args);
1812 + $response = wp_remote_get($api_url, $args);
3198 1813
3199 1814 if (is_wp_error($response)) {
3200 1815 return array(
3201 1816 'text' => __("I'm sorry, I couldn't retrieve any images based on your request.", 'mxchat'),
@@ -3265,22 +1880,17 @@
3265 1880 * @return string The refined search query
3266 1881 */
3267 1882 public function mxchat_interpret_search_query($user_query) {
3268 1883 $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');
3269 -
1884 +
3270 1885 // Get options and determine the selected model
3271 1886 $options = $this->options ?? get_option('mxchat_options');
3272 - $selected_model = isset($options['model']) ? $options['model'] : 'gpt-5.1-chat-latest';
3273 -
3274 - // Custom (OpenAI-compatible) provider routes by model id, not prefix.
3275 - if ($selected_model === 'custom-provider') {
3276 - return $this->interpret_query_with_custom($user_query, $system_prompt);
3277 - }
3278 -
1887 + $selected_model = isset($options['model']) ? $options['model'] : 'gpt-4o';
1888 +
3279 1889 // Extract model prefix to determine the provider
3280 1890 $model_parts = explode('-', $selected_model);
3281 1891 $provider = strtolower($model_parts[0]);
3282 -
1892 +
3283 1893 // Determine which API key to use based on the provider
3284 1894 switch ($provider) {
3285 1895 case 'gemini':
3286 1896 $api_key = isset($options['gemini_api_key']) ? sanitize_text_field($options['gemini_api_key']) : '';
@@ -3321,60 +1931,11 @@
3321 1931 }
3322 1932 }
3323 1933
3324 1934 /**
3325 - * Interpret query against the configured Custom (OpenAI-compatible) provider.
3326 - * Uses the same base URL + auth scheme as the chat dispatcher.
3327 - */
3328 -private function interpret_query_with_custom($user_query, $system_prompt) {
3329 - $cfg = $this->mxchat_resolve_custom_provider();
3330 - if (empty($cfg['base_url'])) {
3331 - return sanitize_text_field($user_query);
3332 - }
3333 - $args = [
3334 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
3335 - 'body' => wp_json_encode([
3336 - 'model' => $cfg['model'],
3337 - 'messages' => [
3338 - ['role' => 'system', 'content' => $system_prompt],
3339 - ['role' => 'user', 'content' => sanitize_text_field($user_query)],
3340 - ],
3341 - 'temperature' => 0.2,
3342 - 'max_tokens' => 20,
3343 - ]),
3344 - 'method' => 'POST',
3345 - 'timeout' => 15,
3346 - ];
3347 - $response = wp_remote_post($cfg['chat_url'], $args);
3348 - if (is_wp_error($response)) {
3349 - return sanitize_text_field($user_query);
3350 - }
3351 - $body = json_decode(wp_remote_retrieve_body($response), true);
3352 - return isset($body['choices'][0]['message']['content'])
3353 - ? sanitize_text_field(trim($body['choices'][0]['message']['content']))
3354 - : sanitize_text_field($user_query);
3355 -}
3356 -
3357 -/**
3358 - * Convert the colon-style header list returned by mxchat_resolve_custom_provider
3359 - * into the assoc-array form wp_remote_post expects.
3360 - */
3361 -private function mxchat_custom_provider_assoc_headers($cfg) {
3362 - $headers = ['Content-Type' => 'application/json'];
3363 - if (!empty($cfg['api_key'])) {
3364 - if (($cfg['auth_scheme'] ?? 'bearer') === 'api-key') {
3365 - $headers['api-key'] = $cfg['api_key'];
3366 - } else {
3367 - $headers['Authorization'] = 'Bearer ' . $cfg['api_key'];
3368 - }
3369 - }
3370 - return $headers;
3371 -}
3372 -
3373 -/**
3374 1935 * Interpret query using OpenAI models
3375 1936 */
3376 -private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-5.1-chat-latest') {
1937 +private function interpret_query_with_openai($user_query, $system_prompt, $api_key, $model = 'gpt-4o') {
3377 1938 $url = 'https://api.openai.com/v1/chat/completions';
3378 1939 $args = [
3379 1940 'headers' => [
3380 1941 'Authorization' => 'Bearer ' . $api_key,
@@ -3404,40 +1965,13 @@
3404 1965 : sanitize_text_field($user_query);
3405 1966 }
3406 1967
3407 1968 /**
3408 - * Anthropic removed temperature/top_p/top_k starting with Opus 4.7 (the API
3409 - * returns 400 if sent) — add new flagship model ids here. (We don't send
3410 - * top_p/top_k in any Claude body, so the list only needs to gate temperature
3411 - * stripping. We never send a `thinking` param either, which is required for
3412 - * claude-fable-5: it rejects an explicit thinking "disabled" — omit only.)
3413 - */
3414 -private function mxchat_claude_omits_temperature($model) {
3415 - $no_temp = array('claude-opus-4-7', 'claude-opus-4-8', 'claude-fable-5');
3416 - return in_array($model, $no_temp, true);
3417 -}
3418 -
3419 -/**
3420 1969 * Interpret query using Claude models
3421 1970 */
3422 1971 private function interpret_query_with_claude($user_query, $system_prompt, $api_key, $model) {
3423 - // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
3424 - // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
3425 - if ($model === 'claude-opus-4-20250514') { $model = 'claude-opus-4-8'; }
3426 - elseif ($model === 'claude-sonnet-4-20250514') { $model = 'claude-sonnet-4-6'; }
3427 1972 $url = 'https://api.anthropic.com/v1/messages';
3428 -
3429 - $payload = [
3430 - 'model' => $model,
3431 - 'system' => $system_prompt,
3432 - 'messages' => [
3433 - ['role' => 'user', 'content' => sanitize_text_field($user_query)]
3434 - ],
3435 - 'max_tokens' => 20,
3436 - 'temperature' => 0.2,
3437 - ];
3438 - if ($this->mxchat_claude_omits_temperature($model)) { unset($payload['temperature']); }
3439 -
1973 +
3440 1974 $args = [
3441 1975 'headers' => [
3442 1976 'Content-Type' => 'application/json',
3443 1977 'x-api-key' => $api_key,
@@ -3442,9 +1976,17 @@
3442 1976 'Content-Type' => 'application/json',
3443 1977 'x-api-key' => $api_key,
3444 1978 'anthropic-version' => '2023-06-01',
3445 1979 ],
3446 - 'body' => wp_json_encode($payload),
1980 + 'body' => wp_json_encode([
1981 + 'model' => $model,
1982 + 'system' => $system_prompt,
1983 + 'messages' => [
1984 + ['role' => 'user', 'content' => sanitize_text_field($user_query)]
1985 + ],
1986 + 'max_tokens' => 20,
1987 + 'temperature' => 0.2,
1988 + ]),
3447 1989 'method' => 'POST',
3448 1990 'timeout' => 15,
3449 1991 ];
3450 1992
@@ -3453,16 +1995,12 @@
3453 1995 return sanitize_text_field($user_query);
3454 1996 }
3455 1997
3456 1998 $body = json_decode(wp_remote_retrieve_body($response), true);
3457 - // claude-fable-5 prepends a thinking block to content — take the first
3458 - // TEXT block, not content[0].
3459 - foreach ((array) ($body['content'] ?? array()) as $block) {
3460 - if (isset($block['type'], $block['text']) && $block['type'] === 'text' && trim($block['text']) !== '') {
3461 - return sanitize_text_field(trim($block['text']));
3462 - }
1999 + if (!empty($body['content'][0]['text'])) {
2000 + return sanitize_text_field(trim($body['content'][0]['text']));
3463 2001 }
3464 -
2002 +
3465 2003 return sanitize_text_field($user_query);
3466 2004 }
3467 2005
3468 2006 /**
@@ -3468,16 +2006,13 @@
3468 2006 /**
3469 2007 * Interpret query using Gemini models
3470 2008 */
3471 2009 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
3472 - if ($model === 'gemini-3-pro-preview') {
3473 - $model = 'gemini-3.1-pro-preview';
3474 - }
3475 - // Use v1beta for preview models, v1 for stable models
3476 - $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1';
3477 -
3478 - $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key);
2010 + // Strip "gemini-" prefix for the API
2011 + $model_version = str_replace('gemini-', '', $model);
3479 2012
2013 + $url = "https://generativelanguage.googleapis.com/v1/models/$model_version:generateContent?key=" . urlencode($api_key);
2014 +
3480 2015 $args = [
3481 2016 'headers' => [
3482 2017 'Content-Type' => 'application/json',
3483 2018 ],
@@ -3673,55 +2208,55 @@
3673 2208 }
3674 2209
3675 2210
3676 2211 /**
3677 - * Enhanced fetch_and_split_pdf_pages with SSRF protection
2212 + * Enhanced fetch_and_split_pdf_pages with detailed debugging
3678 2213 */
3679 2214 private function fetch_and_split_pdf_pages($pdf_source, $max_pages) {
3680 2215 // CLEAR DEBUG LOGGING
3681 - //error_log("=== MXCHAT PDF PROCESSING START ===");
3682 - //error_log("PDF Source: " . $pdf_source);
3683 - //error_log("Max Pages: " . $max_pages);
3684 - //error_log("Session ID: " . ($this->session_id ?? 'not set'));
2216 + error_log("=== MXCHAT PDF PROCESSING START ===");
2217 + error_log("PDF Source: " . $pdf_source);
2218 + error_log("Max Pages: " . $max_pages);
2219 + error_log("Session ID: " . ($this->session_id ?? 'not set'));
3685 2220
3686 2221 // Check if Advanced Claude Toolbar is available and enabled
3687 2222 $claude_available = function_exists('mxchatACT_is_advanced_claude_enabled');
3688 2223 $claude_enabled = $claude_available ? mxchatACT_is_advanced_claude_enabled() : false;
3689 2224
3690 - //error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
3691 - //error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
2225 + error_log("Claude Function Available: " . ($claude_available ? 'YES' : 'NO'));
2226 + error_log("Claude Enabled: " . ($claude_enabled ? 'YES' : 'NO'));
3692 2227
3693 2228 if ($claude_available && $claude_enabled) {
3694 - //error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
2229 + error_log("🚀 ATTEMPTING CLAUDE PROCESSING...");
3695 2230
3696 2231 // Attempt Claude processing first
3697 2232 $claude_result = apply_filters('mxchat_process_pdf_advanced', false, $pdf_source, $max_pages, $this->session_id);
3698 2233
3699 2234 if ($claude_result !== false && is_array($claude_result) && !empty($claude_result)) {
3700 - //error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
3701 - //error_log("Claude returned " . count($claude_result) . " processed pages");
2235 + error_log("✅ CLAUDE PROCESSING SUCCESSFUL!");
2236 + error_log("Claude returned " . count($claude_result) . " processed pages");
3702 2237
3703 2238 // Log first page details for verification
3704 2239 if (isset($claude_result[0])) {
3705 2240 $first_page = $claude_result[0];
3706 - //error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
3707 - //error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
3708 - //error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
2241 + error_log("First page enhanced: " . (isset($first_page['enhanced']) && $first_page['enhanced'] ? 'YES' : 'NO'));
2242 + error_log("Processing method: " . ($first_page['processing_method'] ?? 'not set'));
2243 + error_log("First page text preview: " . substr($first_page['text'] ?? '', 0, 100) . "...");
3709 2244 }
3710 2245
3711 - //error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
2246 + error_log("=== MXCHAT PDF PROCESSING END (CLAUDE) ===");
3712 2247 return $claude_result;
3713 2248 } else {
3714 - //error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
3715 - //error_log("Claude result type: " . gettype($claude_result));
2249 + error_log("❌ CLAUDE PROCESSING FAILED or returned invalid result");
2250 + error_log("Claude result type: " . gettype($claude_result));
3716 2251 if (is_array($claude_result)) {
3717 - //error_log("Claude result count: " . count($claude_result));
2252 + error_log("Claude result count: " . count($claude_result));
3718 2253 }
3719 2254 }
3720 2255 }
3721 2256
3722 2257 // Fallback to basic processing
3723 - //error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
2258 + error_log("🔄 FALLING BACK TO BASIC PDF PROCESSING...");
3724 2259
3725 2260 $upload_dir = wp_upload_dir();
3726 2261 $temp_file = null;
3727 2262
@@ -3729,20 +2264,11 @@
3729 2264 // Your existing basic processing code here...
3730 2265 // (I'll include the key parts with debug logging)
3731 2266
3732 2267 if (filter_var($pdf_source, FILTER_VALIDATE_URL)) {
3733 - //error_log("Downloading PDF from URL...");
3734 -
3735 - // SECURITY FIX: Validate URL before processing
3736 - if (!$this->mxchat_is_safe_pdf_url($pdf_source)) {
3737 - //error_log("❌ SECURITY: Blocked unsafe PDF URL");
3738 - return false;
3739 - }
3740 -
2268 + error_log("Downloading PDF from URL...");
3741 2269 $temp_file = wp_tempnam($pdf_source);
3742 -
3743 - // SECURITY FIX: Changed from wp_remote_get to wp_safe_remote_get
3744 - $response = wp_safe_remote_get($pdf_source, [
2270 + $response = wp_remote_get($pdf_source, [
3745 2271 'timeout' => 60,
3746 2272 'headers' => ['User-Agent' => 'MxChat PDF Processor']
3747 2273 ]);
3748 2274
@@ -3747,35 +2273,29 @@
3747 2273 ]);
3748 2274
3749 2275 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
3750 2276 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
3751 - //error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
2277 + error_log("❌ BASIC PROCESSING: Failed to download PDF: " . $error_message);
3752 2278 return false;
3753 2279 }
3754 2280
3755 - global $wp_filesystem;
3756 - if (empty($wp_filesystem)) {
3757 - require_once ABSPATH . 'wp-admin/includes/file.php';
3758 - WP_Filesystem();
3759 - }
3760 - $wp_filesystem->put_contents($temp_file, wp_remote_retrieve_body($response), FS_CHMOD_FILE);
3761 - //error_log("✅ PDF downloaded successfully");
2281 + file_put_contents($temp_file, wp_remote_retrieve_body($response));
2282 + error_log("✅ PDF downloaded successfully");
3762 2283 } else {
3763 2284 $temp_file = $pdf_source;
3764 - //error_log("Using local PDF file: " . $temp_file);
2285 + error_log("Using local PDF file: " . $temp_file);
3765 2286 }
3766 2287
3767 2288 // Parse PDF
3768 - //error_log("Parsing PDF with basic parser...");
3769 - mxchat_load_pdf_parser();
2289 + error_log("Parsing PDF with basic parser...");
3770 2290 $parser = new \Smalot\PdfParser\Parser();
3771 2291 $pdf = $parser->parseFile($temp_file);
3772 2292 $pages = $pdf->getPages();
3773 2293
3774 - //error_log("PDF contains " . count($pages) . " pages");
2294 + error_log("PDF contains " . count($pages) . " pages");
3775 2295
3776 2296 if (count($pages) > $max_pages) {
3777 - //error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
2297 + error_log("❌ BASIC PROCESSING: Too many pages (" . count($pages) . " > " . $max_pages . ")");
3778 2298 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file) {
3779 2299 unlink($temp_file);
3780 2300 }
3781 2301 return 'too_many_pages';
@@ -3787,9 +2307,9 @@
3787 2307 foreach ($pages as $page_number => $page) {
3788 2308 $text = $page->getText();
3789 2309
3790 2310 if (empty(trim($text))) {
3791 - //error_log("Skipping empty page: " . ($page_number + 1));
2311 + error_log("Skipping empty page: " . ($page_number + 1));
3792 2312 continue;
3793 2313 }
3794 2314
3795 2315 $text = $this->mxchat_clean_text($text);
@@ -3810,9 +2330,9 @@
3810 2330 $processed_pages++;
3811 2331 }
3812 2332 }
3813 2333
3814 - //error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
2334 + error_log("✅ BASIC PROCESSING COMPLETE: " . $processed_pages . " pages processed");
3815 2335
3816 2336 // Cleanup
3817 2337 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
3818 2338 unlink($temp_file);
@@ -3817,46 +2337,21 @@
3817 2337 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
3818 2338 unlink($temp_file);
3819 2339 }
3820 2340
3821 - //error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
2341 + error_log("=== MXCHAT PDF PROCESSING END (BASIC) ===");
3822 2342 return $embeddings;
3823 2343
3824 2344 } catch (\Exception $e) {
3825 - //error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
2345 + error_log("❌ BASIC PROCESSING ERROR: " . $e->getMessage());
3826 2346 if (filter_var($pdf_source, FILTER_VALIDATE_URL) && $temp_file && file_exists($temp_file)) {
3827 2347 unlink($temp_file);
3828 2348 }
3829 - //error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
2349 + error_log("=== MXCHAT PDF PROCESSING END (ERROR) ===");
3830 2350 return false;
3831 2351 }
3832 2352 }
3833 2353
3834 -
3835 -/**
3836 - * Validate PDF URL for security
3837 - * Prevents SSRF attacks by blocking dangerous URLs
3838 - */
3839 -
3840 -private function mxchat_is_safe_pdf_url($url) {
3841 - // Use WordPress core function for comprehensive validation
3842 - // This blocks localhost, private IPs, and reserved IP ranges
3843 - $validated_url = wp_http_validate_url($url);
3844 -
3845 - if ($validated_url === false) {
3846 - return false;
3847 - }
3848 -
3849 - // Additional check: only allow HTTP/HTTPS schemes
3850 - $parsed = parse_url($url);
3851 - if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
3852 - return false;
3853 - }
3854 -
3855 - return true;
3856 -}
3857 -
3858 -
3859 2354 private function mxchat_clean_text($text) {
3860 2355 // Remove excessive whitespace
3861 2356 $text = preg_replace('/\s+/', ' ', $text);
3862 2357
@@ -3895,14 +2390,11 @@
3895 2390 }
3896 2391
3897 2392 return [];
3898 2393 }
3899 -
3900 -
2394 +// Add this to your class
3901 2395 public function handle_pdf_upload() {
3902 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce(wp_unslash((string) $_POST['nonce']))) {
3903 - wp_send_json_error(array('message' => esc_html__('Invalid nonce.', 'mxchat')), 403);
3904 - }
2396 + check_ajax_referer('mxchat_chat_nonce', 'nonce');
3905 2397
3906 2398 if (!isset($_FILES['pdf_file']) || !isset($_POST['session_id'])) {
3907 2399 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3908 2400 return;
@@ -3907,29 +2399,12 @@
3907 2399 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
3908 2400 return;
3909 2401 }
3910 2402
3911 - // SECURITY FIX: Check if PDF uploads are enabled in settings
3912 - $options = get_option('mxchat_options', array());
3913 - $show_pdf_button = isset($options['show_pdf_upload_button']) ? $options['show_pdf_upload_button'] : 'on';
3914 -
3915 - if ($show_pdf_button !== 'on') {
3916 - wp_send_json_error(esc_html__('PDF uploads are currently disabled.', 'mxchat'));
3917 - return;
3918 - }
3919 -
3920 2403 $file = $_FILES['pdf_file'];
3921 2404 $session_id = sanitize_text_field($_POST['session_id']);
3922 2405 $original_filename = sanitize_text_field($file['name']);
3923 2406
3924 - // Update session owner if it changed (e.g. IP changed due to network switch)
3925 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
3926 - $session_owner = get_option("mxchat_session_owner_{$session_id}");
3927 -
3928 - if (!$session_owner || $session_owner !== $current_user_identifier) {
3929 - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
3930 - }
3931 -
3932 2407 $file_type = wp_check_filetype($file['name'], ['pdf' => 'application/pdf']);
3933 2408 if ($file_type['type'] !== 'application/pdf') {
3934 2409 wp_send_json_error(esc_html__('Invalid file type. Only PDF files are allowed.', 'mxchat'));
3935 2410 return;
@@ -3935,12 +2410,9 @@
3935 2410 return;
3936 2411 }
3937 2412
3938 2413 $upload_dir = wp_upload_dir();
3939 -
3940 - // SECURITY FIX: Generate random filename without exposing session_id
3941 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
3942 - $pdf_filename = 'mxchat_' . $random_string . '_' . time() . '.pdf';
2414 + $pdf_filename = 'mxchat_' . $session_id . '_' . time() . '.pdf';
3943 2415 $pdf_path = $upload_dir['path'] . '/' . $pdf_filename;
3944 2416
3945 2417 if (!move_uploaded_file($file['tmp_name'], $pdf_path)) {
3946 2418 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
@@ -3971,9 +2443,8 @@
3971 2443 return;
3972 2444 }
3973 2445
3974 2446 if (!empty($embeddings)) {
3975 - // Store the mapping between session and the random filename
3976 2447 set_transient('mxchat_pdf_url_' . $session_id, $pdf_path, HOUR_IN_SECONDS);
3977 2448 set_transient('mxchat_pdf_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
3978 2449 set_transient('mxchat_pdf_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
3979 2450 set_transient('mxchat_include_pdf_in_context_' . $session_id, true, HOUR_IN_SECONDS);
@@ -3994,11 +2465,9 @@
3994 2465 wp_send_json_error($error_message);
3995 2466 return;
3996 2467 }
3997 2468 public function handle_pdf_remove() {
3998 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce(wp_unslash((string) $_POST['nonce']))) {
3999 - wp_send_json_error(array('message' => esc_html__('Invalid nonce.', 'mxchat')), 403);
4000 - }
2469 + check_ajax_referer('mxchat_chat_nonce', 'nonce');
4001 2470
4002 2471 if (empty($_POST['session_id'])) {
4003 2472 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
4004 2473 wp_die();
@@ -4019,8 +2488,10 @@
4019 2488 wp_die();
4020 2489 }
4021 2490
4022 2491
2492 +
2493 +
4023 2494 function mxchat_fetch_new_messages() {
4024 2495 $session_id = sanitize_text_field($_POST['session_id']);
4025 2496 $last_seen_id = sanitize_text_field($_POST['last_seen_id']);
4026 2497 $persistence_enabled = $_POST['persistence_enabled'] === 'true';
@@ -4033,31 +2504,14 @@
4033 2504 }
4034 2505
4035 2506 $history = get_option("mxchat_history_{$session_id}", []);
4036 2507
4037 - //error_log("MxChat WhatsApp DEBUG: Fetch new messages for session {$session_id}");
4038 - //error_log("MxChat WhatsApp DEBUG: last_seen_id = " . var_export($last_seen_id, true));
4039 - //error_log("MxChat WhatsApp DEBUG: History count = " . count($history));
4040 - //error_log("MxChat WhatsApp DEBUG: Full history = " . print_r($history, true));
4041 -
4042 2508 $new_messages = array_filter($history, function ($message) use ($last_seen_id, $persistence_enabled, $initial_timestamp) {
4043 - //error_log("MxChat WhatsApp DEBUG: Checking message - ID: " . ($message['id'] ?? 'NO_ID') . ", Role: " . ($message['role'] ?? 'NO_ROLE'));
4044 -
4045 2509 // If persistence is enabled, show all new messages
4046 2510 if ($persistence_enabled) {
4047 - $has_id = !empty($message['id']);
4048 - $is_agent = $message['role'] === 'agent';
4049 -
4050 - // If last_seen_id is empty, 'NaN', or invalid, show all agent messages
4051 - if (empty($last_seen_id) || $last_seen_id === 'NaN' || $last_seen_id === 'undefined') {
4052 - $is_newer = true;
4053 - } else {
4054 - $is_newer = strcmp($message['id'] ?? '', $last_seen_id) > 0;
4055 - }
4056 -
4057 - //error_log("MxChat WhatsApp DEBUG: has_id={$has_id}, is_newer={$is_newer}, is_agent={$is_agent}");
4058 -
4059 - return $has_id && $is_newer && $is_agent;
2511 + return !empty($message['id']) &&
2512 + strcmp($message['id'], $last_seen_id) > 0 &&
2513 + $message['role'] === 'agent';
4060 2514 }
4061 2515
4062 2516 // If persistence is disabled, only show messages after initial timestamp
4063 2517 return !empty($message['id']) &&
@@ -4064,16 +2518,12 @@
4064 2518 $message['role'] === 'agent' &&
4065 2519 $message['timestamp'] > $initial_timestamp;
4066 2520 });
4067 2521
4068 - //error_log("MxChat WhatsApp DEBUG: Filtered messages count = " . count($new_messages));
2522 + //error_log(esc_html__("New agent messages fetched for session $session_id. Last seen ID: $last_seen_id", 'mxchat'));
4069 2523
4070 - // Include current chat mode so frontend can detect agent→AI transitions
4071 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
4072 -
4073 2524 wp_send_json_success([
4074 - 'new_messages' => array_values($new_messages),
4075 - 'chat_mode' => $chat_mode
2525 + 'new_messages' => array_values($new_messages)
4076 2526 ]);
4077 2527 wp_die();
4078 2528 }
4079 2529 public function mxchat_live_agent_handover($message, $user_id, $session_id) {
@@ -4106,9 +2556,9 @@
4106 2556 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4107 2557
4108 2558 if (empty($channel_id)) {
4109 2559 // Create new channel with session ID as name
4110 - $channel_name = $this->generate_channel_name($session_id);
2560 + $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
4111 2561
4112 2562 //error_log("Attempting to create channel: $channel_name");
4113 2563
4114 2564 $response = wp_remote_post('https://slack.com/api/conversations.create', [
@@ -4244,507 +2694,9 @@
4244 2694 'fallbackResponse' => $this->fallbackResponse
4245 2695 ]);
4246 2696 wp_die();
4247 2697 }
4248 -
4249 -private function generate_channel_name($session_id) {
4250 - $email = null;
4251 - $name = null;
4252 -
4253 - // 1. First priority: Check if user is logged in and get their info
4254 - if (is_user_logged_in()) {
4255 - $current_user = wp_get_current_user();
4256 - if (!empty($current_user->user_email)) {
4257 - $email = $current_user->user_email;
4258 - //error_log("[DEBUG] Using logged-in user email for channel: {$email}");
4259 - }
4260 - if (!empty($current_user->display_name)) {
4261 - $name = $current_user->display_name;
4262 - //error_log("[DEBUG] Using logged-in user name for channel: {$name}");
4263 - }
4264 - }
4265 -
4266 - // 2. Second priority: Check for saved email/name from "require email to chat" option
4267 - if (empty($email)) {
4268 - $email_option_key = "mxchat_email_{$session_id}";
4269 - $saved_email = get_option($email_option_key);
4270 - if (!empty($saved_email)) {
4271 - $email = $saved_email;
4272 - //error_log("[DEBUG] Using saved email from session for channel: {$email}");
4273 - }
4274 - }
4275 -
4276 - if (empty($name)) {
4277 - $name_option_key = "mxchat_name_{$session_id}";
4278 - $saved_name = get_option($name_option_key);
4279 - if (!empty($saved_name)) {
4280 - $name = $saved_name;
4281 - //error_log("[DEBUG] Using saved name from session for channel: {$name}");
4282 - }
4283 - }
4284 -
4285 - // 3. Third priority: Check existing chat transcript for email/name
4286 - if (empty($email) || empty($name)) {
4287 - global $wpdb;
4288 - $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
4289 - $existing_data = $wpdb->get_row($wpdb->prepare(
4290 - "SELECT user_email, user_name FROM $table_name WHERE session_id = %s AND (user_email IS NOT NULL OR user_name IS NOT NULL) LIMIT 1",
4291 - $session_id
4292 - ));
4293 -
4294 - if ($existing_data) {
4295 - if (empty($email) && !empty($existing_data->user_email)) {
4296 - $email = $existing_data->user_email;
4297 - //error_log("[DEBUG] Using email from chat transcript for channel: {$email}");
4298 - }
4299 - if (empty($name) && !empty($existing_data->user_name)) {
4300 - $name = $existing_data->user_name;
4301 - //error_log("[DEBUG] Using name from chat transcript for channel: {$name}");
4302 - }
4303 - }
4304 - }
4305 -
4306 - // 4. Generate channel name based on priority: Name > Email > Session ID
4307 - $channel_name = '';
4308 -
4309 - if (!empty($name)) {
4310 - // Convert name to valid Slack channel name
4311 - $base_name = strtolower(trim($name));
4312 - // Replace spaces and invalid characters
4313 - $base_name = preg_replace('/[^a-z0-9\s]/', '', $base_name);
4314 - $base_name = preg_replace('/\s+/', '-', $base_name);
4315 - $base_name = trim($base_name, '-');
4316 -
4317 - // Get last 4 characters of session ID for uniqueness
4318 - $session_suffix = substr($session_id, -4);
4319 - $channel_name = 'chat-' . $base_name . '-' . strtolower($session_suffix);
4320 -
4321 - // Slack channel names have a 21 character limit
4322 - if (strlen($channel_name) > 21) {
4323 - // Calculate available space for name (21 - 'chat-' - '-' - session_suffix)
4324 - $available_space = 21 - 5 - 1 - strlen($session_suffix); // 'chat-' = 5, '-' = 1
4325 - $truncated_name = substr($base_name, 0, $available_space);
4326 - $truncated_name = rtrim($truncated_name, '-'); // Remove trailing hyphen
4327 - $channel_name = 'chat-' . $truncated_name . '-' . strtolower($session_suffix);
4328 - }
4329 -
4330 - //error_log("[DEBUG] Using name for channel: {$channel_name} (from name: {$name})");
4331 -
4332 - } elseif (!empty($email)) {
4333 - // Convert email to valid Slack channel name (your existing logic)
4334 - $channel_name = 'chat-' . strtolower(str_replace(['@', '.', '+', '_'], ['-at-', '-', '-plus-', '-'], $email));
4335 - // Remove any remaining invalid characters
4336 - $channel_name = preg_replace('/[^a-z0-9\-]/', '', $channel_name);
4337 - // Ensure it doesn't end with a hyphen
4338 - $channel_name = rtrim($channel_name, '-');
4339 - // Slack channel names have a 21 character limit, so truncate if needed
4340 - if (strlen($channel_name) > 21) {
4341 - $channel_name = substr($channel_name, 0, 21);
4342 - $channel_name = rtrim($channel_name, '-'); // Remove trailing hyphen if truncation created one
4343 - }
4344 -
4345 - //error_log("[DEBUG] Using email for channel: {$channel_name} (from email: {$email})");
4346 -
4347 - } else {
4348 - // Fallback to session ID if no name or email found
4349 - $channel_name = 'chat-' . strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $session_id));
4350 - //error_log("[DEBUG] No name or email found, using session ID for channel: {$channel_name}");
4351 - }
4352 -
4353 - // Final validation - ensure channel name meets Slack requirements
4354 - if (strlen($channel_name) > 21) {
4355 - $channel_name = substr($channel_name, 0, 21);
4356 - $channel_name = rtrim($channel_name, '-');
4357 - }
4358 -
4359 - //error_log("[DEBUG] Generated channel name: {$channel_name}");
4360 - return $channel_name;
4361 -}
4362 -
4363 -/**
4364 - * Telegram Live Agent Handover
4365 - * Creates a forum topic in the Telegram group and notifies agents
4366 - */
4367 -public function mxchat_telegram_live_agent_handover($message, $user_id, $session_id) {
4368 - // Check if Telegram agents are available
4369 - $telegram_available = $this->options['telegram_status'] ?? 'off';
4370 - if ($telegram_available !== 'on') {
4371 - $away_message = $this->options['telegram_away_message'] ?? 'Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.';
4372 - $this->fallbackResponse = [
4373 - 'text' => $away_message,
4374 - 'html' => '',
4375 - 'images' => [],
4376 - 'chat_mode' => 'ai'
4377 - ];
4378 - wp_send_json([
4379 - 'text' => $away_message,
4380 - 'html' => '',
4381 - 'chat_mode' => 'ai',
4382 - 'session_id' => $session_id
4383 - ]);
4384 - wp_die();
4385 - }
4386 -
4387 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4388 - $telegram_group_id = $this->options['telegram_group_id'] ?? '';
4389 -
4390 - if (empty($telegram_bot_token) || empty($telegram_group_id)) {
4391 - return false;
4392 - }
4393 -
4394 - // Check if topic already exists for this session
4395 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4396 -
4397 - if (empty($topic_id)) {
4398 - // Generate topic name
4399 - $topic_name = $this->generate_telegram_topic_name($session_id);
4400 -
4401 - // Random icon color (Telegram forum topic colors)
4402 - $icon_colors = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, 0xFB6F5F];
4403 - $icon_color = $icon_colors[array_rand($icon_colors)];
4404 -
4405 - // Create forum topic
4406 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/createForumTopic", [
4407 - 'headers' => ['Content-Type' => 'application/json'],
4408 - 'body' => json_encode([
4409 - 'chat_id' => $telegram_group_id,
4410 - 'name' => $topic_name,
4411 - 'icon_color' => $icon_color
4412 - ])
4413 - ]);
4414 -
4415 - if (!is_wp_error($response)) {
4416 - $response_body = wp_remote_retrieve_body($response);
4417 - $response_data = json_decode($response_body, true);
4418 -
4419 - if (isset($response_data['ok']) && $response_data['ok']) {
4420 - $topic_id = $response_data['result']['message_thread_id'];
4421 - update_option("mxchat_telegram_topic_{$session_id}", $topic_id);
4422 - update_option("mxchat_telegram_group_{$session_id}", $telegram_group_id);
4423 - }
4424 - }
4425 -
4426 - if (empty($topic_id)) {
4427 - return false; // Failed to create topic
4428 - }
4429 - }
4430 -
4431 - // Get recent chat history
4432 - $history = get_option("mxchat_history_{$session_id}", []);
4433 - $recent_history = array_slice($history, -5);
4434 -
4435 - // Format conversation context for Telegram (HTML format)
4436 - $conversation_context = "";
4437 - if (!empty($recent_history)) {
4438 - $conversation_context = "<b>Recent Conversation:</b>\n";
4439 - foreach ($recent_history as $hist_message) {
4440 - $role_display = $hist_message['role'] === 'user' ? '👤 User' : '🤖 AI';
4441 - $escaped_content = htmlspecialchars($hist_message['content'], ENT_QUOTES, 'UTF-8');
4442 - $conversation_context .= "{$role_display}: {$escaped_content}\n";
4443 - }
4444 - $conversation_context .= "\n";
4445 - }
4446 -
4447 - // Get user info
4448 - $user_email = get_option("mxchat_email_{$session_id}", 'Not provided');
4449 - $user_name = get_option("mxchat_name_{$session_id}", 'Anonymous');
4450 -
4451 - // Update session mode
4452 - update_option("mxchat_mode_{$session_id}", 'agent');
4453 -
4454 - // Send initial message to topic
4455 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
4456 - $topic_message = "🔔 <b>New Live Agent Request</b>\n\n";
4457 - $topic_message .= "<b>Session ID:</b> <code>{$session_id}</code>\n";
4458 - $topic_message .= "<b>User:</b> {$user_name}\n";
4459 - $topic_message .= "<b>Email:</b> {$user_email}\n\n";
4460 -
4461 - if (!empty($conversation_context)) {
4462 - $topic_message .= $conversation_context;
4463 - }
4464 -
4465 - $topic_message .= "<b>Current Message:</b>\n{$escaped_message}\n\n";
4466 - $topic_message .= "<i>Reply in this topic - messages will be sent to the user</i>\n";
4467 - $topic_message .= "<i>Type #close, #end, #disconnect, or #done to end the session</i>";
4468 -
4469 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4470 - 'headers' => ['Content-Type' => 'application/json'],
4471 - 'body' => json_encode([
4472 - 'chat_id' => $telegram_group_id,
4473 - 'message_thread_id' => $topic_id,
4474 - 'text' => $topic_message,
4475 - 'parse_mode' => 'HTML'
4476 - ])
4477 - ]);
4478 -
4479 - $success_message = $this->options['telegram_notification_message'] ?? "I've notified a support agent. Please allow a moment for them to respond.";
4480 - $this->mxchat_save_chat_message($session_id, 'bot', $success_message);
4481 -
4482 - $this->fallbackResponse = [
4483 - 'text' => $success_message,
4484 - 'html' => '',
4485 - 'images' => [],
4486 - 'chat_mode' => 'agent'
4487 - ];
4488 -
4489 - wp_send_json([
4490 - 'success' => true,
4491 - 'text' => $success_message,
4492 - 'html' => '',
4493 - 'chat_mode' => 'agent',
4494 - 'session_id' => $session_id,
4495 - 'fallbackResponse' => $this->fallbackResponse
4496 - ]);
4497 - wp_die();
4498 -}
4499 -
4500 -/**
4501 - * Generate topic name for Telegram forum
4502 - */
4503 -private function generate_telegram_topic_name($session_id) {
4504 - $name = null;
4505 - $email = null;
4506 -
4507 - // Check logged in user
4508 - if (is_user_logged_in()) {
4509 - $current_user = wp_get_current_user();
4510 - if (!empty($current_user->display_name)) {
4511 - $name = $current_user->display_name;
4512 - }
4513 - if (!empty($current_user->user_email)) {
4514 - $email = $current_user->user_email;
4515 - }
4516 - }
4517 -
4518 - // Check session data
4519 - if (empty($name)) {
4520 - $name = get_option("mxchat_name_{$session_id}");
4521 - }
4522 - if (empty($email)) {
4523 - $email = get_option("mxchat_email_{$session_id}");
4524 - }
4525 -
4526 - // Generate topic name
4527 - $session_suffix = substr($session_id, -6);
4528 -
4529 - if (!empty($name)) {
4530 - // Clean name for topic (max 128 chars in Telegram)
4531 - $clean_name = preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $name);
4532 - $clean_name = trim($clean_name);
4533 - if (strlen($clean_name) > 50) {
4534 - $clean_name = substr($clean_name, 0, 50);
4535 - }
4536 - return "Chat - {$clean_name} ({$session_suffix})";
4537 - } elseif (!empty($email)) {
4538 - // Use email prefix
4539 - $email_prefix = explode('@', $email)[0];
4540 - if (strlen($email_prefix) > 30) {
4541 - $email_prefix = substr($email_prefix, 0, 30);
4542 - }
4543 - return "Chat - {$email_prefix} ({$session_suffix})";
4544 - }
4545 -
4546 - return "Chat - {$session_suffix}";
4547 -}
4548 -
4549 -/**
4550 - * Send user message to Telegram agent
4551 - */
4552 -public function mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id) {
4553 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4554 - $topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4555 - $group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4556 -
4557 - if (empty($telegram_bot_token) || empty($topic_id) || empty($group_id)) {
4558 - return false;
4559 - }
4560 -
4561 - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
4562 - $user_message = "👤 <b>User:</b> {$escaped_message}";
4563 -
4564 - $response = wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4565 - 'headers' => ['Content-Type' => 'application/json'],
4566 - 'body' => json_encode([
4567 - 'chat_id' => $group_id,
4568 - 'message_thread_id' => $topic_id,
4569 - 'text' => $user_message,
4570 - 'parse_mode' => 'HTML'
4571 - ])
4572 - ]);
4573 -
4574 - return !is_wp_error($response);
4575 -}
4576 -
4577 -/**
4578 - * Handle incoming Telegram webhook
4579 - */
4580 -public function handle_telegram_webhook(WP_REST_Request $request) {
4581 - $body = $request->get_body();
4582 - $data = json_decode($body, true);
4583 -
4584 - //error_log('[MxChat Telegram DEBUG] Webhook received: ' . $body);
4585 -
4586 - // Handle message events from forum topics
4587 - if (isset($data['message'])) {
4588 - $message_data = $data['message'];
4589 -
4590 - // Skip if not from a forum topic
4591 - if (!isset($message_data['message_thread_id'])) {
4592 - //error_log('[MxChat Telegram DEBUG] Skipped: No message_thread_id (not a forum topic message)');
4593 - return new WP_REST_Response(['ok' => true]);
4594 - }
4595 -
4596 - // Skip bot messages
4597 - if (isset($message_data['from']['is_bot']) && $message_data['from']['is_bot']) {
4598 - //error_log('[MxChat Telegram DEBUG] Skipped: Message from bot');
4599 - return new WP_REST_Response(['ok' => true]);
4600 - }
4601 -
4602 - $chat_id = $message_data['chat']['id'] ?? '';
4603 - $topic_id = $message_data['message_thread_id'];
4604 - $message_text = $message_data['text'] ?? '';
4605 - $message_id = $message_data['message_id'] ?? '';
4606 - $from = $message_data['from'] ?? [];
4607 - $agent_name = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
4608 - if (empty($agent_name)) {
4609 - $agent_name = $from['username'] ?? 'Agent';
4610 - }
4611 -
4612 - //error_log("[MxChat Telegram DEBUG] Parsed: chat_id={$chat_id}, topic_id={$topic_id}, agent={$agent_name}, text={$message_text}");
4613 -
4614 - // Skip empty messages
4615 - if (empty($message_text)) {
4616 - //error_log('[MxChat Telegram DEBUG] Skipped: Empty message text');
4617 - return new WP_REST_Response(['ok' => true]);
4618 - }
4619 -
4620 - // Find session ID by topic ID - cast to string for comparison
4621 - global $wpdb;
4622 - $topic_id_str = strval($topic_id);
4623 - $session_option = $wpdb->get_var(
4624 - $wpdb->prepare(
4625 - "SELECT option_name FROM {$wpdb->options}
4626 - WHERE option_name LIKE %s
4627 - AND option_value = %s",
4628 - 'mxchat_telegram_topic_%',
4629 - $topic_id_str
4630 - )
4631 - );
4632 -
4633 - //error_log("[MxChat Telegram DEBUG] Looking for topic_id={$topic_id_str} in options, found: " . ($session_option ?: 'NULL'));
4634 -
4635 - if ($session_option) {
4636 - $session_id = str_replace('mxchat_telegram_topic_', '', $session_option);
4637 - //error_log("[MxChat Telegram DEBUG] Session ID: {$session_id}");
4638 -
4639 - // Verify the group ID matches
4640 - $stored_group_id = get_option("mxchat_telegram_group_{$session_id}", '');
4641 - //error_log("[MxChat Telegram DEBUG] Stored group_id={$stored_group_id}, received chat_id={$chat_id}");
4642 -
4643 - if (strval($stored_group_id) != strval($chat_id)) {
4644 - //error_log('[MxChat Telegram DEBUG] Skipped: Group ID mismatch');
4645 - return new WP_REST_Response(['ok' => true]);
4646 - }
4647 -
4648 - // Check for closure commands
4649 - $lower_text = strtolower(trim($message_text));
4650 - if (in_array($lower_text, ['#close', '#end', '#disconnect', '#done'])) {
4651 - //error_log("[MxChat Telegram DEBUG] Closure command received: {$lower_text}");
4652 - // End the live agent session
4653 - update_option("mxchat_mode_{$session_id}", 'ai');
4654 -
4655 - // Save disconnect message
4656 - $disconnect_message = "Live agent session ended. You're now chatting with the AI assistant.";
4657 - $this->mxchat_save_chat_message($session_id, 'bot', $disconnect_message);
4658 -
4659 - // Notify in Telegram
4660 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4661 - if (!empty($telegram_bot_token)) {
4662 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4663 - 'headers' => ['Content-Type' => 'application/json'],
4664 - 'body' => json_encode([
4665 - 'chat_id' => $chat_id,
4666 - 'message_thread_id' => $topic_id,
4667 - 'text' => "✅ Session closed. User returned to AI chatbot.",
4668 - 'parse_mode' => 'HTML'
4669 - ])
4670 - ]);
4671 -
4672 - // Optionally close the topic
4673 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/closeForumTopic", [
4674 - 'headers' => ['Content-Type' => 'application/json'],
4675 - 'body' => json_encode([
4676 - 'chat_id' => $chat_id,
4677 - 'message_thread_id' => $topic_id
4678 - ])
4679 - ]);
4680 - }
4681 -
4682 - return new WP_REST_Response(['ok' => true]);
4683 - }
4684 -
4685 - // Deduplicate messages
4686 - $message_key = md5($session_id . $message_id . $message_text);
4687 - $processed_messages = get_transient('mxchat_telegram_messages_' . $session_id) ?: [];
4688 -
4689 - if (in_array($message_key, $processed_messages)) {
4690 - //error_log('[MxChat Telegram DEBUG] Skipped: Duplicate message');
4691 - return new WP_REST_Response(['ok' => true]);
4692 - }
4693 -
4694 - $processed_messages[] = $message_key;
4695 - if (count($processed_messages) > 50) {
4696 - $processed_messages = array_slice($processed_messages, -50);
4697 - }
4698 - set_transient('mxchat_telegram_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
4699 -
4700 - // Save the agent message - format with agent name prefix for proper parsing
4701 - $formatted_message = "Agent: {$agent_name} - {$message_text}";
4702 - //error_log("[MxChat Telegram DEBUG] Saving agent message: {$formatted_message}");
4703 -
4704 - $this->mxchat_save_chat_message($session_id, 'agent', $formatted_message);
4705 -
4706 - // Verify the message was saved to history
4707 - $history = get_option("mxchat_history_{$session_id}", []);
4708 - $last_message = end($history);
4709 - //error_log("[MxChat Telegram DEBUG] History after save - count: " . count($history) . ", last message role: " . ($last_message['role'] ?? 'none'));
4710 -
4711 - // Send confirmation back to Telegram
4712 - $telegram_bot_token = $this->options['telegram_bot_token'] ?? '';
4713 - if (!empty($telegram_bot_token)) {
4714 - $confirm_key = 'mxchat_telegram_confirm_' . $message_key;
4715 - if (!get_transient($confirm_key)) {
4716 - wp_remote_post("https://api.telegram.org/bot{$telegram_bot_token}/sendMessage", [
4717 - 'headers' => ['Content-Type' => 'application/json'],
4718 - 'body' => json_encode([
4719 - 'chat_id' => $chat_id,
4720 - 'message_thread_id' => $topic_id,
4721 - 'text' => "✅ <i>Message sent to user</i>",
4722 - 'parse_mode' => 'HTML',
4723 - 'reply_to_message_id' => $message_id
4724 - ])
4725 - ]);
4726 - set_transient($confirm_key, true, 300);
4727 - }
4728 - }
4729 - } else {
4730 - //error_log("[MxChat Telegram DEBUG] No session found for topic_id={$topic_id}");
4731 - }
4732 - } else {
4733 - //error_log('[MxChat Telegram DEBUG] No message in webhook data');
4734 - }
4735 -
4736 - return new WP_REST_Response(['ok' => true]);
4737 -}
4738 -
4739 2698 public function mxchat_send_user_message_to_agent($message, $user_id, $session_id) {
4740 - // Check if this is a Telegram agent session
4741 - $telegram_topic_id = get_option("mxchat_telegram_topic_{$session_id}", '');
4742 - if (!empty($telegram_topic_id)) {
4743 - return $this->mxchat_send_user_message_to_telegram_agent($message, $user_id, $session_id);
4744 - }
4745 -
4746 - // Otherwise, try Slack
4747 2699 $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
4748 2700 $channel_id = get_option("mxchat_channel_{$session_id}", '');
4749 2701
4750 2702 if (empty($slack_bot_token) || empty($channel_id)) {
@@ -4901,26 +2853,22 @@
4901 2853 'text' => esc_html__("Message sent successfully to session $session_id", 'mxchat')
4902 2854 ], 200);
4903 2855 }
4904 2856 public function mxchat_handle_switch_to_chatbot_intent($message, $user_id, $session_id) {
4905 - // Update mode to AI
2857 + //error_log(esc_html__("Switching back to chatbot mode via intent.", 'mxchat'));
2858 +
2859 + // Just update mode to AI
4906 2860 update_option("mxchat_mode_{$session_id}", 'ai');
4907 -
4908 - // Clear any existing PDF context to start fresh
4909 - $this->clear_pdf_transients($session_id);
4910 -
4911 - // Set the response with explicit chat_mode
4912 - $this->fallbackResponse = [
4913 - 'text' => esc_html__('You are now chatting with the AI chatbot.', 'mxchat'),
4914 - 'html' => '',
4915 - 'images' => [],
4916 - 'chat_mode' => 'ai' // Ensure this is set
4917 - ];
4918 -
4919 - // Return the complete response array instead of just true
4920 - return $this->fallbackResponse;
2861 +
2862 + // Initialize states
2863 + $this->fallbackResponse = ['text' => '', 'html' => '', 'images' => []];
2864 + $this->productCardHtml = '';
2865 +
2866 + // Set the response message
2867 + $this->fallbackResponse['text'] = esc_html__('You are now chatting with the AI chatbot.', 'mxchat');
2868 +
2869 + return true; // Intent was handled
4921 2870 }
4922 -
4923 2871 public function handle_slack_messages(WP_REST_Request $request) {
4924 2872 // Log the incoming request for debugging
4925 2873 //error_log('Slack events request received: ' . $request->get_body());
4926 2874
@@ -4970,33 +2918,33 @@
4970 2918
4971 2919 $channel_id = $event['channel'];
4972 2920 $message_text = $event['text'] ?? '';
4973 2921 $message_ts = $event['ts'] ?? '';
4974 -
2922 +
4975 2923 // Find session ID by looking for matching channel
4976 2924 global $wpdb;
4977 2925 $session_option = $wpdb->get_var(
4978 2926 $wpdb->prepare(
4979 - "SELECT option_name FROM {$wpdb->options}
4980 - WHERE option_name LIKE 'mxchat_channel_%'
2927 + "SELECT option_name FROM {$wpdb->options}
2928 + WHERE option_name LIKE 'mxchat_channel_%'
4981 2929 AND option_value = %s",
4982 2930 $channel_id
4983 2931 )
4984 2932 );
4985 -
2933 +
4986 2934 if ($session_option) {
4987 2935 $session_id = str_replace('mxchat_channel_', '', $session_option);
4988 -
2936 +
4989 2937 // Create a unique key for this specific message
4990 2938 $message_key = md5($session_id . $message_ts . $message_text);
4991 2939 $processed_messages = get_transient('mxchat_processed_messages_' . $session_id) ?: [];
4992 -
2940 +
4993 2941 // Check if we've already processed this exact message
4994 2942 if (in_array($message_key, $processed_messages)) {
4995 2943 //error_log("Duplicate message detected for session $session_id");
4996 2944 return new WP_REST_Response(['ok' => true]);
4997 2945 }
4998 -
2946 +
4999 2947 // Add to processed messages
5000 2948 $processed_messages[] = $message_key;
5001 2949 // Keep only last 50 messages per session
5002 2950 if (count($processed_messages) > 50) {
@@ -5002,46 +2950,14 @@
5002 2950 if (count($processed_messages) > 50) {
5003 2951 $processed_messages = array_slice($processed_messages, -50);
5004 2952 }
5005 2953 set_transient('mxchat_processed_messages_' . $session_id, $processed_messages, HOUR_IN_SECONDS);
5006 -
5007 - $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
5008 -
5009 - // Handle agent ending the chat — transfer back to AI
5010 - // Format: "!endchat" or "!endchat <custom message to user>"
5011 - if (preg_match('/^!endchat\b/i', trim($message_text))) {
5012 - update_option("mxchat_mode_{$session_id}", 'ai');
5013 -
5014 - // Extract custom message after !endchat, or use empty string
5015 - $custom_message = trim(preg_replace('/^!endchat\s*/i', '', trim($message_text)));
5016 -
5017 - // Send the agent's custom farewell message if provided
5018 - if (!empty($custom_message)) {
5019 - $this->mxchat_save_chat_message($session_id, 'agent', $custom_message);
5020 - }
5021 -
5022 - // Confirm in Slack channel
5023 - if (!empty($slack_bot_token)) {
5024 - wp_remote_post('https://slack.com/api/chat.postMessage', [
5025 - 'headers' => [
5026 - 'Content-Type' => 'application/json',
5027 - 'Authorization' => 'Bearer ' . $slack_bot_token
5028 - ],
5029 - 'body' => json_encode([
5030 - 'channel' => $channel_id,
5031 - 'text' => "✅ *Chat ended.* User has been transferred back to AI mode.",
5032 - 'mrkdwn' => true
5033 - ])
5034 - ]);
5035 - }
5036 -
5037 - return new WP_REST_Response(['ok' => true]);
5038 - }
5039 -
2954 +
5040 2955 // Save the agent message
5041 2956 $this->mxchat_save_chat_message($session_id, 'agent', $message_text);
5042 -
2957 +
5043 2958 // Send confirmation back to Slack (only once)
2959 + $slack_bot_token = $this->options['live_agent_bot_token'] ?? '';
5044 2960 if (!empty($slack_bot_token)) {
5045 2961 // Use a transient to prevent duplicate confirmations
5046 2962 $confirm_key = 'mxchat_confirm_' . $message_key;
5047 2963 if (!get_transient($confirm_key)) {
@@ -5051,9 +2967,9 @@
5051 2967 'Authorization' => 'Bearer ' . $slack_bot_token
5052 2968 ],
5053 2969 'body' => json_encode([
5054 2970 'channel' => $channel_id,
5055 - 'text' => "✅ _Message sent to user_",
2971 + 'text' => "✅ _Message sent to user_",
5056 2972 'thread_ts' => $event['ts'] // Reply in thread
5057 2973 ])
5058 2974 ]);
5059 2975 // Set transient to prevent duplicate confirmations
@@ -5093,15 +3009,9 @@
5093 3009 try {
5094 3010 // Get options and selected model
5095 3011 $options = get_option('mxchat_options');
5096 3012 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
5097 -
5098 - // Opt-in: route embeddings through the Custom (OpenAI-compatible) provider.
5099 - // Off by default so existing sites see byte-identical behavior.
5100 - if (!empty($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
5101 - return $this->mxchat_generate_embedding_custom($text);
5102 - }
5103 -
3013 +
5104 3014 // Determine endpoint and API key based on model
5105 3015 if (strpos($selected_model, 'voyage') === 0) {
5106 3016 $endpoint = 'https://api.voyageai.com/v1/embeddings';
5107 3017 $api_key = $options['voyage_api_key'] ?? '';
@@ -5296,653 +3206,254 @@
5296 3206 'error_code' => 'embedding_exception'
5297 3207 ];
5298 3208 }
5299 3209 }
3210 +private function mxchat_find_relevant_content($user_embedding) {
3211 + //error_log('MXChat Vector Search: Starting content search...');
5300 3212
3213 + // Retrieve the add-on settings from the database.
3214 + $addon_options = get_option('mxchat_pinecone_addon_options', array());
5301 3215
5302 -/**
5303 - * Generate embedding via a Custom (OpenAI-compatible) provider's /embeddings route.
5304 - * Only called when the opt-in 'custom_provider_for_embeddings' setting is on.
5305 - * Returns a numeric array (the embedding vector) on success, or ['error','error_code'] on failure.
5306 - */
5307 -private function mxchat_generate_embedding_custom($text) {
5308 - if (empty($text)) {
5309 - return ['error' => esc_html__('No text provided for embedding generation', 'mxchat'), 'error_code' => 'empty_embedding_text'];
5310 - }
5311 - $cfg = $this->mxchat_resolve_custom_provider();
5312 - if (empty($cfg['base_url'])) {
5313 - return ['error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url'];
5314 - }
3216 + // Determine whether Pinecone is enabled.
3217 + $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1') ? 1 : 0;
5315 3218
5316 - $options = get_option('mxchat_options');
5317 - $embed_url = $cfg['base_url'] . '/embeddings';
5318 - if (!empty($cfg['api_version'])) {
5319 - $embed_url .= (strpos($embed_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($cfg['api_version']);
5320 - }
5321 - $model = isset($options['custom_provider_embedding_model']) && trim((string) $options['custom_provider_embedding_model']) !== ''
5322 - ? trim((string) $options['custom_provider_embedding_model'])
5323 - : $cfg['model'];
3219 + //error_log('Pinecone enabled flag: ' . $use_pinecone);
5324 3220
5325 - $response = wp_remote_post($embed_url, [
5326 - 'headers' => $this->mxchat_custom_provider_assoc_headers($cfg),
5327 - 'body' => wp_json_encode(['input' => $text, 'model' => $model]),
5328 - 'timeout' => 60,
5329 - ]);
5330 - if (is_wp_error($response)) {
5331 - return [
5332 - 'error' => esc_html__('Connection error when generating embeddings (custom provider): ', 'mxchat') . esc_html($response->get_error_message()),
5333 - 'error_code' => 'embedding_custom_connection_error',
5334 - ];
5335 - }
5336 - $status = wp_remote_retrieve_response_code($response);
5337 - $body = json_decode(wp_remote_retrieve_body($response), true);
5338 - if ($status !== 200) {
5339 - $msg = isset($body['error']['message']) ? $body['error']['message'] : 'HTTP ' . $status;
5340 - return [
5341 - 'error' => esc_html__('Custom embedding endpoint error: ', 'mxchat') . esc_html($msg),
5342 - 'error_code' => 'embedding_custom_api_error',
5343 - 'status_code' => $status,
5344 - ];
5345 - }
5346 - if (isset($body['data'][0]['embedding']) && is_array($body['data'][0]['embedding'])) {
5347 - return $body['data'][0]['embedding'];
5348 - }
5349 - return [
5350 - 'error' => esc_html__('Invalid embedding response from custom provider.', 'mxchat'),
5351 - 'error_code' => 'embedding_custom_invalid_response',
5352 - ];
5353 -}
5354 -
5355 -private function mxchat_find_relevant_content($user_embedding, $bot_id = 'default', $user_query = '') {
5356 - //error_log("MXCHAT DEBUG: find_relevant_content called with bot_id: " . $bot_id);
5357 -
5358 - // Check for OpenAI Vector Store first (takes priority when enabled)
5359 - $bot_vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
5360 -
5361 - if ($bot_vectorstore_config['use_vectorstore']) {
5362 - // Get current model to verify it's an OpenAI model
5363 - $bot_options = $this->get_bot_options($bot_id);
5364 - $mxchat_options = get_option('mxchat_options', array());
5365 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
5366 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
5367 -
5368 - if ($this->is_openai_chat_model($selected_model)) {
5369 - //error_log("MXCHAT DEBUG: Using OpenAI Vector Store for knowledge retrieval");
5370 - return $this->find_relevant_content_openai_vectorstore($user_query, $bot_id, $bot_vectorstore_config);
5371 - } else {
5372 - //error_log("MXCHAT DEBUG: Vector Store enabled but model is not OpenAI (" . $selected_model . "), skipping Vector Store");
5373 - }
5374 - }
5375 -
5376 - // Get bot-specific Pinecone configuration
5377 - $bot_pinecone_config = $this->get_bot_pinecone_config($bot_id);
5378 -
5379 - // Debug: Log the Pinecone configuration
5380 - //error_log("MXCHAT DEBUG: Pinecone config for bot '$bot_id':");
5381 - //error_log(" - use_pinecone: " . ($bot_pinecone_config['use_pinecone'] ? 'true' : 'false'));
5382 - //error_log(" - api_key: " . (empty($bot_pinecone_config['api_key']) ? 'EMPTY' : 'SET (hidden)'));
5383 - //error_log(" - host: " . ($bot_pinecone_config['host'] ?? 'NOT SET'));
5384 - //error_log(" - namespace: " . ($bot_pinecone_config['namespace'] ?? 'NOT SET'));
5385 -
5386 - // Determine whether to use Pinecone based on bot configuration
5387 - $use_pinecone = isset($bot_pinecone_config['use_pinecone']) ? $bot_pinecone_config['use_pinecone'] : false;
5388 -
5389 - //error_log("MXCHAT DEBUG: Using " . ($use_pinecone ? "Pinecone" : "WordPress Database") . " for knowledge retrieval");
5390 -
5391 - if ($use_pinecone) {
5392 - return $this->find_relevant_content_pinecone($user_embedding, $bot_id, $bot_pinecone_config);
3221 + if ($use_pinecone === 1) {
3222 + //error_log('MXChat Vector Search: Using Pinecone database');
3223 + return $this->find_relevant_content_pinecone($user_embedding);
5393 3224 } else {
5394 - return $this->find_relevant_content_wordpress($user_embedding, $bot_id);
3225 + //error_log('MXChat Vector Search: Using WordPress database');
3226 + return $this->find_relevant_content_wordpress($user_embedding);
5395 3227 }
5396 3228 }
5397 3229
5398 -private function find_relevant_content_wordpress($user_embedding, $bot_id = 'default') {
3230 +private function find_relevant_content_wordpress($user_embedding) {
5399 3231 global $wpdb;
5400 3232 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3233 + $cache_key = 'mxchat_system_prompt_embeddings';
3234 + $batch_size = 500;
3235 +
5401 3236 // Initialize similarity analysis storage
5402 3237 $this->last_similarity_analysis = [
5403 3238 'knowledge_base_type' => 'WordPress Database',
5404 - 'bot_id' => $bot_id,
5405 3239 'top_matches' => [],
5406 3240 'threshold_used' => 0,
5407 3241 'total_checked' => 0
5408 3242 ];
5409 3243
5410 - // NEW: Initialize valid URLs array
5411 - $valid_urls = [];
3244 + // Retrieve embeddings from cache or database
3245 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3246 + if ($embeddings === false) {
3247 + // Cache miss - load embeddings from database WITH CONTENT for testing
3248 + $embeddings = [];
3249 + $offset = 0;
5412 3250
5413 - // Get bot-specific options for similarity threshold
5414 - $bot_options = $this->get_bot_options($bot_id);
5415 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
3251 + do {
3252 + $query = $wpdb->prepare(
3253 + "SELECT id, embedding_vector, article_content, source_url
3254 + FROM {$system_prompt_table}
3255 + LIMIT %d OFFSET %d",
3256 + $batch_size,
3257 + $offset
3258 + );
5416 3259
5417 - // Get knowledge manager instance for role checking
5418 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3260 + $batch = $wpdb->get_results($query);
3261 + if (empty($batch)) {
3262 + break;
3263 + }
5419 3264
5420 - // Get base similarity threshold from bot options or default options
5421 - $similarity_threshold = isset($current_options['similarity_threshold'])
5422 - ? ((int) $current_options['similarity_threshold']) / 100
5423 - : 0.35;
5424 - $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3265 + $embeddings = array_merge($embeddings, $batch);
3266 + $offset += $batch_size;
3267 + unset($batch);
3268 + } while (true);
5425 3269
5426 - // Precompute bot_filter once, outside the streaming loop
5427 - $bot_filter = '';
5428 - if ($bot_id !== 'default') {
5429 - $column_exists = $wpdb->get_var("SHOW COLUMNS FROM {$system_prompt_table} LIKE 'bot_metadata'");
5430 - if ($column_exists) {
5431 - $bot_filter = $wpdb->prepare(" AND (bot_metadata = %s OR bot_metadata IS NULL OR bot_metadata = '')", $bot_id);
3270 + if (empty($embeddings)) {
3271 + return '';
5432 3272 }
3273 +
3274 + // Cache embeddings for future use (but note: this now includes content)
3275 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
5433 3276 }
5434 3277
5435 - // ===== STREAMING TOP-K PASS =====
5436 - // Stream rows in small batches, compute cosine similarity per row, and keep only:
5437 - // - top 10 by raw similarity (for the testing/debug display panel)
5438 - // - candidates above threshold with access (capped) for context assembly
5439 - // This bounds peak memory regardless of knowledge base size and avoids loading
5440 - // article_content for every row. article_content is fetched in Phase 2 for winners only.
5441 - $batch_size = 250;
5442 - $max_candidates = 200; // safety cap, well above rag_sources_limit * max_chunks_per_source
5443 - $top_display = [];
5444 - $candidates = [];
5445 - $total_checked = 0;
5446 - $offset = 0;
5447 -
5448 - do {
5449 - $batch = $wpdb->get_results($wpdb->prepare(
5450 - "SELECT id, embedding_vector, source_url, role_restriction
5451 - FROM {$system_prompt_table}
5452 - WHERE 1=1 {$bot_filter}
5453 - LIMIT %d OFFSET %d",
5454 - $batch_size,
5455 - $offset
5456 - ));
5457 -
5458 - if (empty($batch)) {
5459 - break;
5460 - }
5461 -
5462 - foreach ($batch as $row) {
5463 - $database_embedding = $row->embedding_vector
5464 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
5465 - : null;
5466 -
5467 - if (!is_array($database_embedding) || !is_array($user_embedding)) {
5468 - unset($database_embedding);
5469 - continue;
5470 - }
5471 -
3278 + // Get configuration options
3279 + $main_options = get_option('mxchat_options', []);
3280 +
3281 + // Get base similarity threshold (default 75%)
3282 + $similarity_threshold = isset($main_options['similarity_threshold'])
3283 + ? ((int) $main_options['similarity_threshold']) / 100
3284 + : 0.75;
3285 +
3286 + $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
3287 +
3288 + // Calculate similarities and build results array
3289 + $all_similarities = [];
3290 + $relevant_results = [];
3291 +
3292 + foreach ($embeddings as $embedding) {
3293 + $database_embedding = $embedding->embedding_vector
3294 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3295 + : null;
3296 +
3297 + if (is_array($database_embedding) && is_array($user_embedding)) {
5472 3298 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
5473 - unset($database_embedding);
5474 -
5475 - $role_restriction = $row->role_restriction ?? 'public';
5476 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
5477 - $source_url = $row->source_url ?? '';
5478 -
5479 - // Maintain top 10 display buffer (insert-if-beats-worst)
5480 - if (count($top_display) < 10) {
5481 - $top_display[] = [
5482 - 'id' => $row->id,
5483 - 'similarity' => $similarity,
5484 - 'source_url' => $source_url,
5485 - 'role_restriction' => $role_restriction,
5486 - 'has_access' => $has_access,
5487 - ];
5488 - usort($top_display, function ($a, $b) {
5489 - return $b['similarity'] <=> $a['similarity'];
5490 - });
5491 - } elseif ($similarity > $top_display[9]['similarity']) {
5492 - $top_display[9] = [
5493 - 'id' => $row->id,
5494 - 'similarity' => $similarity,
5495 - 'source_url' => $source_url,
5496 - 'role_restriction' => $role_restriction,
5497 - 'has_access' => $has_access,
5498 - ];
5499 - usort($top_display, function ($a, $b) {
5500 - return $b['similarity'] <=> $a['similarity'];
5501 - });
3299 +
3300 + // Store ALL similarities for testing (top 10)
3301 + $source_display = '';
3302 + if (!empty($embedding->source_url) && $embedding->source_url !== '#') {
3303 + $source_display = $embedding->source_url;
3304 + } else {
3305 + $content_preview = strip_tags($embedding->article_content ?? '');
3306 + $content_preview = preg_replace('/\s+/', ' ', $content_preview);
3307 + $source_display = substr(trim($content_preview), 0, 50) . '...';
5502 3308 }
5503 -
5504 - // Track candidates for context assembly (above threshold + has access)
5505 - if ($similarity >= $similarity_threshold && $has_access) {
5506 - $candidates[] = [
5507 - 'id' => $row->id,
5508 - 'similarity' => $similarity,
5509 - 'source_url' => $source_url,
3309 +
3310 + $all_similarities[] = [
3311 + 'document_id' => $embedding->id,
3312 + 'similarity' => $similarity,
3313 + 'similarity_percentage' => round($similarity * 100, 2),
3314 + 'above_threshold' => $similarity >= $similarity_threshold,
3315 + 'source_display' => $source_display,
3316 + 'content_preview' => substr(strip_tags($embedding->article_content ?? ''), 0, 100) . '...',
3317 + 'used_for_context' => false // Initialize as false, we'll update this later
3318 + ];
3319 +
3320 + // Only consider results above threshold for actual content retrieval
3321 + if ($similarity >= $similarity_threshold) {
3322 + $relevant_results[] = [
3323 + 'id' => $embedding->id,
3324 + 'similarity' => $similarity
5510 3325 ];
5511 3326 }
5512 -
5513 - $total_checked++;
5514 3327 }
5515 -
5516 - unset($batch);
5517 -
5518 - // Trim candidates periodically to cap memory during long scans
5519 - if (count($candidates) > $max_candidates) {
5520 - usort($candidates, function ($a, $b) {
5521 - return $b['similarity'] <=> $a['similarity'];
5522 - });
5523 - $candidates = array_slice($candidates, 0, $max_candidates);
5524 - }
5525 -
5526 - $offset += $batch_size;
5527 - } while (true);
5528 -
5529 - if ($total_checked === 0) {
5530 - $this->current_valid_urls = [];
5531 - return '';
3328 +
3329 + unset($database_embedding);
5532 3330 }
5533 3331
5534 - // Final candidates sort (best first)
5535 - if (count($candidates) > 1) {
5536 - usort($candidates, function ($a, $b) {
5537 - return $b['similarity'] <=> $a['similarity'];
5538 - });
5539 - }
5540 -
5541 - // ===== PHASE 2: FETCH ARTICLE CONTENT ONLY FOR WINNERS =====
5542 - // Gather unique IDs we actually need (top_display + candidates) and pull
5543 - // article_content in bounded IN() batches. This avoids loading content for
5544 - // every row during the similarity scan.
5545 - $needed_ids = [];
5546 - foreach ($top_display as $item) {
5547 - $needed_ids[$item['id']] = true;
5548 - }
5549 - foreach ($candidates as $item) {
5550 - $needed_ids[$item['id']] = true;
5551 - }
5552 - $needed_ids = array_keys($needed_ids);
5553 -
5554 - $content_map = [];
5555 - if (!empty($needed_ids)) {
5556 - foreach (array_chunk($needed_ids, 250) as $chunk_ids) {
5557 - $placeholders = implode(',', array_fill(0, count($chunk_ids), '%d'));
5558 - $rows = $wpdb->get_results($wpdb->prepare(
5559 - "SELECT id, article_content FROM {$system_prompt_table} WHERE id IN ($placeholders)",
5560 - ...$chunk_ids
5561 - ));
5562 - foreach ($rows as $r) {
5563 - $content_map[$r->id] = $r->article_content;
5564 - }
5565 - unset($rows);
5566 - }
5567 - }
5568 -
5569 - // Build the all_similarities display array from the top 10
5570 - $all_similarities = [];
5571 - foreach ($top_display as $item) {
5572 - $article_content_for_parse = $content_map[$item['id']] ?? '';
5573 - $parsed_for_display = MxChat_Chunker::parse_stored_chunk($article_content_for_parse);
5574 - $is_chunk = $parsed_for_display['is_chunked'];
5575 - $chunk_meta = $parsed_for_display['metadata'];
5576 -
5577 - if (!empty($item['source_url']) && $item['source_url'] !== '#') {
5578 - $source_display = $item['source_url'];
5579 - } else {
5580 - $content_preview = strip_tags($article_content_for_parse);
5581 - $content_preview = preg_replace('/\s+/', ' ', $content_preview);
5582 - $source_display = substr(trim($content_preview), 0, 50) . '...';
5583 - }
5584 -
5585 - $all_similarities[] = [
5586 - 'document_id' => $item['id'],
5587 - 'similarity' => $item['similarity'],
5588 - 'similarity_percentage' => round($item['similarity'] * 100, 2),
5589 - 'above_threshold' => $item['similarity'] >= $similarity_threshold,
5590 - 'source_display' => $source_display,
5591 - 'content_preview' => substr(strip_tags($parsed_for_display['text'] ?? ''), 0, 100) . '...',
5592 - 'used_for_context' => false,
5593 - 'role_restriction' => $item['role_restriction'],
5594 - 'has_access' => $item['has_access'],
5595 - 'filtered_out' => !$item['has_access'],
5596 - 'is_chunk' => $is_chunk,
5597 - 'chunk_index' => $is_chunk ? ($chunk_meta['chunk_index'] ?? 0) : null,
5598 - 'total_chunks' => $is_chunk ? ($chunk_meta['total_chunks'] ?? 1) : null
5599 - ];
5600 - }
5601 -
5602 - // Build url_groups from candidates for chunk reassembly
5603 - $url_groups = array();
5604 - foreach ($candidates as $cand) {
5605 - $article_content = $content_map[$cand['id']] ?? '';
5606 - $parsed = MxChat_Chunker::parse_stored_chunk($article_content);
5607 - $is_chunked = $parsed['is_chunked'];
5608 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5609 - $text_content = $parsed['text'];
5610 -
5611 - $source_url = $cand['source_url'];
5612 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $cand['id'];
5613 -
5614 - if (!isset($url_groups[$group_key])) {
5615 - $url_groups[$group_key] = array(
5616 - 'source_url' => $source_url,
5617 - 'best_score' => 0,
5618 - 'is_chunked' => $is_chunked,
5619 - 'chunks' => array(),
5620 - 'single_text' => '',
5621 - 'single_id' => null
5622 - );
5623 - }
5624 -
5625 - if ($cand['similarity'] > $url_groups[$group_key]['best_score']) {
5626 - $url_groups[$group_key]['best_score'] = $cand['similarity'];
5627 - }
5628 -
5629 - if ($is_chunked) {
5630 - $url_groups[$group_key]['is_chunked'] = true;
5631 - $url_groups[$group_key]['chunks'][] = array(
5632 - 'id' => $cand['id'],
5633 - 'score' => $cand['similarity'],
5634 - 'chunk_index' => $chunk_index,
5635 - 'text' => $text_content
5636 - );
5637 - } else {
5638 - $url_groups[$group_key]['single_text'] = $text_content;
5639 - $url_groups[$group_key]['single_id'] = $cand['id'];
5640 - }
5641 - }
5642 -
5643 3332 // Sort ALL similarities for testing display (highest first)
5644 3333 usort($all_similarities, function ($a, $b) {
5645 3334 return $b['similarity'] <=> $a['similarity'];
5646 3335 });
5647 -
5648 - // Sort URL groups by best score (highest first)
5649 - uasort($url_groups, function($a, $b) {
5650 - return $b['best_score'] <=> $a['best_score'];
3336 +
3337 + // Sort relevant results by similarity (highest first)
3338 + usort($relevant_results, function ($a, $b) {
3339 + return $b['similarity'] <=> $a['similarity'];
5651 3340 });
5652 -
5653 - // Get RAG sources limit from options (default 6, min 3, max 10)
5654 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
5655 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
5656 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
5657 -
5658 - // Take top N unique URLs based on user setting
5659 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
5660 -
5661 - // Track which document IDs are used for context
3341 +
3342 + // Get top 5 results for actual content (standard approach)
3343 + $top_results = array_slice($relevant_results, 0, 5);
3344 +
3345 + // NOW mark which documents are actually used for context
5662 3346 $used_document_ids = [];
5663 - foreach ($top_urls as $group) {
5664 - if ($group['is_chunked']) {
5665 - foreach ($group['chunks'] as $chunk) {
5666 - $used_document_ids[] = $chunk['id'];
5667 - }
5668 - } elseif ($group['single_id']) {
5669 - $used_document_ids[] = $group['single_id'];
5670 - }
3347 + foreach ($top_results as $result) {
3348 + $used_document_ids[] = $result['id'];
5671 3349 }
5672 -
3350 +
5673 3351 // Update the all_similarities array to mark which were actually used
5674 3352 foreach ($all_similarities as &$similarity_item) {
5675 3353 $similarity_item['used_for_context'] = in_array($similarity_item['document_id'], $used_document_ids);
5676 3354 }
5677 -
5678 - // Store top 10 for testing panel
3355 +
3356 + // Store top 10 for testing panel (now with correct used_for_context flags)
5679 3357 $this->last_similarity_analysis['top_matches'] = array_slice($all_similarities, 0, 10);
5680 - $this->last_similarity_analysis['total_checked'] = $total_checked;
5681 -
3358 + $this->last_similarity_analysis['total_checked'] = count($embeddings);
3359 +
3360 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " top matches for testing");
3361 +
5682 3362 // Initialize final content
5683 3363 $content = '';
5684 - $matches_used = 0;
5685 - $total_chunks_used = 0;
5686 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
5687 - if ($max_total_chunks < 8) $max_total_chunks = 8;
5688 - if ($max_total_chunks > 20) $max_total_chunks = 20;
5689 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
5690 -
5691 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
5692 - // Use fresh options to ensure we get the latest setting value
5693 - $fresh_options = get_option('mxchat_options', []);
5694 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
5695 -
5696 - // Build content from top sources
5697 - foreach ($top_urls as $group_key => $group) {
5698 - $source_url = $group['source_url']; // Use actual source_url, not the group key
5699 -
5700 - // Stop if we've hit the total chunk limit
5701 - if ($total_chunks_used >= $max_total_chunks) {
5702 - break;
3364 +
3365 + // Track document IDs to avoid duplicates
3366 + $added_document_ids = [];
3367 +
3368 + // Fetch and format content for each selected result
3369 + foreach ($top_results as $index => $result) {
3370 + if (in_array($result['id'], $added_document_ids)) {
3371 + continue;
5703 3372 }
5704 -
5705 - $full_text = '';
5706 - $chunks_in_this_source = 1; // Default for non-chunked content
5707 -
5708 - if ($group['is_chunked']) {
5709 - // Calculate how many chunks we can still use (respect both total and per-source caps)
5710 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
5711 -
5712 - // Fetch chunks for this URL with limit
5713 - $full_text = $this->reassemble_chunks_from_wordpress($source_url, $chunks_remaining, $chunks_in_this_source);
5714 -
5715 - // If fetching all chunks fails, fall back to matched chunks
5716 - if (empty($full_text)) {
5717 - // Sort matched chunks by index and concatenate
5718 - usort($group['chunks'], function($a, $b) {
5719 - return $a['chunk_index'] <=> $b['chunk_index'];
5720 - });
5721 -
5722 - $chunk_texts = array();
5723 - $chunks_in_this_source = 0;
5724 - foreach ($group['chunks'] as $chunk) {
5725 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
5726 - break;
5727 - }
5728 - $chunk_texts[] = $chunk['text'];
5729 - $chunks_in_this_source++;
5730 - }
5731 - $full_text = implode("\n\n", $chunk_texts);
3373 +
3374 + $chunk_content = $this->fetch_content_with_product_links($result['id']);
3375 + $added_document_ids[] = $result['id'];
3376 +
3377 + $content .= "## Reference " . ($index + 1) . " ##\n";
3378 + $content .= $chunk_content . "\n\n";
3379 +
3380 + // PDF surrounding pages logic (unchanged)
3381 + if (strpos($chunk_content, '{"document_type":"pdf"') !== false) {
3382 + $surrounding_content = $wpdb->get_results($wpdb->prepare(
3383 + "SELECT id, article_content FROM {$system_prompt_table}
3384 + WHERE id IN (
3385 + (SELECT id FROM {$system_prompt_table} WHERE id < %d ORDER BY id DESC LIMIT 1),
3386 + (SELECT id FROM {$system_prompt_table} WHERE id > %d ORDER BY id ASC LIMIT 1)
3387 + )",
3388 + $result['id'],
3389 + $result['id']
3390 + ));
3391 +
3392 + if (!empty($surrounding_content[0])) {
3393 + $content .= "## Related Content ##\n";
3394 + $content .= $surrounding_content[0]->article_content . "\n\n";
3395 + $added_document_ids[] = $surrounding_content[0]->id;
5732 3396 }
5733 - } else {
5734 - $full_text = $group['single_text'];
5735 - $chunks_in_this_source = 1;
5736 - }
5737 -
5738 - if (!empty($full_text)) {
5739 - // Strip URLs from content if citation links are disabled
5740 - if (!$citation_links_enabled) {
5741 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
5742 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
3397 +
3398 + if (!empty($surrounding_content[1])) {
3399 + $content .= "## Related Content ##\n";
3400 + $content .= $surrounding_content[1]->article_content . "\n\n";
3401 + $added_document_ids[] = $surrounding_content[1]->id;
5743 3402 }
5744 -
5745 - // Use numbered reference for URL-based entries, plain info label for manual entries
5746 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
5747 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
5748 - $matches_used++;
5749 - $content .= "## Reference " . $matches_used . " ##\n";
5750 - $content .= $full_text . "\n\n";
5751 -
5752 - // Only include citation URLs if citation links are enabled
5753 - if ($citation_links_enabled) {
5754 - $valid_urls[] = $source_url;
5755 - $content .= "URL: " . $source_url . "\n\n";
5756 - }
5757 - } else {
5758 - // Manual entry — no reference number, no citation
5759 - $content .= "## Information ##\n";
5760 - $content .= $full_text . "\n\n";
5761 - }
5762 -
5763 - // Extract any URLs from the text content itself (only if citation links enabled)
5764 - if ($citation_links_enabled) {
5765 - preg_match_all(
5766 - '#\bhttps?://[^\s<>"\']+#i',
5767 - $full_text,
5768 - $content_urls
5769 - );
5770 - if (!empty($content_urls[0])) {
5771 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
5772 - }
5773 - }
5774 -
5775 - $total_chunks_used += $chunks_in_this_source;
5776 3403 }
5777 3404 }
5778 -
5779 - // NEW: Store unique valid URLs for validation
5780 - $this->current_valid_urls = array_unique($valid_urls);
5781 -
5782 - // Store sources and chunks counts for testing/transcript display
5783 - $this->last_similarity_analysis['sources_used'] = $matches_used;
5784 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
5785 -
5786 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
5787 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
5788 -
5789 - // Add response guidelines
5790 - if (empty($top_urls)) {
5791 - $content = "No reference information was found for this query.\n\n";
5792 - } else {
5793 - // Build response guidelines based on citation links setting
5794 - $content .= "\n## Response Guidelines ##\n" .
5795 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
5796 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
5797 - "If you don't have specific information or are uncertain about any details, it's always " .
5798 - "better to honestly say you don't know rather than making up or guessing at answers. " .
5799 - "When information is incomplete, let them know you are unsure.\n\n";
5800 -
5801 - // Only add hyperlink instructions if citation links are enabled
5802 - if ($citation_links_enabled) {
5803 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
5804 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
5805 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
3405 +
3406 + // Add response guidelines
3407 + if (empty($top_results)) {
3408 + $content = "No reference information was found for this query.\n\n";
5806 3409 } else {
5807 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
5808 - "Simply provide helpful answers based on the reference information without citing sources.";
3410 + $content .= "\n## Response Guidelines ##\n" .
3411 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3412 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3413 + "If you don't have specific information or are uncertain about any details, it's always " .
3414 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3415 + "When information is incomplete, let them know you are unsure.";
5809 3416 }
5810 - }
5811 -
3417 +
5812 3418 return trim($content);
5813 3419 }
5814 3420
5815 -/**
5816 - * Fetch and reassemble chunks for a URL from WordPress database
5817 - *
5818 - * @param string $source_url The source URL to fetch chunks for
5819 - * @param int $max_chunks Maximum number of chunks to return (0 = unlimited)
5820 - * @param int &$chunk_count Reference to store the actual number of chunks returned
5821 - * @return string Reassembled content from chunks
5822 - */
5823 -private function reassemble_chunks_from_wordpress($source_url, $max_chunks = 0, &$chunk_count = 0) {
5824 - global $wpdb;
5825 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
5826 -
5827 - // Fetch all rows with this source_url
5828 - $rows = $wpdb->get_results($wpdb->prepare(
5829 - "SELECT article_content FROM {$table}
5830 - WHERE source_url = %s
5831 - ORDER BY id ASC",
5832 - $source_url
5833 - ));
5834 -
5835 - if (empty($rows)) {
5836 - $chunk_count = 0;
5837 - return '';
5838 - }
5839 -
5840 - // Parse and sort chunks by index
5841 - $chunks = array();
5842 - foreach ($rows as $row) {
5843 - $parsed = MxChat_Chunker::parse_stored_chunk($row->article_content);
5844 -
5845 - if ($parsed['is_chunked']) {
5846 - $chunk_index = $parsed['metadata']['chunk_index'] ?? 0;
5847 - $chunks[$chunk_index] = $parsed['text'];
5848 - } else {
5849 - // Non-chunked content - just return it
5850 - $chunks[] = $parsed['text'];
5851 - }
5852 - }
5853 -
5854 - // Sort by chunk index
5855 - ksort($chunks);
5856 -
5857 - // Apply chunk limit if specified
5858 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
5859 - $chunks = array_slice($chunks, 0, $max_chunks, true);
5860 - }
5861 -
5862 - // Store actual chunk count
5863 - $chunk_count = count($chunks);
5864 -
5865 - // Reassemble content
5866 - return implode("\n\n", $chunks);
5867 -}
5868 -
5869 -private function find_relevant_content_pinecone($user_embedding, $bot_id = 'default', $bot_config = null) {
5870 - global $wpdb;
3421 +private function find_relevant_content_pinecone($user_embedding) {
3422 + $options = get_option('mxchat_pinecone_addon_options', array());
3423 + $api_key = $options['mxchat_pinecone_api_key'] ?? '';
3424 + $host = $options['mxchat_pinecone_host'] ?? '';
5871 3425
5872 - //error_log("MXCHAT DEBUG: find_relevant_content_pinecone called");
5873 - //error_log(" - bot_id: " . $bot_id);
5874 - //error_log(" - user_embedding is array: " . (is_array($user_embedding) ? 'yes' : 'no'));
5875 - //error_log(" - user_embedding count: " . (is_array($user_embedding) ? count($user_embedding) : 'N/A'));
5876 -
5877 - // Use bot-specific config or fall back to default
5878 - if ($bot_config === null) {
5879 - $bot_config = $this->get_bot_pinecone_config($bot_id);
5880 - }
5881 -
5882 - $api_key = $bot_config['api_key'] ?? '';
5883 - $host = $bot_config['host'] ?? '';
5884 - $namespace = $bot_config['namespace'] ?? '';
5885 -
5886 - //error_log("MXCHAT DEBUG: Pinecone query parameters:");
5887 - //error_log(" - API Key: " . (empty($api_key) ? 'EMPTY - ERROR!' : 'Present (length: ' . strlen($api_key) . ')'));
5888 - //error_log(" - Host: " . (empty($host) ? 'EMPTY - ERROR!' : $host));
5889 - //error_log(" - Namespace: " . (empty($namespace) ? 'EMPTY (will use default)' : $namespace));
5890 -
5891 3426 // Initialize similarity analysis storage
5892 3427 $this->last_similarity_analysis = [
5893 3428 'knowledge_base_type' => 'Pinecone',
5894 - 'bot_id' => $bot_id,
5895 - 'namespace' => $namespace,
5896 3429 'top_matches' => [],
5897 3430 'threshold_used' => 0,
5898 3431 'total_checked' => 0
5899 3432 ];
5900 3433
5901 - // NEW: Initialize valid URLs array
5902 - $valid_urls = [];
5903 -
5904 3434 if (empty($host) || empty($api_key)) {
5905 - //error_log("MXCHAT DEBUG ERROR: Missing Pinecone host or API key!");
5906 - //error_log(" - Host empty: " . (empty($host) ? 'YES' : 'NO'));
5907 - //error_log(" - API key empty: " . (empty($api_key) ? 'YES' : 'NO'));
5908 - // Store empty array for valid URLs since we can't proceed
5909 - $this->current_valid_urls = [];
5910 3435 return '';
5911 3436 }
5912 3437
5913 - // Get knowledge manager instance for role checking
5914 - $knowledge_manager = MxChat_Knowledge_Manager::get_instance();
3438 + // Get the similarity threshold from the main options
3439 + $main_options = get_option('mxchat_options', []);
3440 + $similarity_threshold = isset($main_options['similarity_threshold'])
3441 + ? ((int) $main_options['similarity_threshold']) / 100
3442 + : 0.75;
5915 3443
5916 - // Get the similarity threshold from the bot options or main options
5917 - $bot_options = $this->get_bot_options($bot_id);
5918 - $current_options = !empty($bot_options) ? $bot_options : get_option('mxchat_options', []);
5919 -
5920 - $similarity_threshold = isset($current_options['similarity_threshold'])
5921 - ? ((int) $current_options['similarity_threshold']) / 100
5922 - : 0.35;
5923 -
5924 3444 $this->last_similarity_analysis['threshold_used'] = $similarity_threshold;
5925 3445
5926 - // Prepare the query request for Pinecone
3446 + // Prepare the query request for Pinecone (request more for testing)
5927 3447 $api_endpoint = "https://{$host}/query";
5928 3448
5929 3449 $request_body = array(
5930 3450 'vector' => $user_embedding,
5931 - 'topK' => 50, // Increased for chunked content grouping - need more candidates to find top N unique URLs
3451 + 'topK' => 20, // Request more to get good testing data
5932 3452 'includeMetadata' => true,
5933 3453 'includeValues' => true
5934 3454 );
5935 3455
5936 - // Add namespace if specified for this bot
5937 - if (!empty($namespace)) {
5938 - $request_body['namespace'] = $namespace;
5939 - }
5940 -
5941 - //error_log("MXCHAT DEBUG: About to call Pinecone API");
5942 - //error_log(" - Endpoint: " . $api_endpoint);
5943 - //error_log(" - Namespace in request: " . (!empty($namespace) ? $namespace : 'NOT SET'));
5944 -
5945 3456 $response = wp_remote_post($api_endpoint, array(
5946 3457 'headers' => array(
5947 3458 'Api-Key' => $api_key,
5948 3459 'accept' => 'application/json',
@@ -5952,253 +3463,47 @@
5952 3463 'timeout' => 30
5953 3464 ));
5954 3465
5955 3466 if (is_wp_error($response)) {
5956 - //error_log("MXCHAT DEBUG ERROR: WP Error in Pinecone request: " . $response->get_error_message());
5957 - // Store empty array for valid URLs
5958 - $this->current_valid_urls = [];
5959 3467 return '';
5960 3468 }
5961 3469
5962 3470 $response_code = wp_remote_retrieve_response_code($response);
5963 - //error_log("MXCHAT DEBUG: Pinecone response code: " . $response_code);
5964 -
5965 3471 if ($response_code !== 200) {
5966 - $response_body = wp_remote_retrieve_body($response);
5967 - //error_log("MXCHAT DEBUG ERROR: Pinecone API error response: " . substr($response_body, 0, 500));
5968 - // Store empty array for valid URLs
5969 - $this->current_valid_urls = [];
5970 3472 return '';
5971 3473 }
5972 3474
5973 - // ADD DETAILED DEBUG SECTION HERE
5974 - $response_body = wp_remote_retrieve_body($response);
5975 - //error_log("MXCHAT DEBUG: Raw Pinecone response length: " . strlen($response_body));
5976 -
5977 - $results = json_decode($response_body, true);
5978 -
5979 - if (json_last_error() !== JSON_ERROR_NONE) {
5980 - //error_log("MXCHAT DEBUG ERROR: JSON decode error: " . json_last_error_msg());
5981 - //error_log("MXCHAT DEBUG: First 500 chars of response: " . substr($response_body, 0, 500));
5982 - // Store empty array for valid URLs
5983 - $this->current_valid_urls = [];
5984 - return '';
5985 - }
5986 -
5987 - //error_log("MXCHAT DEBUG: Pinecone response structure:");
5988 - //error_log(" - Has 'matches' key: " . (isset($results['matches']) ? 'yes' : 'no'));
5989 - //error_log(" - Has 'namespace' key: " . (isset($results['namespace']) ? 'yes (' . $results['namespace'] . ')' : 'no'));
5990 -
3475 + $results = json_decode(wp_remote_retrieve_body($response), true);
5991 3476 if (empty($results['matches'])) {
5992 - //error_log("MXCHAT DEBUG: No matches found in Pinecone response");
5993 - //error_log("MXCHAT DEBUG: Response keys: " . implode(', ', array_keys($results)));
5994 - // Store empty array for valid URLs
5995 - $this->current_valid_urls = [];
5996 3477 return '';
5997 3478 }
5998 3479
5999 - //error_log("MXCHAT DEBUG: Found " . count($results['matches']) . " matches in Pinecone");
3480 + // First, determine which matches will actually be used for content
3481 + $matches_used_for_context = [];
3482 + $matches_used = 0;
6000 3483
6001 - // Log first match details for debugging
6002 - if (!empty($results['matches'][0])) {
6003 - $first_match = $results['matches'][0];
6004 - //error_log("MXCHAT DEBUG: First match details:");
6005 - //error_log(" - Score: " . ($first_match['score'] ?? 'no score'));
6006 - //error_log(" - Has metadata: " . (isset($first_match['metadata']) ? 'yes' : 'no'));
6007 - if (isset($first_match['metadata'])) {
6008 - //error_log(" - Metadata keys: " . implode(', ', array_keys($first_match['metadata'])));
6009 - }
6010 - }
6011 -
6012 - // Initialize the final content
6013 - $content = '';
6014 - $matches_used = 0;
6015 - $matches_used_for_context = [];
6016 - $total_chunks_used = 0;
6017 - $max_total_chunks = isset($current_options['rag_chunks_limit']) ? intval($current_options['rag_chunks_limit']) : 15;
6018 - if ($max_total_chunks < 8) $max_total_chunks = 8;
6019 - if ($max_total_chunks > 20) $max_total_chunks = 20;
6020 - $max_chunks_per_source = 5; // Cap per individual source to limit token usage
6021 -
6022 - // Check if citation links are enabled (default to 'on' for backwards compatibility)
6023 - // Use fresh options to ensure we get the latest setting value
6024 - $fresh_options = get_option('mxchat_options', []);
6025 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
6026 -
6027 - // NEW CHUNKING LOGIC: Group results by source_url for chunk reassembly
6028 - $url_groups = array();
6029 -
6030 3484 foreach ($results['matches'] as $index => $match) {
6031 3485 // Skip if similarity is below threshold
6032 3486 if ($match['score'] < $similarity_threshold) {
6033 3487 continue;
6034 3488 }
6035 -
6036 - $metadata = $match['metadata'] ?? array();
6037 - $source_url = $metadata['source_url'] ?? '';
6038 - $match_id = $match['id'] ?? '';
6039 -
6040 - // LAZY ROLE CHECK: Only check role for content we're actually considering
6041 - $role_restriction = $this->get_single_vector_role($match_id, $metadata);
6042 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
6043 -
6044 - // Skip if user doesn't have access
6045 - if (!$has_access) {
6046 - continue;
6047 - }
6048 -
6049 - // Use a unique key for manual entries without a source URL
6050 - $group_key = !empty($source_url) ? $source_url : '_manual_' . $match_id;
6051 -
6052 - // Group by source URL (or unique key for manual entries)
6053 - if (!isset($url_groups[$group_key])) {
6054 - $url_groups[$group_key] = array(
6055 - 'source_url' => $source_url,
6056 - 'best_score' => 0,
6057 - 'is_chunked' => isset($metadata['is_chunked']) && $metadata['is_chunked'],
6058 - 'chunks' => array(),
6059 - 'single_text' => ''
6060 - );
6061 - }
6062 -
6063 - // Track best score for this group
6064 - if ($match['score'] > $url_groups[$group_key]['best_score']) {
6065 - $url_groups[$group_key]['best_score'] = $match['score'];
6066 - }
6067 -
6068 - // Store chunk info or single text
6069 - if ($url_groups[$group_key]['is_chunked']) {
6070 - $url_groups[$group_key]['chunks'][] = array(
6071 - 'id' => $match_id,
6072 - 'score' => $match['score'],
6073 - 'chunk_index' => $metadata['chunk_index'] ?? 0,
6074 - 'text' => $metadata['text'] ?? ''
6075 - );
6076 - } else {
6077 - // Non-chunked content - just store the text
6078 - $url_groups[$group_key]['single_text'] = $metadata['text'] ?? '';
6079 - $url_groups[$group_key]['single_id'] = $match_id;
6080 - }
6081 - }
6082 -
6083 - // Sort URL groups by best score (highest first)
6084 - uasort($url_groups, function($a, $b) {
6085 - return $b['best_score'] <=> $a['best_score'];
6086 - });
6087 -
6088 - // Get RAG sources limit from options (default 6, min 3, max 10)
6089 - $rag_sources_limit = isset($current_options['rag_sources_limit']) ? intval($current_options['rag_sources_limit']) : 3;
6090 - if ($rag_sources_limit < 3) $rag_sources_limit = 3;
6091 - if ($rag_sources_limit > 10) $rag_sources_limit = 10;
6092 -
6093 - // Take top N unique URLs based on user setting
6094 - $top_urls = array_slice($url_groups, 0, $rag_sources_limit, true);
6095 -
6096 - // Track which match IDs are actually used for context
6097 - foreach ($top_urls as $group) {
6098 - if ($group['is_chunked']) {
6099 - foreach ($group['chunks'] as $chunk) {
6100 - $matches_used_for_context[] = $chunk['id'];
6101 - }
6102 - } elseif (!empty($group['single_id'])) {
6103 - $matches_used_for_context[] = $group['single_id'];
6104 - }
6105 - }
6106 -
6107 - // Build content from top sources
6108 - foreach ($top_urls as $group_key => $group) {
6109 - $source_url = $group['source_url']; // Use actual source_url, not the group key
6110 -
6111 - // Stop if we've hit the total chunk limit
6112 - if ($total_chunks_used >= $max_total_chunks) {
3489 +
3490 + // Limit to top 5 matches above threshold
3491 + if ($matches_used >= 5) {
6113 3492 break;
6114 3493 }
6115 -
6116 - $full_text = '';
6117 - $chunks_in_this_source = 1; // Default for non-chunked content
6118 -
6119 - if ($group['is_chunked']) {
6120 - // Calculate how many chunks we can still use (respect both total and per-source caps)
6121 - $chunks_remaining = min($max_chunks_per_source, $max_total_chunks - $total_chunks_used);
6122 -
6123 - // Fetch chunks for this URL with limit
6124 - $full_text = $this->reassemble_chunks_from_pinecone($source_url, $bot_config, $chunks_remaining, $chunks_in_this_source);
6125 -
6126 - // If fetching all chunks fails, fall back to matched chunks
6127 - if (empty($full_text)) {
6128 - // Sort matched chunks by index and concatenate
6129 - usort($group['chunks'], function($a, $b) {
6130 - return $a['chunk_index'] <=> $b['chunk_index'];
6131 - });
6132 -
6133 - $chunk_texts = array();
6134 - $chunks_in_this_source = 0;
6135 - foreach ($group['chunks'] as $chunk) {
6136 - if ($total_chunks_used + $chunks_in_this_source >= $max_total_chunks) {
6137 - break;
6138 - }
6139 - $chunk_texts[] = $chunk['text'];
6140 - $chunks_in_this_source++;
6141 - }
6142 - $full_text = implode("\n\n", $chunk_texts);
6143 - }
6144 - } else {
6145 - $full_text = $group['single_text'];
6146 - $chunks_in_this_source = 1;
3494 +
3495 + if (!empty($match['metadata']['text'])) {
3496 + $matches_used_for_context[] = $match['id'] ?? $index;
3497 + $matches_used++;
6147 3498 }
6148 -
6149 - if (!empty($full_text)) {
6150 - // Strip URLs from content if citation links are disabled
6151 - if (!$citation_links_enabled) {
6152 - $full_text = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $full_text);
6153 - $full_text = preg_replace('/\s+/', ' ', trim($full_text)); // Clean up extra spaces
6154 - }
6155 -
6156 - // Use numbered reference for URL-based entries, plain info label for manual entries
6157 - // Manual entries are stored with an internal mxchat:// placeholder URL — never expose them as citations
6158 - if (!empty($source_url) && $source_url !== '#' && strpos($source_url, 'mxchat://') !== 0) {
6159 - $matches_used++;
6160 - $content .= "## Reference " . $matches_used . " ##\n";
6161 - $content .= $full_text . "\n\n";
6162 -
6163 - // Only include citation URLs if citation links are enabled
6164 - if ($citation_links_enabled) {
6165 - $valid_urls[] = $source_url;
6166 - $content .= "URL: " . $source_url . "\n\n";
6167 - }
6168 - } else {
6169 - // Manual entry — no reference number, no citation
6170 - $content .= "## Information ##\n";
6171 - $content .= $full_text . "\n\n";
6172 - }
6173 -
6174 - // Extract any URLs from the text content itself (only if citation links enabled)
6175 - if ($citation_links_enabled) {
6176 - preg_match_all(
6177 - '#\bhttps?://[^\s<>"\']+#i',
6178 - $full_text,
6179 - $content_urls
6180 - );
6181 - if (!empty($content_urls[0])) {
6182 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6183 - }
6184 - }
6185 -
6186 - $total_chunks_used += $chunks_in_this_source;
6187 - }
6188 3499 }
6189 -
6190 - // Process ALL matches for testing data (top 10) - with role checking for testing display
3500 +
3501 + // Process ALL matches for testing data (top 10)
6191 3502 $all_matches = [];
6192 3503 foreach ($results['matches'] as $index => $match) {
6193 3504 if ($index >= 10) break; // Limit to top 10 for testing
6194 3505
6195 - $match_id = $match['id'] ?? '';
6196 -
6197 - // Check role access for testing display (use cache if available)
6198 - $role_restriction = $this->get_single_vector_role($match_id, $match['metadata']);
6199 - $has_access = $knowledge_manager->mxchat_user_has_content_access($role_restriction);
6200 -
6201 3506 $source_display = '';
6202 3507 if (!empty($match['metadata']['source_url'])) {
6203 3508 $source_display = $match['metadata']['source_url'];
6204 3509 } else {
@@ -6206,34 +3511,18 @@
6206 3511 $content_preview = preg_replace('/\s+/', ' ', $content_preview);
6207 3512 $source_display = substr(trim($content_preview), 0, 50) . '...';
6208 3513 }
6209 3514
6210 - $match_id_for_display = $match['id'] ?? $index;
6211 -
6212 - // Check for chunk metadata in Pinecone
6213 - $is_chunk = isset($match['metadata']['is_chunked']) && $match['metadata']['is_chunked'];
6214 - $chunk_index = isset($match['metadata']['chunk_index']) ? intval($match['metadata']['chunk_index']) : null;
6215 - $total_chunks = isset($match['metadata']['total_chunks']) ? intval($match['metadata']['total_chunks']) : null;
6216 -
6217 - // Also detect chunk from vector ID pattern: {hash}_chunk_{index}
6218 - if (!$is_chunk && MxChat_Chunker::is_chunk_vector_id($match_id_for_display)) {
6219 - $is_chunk = true;
6220 - }
6221 -
3515 + $match_id = $match['id'] ?? $index;
3516 +
6222 3517 $all_matches[] = [
6223 - 'document_id' => $match_id_for_display,
3518 + 'document_id' => $match_id,
6224 3519 'similarity' => $match['score'],
6225 3520 'similarity_percentage' => round($match['score'] * 100, 2),
6226 3521 'above_threshold' => $match['score'] >= $similarity_threshold,
6227 3522 'source_display' => $source_display,
6228 3523 'content_preview' => substr(strip_tags($match['metadata']['text'] ?? ''), 0, 100) . '...',
6229 - 'used_for_context' => in_array($match_id_for_display, $matches_used_for_context),
6230 - 'role_restriction' => $role_restriction,
6231 - 'has_access' => $has_access,
6232 - 'filtered_out' => !$has_access,
6233 - 'is_chunk' => $is_chunk,
6234 - 'chunk_index' => $chunk_index,
6235 - 'total_chunks' => $total_chunks
3524 + 'used_for_context' => in_array($match_id, $matches_used_for_context) // Correct usage flag
6236 3525 ];
6237 3526 }
6238 3527
6239 3528 // Store for testing panel
@@ -6238,591 +3527,54 @@
6238 3527
6239 3528 // Store for testing panel
6240 3529 $this->last_similarity_analysis['top_matches'] = $all_matches;
6241 3530 $this->last_similarity_analysis['total_checked'] = count($results['matches']);
6242 - $this->last_similarity_analysis['sources_used'] = $matches_used;
6243 - $this->last_similarity_analysis['total_chunks_used'] = $total_chunks_used;
6244 -
6245 - // NEW: Store unique valid URLs for validation
6246 - $this->current_valid_urls = array_unique($valid_urls);
6247 -
6248 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6249 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6250 -
6251 - // Add response guidelines
6252 - if ($matches_used === 0) {
6253 - $content = "No reference information was found for this query.\n\n";
6254 - } else {
6255 - // Build response guidelines based on citation links setting
6256 - $content .= "\n## Response Guidelines ##\n" .
6257 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6258 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6259 - "If you don't have specific information or are uncertain about any details, it's always " .
6260 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6261 - "When information is incomplete, let them know you are unsure.\n\n";
6262 -
6263 - // Only add hyperlink instructions if citation links are enabled
6264 - if ($citation_links_enabled) {
6265 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6266 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about. " .
6267 - "Only cite references that have a URL. Do not cite or add source labels to Information sections that have no URL.";
6268 - } else {
6269 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6270 - "Simply provide helpful answers based on the reference information without citing sources.";
6271 - }
6272 - }
6273 -
6274 - return trim($content);
6275 -}
6276 -
6277 -/**
6278 - * Get role restriction for a single vector (with caching)
6279 - */
6280 -private function get_single_vector_role($vector_id, $metadata = array()) {
6281 - global $wpdb;
6282 3531
6283 - if (empty($vector_id)) {
6284 - return 'public';
6285 - }
3532 + //error_log("MxChat Testing: Stored " . count($this->last_similarity_analysis['top_matches']) . " Pinecone matches for testing");
6286 3533
6287 - // Check cache first
6288 - $cache_key = 'mxchat_vector_role_' . $vector_id;
6289 - $cached_role = wp_cache_get($cache_key, 'mxchat_vector_roles');
3534 + // Initialize the final content
3535 + $content = '';
3536 + $matches_used = 0;
6290 3537
6291 - if ($cached_role !== false) {
6292 - return $cached_role;
6293 - }
6294 -
6295 - $role_restriction = 'public';
6296 -
6297 - // First try Pinecone metadata
6298 - if (!empty($metadata['role_restriction'])) {
6299 - $role_restriction = $metadata['role_restriction'];
6300 - } else {
6301 - // Check WordPress table for user-modified roles
6302 - $roles_table = $wpdb->prefix . 'mxchat_pinecone_roles';
6303 - $stored_role = $wpdb->get_var($wpdb->prepare(
6304 - "SELECT role_restriction FROM {$roles_table} WHERE vector_id = %s",
6305 - $vector_id
6306 - ));
3538 + // Process each match for actual content (this is the real content generation)
3539 + foreach ($results['matches'] as $index => $match) {
3540 + // Skip if similarity is below threshold
3541 + if ($match['score'] < $similarity_threshold) {
3542 + continue;
3543 + }
6307 3544
6308 - if ($stored_role) {
6309 - $role_restriction = $stored_role;
3545 + // Limit to top 5 matches above threshold
3546 + if ($matches_used >= 5) {
3547 + break;
6310 3548 }
6311 - }
6312 -
6313 - // Cache individual role for 1 hour
6314 - wp_cache_set($cache_key, $role_restriction, 'mxchat_vector_roles', 3600);
6315 -
6316 - return $role_restriction;
6317 -}
6318 -
6319 -/**
6320 - * Fetch and reassemble all chunks for a URL from Pinecone
6321 - *
6322 - * @param string $source_url The source URL to fetch chunks for
6323 - * @param array $bot_config Bot-specific Pinecone configuration
6324 - * @return string Reassembled content from all chunks
6325 - */
6326 -private function reassemble_chunks_from_pinecone($source_url, $bot_config, $max_chunks = 0, &$chunk_count = 0) {
6327 - $api_key = $bot_config['api_key'] ?? '';
6328 - $host = $bot_config['host'] ?? '';
6329 - $namespace = $bot_config['namespace'] ?? '';
6330 -
6331 - if (empty($host) || empty($api_key)) {
6332 - $chunk_count = 0;
6333 - return '';
6334 - }
6335 -
6336 - $base_hash = md5($source_url);
6337 -
6338 - // Use Pinecone list API to find all chunk vectors with this prefix
6339 - $list_url = "https://{$host}/vectors/list";
6340 -
6341 - // Limit to max_chunks if specified, otherwise fetch up to 100
6342 - $fetch_limit = ($max_chunks > 0 && $max_chunks < 100) ? $max_chunks : 100;
6343 -
6344 - $list_body = array(
6345 - 'prefix' => $base_hash . '_chunk_',
6346 - 'limit' => $fetch_limit
6347 - );
6348 -
6349 - if (!empty($namespace)) {
6350 - $list_body['namespace'] = $namespace;
6351 - }
6352 -
6353 - $list_response = wp_remote_post($list_url, array(
6354 - 'headers' => array(
6355 - 'Api-Key' => $api_key,
6356 - 'accept' => 'application/json',
6357 - 'content-type' => 'application/json'
6358 - ),
6359 - 'body' => wp_json_encode($list_body),
6360 - 'timeout' => 30
6361 - ));
6362 -
6363 - if (is_wp_error($list_response)) {
6364 - //error_log('[MXCHAT-CHUNK] List API error: ' . $list_response->get_error_message());
6365 - return '';
6366 - }
6367 -
6368 - $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
6369 -
6370 - if (empty($list_data['vectors'])) {
6371 - //error_log('[MXCHAT-CHUNK] No chunk vectors found for URL: ' . $source_url);
6372 - return '';
6373 - }
6374 -
6375 - // Extract vector IDs
6376 - $vector_ids = array();
6377 - foreach ($list_data['vectors'] as $vector) {
6378 - if (isset($vector['id'])) {
6379 - $vector_ids[] = $vector['id'];
6380 - }
6381 - }
6382 -
6383 - if (empty($vector_ids)) {
6384 - return '';
6385 - }
6386 -
6387 - // Fetch all chunk content
6388 - $fetch_url = "https://{$host}/vectors/fetch";
6389 -
6390 - $fetch_body = array(
6391 - 'ids' => $vector_ids
6392 - );
6393 -
6394 - if (!empty($namespace)) {
6395 - $fetch_body['namespace'] = $namespace;
6396 - }
6397 -
6398 - $fetch_response = wp_remote_post($fetch_url, array(
6399 - 'headers' => array(
6400 - 'Api-Key' => $api_key,
6401 - 'accept' => 'application/json',
6402 - 'content-type' => 'application/json'
6403 - ),
6404 - 'body' => wp_json_encode($fetch_body),
6405 - 'timeout' => 30
6406 - ));
6407 -
6408 - if (is_wp_error($fetch_response)) {
6409 - //error_log('[MXCHAT-CHUNK] Fetch API error: ' . $fetch_response->get_error_message());
6410 - return '';
6411 - }
6412 -
6413 - $fetch_data = json_decode(wp_remote_retrieve_body($fetch_response), true);
6414 -
6415 - if (empty($fetch_data['vectors'])) {
6416 - return '';
6417 - }
6418 -
6419 - // Sort chunks by index and reassemble
6420 - $chunks = array();
6421 - foreach ($fetch_data['vectors'] as $id => $vector) {
6422 - $metadata = $vector['metadata'] ?? array();
6423 - $chunk_index = $metadata['chunk_index'] ?? 0;
6424 - $text = $metadata['text'] ?? '';
6425 -
6426 - // Store chunk with its index
6427 - $chunks[$chunk_index] = $text;
6428 - }
6429 -
6430 - // Sort by chunk index
6431 - ksort($chunks);
6432 -
6433 - // Apply chunk limit if specified
6434 - if ($max_chunks > 0 && count($chunks) > $max_chunks) {
6435 - $chunks = array_slice($chunks, 0, $max_chunks, true);
6436 - }
6437 -
6438 - // Store actual chunk count
6439 - $chunk_count = count($chunks);
6440 -
6441 - // Reassemble content
6442 - return implode("\n\n", $chunks);
6443 -}
6444 -
6445 -/**
6446 - * Search for relevant content using OpenAI Vector Store (File Search)
6447 - *
6448 - * @param string $user_query The user's query text
6449 - * @param string $bot_id The bot ID
6450 - * @param array $vectorstore_config Vector Store configuration
6451 - * @return string Formatted context string with references
6452 - */
6453 -private function find_relevant_content_openai_vectorstore($user_query, $bot_id = 'default', $vectorstore_config = array()) {
6454 - //error_log("MXCHAT DEBUG: find_relevant_content_openai_vectorstore called");
6455 - //error_log(" - bot_id: " . $bot_id);
6456 - //error_log(" - user_query length: " . strlen($user_query));
6457 -
6458 - // Get OpenAI API key
6459 - $mxchat_options = get_option('mxchat_options', array());
6460 - $api_key = $mxchat_options['api_key'] ?? '';
6461 -
6462 - // Reset vectorstore error tracking
6463 - $this->last_vectorstore_error = null;
6464 -
6465 - if (empty($api_key)) {
6466 - //error_log("MXCHAT DEBUG ERROR: OpenAI API key not configured");
6467 - $this->last_vectorstore_error = 'Vector Store search failed: OpenAI API key is not configured.';
6468 - $this->current_valid_urls = [];
6469 - return '';
6470 - }
6471 -
6472 - // Get Vector Store configuration
6473 - if (empty($vectorstore_config)) {
6474 - $vectorstore_config = $this->get_bot_vectorstore_config($bot_id);
6475 - }
6476 -
6477 - $vectorstore_ids_string = $vectorstore_config['vectorstore_ids'] ?? '';
6478 - $max_results = $vectorstore_config['max_results'] ?? 5;
6479 -
6480 - if (empty($vectorstore_ids_string)) {
6481 - //error_log("MXCHAT DEBUG ERROR: No Vector Store IDs configured");
6482 - $this->last_vectorstore_error = 'Vector Store search failed: No Vector Store IDs are configured for this bot.';
6483 - $this->current_valid_urls = [];
6484 - return '';
6485 - }
6486 -
6487 - // Parse Vector Store IDs
6488 - $vectorstore_ids = array_map('trim', explode(',', $vectorstore_ids_string));
6489 - $vectorstore_ids = array_filter($vectorstore_ids); // Remove empty values
6490 -
6491 - //error_log("MXCHAT DEBUG: Vector Store IDs: " . implode(', ', $vectorstore_ids));
6492 - //error_log("MXCHAT DEBUG: Max results: " . $max_results);
6493 -
6494 - // Initialize similarity analysis storage
6495 - $this->last_similarity_analysis = [
6496 - 'knowledge_base_type' => 'OpenAI Vector Store',
6497 - 'bot_id' => $bot_id,
6498 - 'vectorstore_ids' => $vectorstore_ids,
6499 - 'top_matches' => [],
6500 - 'threshold_used' => 0,
6501 - 'total_checked' => 0
6502 - ];
6503 -
6504 - $valid_urls = [];
6505 -
6506 - // Get the selected model
6507 - $bot_options = $this->get_bot_options($bot_id);
6508 - $current_options = !empty($bot_options) ? $bot_options : $mxchat_options;
6509 - $selected_model = $current_options['model'] ?? 'gpt-5.1-chat-latest';
6510 -
6511 - // Verify it's an OpenAI model
6512 - if (!$this->is_openai_chat_model($selected_model)) {
6513 - //error_log("MXCHAT DEBUG ERROR: Vector Store search requires OpenAI model. Current: " . $selected_model);
6514 - $this->last_vectorstore_error = 'Vector Store search requires an OpenAI model. Current model: ' . $selected_model;
6515 - $this->current_valid_urls = [];
6516 - return '';
6517 - }
6518 -
6519 - // Use OpenAI Responses API with file_search tool
6520 - $request_body = array(
6521 - 'model' => $selected_model,
6522 - 'input' => $user_query,
6523 - 'tools' => array(
6524 - array(
6525 - 'type' => 'file_search',
6526 - 'vector_store_ids' => $vectorstore_ids,
6527 - 'max_num_results' => intval($max_results)
6528 - )
6529 - ),
6530 - 'include' => array('output[*].file_search_call.search_results')
6531 - );
6532 -
6533 - //error_log("MXCHAT VECTORSTORE: ========== REQUEST START ==========");
6534 - //error_log("MXCHAT VECTORSTORE: Model: " . $selected_model);
6535 - //error_log("MXCHAT VECTORSTORE: Query: " . substr($user_query, 0, 200));
6536 - //error_log("MXCHAT VECTORSTORE: Vector Store IDs: " . implode(', ', $vectorstore_ids));
6537 - //error_log("MXCHAT VECTORSTORE: Max Results: " . $max_results);
6538 - //error_log("MXCHAT VECTORSTORE: Request body: " . wp_json_encode($request_body));
6539 -
6540 - $response = wp_remote_post('https://api.openai.com/v1/responses', array(
6541 - 'headers' => array(
6542 - 'Authorization' => 'Bearer ' . $api_key,
6543 - 'Content-Type' => 'application/json'
6544 - ),
6545 - 'body' => wp_json_encode($request_body),
6546 - 'timeout' => 60
6547 - ));
6548 -
6549 - if (is_wp_error($response)) {
6550 - //error_log("MXCHAT VECTORSTORE ERROR: WP Error: " . $response->get_error_message());
6551 - $this->last_vectorstore_error = 'Vector Store API request failed: ' . $response->get_error_message();
6552 - $this->current_valid_urls = [];
6553 - return '';
6554 - }
6555 -
6556 - $response_code = wp_remote_retrieve_response_code($response);
6557 - //error_log("MXCHAT VECTORSTORE: Response code: " . $response_code);
6558 -
6559 - $response_body = wp_remote_retrieve_body($response);
6560 - //error_log("MXCHAT VECTORSTORE: Raw response (first 2000 chars): " . substr($response_body, 0, 2000));
6561 -
6562 - if ($response_code !== 200) {
6563 - //error_log("MXCHAT VECTORSTORE ERROR: API error response: " . $response_body);
6564 - $api_error_detail = '';
6565 - $decoded_error = json_decode($response_body, true);
6566 - if (isset($decoded_error['error']['message'])) {
6567 - $api_error_detail = $decoded_error['error']['message'];
6568 - }
6569 - $this->last_vectorstore_error = 'Vector Store API returned HTTP ' . $response_code . ($api_error_detail ? ': ' . $api_error_detail : '');
6570 - $this->current_valid_urls = [];
6571 - return '';
6572 - }
6573 - $result = json_decode($response_body, true);
6574 -
6575 - if (json_last_error() !== JSON_ERROR_NONE) {
6576 - //error_log("MXCHAT VECTORSTORE ERROR: JSON decode error: " . json_last_error_msg());
6577 - $this->last_vectorstore_error = 'Vector Store response could not be parsed: ' . json_last_error_msg();
6578 - $this->current_valid_urls = [];
6579 - return '';
6580 - }
6581 -
6582 - // Debug: Log the structure of the result
6583 - //error_log("MXCHAT VECTORSTORE: Result keys: " . implode(', ', array_keys($result)));
6584 - if (isset($result['output'])) {
6585 - //error_log("MXCHAT VECTORSTORE: Output count: " . count($result['output']));
6586 - foreach ($result['output'] as $idx => $out) {
6587 - //error_log("MXCHAT VECTORSTORE: Output[$idx] type: " . ($out['type'] ?? 'unknown'));
6588 - //error_log("MXCHAT VECTORSTORE: Output[$idx] keys: " . implode(', ', array_keys($out)));
6589 - }
6590 - } else {
6591 - //error_log("MXCHAT VECTORSTORE: No 'output' key in result!");
6592 - }
6593 -
6594 - // Extract file search results from the response
6595 - $content = '';
6596 - $matches_used = 0;
6597 - $all_matches = [];
6598 -
6599 - // The Responses API returns output array with tool results
6600 - if (isset($result['output']) && is_array($result['output'])) {
6601 - foreach ($result['output'] as $output_item) {
6602 - // Look for file_search_call results
6603 - if (isset($output_item['type']) && $output_item['type'] === 'file_search_call') {
6604 - //error_log("MXCHAT VECTORSTORE: Found file_search_call output item");
6605 - //error_log("MXCHAT VECTORSTORE: file_search_call keys: " . implode(', ', array_keys($output_item)));
6606 -
6607 - // Check for search_results in the output item directly
6608 - $search_results = $output_item['search_results'] ?? $output_item['results'] ?? [];
6609 - //error_log("MXCHAT VECTORSTORE: Search results count: " . count($search_results));
6610 -
6611 - if (empty($search_results)) {
6612 - //error_log("MXCHAT VECTORSTORE: No search results found in file_search_call");
6613 - //error_log("MXCHAT VECTORSTORE: file_search_call content: " . wp_json_encode($output_item));
6614 - }
6615 -
6616 - foreach ($search_results as $index => $search_result) {
6617 - $filename = $search_result['filename'] ?? '';
6618 - $score = $search_result['score'] ?? 0;
6619 - $text_content = '';
6620 -
6621 - // Extract text content from the result
6622 - // The text can be directly on the result OR nested under content array
6623 - if (isset($search_result['text']) && !empty($search_result['text'])) {
6624 - // Direct text field (OpenAI's actual format)
6625 - $text_content = $search_result['text'];
6626 - //error_log("MXCHAT VECTORSTORE: Found text directly on result[$index], length: " . strlen($text_content));
6627 - } elseif (isset($search_result['content']) && is_array($search_result['content'])) {
6628 - // Nested content array format
6629 - foreach ($search_result['content'] as $content_item) {
6630 - if (isset($content_item['text'])) {
6631 - $text_content .= $content_item['text'] . "\n";
6632 - }
6633 - }
6634 - //error_log("MXCHAT VECTORSTORE: Found text in content array for result[$index], length: " . strlen($text_content));
6635 - } else {
6636 - //error_log("MXCHAT VECTORSTORE: No text found for result[$index]. Keys: " . implode(', ', array_keys($search_result)));
6637 - }
6638 -
6639 - if (!empty($text_content)) {
6640 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6641 - $content .= trim($text_content) . "\n\n";
6642 -
6643 - if (!empty($filename)) {
6644 - $content .= "Source: " . $filename . "\n\n";
6645 - }
6646 -
6647 - // Extract URLs from content
6648 - preg_match_all(
6649 - '#\bhttps?://[^\s<>"\']+#i',
6650 - $text_content,
6651 - $content_urls
6652 - );
6653 - if (!empty($content_urls[0])) {
6654 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6655 - }
6656 -
6657 - $matches_used++;
6658 - }
6659 -
6660 - // Store for similarity analysis
6661 - $all_matches[] = [
6662 - 'document_id' => $filename ?: ('result_' . $index),
6663 - 'similarity' => $score,
6664 - 'similarity_percentage' => round($score * 100, 2),
6665 - 'above_threshold' => true,
6666 - 'source_display' => $filename,
6667 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6668 - 'used_for_context' => true,
6669 - 'role_restriction' => 'public',
6670 - 'has_access' => true,
6671 - 'filtered_out' => false
6672 - ];
6673 - }
3549 +
3550 + if (!empty($match['metadata']['text'])) {
3551 + $content .= "## Reference " . ($matches_used + 1) . " ##\n";
3552 + $content .= $match['metadata']['text'] . "\n\n";
3553 +
3554 + if (!empty($match['metadata']['source_url'])) {
3555 + $content .= "URL: " . $match['metadata']['source_url'] . "\n\n";
6674 3556 }
6675 -
6676 - // Also check for message content with annotations (citations)
6677 - if (isset($output_item['type']) && $output_item['type'] === 'message') {
6678 - if (isset($output_item['content']) && is_array($output_item['content'])) {
6679 - foreach ($output_item['content'] as $content_block) {
6680 - if (isset($content_block['annotations']) && is_array($content_block['annotations'])) {
6681 - foreach ($content_block['annotations'] as $annotation) {
6682 - if (isset($annotation['filename'])) {
6683 - $filename = $annotation['filename'];
6684 - $score = $annotation['score'] ?? 0;
6685 - $text_content = '';
6686 -
6687 - if (isset($annotation['content']) && is_array($annotation['content'])) {
6688 - foreach ($annotation['content'] as $ann_content) {
6689 - if (isset($ann_content['text'])) {
6690 - $text_content .= $ann_content['text'] . "\n";
6691 - }
6692 - }
6693 - }
6694 -
6695 - if (!empty($text_content) && $matches_used < $max_results) {
6696 - $content .= "## Reference " . ($matches_used + 1) . " ##\n";
6697 - $content .= trim($text_content) . "\n\n";
6698 - $content .= "Source: " . $filename . "\n\n";
6699 -
6700 - preg_match_all(
6701 - '#\bhttps?://[^\s<>"\']+#i',
6702 - $text_content,
6703 - $content_urls
6704 - );
6705 - if (!empty($content_urls[0])) {
6706 - $valid_urls = array_merge($valid_urls, $content_urls[0]);
6707 - }
6708 -
6709 - $matches_used++;
6710 -
6711 - $all_matches[] = [
6712 - 'document_id' => $filename,
6713 - 'similarity' => $score,
6714 - 'similarity_percentage' => round($score * 100, 2),
6715 - 'above_threshold' => true,
6716 - 'source_display' => $filename,
6717 - 'content_preview' => substr(strip_tags($text_content), 0, 100) . '...',
6718 - 'used_for_context' => true,
6719 - 'role_restriction' => 'public',
6720 - 'has_access' => true,
6721 - 'filtered_out' => false
6722 - ];
6723 - }
6724 - }
6725 - }
6726 - }
6727 - }
6728 - }
6729 - }
3557 +
3558 + $matches_used++;
6730 3559 }
6731 3560 }
6732 -
6733 - // Store for testing panel
6734 - $this->last_similarity_analysis['top_matches'] = $all_matches;
6735 - $this->last_similarity_analysis['total_checked'] = count($all_matches);
6736 -
6737 - // Store unique valid URLs for validation
6738 - $this->current_valid_urls = array_unique($valid_urls);
6739 -
6740 - // Allow add-ons to act on similarity results (e.g. WooCommerce product card display)
6741 - do_action('mxchat_similarity_results', $this->last_similarity_analysis['top_matches'], $bot_id);
6742 -
6743 - //error_log("MXCHAT VECTORSTORE: ========== SEARCH COMPLETE ==========");
6744 - //error_log("MXCHAT VECTORSTORE: Matches used: " . $matches_used);
6745 - //error_log("MXCHAT VECTORSTORE: All matches count: " . count($all_matches));
6746 - //error_log("MXCHAT VECTORSTORE: Content length: " . strlen($content));
6747 - if ($matches_used > 0) {
6748 - //error_log("MXCHAT VECTORSTORE: Content preview: " . substr($content, 0, 500));
6749 - }
6750 -
6751 - // Check if citation links are enabled
6752 - $citation_links_enabled = ($mxchat_options['citation_links_toggle'] ?? 'on') === 'on';
6753 -
3561 +
6754 3562 // Add response guidelines
6755 3563 if ($matches_used === 0) {
6756 - //error_log("MXCHAT VECTORSTORE: No matches found - returning empty reference message");
6757 3564 $content = "No reference information was found for this query.\n\n";
6758 3565 } else {
6759 - // Build response guidelines based on citation links setting
6760 - $content .= "\n## Response Guidelines ##\n" .
6761 - "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
6762 - "Be conversational and friendly, but never mention your knowledge base or training data. " .
6763 - "If you don't have specific information or are uncertain about any details, it's always " .
6764 - "better to honestly say you don't know rather than making up or guessing at answers. " .
6765 - "When information is incomplete, let them know you are unsure.\n\n";
6766 -
6767 - // Only add hyperlink instructions if citation links are enabled
6768 - if ($citation_links_enabled) {
6769 - $content .= "CRITICAL: When creating hyperlinks, always use proper markdown format with descriptive text: " .
6770 - "[descriptive text](url). NEVER use empty brackets like [](url). The text in brackets must describe what the link is about.";
6771 - } else {
6772 - $content .= "IMPORTANT: Do not include any citation links, source URLs, or hyperlinks in your responses. " .
6773 - "Simply provide helpful answers based on the reference information without citing sources.";
6774 - }
3566 + $content .= "\n## Response Guidelines ##\n" .
3567 + "You are an AI Chatbot. Answer naturally and helpfully using only the information from the references above. " .
3568 + "Be conversational and friendly, but never mention your knowledge base or training data. " .
3569 + "If you don't have specific information or are uncertain about any details, it's always " .
3570 + "better to honestly say you don't know rather than making up or guessing at answers. " .
3571 + "When information is incomplete, let them know you are unsure.";
6775 3572 }
6776 -
6777 - //error_log("MXCHAT DEBUG: Vector Store search complete. Matches used: " . $matches_used);
6778 -
3573 +
6779 3574 return trim($content);
6780 3575 }
6781 3576
6782 -/**
6783 - * Check if the given model is an OpenAI chat model
6784 - *
6785 - * @param string $model The model ID
6786 - * @return bool True if it's an OpenAI model
6787 - */
6788 -private function is_openai_chat_model($model) {
6789 - $openai_prefixes = array('gpt-', 'o1-', 'o3-');
6790 - foreach ($openai_prefixes as $prefix) {
6791 - if (strpos($model, $prefix) === 0) {
6792 - return true;
6793 - }
6794 - }
6795 - return false;
6796 -}
6797 -
6798 -/**
6799 - * Get bot-specific Vector Store configuration
6800 - *
6801 - * @param string $bot_id The bot ID
6802 - * @return array Configuration array
6803 - */
6804 -private function get_bot_vectorstore_config($bot_id = 'default') {
6805 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
6806 -
6807 - // Default global settings
6808 - $default_config = array(
6809 - 'use_vectorstore' => ($vectorstore_options['mxchat_use_openai_vectorstore'] ?? '0') === '1',
6810 - 'vectorstore_ids' => $vectorstore_options['mxchat_vectorstore_ids'] ?? '',
6811 - 'max_results' => $vectorstore_options['mxchat_vectorstore_max_results'] ?? 5
6812 - );
6813 -
6814 - // Allow multi-bot plugin to override with bot-specific settings
6815 - $bot_config = apply_filters('mxchat_get_bot_vectorstore_config', $default_config, $bot_id);
6816 -
6817 - // Preserve max_results from global settings if not set in bot config
6818 - if (!isset($bot_config['max_results'])) {
6819 - $bot_config['max_results'] = $default_config['max_results'];
6820 - }
6821 -
6822 - return $bot_config;
6823 -}
6824 -
6825 3577 private function mxchat_find_relevant_products($user_embedding) {
6826 3578 //error_log('MXChat Vector Search: Starting product search...');
6827 3579
6828 3580 // Retrieve the add-on settings from the database
@@ -6843,75 +3595,73 @@
6843 3595 }
6844 3596 private function find_relevant_products_wordpress($user_embedding) {
6845 3597 global $wpdb;
6846 3598 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
3599 + $cache_key = 'mxchat_system_prompt_embeddings';
3600 + $batch_size = 500;
6847 3601
6848 - if (!is_array($user_embedding)) {
6849 - return '';
6850 - }
3602 + // Original WordPress database search logic
3603 + // [Previous implementation remains the same]
3604 + $embeddings = wp_cache_get($cache_key, 'mxchat_system_prompts');
3605 + if ($embeddings === false) {
3606 + $embeddings = [];
3607 + $offset = 0;
6851 3608
6852 - // Streaming top-K pass: scan rows in small batches, keep only the top 3
6853 - // results above the similarity threshold. Peak memory is bounded by
6854 - // $batch_size embedding rows plus a 3-element top list.
6855 - $batch_size = 250;
6856 - $similarity_threshold = 0.85;
6857 - $top_k = 3;
6858 - $top_results = [];
6859 - $offset = 0;
3609 + do {
3610 + $query = $wpdb->prepare(
3611 + "SELECT id, embedding_vector
3612 + FROM {$system_prompt_table}
3613 + LIMIT %d OFFSET %d",
3614 + $batch_size,
3615 + $offset
3616 + );
6860 3617
6861 - do {
6862 - $batch = $wpdb->get_results($wpdb->prepare(
6863 - "SELECT id, embedding_vector
6864 - FROM {$system_prompt_table}
6865 - LIMIT %d OFFSET %d",
6866 - $batch_size,
6867 - $offset
6868 - ));
3618 + $batch = $wpdb->get_results($query);
3619 + if (empty($batch)) {
3620 + break;
3621 + }
6869 3622
6870 - if (empty($batch)) {
6871 - break;
6872 - }
3623 + $embeddings = array_merge($embeddings, $batch);
3624 + $offset += $batch_size;
6873 3625
6874 - foreach ($batch as $row) {
6875 - $database_embedding = $row->embedding_vector
6876 - ? unserialize($row->embedding_vector, ['allowed_classes' => false])
6877 - : null;
3626 + unset($batch);
6878 3627
6879 - if (!is_array($database_embedding)) {
6880 - unset($database_embedding);
6881 - continue;
6882 - }
3628 + } while (true);
6883 3629
3630 + if (empty($embeddings)) {
3631 + return '';
3632 + }
3633 + wp_cache_set($cache_key, $embeddings, 'mxchat_system_prompts', 3600);
3634 + }
3635 +
3636 + $relevant_results = [];
3637 + foreach ($embeddings as $embedding) {
3638 + $database_embedding = $embedding->embedding_vector
3639 + ? unserialize($embedding->embedding_vector, ['allowed_classes' => false])
3640 + : null;
3641 + if (is_array($database_embedding) && is_array($user_embedding)) {
6884 3642 $similarity = $this->mxchat_calculate_cosine_similarity($user_embedding, $database_embedding);
6885 - unset($database_embedding);
6886 -
6887 - if ($similarity < $similarity_threshold) {
6888 - continue;
6889 - }
6890 -
6891 - // Insert into bounded top-K (kept sorted descending)
6892 - if (count($top_results) < $top_k) {
6893 - $top_results[] = ['id' => $row->id, 'similarity' => $similarity];
6894 - usort($top_results, function ($a, $b) {
6895 - return $b['similarity'] <=> $a['similarity'];
6896 - });
6897 - } elseif ($similarity > $top_results[$top_k - 1]['similarity']) {
6898 - $top_results[$top_k - 1] = ['id' => $row->id, 'similarity' => $similarity];
6899 - usort($top_results, function ($a, $b) {
6900 - return $b['similarity'] <=> $a['similarity'];
6901 - });
6902 - }
3643 + $relevant_results[] = [
3644 + 'id' => $embedding->id,
3645 + 'similarity' => $similarity
3646 + ];
6903 3647 }
3648 + unset($database_embedding);
3649 + }
6904 3650
6905 - unset($batch);
6906 - $offset += $batch_size;
6907 - } while (true);
3651 + // Use fixed threshold for products
3652 + $similarity_threshold = 0.85;
6908 3653
6909 - if (empty($top_results)) {
6910 - return '';
6911 - }
3654 + $relevant_results = array_filter($relevant_results, function ($result) use ($similarity_threshold) {
3655 + return $result['similarity'] >= $similarity_threshold;
3656 + });
3657 + usort($relevant_results, function ($a, $b) {
3658 + return $b['similarity'] <=> $a['similarity'];
3659 + });
6912 3660
3661 + $top_results = array_slice($relevant_results, 0, 5);
6913 3662 $content = '';
3663 +
6914 3664 foreach ($top_results as $result) {
6915 3665 $chunk_content = $this->fetch_content_with_product_links($result['id']);
6916 3666 $content .= $chunk_content . "\n\n";
6917 3667 }
@@ -6917,10 +3667,8 @@
6917 3667 }
6918 3668
6919 3669 return trim($content);
6920 3670 }
6921 -
6922 -
6923 3671 private function find_relevant_products_pinecone($user_embedding) {
6924 3672 //error_log('Starting Pinecone product search...');
6925 3673
6926 3674 $options = get_option('mxchat_pinecone_addon_options', array());
@@ -6995,10 +3743,8 @@
6995 3743 }
6996 3744
6997 3745 return trim($content);
6998 3746 }
6999 -
7000 -
7001 3747 private function fetch_content_with_product_links($most_relevant_id) {
7002 3748 global $wpdb;
7003 3749 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
7004 3750
@@ -7018,581 +3764,14 @@
7018 3764 return null;
7019 3765 }
7020 3766
7021 3767 /**
7022 - * Get system instructions for a specific bot or default
7023 - * Checks for multi-bot add-on and uses bot-specific instructions if available
7024 - * Automatically strips URLs if citation links are disabled
7025 - * Replaces {visitor_name} placeholder with actual visitor name if available
7026 - *
7027 - * @param string $bot_id The bot ID to get instructions for
7028 - * @param string $session_id Optional session ID to lookup visitor name
3768 + * Modified streaming functions to include testing data
7029 3769 */
7030 -private function get_system_instructions($bot_id = 'default', $session_id = '') {
7031 - $instructions = '';
7032 3770
7033 - // Check if multi-bot add-on is active
7034 - if (class_exists('MxChat_Multi_Bot_Core_Manager') && $bot_id !== 'default') {
7035 - // Get bot-specific options from multi-bot add-on
7036 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
7037 -
7038 - // If bot has custom system instructions, use those
7039 - if (!empty($bot_options['system_prompt_instructions'])) {
7040 - $instructions = $bot_options['system_prompt_instructions'];
7041 - }
7042 - }
7043 -
7044 - // Fall back to default system instructions
7045 - if (empty($instructions)) {
7046 - $instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
7047 - }
7048 -
7049 - // Check if citation links are disabled - if so, strip URLs from instructions
7050 - $fresh_options = get_option('mxchat_options', []);
7051 - $citation_links_enabled = isset($fresh_options['citation_links_toggle']) ? ($fresh_options['citation_links_toggle'] === 'on') : true;
7052 -
7053 - if (!$citation_links_enabled && !empty($instructions)) {
7054 - $instructions = preg_replace('#\bhttps?://[^\s<>"\']+#i', '', $instructions);
7055 - $instructions = preg_replace('/\s+/', ' ', trim($instructions)); // Clean up extra spaces
7056 - }
7057 -
7058 - // Replace {visitor_name} placeholder with actual visitor name if available
7059 - if (!empty($instructions) && !empty($session_id) && stripos($instructions, '{visitor_name}') !== false) {
7060 - $name_option_key = "mxchat_name_{$session_id}";
7061 - $visitor_name = get_option($name_option_key, '');
7062 -
7063 - if (!empty($visitor_name)) {
7064 - $instructions = str_ireplace('{visitor_name}', sanitize_text_field($visitor_name), $instructions);
7065 - } else {
7066 - // Remove placeholder if no name is available
7067 - $instructions = str_ireplace('{visitor_name}', '', $instructions);
7068 - $instructions = preg_replace('/\s{2,}/', ' ', trim($instructions)); // Clean up extra spaces
7069 - }
7070 - }
7071 -
7072 - // Allow developers to filter system instructions and process shortcodes
7073 - $instructions = apply_filters('mxchat_system_instructions', $instructions, $bot_id, $session_id);
7074 - $instructions = do_shortcode($instructions);
7075 -
7076 - return $instructions;
7077 -}
7078 -/**
7079 - * Get the current bot ID from session or request context
7080 - */
7081 -private function get_current_bot_id($session_id = '') {
7082 - // First, check if bot_id is passed in the current request
7083 - if (isset($_POST['bot_id']) && !empty($_POST['bot_id'])) {
7084 - return sanitize_key($_POST['bot_id']);
7085 - }
7086 -
7087 - // If not in POST, try to get it from session data
7088 - if (!empty($session_id)) {
7089 - $bot_id = get_option("mxchat_session_bot_{$session_id}", '');
7090 - if (!empty($bot_id)) {
7091 - return $bot_id;
7092 - }
7093 - }
7094 -
7095 - // Fall back to default
7096 - return 'default';
7097 -}
7098 -/* ====================================================================== *
7099 - * Native function-calling loop (plan-mxchat-20260617-a41dee)
7100 - *
7101 - * Model-driven tool use. The model is offered MxChat's enabled callbacks as
7102 - * tools (sourced from MxChat_Tool_Registry, the single source the admin AI
7103 - * Tools checklist also reads). When the model calls a tool, the matching
7104 - * callback runs through its EXISTING permission checks, its output is fed
7105 - * back, and the loop continues up to a depth cap. INDEPENDENT of the
7106 - * intent→callback router — it runs only after intents miss, and works with
7107 - * ZERO Actions created.
7108 - *
7109 - * Entered ONLY when: function calling is enabled + the active model is
7110 - * tool-capable + at least one tool is enabled. Default-off, so existing
7111 - * installs never enter this branch (byte-for-byte unchanged behavior). The
7112 - * tool round is buffered (non-streaming) per the plan; the final answer is
7113 - * emitted via the same SSE/JSON envelopes the normal path uses.
7114 - * ====================================================================== */
7115 -
7116 -/** Gate: should the function-calling loop handle this turn? */
7117 -private function mxchat_fc_should_run($selected_model) {
7118 - if (!class_exists('MxChat_Tool_Registry') || !MxChat_Tool_Registry::is_enabled()) {
7119 - return false;
7120 - }
7121 - if (class_exists('MxChat_Model_Catalog') && !MxChat_Model_Catalog::supports_tools($selected_model)) {
7122 - return false;
7123 - }
7124 - $tools = MxChat_Tool_Registry::enabled_tools();
7125 - return !empty($tools);
7126 -}
7127 -
7128 -private function mxchat_fc_log($msg) {
7129 - if (defined('MXCHAT_DEV_MODE') && MXCHAT_DEV_MODE) {
7130 - error_log('[MxChat FC] ' . $msg);
7131 - }
7132 -}
7133 -
7134 -/**
7135 - * Resolve provider transport details. Returns null when FC can't run for this
7136 - * model/config (missing key, unsupported provider) so the caller falls back to
7137 - * the normal path. OpenAI/xAI/DeepSeek/OpenRouter/Custom share the
7138 - * OpenAI-compatible 'openai' family; Claude and Gemini are distinct.
7139 - */
7140 -private function mxchat_fc_resolve_provider($selected_model, $opts) {
7141 - // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
7142 - // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
7143 - if ($selected_model === 'claude-opus-4-20250514') { $selected_model = 'claude-opus-4-8'; }
7144 - elseif ($selected_model === 'claude-sonnet-4-20250514') { $selected_model = 'claude-sonnet-4-6'; }
7145 - if ($selected_model === 'openrouter') {
7146 - $model = isset($opts['openrouter_selected_model']) ? $opts['openrouter_selected_model'] : '';
7147 - $key = isset($opts['openrouter_api_key']) ? $opts['openrouter_api_key'] : '';
7148 - if ($model === '' || $key === '') return null;
7149 - return array('family'=>'openai','model'=>$model,'url'=>'https://openrouter.ai/api/v1/chat/completions',
7150 - 'headers'=>array('Content-Type'=>'application/json','Authorization'=>'Bearer '.$key),'tag'=>'openai');
7151 - }
7152 - $prefix = strtolower(explode('-', $selected_model)[0]);
7153 - switch ($prefix) {
7154 - case 'gpt': case 'o1': case 'o3': case 'o4':
7155 - $key = isset($opts['api_key']) ? $opts['api_key'] : '';
7156 - if ($key === '') return null;
7157 - return array('family'=>'openai','model'=>$selected_model,'url'=>'https://api.openai.com/v1/chat/completions',
7158 - 'headers'=>array('Content-Type'=>'application/json','Authorization'=>'Bearer '.$key),'tag'=>'openai');
7159 - case 'claude':
7160 - $key = isset($opts['claude_api_key']) ? $opts['claude_api_key'] : '';
7161 - if ($key === '') return null;
7162 - return array('family'=>'anthropic','model'=>$selected_model,'url'=>'https://api.anthropic.com/v1/messages',
7163 - 'headers'=>array('Content-Type'=>'application/json','x-api-key'=>$key,'anthropic-version'=>'2023-06-01'),'tag'=>'anthropic');
7164 - case 'gemini':
7165 - $key = isset($opts['gemini_api_key']) ? $opts['gemini_api_key'] : '';
7166 - if ($key === '') return null;
7167 - return array('family'=>'gemini','model'=>$selected_model,'key'=>$key,'tag'=>'gemini');
7168 - case 'grok': case 'xai':
7169 - $key = isset($opts['xai_api_key']) ? $opts['xai_api_key'] : '';
7170 - if ($key === '') return null;
7171 - return array('family'=>'openai','model'=>$selected_model,'url'=>'https://api.x.ai/v1/chat/completions',
7172 - 'headers'=>array('Content-Type'=>'application/json','Authorization'=>'Bearer '.$key),'tag'=>'xai');
7173 - case 'deepseek':
7174 - $key = isset($opts['deepseek_api_key']) ? $opts['deepseek_api_key'] : '';
7175 - if ($key === '') return null;
7176 - return array('family'=>'openai','model'=>$selected_model,'url'=>'https://api.deepseek.com/v1/chat/completions',
7177 - 'headers'=>array('Content-Type'=>'application/json','Authorization'=>'Bearer '.$key),'tag'=>'openai');
7178 - case 'custom':
7179 - $base = isset($opts['custom_provider_base_url']) ? rtrim($opts['custom_provider_base_url'], '/') : '';
7180 - $key = isset($opts['custom_provider_api_key']) ? $opts['custom_provider_api_key'] : '';
7181 - $model = isset($opts['custom_provider_model']) ? $opts['custom_provider_model'] : '';
7182 - if ($base === '' || $model === '') return null;
7183 - $url = (strpos($base, 'chat/completions') !== false) ? $base : $base . '/chat/completions';
7184 - $headers = array('Content-Type'=>'application/json');
7185 - if ($key !== '') $headers['Authorization'] = 'Bearer '.$key;
7186 - return array('family'=>'openai','model'=>$model,'url'=>$url,'headers'=>$headers,'tag'=>'openai');
7187 - }
7188 - return null;
7189 -}
7190 -
7191 -/**
7192 - * Top-level function-calling attempt. Returns:
7193 - * ['handled'=>true, 'text'=>'<final answer>'] when the model used ≥1 tool
7194 - * ['handled'=>false] otherwise (caller falls back
7195 - * to the normal streamed path)
7196 - */
7197 -private function mxchat_fc_attempt($message, $relevant_content, $conversation_history, $selected_model, $opts, $session_id, $user_id) {
7198 - $prov = $this->mxchat_fc_resolve_provider($selected_model, $opts);
7199 - if (!$prov) {
7200 - return array('handled' => false);
7201 - }
7202 - $tools = MxChat_Tool_Registry::enabled_tools();
7203 - if (empty($tools)) {
7204 - return array('handled' => false);
7205 - }
7206 -
7207 - $bot_id = $this->get_current_bot_id($session_id);
7208 - $system = $this->get_system_instructions($bot_id, $session_id);
7209 -
7210 - // Force callbacks into return-mode (some echo SSE directly when streaming);
7211 - // we buffer the whole tool round, then emit once. Restored in finally.
7212 - $prev_streaming = $this->is_streaming;
7213 - $this->is_streaming = false;
3771 +// 1. Update the main handler to pass testing data to streaming functions
3772 +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) {
7214 3773 try {
7215 - if ($prov['family'] === 'anthropic') {
7216 - return $this->mxchat_fc_loop_anthropic($prov, $system, $relevant_content, $conversation_history, $tools, $message, $user_id, $session_id);
7217 - } elseif ($prov['family'] === 'gemini') {
7218 - return $this->mxchat_fc_loop_gemini($prov, $system, $relevant_content, $conversation_history, $tools, $message, $user_id, $session_id);
7219 - }
7220 - return $this->mxchat_fc_loop_openai($prov, $system, $relevant_content, $conversation_history, $tools, $message, $user_id, $session_id);
7221 - } catch (\Throwable $e) {
7222 - $this->mxchat_fc_log('attempt threw: ' . $e->getMessage());
7223 - return array('handled' => false);
7224 - } finally {
7225 - $this->is_streaming = $prev_streaming;
7226 - }
7227 -}
7228 -
7229 -/** Normalize MxChat history rows to [{role:user|assistant, content}]. */
7230 -private function mxchat_fc_normalize_history($conversation_history) {
7231 - $out = array();
7232 - if (!is_array($conversation_history)) return $out;
7233 - foreach ($conversation_history as $m) {
7234 - if (!is_array($m) || !isset($m['role']) || !isset($m['content'])) continue;
7235 - $role = $m['role'];
7236 - if ($role === 'bot' || $role === 'agent') $role = 'assistant';
7237 - if (!in_array($role, array('user', 'assistant'), true)) $role = 'user';
7238 - $out[] = array('role' => $role, 'content' => (string) $m['content']);
7239 - }
7240 - return $out;
7241 -}
7242 -
7243 -/** Execute the matched callback for a tool call. Returns ['ok'=>bool,'content'=>string]. */
7244 -private function mxchat_fc_execute_tool($tool_name, $args, $orig_message, $user_id, $session_id) {
7245 - $tool = MxChat_Tool_Registry::tool_by_name($tool_name, true); // enabled-only
7246 - if (!$tool) {
7247 - return array('ok' => false, 'content' => 'This tool is not available or not enabled.');
7248 - }
7249 - $fn = $tool['callback'];
7250 -
7251 - // MxChat callbacks are message-driven: hand them the model's `query`
7252 - // (falling back to the original user message).
7253 - $query = '';
7254 - if (is_array($args) && isset($args['query']) && is_string($args['query'])) {
7255 - $query = $args['query'];
7256 - }
7257 - if ($query === '') $query = $orig_message;
7258 -
7259 - // Synthetic intent row (matches wp_mxchat_intents columns → no undefined-prop warnings).
7260 - $synthetic_intent = (object) array(
7261 - 'id' => 0, 'intent_label' => $tool['label'], 'phrases' => '',
7262 - 'embedding_vector' => '', 'callback_function' => $fn,
7263 - 'similarity_threshold' => 0.0, 'enabled' => 1, 'enabled_bots' => null,
7264 - );
7265 -
7266 - try {
7267 - if (!empty($tool['is_addon'])) {
7268 - $result = apply_filters($fn, false, $query, $user_id, $session_id, $synthetic_intent);
7269 - } elseif (method_exists($this, $fn)) {
7270 - $result = call_user_func(array($this, $fn), $query, $user_id, $session_id, $synthetic_intent, null);
7271 - } else {
7272 - return array('ok' => false, 'content' => 'Tool implementation not found.');
7273 - }
7274 - } catch (\Throwable $e) {
7275 - $this->mxchat_fc_log("tool {$fn} threw: " . $e->getMessage());
7276 - return array('ok' => false, 'content' => 'The tool failed to run.');
7277 - }
7278 -
7279 - // plan-mxchat-20260617-48a57a — surface UI-bearing tool output.
7280 - // If the callback produced a UI element (generated image, product card, image
7281 - // gallery), its html MUST reach the FRONTEND as a real rendered bot message —
7282 - // NOT be stripped to text and handed to the model to paraphrase (that was the
7283 - // bug: under function calling, UI-bearing actions rendered nothing). Capture
7284 - // the html here; the FC outcome handler emits it in the response envelope.
7285 - $ui = $this->mxchat_fc_ui_payload_from($result);
7286 - if ($ui['html'] !== '' || !empty($ui['images'])) {
7287 - if ($ui['html'] !== '') {
7288 - $this->fc_ui_html .= ($this->fc_ui_html !== '' ? "\n" : '') . $ui['html'];
7289 - }
7290 - if (!empty($ui['images']) && is_array($ui['images'])) {
7291 - $this->fc_ui_images = array_merge($this->fc_ui_images, $ui['images']);
7292 - }
7293 - $this->fc_ui_captured = true;
7294 -
7295 - // Persist the html to the transcript ONLY if the callback did not already
7296 - // do so itself. Core image/search callbacks self-save (text + html);
7297 - // add-on callbacks (e.g. woo product cards) return html for the caller to
7298 - // save. ui_self_saves carries this from the registry; default by source
7299 - // (core self-saves, add-on does not) when a tool predates the flag.
7300 - $self_saves = array_key_exists('ui_self_saves', $tool)
7301 - ? !empty($tool['ui_self_saves'])
7302 - : empty($tool['is_addon']);
7303 - if ($ui['html'] !== '' && !$self_saves) {
7304 - $this->mxchat_save_chat_message($session_id, 'bot', $ui['html']);
7305 - }
7306 -
7307 - // Hand the MODEL a short acknowledgment (never the raw or stripped html)
7308 - // so the loop can add a one-line caption without trying to re-describe a
7309 - // visual it cannot see and without duplicating the displayed element.
7310 - $summary = isset($ui['text']) ? trim((string) $ui['text']) : '';
7311 - $ack = __('[A visual result has already been shown to the user in the chat. Do not repeat or describe it in detail — reply with at most a brief one-line caption.]', 'mxchat');
7312 - $content = $summary !== '' ? ($ack . ' ' . $summary) : $ack;
7313 - $this->mxchat_fc_log("executed {$fn} → [ui payload surfaced] " . substr($content, 0, 120));
7314 - return array('ok' => true, 'content' => $content);
7315 - }
7316 -
7317 - $content = $this->mxchat_fc_stringify_result($result);
7318 - $this->mxchat_fc_log("executed {$fn} → " . substr($content, 0, 160));
7319 - return array('ok' => true, 'content' => $content);
7320 -}
7321 -
7322 -/**
7323 - * Extract a UI payload (html + images + text) from a tool callback's return,
7324 - * falling back to $this->fallbackResponse for callbacks that return true after
7325 - * setting it. plan-mxchat-20260617-48a57a.
7326 - *
7327 - * @return array{html:string,images:array,text:string}
7328 - */
7329 -private function mxchat_fc_ui_payload_from($result) {
7330 - $src = null;
7331 - if (is_array($result)) {
7332 - $src = $result;
7333 - } elseif ($result === true && isset($this->fallbackResponse) && is_array($this->fallbackResponse)) {
7334 - $src = $this->fallbackResponse;
7335 - }
7336 - $html = (is_array($src) && isset($src['html']) && is_string($src['html'])) ? $src['html'] : '';
7337 - $images = (is_array($src) && isset($src['images']) && is_array($src['images'])) ? $src['images'] : array();
7338 - $text = (is_array($src) && isset($src['text'])) ? (string) $src['text'] : '';
7339 - return array('html' => $html, 'images' => $images, 'text' => $text);
7340 -}
7341 -
7342 -/** Coerce a callback's return (string|array|true|false) into a tool-result string. */
7343 -private function mxchat_fc_stringify_result($result) {
7344 - if (is_string($result)) {
7345 - return $result === '' ? 'No result.' : $result;
7346 - }
7347 - if ($result === true) {
7348 - // Callbacks that set fallbackResponse and return true.
7349 - $fb = isset($this->fallbackResponse) ? $this->fallbackResponse : null;
7350 - if (is_array($fb)) {
7351 - if (!empty($fb['text'])) return (string) $fb['text'];
7352 - if (!empty($fb['html'])) return wp_strip_all_tags((string) $fb['html']);
7353 - }
7354 - return 'Done.';
7355 - }
7356 - if ($result === false || $result === null) {
7357 - return 'No result.';
7358 - }
7359 - if (is_array($result)) {
7360 - if (isset($result['text']) && $result['text'] !== '') return (string) $result['text'];
7361 - if (isset($result['html']) && $result['html'] !== '') return wp_strip_all_tags((string) $result['html']);
7362 - $json = wp_json_encode($result);
7363 - return $json !== false ? $json : 'No result.';
7364 - }
7365 - return (string) $result;
7366 -}
7367 -
7368 -/** HTTP code + decoded body for a function-calling request. */
7369 -private function mxchat_fc_post($url, $body, $headers, $tag) {
7370 - $args = array(
7371 - 'body' => wp_json_encode($body),
7372 - 'headers' => $headers,
7373 - 'timeout' => 60,
7374 - 'redirection' => 5,
7375 - 'blocking' => true,
7376 - 'httpversion' => '1.0',
7377 - 'sslverify' => true,
7378 - );
7379 - $response = $this->mxchat_provider_call_with_retry($url, $args, $tag);
7380 - if (is_wp_error($response)) {
7381 - return array('code' => 0, 'data' => null, 'error' => $response->get_error_message());
7382 - }
7383 - $code = (int) wp_remote_retrieve_response_code($response);
7384 - $data = json_decode(wp_remote_retrieve_body($response), true);
7385 - return array('code' => $code, 'data' => $data, 'error' => null);
7386 -}
7387 -
7388 -/* ---------------- OpenAI-compatible loop (OpenAI/xAI/DeepSeek/OpenRouter/Custom) -------------- */
7389 -private function mxchat_fc_loop_openai($prov, $system, $relevant_content, $conversation_history, $tools, $orig_message, $user_id, $session_id) {
7390 - $messages = array();
7391 - $messages[] = array('role' => 'system', 'content' => $system . ' ' . $relevant_content);
7392 - foreach ($this->mxchat_fc_normalize_history($conversation_history) as $m) {
7393 - $messages[] = $m;
7394 - }
7395 -
7396 - $depth = MxChat_Tool_Registry::max_depth();
7397 - $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
7398 - $tool_schema = MxChat_Tool_Registry::to_openai_tools($tools);
7399 - $used_tool = false;
7400 - $calls_made = 0;
7401 -
7402 - for ($step = 0; $step <= $depth; $step++) {
7403 - $offer_tools = ($step < $depth) && !empty($tool_schema);
7404 - $body = array('model' => $prov['model'], 'messages' => $messages, 'temperature' => 1, 'stream' => false);
7405 - if ($offer_tools) {
7406 - $body['tools'] = $tool_schema;
7407 - $body['tool_choice'] = 'auto';
7408 - }
7409 - $r = $this->mxchat_fc_post($prov['url'], $body, $prov['headers'], $prov['tag']);
7410 - if ($r['code'] !== 200 || !is_array($r['data'])) {
7411 - $this->mxchat_fc_log('openai call failed: code=' . $r['code'] . ' err=' . ($r['error'] ?? ''));
7412 - return $used_tool ? array('handled' => true, 'text' => $this->mxchat_fc_giveup_text()) : array('handled' => false);
7413 - }
7414 - $msg = isset($r['data']['choices'][0]['message']) ? $r['data']['choices'][0]['message'] : null;
7415 - if (!$msg) {
7416 - return $used_tool ? array('handled' => true, 'text' => $this->mxchat_fc_giveup_text()) : array('handled' => false);
7417 - }
7418 - $tool_calls = isset($msg['tool_calls']) && is_array($msg['tool_calls']) ? $msg['tool_calls'] : array();
7419 - if (empty($tool_calls)) {
7420 - $text = isset($msg['content']) ? trim((string) $msg['content']) : '';
7421 - if (!$used_tool) return array('handled' => false); // model never used a tool → normal path
7422 - return array('handled' => true, 'text' => ($text !== '' ? $text : $this->mxchat_fc_giveup_text()));
7423 - }
7424 - // Append the assistant tool-call turn verbatim, then a tool result per call.
7425 - $used_tool = true;
7426 - $messages[] = $msg;
7427 - foreach ($tool_calls as $tc) {
7428 - if ($calls_made >= $budget) break;
7429 - $calls_made++;
7430 - $name = isset($tc['function']['name']) ? $tc['function']['name'] : '';
7431 - $args = array();
7432 - if (isset($tc['function']['arguments'])) {
7433 - $decoded = json_decode($tc['function']['arguments'], true);
7434 - if (is_array($decoded)) $args = $decoded;
7435 - }
7436 - $exec = $this->mxchat_fc_execute_tool($name, $args, $orig_message, $user_id, $session_id);
7437 - $messages[] = array(
7438 - 'role' => 'tool',
7439 - 'tool_call_id' => isset($tc['id']) ? $tc['id'] : '',
7440 - 'content' => $exec['content'],
7441 - );
7442 - }
7443 - }
7444 - return $used_tool ? array('handled' => true, 'text' => $this->mxchat_fc_giveup_text()) : array('handled' => false);
7445 -}
7446 -
7447 -/* ---------------- Anthropic Claude loop ---------------- */
7448 -private function mxchat_fc_loop_anthropic($prov, $system, $relevant_content, $conversation_history, $tools, $orig_message, $user_id, $session_id) {
7449 - $messages = $this->mxchat_fc_normalize_history($conversation_history);
7450 - $messages[] = array('role' => 'user', 'content' => $relevant_content);
7451 -
7452 - $depth = MxChat_Tool_Registry::max_depth();
7453 - $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
7454 - $tool_schema = MxChat_Tool_Registry::to_anthropic_tools($tools);
7455 - $omit_temp = $this->mxchat_claude_omits_temperature($prov['model']);
7456 - $used_tool = false;
7457 - $calls_made = 0;
7458 -
7459 - for ($step = 0; $step <= $depth; $step++) {
7460 - $offer_tools = ($step < $depth) && !empty($tool_schema);
7461 - $body = array('model' => $prov['model'], 'max_tokens' => 1024, 'temperature' => 0.8,
7462 - 'messages' => $messages, 'system' => $system);
7463 - if ($omit_temp) unset($body['temperature']);
7464 - if ($offer_tools) {
7465 - $body['tools'] = $tool_schema;
7466 - $body['tool_choice'] = array('type' => 'auto');
7467 - }
7468 - $r = $this->mxchat_fc_post($prov['url'], $body, $prov['headers'], $prov['tag']);
7469 - if ($r['code'] !== 200 || !is_array($r['data'])) {
7470 - $this->mxchat_fc_log('anthropic call failed: code=' . $r['code'] . ' err=' . ($r['error'] ?? ''));
7471 - return $used_tool ? array('handled' => true, 'text' => $this->mxchat_fc_giveup_text()) : array('handled' => false);
7472 - }
7473 - $content = isset($r['data']['content']) && is_array($r['data']['content']) ? $r['data']['content'] : array();
7474 - $tool_uses = array();
7475 - $text_out = '';
7476 - foreach ($content as $block) {
7477 - if (!isset($block['type'])) continue;
7478 - if ($block['type'] === 'tool_use') {
7479 - $tool_uses[] = $block;
7480 - } elseif ($block['type'] === 'text' && isset($block['text'])) {
7481 - $text_out .= $block['text'];
7482 - }
7483 - }
7484 - if (empty($tool_uses)) {
7485 - if (!$used_tool) return array('handled' => false);
7486 - $text_out = trim($text_out);
7487 - return array('handled' => true, 'text' => ($text_out !== '' ? $text_out : $this->mxchat_fc_giveup_text()));
7488 - }
7489 - // Append the assistant turn (the full content array), then a user turn of tool_result blocks.
7490 - $used_tool = true;
7491 - $messages[] = array('role' => 'assistant', 'content' => $content);
7492 - $results = array();
7493 - foreach ($tool_uses as $tu) {
7494 - if ($calls_made >= $budget) break;
7495 - $calls_made++;
7496 - $name = isset($tu['name']) ? $tu['name'] : '';
7497 - $args = isset($tu['input']) && is_array($tu['input']) ? $tu['input'] : array();
7498 - $exec = $this->mxchat_fc_execute_tool($name, $args, $orig_message, $user_id, $session_id);
7499 - $results[] = array(
7500 - 'type' => 'tool_result',
7501 - 'tool_use_id' => isset($tu['id']) ? $tu['id'] : '',
7502 - 'content' => $exec['content'],
7503 - );
7504 - }
7505 - $messages[] = array('role' => 'user', 'content' => $results);
7506 - }
7507 - return $used_tool ? array('handled' => true, 'text' => $this->mxchat_fc_giveup_text()) : array('handled' => false);
7508 -}
7509 -
7510 -/* ---------------- Google Gemini loop ---------------- */
7511 -private function mxchat_fc_loop_gemini($prov, $system, $relevant_content, $conversation_history, $tools, $orig_message, $user_id, $session_id) {
7512 - $contents = array();
7513 - $contents[] = array('role' => 'user', 'parts' => array(array('text' => '[System Instructions] ' . $system . ' ' . $relevant_content)));
7514 - $contents[] = array('role' => 'model', 'parts' => array(array('text' => 'I understand and will follow these instructions.')));
7515 - foreach ($this->mxchat_fc_normalize_history($conversation_history) as $m) {
7516 - $contents[] = array('role' => ($m['role'] === 'assistant' ? 'model' : 'user'),
7517 - 'parts' => array(array('text' => $m['content'])));
7518 - }
7519 -
7520 - $depth = MxChat_Tool_Registry::max_depth();
7521 - $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
7522 - $tool_schema = MxChat_Tool_Registry::to_gemini_tools($tools);
7523 - // Function calling (tools + functionDeclarations + toolConfig) is a v1beta feature on the
7524 - // Generative Language REST API. The v1 endpoint silently ignores the tools array, so a
7525 - // non-preview model (e.g. gemini-2.5-pro, gemini-3.5-flash, gemini-3.1-flash-lite) would
7526 - // just answer in text and never emit a tool call. Always use v1beta for the FC loop —
7527 - // confirmed against Google's function-calling docs (their REST example targets
7528 - // v1beta/models/gemini-3.5-flash:generateContent). v1beta is a superset, so every model
7529 - // reachable on v1 is also reachable here.
7530 - $api_version = 'v1beta';
7531 - $url = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $prov['model'] . ':generateContent?key=' . $prov['key'];
7532 - $headers = array('Content-Type' => 'application/json');
7533 - $used_tool = false;
7534 - $calls_made = 0;
7535 -
7536 - for ($step = 0; $step <= $depth; $step++) {
7537 - $offer_tools = ($step < $depth) && !empty($tool_schema);
7538 - $body = array(
7539 - 'contents' => $contents,
7540 - 'generationConfig' => array('temperature' => 0.7, 'topP' => 0.95, 'topK' => 40, 'maxOutputTokens' => 8192),
7541 - );
7542 - if ($offer_tools) {
7543 - $body['tools'] = $tool_schema;
7544 - $body['toolConfig'] = array('functionCallingConfig' => array('mode' => 'AUTO'));
7545 - }
7546 - $r = $this->mxchat_fc_post($url, $body, $headers, 'gemini');
7547 - if ($r['code'] !== 200 || !is_array($r['data']) || isset($r['data']['error'])) {
7548 - $this->mxchat_fc_log('gemini call failed: code=' . $r['code'] . ' err=' . ($r['error'] ?? ''));
7549 - return $used_tool ? array('handled' => true, 'text' => $this->mxchat_fc_giveup_text()) : array('handled' => false);
7550 - }
7551 - $parts = isset($r['data']['candidates'][0]['content']['parts']) && is_array($r['data']['candidates'][0]['content']['parts'])
7552 - ? $r['data']['candidates'][0]['content']['parts'] : array();
7553 - $fn_calls = array();
7554 - $text_out = '';
7555 - foreach ($parts as $p) {
7556 - if (isset($p['functionCall'])) {
7557 - $fn_calls[] = $p['functionCall'];
7558 - } elseif (isset($p['text'])) {
7559 - $text_out .= $p['text'];
7560 - }
7561 - }
7562 - if (empty($fn_calls)) {
7563 - if (!$used_tool) return array('handled' => false);
7564 - $text_out = trim($text_out);
7565 - return array('handled' => true, 'text' => ($text_out !== '' ? $text_out : $this->mxchat_fc_giveup_text()));
7566 - }
7567 - // Append the model turn (its parts) then a user turn of functionResponse parts.
7568 - $used_tool = true;
7569 - $contents[] = array('role' => 'model', 'parts' => $parts);
7570 - $resp_parts = array();
7571 - foreach ($fn_calls as $fcall) {
7572 - if ($calls_made >= $budget) break;
7573 - $calls_made++;
7574 - $name = isset($fcall['name']) ? $fcall['name'] : '';
7575 - $args = isset($fcall['args']) && is_array($fcall['args']) ? $fcall['args'] : array();
7576 - $exec = $this->mxchat_fc_execute_tool($name, $args, $orig_message, $user_id, $session_id);
7577 - $fr = array('name' => $name, 'response' => array('result' => $exec['content']));
7578 - // Gemini 3 function calls carry a unique id; echo the matching id back in the
7579 - // functionResponse so the model maps the result to the right call (Google REST
7580 - // guidance). Older models omit the id — then we send none, exactly as before.
7581 - if (isset($fcall['id']) && $fcall['id'] !== '') { $fr['id'] = $fcall['id']; }
7582 - $resp_parts[] = array('functionResponse' => $fr);
7583 - }
7584 - $contents[] = array('role' => 'user', 'parts' => $resp_parts);
7585 - }
7586 - return $used_tool ? array('handled' => true, 'text' => $this->mxchat_fc_giveup_text()) : array('handled' => false);
7587 -}
7588 -
7589 -private function mxchat_fc_giveup_text() {
7590 - return esc_html__('I looked into that but could not put together a final answer. Please try rephrasing your request.', 'mxchat');
7591 -}
7592 -
7593 -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') {
7594 - try {
7595 3774 if (!$relevant_content) {
7596 3775 $error_response = [
7597 3776 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
7598 3777 'error_code' => 'no_relevant_content'
@@ -7597,75 +3776,25 @@
7597 3776 'error' => esc_html__("I couldn't find relevant information on that topic.", 'mxchat'),
7598 3777 'error_code' => 'no_relevant_content'
7599 3778 ];
7600 3779
3780 + // Add testing data to error response if available
7601 3781 if ($testing_data !== null) {
7602 3782 $error_response['testing_data'] = $testing_data;
3783 + //error_log("MxChat Testing: Added testing data to no_relevant_content error");
7603 3784 }
7604 3785
7605 3786 return $error_response;
7606 3787 }
7607 3788
3789 + // Ensure conversation_history is an array
7608 3790 if (!is_array($conversation_history)) {
7609 3791 $conversation_history = array();
7610 3792 }
7611 3793
7612 - // Check if this is an OpenRouter model
7613 - if ($selected_model === 'openrouter') {
7614 - // Get the actual OpenRouter model from options
7615 - $openrouter_selected_model = $this->options['openrouter_selected_model'] ?? '';
7616 -
7617 - if (empty($openrouter_selected_model)) {
7618 - $error_response = [
7619 - 'error' => esc_html__('No OpenRouter model selected. Please select a model in settings.', 'mxchat'),
7620 - 'error_code' => 'no_openrouter_model_selected'
7621 - ];
7622 - if ($testing_data !== null) {
7623 - $error_response['testing_data'] = $testing_data;
7624 - }
7625 - return $error_response;
7626 - }
7627 -
7628 - if (empty($openrouter_api_key)) {
7629 - $error_response = [
7630 - 'error' => esc_html__('OpenRouter API key is not configured', 'mxchat'),
7631 - 'error_code' => 'missing_openrouter_api_key'
7632 - ];
7633 - if ($testing_data !== null) {
7634 - $error_response['testing_data'] = $testing_data;
7635 - }
7636 - return $error_response;
7637 - }
7638 -
7639 - if ($streaming) {
7640 - return $this->mxchat_generate_response_openrouter_stream(
7641 - $openrouter_selected_model,
7642 - $openrouter_api_key,
7643 - $conversation_history,
7644 - $relevant_content,
7645 - $session_id,
7646 - $testing_data
7647 - );
7648 - } else {
7649 - $response = $this->mxchat_generate_response_openrouter(
7650 - $openrouter_selected_model,
7651 - $openrouter_api_key,
7652 - $conversation_history,
7653 - $relevant_content,
7654 - $session_id
7655 - );
7656 - }
7657 -
7658 - if (is_array($response) && isset($response['error'])) {
7659 - if ($testing_data !== null) {
7660 - $response['testing_data'] = $testing_data;
7661 - }
7662 - return $response;
7663 - }
7664 -
7665 - return $response;
7666 - }
7667 -
3794 + // Get selected model with default fallback
3795 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
3796 +
7668 3797 // Extract model prefix to determine the provider
7669 3798 $model_parts = explode('-', $selected_model);
7670 3799 $provider = strtolower($model_parts[0]);
7671 3800
@@ -7685,10 +3814,9 @@
7685 3814 $response = $this->mxchat_generate_response_gemini(
7686 3815 $selected_model,
7687 3816 $gemini_api_key,
7688 3817 $conversation_history,
7689 - $relevant_content,
7690 - $session_id
3818 + $relevant_content
7691 3819 );
7692 3820 break;
7693 3821
7694 3822 case 'claude':
@@ -7708,9 +3836,9 @@
7708 3836 $claude_api_key,
7709 3837 $conversation_history,
7710 3838 $relevant_content,
7711 3839 $session_id,
7712 - $testing_data
3840 + $testing_data // Pass testing data
7713 3841 );
7714 3842 } else {
7715 3843 $response = $this->mxchat_generate_response_claude(
7716 3844 $selected_model,
@@ -7715,10 +3843,9 @@
7715 3843 $response = $this->mxchat_generate_response_claude(
7716 3844 $selected_model,
7717 3845 $claude_api_key,
7718 3846 $conversation_history,
7719 - $relevant_content,
7720 - $session_id
3847 + $relevant_content
7721 3848 );
7722 3849 }
7723 3850 break;
7724 3851
@@ -7739,9 +3866,9 @@
7739 3866 $xai_api_key,
7740 3867 $conversation_history,
7741 3868 $relevant_content,
7742 3869 $session_id,
7743 - $testing_data
3870 + $testing_data // Pass testing data
7744 3871 );
7745 3872 } else {
7746 3873 $response = $this->mxchat_generate_response_xai(
7747 3874 $selected_model,
@@ -7746,10 +3873,9 @@
7746 3873 $response = $this->mxchat_generate_response_xai(
7747 3874 $selected_model,
7748 3875 $xai_api_key,
7749 3876 $conversation_history,
7750 - $relevant_content,
7751 - $session_id
3877 + $relevant_content
7752 3878 );
7753 3879 }
7754 3880 break;
7755 3881
@@ -7763,58 +3889,16 @@
7763 3889 $error_response['testing_data'] = $testing_data;
7764 3890 }
7765 3891 return $error_response;
7766 3892 }
7767 - if ($streaming) {
7768 - return $this->mxchat_generate_response_deepseek_stream(
7769 - $selected_model,
7770 - $deepseek_api_key,
7771 - $conversation_history,
7772 - $relevant_content,
7773 - $session_id,
7774 - $testing_data
7775 - );
7776 - } else {
7777 - $response = $this->mxchat_generate_response_deepseek(
7778 - $selected_model,
7779 - $deepseek_api_key,
7780 - $conversation_history,
7781 - $relevant_content,
7782 - $session_id
7783 - );
7784 - }
3893 + $response = $this->mxchat_generate_response_deepseek(
3894 + $selected_model,
3895 + $deepseek_api_key,
3896 + $conversation_history,
3897 + $relevant_content
3898 + );
7785 3899 break;
7786 3900
7787 - case 'custom':
7788 - // Custom (OpenAI-compatible) provider — Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI
7789 - $cp_base_url = isset($this->options['custom_provider_base_url']) ? trim((string) $this->options['custom_provider_base_url']) : '';
7790 - if (empty($cp_base_url)) {
7791 - $error_response = [
7792 - 'error' => esc_html__('Custom provider is not configured. Set Base URL in MxChat → API Keys → Custom Provider.', 'mxchat'),
7793 - 'error_code' => 'missing_custom_provider_base_url'
7794 - ];
7795 - if ($testing_data !== null) {
7796 - $error_response['testing_data'] = $testing_data;
7797 - }
7798 - return $error_response;
7799 - }
7800 - if ($streaming) {
7801 - return $this->mxchat_generate_response_custom_stream(
7802 - $selected_model,
7803 - $conversation_history,
7804 - $relevant_content,
7805 - $session_id,
7806 - $testing_data
7807 - );
7808 - } else {
7809 - $response = $this->mxchat_generate_response_custom(
7810 - $selected_model,
7811 - $conversation_history,
7812 - $relevant_content
7813 - );
7814 - }
7815 - break;
7816 -
7817 3901 case 'gpt':
7818 3902 case 'o1':
7819 3903 if (empty($api_key)) {
7820 3904 $error_response = [
@@ -7825,27 +3909,9 @@
7825 3909 $error_response['testing_data'] = $testing_data;
7826 3910 }
7827 3911 return $error_response;
7828 3912 }
7829 -
7830 - // Check if web search is enabled for this OpenAI model
7831 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7832 - // Models that don't support web search
7833 - $unsupported_web_search_models = array('gpt-4.1-nano');
7834 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
7835 -
7836 - if ($web_search_enabled && $model_supports_web_search) {
7837 - // Use Responses API (required for some models, or when web search is enabled)
7838 - return $this->mxchat_generate_response_openai_web_search(
7839 - $selected_model,
7840 - $api_key,
7841 - $conversation_history,
7842 - $relevant_content,
7843 - $session_id,
7844 - $testing_data,
7845 - $streaming
7846 - );
7847 - } elseif ($streaming) {
3913 + if ($streaming) {
7848 3914 return $this->mxchat_generate_response_openai_stream(
7849 3915 $selected_model,
7850 3916 $api_key,
7851 3917 $conversation_history,
@@ -7850,9 +3916,9 @@
7850 3916 $api_key,
7851 3917 $conversation_history,
7852 3918 $relevant_content,
7853 3919 $session_id,
7854 - $testing_data
3920 + $testing_data // Pass testing data
7855 3921 );
7856 3922 } else {
7857 3923 $response = $this->mxchat_generate_response_openai(
7858 3924 $selected_model,
@@ -7857,15 +3923,15 @@
7857 3923 $response = $this->mxchat_generate_response_openai(
7858 3924 $selected_model,
7859 3925 $api_key,
7860 3926 $conversation_history,
7861 - $relevant_content,
7862 - $session_id
3927 + $relevant_content
7863 3928 );
7864 3929 }
7865 3930 break;
7866 3931
7867 3932 default:
3933 + // Default to OpenAI for custom models or unrecognized prefixes
7868 3934 if (empty($api_key)) {
7869 3935 $error_response = [
7870 3936 'error' => esc_html__('OpenAI API key is not configured', 'mxchat'),
7871 3937 'error_code' => 'missing_openai_api_key'
@@ -7874,25 +3940,9 @@
7874 3940 $error_response['testing_data'] = $testing_data;
7875 3941 }
7876 3942 return $error_response;
7877 3943 }
7878 -
7879 - // Check if web search is enabled (default case also handles OpenAI models)
7880 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
7881 - $unsupported_web_search_models = array('gpt-4.1-nano');
7882 - $model_supports_web_search = !in_array($selected_model, $unsupported_web_search_models);
7883 -
7884 - if ($web_search_enabled && $model_supports_web_search) {
7885 - return $this->mxchat_generate_response_openai_web_search(
7886 - $selected_model,
7887 - $api_key,
7888 - $conversation_history,
7889 - $relevant_content,
7890 - $session_id,
7891 - $testing_data,
7892 - $streaming
7893 - );
7894 - } elseif ($streaming) {
3944 + if ($streaming) {
7895 3945 return $this->mxchat_generate_response_openai_stream(
7896 3946 $selected_model,
7897 3947 $api_key,
7898 3948 $conversation_history,
@@ -7897,9 +3947,9 @@
7897 3947 $api_key,
7898 3948 $conversation_history,
7899 3949 $relevant_content,
7900 3950 $session_id,
7901 - $testing_data
3951 + $testing_data // Pass testing data
7902 3952 );
7903 3953 } else {
7904 3954 $response = $this->mxchat_generate_response_openai(
7905 3955 $selected_model,
@@ -7904,25 +3954,30 @@
7904 3954 $response = $this->mxchat_generate_response_openai(
7905 3955 $selected_model,
7906 3956 $api_key,
7907 3957 $conversation_history,
7908 - $relevant_content,
7909 - $session_id
3958 + $relevant_content
7910 3959 );
7911 3960 }
7912 3961 break;
7913 3962 }
7914 3963
3964 + // Check if the response is an error array from the provider-specific function
7915 3965 if (is_array($response) && isset($response['error'])) {
3966 + // Add testing data to error response if available
7916 3967 if ($testing_data !== null) {
7917 3968 $response['testing_data'] = $testing_data;
3969 + //error_log("MxChat Testing: Added testing data to provider error response");
7918 3970 }
7919 - return $response;
3971 + return $response; // Pass through the error with testing data
7920 3972 }
7921 3973
3974 + // For successful non-streaming responses, we don't add testing data here
3975 + // because it will be added in the main handler
7922 3976 return $response;
7923 3977
7924 3978 } catch (Exception $e) {
3979 + //error_log('MXChat Error: ' . $e->getMessage());
7925 3980 $error_response = [
7926 3981 'error' => sprintf(esc_html__('An error occurred: %s', 'mxchat'), esc_html($e->getMessage())),
7927 3982 'error_code' => 'system_exception',
7928 3983 'exception_details' => $e->getMessage()
@@ -7927,1223 +3982,24 @@
7927 3982 'error_code' => 'system_exception',
7928 3983 'exception_details' => $e->getMessage()
7929 3984 ];
7930 3985
3986 + // Add testing data to exception response if available
7931 3987 if ($testing_data !== null) {
7932 3988 $error_response['testing_data'] = $testing_data;
3989 + //error_log("MxChat Testing: Added testing data to exception response");
7933 3990 }
7934 3991
7935 3992 return $error_response;
7936 3993 }
7937 3994 }
7938 -private function mxchat_generate_response_openrouter_stream($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
7939 - try {
7940 - $bot_id = $this->get_current_bot_id($session_id);
7941 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
7942 -
7943 - if (!is_array($conversation_history)) {
7944 - $conversation_history = array();
7945 - }
7946 3995
7947 - $formatted_conversation = array();
7948 -
7949 - $formatted_conversation[] = array(
7950 - 'role' => 'system',
7951 - 'content' => $system_prompt_instructions . " " . $relevant_content
7952 - );
7953 -
7954 - foreach ($conversation_history as $message) {
7955 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
7956 - $role = $message['role'];
7957 - if ($role === 'bot' || $role === 'agent') {
7958 - $role = 'assistant';
7959 - }
7960 - if (!in_array($role, ['system', 'assistant', 'user'])) {
7961 - $role = 'user';
7962 - }
7963 - $formatted_conversation[] = array(
7964 - 'role' => $role,
7965 - 'content' => $message['content']
7966 - );
7967 - }
7968 - }
7969 -
7970 - if (headers_sent() || !function_exists('curl_init')) {
7971 - $regular_response = $this->mxchat_generate_response_openrouter(
7972 - $selected_model,
7973 - $openrouter_api_key,
7974 - $conversation_history,
7975 - $relevant_content,
7976 - $session_id
7977 - );
7978 -
7979 - // Save bot response to transcript
7980 - if (!empty($regular_response) && !empty($session_id)) {
7981 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
7982 - }
7983 -
7984 - $response_data = [
7985 - 'text' => $regular_response,
7986 - 'html' => '',
7987 - 'session_id' => $session_id
7988 - ];
7989 -
7990 - if ($testing_data !== null) {
7991 - $response_data['testing_data'] = $testing_data;
7992 - }
7993 -
7994 - header('Content-Type: application/json');
7995 - echo json_encode($response_data);
7996 - return true;
7997 - }
7998 -
7999 - $body = json_encode([
8000 - 'model' => $selected_model,
8001 - 'messages' => $formatted_conversation,
8002 - 'temperature' => 1,
8003 - 'stream' => true
8004 - ]);
8005 -
8006 - // V2 retry-on-initial-connect: setup_streaming_headers is now lazy-fired
8007 - // inside WRITEFUNCTION on first byte of a successful upstream.
8008 -
8009 - $captured_status_code = 0;
8010 - $captured_body_pre_stream = '';
8011 - $full_response = '';
8012 - $stream_started = false;
8013 - $buffer = '';
8014 - $errno = 0;
8015 - $last_curl_error = '';
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, 'https://openrouter.ai/api/v1/chat/completions');
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, array(
8037 - 'Content-Type: application/json',
8038 - 'Authorization: Bearer ' . $openrouter_api_key,
8039 - 'HTTP-Referer: ' . home_url(),
8040 - 'X-Title: ' . get_bloginfo('name')
8041 - ));
8042 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8043 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8044 -
8045 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
8046 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
8047 - $captured_status_code = (int) $m[1];
8048 - }
8049 - return strlen($header);
8050 - });
8051 -
8052 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
8053 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
8054 - $captured_body_pre_stream .= $data;
8055 - return strlen($data);
8056 - }
8057 -
8058 - if (!$this->streaming_headers_sent) {
8059 - $this->setup_streaming_headers();
8060 - }
8061 -
8062 - if (!$stream_started && $testing_data !== null) {
8063 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8064 - flush();
8065 - $stream_started = true;
8066 - }
8067 -
8068 - $buffer .= $data;
8069 - $lines = explode("\n", $buffer);
8070 - $buffer = array_pop($lines);
8071 -
8072 - foreach ($lines as $line) {
8073 - if (trim($line) === '') {
8074 - continue;
8075 - }
8076 - if (strpos($line, 'data: ') !== 0) {
8077 - continue;
8078 - }
8079 -
8080 - $json_str = substr($line, 6);
8081 -
8082 - if (trim($json_str) === '[DONE]') {
8083 - echo "data: [DONE]\n\n";
8084 - flush();
8085 - continue;
8086 - }
8087 -
8088 - $json = json_decode(trim($json_str), true);
8089 - if ($json && isset($json['choices'][0]['delta']['content'])) {
8090 - $content = $json['choices'][0]['delta']['content'];
8091 - $full_response .= $content;
8092 -
8093 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
8094 - flush();
8095 - }
8096 - }
8097 -
8098 - return strlen($data);
8099 - });
8100 -
8101 - $response = curl_exec($ch);
8102 - $errno = curl_errno($ch);
8103 - $last_curl_error = curl_error($ch);
8104 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
8105 - curl_close($ch);
8106 -
8107 - if (!$errno && $http_code === 200) {
8108 - break;
8109 - }
8110 -
8111 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
8112 - $can_retry = !$this->streaming_headers_sent
8113 - && ($attempt + 1) < $max_attempts
8114 - && $is_transient;
8115 -
8116 - if (defined('WP_DEBUG') && WP_DEBUG) {
8117 - error_log(sprintf(
8118 - '[MxChat] openrouter_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
8119 - $attempt + 1, $max_attempts, $http_code, $errno,
8120 - $is_transient ? 'yes' : 'no',
8121 - $can_retry ? 'Retrying.' : 'Giving up.'
8122 - ));
8123 - }
8124 -
8125 - if (!$can_retry) {
8126 - break;
8127 - }
8128 - }
8129 -
8130 - if (!$errno && $http_code === 200) {
8131 - if (!empty($full_response) && !empty($session_id)) {
8132 - $rag_context_for_storage = null;
8133 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8134 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8135 -
8136 - if ($has_rag_data || $has_action_data) {
8137 - $rag_context_for_storage = [];
8138 -
8139 - if ($has_rag_data) {
8140 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8141 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8142 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8143 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8144 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8145 - }
8146 -
8147 - if ($has_action_data) {
8148 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8149 - }
8150 - }
8151 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
8152 - }
8153 - return true;
8154 - }
8155 -
8156 - return $this->mxchat_stream_emit_fallback(
8157 - 'openai',
8158 - $this->mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id),
8159 - $session_id,
8160 - $testing_data
8161 - );
8162 -
8163 - } catch (Exception $e) {
8164 - return $this->mxchat_stream_emit_fallback(
8165 - 'openai',
8166 - $this->mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id),
8167 - $session_id,
8168 - $testing_data
8169 - );
8170 - }
8171 -}
8172 -private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
3996 +// 2. streaming function
3997 +private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8173 3998 try {
8174 - $bot_id = $this->get_current_bot_id($session_id);
8175 -
8176 - // Get system prompt instructions using centralized function
8177 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8178 -
8179 - // Ensure conversation_history is an array
8180 - if (!is_array($conversation_history)) {
8181 - $conversation_history = array();
8182 - }
3999 + // Get system prompt instructions from options
4000 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
8183 4001
8184 - // Format conversation history for OpenAI
8185 - $formatted_conversation = array();
8186 -
8187 - $formatted_conversation[] = array(
8188 - 'role' => 'system',
8189 - 'content' => $system_prompt_instructions . " " . $relevant_content
8190 - );
8191 -
8192 - foreach ($conversation_history as $message) {
8193 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8194 - $role = $message['role'];
8195 - if ($role === 'bot' || $role === 'agent') {
8196 - $role = 'assistant';
8197 - }
8198 - if (!in_array($role, ['system', 'assistant', 'user', 'function', 'tool'])) {
8199 - $role = 'user';
8200 - }
8201 - $formatted_conversation[] = array(
8202 - 'role' => $role,
8203 - 'content' => $message['content']
8204 - );
8205 - }
8206 - }
8207 -
8208 - // Check if we can actually stream
8209 - if (headers_sent() || !function_exists('curl_init')) {
8210 - // Fallback to regular response with testing data
8211 - $regular_response = $this->mxchat_generate_response_openai(
8212 - $selected_model,
8213 - $api_key,
8214 - $conversation_history,
8215 - $relevant_content,
8216 - $session_id
8217 - );
8218 -
8219 - // Save bot response to transcript
8220 - if (!empty($regular_response) && !empty($session_id)) {
8221 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
8222 - }
8223 -
8224 - $response_data = [
8225 - 'text' => $regular_response,
8226 - 'html' => '',
8227 - 'session_id' => $session_id
8228 - ];
8229 -
8230 - if ($testing_data !== null) {
8231 - $response_data['testing_data'] = $testing_data;
8232 - }
8233 -
8234 - header('Content-Type: application/json');
8235 - echo json_encode($response_data);
8236 - return true;
8237 - }
8238 -
8239 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
8240 - $is_gpt5_model = (
8241 - strpos($selected_model, 'gpt-5') === 0 ||
8242 - $selected_model === 'gpt-5.2' ||
8243 - $selected_model === 'gpt-5.1-2025-11-13' ||
8244 - $selected_model === 'gpt-5' ||
8245 - $selected_model === 'gpt-5-mini' ||
8246 - $selected_model === 'gpt-5-nano'
8247 - );
8248 -
8249 - // Build request body with optimal settings for fast streaming
8250 - $request_body = [
8251 - 'model' => $selected_model,
8252 - 'messages' => $formatted_conversation,
8253 - 'temperature' => 1,
8254 - 'stream' => true
8255 - ];
8256 -
8257 - // Add reasoning_effort only for GPT-5 models that support it
8258 - // These chat models don't support reasoning_effort parameter
8259 - $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');
8260 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
8261 - // GPT-5.1 uses 'low' instead of 'minimal'
8262 - if ($selected_model === 'gpt-5.1-2025-11-13') {
8263 - $request_body['reasoning_effort'] = 'low';
8264 - } elseif ($selected_model === 'gpt-5.5') {
8265 - $request_body['reasoning_effort'] = 'none';
8266 - } elseif ($selected_model === 'gpt-5.4') {
8267 - $request_body['reasoning_effort'] = 'none';
8268 - } else {
8269 - $request_body['reasoning_effort'] = 'minimal';
8270 - }
8271 - }
8272 -
8273 - $body = json_encode($request_body);
8274 -
8275 - // V2 retry-on-initial-connect: do NOT call setup_streaming_headers() here.
8276 - // It is now lazy-fired inside the WRITEFUNCTION on the first byte of a
8277 - // SUCCESSFUL upstream response, gated by the captured HTTP status.
8278 -
8279 - $captured_status_code = 0;
8280 - $captured_body_pre_stream = '';
8281 - $full_response = '';
8282 - $stream_started = false;
8283 - $buffer = '';
8284 - $errno = 0;
8285 - $last_curl_error = '';
8286 - $http_code = 0;
8287 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
8288 - $backoff_ms = array(0, 750, 2000);
8289 -
8290 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
8291 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
8292 - usleep($backoff_ms[$attempt] * 1000);
8293 - }
8294 -
8295 - // Reset per-attempt capture state.
8296 - $captured_status_code = 0;
8297 - $captured_body_pre_stream = '';
8298 - $full_response = '';
8299 - $stream_started = false;
8300 - $buffer = '';
8301 -
8302 - $ch = curl_init();
8303 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
8304 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8305 - curl_setopt($ch, CURLOPT_POST, true);
8306 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
8307 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
8308 - 'Content-Type: application/json',
8309 - 'Authorization: Bearer ' . $api_key
8310 - ));
8311 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8312 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
8313 -
8314 - // Capture HTTP status as soon as response headers arrive — fires before WRITEFUNCTION.
8315 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
8316 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
8317 - $captured_status_code = (int) $m[1];
8318 - }
8319 - return strlen($header);
8320 - });
8321 -
8322 - // Buffer control for real-time streaming
8323 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
8324 - // V2 guard: if upstream returned non-200, buffer body for transient
8325 - // classification and DO NOT emit to client. Stream channel must NOT open.
8326 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
8327 - $captured_body_pre_stream .= $data;
8328 - return strlen($data);
8329 - }
8330 -
8331 - // Lazy-fire streaming headers on first byte of a SUCCESSFUL upstream.
8332 - // After this point streaming_headers_sent === true → retry is structurally blocked.
8333 - if (!$this->streaming_headers_sent) {
8334 - $this->setup_streaming_headers();
8335 - }
8336 -
8337 - // Send testing data as the first event if available
8338 - if (!$stream_started && $testing_data !== null) {
8339 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
8340 - flush();
8341 - $stream_started = true;
8342 - }
8343 -
8344 - // CRITICAL FIX: Append new data to buffer
8345 - $buffer .= $data;
8346 -
8347 - // Process complete lines only
8348 - $lines = explode("\n", $buffer);
8349 -
8350 - // CRITICAL FIX: Keep the last incomplete line in the buffer
8351 - $buffer = array_pop($lines);
8352 -
8353 - foreach ($lines as $line) {
8354 - if (trim($line) === '') {
8355 - continue;
8356 - }
8357 - if (strpos($line, 'data: ') !== 0) {
8358 - continue;
8359 - }
8360 -
8361 - $json_str = substr($line, 6);
8362 -
8363 - if (trim($json_str) === '[DONE]') {
8364 - echo "data: [DONE]\n\n";
8365 - flush();
8366 - continue;
8367 - }
8368 -
8369 - $json = json_decode(trim($json_str), true);
8370 - if ($json && isset($json['choices'][0]['delta']['content'])) {
8371 - $content = $json['choices'][0]['delta']['content'];
8372 - $full_response .= $content;
8373 -
8374 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
8375 - flush();
8376 - }
8377 - }
8378 -
8379 - return strlen($data);
8380 - });
8381 -
8382 - $response = curl_exec($ch);
8383 - $errno = curl_errno($ch);
8384 - $last_curl_error = curl_error($ch);
8385 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
8386 - curl_close($ch);
8387 -
8388 - if (!$errno && $http_code === 200) {
8389 - break; // Happy path — WRITEFUNCTION already streamed everything.
8390 - }
8391 -
8392 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
8393 - $can_retry = !$this->streaming_headers_sent
8394 - && ($attempt + 1) < $max_attempts
8395 - && $is_transient;
8396 -
8397 - if (defined('WP_DEBUG') && WP_DEBUG) {
8398 - error_log(sprintf(
8399 - '[MxChat] openai_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
8400 - $attempt + 1, $max_attempts, $http_code, $errno,
8401 - $is_transient ? 'yes' : 'no',
8402 - $can_retry ? 'Retrying.' : 'Giving up.'
8403 - ));
8404 - }
8405 -
8406 - if (!$can_retry) {
8407 - break;
8408 - }
8409 - }
8410 -
8411 - // Post-loop branch.
8412 - if (!$errno && $http_code === 200) {
8413 - // Happy path — save the complete response to maintain chat persistence.
8414 - if (!empty($full_response) && !empty($session_id)) {
8415 - $rag_context_for_storage = null;
8416 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
8417 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
8418 -
8419 - if ($has_rag_data || $has_action_data) {
8420 - $rag_context_for_storage = [];
8421 -
8422 - if ($has_rag_data) {
8423 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
8424 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
8425 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
8426 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
8427 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
8428 - }
8429 -
8430 - if ($has_action_data) {
8431 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
8432 - }
8433 - }
8434 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
8435 - }
8436 -
8437 - return true;
8438 - }
8439 -
8440 - // Failure path — branch on whether SSE channel was opened.
8441 - return $this->mxchat_stream_emit_fallback(
8442 - 'openai',
8443 - $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content, $session_id),
8444 - $session_id,
8445 - $testing_data
8446 - );
8447 -
8448 - } catch (Exception $e) {
8449 - return $this->mxchat_stream_emit_fallback(
8450 - 'openai',
8451 - $this->mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content, $session_id),
8452 - $session_id,
8453 - $testing_data
8454 - );
8455 - }
8456 -}
8457 -
8458 -/**
8459 - * Shared fallback emitter for streaming chat functions. Two outcomes:
8460 - * - streaming_headers_sent === true: SSE channel is open. Emit fallback content
8461 - * as `data: {...}\n\n` + `data: [DONE]\n\n` so the widget renders it as a
8462 - * normal bot bubble. Transcript row is persisted.
8463 - * - streaming_headers_sent === false: SSE channel never opened (retries
8464 - * exhausted on initial connect). Emit a clean JSON response — the path
8465 - * the widget would normally hit if streaming wasn't even attempted.
8466 - *
8467 - * Used by all six *_stream functions after their per-attempt retry loop.
8468 - */
8469 -private function mxchat_stream_emit_fallback($provider_hint, $regular_response, $session_id, $testing_data = null) {
8470 - $is_error_array = is_array($regular_response) && isset($regular_response['error']);
8471 -
8472 - if ($this->streaming_headers_sent) {
8473 - if ($is_error_array) {
8474 - echo "data: " . json_encode([
8475 - 'error' => true,
8476 - 'error_message' => $regular_response['error'],
8477 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
8478 - 'text' => $regular_response['error'],
8479 - 'message' => $regular_response['error']
8480 - ]) . "\n\n";
8481 - echo "data: [DONE]\n\n";
8482 - flush();
8483 - return true;
8484 - }
8485 - $fallback_message = (string) $regular_response;
8486 - if (!empty($fallback_message) && !empty($session_id)) {
8487 - $this->mxchat_save_chat_message($session_id, 'bot', $fallback_message);
8488 - }
8489 - echo "data: " . json_encode(['content' => $fallback_message]) . "\n\n";
8490 - echo "data: [DONE]\n\n";
8491 - flush();
8492 - return true;
8493 - }
8494 -
8495 - // SSE channel never opened — clean JSON fallback.
8496 - if ($is_error_array) {
8497 - header('Content-Type: application/json');
8498 - echo json_encode(array(
8499 - 'error' => true,
8500 - 'error_message' => $regular_response['error'],
8501 - 'error_code' => $regular_response['error_code'] ?? 'api_error',
8502 - 'text' => $regular_response['error'],
8503 - 'message' => $regular_response['error'],
8504 - ));
8505 - return true;
8506 - }
8507 -
8508 - $fallback_message = (string) $regular_response;
8509 - if (!empty($fallback_message) && !empty($session_id)) {
8510 - $this->mxchat_save_chat_message($session_id, 'bot', $fallback_message);
8511 - }
8512 - $response_data = array(
8513 - 'text' => $fallback_message,
8514 - 'html' => '',
8515 - 'session_id' => $session_id,
8516 - );
8517 - if ($testing_data !== null) {
8518 - $response_data['testing_data'] = $testing_data;
8519 - }
8520 - header('Content-Type: application/json');
8521 - echo json_encode($response_data);
8522 - return true;
8523 -}
8524 -
8525 -/**
8526 - * Resolve custom (OpenAI-compatible) provider config from settings.
8527 - * Returns ['base_url','api_key','model','auth_scheme','api_version','chat_url','headers'].
8528 - */
8529 -private function mxchat_resolve_custom_provider() {
8530 - $base_url = isset($this->options['custom_provider_base_url']) ? rtrim(trim((string) $this->options['custom_provider_base_url']), '/') : '';
8531 - $api_key = isset($this->options['custom_provider_api_key']) ? trim((string) $this->options['custom_provider_api_key']) : '';
8532 - $model = isset($this->options['custom_provider_model']) ? trim((string) $this->options['custom_provider_model']) : '';
8533 - $auth_scheme = isset($this->options['custom_provider_auth_scheme']) ? $this->options['custom_provider_auth_scheme'] : 'bearer';
8534 - $api_version = isset($this->options['custom_provider_api_version']) ? trim((string) $this->options['custom_provider_api_version']) : '';
8535 -
8536 - $chat_url = $base_url . '/chat/completions';
8537 - if (!empty($api_version)) {
8538 - $chat_url .= (strpos($chat_url, '?') === false ? '?' : '&') . 'api-version=' . rawurlencode($api_version);
8539 - }
8540 -
8541 - $headers = array('Content-Type: application/json');
8542 - if (!empty($api_key)) {
8543 - if ($auth_scheme === 'api-key') {
8544 - $headers[] = 'api-key: ' . $api_key;
8545 - } else {
8546 - $headers[] = 'Authorization: Bearer ' . $api_key;
8547 - }
8548 - }
8549 -
8550 - return array(
8551 - 'base_url' => $base_url,
8552 - 'api_key' => $api_key,
8553 - 'model' => $model !== '' ? $model : 'default',
8554 - 'auth_scheme' => $auth_scheme,
8555 - 'api_version' => $api_version,
8556 - 'chat_url' => $chat_url,
8557 - 'headers' => $headers,
8558 - );
8559 -}
8560 -
8561 -/**
8562 - * Streaming chat completion against an OpenAI-compatible custom provider
8563 - * (Ollama, LM Studio, vLLM, llama.cpp, Azure OpenAI, etc.).
8564 - * Mirrors mxchat_generate_response_openai_stream but with parameterized URL/auth/model.
8565 - */
8566 -private function mxchat_generate_response_custom_stream($selected_model, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
8567 - try {
8568 - $cfg = $this->mxchat_resolve_custom_provider();
8569 - if (empty($cfg['base_url'])) {
8570 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
8571 - }
8572 -
8573 - $bot_id = $this->get_current_bot_id($session_id);
8574 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8575 - if (!is_array($conversation_history)) {
8576 - $conversation_history = array();
8577 - }
8578 -
8579 - $formatted_conversation = array();
8580 - $formatted_conversation[] = array(
8581 - 'role' => 'system',
8582 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
8583 - );
8584 - foreach ($conversation_history as $message) {
8585 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8586 - $role = $message['role'];
8587 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
8588 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
8589 - $formatted_conversation[] = array('role' => $role, 'content' => $message['content']);
8590 - }
8591 - }
8592 -
8593 - if (headers_sent() || !function_exists('curl_init')) {
8594 - // No streaming capability — fall through to non-stream wrapper
8595 - $regular = $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content);
8596 - if (!empty($regular) && !empty($session_id) && is_string($regular)) {
8597 - $this->mxchat_save_chat_message($session_id, 'bot', $regular);
8598 - }
8599 - $response_data = array('text' => is_string($regular) ? $regular : '', 'html' => '', 'session_id' => $session_id);
8600 - if ($testing_data !== null) { $response_data['testing_data'] = $testing_data; }
8601 - header('Content-Type: application/json');
8602 - echo json_encode($response_data);
8603 - return true;
8604 - }
8605 -
8606 - $request_body = array(
8607 - 'model' => $cfg['model'],
8608 - 'messages' => $formatted_conversation,
8609 - 'stream' => true,
8610 - );
8611 - $body = json_encode($request_body);
8612 -
8613 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
8614 -
8615 - $captured_status_code = 0;
8616 - $captured_body_pre_stream = '';
8617 - $full_response = '';
8618 - $stream_started = false;
8619 - $buffer = '';
8620 - $errno = 0;
8621 - $http_code = 0;
8622 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
8623 - $backoff_ms = array(0, 750, 2000);
8624 -
8625 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
8626 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
8627 - usleep($backoff_ms[$attempt] * 1000);
8628 - }
8629 -
8630 - $captured_status_code = 0;
8631 - $captured_body_pre_stream = '';
8632 - $full_response = '';
8633 - $stream_started = false;
8634 - $buffer = '';
8635 -
8636 - $ch = curl_init();
8637 - curl_setopt($ch, CURLOPT_URL, $cfg['chat_url']);
8638 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8639 - curl_setopt($ch, CURLOPT_POST, true);
8640 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
8641 - curl_setopt($ch, CURLOPT_HTTPHEADER, $cfg['headers']);
8642 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
8643 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
8644 -
8645 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
8646 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
8647 - $captured_status_code = (int) $m[1];
8648 - }
8649 - return strlen($header);
8650 - });
8651 -
8652 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
8653 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
8654 - $captured_body_pre_stream .= $data;
8655 - return strlen($data);
8656 - }
8657 -
8658 - if (!$this->streaming_headers_sent) {
8659 - $this->setup_streaming_headers();
8660 - }
8661 -
8662 - if (!$stream_started && $testing_data !== null) {
8663 - echo "data: " . json_encode(array('testing_data' => $testing_data)) . "\n\n";
8664 - flush();
8665 - $stream_started = true;
8666 - }
8667 - $buffer .= $data;
8668 - $lines = explode("\n", $buffer);
8669 - $buffer = array_pop($lines);
8670 - foreach ($lines as $line) {
8671 - if (trim($line) === '') { continue; }
8672 - if (strpos($line, 'data: ') !== 0) { continue; }
8673 - $json_str = substr($line, 6);
8674 - if (trim($json_str) === '[DONE]') {
8675 - echo "data: [DONE]\n\n";
8676 - flush();
8677 - continue;
8678 - }
8679 - $json = json_decode(trim($json_str), true);
8680 - if ($json && isset($json['choices'][0]['delta']['content'])) {
8681 - $content = $json['choices'][0]['delta']['content'];
8682 - $full_response .= $content;
8683 - echo "data: " . json_encode(array('content' => $content)) . "\n\n";
8684 - flush();
8685 - }
8686 - }
8687 - return strlen($data);
8688 - });
8689 -
8690 - $response = curl_exec($ch);
8691 - $errno = curl_errno($ch);
8692 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
8693 - curl_close($ch);
8694 -
8695 - if (!$errno && $http_code === 200) {
8696 - break;
8697 - }
8698 -
8699 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
8700 - $can_retry = !$this->streaming_headers_sent
8701 - && ($attempt + 1) < $max_attempts
8702 - && $is_transient;
8703 -
8704 - if (defined('WP_DEBUG') && WP_DEBUG) {
8705 - error_log(sprintf(
8706 - '[MxChat] custom_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
8707 - $attempt + 1, $max_attempts, $http_code, $errno,
8708 - $is_transient ? 'yes' : 'no',
8709 - $can_retry ? 'Retrying.' : 'Giving up.'
8710 - ));
8711 - }
8712 -
8713 - if (!$can_retry) {
8714 - break;
8715 - }
8716 - }
8717 -
8718 - if (!$errno && $http_code === 200) {
8719 - if (!empty($full_response) && !empty($session_id)) {
8720 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
8721 - }
8722 - return true;
8723 - }
8724 -
8725 - return $this->mxchat_stream_emit_fallback(
8726 - 'openai',
8727 - $this->mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content),
8728 - $session_id,
8729 - $testing_data
8730 - );
8731 -
8732 - } catch (Exception $e) {
8733 - return array('error' => sprintf(esc_html__('Custom provider error: %s', 'mxchat'), $e->getMessage()), 'error_code' => 'custom_provider_exception');
8734 - }
8735 -}
8736 -
8737 -/**
8738 - * Non-streaming chat completion against a custom OpenAI-compatible provider.
8739 - * Returns string content on success, array['error'=>...] on failure.
8740 - */
8741 -private function mxchat_generate_response_custom($selected_model, $conversation_history, $relevant_content) {
8742 - $cfg = $this->mxchat_resolve_custom_provider();
8743 - if (empty($cfg['base_url'])) {
8744 - return array('error' => esc_html__('Custom provider Base URL is not configured.', 'mxchat'), 'error_code' => 'missing_custom_provider_base_url');
8745 - }
8746 -
8747 - $bot_id = $this->get_current_bot_id(null);
8748 - $system_prompt_instructions = $this->get_system_instructions($bot_id, null);
8749 - if (!is_array($conversation_history)) {
8750 - $conversation_history = array();
8751 - }
8752 -
8753 - $messages = array(array(
8754 - 'role' => 'system',
8755 - 'content' => $system_prompt_instructions . ' ' . $relevant_content,
8756 - ));
8757 - foreach ($conversation_history as $message) {
8758 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8759 - $role = $message['role'];
8760 - if ($role === 'bot' || $role === 'agent') { $role = 'assistant'; }
8761 - if (!in_array($role, array('system', 'assistant', 'user', 'function', 'tool'))) { $role = 'user'; }
8762 - $messages[] = array('role' => $role, 'content' => $message['content']);
8763 - }
8764 - }
8765 -
8766 - $headers_assoc = array('Content-Type' => 'application/json');
8767 - if (!empty($cfg['api_key'])) {
8768 - if ($cfg['auth_scheme'] === 'api-key') {
8769 - $headers_assoc['api-key'] = $cfg['api_key'];
8770 - } else {
8771 - $headers_assoc['Authorization'] = 'Bearer ' . $cfg['api_key'];
8772 - }
8773 - }
8774 -
8775 - $response = $this->mxchat_provider_call_with_retry($cfg['chat_url'], array(
8776 - 'headers' => $headers_assoc,
8777 - 'body' => wp_json_encode(array(
8778 - 'model' => $cfg['model'],
8779 - 'messages' => $messages,
8780 - )),
8781 - 'timeout' => 120,
8782 - ), 'openai');
8783 -
8784 - if (is_wp_error($response)) {
8785 - return array('error' => sprintf(esc_html__('Custom provider request failed: %s', 'mxchat'), $response->get_error_message()), 'error_code' => 'custom_provider_network_error');
8786 - }
8787 - $code = (int) wp_remote_retrieve_response_code($response);
8788 - if ($code < 200 || $code >= 300) {
8789 - return array('error' => sprintf(esc_html__('Custom provider returned HTTP %d.', 'mxchat'), $code), 'error_code' => 'custom_provider_http_error');
8790 - }
8791 - $body = json_decode(wp_remote_retrieve_body($response), true);
8792 - if (isset($body['choices'][0]['message']['content'])) {
8793 - return (string) $body['choices'][0]['message']['content'];
8794 - }
8795 - return array('error' => esc_html__('Custom provider returned an unexpected response shape.', 'mxchat'), 'error_code' => 'custom_provider_response_shape');
8796 -}
8797 -
8798 -/**
8799 - * Generate response using OpenAI Responses API with web search tool
8800 - * This uses the newer Responses API which supports web search functionality
8801 - */
8802 -private function mxchat_generate_response_openai_web_search($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null, $streaming = false) {
8803 - try {
8804 - $bot_id = $this->get_current_bot_id($session_id);
8805 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
8806 -
8807 - if (!is_array($conversation_history)) {
8808 - $conversation_history = array();
8809 - }
8810 -
8811 - // Build the input for Responses API
8812 - // The Responses API uses a different format - we need to construct the input properly
8813 - $input_parts = [];
8814 -
8815 - // Add system instructions as context
8816 - $system_context = $system_prompt_instructions . "\n\n" . $relevant_content;
8817 -
8818 - // Build conversation as input items for Responses API
8819 - foreach ($conversation_history as $message) {
8820 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
8821 - $role = $message['role'];
8822 - if ($role === 'bot' || $role === 'agent') {
8823 - $role = 'assistant';
8824 - }
8825 - if (!in_array($role, ['assistant', 'user'])) {
8826 - $role = 'user';
8827 - }
8828 - $input_parts[] = [
8829 - 'type' => 'message',
8830 - 'role' => $role,
8831 - 'content' => $message['content']
8832 - ];
8833 - }
8834 - }
8835 -
8836 - // Build request body for Responses API
8837 - $request_body = [
8838 - 'model' => $selected_model,
8839 - 'input' => $input_parts,
8840 - 'instructions' => $system_context,
8841 - 'stream' => $streaming
8842 - ];
8843 -
8844 - // Only add web search tool if web search is enabled in settings
8845 - $web_search_enabled = isset($this->options['enable_web_search']) && $this->options['enable_web_search'] === 'on';
8846 - if ($web_search_enabled) {
8847 - $request_body['tools'] = [
8848 - ['type' => 'web_search']
8849 - ];
8850 - }
8851 -
8852 - // Add reasoning effort for supported models
8853 - $is_gpt5_model = strpos($selected_model, 'gpt-5') === 0;
8854 - $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
8855 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_web, true)) {
8856 - if ($selected_model === 'gpt-5.1-2025-11-13') {
8857 - $request_body['reasoning'] = ['effort' => 'low'];
8858 - } elseif ($selected_model === 'gpt-5.5') {
8859 - $request_body['reasoning'] = ['effort' => 'low'];
8860 - } elseif ($selected_model === 'gpt-5.4') {
8861 - $request_body['reasoning'] = ['effort' => 'low'];
8862 - }
8863 - }
8864 -
8865 - //error_log("MXCHAT WEB SEARCH: Request body: " . json_encode($request_body));
8866 -
8867 - if ($streaming) {
8868 - return $this->mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data);
8869 - } else {
8870 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
8871 - }
8872 -
8873 - } catch (Exception $e) {
8874 - //error_log("MXCHAT WEB SEARCH ERROR: " . $e->getMessage());
8875 - return [
8876 - 'error' => sprintf(esc_html__('Web search error: %s', 'mxchat'), esc_html($e->getMessage())),
8877 - 'error_code' => 'web_search_exception'
8878 - ];
8879 - }
8880 -}
8881 -
8882 -/**
8883 - * Handle non-streaming web search response
8884 - */
8885 -private function mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data) {
8886 - $request_body['stream'] = false;
8887 -
8888 - $response = $this->mxchat_provider_call_with_retry('https://api.openai.com/v1/responses', array(
8889 - 'headers' => array(
8890 - 'Authorization' => 'Bearer ' . $api_key,
8891 - 'Content-Type' => 'application/json'
8892 - ),
8893 - 'body' => json_encode($request_body),
8894 - 'timeout' => 90
8895 - ), 'openai');
8896 -
8897 - if (is_wp_error($response)) {
8898 - //error_log("MXCHAT WEB SEARCH ERROR: WP Error: " . $response->get_error_message());
8899 - return [
8900 - 'error' => esc_html__('Failed to connect to OpenAI web search API', 'mxchat'),
8901 - 'error_code' => 'web_search_connection_error'
8902 - ];
8903 - }
8904 -
8905 - $response_code = wp_remote_retrieve_response_code($response);
8906 - $response_body = wp_remote_retrieve_body($response);
8907 -
8908 - //error_log("MXCHAT WEB SEARCH: Response code: " . $response_code);
8909 - //error_log("MXCHAT WEB SEARCH: Response body (first 2000): " . substr($response_body, 0, 2000));
8910 -
8911 - if ($response_code !== 200) {
8912 - $error_data = json_decode($response_body, true);
8913 - $error_message = $error_data['error']['message'] ?? 'Unknown API error';
8914 - return [
8915 - 'error' => sprintf(esc_html__('OpenAI API error: %s', 'mxchat'), esc_html($error_message)),
8916 - 'error_code' => 'web_search_api_error'
8917 - ];
8918 - }
8919 -
8920 - $result = json_decode($response_body, true);
8921 -
8922 - if (json_last_error() !== JSON_ERROR_NONE) {
8923 - return [
8924 - 'error' => esc_html__('Invalid response from OpenAI', 'mxchat'),
8925 - 'error_code' => 'web_search_json_error'
8926 - ];
8927 - }
8928 -
8929 - // Extract the response text and citations from Responses API format
8930 - $output_text = '';
8931 - $citations = [];
8932 -
8933 - if (isset($result['output'])) {
8934 - foreach ($result['output'] as $output_item) {
8935 - if ($output_item['type'] === 'message' && isset($output_item['content'])) {
8936 - foreach ($output_item['content'] as $content_item) {
8937 - if ($content_item['type'] === 'output_text') {
8938 - $output_text .= $content_item['text'];
8939 -
8940 - // Extract citations/annotations
8941 - if (isset($content_item['annotations'])) {
8942 - foreach ($content_item['annotations'] as $annotation) {
8943 - if ($annotation['type'] === 'url_citation') {
8944 - $citations[] = [
8945 - 'url' => $annotation['url'],
8946 - 'title' => $annotation['title'] ?? ''
8947 - ];
8948 - }
8949 - }
8950 - }
8951 - }
8952 - }
8953 - }
8954 - }
8955 - }
8956 -
8957 - // If we have citations, append them to the response
8958 - if (!empty($citations)) {
8959 - $output_text .= "\n\n**Sources:**\n";
8960 - $seen_urls = [];
8961 - foreach ($citations as $citation) {
8962 - if (!in_array($citation['url'], $seen_urls)) {
8963 - $seen_urls[] = $citation['url'];
8964 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
8965 - $output_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
8966 - }
8967 - }
8968 - }
8969 -
8970 - // Transcript save is handled by the main handler (mxchat_handle_chat_request)
8971 - // which includes rag_context for the "sources" link in transcripts.
8972 -
8973 - return $output_text;
8974 -}
8975 -
8976 -/**
8977 - * Handle streaming web search response using Responses API
8978 - */
8979 -private function mxchat_web_search_streaming_response($request_body, $api_key, $session_id, $testing_data) {
8980 - $request_body['stream'] = true;
8981 -
8982 - // Check if we can stream
8983 - if (headers_sent() || !function_exists('curl_init')) {
8984 - // Fallback to non-streaming
8985 - return $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data);
8986 - }
8987 -
8988 - // Setup streaming headers
8989 - $this->setup_streaming_headers();
8990 -
8991 - $ch = curl_init();
8992 - curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/responses');
8993 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
8994 - curl_setopt($ch, CURLOPT_POST, true);
8995 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request_body));
8996 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
8997 - 'Content-Type: application/json',
8998 - 'Authorization: Bearer ' . $api_key
8999 - ));
9000 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
9001 - curl_setopt($ch, CURLOPT_TIMEOUT, 120);
9002 -
9003 - $full_response = '';
9004 - $stream_started = false;
9005 - $buffer = '';
9006 - $citations = [];
9007 -
9008 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$citations, $testing_data) {
9009 - // Send testing data as first event if available
9010 - if (!$stream_started && $testing_data !== null) {
9011 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
9012 - flush();
9013 - $stream_started = true;
9014 - }
9015 -
9016 - $buffer .= $data;
9017 - $lines = explode("\n", $buffer);
9018 - $buffer = array_pop($lines);
9019 -
9020 - foreach ($lines as $line) {
9021 - if (trim($line) === '') continue;
9022 - if (strpos($line, 'data: ') !== 0) continue;
9023 -
9024 - $json_str = substr($line, 6);
9025 -
9026 - if (trim($json_str) === '[DONE]') {
9027 - // Append citations if we have any
9028 - if (!empty($citations)) {
9029 - $citation_text = "\n\n**Sources:**\n";
9030 - $seen_urls = [];
9031 - foreach ($citations as $citation) {
9032 - if (!in_array($citation['url'], $seen_urls)) {
9033 - $seen_urls[] = $citation['url'];
9034 - $title = !empty($citation['title']) ? $citation['title'] : $citation['url'];
9035 - $citation_text .= "- [" . $title . "](" . $citation['url'] . ")\n";
9036 - }
9037 - }
9038 - echo "data: " . json_encode(['content' => $citation_text]) . "\n\n";
9039 - $full_response .= $citation_text;
9040 - flush();
9041 - }
9042 - echo "data: [DONE]\n\n";
9043 - flush();
9044 - continue;
9045 - }
9046 -
9047 - $json = json_decode(trim($json_str), true);
9048 - if (!$json) continue;
9049 -
9050 - // Handle Responses API streaming events
9051 - // The format is different from Chat Completions
9052 - if (isset($json['type'])) {
9053 - switch ($json['type']) {
9054 - case 'response.output_text.delta':
9055 - // Text content delta
9056 - if (isset($json['delta'])) {
9057 - $content = $json['delta'];
9058 - $full_response .= $content;
9059 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
9060 - flush();
9061 - }
9062 - break;
9063 -
9064 - case 'response.output_item.done':
9065 - // Check for citations in completed items
9066 - if (isset($json['item']['content'])) {
9067 - foreach ($json['item']['content'] as $content_item) {
9068 - if (isset($content_item['annotations'])) {
9069 - foreach ($content_item['annotations'] as $annotation) {
9070 - if ($annotation['type'] === 'url_citation') {
9071 - $citations[] = [
9072 - 'url' => $annotation['url'],
9073 - 'title' => $annotation['title'] ?? ''
9074 - ];
9075 - }
9076 - }
9077 - }
9078 - }
9079 - }
9080 - break;
9081 - }
9082 - }
9083 - }
9084 -
9085 - return strlen($data);
9086 - });
9087 -
9088 - $response = curl_exec($ch);
9089 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
9090 -
9091 - if (curl_errno($ch) || $http_code !== 200) {
9092 - $curl_error = curl_error($ch);
9093 - curl_close($ch);
9094 -
9095 - //error_log("MXCHAT WEB SEARCH STREAM ERROR: HTTP $http_code, cURL error: $curl_error");
9096 -
9097 - return $this->mxchat_stream_emit_fallback(
9098 - 'web_search',
9099 - $this->mxchat_web_search_non_streaming_response($request_body, $api_key, $session_id, $testing_data),
9100 - $session_id,
9101 - $testing_data
9102 - );
9103 - }
9104 -
9105 - curl_close($ch);
9106 -
9107 - // Save the complete response with RAG context so the "sources" link
9108 - // appears in transcripts — mirrors the pattern used by Claude/OpenAI streaming.
9109 - if (!empty($full_response) && !empty($session_id)) {
9110 - $rag_context_for_storage = null;
9111 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
9112 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
9113 -
9114 - if ($has_rag_data || $has_action_data) {
9115 - $rag_context_for_storage = [];
9116 -
9117 - if ($has_rag_data) {
9118 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
9119 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
9120 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
9121 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
9122 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
9123 - }
9124 -
9125 - if ($has_action_data) {
9126 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
9127 - }
9128 - }
9129 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
9130 - }
9131 -
9132 - return true;
9133 -}
9134 -
9135 -private function mxchat_generate_response_claude_stream($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
9136 - // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
9137 - // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
9138 - if ($selected_model === 'claude-opus-4-20250514') { $selected_model = 'claude-opus-4-8'; }
9139 - elseif ($selected_model === 'claude-sonnet-4-20250514') { $selected_model = 'claude-sonnet-4-6'; }
9140 - try {
9141 - // Get bot ID from session or request
9142 - $bot_id = $this->get_current_bot_id($session_id);
9143 -
9144 - // Get system prompt instructions using centralized function
9145 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9146 4002 // Ensure conversation_history is an array
9147 4003 if (!is_array($conversation_history)) {
9148 4004 $conversation_history = array();
9149 4005 }
@@ -9175,9 +4031,9 @@
9175 4031 'content' => $relevant_content
9176 4032 ];
9177 4033
9178 4034 // Prepare the request body with stream: true
9179 - $payload = [
4035 + $body = json_encode([
9180 4036 'model' => $selected_model,
9181 4037 'messages' => $conversation_history,
9182 4038 'max_tokens' => 1000,
9183 4039 'temperature' => 0.8,
@@ -9182,11 +4038,9 @@
9182 4038 'max_tokens' => 1000,
9183 4039 'temperature' => 0.8,
9184 4040 'system' => $system_prompt_instructions,
9185 4041 'stream' => true
9186 - ];
9187 - if ($this->mxchat_claude_omits_temperature($selected_model)) { unset($payload['temperature']); }
9188 - $body = json_encode($payload);
4042 + ]);
9189 4043
9190 4044 // Check if we can actually stream (headers not sent, etc.)
9191 4045 if (headers_sent() || !function_exists('curl_init')) {
9192 4046 // Fallback to regular response with testing data
@@ -9194,17 +4048,11 @@
9194 4048 $regular_response = $this->mxchat_generate_response_claude(
9195 4049 $selected_model,
9196 4050 $claude_api_key,
9197 4051 array_slice($conversation_history, 0, -1), // Remove the added content
9198 - $relevant_content,
9199 - $session_id
4052 + $relevant_content
9200 4053 );
9201 4054
9202 - // Save bot response to transcript
9203 - if (!empty($regular_response) && !empty($session_id)) {
9204 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
9205 - }
9206 -
9207 4055 // Return as JSON with testing data
9208 4056 $response_data = [
9209 4057 'text' => $regular_response,
9210 4058 'html' => '',
@@ -9223,203 +4071,168 @@
9223 4071 echo json_encode($response_data);
9224 4072 return true; // Indicate we handled the response
9225 4073 }
9226 4074
9227 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
4075 + // Use cURL for streaming support
4076 + $ch = curl_init();
4077 + curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
4078 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4079 + curl_setopt($ch, CURLOPT_POST, true);
4080 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4081 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4082 + 'Content-Type: application/json',
4083 + 'x-api-key: ' . $claude_api_key,
4084 + 'anthropic-version: 2023-06-01'
4085 + ));
4086 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4087 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
9228 4088
9229 - $captured_status_code = 0;
9230 - $captured_body_pre_stream = '';
9231 - $full_response = '';
4089 + $full_response = ''; // Accumulate full response for saving
9232 4090 $stream_started = false;
9233 - $buffer = '';
9234 - $errno = 0;
9235 - $http_code = 0;
9236 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
9237 - $backoff_ms = array(0, 750, 2000);
9238 4091
9239 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
9240 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
9241 - usleep($backoff_ms[$attempt] * 1000);
4092 + // Buffer control for real-time streaming
4093 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4094 + // Send testing data as the first event if available
4095 + if (!$stream_started && $testing_data !== null) {
4096 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4097 + flush();
4098 + $stream_started = true;
4099 + //error_log("MxChat Testing: Sent testing data in Claude stream");
9242 4100 }
4101 +
4102 + // Process each chunk of data
4103 + $lines = explode("\n", $data);
9243 4104
9244 - $captured_status_code = 0;
9245 - $captured_body_pre_stream = '';
9246 - $full_response = '';
9247 - $stream_started = false;
9248 - $buffer = '';
9249 -
9250 - $ch = curl_init();
9251 - curl_setopt($ch, CURLOPT_URL, 'https://api.anthropic.com/v1/messages');
9252 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
9253 - curl_setopt($ch, CURLOPT_POST, true);
9254 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
9255 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
9256 - 'Content-Type: application/json',
9257 - 'x-api-key: ' . $claude_api_key,
9258 - 'anthropic-version: 2023-06-01'
9259 - ));
9260 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
9261 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
9262 -
9263 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
9264 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
9265 - $captured_status_code = (int) $m[1];
4105 + foreach ($lines as $line) {
4106 + if (trim($line) === '') {
4107 + continue;
9266 4108 }
9267 - return strlen($header);
9268 - });
9269 4109
9270 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
9271 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
9272 - $captured_body_pre_stream .= $data;
9273 - return strlen($data);
4110 + // Claude uses event: and data: format
4111 + if (strpos($line, 'event: ') === 0) {
4112 + // Store the event type for the next data line
4113 + continue;
9274 4114 }
9275 4115
9276 - if (!$this->streaming_headers_sent) {
9277 - $this->setup_streaming_headers();
9278 - }
4116 + if (strpos($line, 'data: ') === 0) {
4117 + $json_str = substr($line, 6); // Remove 'data: ' prefix
9279 4118
9280 - if (!$stream_started && $testing_data !== null) {
9281 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
9282 - flush();
9283 - $stream_started = true;
9284 - }
9285 -
9286 - $buffer .= $data;
9287 - $lines = explode("\n", $buffer);
9288 - $buffer = array_pop($lines);
9289 -
9290 - foreach ($lines as $line) {
9291 - if (trim($line) === '') {
4119 + $json = json_decode($json_str, true);
4120 + if (json_last_error() !== JSON_ERROR_NONE) {
9292 4121 continue;
9293 4122 }
9294 4123
9295 - if (strpos($line, 'event: ') === 0) {
9296 - continue;
9297 - }
4124 + // Handle different event types
4125 + if (isset($json['type'])) {
4126 + switch ($json['type']) {
4127 + case 'content_block_delta':
4128 + if (isset($json['delta']['text'])) {
4129 + $content = $json['delta']['text'];
4130 + $full_response .= $content; // Accumulate
4131 + // Send as SSE format compatible with your frontend
4132 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
4133 + flush();
4134 + }
4135 + break;
9298 4136
9299 - if (strpos($line, 'data: ') === 0) {
9300 - $json_str = substr($line, 6);
4137 + case 'message_stop':
4138 + echo "data: [DONE]\n\n";
4139 + flush();
4140 + break;
9301 4141
9302 - $json = json_decode(trim($json_str), true);
9303 - if (json_last_error() !== JSON_ERROR_NONE) {
9304 - continue;
4142 + case 'error':
4143 + echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
4144 + flush();
4145 + break;
9305 4146 }
9306 -
9307 - if (isset($json['type'])) {
9308 - switch ($json['type']) {
9309 - case 'content_block_delta':
9310 - if (isset($json['delta']['text'])) {
9311 - $content = $json['delta']['text'];
9312 - $full_response .= $content;
9313 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
9314 - flush();
9315 - }
9316 - break;
9317 -
9318 - case 'message_stop':
9319 - echo "data: [DONE]\n\n";
9320 - flush();
9321 - break;
9322 -
9323 - case 'error':
9324 - echo "data: " . json_encode(['error' => $json['error']['message'] ?? 'Unknown error']) . "\n\n";
9325 - flush();
9326 - break;
9327 - }
9328 - }
9329 4147 }
9330 4148 }
9331 -
9332 - return strlen($data);
9333 - });
9334 -
9335 - $response = curl_exec($ch);
9336 - $errno = curl_errno($ch);
9337 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
9338 - curl_close($ch);
9339 -
9340 - if (!$errno && $http_code === 200) {
9341 - break;
9342 4149 }
9343 4150
9344 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'anthropic', $errno);
9345 - $can_retry = !$this->streaming_headers_sent
9346 - && ($attempt + 1) < $max_attempts
9347 - && $is_transient;
4151 + return strlen($data);
4152 + });
9348 4153
9349 - if (defined('WP_DEBUG') && WP_DEBUG) {
9350 - error_log(sprintf(
9351 - '[MxChat] claude_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
9352 - $attempt + 1, $max_attempts, $http_code, $errno,
9353 - $is_transient ? 'yes' : 'no',
9354 - $can_retry ? 'Retrying.' : 'Giving up.'
9355 - ));
9356 - }
4154 + $response = curl_exec($ch);
4155 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
9357 4156
9358 - if (!$can_retry) {
9359 - break;
9360 - }
4157 + if (curl_errno($ch)) {
4158 + curl_close($ch);
4159 + throw new Exception('cURL Error: ' . curl_error($ch));
9361 4160 }
9362 4161
9363 - if ($errno || $http_code !== 200) {
9364 - return $this->mxchat_stream_emit_fallback(
9365 - 'anthropic',
9366 - $this->mxchat_generate_response_claude($selected_model, $claude_api_key, array_slice($conversation_history, 0, -1), $relevant_content, $session_id),
9367 - $session_id,
9368 - $testing_data
4162 + curl_close($ch);
4163 +
4164 + if ($http_code !== 200) {
4165 + // Fallback to regular response
4166 + //error_log("MxChat: Claude streaming failed with HTTP $http_code, falling back");
4167 + $regular_response = $this->mxchat_generate_response_claude(
4168 + $selected_model,
4169 + $claude_api_key,
4170 + array_slice($conversation_history, 0, -1), // Remove the added content
4171 + $relevant_content
9369 4172 );
4173 +
4174 + $response_data = [
4175 + 'text' => $regular_response,
4176 + 'html' => '',
4177 + 'session_id' => $session_id
4178 + ];
4179 +
4180 + if ($testing_data !== null) {
4181 + $response_data['testing_data'] = $testing_data;
4182 + //error_log("MxChat Testing: Added testing data to Claude error fallback");
4183 + }
4184 +
4185 + header('Content-Type: application/json');
4186 + echo json_encode($response_data);
4187 + return true;
9370 4188 }
9371 4189
9372 4190 // Save the complete response to maintain chat persistence
9373 4191 if (!empty($full_response) && !empty($session_id)) {
9374 - // Prepare RAG context for streaming response
9375 - $rag_context_for_storage = null;
9376 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
9377 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
9378 -
9379 - if ($has_rag_data || $has_action_data) {
9380 - $rag_context_for_storage = [];
9381 -
9382 - if ($has_rag_data) {
9383 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
9384 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
9385 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
9386 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
9387 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
9388 - }
9389 -
9390 - if ($has_action_data) {
9391 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
9392 - }
9393 - }
9394 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4192 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
9395 4193 }
9396 4194
9397 4195 return true; // Indicate streaming completed successfully
9398 4196
9399 4197 } catch (Exception $e) {
9400 - return $this->mxchat_stream_emit_fallback(
9401 - 'anthropic',
9402 - $this->mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id),
9403 - $session_id,
9404 - $testing_data
4198 + //error_log("MxChat Claude streaming exception: " . $e->getMessage());
4199 +
4200 + // Fallback to regular response on exception
4201 + $regular_response = $this->mxchat_generate_response_claude(
4202 + $selected_model,
4203 + $claude_api_key,
4204 + $conversation_history,
4205 + $relevant_content
9405 4206 );
4207 +
4208 + $response_data = [
4209 + 'text' => $regular_response,
4210 + 'html' => '',
4211 + 'session_id' => $session_id
4212 + ];
4213 +
4214 + if ($testing_data !== null) {
4215 + $response_data['testing_data'] = $testing_data;
4216 + //error_log("MxChat Testing: Added testing data to Claude exception fallback");
4217 + }
4218 +
4219 + header('Content-Type: application/json');
4220 + echo json_encode($response_data);
4221 + return true;
9406 4222 }
9407 4223 }
9408 -private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4224 +private function mxchat_generate_response_openai_stream($selected_model, $api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
9409 4225 try {
9410 - // Get bot ID from session or request
9411 - $bot_id = $this->get_current_bot_id($session_id);
4226 + // Get system prompt instructions from options
4227 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
9412 4228
9413 - // Get system prompt instructions using centralized function
9414 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9415 -
9416 4229 // Ensure conversation_history is an array
9417 4230 if (!is_array($conversation_history)) {
9418 4231 $conversation_history = array();
9419 4232 }
9420 4233
9421 - // Format conversation history for X.AI (same as OpenAI format)
4234 + // Format conversation history for OpenAI
9422 4235 $formatted_conversation = array();
9423 4236
9424 4237 $formatted_conversation[] = array(
9425 4238 'role' => 'system',
@@ -9444,22 +4257,16 @@
9444 4257
9445 4258 // Check if we can actually stream
9446 4259 if (headers_sent() || !function_exists('curl_init')) {
9447 4260 // Fallback to regular response with testing data
9448 - //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
9449 - $regular_response = $this->mxchat_generate_response_xai(
4261 + //error_log("MxChat: OpenAI streaming not possible, falling back to regular response");
4262 + $regular_response = $this->mxchat_generate_response_openai(
9450 4263 $selected_model,
9451 - $xai_api_key,
4264 + $api_key,
9452 4265 $conversation_history,
9453 - $relevant_content,
9454 - $session_id
4266 + $relevant_content
9455 4267 );
9456 4268
9457 - // Save bot response to transcript
9458 - if (!empty($regular_response) && !empty($session_id)) {
9459 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
9460 - }
9461 -
9462 4269 $response_data = [
9463 4270 'text' => $regular_response,
9464 4271 'html' => '',
9465 4272 'session_id' => $session_id
@@ -9466,9 +4273,9 @@
9466 4273 ];
9467 4274
9468 4275 if ($testing_data !== null) {
9469 4276 $response_data['testing_data'] = $testing_data;
9470 - //error_log("MxChat Testing: Added testing data to X.AI fallback response");
4277 + //error_log("MxChat Testing: Added testing data to OpenAI fallback response");
9471 4278 }
9472 4279
9473 4280 header('Content-Type: application/json');
9474 4281 echo json_encode($response_data);
@@ -9482,185 +4289,141 @@
9482 4289 'temperature' => 0.8,
9483 4290 'stream' => true
9484 4291 ]);
9485 4292
9486 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
9487 -
9488 - $captured_status_code = 0;
9489 - $captured_body_pre_stream = '';
9490 - $full_response = '';
4293 + // Use cURL for streaming support
4294 + $ch = curl_init();
4295 + curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
4296 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4297 + curl_setopt($ch, CURLOPT_POST, true);
4298 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4299 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4300 + 'Content-Type: application/json',
4301 + 'Authorization: Bearer ' . $api_key
4302 + ));
4303 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4304 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4305 +
4306 + $full_response = ''; // Accumulate full response for saving
9491 4307 $stream_started = false;
9492 - $buffer = '';
9493 - $errno = 0;
9494 - $http_code = 0;
9495 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
9496 - $backoff_ms = array(0, 750, 2000);
9497 -
9498 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
9499 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
9500 - usleep($backoff_ms[$attempt] * 1000);
4308 +
4309 + // Buffer control for real-time streaming
4310 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4311 + // Send testing data as the first event if available
4312 + if (!$stream_started && $testing_data !== null) {
4313 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4314 + flush();
4315 + $stream_started = true;
4316 + //error_log("MxChat Testing: Sent testing data in OpenAI stream");
9501 4317 }
9502 -
9503 - $captured_status_code = 0;
9504 - $captured_body_pre_stream = '';
9505 - $full_response = '';
9506 - $stream_started = false;
9507 - $buffer = '';
9508 -
9509 - $ch = curl_init();
9510 - curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
9511 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
9512 - curl_setopt($ch, CURLOPT_POST, true);
9513 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
9514 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
9515 - 'Content-Type: application/json',
9516 - 'Authorization: Bearer ' . $xai_api_key
9517 - ));
9518 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
9519 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
9520 -
9521 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
9522 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
9523 - $captured_status_code = (int) $m[1];
4318 +
4319 + // Process each chunk of data
4320 + $lines = explode("\n", $data);
4321 +
4322 + foreach ($lines as $line) {
4323 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4324 + continue;
9524 4325 }
9525 - return strlen($header);
9526 - });
9527 -
9528 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
9529 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
9530 - $captured_body_pre_stream .= $data;
9531 - return strlen($data);
4326 +
4327 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4328 +
4329 + if ($json_str === '[DONE]') {
4330 + echo "data: [DONE]\n\n";
4331 + flush();
4332 + continue;
9532 4333 }
9533 -
9534 - if (!$this->streaming_headers_sent) {
9535 - $this->setup_streaming_headers();
9536 - }
9537 -
9538 - if (!$stream_started && $testing_data !== null) {
9539 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4334 +
4335 + $json = json_decode($json_str, true);
4336 + if (isset($json['choices'][0]['delta']['content'])) {
4337 + $content = $json['choices'][0]['delta']['content'];
4338 + $full_response .= $content; // Accumulate
4339 + // Send as SSE format
4340 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
9540 4341 flush();
9541 - $stream_started = true;
9542 4342 }
9543 -
9544 - $buffer .= $data;
9545 - $lines = explode("\n", $buffer);
9546 - $buffer = array_pop($lines);
9547 -
9548 - foreach ($lines as $line) {
9549 - if (trim($line) === '') {
9550 - continue;
9551 - }
9552 - if (strpos($line, 'data: ') !== 0) {
9553 - continue;
9554 - }
9555 -
9556 - $json_str = substr($line, 6);
9557 -
9558 - if (trim($json_str) === '[DONE]') {
9559 - echo "data: [DONE]\n\n";
9560 - flush();
9561 - continue;
9562 - }
9563 -
9564 - $json = json_decode(trim($json_str), true);
9565 - if ($json && isset($json['choices'][0]['delta']['content'])) {
9566 - $content = $json['choices'][0]['delta']['content'];
9567 - $full_response .= $content;
9568 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
9569 - flush();
9570 - }
9571 - }
9572 -
9573 - return strlen($data);
9574 - });
9575 -
9576 - $response = curl_exec($ch);
9577 - $errno = curl_errno($ch);
9578 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
4343 + }
4344 +
4345 + return strlen($data);
4346 + });
4347 +
4348 + $response = curl_exec($ch);
4349 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4350 +
4351 + if (curl_errno($ch) || $http_code !== 200) {
9579 4352 curl_close($ch);
9580 -
9581 - if (!$errno && $http_code === 200) {
9582 - break;
4353 +
4354 + // Fallback to regular response
4355 + //error_log("MxChat: OpenAI streaming failed, falling back");
4356 + $regular_response = $this->mxchat_generate_response_openai(
4357 + $selected_model,
4358 + $api_key,
4359 + $conversation_history,
4360 + $relevant_content
4361 + );
4362 +
4363 + $response_data = [
4364 + 'text' => $regular_response,
4365 + 'html' => '',
4366 + 'session_id' => $session_id
4367 + ];
4368 +
4369 + if ($testing_data !== null) {
4370 + $response_data['testing_data'] = $testing_data;
4371 + //error_log("MxChat Testing: Added testing data to OpenAI error fallback");
9583 4372 }
9584 -
9585 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'xai', $errno);
9586 - $can_retry = !$this->streaming_headers_sent
9587 - && ($attempt + 1) < $max_attempts
9588 - && $is_transient;
9589 -
9590 - if (defined('WP_DEBUG') && WP_DEBUG) {
9591 - error_log(sprintf(
9592 - '[MxChat] xai_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
9593 - $attempt + 1, $max_attempts, $http_code, $errno,
9594 - $is_transient ? 'yes' : 'no',
9595 - $can_retry ? 'Retrying.' : 'Giving up.'
9596 - ));
9597 - }
9598 -
9599 - if (!$can_retry) {
9600 - break;
9601 - }
4373 +
4374 + header('Content-Type: application/json');
4375 + echo json_encode($response_data);
4376 + return true;
9602 4377 }
9603 -
9604 - if ($errno || $http_code !== 200) {
9605 - return $this->mxchat_stream_emit_fallback(
9606 - 'xai',
9607 - $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id),
9608 - $session_id,
9609 - $testing_data
9610 - );
9611 - }
9612 -
4378 +
4379 + curl_close($ch);
4380 +
9613 4381 // Save the complete response to maintain chat persistence
9614 4382 if (!empty($full_response) && !empty($session_id)) {
9615 - // Prepare RAG context for streaming response
9616 - $rag_context_for_storage = null;
9617 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
9618 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
9619 -
9620 - if ($has_rag_data || $has_action_data) {
9621 - $rag_context_for_storage = [];
9622 -
9623 - if ($has_rag_data) {
9624 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
9625 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
9626 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
9627 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
9628 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
9629 - }
9630 -
9631 - if ($has_action_data) {
9632 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
9633 - }
9634 - }
9635 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4383 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
9636 4384 }
9637 -
4385 +
9638 4386 return true; // Indicate streaming completed successfully
9639 -
4387 +
9640 4388 } catch (Exception $e) {
9641 - return $this->mxchat_stream_emit_fallback(
9642 - 'xai',
9643 - $this->mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content),
9644 - $session_id,
9645 - $testing_data
4389 + //error_log("MxChat OpenAI streaming exception: " . $e->getMessage());
4390 +
4391 + // Fallback to regular response
4392 + $regular_response = $this->mxchat_generate_response_openai(
4393 + $selected_model,
4394 + $api_key,
4395 + $conversation_history,
4396 + $relevant_content
9646 4397 );
4398 +
4399 + $response_data = [
4400 + 'text' => $regular_response,
4401 + 'html' => '',
4402 + 'session_id' => $session_id
4403 + ];
4404 +
4405 + if ($testing_data !== null) {
4406 + $response_data['testing_data'] = $testing_data;
4407 + //error_log("MxChat Testing: Added testing data to OpenAI exception fallback");
4408 + }
4409 +
4410 + header('Content-Type: application/json');
4411 + echo json_encode($response_data);
4412 + return true;
9647 4413 }
9648 4414 }
9649 -private function mxchat_generate_response_deepseek_stream($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
4415 +private function mxchat_generate_response_xai_stream($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id, $testing_data = null) {
9650 4416 try {
9651 - // Get bot ID from session or request
9652 - $bot_id = $this->get_current_bot_id($session_id);
4417 + // Get system prompt instructions from options
4418 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
9653 4419
9654 - // Get system prompt instructions using centralized function
9655 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9656 -
9657 4420 // Ensure conversation_history is an array
9658 4421 if (!is_array($conversation_history)) {
9659 4422 $conversation_history = array();
9660 4423 }
9661 4424
9662 - // Format conversation history for DeepSeek
4425 + // Format conversation history for X.AI (same as OpenAI format)
9663 4426 $formatted_conversation = array();
9664 4427
9665 4428 $formatted_conversation[] = array(
9666 4429 'role' => 'system',
@@ -9685,22 +4448,16 @@
9685 4448
9686 4449 // Check if we can actually stream
9687 4450 if (headers_sent() || !function_exists('curl_init')) {
9688 4451 // Fallback to regular response with testing data
9689 - //error_log("MxChat: DeepSeek streaming not possible, falling back to regular response");
9690 - $regular_response = $this->mxchat_generate_response_deepseek(
4452 + //error_log("MxChat: X.AI streaming not possible, falling back to regular response");
4453 + $regular_response = $this->mxchat_generate_response_xai(
9691 4454 $selected_model,
9692 - $deepseek_api_key,
4455 + $xai_api_key,
9693 4456 $conversation_history,
9694 - $relevant_content,
9695 - $session_id
4457 + $relevant_content
9696 4458 );
9697 4459
9698 - // Save bot response to transcript
9699 - if (!empty($regular_response) && !empty($session_id)) {
9700 - $this->mxchat_save_chat_message($session_id, 'bot', $regular_response);
9701 - }
9702 -
9703 4460 $response_data = [
9704 4461 'text' => $regular_response,
9705 4462 'html' => '',
9706 4463 'session_id' => $session_id
@@ -9707,9 +4464,9 @@
9707 4464 ];
9708 4465
9709 4466 if ($testing_data !== null) {
9710 4467 $response_data['testing_data'] = $testing_data;
9711 - //error_log("MxChat Testing: Added testing data to DeepSeek fallback response");
4468 + //error_log("MxChat Testing: Added testing data to X.AI fallback response");
9712 4469 }
9713 4470
9714 4471 header('Content-Type: application/json');
9715 4472 echo json_encode($response_data);
@@ -9723,347 +4480,251 @@
9723 4480 'temperature' => 0.8,
9724 4481 'stream' => true
9725 4482 ]);
9726 4483
9727 - // V2 retry-on-initial-connect: setup_streaming_headers is lazy-fired in WRITEFUNCTION.
9728 -
9729 - $captured_status_code = 0;
9730 - $captured_body_pre_stream = '';
9731 - $full_response = '';
4484 + // Use cURL for streaming support
4485 + $ch = curl_init();
4486 + curl_setopt($ch, CURLOPT_URL, 'https://api.x.ai/v1/chat/completions');
4487 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
4488 + curl_setopt($ch, CURLOPT_POST, true);
4489 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
4490 + curl_setopt($ch, CURLOPT_HTTPHEADER, array(
4491 + 'Content-Type: application/json',
4492 + 'Authorization: Bearer ' . $xai_api_key
4493 + ));
4494 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
4495 + curl_setopt($ch, CURLOPT_TIMEOUT, 60);
4496 +
4497 + $full_response = ''; // Accumulate full response for saving
9732 4498 $stream_started = false;
9733 - $buffer = '';
9734 - $errno = 0;
9735 - $http_code = 0;
9736 - $max_attempts = $this->mxchat_retry_enabled() ? 3 : 1;
9737 - $backoff_ms = array(0, 750, 2000);
9738 -
9739 - for ($attempt = 0; $attempt < $max_attempts; $attempt++) {
9740 - if ($attempt > 0 && $backoff_ms[$attempt] > 0) {
9741 - usleep($backoff_ms[$attempt] * 1000);
4499 +
4500 + // Buffer control for real-time streaming
4501 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, $testing_data) {
4502 + // Send testing data as the first event if available
4503 + if (!$stream_started && $testing_data !== null) {
4504 + echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4505 + flush();
4506 + $stream_started = true;
4507 + //error_log("MxChat Testing: Sent testing data in X.AI stream");
9742 4508 }
9743 -
9744 - $captured_status_code = 0;
9745 - $captured_body_pre_stream = '';
9746 - $full_response = '';
9747 - $stream_started = false;
9748 - $buffer = '';
9749 -
9750 - $ch = curl_init();
9751 - curl_setopt($ch, CURLOPT_URL, 'https://api.deepseek.com/v1/chat/completions');
9752 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
9753 - curl_setopt($ch, CURLOPT_POST, true);
9754 - curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
9755 - curl_setopt($ch, CURLOPT_HTTPHEADER, array(
9756 - 'Content-Type: application/json',
9757 - 'Authorization: Bearer ' . $deepseek_api_key
9758 - ));
9759 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
9760 - curl_setopt($ch, CURLOPT_TIMEOUT, 60);
9761 -
9762 - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$captured_status_code) {
9763 - if ($captured_status_code === 0 && preg_match('#^HTTP/\S+\s+(\d+)\b#', $header, $m)) {
9764 - $captured_status_code = (int) $m[1];
4509 +
4510 + // Process each chunk of data
4511 + $lines = explode("\n", $data);
4512 +
4513 + foreach ($lines as $line) {
4514 + if (trim($line) === '' || strpos($line, 'data: ') !== 0) {
4515 + continue;
9765 4516 }
9766 - return strlen($header);
9767 - });
9768 -
9769 - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$full_response, &$stream_started, &$buffer, &$captured_status_code, &$captured_body_pre_stream, $testing_data) {
9770 - if ($captured_status_code !== 0 && $captured_status_code !== 200) {
9771 - $captured_body_pre_stream .= $data;
9772 - return strlen($data);
4517 +
4518 + $json_str = substr($line, 6); // Remove 'data: ' prefix
4519 +
4520 + if ($json_str === '[DONE]') {
4521 + echo "data: [DONE]\n\n";
4522 + flush();
4523 + continue;
9773 4524 }
9774 -
9775 - if (!$this->streaming_headers_sent) {
9776 - $this->setup_streaming_headers();
9777 - }
9778 -
9779 - if (!$stream_started && $testing_data !== null) {
9780 - echo "data: " . json_encode(['testing_data' => $testing_data]) . "\n\n";
4525 +
4526 + $json = json_decode($json_str, true);
4527 + if (isset($json['choices'][0]['delta']['content'])) {
4528 + $content = $json['choices'][0]['delta']['content'];
4529 + $full_response .= $content; // Accumulate
4530 + // Send as SSE format
4531 + echo "data: " . json_encode(['content' => $content]) . "\n\n";
9781 4532 flush();
9782 - $stream_started = true;
9783 4533 }
9784 -
9785 - $buffer .= $data;
9786 - $lines = explode("\n", $buffer);
9787 - $buffer = array_pop($lines);
9788 -
9789 - foreach ($lines as $line) {
9790 - if (trim($line) === '') {
9791 - continue;
9792 - }
9793 - if (strpos($line, 'data: ') !== 0) {
9794 - continue;
9795 - }
9796 -
9797 - $json_str = substr($line, 6);
9798 -
9799 - if (trim($json_str) === '[DONE]') {
9800 - echo "data: [DONE]\n\n";
9801 - flush();
9802 - continue;
9803 - }
9804 -
9805 - $json = json_decode(trim($json_str), true);
9806 - if ($json && isset($json['choices'][0]['delta']['content'])) {
9807 - $content = $json['choices'][0]['delta']['content'];
9808 - $full_response .= $content;
9809 - echo "data: " . json_encode(['content' => $content]) . "\n\n";
9810 - flush();
9811 - }
9812 - }
9813 -
9814 - return strlen($data);
9815 - });
9816 -
9817 - $response = curl_exec($ch);
9818 - $errno = curl_errno($ch);
9819 - $http_code = $captured_status_code !== 0 ? $captured_status_code : (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
4534 + }
4535 +
4536 + return strlen($data);
4537 + });
4538 +
4539 + $response = curl_exec($ch);
4540 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4541 +
4542 + if (curl_errno($ch) || $http_code !== 200) {
9820 4543 curl_close($ch);
9821 -
9822 - if (!$errno && $http_code === 200) {
9823 - break;
4544 +
4545 + // Fallback to regular response
4546 + //error_log("MxChat: X.AI streaming failed, falling back");
4547 + $regular_response = $this->mxchat_generate_response_xai(
4548 + $selected_model,
4549 + $xai_api_key,
4550 + $conversation_history,
4551 + $relevant_content
4552 + );
4553 +
4554 + $response_data = [
4555 + 'text' => $regular_response,
4556 + 'html' => '',
4557 + 'session_id' => $session_id
4558 + ];
4559 +
4560 + if ($testing_data !== null) {
4561 + $response_data['testing_data'] = $testing_data;
4562 + //error_log("MxChat Testing: Added testing data to X.AI error fallback");
9824 4563 }
9825 -
9826 - $is_transient = $this->mxchat_is_transient_provider_error_raw($http_code, $captured_body_pre_stream, 'openai', $errno);
9827 - $can_retry = !$this->streaming_headers_sent
9828 - && ($attempt + 1) < $max_attempts
9829 - && $is_transient;
9830 -
9831 - if (defined('WP_DEBUG') && WP_DEBUG) {
9832 - error_log(sprintf(
9833 - '[MxChat] deepseek_stream initial-connect failure (attempt=%d/%d, status=%d, errno=%d, transient=%s, %s).',
9834 - $attempt + 1, $max_attempts, $http_code, $errno,
9835 - $is_transient ? 'yes' : 'no',
9836 - $can_retry ? 'Retrying.' : 'Giving up.'
9837 - ));
9838 - }
9839 -
9840 - if (!$can_retry) {
9841 - break;
9842 - }
4564 +
4565 + header('Content-Type: application/json');
4566 + echo json_encode($response_data);
4567 + return true;
9843 4568 }
9844 -
9845 - if ($errno || $http_code !== 200) {
9846 - return $this->mxchat_stream_emit_fallback(
9847 - 'openai',
9848 - $this->mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id),
9849 - $session_id,
9850 - $testing_data
9851 - );
9852 - }
9853 -
4569 +
4570 + curl_close($ch);
4571 +
9854 4572 // Save the complete response to maintain chat persistence
9855 4573 if (!empty($full_response) && !empty($session_id)) {
9856 - // Prepare RAG context for streaming response
9857 - $rag_context_for_storage = null;
9858 - $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']);
9859 - $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis);
9860 -
9861 - if ($has_rag_data || $has_action_data) {
9862 - $rag_context_for_storage = [];
9863 -
9864 - if ($has_rag_data) {
9865 - $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches'];
9866 - $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? [];
9867 - $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35;
9868 - $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database';
9869 - $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0;
9870 - }
9871 -
9872 - if ($has_action_data) {
9873 - $rag_context_for_storage['action_analysis'] = $this->last_action_analysis;
9874 - }
9875 - }
9876 - $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
4574 + $this->mxchat_save_chat_message($session_id, 'bot', $full_response);
9877 4575 }
9878 -
4576 +
9879 4577 return true; // Indicate streaming completed successfully
9880 -
4578 +
9881 4579 } catch (Exception $e) {
9882 - return $this->mxchat_stream_emit_fallback(
9883 - 'openai',
9884 - $this->mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content),
9885 - $session_id,
9886 - $testing_data
4580 + //error_log("MxChat X.AI streaming exception: " . $e->getMessage());
4581 +
4582 + // Fallback to regular response
4583 + $regular_response = $this->mxchat_generate_response_xai(
4584 + $selected_model,
4585 + $xai_api_key,
4586 + $conversation_history,
4587 + $relevant_content
9887 4588 );
4589 +
4590 + $response_data = [
4591 + 'text' => $regular_response,
4592 + 'html' => '',
4593 + 'session_id' => $session_id
4594 + ];
4595 +
4596 + if ($testing_data !== null) {
4597 + $response_data['testing_data'] = $testing_data;
4598 + //error_log("MxChat Testing: Added testing data to X.AI exception fallback");
4599 + }
4600 +
4601 + header('Content-Type: application/json');
4602 + echo json_encode($response_data);
4603 + return true;
9888 4604 }
9889 4605 }
9890 4606
9891 4607
9892 -private function mxchat_generate_response_openrouter($selected_model, $openrouter_api_key, $conversation_history, $relevant_content, $session_id = '') {
9893 - try {
9894 - if (!is_array($conversation_history)) {
9895 - $conversation_history = array();
9896 - }
4608 +public function test_streaming_request() {
4609 + $options = get_option('mxchat_options', []);
4610 + $model = $options['model'] ?? 'gpt-4o';
9897 4611
9898 - $bot_id = $this->get_current_bot_id($session_id);
9899 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
9900 -
9901 - $formatted_conversation = array();
4612 + // Detect provider from model prefix
4613 + $provider = strtolower(explode('-', $model)[0]);
9902 4614
9903 - $formatted_conversation[] = array(
9904 - 'role' => 'system',
9905 - 'content' => $system_prompt_instructions . " " . $relevant_content
9906 - );
4615 + $sample_prompt = 'Hello! Can you stream this response back to me?';
4616 + $messages = [['role' => 'user', 'content' => $sample_prompt]];
4617 + $headers = [];
4618 + $body = [];
4619 + $url = '';
4620 + $api_key = '';
9907 4621
9908 - foreach ($conversation_history as $message) {
9909 - if (is_array($message) && isset($message['role']) && isset($message['content'])) {
9910 - $role = $message['role'];
4622 + switch ($provider) {
4623 + case 'gpt':
4624 + case 'o1':
4625 + $api_key = $options['api_key'] ?? '';
4626 + if (empty($api_key)) return '❌ Missing API key for OpenAI';
4627 + $url = 'https://api.openai.com/v1/chat/completions';
4628 + $headers = [
4629 + 'Content-Type: application/json',
4630 + 'Authorization: Bearer ' . $api_key
4631 + ];
4632 + $body = [
4633 + 'model' => $model,
4634 + 'messages' => $messages,
4635 + 'stream' => true
4636 + ];
4637 + break;
9911 4638
9912 - if ($role === 'bot' || $role === 'agent') {
9913 - $role = 'assistant';
9914 - }
9915 - if (!in_array($role, ['system', 'assistant', 'user'])) {
9916 - $role = 'user';
9917 - }
4639 + case 'claude':
4640 + $api_key = $options['claude_api_key'] ?? '';
4641 + if (empty($api_key)) return '❌ Missing API key for Claude';
4642 + $url = 'https://api.anthropic.com/v1/messages';
4643 + $headers = [
4644 + 'Content-Type: application/json',
4645 + 'x-api-key: ' . $api_key,
4646 + 'anthropic-version: 2023-06-01'
4647 + ];
4648 + $body = [
4649 + 'model' => $model,
4650 + 'messages' => $messages,
4651 + 'max_tokens' => 100,
4652 + 'stream' => true
4653 + ];
4654 + break;
9918 4655
9919 - $formatted_conversation[] = array(
9920 - 'role' => $role,
9921 - 'content' => $message['content']
9922 - );
9923 - }
9924 - }
4656 + case 'grok':
4657 + $api_key = $options['xai_api_key'] ?? '';
4658 + if (empty($api_key)) return '❌ Missing API key for X.AI';
4659 + $url = 'https://api.x.ai/v1/chat/completions';
4660 + $headers = [
4661 + 'Content-Type: application/json',
4662 + 'Authorization: Bearer ' . $api_key
4663 + ];
4664 + $body = [
4665 + 'model' => $model,
4666 + 'messages' => $messages,
4667 + 'stream' => true
4668 + ];
4669 + break;
9925 4670
9926 - $body = json_encode([
9927 - 'model' => $selected_model,
9928 - 'messages' => $formatted_conversation,
9929 - 'temperature' => 1,
9930 - ]);
9931 -
9932 - $args = [
9933 - 'body' => $body,
9934 - 'headers' => [
9935 - 'Content-Type' => 'application/json',
9936 - 'Authorization' => 'Bearer ' . $openrouter_api_key,
9937 - 'HTTP-Referer' => home_url(),
9938 - 'X-Title' => get_bloginfo('name'),
9939 - ],
9940 - 'timeout' => 60,
9941 - 'redirection' => 5,
9942 - 'blocking' => true,
9943 - 'httpversion' => '1.0',
9944 - 'sslverify' => true,
9945 - ];
9946 -
9947 - $response = $this->mxchat_provider_call_with_retry('https://openrouter.ai/api/v1/chat/completions', $args, 'openai');
9948 -
9949 - if (is_wp_error($response)) {
9950 - $error_message = $response->get_error_message();
9951 - return [
9952 - 'error' => $this->mxchat_friendly_chat_error(0, $error_message, 'OpenRouter'),
9953 - 'error_code' => 'openrouter_connection_error',
9954 - 'provider' => 'openrouter'
4671 + case 'deepseek':
4672 + $api_key = $options['deepseek_api_key'] ?? '';
4673 + if (empty($api_key)) return '❌ Missing API key for DeepSeek';
4674 + $url = 'https://api.deepseek.com/v1/chat/completions';
4675 + $headers = [
4676 + 'Content-Type: application/json',
4677 + 'Authorization: Bearer ' . $api_key
9955 4678 ];
9956 - }
4679 + $body = [
4680 + 'model' => $model,
4681 + 'messages' => $messages,
4682 + 'stream' => true
4683 + ];
4684 + break;
9957 4685
9958 - $status_code = wp_remote_retrieve_response_code($response);
9959 - if ($status_code !== 200) {
9960 - $response_body = wp_remote_retrieve_body($response);
9961 - $decoded_response = json_decode($response_body, true);
9962 -
9963 - $error_message = isset($decoded_response['error']['message'])
9964 - ? $decoded_response['error']['message']
9965 - : 'HTTP Error ' . $status_code;
9966 -
9967 - return [
9968 - 'error' => esc_html__('OpenRouter API error: ', 'mxchat') . esc_html($error_message),
9969 - 'error_code' => 'openrouter_api_error',
9970 - 'provider' => 'openrouter',
9971 - 'status_code' => $status_code
4686 + case 'gemini':
4687 + $api_key = $options['gemini_api_key'] ?? '';
4688 + if (empty($api_key)) return '❌ Missing API key for Gemini';
4689 + $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
4690 + $headers = ['Content-Type: application/json'];
4691 + $body = [
4692 + 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
4693 + 'generationConfig' => ['temperature' => 0.7]
9972 4694 ];
9973 - }
4695 + break;
9974 4696
9975 - $response_body = wp_remote_retrieve_body($response);
9976 - $decoded_response = json_decode($response_body, true);
9977 -
9978 - if (isset($decoded_response['choices'][0]['message']['content'])) {
9979 - return trim($decoded_response['choices'][0]['message']['content']);
9980 - } else {
9981 - return [
9982 - 'error' => esc_html__('Unexpected response format from OpenRouter.', 'mxchat'),
9983 - 'error_code' => 'openrouter_response_format_error',
9984 - 'provider' => 'openrouter'
9985 - ];
9986 - }
9987 - } catch (Exception $e) {
9988 - return [
9989 - 'error' => esc_html__('System error when processing OpenRouter request: ', 'mxchat') . esc_html($e->getMessage()),
9990 - 'error_code' => 'openrouter_exception',
9991 - 'provider' => 'openrouter'
9992 - ];
4697 + default:
4698 + return '❌ Unsupported provider: ' . $provider;
9993 4699 }
9994 -}
9995 4700
9996 -/**
9997 - * Build a chat-bubble-safe message for a non-200 provider (chat) error.
9998 - *
9999 - * Visitors must NEVER see raw API internals (model names, key/billing/quota
10000 - * text). Admins (manage_options) get an actionable hint — and, for the common
10001 - * "model not available on this key" case, a direct pointer to change the model
10002 - * (the site owner can fix it in one click). Anthropic returns model-access as a
10003 - * 4xx with a message like "Claude Fable 5 is not available. Please use Opus 4.8."
10004 - *
10005 - * Provider-agnostic by design (reusable for the xai/gemini/deepseek branches),
10006 - * but Anthropic is the confirmed, reproduced case wired up here (plan 1d3b0f).
10007 - *
10008 - * @param int $http_code HTTP status from the provider.
10009 - * @param string $error_message Raw provider error.message (may be empty).
10010 - * @param string $provider_label Human provider name, e.g. 'Anthropic'.
10011 - * @return string Message safe to render as a chat bubble.
10012 - */
10013 -private function mxchat_friendly_chat_error($http_code, $error_message, $provider_label = '') {
10014 - $raw = trim((string) $error_message);
4701 + // Do the actual streaming test
4702 + $ch = curl_init($url);
4703 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
4704 + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
4705 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
4706 + curl_setopt($ch, CURLOPT_TIMEOUT, 15);
4707 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
10015 4708
10016 - // Detect a model-access / availability problem the site owner can fix by
10017 - // choosing a different model. (Anthropic phrasing + the common API shapes.)
10018 - $low = strtolower($raw);
10019 - $is_model_access = (strpos($low, 'not available') !== false)
10020 - || (strpos($low, 'does not have access') !== false)
10021 - || (strpos($low, 'do not have access') !== false)
10022 - || (strpos($low, 'does not exist') !== false) // OpenAI: "model `x` does not exist or you do not have access"
10023 - || (strpos($low, 'model_not_found') !== false)
10024 - || (strpos($low, 'not_found_error') !== false)
10025 - || (strpos($low, 'model not found') !== false) // xAI
10026 - || (strpos($low, 'not found') !== false) // Gemini: "models/x is not found for API version ..."
10027 - || (strpos($low, 'permission_denied') !== false) // Gemini gated model
10028 - || (strpos($low, 'permission denied') !== false);
4709 + $response = curl_exec($ch);
4710 + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
4711 + $error = curl_error($ch);
4712 + curl_close($ch);
10029 4713
10030 - if (current_user_can('manage_options')) {
10031 - if ($is_model_access) {
10032 - return $raw !== ''
10033 - ? sprintf(
10034 - /* translators: %s: raw provider error detail */
10035 - esc_html__('The selected AI model isn\'t available on your API key. Choose another model in MxChat → Settings. (Details: %s)', 'mxchat'),
10036 - $raw
10037 - )
10038 - : esc_html__('The selected AI model isn\'t available on your API key. Choose another model in MxChat → Settings.', 'mxchat');
10039 - }
10040 - return $raw !== ''
10041 - ? sprintf(
10042 - /* translators: 1: provider label, 2: raw provider error detail */
10043 - esc_html__('The AI provider (%1$s) returned an error: %2$s. Check your model and API key in MxChat → Settings.', 'mxchat'),
10044 - $provider_label !== '' ? $provider_label : esc_html__('AI', 'mxchat'),
10045 - $raw
10046 - )
10047 - : esc_html__('The AI provider returned an error. Check your model and API key in MxChat → Settings.', 'mxchat');
4714 + if ($error) return "❌ cURL error: $error";
4715 + if ($http_code !== 200) {
4716 + $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
4717 + return "❌ HTTP $http_code: $error_message";
10048 4718 }
10049 4719
10050 - // Visitors: friendly, generic, no internals leaked.
10051 - return esc_html__('Sorry, I\'m having trouble responding right now. Please try again in a moment.', 'mxchat');
4720 + return true;
10052 4721 }
10053 4722
10054 -private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content, $session_id = '') {
10055 - // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
10056 - // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
10057 - if ($selected_model === 'claude-opus-4-20250514') { $selected_model = 'claude-opus-4-8'; }
10058 - elseif ($selected_model === 'claude-sonnet-4-20250514') { $selected_model = 'claude-sonnet-4-6'; }
4723 +private function mxchat_generate_response_claude($selected_model, $claude_api_key, $conversation_history, $relevant_content) {
4724 + // Get system prompt instructions from options
4725 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
10059 4726
10060 - // Get bot ID from session or request
10061 - $bot_id = $this->get_current_bot_id($session_id);
10062 -
10063 - // Get system prompt instructions using centralized function
10064 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
10065 -
10066 4727 // Clean and validate conversation history
10067 4728 foreach ($conversation_history as &$message) {
10068 4729 // Convert bot and agent roles to assistant
10069 4730 if ($message['role'] === 'bot' || $message['role'] === 'agent') {
@@ -10090,17 +4751,15 @@
10090 4751 'content' => $relevant_content
10091 4752 ];
10092 4753
10093 4754 // Build request body
10094 - $payload = [
4755 + $body = json_encode([
10095 4756 'model' => $selected_model,
10096 4757 'max_tokens' => 1000,
10097 4758 'temperature' => 0.8,
10098 4759 'messages' => $conversation_history,
10099 4760 'system' => $system_prompt_instructions
10100 - ];
10101 - if ($this->mxchat_claude_omits_temperature($selected_model)) { unset($payload['temperature']); }
10102 - $body = json_encode($payload);
4761 + ]);
10103 4762
10104 4763 // Set up API request
10105 4764 $args = [
10106 4765 'body' => $body,
@@ -10116,9 +4775,9 @@
10116 4775 'sslverify' => true,
10117 4776 ];
10118 4777
10119 4778 // Make API request
10120 - $response = $this->mxchat_provider_call_with_retry('https://api.anthropic.com/v1/messages', $args, 'anthropic');
4779 + $response = wp_remote_post('https://api.anthropic.com/v1/messages', $args);
10121 4780
10122 4781 // Check for WordPress errors
10123 4782 if (is_wp_error($response)) {
10124 4783 //error_log("Claude API request error: " . $response->get_error_message());
@@ -10132,17 +4791,13 @@
10132 4791 //error_log("Claude API HTTP error: " . $http_code . " - " . $error_body);
10133 4792
10134 4793 // Try to extract error message from response
10135 4794 $error_data = json_decode($error_body, true);
10136 - $error_message = isset($error_data['error']['message']) ?
10137 - $error_data['error']['message'] :
4795 + $error_message = isset($error_data['error']['message']) ?
4796 + $error_data['error']['message'] :
10138 4797 "HTTP error " . $http_code;
10139 -
10140 - // Surface an admin-actionable message (and a model-change pointer for the
10141 - // model-access case) without leaking raw API internals to visitors. This
10142 - // is the single chokepoint for BOTH the non-streaming and streaming Claude
10143 - // paths (the stream's non-200 fallback re-enters this method). plan 1d3b0f.
10144 - return $this->mxchat_friendly_chat_error($http_code, $error_message, 'Anthropic');
4798 +
4799 + return "Sorry, the API returned an error: " . $error_message;
10145 4800 }
10146 4801
10147 4802 // Parse response
10148 4803 $response_body = json_decode(wp_remote_retrieve_body($response), true);
@@ -10152,17 +4807,14 @@
10152 4807 //error_log("Claude API JSON decode error: " . json_last_error_msg());
10153 4808 return "Sorry, there was an error processing the API response.";
10154 4809 }
10155 4810
10156 - // Extract and validate response content. claude-fable-5 prepends a
10157 - // thinking block to content even with no thinking param — take the first
10158 - // TEXT block rather than content[0].
10159 - if (isset($response_body['content']) && is_array($response_body['content'])) {
10160 - foreach ($response_body['content'] as $block) {
10161 - if (isset($block['type'], $block['text']) && $block['type'] === 'text') {
10162 - return trim($block['text']);
10163 - }
10164 - }
4811 + // Extract and validate response content
4812 + if (isset($response_body['content']) &&
4813 + is_array($response_body['content']) &&
4814 + !empty($response_body['content']) &&
4815 + isset($response_body['content'][0]['text'])) {
4816 + return trim($response_body['content'][0]['text']);
10165 4817 }
10166 4818
10167 4819 // Log unexpected response format
10168 4820 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
@@ -10167,9 +4819,9 @@
10167 4819 // Log unexpected response format
10168 4820 //error_log("Claude API unexpected response format: " . print_r($response_body, true));
10169 4821 return "Sorry, I received an unexpected response format from the API.";
10170 4822 }
10171 -private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content, $session_id = '') {
4823 +private function mxchat_generate_response_openai($selected_model, $api_key, $conversation_history, $relevant_content) {
10172 4824 try {
10173 4825 // Ensure conversation_history is an array
10174 4826 if (!is_array($conversation_history)) {
10175 4827 $conversation_history = array();
@@ -10174,16 +4826,11 @@
10174 4826 if (!is_array($conversation_history)) {
10175 4827 $conversation_history = array();
10176 4828 }
10177 4829
10178 - // Get bot ID from session or request. plan eb9c38: resolve the real bot
10179 - // from the session (was hardcoded '' → always default bot on multi-bot
10180 - // installs) and fix the undefined $session_id that fed get_system_instructions.
10181 - $bot_id = $this->get_current_bot_id($session_id);
4830 + // Get system prompt instructions from options
4831 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
10182 4832
10183 - // Get system prompt instructions using centralized function
10184 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
10185 -
10186 4833 // Create a new array for the formatted conversation
10187 4834 $formatted_conversation = array();
10188 4835
10189 4836 // Add system message first
@@ -10211,44 +4858,15 @@
10211 4858 );
10212 4859 }
10213 4860 }
10214 4861
10215 - // Check if this is a GPT-5 model (supports reasoning_effort parameter)
10216 - $is_gpt5_model = (
10217 - strpos($selected_model, 'gpt-5') === 0 ||
10218 - $selected_model === 'gpt-5.2' ||
10219 - $selected_model === 'gpt-5.1-2025-11-13' ||
10220 - $selected_model === 'gpt-5' ||
10221 - $selected_model === 'gpt-5-mini' ||
10222 - $selected_model === 'gpt-5-nano'
10223 - );
10224 -
10225 - // Build request body with optimal settings for fast responses
10226 - $request_body = [
4862 + $body = json_encode([
10227 4863 'model' => $selected_model,
10228 4864 'messages' => $formatted_conversation,
10229 - 'temperature' => 1,
4865 + 'temperature' => 0.8,
10230 4866 'stream' => false
10231 - ];
4867 + ]);
10232 4868
10233 - // Add reasoning_effort only for GPT-5 models that support it
10234 - // These chat models don't support reasoning_effort parameter
10235 - $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');
10236 - if ($is_gpt5_model && !in_array($selected_model, $no_reasoning_models, true)) {
10237 - // GPT-5.1 uses 'low' instead of 'minimal'
10238 - if ($selected_model === 'gpt-5.1-2025-11-13') {
10239 - $request_body['reasoning_effort'] = 'low';
10240 - } elseif ($selected_model === 'gpt-5.5') {
10241 - $request_body['reasoning_effort'] = 'none';
10242 - } elseif ($selected_model === 'gpt-5.4') {
10243 - $request_body['reasoning_effort'] = 'none';
10244 - } else {
10245 - $request_body['reasoning_effort'] = 'minimal';
10246 - }
10247 - }
10248 -
10249 - $body = json_encode($request_body);
10250 -
10251 4869 $args = [
10252 4870 'body' => $body,
10253 4871 'headers' => [
10254 4872 'Content-Type' => 'application/json',
@@ -10260,14 +4878,15 @@
10260 4878 'httpversion' => '1.0',
10261 4879 'sslverify' => true,
10262 4880 ];
10263 4881
10264 - $response = $this->mxchat_provider_call_with_retry('https://api.openai.com/v1/chat/completions', $args, 'openai');
4882 + $response = wp_remote_post('https://api.openai.com/v1/chat/completions', $args);
10265 4883
10266 4884 if (is_wp_error($response)) {
10267 4885 $error_message = $response->get_error_message();
4886 + //error_log('OpenAI API Error: ' . $error_message);
10268 4887 return [
10269 - 'error' => $this->mxchat_friendly_chat_error(0, $error_message, 'OpenAI'),
4888 + 'error' => esc_html__('Connection error when contacting OpenAI: ', 'mxchat') . esc_html($error_message),
10270 4889 'error_code' => 'openai_connection_error',
10271 4890 'provider' => 'openai'
10272 4891 ];
10273 4892 }
@@ -10284,8 +4903,10 @@
10284 4903 $error_type = isset($decoded_response['error']['type'])
10285 4904 ? $decoded_response['error']['type']
10286 4905 : 'unknown';
10287 4906
4907 + //error_log('OpenAI API HTTP Error: ' . $status_code . ' - ' . $error_message);
4908 +
10288 4909 // Handle specific error types
10289 4910 switch ($error_type) {
10290 4911 case 'invalid_request_error':
10291 4912 if (strpos($error_message, 'API key') !== false) {
@@ -10318,13 +4939,11 @@
10318 4939 'provider' => 'openai'
10319 4940 ];
10320 4941 }
10321 4942
10322 - // Generic error fallback only — the typed cases above already produce
10323 - // clean messages. Route the raw-tail generic case through the leak-safe
10324 - // helper so visitors never see provider internals. plan 5da59a.
4943 + // Generic error fallback
10325 4944 return [
10326 - 'error' => $this->mxchat_friendly_chat_error($status_code, $error_message, 'OpenAI'),
4945 + 'error' => esc_html__('OpenAI API error: ', 'mxchat') . esc_html($error_message),
10327 4946 'error_code' => 'openai_api_error',
10328 4947 'provider' => 'openai',
10329 4948 'status_code' => $status_code
10330 4949 ];
@@ -10335,8 +4954,9 @@
10335 4954
10336 4955 if (isset($decoded_response['choices'][0]['message']['content'])) {
10337 4956 return trim($decoded_response['choices'][0]['message']['content']);
10338 4957 } else {
4958 + //error_log('OpenAI API Response Format Error: ' . print_r($decoded_response, true));
10339 4959 return [
10340 4960 'error' => esc_html__('Unexpected response format from OpenAI.', 'mxchat'),
10341 4961 'error_code' => 'openai_response_format_error',
10342 4962 'provider' => 'openai'
@@ -10342,8 +4962,9 @@
10342 4962 'provider' => 'openai'
10343 4963 ];
10344 4964 }
10345 4965 } catch (Exception $e) {
4966 + //error_log('OpenAI Exception: ' . $e->getMessage());
10346 4967 return [
10347 4968 'error' => esc_html__('System error when processing OpenAI request: ', 'mxchat') . esc_html($e->getMessage()),
10348 4969 'error_code' => 'openai_exception',
10349 4970 'provider' => 'openai'
@@ -10349,17 +4970,13 @@
10349 4970 'provider' => 'openai'
10350 4971 ];
10351 4972 }
10352 4973 }
4974 +private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content) {
4975 + try {
4976 + // Get system prompt instructions from options
4977 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
10353 4978
10354 -private function mxchat_generate_response_xai($selected_model, $xai_api_key, $conversation_history, $relevant_content, $session_id = '') {
10355 - try {
10356 - // Get bot ID from session or request
10357 - $bot_id = $this->get_current_bot_id($session_id);
10358 -
10359 - // Get system prompt instructions using centralized function
10360 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
10361 -
10362 4979 // Add system prompt to relevant content
10363 4980 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
10364 4981
10365 4982 // Prepend system instructions to the conversation history
@@ -10408,9 +5025,9 @@
10408 5025 'sslverify' => true,
10409 5026 ];
10410 5027
10411 5028 // Make the API request
10412 - $response = $this->mxchat_provider_call_with_retry('https://api.x.ai/v1/chat/completions', $args, 'xai');
5029 + $response = wp_remote_post('https://api.x.ai/v1/chat/completions', $args);
10413 5030
10414 5031 // Process the response
10415 5032 if (is_wp_error($response)) {
10416 5033 $error_message = $response->get_error_message();
@@ -10415,9 +5032,9 @@
10415 5032 if (is_wp_error($response)) {
10416 5033 $error_message = $response->get_error_message();
10417 5034 //error_log('X.AI API Error: ' . $error_message);
10418 5035 return [
10419 - 'error' => $this->mxchat_friendly_chat_error(0, $error_message, 'X.AI'),
5036 + 'error' => esc_html__('Connection error when contacting X.AI: ', 'mxchat') . esc_html($error_message),
10420 5037 'error_code' => 'xai_connection_error',
10421 5038 'provider' => 'xai'
10422 5039 ];
10423 5040 }
@@ -10511,14 +5128,11 @@
10511 5128 'provider' => 'xai'
10512 5129 ];
10513 5130 }
10514 5131
10515 - // Generic error fallback. Route the user-facing text through the
10516 - // leak-safe helper (admins get an actionable hint, visitors a generic
10517 - // fallback) instead of echoing raw provider internals. Preserve the
10518 - // structured contract (error_code/provider/status_code) for logging. plan 5da59a.
5132 + // Generic error fallback with the actual error message
10519 5133 return [
10520 - 'error' => $this->mxchat_friendly_chat_error($status_code, $error_message, 'xAI'),
5134 + 'error' => esc_html__('X.AI API error: ', 'mxchat') . esc_html($error_message),
10521 5135 'error_code' => 'xai_api_error',
10522 5136 'provider' => 'xai',
10523 5137 'status_code' => $status_code
10524 5138 ];
@@ -10547,9 +5161,10 @@
10547 5161 }
10548 5162
10549 5163
10550 5164 }
10551 -private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content, $session_id = '') {
5165 +
5166 +private function mxchat_generate_response_deepseek($selected_model, $deepseek_api_key, $conversation_history, $relevant_content) {
10552 5167 try {
10553 5168 // Ensure conversation_history is an array
10554 5169 if (!is_array($conversation_history)) {
10555 5170 $conversation_history = array();
@@ -10554,14 +5169,11 @@
10554 5169 if (!is_array($conversation_history)) {
10555 5170 $conversation_history = array();
10556 5171 }
10557 5172
10558 - // Get bot ID from session or request
10559 - $bot_id = $this->get_current_bot_id($session_id);
10560 -
10561 - // Get system prompt instructions using centralized function
10562 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
10563 -
5173 + // Get system prompt instructions from options
5174 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5175 +
10564 5176 // Create a new array for the formatted conversation
10565 5177 $formatted_conversation = array();
10566 5178
10567 5179 // Add system message first
@@ -10609,15 +5221,15 @@
10609 5221 'httpversion' => '1.0',
10610 5222 'sslverify' => true,
10611 5223 ];
10612 5224
10613 - $response = $this->mxchat_provider_call_with_retry('https://api.deepseek.com/v1/chat/completions', $args, 'openai');
5225 + $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', $args);
10614 5226
10615 5227 if (is_wp_error($response)) {
10616 5228 $error_message = $response->get_error_message();
10617 5229 //error_log('DeepSeek API Error: ' . $error_message);
10618 5230 return [
10619 - 'error' => $this->mxchat_friendly_chat_error(0, $error_message, 'DeepSeek'),
5231 + 'error' => esc_html__('Connection error when contacting DeepSeek: ', 'mxchat') . esc_html($error_message),
10620 5232 'error_code' => 'deepseek_connection_error',
10621 5233 'provider' => 'deepseek'
10622 5234 ];
10623 5235 }
@@ -10681,11 +5293,11 @@
10681 5293 'provider' => 'deepseek'
10682 5294 ];
10683 5295 }
10684 5296
10685 - // Generic error fallback — leak-safe helper (see plan 5da59a / 1d3b0f).
5297 + // Generic error fallback
10686 5298 return [
10687 - 'error' => $this->mxchat_friendly_chat_error($status_code, $error_message, 'DeepSeek'),
5299 + 'error' => esc_html__('DeepSeek API error: ', 'mxchat') . esc_html($error_message),
10688 5300 'error_code' => 'deepseek_api_error',
10689 5301 'provider' => 'deepseek',
10690 5302 'status_code' => $status_code
10691 5303 ];
@@ -10712,20 +5324,13 @@
10712 5324 'provider' => 'deepseek'
10713 5325 ];
10714 5326 }
10715 5327 }
10716 -private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content, $session_id = '') {
10717 - // Read-time remap: gemini-3-pro-preview was shut down March 9, 2026.
10718 - // Auto-rescue existing installs whose saved model is the dead ID.
10719 - if ($selected_model === 'gemini-3-pro-preview') {
10720 - $selected_model = 'gemini-3.1-pro-preview';
10721 - }
10722 - // Get bot ID from session or request
10723 - $bot_id = $this->get_current_bot_id($session_id);
10724 -
10725 - // Get system prompt instructions using centralized function
10726 - $system_prompt_instructions = $this->get_system_instructions($bot_id, $session_id);
10727 -
5328 +
5329 +private function mxchat_generate_response_gemini($selected_model, $gemini_api_key, $conversation_history, $relevant_content) {
5330 + // Get system prompt instructions from options
5331 + $system_prompt_instructions = isset($this->options['system_prompt_instructions']) ? $this->options['system_prompt_instructions'] : '';
5332 +
10728 5333 // Add system prompt to relevant content
10729 5334 $content_with_instructions = $system_prompt_instructions . " " . $relevant_content;
10730 5335
10731 5336 // Format messages for Gemini API
@@ -10820,11 +5425,9 @@
10820 5425 ]
10821 5426 ]);
10822 5427
10823 5428 // Prepare the API endpoint
10824 - // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models
10825 - $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1';
10826 - $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
5429 + $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key;
10827 5430
10828 5431 // Set up the API request
10829 5432 $args = [
10830 5433 'body' => $body,
@@ -10838,31 +5441,22 @@
10838 5441 'sslverify' => true,
10839 5442 ];
10840 5443
10841 5444 // Make the API request
10842 - $response = $this->mxchat_provider_call_with_retry($api_endpoint, $args, 'gemini');
10843 -
5445 + $response = wp_remote_post($api_endpoint, $args);
5446 +
10844 5447 // Process the response
10845 5448 if (is_wp_error($response)) {
10846 - // plan b13282: route the transport-error string through the leak-safe helper
10847 - // (admin-actionable, generic for visitors) instead of echoing the raw WP HTTP
10848 - // error. http_code 0 = no HTTP response, so the helper uses the generic branch.
10849 - return $this->mxchat_friendly_chat_error(0, $response->get_error_message(), 'Gemini');
5449 + return "Sorry, there was an error processing your request: " . $response->get_error_message();
10850 5450 }
10851 5451
10852 5452 $response_body = json_decode(wp_remote_retrieve_body($response), true);
10853 5453
10854 - // Handle potential errors in the response. Gemini surfaces errors as a
10855 - // 200/non-200 body with an `error` envelope; route the user-facing text
10856 - // through the leak-safe helper (admin-actionable, no visitor leak) rather
10857 - // than echoing the raw provider message. plan 5da59a.
5454 + // Handle potential errors in the response
10858 5455 if (isset($response_body['error'])) {
10859 5456 //error_log('Gemini API Error: ' . json_encode($response_body['error']));
10860 - $gemini_error_message = isset($response_body['error']['message'])
10861 - ? $response_body['error']['message']
10862 - : 'Unknown error';
10863 - $gemini_http_code = wp_remote_retrieve_response_code($response);
10864 - return $this->mxchat_friendly_chat_error($gemini_http_code, $gemini_error_message, 'Gemini');
5457 + return "Sorry, there was an error with the Gemini API: " .
5458 + (isset($response_body['error']['message']) ? $response_body['error']['message'] : 'Unknown error');
10865 5459 }
10866 5460
10867 5461 // Extract the response text
10868 5462 if (isset($response_body['candidates'][0]['content']['parts'][0]['text'])) {
@@ -10873,139 +5467,9 @@
10873 5467 }
10874 5468 }
10875 5469
10876 5470
10877 -public function test_streaming_request() {
10878 - $options = get_option('mxchat_options', []);
10879 - $model = $options['model'] ?? 'gpt-5.1-chat-latest';
10880 5471
10881 - // Detect provider from model prefix
10882 - $provider = strtolower(explode('-', $model)[0]);
10883 -
10884 - $sample_prompt = 'Hello! Can you stream this response back to me?';
10885 - $messages = [['role' => 'user', 'content' => $sample_prompt]];
10886 - $headers = [];
10887 - $body = [];
10888 - $url = '';
10889 - $api_key = '';
10890 -
10891 - switch ($provider) {
10892 - case 'gpt':
10893 - case 'o1':
10894 - $api_key = $options['api_key'] ?? '';
10895 - if (empty($api_key)) return '❌ Missing API key for OpenAI';
10896 - $url = 'https://api.openai.com/v1/chat/completions';
10897 - $headers = [
10898 - 'Content-Type: application/json',
10899 - 'Authorization: Bearer ' . $api_key
10900 - ];
10901 - $body = [
10902 - 'model' => $model,
10903 - 'messages' => $messages,
10904 - 'stream' => true
10905 - ];
10906 - break;
10907 -
10908 - case 'claude':
10909 - $api_key = $options['claude_api_key'] ?? '';
10910 - if (empty($api_key)) return '❌ Missing API key for Claude';
10911 - $url = 'https://api.anthropic.com/v1/messages';
10912 - $headers = [
10913 - 'Content-Type: application/json',
10914 - 'x-api-key: ' . $api_key,
10915 - 'anthropic-version: 2023-06-01'
10916 - ];
10917 - $body = [
10918 - 'model' => $model,
10919 - 'messages' => $messages,
10920 - 'max_tokens' => 100,
10921 - 'stream' => true
10922 - ];
10923 - break;
10924 -
10925 - case 'grok':
10926 - $api_key = $options['xai_api_key'] ?? '';
10927 - if (empty($api_key)) return '❌ Missing API key for X.AI';
10928 - $url = 'https://api.x.ai/v1/chat/completions';
10929 - $headers = [
10930 - 'Content-Type: application/json',
10931 - 'Authorization: Bearer ' . $api_key
10932 - ];
10933 - $body = [
10934 - 'model' => $model,
10935 - 'messages' => $messages,
10936 - 'stream' => true
10937 - ];
10938 - break;
10939 -
10940 - case 'deepseek':
10941 - if (empty($deepseek_api_key)) {
10942 - $error_response = [
10943 - 'error' => esc_html__('DeepSeek API key is not configured', 'mxchat'),
10944 - 'error_code' => 'missing_deepseek_api_key'
10945 - ];
10946 - if ($testing_data !== null) {
10947 - $error_response['testing_data'] = $testing_data;
10948 - }
10949 - return $error_response;
10950 - }
10951 - if ($streaming) {
10952 - return $this->mxchat_generate_response_deepseek_stream(
10953 - $selected_model,
10954 - $deepseek_api_key,
10955 - $conversation_history,
10956 - $relevant_content,
10957 - $session_id,
10958 - $testing_data // Pass testing data
10959 - );
10960 - } else {
10961 - $response = $this->mxchat_generate_response_deepseek(
10962 - $selected_model,
10963 - $deepseek_api_key,
10964 - $conversation_history,
10965 - $relevant_content,
10966 - $session_id
10967 - );
10968 - }
10969 - break;
10970 -
10971 - case 'gemini':
10972 - $api_key = $options['gemini_api_key'] ?? '';
10973 - if (empty($api_key)) return '❌ Missing API key for Gemini';
10974 - $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':streamGenerateContent?key=' . $api_key;
10975 - $headers = ['Content-Type: application/json'];
10976 - $body = [
10977 - 'contents' => [['role' => 'user', 'parts' => [['text' => $sample_prompt]]]],
10978 - 'generationConfig' => ['temperature' => 0.7]
10979 - ];
10980 - break;
10981 -
10982 - default:
10983 - return '❌ Unsupported provider: ' . $provider;
10984 - }
10985 -
10986 - // Do the actual streaming test
10987 - $ch = curl_init($url);
10988 - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
10989 - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
10990 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
10991 - curl_setopt($ch, CURLOPT_TIMEOUT, 15);
10992 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
10993 -
10994 - $response = curl_exec($ch);
10995 - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
10996 - $error = curl_error($ch);
10997 - curl_close($ch);
10998 -
10999 - if ($error) return "❌ cURL error: $error";
11000 - if ($http_code !== 200) {
11001 - $error_message = json_decode($response, true)['error']['message'] ?? 'Unknown';
11002 - return "❌ HTTP $http_code: $error_message";
11003 - }
11004 -
11005 - return true;
11006 -}
11007 -
11008 5472 public function mxchat_dismiss_pre_chat_message() {
11009 5473 // Get and sanitize the user identifier
11010 5474 $user_id = $this->mxchat_get_user_identifier();
11011 5475 $user_id = sanitize_key($user_id);
@@ -11059,63 +5523,40 @@
11059 5523
11060 5524 return $dotProduct / ($normA * $normB);
11061 5525 }
11062 5526
11063 -
11064 5527 public function mxchat_enqueue_scripts_styles() {
11065 - // Fetch options from the database first to check loading strategy
11066 - $this->options = get_option('mxchat_options');
11067 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
11068 -
11069 - // Always enqueue CSS immediately
5528 + // Define version numbers for the styles and scripts
5529 + $chat_style_version = '2.3.6';
5530 + $chat_script_version = '2.3.6';
5531 + // Enqueue the script
5532 + wp_enqueue_script(
5533 + 'mxchat-chat-js',
5534 + plugin_dir_url(__FILE__) . '../js/chat-script.js',
5535 + array('jquery'),
5536 + $chat_script_version,
5537 + true
5538 + );
5539 + // Enqueue the CSS
11070 5540 wp_enqueue_style(
11071 5541 'mxchat-chat-css',
11072 5542 plugin_dir_url(__FILE__) . '../css/chat-style.css',
11073 5543 array(),
11074 - MXCHAT_VERSION
5544 + $chat_style_version
11075 5545 );
11076 -
11077 - // Handle script loading based on strategy
11078 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
11079 - // Enqueue the script normally
11080 - wp_enqueue_script(
11081 - 'mxchat-chat-js',
11082 - plugin_dir_url(__FILE__) . '../js/chat-script.js',
11083 - array('jquery'),
11084 - MXCHAT_VERSION,
11085 - true
11086 - );
11087 -
11088 - // Add defer attribute if strategy is 'defer'
11089 - if ($loading_strategy === 'defer') {
11090 - wp_script_add_data('mxchat-chat-js', 'strategy', 'defer');
11091 - }
11092 - } else {
11093 - // For delay or interaction-based loading, we'll use a custom loader
11094 - // Don't enqueue the main script - we'll load it dynamically
11095 - add_action('wp_footer', array($this, 'mxchat_output_delayed_script_loader'), 99);
11096 - }
11097 -
5546 + // Fetch options from the database
5547 + $this->options = get_option('mxchat_options');
11098 5548 $prompts_options = get_option('mxchat_prompts_options', array());
11099 -
11100 - // Check if AI theme is active - if so, skip inline colors in JavaScript
11101 - $theme_options = get_option('mxchat_theme_options', array());
11102 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
11103 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
11104 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
11105 -
5549 +
11106 5550 // Prepare settings for JavaScript
11107 5551 $style_settings = array(
11108 5552 'ajax_url' => admin_url('admin-ajax.php'),
11109 - // The chat-send nonce is now fetched per-request from /wp-json/mxchat/v1/nonce
11110 - // (plan-6a68c9) so it never sits in cached HTML. We still emit a nonce here
11111 - // as a one-shot fallback for the first interaction on a fresh page load
11112 - // (so the very first chat-send doesn't need to wait for a REST round-trip),
11113 - // but the widget refetches before each subsequent send.
11114 - 'nonce' => wp_create_nonce('mxchat_chat_send'),
11115 - 'rest_url' => esc_url_raw(trailingslashit(rest_url('mxchat/v1'))),
11116 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
5553 + 'nonce' => wp_create_nonce('mxchat_chat_nonce'),
5554 + 'model' => isset($this->options['model']) ? $this->options['model'] : 'gpt-4o',
5555 + 'enable_streaming_toggle' => isset($this->options['enable_streaming_toggle']) ? $this->options['enable_streaming_toggle'] : 'on',
5556 + 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off', // ADD THIS LINE
11117 5557 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
5558 + 'rate_limit_message' => $this->options['rate_limit_message'] ?? 'Rate limit exceeded. Please try again later.',
11118 5559 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
11119 5560 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
11120 5561 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
11121 5562 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
@@ -11130,152 +5571,20 @@
11130 5571 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
11131 5572 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
11132 5573 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
11133 5574 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
5575 + 'chat_toolbar_toggle' => $this->options['chat_toolbar_toggle'] ?? 'off',
11134 5576 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
11135 5577 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
11136 5578 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
11137 5579 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
11138 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off', // FIXED
11139 - 'initial_email_state' => null, // Also fixed this undefined variable
11140 - 'skip_email_check' => true,
11141 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
11142 - 'skip_inline_colors' => $skip_inline_colors,
11143 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
5580 + 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1'
11144 5581 );
11145 -
11146 - // Behavior gates + labels (model, streaming, rate-limit copy, toolbar,
11147 - // print/transcript, satisfaction rating) come from the shared
11148 - // dynamic-settings method so this inline payload and the first-open
11149 - // refresh endpoint can never drift (plan-32db95).
11150 - $style_settings = array_merge($style_settings, $this->get_dynamic_widget_settings());
11151 -
11152 - // For normal/defer loading, use wp_localize_script
11153 - // For delayed loading, we store settings in a transient to be output inline
11154 - if ($loading_strategy === 'default' || $loading_strategy === 'defer') {
11155 - wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
11156 - } else {
11157 - // Store settings for the delayed loader to use
11158 - set_transient('mxchat_delayed_settings_' . get_current_user_id(), $style_settings, 60);
11159 - }
5582 + // Pass the settings to the script
5583 + wp_localize_script('mxchat-chat-js', 'mxchatChat', $style_settings);
11160 5584 }
11161 5585
11162 -/**
11163 - * Output the delayed script loader for performance optimization
11164 - */
11165 -public function mxchat_output_delayed_script_loader() {
11166 - $this->options = get_option('mxchat_options');
11167 - $loading_strategy = isset($this->options['script_loading_strategy']) ? $this->options['script_loading_strategy'] : 'default';
11168 - $script_url = plugin_dir_url(__FILE__) . '../js/chat-script.js?ver=' . MXCHAT_VERSION;
11169 5586
11170 - // Get the stored settings
11171 - $prompts_options = get_option('mxchat_prompts_options', array());
11172 - $theme_options = get_option('mxchat_theme_options', array());
11173 - $ai_theme_active = !empty($theme_options['active_ai_theme_css']);
11174 - $has_bot_theme_assignments = !empty($theme_options['bot_theme_assignments']);
11175 - $skip_inline_colors = $ai_theme_active || $has_bot_theme_assignments;
11176 -
11177 - $style_settings = array(
11178 - 'ajax_url' => admin_url('admin-ajax.php'),
11179 - // Per-request nonce — see plan-6a68c9; widget fetches via /wp-json/mxchat/v1/nonce
11180 - // before each send. This inline value is a one-shot fallback for the first interaction.
11181 - 'nonce' => wp_create_nonce('mxchat_chat_send'),
11182 - 'rest_url' => esc_url_raw(trailingslashit(rest_url('mxchat/v1'))),
11183 - 'contextual_awareness_toggle' => isset($this->options['contextual_awareness_toggle']) ? $this->options['contextual_awareness_toggle'] : 'off',
11184 - 'link_target_toggle' => $this->options['link_target_toggle'] ?? 'off',
11185 - 'complianz_toggle' => isset($this->options['complianz_toggle']) && $this->options['complianz_toggle'] === 'on',
11186 - 'user_message_bg_color' => $this->options['user_message_bg_color'] ?? '#fff',
11187 - 'user_message_font_color' => $this->options['user_message_font_color'] ?? '#212121',
11188 - 'bot_message_bg_color' => $this->options['bot_message_bg_color'] ?? '#212121',
11189 - 'bot_message_font_color' => $this->options['bot_message_font_color'] ?? '#fff',
11190 - 'top_bar_bg_color' => $this->options['top_bar_bg_color'] ?? '#212121',
11191 - 'send_button_font_color' => $this->options['send_button_font_color'] ?? '#212121',
11192 - 'close_button_color' => $this->options['close_button_color'] ?? '#fff',
11193 - 'chatbot_background_color' => $this->options['chatbot_background_color'] ?? '#212121',
11194 - 'chatbot_bg_color' => $this->options['chatbot_bg_color'] ?? '#fff',
11195 - 'icon_color' => $this->options['icon_color'] ?? '#fff',
11196 - 'chat_input_font_color' => $this->options['chat_input_font_color'] ?? '#212121',
11197 - 'chat_persistence_toggle' => $this->options['chat_persistence_toggle'] ?? 'off',
11198 - 'appendWidgetToBody' => $this->options['append_to_body'] ?? 'off',
11199 - 'live_agent_message_bg_color' => $this->options['live_agent_message_bg_color'] ?? '#ffffff',
11200 - 'live_agent_message_font_color' => $this->options['live_agent_message_font_color'] ?? '#333333',
11201 - 'mode_indicator_bg_color' => $this->options['mode_indicator_bg_color'] ?? '#767676',
11202 - 'mode_indicator_font_color' => $this->options['mode_indicator_font_color'] ?? '#ffffff',
11203 - 'toolbar_icon_color' => $this->options['toolbar_icon_color'] ?? '#212121',
11204 - 'use_pinecone' => $prompts_options['mxchat_use_pinecone'] ?? '0',
11205 - 'email_collection_enabled' => $this->options['enable_email_block'] ?? 'off',
11206 - 'initial_email_state' => null,
11207 - 'skip_email_check' => true,
11208 - 'pinecone_enabled' => isset($prompts_options['mxchat_use_pinecone']) && $prompts_options['mxchat_use_pinecone'] === '1',
11209 - 'skip_inline_colors' => $skip_inline_colors,
11210 - 'bot_theme_assignments' => $theme_options['bot_theme_assignments'] ?? array(),
11211 - );
11212 -
11213 - // Behavior gates + labels (model, streaming, rate-limit copy, toolbar,
11214 - // print/transcript, satisfaction rating) come from the shared
11215 - // dynamic-settings method so this inline payload and the first-open
11216 - // refresh endpoint can never drift (plan-32db95).
11217 - $style_settings = array_merge($style_settings, $this->get_dynamic_widget_settings());
11218 -
11219 - // Determine delay time based on strategy
11220 - $delay_ms = 0;
11221 - switch ($loading_strategy) {
11222 - case 'delay_1s':
11223 - $delay_ms = 1000;
11224 - break;
11225 - case 'delay_3s':
11226 - $delay_ms = 3000;
11227 - break;
11228 - case 'delay_5s':
11229 - $delay_ms = 5000;
11230 - break;
11231 - }
11232 -
11233 - ?>
11234 - <script type="text/javascript">
11235 - (function() {
11236 - var mxchatLoaded = false;
11237 - var mxchatChat = <?php echo wp_json_encode($style_settings); ?>;
11238 - window.mxchatChat = mxchatChat;
11239 -
11240 - function loadMxChatScript() {
11241 - if (mxchatLoaded) return;
11242 - mxchatLoaded = true;
11243 -
11244 - function appendChatScript() {
11245 - var script = document.createElement('script');
11246 - script.src = <?php echo wp_json_encode($script_url); ?>;
11247 - script.type = 'text/javascript';
11248 - document.body.appendChild(script);
11249 - }
11250 -
11251 - if (typeof jQuery !== 'undefined') {
11252 - appendChatScript();
11253 - } else {
11254 - var jq = document.createElement('script');
11255 - jq.src = <?php echo wp_json_encode(includes_url('js/jquery/jquery.min.js')); ?>;
11256 - jq.onload = appendChatScript;
11257 - document.body.appendChild(jq);
11258 - }
11259 - }
11260 -
11261 - <?php if ($loading_strategy === 'on_interaction'): ?>
11262 - // Load on user interaction
11263 - var events = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
11264 - events.forEach(function(evt) {
11265 - window.addEventListener(evt, loadMxChatScript, {once: true, passive: true});
11266 - });
11267 - // Fallback: load after 8 seconds if no interaction
11268 - setTimeout(loadMxChatScript, 8000);
11269 - <?php else: ?>
11270 - // Load after specified delay
11271 - setTimeout(loadMxChatScript, <?php echo intval($delay_ms); ?>);
11272 - <?php endif; ?>
11273 - })();
11274 - </script>
11275 - <?php
11276 -}
11277 -
11278 5587 /**
11279 5588 * Setup the cron jobs for rate limits with guard against multiple calls
11280 5589 */
11281 5590 public function setup_rate_limit_cron_jobs() {
@@ -11291,9 +5600,9 @@
11291 5600
11292 5601 try {
11293 5602 // First, check if WordPress cron is disabled
11294 5603 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
11295 - //error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
5604 + error_log('MxChat: WordPress cron is disabled (DISABLE_WP_CRON = true), using fallback system');
11296 5605 $this->setup_fallback_rate_limit_system();
11297 5606 return;
11298 5607 }
11299 5608
@@ -11298,9 +5607,9 @@
11298 5607 }
11299 5608
11300 5609 // Check if cron is already scheduled - if so, don't mess with it
11301 5610 if (wp_next_scheduled('mxchat_reset_rate_limits')) {
11302 - //error_log('MxChat: Rate limit cron already scheduled, skipping setup');
5611 + error_log('MxChat: Rate limit cron already scheduled, skipping setup');
11303 5612 return;
11304 5613 }
11305 5614
11306 5615 // Clear any orphaned hooks (but don't loop indefinitely)
@@ -11328,16 +5637,16 @@
11328 5637 $initial_time = time() + 300; // Start in 5 minutes
11329 5638 $result = wp_schedule_event($initial_time, 'hourly', 'mxchat_reset_rate_limits');
11330 5639
11331 5640 if ($result === false) {
11332 - //error_log('MxChat: Failed to schedule cron, using fallback system');
5641 + error_log('MxChat: Failed to schedule cron, using fallback system');
11333 5642 $this->setup_fallback_rate_limit_system();
11334 5643 } else {
11335 - //error_log('MxChat: Successfully scheduled rate limit reset cron');
5644 + error_log('MxChat: Successfully scheduled rate limit reset cron');
11336 5645 }
11337 5646
11338 5647 } catch (Exception $e) {
11339 - //error_log('MxChat: Cron setup exception: ' . $e->getMessage());
5648 + error_log('MxChat: Cron setup exception: ' . $e->getMessage());
11340 5649 $this->setup_fallback_rate_limit_system();
11341 5650 }
11342 5651 }
11343 5652
@@ -11348,9 +5657,9 @@
11348 5657 try {
11349 5658 // Method 1: Try with current time instead of future time
11350 5659 $result1 = wp_schedule_event(time(), 'hourly', 'mxchat_reset_rate_limits');
11351 5660 if ($result1 !== false) {
11352 - //error_log('MxChat: Alternative method 1 (current time) succeeded');
5661 + error_log('MxChat: Alternative method 1 (current time) succeeded');
11353 5662 return true;
11354 5663 }
11355 5664
11356 5665 // Method 2: Try with a different interval
@@ -11355,9 +5664,9 @@
11355 5664
11356 5665 // Method 2: Try with a different interval
11357 5666 $result2 = wp_schedule_event($initial_time, 'daily', 'mxchat_reset_rate_limits');
11358 5667 if ($result2 !== false) {
11359 - //error_log('MxChat: Alternative method 2 (daily interval) succeeded');
5668 + error_log('MxChat: Alternative method 2 (daily interval) succeeded');
11360 5669 return true;
11361 5670 }
11362 5671
11363 5672 // Method 3: Try wp_schedule_single_event first, then recurring
@@ -11362,9 +5671,9 @@
11362 5671
11363 5672 // Method 3: Try wp_schedule_single_event first, then recurring
11364 5673 $result3 = wp_schedule_single_event($initial_time, 'mxchat_reset_rate_limits');
11365 5674 if ($result3 !== false) {
11366 - //error_log('MxChat: Alternative method 3 (single event) succeeded');
5675 + error_log('MxChat: Alternative method 3 (single event) succeeded');
11367 5676 // Schedule the next one manually in the handler
11368 5677 return true;
11369 5678 }
11370 5679
@@ -11370,9 +5679,9 @@
11370 5679
11371 5680 return false;
11372 5681
11373 5682 } catch (Exception $e) {
11374 - //error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
5683 + error_log('MxChat: Alternative cron scheduling exception: ' . $e->getMessage());
11375 5684 return false;
11376 5685 }
11377 5686 }
11378 5687
@@ -11388,9 +5697,9 @@
11388 5697
11389 5698 // Also set up a more frequent fallback check (every 4 hours)
11390 5699 update_option('mxchat_fallback_check_interval', 4 * 3600);
11391 5700
11392 - //error_log('MxChat: Fallback rate limit system activated');
5701 + error_log('MxChat: Fallback rate limit system activated');
11393 5702 }
11394 5703
11395 5704 /**
11396 5705 * Enhanced fallback check method
@@ -11405,9 +5714,9 @@
11405 5714 $next_check = get_option('mxchat_next_rate_limit_check', 0);
11406 5715 $check_interval = get_option('mxchat_fallback_check_interval', 3600);
11407 5716
11408 5717 if (time() >= $next_check) {
11409 - //error_log('MxChat: Running fallback rate limit cleanup');
5718 + error_log('MxChat: Running fallback rate limit cleanup');
11410 5719 $this->mxchat_reset_rate_limits();
11411 5720
11412 5721 // Schedule next check
11413 5722 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
@@ -11413,9 +5722,9 @@
11413 5722 update_option('mxchat_next_rate_limit_check', time() + $check_interval);
11414 5723 }
11415 5724 }
11416 5725 /**
11417 - * Enhanced rate limit check that includes fallback cleanup and bot-specific rate limits
5726 + * Enhanced rate limit check that includes fallback cleanup
11418 5727 */
11419 5728 public function check_rate_limit() {
11420 5729 // Check if we need to run fallback cleanup
11421 5730 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
@@ -11425,66 +5734,11 @@
11425 5734 $this->mxchat_reset_rate_limits();
11426 5735 update_option('mxchat_next_rate_limit_check', time() + 3600); // Next hour
11427 5736 }
11428 5737
11429 - // Get bot ID from current request context
11430 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
5738 + // Continue with your existing rate limit logic...
5739 + $all_options = get_option('mxchat_options', []);
11431 5740
11432 - // Get bot-specific options (includes rate limits if overridden)
11433 - $bot_options = $this->get_bot_options($bot_id);
11434 - $current_options = !empty($bot_options) ? $bot_options : $this->options;
11435 -
11436 - // Use bot-specific rate limits if available, otherwise fall back to default
11437 - $rate_limits_source = isset($current_options['rate_limits']) ? $current_options['rate_limits'] : get_option('mxchat_options', [])['rate_limits'] ?? [];
11438 -
11439 - // -------------------------------------------------------------------
11440 - // Whole-chatbot global cap (independent of role). Evaluated FIRST so
11441 - // it acts as a hard ceiling across all users + all roles. Default is
11442 - // 'unlimited' so existing installs are unchanged. Counter key drops
11443 - // both <role> and <user_id> segments — single pool per bot.
11444 - // -------------------------------------------------------------------
11445 - $global_cfg = isset($current_options['rate_limits_global']) && is_array($current_options['rate_limits_global'])
11446 - ? $current_options['rate_limits_global']
11447 - : (isset(get_option('mxchat_options', [])['rate_limits_global']) ? get_option('mxchat_options', [])['rate_limits_global'] : []);
11448 - $global_limit_raw = isset($global_cfg['limit']) ? (string) $global_cfg['limit'] : 'unlimited';
11449 - $global_timeframe = isset($global_cfg['timeframe']) ? (string) $global_cfg['timeframe'] : 'daily';
11450 - if ($global_limit_raw !== '' && $global_limit_raw !== 'unlimited' && (int) $global_limit_raw >= 1) {
11451 - $bot_id_for_global = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
11452 - $safe_bot_global = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id_for_global);
11453 - $global_option = 'mxchat_chat_limit_' . $safe_bot_global . '_global';
11454 - $global_data = get_option($global_option, ['count' => 0, 'timestamp' => time()]);
11455 - if ((int) $global_data['count'] === 0) {
11456 - $global_data['timestamp'] = time();
11457 - update_option($global_option, $global_data);
11458 - }
11459 - $now = time();
11460 - $ts = (int) $global_data['timestamp'];
11461 - $reset = false;
11462 - switch ($global_timeframe) {
11463 - case 'hourly': $reset = ($now - $ts) >= 3600; break;
11464 - case 'daily': $reset = ($now - $ts) >= 86400; break;
11465 - case 'weekly': $reset = ($now - $ts) >= 604800; break;
11466 - case 'monthly': $reset = ($now - $ts) >= 2592000; break;
11467 - }
11468 - if ($reset) {
11469 - $global_data = ['count' => 0, 'timestamp' => $now];
11470 - update_option($global_option, $global_data);
11471 - }
11472 - if ((int) $global_data['count'] >= (int) $global_limit_raw) {
11473 - $global_msg = !empty($global_cfg['message'])
11474 - ? $global_cfg['message']
11475 - : __('This chatbot has reached its message limit. Please try again later.', 'mxchat');
11476 - return [
11477 - 'error' => true,
11478 - 'message' => $this->process_rate_limit_message_html($global_msg),
11479 - ];
11480 - }
11481 - // Reserve the slot for this request. Per-role check below also increments
11482 - // its own counter — that is intentional, both ceilings apply independently.
11483 - $global_data['count']++;
11484 - update_option($global_option, $global_data);
11485 - }
11486 -
11487 5741 // Determine user role or if logged out
11488 5742 if (is_user_logged_in()) {
11489 5743 $user = wp_get_current_user();
11490 5744 $user_id = $user->ID;
@@ -11504,13 +5758,13 @@
11504 5758 $user_id = $this->get_client_ip();
11505 5759 }
11506 5760
11507 5761 // Check if rate limits are configured for this role
11508 - if (!isset($rate_limits_source[$role])) {
5762 + if (!isset($all_options['rate_limits'][$role])) {
11509 5763 return true; // No limit set for this role
11510 5764 }
11511 5765
11512 - $limit = $rate_limits_source[$role]['limit'];
5766 + $limit = $all_options['rate_limits'][$role]['limit'];
11513 5767
11514 5768 // If unlimited, return true immediately
11515 5769 if ($limit === 'unlimited') {
11516 5770 return true;
@@ -11515,16 +5769,13 @@
11515 5769 if ($limit === 'unlimited') {
11516 5770 return true;
11517 5771 }
11518 5772
11519 - // Get the option name for this user/role with safer naming (include bot_id for bot-specific limits)
5773 + // Get the option name for this user/role with safer naming
11520 5774 $safe_role = preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
11521 5775 $safe_user_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $user_id);
11522 - $safe_bot_id = preg_replace('/[^a-zA-Z0-9_]/', '_', $bot_id);
5776 + $option_name = 'mxchat_chat_limit_' . $safe_role . '_' . $safe_user_id;
11523 5777
11524 - // Include bot_id in option name so each bot has separate rate limits
11525 - $option_name = 'mxchat_chat_limit_' . $safe_bot_id . '_' . $safe_role . '_' . $safe_user_id;
11526 -
11527 5778 // Get the counter data
11528 5779 $limit_data = get_option($option_name, ['count' => 0, 'timestamp' => time()]);
11529 5780
11530 5781 // If first request or counter reset needed, set the initial timestamp
@@ -11533,10 +5784,10 @@
11533 5784 update_option($option_name, $limit_data);
11534 5785 }
11535 5786
11536 5787 // Get the timeframe
11537 - $timeframe = isset($rate_limits_source[$role]['timeframe']) ?
11538 - $rate_limits_source[$role]['timeframe'] : 'daily';
5788 + $timeframe = isset($all_options['rate_limits'][$role]['timeframe']) ?
5789 + $all_options['rate_limits'][$role]['timeframe'] : 'daily';
11539 5790
11540 5791 // Check if the counter needs to be reset based on timeframe
11541 5792 $current_time = time();
11542 5793 $timestamp = $limit_data['timestamp'];
@@ -11565,10 +5816,10 @@
11565 5816
11566 5817 // Check if user has exceeded their limit
11567 5818 if ($limit_data['count'] >= intval($limit)) {
11568 5819 // Get the custom message for this role
11569 - $message = !empty($rate_limits_source[$role]['message'])
11570 - ? $rate_limits_source[$role]['message']
5820 + $message = !empty($all_options['rate_limits'][$role]['message'])
5821 + ? $all_options['rate_limits'][$role]['message']
11571 5822 : __('Rate limit exceeded. Please try again later.', 'mxchat');
11572 5823
11573 5824 // Add timeframe information to the message if placeholders exist
11574 5825 $timeframe_label = '';
@@ -11640,9 +5891,9 @@
11640 5891
11641 5892 foreach ($option_names as $option_name) {
11642 5893 // Check processing time limit
11643 5894 if ((time() - $start_time) > $max_processing_time) {
11644 - //error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
5895 + error_log('MxChat: Rate limit reset timeout after processing ' . $processed_count . ' entries');
11645 5896 break;
11646 5897 }
11647 5898
11648 5899 // Parse the option name more safely
@@ -11706,12 +5957,12 @@
11706 5957
11707 5958 // Clean up any orphaned cache entries
11708 5959 wp_cache_delete('mxchat_all_chat_limits', 'options');
11709 5960
11710 - //error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
5961 + error_log("MxChat: Rate limit reset completed. Processed {$processed_count} entries.");
11711 5962
11712 5963 } catch (Exception $e) {
11713 - //error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
5964 + error_log('MxChat: Rate limit reset error: ' . $e->getMessage());
11714 5965 }
11715 5966 }
11716 5967
11717 5968
@@ -11858,11 +6109,8 @@
11858 6109
11859 6110 /**
11860 6111 * AJAX handler to get system information for testing panel
11861 6112 */
11862 -/**
11863 - * AJAX handler to get system information for testing panel
11864 - */
11865 6113 public function mxchat_get_system_info() {
11866 6114 // Verify nonce for security
11867 6115 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
11868 6116 wp_send_json_error(['message' => 'Invalid nonce']);
@@ -11880,24 +6128,10 @@
11880 6128 ? $this->options['system_prompt_instructions']
11881 6129 : 'No system prompt configured';
11882 6130
11883 6131 // Get selected model
11884 - $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-5.1-chat-latest';
6132 + $selected_model = isset($this->options['model']) ? $this->options['model'] : 'gpt-4o';
11885 6133
11886 - // Check if OpenRouter is being used
11887 - $is_openrouter = ($selected_model === 'openrouter');
11888 - $openrouter_model = '';
11889 -
11890 - if ($is_openrouter) {
11891 - // Get the actual OpenRouter model that's selected
11892 - $openrouter_model = isset($this->options['openrouter_selected_model'])
11893 - ? $this->options['openrouter_selected_model']
11894 - : 'No OpenRouter model selected';
11895 -
11896 - // Update selected_model display to show both
11897 - $selected_model = 'OpenRouter: ' . $openrouter_model;
11898 - }
11899 -
11900 6134 // Get API key status (just check if they exist, don't expose the keys)
11901 6135 $api_status = [];
11902 6136 $api_status['openai'] = !empty($this->options['api_key']);
11903 6137 $api_status['claude'] = !empty($this->options['claude_api_key']);
@@ -11903,15 +6137,12 @@
11903 6137 $api_status['claude'] = !empty($this->options['claude_api_key']);
11904 6138 $api_status['gemini'] = !empty($this->options['gemini_api_key']);
11905 6139 $api_status['xai'] = !empty($this->options['xai_api_key']);
11906 6140 $api_status['deepseek'] = !empty($this->options['deepseek_api_key']);
11907 - $api_status['openrouter'] = !empty($this->options['openrouter_api_key']);
11908 6141
11909 6142 wp_send_json_success([
11910 6143 'system_prompt' => $system_prompt,
11911 6144 'selected_model' => $selected_model,
11912 - 'is_openrouter' => $is_openrouter,
11913 - 'openrouter_model' => $openrouter_model,
11914 6145 'api_status' => $api_status
11915 6146 ]);
11916 6147 }
11917 6148
@@ -11930,12 +6161,12 @@
11930 6161 wp_send_json_error(['message' => 'Unauthorized']);
11931 6162 return;
11932 6163 }
11933 6164
11934 - // Get similarity threshold from main options (default 35%)
6165 + // Get similarity threshold from main options (default 75%)
11935 6166 $similarity_threshold = isset($this->options['similarity_threshold'])
11936 6167 ? ((int) $this->options['similarity_threshold']) / 100
11937 - : 0.35;
6168 + : 0.75;
11938 6169
11939 6170 wp_send_json_success([
11940 6171 'threshold' => $similarity_threshold,
11941 6172 'threshold_percentage' => ($similarity_threshold * 100) . '%'
@@ -11950,42 +6181,24 @@
11950 6181 if (!wp_verify_nonce($_POST['nonce'], 'mxchat_test_nonce')) {
11951 6182 wp_send_json_error(['message' => 'Invalid nonce']);
11952 6183 return;
11953 6184 }
11954 -
6185 +
11955 6186 // Only allow admin users
11956 6187 if (!current_user_can('administrator')) {
11957 6188 wp_send_json_error(['message' => 'Unauthorized']);
11958 6189 return;
11959 6190 }
11960 -
11961 - // Check OpenAI Vector Store first (takes priority)
11962 - $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array());
11963 - $use_vectorstore = (isset($vectorstore_options['mxchat_use_openai_vectorstore']) && $vectorstore_options['mxchat_use_openai_vectorstore'] === '1');
11964 -
11965 - if ($use_vectorstore) {
11966 - $vectorstore_ids = $vectorstore_options['mxchat_openai_vectorstore_ids'] ?? '';
11967 - $id_count = !empty($vectorstore_ids) ? count(array_filter(array_map('trim', explode(',', $vectorstore_ids)))) : 0;
11968 -
11969 - $kb_info = [
11970 - 'type' => 'OpenAI Vector Store',
11971 - 'status' => 'Active',
11972 - 'documents' => $id_count > 0 ? $id_count . ' vector store' . ($id_count > 1 ? 's' : '') . ' configured' : 'No vector stores configured'
11973 - ];
11974 -
11975 - wp_send_json_success($kb_info);
11976 - return;
11977 - }
11978 -
6191 +
11979 6192 // Check Pinecone vs WordPress
11980 6193 $addon_options = get_option('mxchat_pinecone_addon_options', array());
11981 6194 $use_pinecone = (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1');
11982 -
6195 +
11983 6196 $kb_info = [
11984 6197 'type' => $use_pinecone ? 'Pinecone' : 'WordPress Database',
11985 6198 'status' => 'Active'
11986 6199 ];
11987 -
6200 +
11988 6201 // Get document count
11989 6202 if ($use_pinecone) {
11990 6203 $kb_info['documents'] = 'Connected to Pinecone';
11991 6204 $kb_info['api_configured'] = !empty($addon_options['mxchat_pinecone_api_key']);
@@ -11995,9 +6208,9 @@
11995 6208 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
11996 6209 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
11997 6210 $kb_info['documents'] = $count ? $count . ' documents' : 'No documents';
11998 6211 }
11999 -
6212 +
12000 6213 wp_send_json_success($kb_info);
12001 6214 }
12002 6215
12003 6216 /**
@@ -12079,13 +6292,9 @@
12079 6292 // Clear any other session-specific transients
12080 6293 delete_transient("mxchat_waiting_for_pdf_url_{$session_id}");
12081 6294 delete_transient("mxchat_include_pdf_in_context_{$session_id}");
12082 6295 delete_transient("mxchat_include_word_in_context_{$session_id}");
12083 -
12084 - // Clear form addon state (pending forms and submitted forms)
12085 - delete_option("mxchat_pending_form_{$session_id}");
12086 - delete_option("mxchat_submitted_forms_{$session_id}");
12087 -
6296 +
12088 6297 //error_log("MxChat: Cleared all data for session: {$session_id}");
12089 6298 }
12090 6299
12091 6300 /**
@@ -12120,15 +6329,15 @@
12120 6329 $testing_data = [
12121 6330 'query' => $message,
12122 6331 'timestamp' => time(),
12123 6332 'top_matches' => [],
12124 - 'action_matches' => [] // Add action matches
6333 + 'action_matches' => [] // NEW: Add action matches
12125 6334 ];
12126 6335
12127 6336 // Get similarity threshold
12128 6337 $similarity_threshold = isset($this->options['similarity_threshold'])
12129 6338 ? ((int) $this->options['similarity_threshold']) / 100
12130 - : 0.35;
6339 + : 0.75;
12131 6340
12132 6341 $testing_data['similarity_threshold'] = $similarity_threshold;
12133 6342
12134 6343 // Use the real similarity analysis if available
@@ -12143,9 +6352,9 @@
12143 6352
12144 6353 $testing_data['knowledge_base_type'] = $use_pinecone ? 'Pinecone' : 'WordPress Database';
12145 6354 }
12146 6355
12147 - // Include action analysis if available
6356 + // NEW: Include action analysis if available
12148 6357 if (isset($this->last_action_analysis) && !empty($this->last_action_analysis)) {
12149 6358 $testing_data['action_matches'] = $this->last_action_analysis;
12150 6359
12151 6360 // Clear it after capturing to avoid stale data
@@ -12156,13 +6365,13 @@
12156 6365 }
12157 6366
12158 6367
12159 6368 /**
12160 - * Track URL clicks from chatbot responses
6369 + * NEW: Track URL clicks from chatbot responses
12161 6370 */
12162 6371 public function mxchat_track_url_click() {
12163 6372 // Verify nonce for security
12164 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
6373 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
12165 6374 wp_send_json_error(['message' => 'Invalid nonce']);
12166 6375 wp_die();
12167 6376 }
12168 6377
@@ -12195,9 +6404,9 @@
12195 6404 wp_die();
12196 6405 }
12197 6406
12198 6407 /**
12199 - * Get URL click analytics for a session
6408 + * NEW: Get URL click analytics for a session
12200 6409 */
12201 6410 public function mxchat_get_url_clicks($session_id) {
12202 6411 global $wpdb;
12203 6412 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
@@ -12209,13 +6418,13 @@
12209 6418
12210 6419 return $clicks;
12211 6420 }
12212 6421 /**
12213 - * Track the originating page where chat was started
6422 + * NEW: Track the originating page where chat was started
12214 6423 */
12215 6424 public function mxchat_track_originating_page() {
12216 6425 // Verify nonce
12217 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
6426 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_chat_nonce')) {
12218 6427 wp_send_json_error(['message' => 'Invalid nonce']);
12219 6428 wp_die();
12220 6429 }
12221 6430
@@ -12260,195 +6469,8 @@
12260 6469 wp_send_json_success(['message' => 'Originating page tracked']);
12261 6470 wp_die();
12262 6471 }
12263 6472
12264 -/**
12265 - * Validate and clean URLs from AI response
12266 - * Removes any URLs that aren't in the knowledge base
12267 - *
12268 - * @param string $response_text The AI-generated response
12269 - * @param array $valid_urls Array of URLs from the knowledge base
12270 - * @return string Cleaned response with invalid URLs removed/flagged
12271 - */
12272 -private function validate_and_clean_urls($response_text, $valid_urls) {
12273 - // DEBUG: Log what we're working with
12274 - //error_log("=== MxChat URL Validation Debug ===");
12275 - //error_log("Valid URLs count: " . count($valid_urls));
12276 - //error_log("Valid URLs: " . print_r($valid_urls, true));
12277 - //error_log("Response text length: " . strlen($response_text));
12278 - //error_log("Response text preview: " . substr($response_text, 0, 500));
12279 -
12280 - // If no valid URLs provided or empty response, return as-is
12281 - if (empty($valid_urls) || empty($response_text)) {
12282 - //error_log("Validation skipped - empty valid_urls or response");
12283 - return $response_text;
12284 - }
12285 -
12286 - // Extract all URLs from the AI response
12287 - // This regex matches http:// and https:// URLs
12288 - preg_match_all(
12289 - '#\bhttps?://[^\s<>"\')\]]+#i',
12290 - $response_text,
12291 - $matches
12292 - );
12293 -
12294 - // If no URLs found in response, return as-is
12295 - if (empty($matches[0])) {
12296 - //error_log("No URLs found in response");
12297 - return $response_text;
12298 - }
12299 -
12300 - $found_urls = $matches[0];
12301 - $cleaned_response = $response_text;
12302 - $removed_count = 0;
12303 -
12304 - // Normalize valid URLs for comparison (remove trailing slashes, fragments, etc.)
12305 - $normalized_valid_urls = array_map(function($url) {
12306 - // Remove trailing slash
12307 - $url = rtrim($url, '/');
12308 - // Remove URL fragments (#section)
12309 - $url = preg_replace('/#.*$/', '', $url);
12310 - // Remove trailing punctuation that might have been captured
12311 - $url = rtrim($url, '.,;:!?');
12312 - return $url;
12313 - }, $valid_urls);
12314 -
12315 - //error_log("Normalized valid URLs: " . print_r($normalized_valid_urls, true));
12316 -
12317 - foreach ($found_urls as $found_url) {
12318 - // Clean up the found URL (remove trailing punctuation that might have been captured)
12319 - $clean_found_url = rtrim($found_url, '.,;:!?)');
12320 -
12321 - // DEBUG: Log each URL being checked
12322 - //error_log("Checking found URL: " . $found_url);
12323 -
12324 - // Normalize for comparison
12325 - $normalized_found = rtrim($clean_found_url, '/');
12326 - $normalized_found = preg_replace('/#.*$/', '', $normalized_found);
12327 -
12328 - //error_log("Normalized found URL: " . $normalized_found);
12329 -
12330 - // Check if this URL exists in our valid URLs list
12331 - $is_valid = false;
12332 -
12333 - //error_log("Starting validation checks for: " . $normalized_found);
12334 -
12335 - // First, try exact match
12336 - if (in_array($normalized_found, $normalized_valid_urls)) {
12337 - $is_valid = true;
12338 - //error_log("EXACT MATCH FOUND");
12339 - } else {
12340 - //error_log("No exact match, checking variations...");
12341 - // If no exact match, check if it's a variation (with query params, etc.)
12342 - foreach ($normalized_valid_urls as $valid_url) {
12343 - //error_log(" Comparing against valid URL: " . $valid_url);
12344 -
12345 - // Check if the found URL starts with a valid URL (handles query params)
12346 - if (strpos($normalized_found, $valid_url) === 0) {
12347 - // Check what comes after the valid URL
12348 - $remainder = substr($normalized_found, strlen($valid_url));
12349 -
12350 - // Only valid if:
12351 - // 1. Exact match (remainder is empty)
12352 - // 2. Query params (starts with ?)
12353 - // 3. Fragment (starts with #)
12354 - if (empty($remainder) || $remainder[0] === '?' || $remainder[0] === '#') {
12355 - $is_valid = true;
12356 - //error_log(" MATCH: Found URL is valid variation of base URL");
12357 - break;
12358 - } else {
12359 - //error_log(" NOT A MATCH: Found URL extends path beyond valid URL (remainder: " . $remainder . ")");
12360 - }
12361 - }
12362 - // Also check the reverse (in case valid URL has query params)
12363 - if (strpos($valid_url, $normalized_found) === 0) {
12364 - $is_valid = true;
12365 - //error_log(" MATCH: Valid URL starts with found URL");
12366 - break;
12367 - }
12368 - }
12369 -
12370 - if (!$is_valid) {
12371 - //error_log("NO MATCH FOUND - URL should be removed");
12372 - }
12373 - }
12374 -
12375 - // If URL is not valid, remove it from the response
12376 - if (!$is_valid) {
12377 - // Log the removal for debugging
12378 - //error_log("MxChat: Removed hallucinated URL: " . $found_url);
12379 - //error_log("MxChat: Valid URLs were: " . implode(', ', array_slice($normalized_valid_urls, 0, 5)));
12380 -
12381 - $removed_count++;
12382 -
12383 - // Check if URL is part of a markdown link: [text](url)
12384 - $markdown_pattern = '/\[([^\]]+)\]\(' . preg_quote($found_url, '/') . '\)/';
12385 - if (preg_match($markdown_pattern, $cleaned_response)) {
12386 - //error_log("Found markdown link, removing but keeping text");
12387 - // Remove the markdown link but keep the text
12388 - $cleaned_response = preg_replace($markdown_pattern, '$1', $cleaned_response);
12389 - }
12390 - // Check if URL is part of an HTML link: <a href="url">text</a>
12391 - else if (preg_match('/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>(.*?)<\/a>/i', $cleaned_response, $link_match)) {
12392 - //error_log("Found HTML link, removing but keeping text");
12393 - // Remove the HTML link but keep the text
12394 - $link_text = $link_match[1];
12395 - $cleaned_response = preg_replace(
12396 - '/<a[^>]*href=["\']' . preg_quote($found_url, '/') . '["\'][^>]*>.*?<\/a>/i',
12397 - $link_text,
12398 - $cleaned_response
12399 - );
12400 - }
12401 - // Otherwise just remove the bare URL
12402 - else {
12403 - //error_log("Removing bare URL");
12404 - $cleaned_response = str_replace($found_url, '', $cleaned_response);
12405 - }
12406 - }
12407 - }
12408 -
12409 - // Log summary if any URLs were removed
12410 - if ($removed_count > 0) {
12411 - //error_log("MxChat: URL Validation Summary - Removed {$removed_count} hallucinated URL(s)");
12412 - } else {
12413 - //error_log("MxChat: URL Validation Summary - No URLs removed, all were valid");
12414 - }
12415 -
12416 - // Clean up any double spaces or awkward punctuation left behind
12417 - // IMPORTANT: Only collapse horizontal whitespace (spaces/tabs), preserve newlines for markdown formatting
12418 - $cleaned_response = preg_replace('/[^\S\n]+/', ' ', $cleaned_response); // Collapse spaces/tabs but NOT newlines
12419 - $cleaned_response = preg_replace('/[^\S\n]+([.,;:!?])/', '$1', $cleaned_response); // Same for punctuation cleanup
12420 -
12421 - //error_log("Final cleaned response: " . $cleaned_response);
12422 -
12423 - return trim($cleaned_response);
12424 -}
12425 -
12426 -/**
12427 - * AJAX handler to get current chat mode for a session
12428 - */
12429 -public function mxchat_get_current_chat_mode() {
12430 - // Verify nonce for security
12431 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce($_POST['nonce'])) {
12432 - wp_send_json_error(['message' => 'Invalid nonce']);
12433 - wp_die();
12434 - }
12435 -
12436 - $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : '';
12437 -
12438 - if (empty($session_id)) {
12439 - wp_send_json_error(['message' => 'Session ID missing']);
12440 - wp_die();
12441 - }
12442 -
12443 - // Get the current chat mode for this session
12444 - $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
12445 -
12446 - wp_send_json_success([
12447 - 'chat_mode' => $chat_mode
12448 - ]);
12449 - wp_die();
12450 -}
12451 6473
12452 6474
12453 6475
12454 6476 }