| @@ -59,8 +59,11 @@ | ||
| 59 | 59 | add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries')); |
| 60 | 60 | add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content')); |
| 61 | 61 | add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content')); |
| 62 | 62 | |
| 63 | + // Hook for content deletion | |
| 64 | + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1); | |
| 65 | + | |
| 63 | 66 | // WordPress post management hooks |
| 64 | 67 | add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2); |
| 65 | 68 | add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3); |
| 66 | 69 | add_action('before_delete_post', array($this, 'mxchat_handle_post_delete')); |
| @@ -990,18 +993,10 @@ | ||
| 990 | 993 | wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); |
| 991 | 994 | exit; |
| 992 | 995 | } |
| 993 | 996 | |
| 994 | - // Fetch URL — use browser-like headers so servers with bot protection don't block us | |
| 995 | - $response = wp_remote_get($submitted_url, array( | |
| 996 | - 'timeout' => 30, | |
| 997 | - 'sslverify' => false, | |
| 998 | - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 999 | - 'headers' => array( | |
| 1000 | - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| 1001 | - 'Accept-Language' => 'en-US,en;q=0.9', | |
| 1002 | - ), | |
| 1003 | - )); | |
| 997 | + // Fetch URL | |
| 998 | + $response = wp_remote_get($submitted_url, array('timeout' => 30)); | |
| 1004 | 999 | |
| 1005 | 1000 | if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { |
| 1006 | 1001 | $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response); |
| 1007 | 1002 | set_transient('mxchat_admin_notice_error', |
| @@ -2835,12 +2830,11 @@ | ||
| 2835 | 2830 | foreach ($primary_indexes as $path => $source) { |
| 2836 | 2831 | $url = trailingslashit($site_url) . $path; |
| 2837 | 2832 | |
| 2838 | 2833 | $response = wp_remote_head($url, array( |
| 2839 | - 'timeout' => 10, | |
| 2834 | + 'timeout' => 3, // Short timeout | |
| 2840 | 2835 | 'sslverify' => false, |
| 2841 | - 'redirection' => 1, | |
| 2842 | - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 2836 | + 'redirection' => 1 | |
| 2843 | 2837 | )); |
| 2844 | 2838 | |
| 2845 | 2839 | if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { |
| 2846 | 2840 | // Found a sitemap index - parse it to get sub-sitemaps |
| @@ -2898,15 +2892,10 @@ | ||
| 2898 | 2892 | private function parse_sitemap_index($url) { |
| 2899 | 2893 | $sub_sitemaps = array(); |
| 2900 | 2894 | |
| 2901 | 2895 | $response = wp_remote_get($url, array( |
| 2902 | - 'timeout' => 30, | |
| 2903 | - 'sslverify' => false, | |
| 2904 | - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 2905 | - 'headers' => array( | |
| 2906 | - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| 2907 | - 'Accept-Language' => 'en-US,en;q=0.9', | |
| 2908 | - ), | |
| 2896 | + 'timeout' => 5, | |
| 2897 | + 'sslverify' => false | |
| 2909 | 2898 | )); |
| 2910 | 2899 | |
| 2911 | 2900 | if (is_wp_error($response)) { |
| 2912 | 2901 | return $sub_sitemaps; |
| @@ -2957,15 +2946,10 @@ | ||
| 2957 | 2946 | * Get URL count from a sitemap |
| 2958 | 2947 | */ |
| 2959 | 2948 | private function get_sitemap_url_count($url) { |
| 2960 | 2949 | $response = wp_remote_get($url, array( |
| 2961 | - 'timeout' => 30, | |
| 2962 | - 'sslverify' => false, | |
| 2963 | - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 2964 | - 'headers' => array( | |
| 2965 | - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| 2966 | - 'Accept-Language' => 'en-US,en;q=0.9', | |
| 2967 | - ), | |
| 2950 | + 'timeout' => 10, | |
| 2951 | + 'sslverify' => false | |
| 2968 | 2952 | )); |
| 2969 | 2953 | |
| 2970 | 2954 | if (is_wp_error($response)) { |
| 2971 | 2955 | return 0; |
| @@ -2988,11 +2972,10 @@ | ||
| 2988 | 2972 | $sitemaps = array(); |
| 2989 | 2973 | $robots_url = trailingslashit($site_url) . 'robots.txt'; |
| 2990 | 2974 | |
| 2991 | 2975 | $response = wp_remote_get($robots_url, array( |
| 2992 | - 'timeout' => 15, | |
| 2993 | - 'sslverify' => false, | |
| 2994 | - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 2976 | + 'timeout' => 5, | |
| 2977 | + 'sslverify' => false | |
| 2995 | 2978 | )); |
| 2996 | 2979 | |
| 2997 | 2980 | if (is_wp_error($response)) { |
| 2998 | 2981 | return $sitemaps; |
| @@ -3482,22 +3465,9 @@ | ||
| 3482 | 3465 | } |
| 3483 | 3466 | |
| 3484 | 3467 | // Get bot_id from request |
| 3485 | 3468 | $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; |
| 3486 | - | |
| 3487 | - // ACF→PDF extraction is opt-in per import batch. Persist the last-used value so users | |
| 3488 | - // don't re-check on every batch; the default is OFF for installs that haven't set it. | |
| 3489 | - $extract_acf_pdfs = !empty($_POST['extract_acf_pdfs']) && $_POST['extract_acf_pdfs'] !== 'false'; | |
| 3490 | - $mxchat_options = get_option('mxchat_options', array()); | |
| 3491 | - if (!is_array($mxchat_options)) { | |
| 3492 | - $mxchat_options = array(); | |
| 3493 | - } | |
| 3494 | - $prior_default = !empty($mxchat_options['acf_pdf_extract_default']); | |
| 3495 | - if ($prior_default !== $extract_acf_pdfs) { | |
| 3496 | - $mxchat_options['acf_pdf_extract_default'] = $extract_acf_pdfs ? 1 : 0; | |
| 3497 | - update_option('mxchat_options', $mxchat_options); | |
| 3498 | - } | |
| 3499 | - | |
| 3469 | + | |
| 3500 | 3470 | // Process only ONE post at a time to avoid request size issues |
| 3501 | 3471 | $post_id = reset($post_ids); |
| 3502 | 3472 | $post = get_post($post_id); |
| 3503 | 3473 | |
| @@ -3603,57 +3573,23 @@ | ||
| 3603 | 3573 | } |
| 3604 | 3574 | |
| 3605 | 3575 | // ADD ACF FIELDS SUPPORT |
| 3606 | 3576 | $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id); |
| 3607 | - $pdf_extracted_count = 0; | |
| 3608 | 3577 | if (!empty($acf_fields)) { |
| 3609 | 3578 | $acf_content_parts = array(); |
| 3610 | - $pdf_attachment_ids = array(); | |
| 3611 | - | |
| 3579 | + | |
| 3612 | 3580 | foreach ($acf_fields as $field_name => $field_value) { |
| 3613 | 3581 | $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id); |
| 3614 | - | |
| 3582 | + | |
| 3615 | 3583 | if (!empty($formatted_value)) { |
| 3616 | 3584 | $field_label = ucwords(str_replace('_', ' ', $field_name)); |
| 3617 | 3585 | $acf_content_parts[] = $field_label . ": " . $formatted_value; |
| 3618 | 3586 | } |
| 3619 | - | |
| 3620 | - // Walk this field's value tree for any PDF attachment references and queue them for extraction. | |
| 3621 | - // Only when the user opted into ACF→PDF extraction for this batch; otherwise the ACF text | |
| 3622 | - // still lands in the KB but the heavier PDF parsing is skipped. | |
| 3623 | - if ($extract_acf_pdfs) { | |
| 3624 | - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids); | |
| 3625 | - } | |
| 3626 | 3587 | } |
| 3627 | - | |
| 3588 | + | |
| 3628 | 3589 | if (!empty($acf_content_parts)) { |
| 3629 | 3590 | $content .= "\n\n" . implode("\n", $acf_content_parts); |
| 3630 | 3591 | } |
| 3631 | - | |
| 3632 | - // Extract text from each unique PDF found in ACF fields and append as a labeled section | |
| 3633 | - if ($extract_acf_pdfs && !empty($pdf_attachment_ids)) { | |
| 3634 | - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids))); | |
| 3635 | - $pdf_sections = array(); | |
| 3636 | - foreach ($pdf_attachment_ids as $att_id) { | |
| 3637 | - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id); | |
| 3638 | - if (!empty($pdf_text)) { | |
| 3639 | - $pdf_title = get_the_title($att_id); | |
| 3640 | - $pdf_url = wp_get_attachment_url($att_id); | |
| 3641 | - $header = 'PDF Attachment'; | |
| 3642 | - if (!empty($pdf_title)) { | |
| 3643 | - $header .= ': ' . $pdf_title; | |
| 3644 | - } | |
| 3645 | - if (!empty($pdf_url)) { | |
| 3646 | - $header .= ' (' . $pdf_url . ')'; | |
| 3647 | - } | |
| 3648 | - $pdf_sections[] = $header . "\n" . $pdf_text; | |
| 3649 | - $pdf_extracted_count++; | |
| 3650 | - } | |
| 3651 | - } | |
| 3652 | - if (!empty($pdf_sections)) { | |
| 3653 | - $content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections); | |
| 3654 | - } | |
| 3655 | - } | |
| 3656 | 3592 | } |
| 3657 | 3593 | |
| 3658 | 3594 | // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields) |
| 3659 | 3595 | $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id); |
| @@ -3783,9 +3719,8 @@ | ||
| 3783 | 3719 | 'title' => $post->post_title, |
| 3784 | 3720 | 'operation_type' => $operation_type, |
| 3785 | 3721 | 'vector_id' => $vector_id, |
| 3786 | 3722 | 'acf_fields_found' => $acf_field_count, |
| 3787 | - 'pdf_extracted_count' => (int) $pdf_extracted_count, | |
| 3788 | 3723 | 'content_preview' => substr($content, 0, 100) . '...', |
| 3789 | 3724 | 'bot_id' => $bot_id |
| 3790 | 3725 | )); |
| 3791 | 3726 | exit; |
| @@ -4200,20 +4135,9 @@ | ||
| 4200 | 4135 | |
| 4201 | 4136 | // Get bot-specific options |
| 4202 | 4137 | $bot_options = $this->get_bot_options($bot_id); |
| 4203 | 4138 | $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); |
| 4204 | - | |
| 4205 | - // Opt-in: when the custom provider is selected for embeddings, index through | |
| 4206 | - // the same custom endpoint the query path uses so stored vectors and query | |
| 4207 | - // vectors share a model. Returns the vector array on success, or an error | |
| 4208 | - // string on failure (this function's existing failure contract). | |
| 4209 | - if (isset($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') { | |
| 4210 | - if (!class_exists('MxChat_Utils')) { | |
| 4211 | - require_once dirname(__FILE__) . '/../includes/class-mxchat-utils.php'; | |
| 4212 | - } | |
| 4213 | - return MxChat_Utils::generate_embedding_custom($text, $options); | |
| 4214 | - } | |
| 4215 | - | |
| 4139 | + | |
| 4216 | 4140 | $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; |
| 4217 | 4141 | //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model); |
| 4218 | 4142 | |
| 4219 | 4143 | // Determine provider and endpoint |
| @@ -5131,197 +5055,8 @@ | ||
| 5131 | 5055 | return implode(', ', array_filter($text_parts)); |
| 5132 | 5056 | } |
| 5133 | 5057 | |
| 5134 | 5058 | /** |
| 5135 | - * Walk an ACF field value tree and collect attachment IDs for any value that | |
| 5136 | - * resolves to a PDF in the WordPress media library. Handles the three shapes | |
| 5137 | - * ACF returns for File/Image/URL fields (array with ID+url, integer attachment ID, | |
| 5138 | - * plain URL string), and recurses through repeater/group/flexible content. | |
| 5139 | - * | |
| 5140 | - * @param mixed $value The ACF field value (any depth) | |
| 5141 | - * @param array $out Accumulator (passed by reference) for attachment IDs | |
| 5142 | - * @param int $depth Recursion guard | |
| 5143 | - */ | |
| 5144 | -private function mxchat_collect_pdf_attachment_ids_from_acf_value($value, &$out, $depth = 0) { | |
| 5145 | - if ($depth > 6) { | |
| 5146 | - return; // prevent runaway recursion on circular/very-deep structures | |
| 5147 | - } | |
| 5148 | - | |
| 5149 | - if (empty($value)) { | |
| 5150 | - return; | |
| 5151 | - } | |
| 5152 | - | |
| 5153 | - // Array shapes: ACF File/Image return value=array; repeaters/groups are arrays of arrays | |
| 5154 | - if (is_array($value)) { | |
| 5155 | - // Direct File/Image-style array (has 'url' and usually 'ID' + 'mime_type') | |
| 5156 | - $looks_like_attachment = isset($value['url']) || isset($value['ID']) || isset($value['id']); | |
| 5157 | - if ($looks_like_attachment) { | |
| 5158 | - $att_id = 0; | |
| 5159 | - if (!empty($value['ID']) && is_numeric($value['ID'])) { | |
| 5160 | - $att_id = (int) $value['ID']; | |
| 5161 | - } elseif (!empty($value['id']) && is_numeric($value['id'])) { | |
| 5162 | - $att_id = (int) $value['id']; | |
| 5163 | - } elseif (!empty($value['url']) && is_string($value['url'])) { | |
| 5164 | - $att_id = (int) attachment_url_to_postid($value['url']); | |
| 5165 | - } | |
| 5166 | - | |
| 5167 | - $is_pdf = false; | |
| 5168 | - if (!empty($value['mime_type']) && $value['mime_type'] === 'application/pdf') { | |
| 5169 | - $is_pdf = true; | |
| 5170 | - } elseif (!empty($value['subtype']) && strtolower((string) $value['subtype']) === 'pdf') { | |
| 5171 | - $is_pdf = true; | |
| 5172 | - } elseif (!empty($value['url']) && is_string($value['url']) && $this->mxchat_url_looks_like_pdf($value['url'])) { | |
| 5173 | - $is_pdf = true; | |
| 5174 | - } elseif ($att_id && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5175 | - $is_pdf = true; | |
| 5176 | - } | |
| 5177 | - | |
| 5178 | - if ($is_pdf && $att_id && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5179 | - $out[] = $att_id; | |
| 5180 | - } | |
| 5181 | - // An array node that represents one attachment doesn't contain other | |
| 5182 | - // attachments inside it — done with this branch. | |
| 5183 | - return; | |
| 5184 | - } | |
| 5185 | - | |
| 5186 | - // Recurse: repeater rows, flexible-content layouts, groups, etc. | |
| 5187 | - foreach ($value as $sub) { | |
| 5188 | - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($sub, $out, $depth + 1); | |
| 5189 | - } | |
| 5190 | - return; | |
| 5191 | - } | |
| 5192 | - | |
| 5193 | - // Plain numeric attachment ID (ACF File field set to "Return: ID") | |
| 5194 | - if (is_numeric($value)) { | |
| 5195 | - $att_id = (int) $value; | |
| 5196 | - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5197 | - $out[] = $att_id; | |
| 5198 | - } | |
| 5199 | - return; | |
| 5200 | - } | |
| 5201 | - | |
| 5202 | - // Plain string — URL pointing at a PDF (ACF File field set to "Return: URL", or a custom URL/text field) | |
| 5203 | - if (is_string($value)) { | |
| 5204 | - $trimmed = trim($value); | |
| 5205 | - if ($trimmed !== '' && $this->mxchat_url_looks_like_pdf($trimmed)) { | |
| 5206 | - $att_id = (int) attachment_url_to_postid($trimmed); | |
| 5207 | - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5208 | - $out[] = $att_id; | |
| 5209 | - } | |
| 5210 | - } | |
| 5211 | - return; | |
| 5212 | - } | |
| 5213 | -} | |
| 5214 | - | |
| 5215 | -/** | |
| 5216 | - * Heuristic: does this URL/string look like a PDF reference? | |
| 5217 | - * Tolerates query strings and fragments (#page=2). | |
| 5218 | - */ | |
| 5219 | -private function mxchat_url_looks_like_pdf($url) { | |
| 5220 | - if (!is_string($url) || $url === '') { | |
| 5221 | - return false; | |
| 5222 | - } | |
| 5223 | - // Strip query + fragment before checking extension | |
| 5224 | - $path = preg_replace('/[?#].*$/', '', $url); | |
| 5225 | - return (bool) preg_match('/\.pdf$/i', $path); | |
| 5226 | -} | |
| 5227 | - | |
| 5228 | -/** | |
| 5229 | - * Extract text from a PDF attachment by ID using the bundled Smalot parser. | |
| 5230 | - * Reads the file directly from disk via get_attached_file (no HTTP fetch). | |
| 5231 | - * Result is cached on the attachment as post_meta keyed by file mtime so we | |
| 5232 | - * only parse the same PDF once unless the file changes on disk. | |
| 5233 | - * | |
| 5234 | - * @param int $attachment_id | |
| 5235 | - * @return string Extracted plain text, or '' on failure. | |
| 5236 | - */ | |
| 5237 | -private function mxchat_extract_pdf_text_by_attachment_id($attachment_id) { | |
| 5238 | - $attachment_id = (int) $attachment_id; | |
| 5239 | - if ($attachment_id <= 0) { | |
| 5240 | - return ''; | |
| 5241 | - } | |
| 5242 | - if (get_post_mime_type($attachment_id) !== 'application/pdf') { | |
| 5243 | - return ''; | |
| 5244 | - } | |
| 5245 | - | |
| 5246 | - $pdf_path = get_attached_file($attachment_id); | |
| 5247 | - if (empty($pdf_path) || !file_exists($pdf_path) || !is_readable($pdf_path)) { | |
| 5248 | - return ''; | |
| 5249 | - } | |
| 5250 | - | |
| 5251 | - // Raw-file size cap. Parsing very large PDFs can OOM the request; skip with a log entry | |
| 5252 | - // and let the rest of the ACF content land in the KB. Filterable for users who need it bigger. | |
| 5253 | - $default_max_bytes = 25 * 1024 * 1024; | |
| 5254 | - $max_bytes = (int) apply_filters('mxchat_acf_pdf_max_bytes', $default_max_bytes, $attachment_id, $pdf_path); | |
| 5255 | - if ($max_bytes > 0) { | |
| 5256 | - $file_size = @filesize($pdf_path); | |
| 5257 | - if ($file_size !== false && $file_size > $max_bytes) { | |
| 5258 | - error_log(sprintf( | |
| 5259 | - '[mxchat] ACF PDF skipped (over size cap): attachment %d "%s" %d bytes > cap %d', | |
| 5260 | - $attachment_id, | |
| 5261 | - basename($pdf_path), | |
| 5262 | - $file_size, | |
| 5263 | - $max_bytes | |
| 5264 | - )); | |
| 5265 | - return ''; | |
| 5266 | - } | |
| 5267 | - } | |
| 5268 | - | |
| 5269 | - $mtime = @filemtime($pdf_path); | |
| 5270 | - $cache_meta_key = '_mxchat_acf_pdf_text_v1'; | |
| 5271 | - $cached = get_post_meta($attachment_id, $cache_meta_key, true); | |
| 5272 | - if (is_array($cached) && isset($cached['mtime'], $cached['text']) && (int) $cached['mtime'] === (int) $mtime) { | |
| 5273 | - return (string) $cached['text']; | |
| 5274 | - } | |
| 5275 | - | |
| 5276 | - $text = ''; | |
| 5277 | - try { | |
| 5278 | - if (function_exists('mxchat_load_pdf_parser')) { | |
| 5279 | - mxchat_load_pdf_parser(); | |
| 5280 | - } | |
| 5281 | - if (!class_exists('\\Smalot\\PdfParser\\Parser')) { | |
| 5282 | - return ''; | |
| 5283 | - } | |
| 5284 | - $parser = new \Smalot\PdfParser\Parser(); | |
| 5285 | - $pdf = $parser->parseFile($pdf_path); | |
| 5286 | - $pages = $pdf->getPages(); | |
| 5287 | - $page_texts = array(); | |
| 5288 | - foreach ($pages as $page) { | |
| 5289 | - $page_text = ''; | |
| 5290 | - try { | |
| 5291 | - $page_text = $page->getText(); | |
| 5292 | - } catch (\Exception $e) { | |
| 5293 | - $page_text = ''; | |
| 5294 | - } | |
| 5295 | - if (!empty($page_text)) { | |
| 5296 | - $page_texts[] = $page_text; | |
| 5297 | - } | |
| 5298 | - } | |
| 5299 | - $text = trim(implode("\n\n", $page_texts)); | |
| 5300 | - } catch (\Exception $e) { | |
| 5301 | - error_log('[mxchat] ACF PDF extraction failed for attachment ' . $attachment_id . ': ' . $e->getMessage()); | |
| 5302 | - return ''; | |
| 5303 | - } catch (\Throwable $e) { | |
| 5304 | - error_log('[mxchat] ACF PDF extraction error for attachment ' . $attachment_id . ': ' . $e->getMessage()); | |
| 5305 | - return ''; | |
| 5306 | - } | |
| 5307 | - | |
| 5308 | - // Cap per-PDF text to avoid blowing up the embedding payload on enormous PDFs. | |
| 5309 | - // The chunker downstream will still split this into multiple vectors. | |
| 5310 | - $max_len = (int) apply_filters('mxchat_acf_pdf_text_max_length', 50000); | |
| 5311 | - if ($max_len > 0 && strlen($text) > $max_len) { | |
| 5312 | - $text = substr($text, 0, $max_len); | |
| 5313 | - } | |
| 5314 | - | |
| 5315 | - update_post_meta($attachment_id, $cache_meta_key, array( | |
| 5316 | - 'mtime' => (int) $mtime, | |
| 5317 | - 'text' => $text, | |
| 5318 | - )); | |
| 5319 | - | |
| 5320 | - return $text; | |
| 5321 | -} | |
| 5322 | - | |
| 5323 | -/** | |
| 5324 | 5059 | * Handle ACF save - fires after ACF fields are saved |
| 5325 | 5060 | * This ensures ACF field data is available when syncing to knowledge base |
| 5326 | 5061 | */ |
| 5327 | 5062 | public function mxchat_handle_acf_save($post_id) { |
| @@ -5429,33 +5164,40 @@ | ||
| 5429 | 5164 | // If the post was previously published but is now not published, remove from knowledge base |
| 5430 | 5165 | if ($previous_status === 'publish' && $post->post_status !== 'publish') { |
| 5431 | 5166 | // Use the stored URL from when it was published, or fall back to current permalink |
| 5432 | 5167 | $source_url = $previous_url ?: get_permalink($post_id); |
| 5168 | + | |
| 5169 | + if ($source_url) { | |
| 5170 | + // Check if Pinecone is enabled | |
| 5171 | + $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); | |
| 5172 | + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; | |
| 5433 | 5173 | |
| 5434 | - if ($source_url) { | |
| 5435 | - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks) | |
| 5436 | - MxChat_Utils::delete_chunks_for_url($source_url, 'default'); | |
| 5174 | + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { | |
| 5175 | + // Delete from Pinecone | |
| 5176 | + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); | |
| 5177 | + } else { | |
| 5178 | + // Delete from WordPress DB | |
| 5179 | + global $wpdb; | |
| 5180 | + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 5181 | + | |
| 5182 | + $result = $wpdb->delete( | |
| 5183 | + $table_name, | |
| 5184 | + array('source_url' => $source_url), | |
| 5185 | + array('%s') | |
| 5186 | + ); | |
| 5187 | + } | |
| 5437 | 5188 | } |
| 5438 | - | |
| 5189 | + | |
| 5439 | 5190 | // Clean up the transients and exit early |
| 5440 | 5191 | delete_transient($previous_status_key); |
| 5441 | 5192 | delete_transient($previous_url_key); |
| 5442 | 5193 | return; |
| 5443 | 5194 | } |
| 5444 | - | |
| 5445 | - // Slug/permalink rename while still published: delete the old vectors before upserting new ones. | |
| 5446 | - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL. | |
| 5447 | - if ($post->post_status === 'publish' && !empty($previous_url)) { | |
| 5448 | - $current_url = get_permalink($post_id); | |
| 5449 | - if ($current_url && $current_url !== $previous_url) { | |
| 5450 | - MxChat_Utils::delete_chunks_for_url($previous_url, 'default'); | |
| 5451 | - } | |
| 5452 | - } | |
| 5453 | - | |
| 5195 | + | |
| 5454 | 5196 | // Store the current status for next time (if this is an update) |
| 5455 | 5197 | if ($update) { |
| 5456 | 5198 | set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS); |
| 5457 | - | |
| 5199 | + | |
| 5458 | 5200 | // If the post is currently published, also store its URL |
| 5459 | 5201 | if ($post->post_status === 'publish') { |
| 5460 | 5202 | $current_url = get_permalink($post_id); |
| 5461 | 5203 | set_transient($previous_url_key, $current_url, DAY_IN_SECONDS); |
| @@ -5564,9 +5306,8 @@ | ||
| 5564 | 5306 | // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior) |
| 5565 | 5307 | $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id); |
| 5566 | 5308 | if (!empty($acf_fields)) { |
| 5567 | 5309 | $acf_content_parts = array(); |
| 5568 | - $pdf_attachment_ids = array(); | |
| 5569 | 5310 | |
| 5570 | 5311 | foreach ($acf_fields as $field_name => $field_value) { |
| 5571 | 5312 | $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id); |
| 5572 | 5313 | if (!empty($formatted_value)) { |
| @@ -5573,44 +5314,13 @@ | ||
| 5573 | 5314 | // Convert field name to readable label |
| 5574 | 5315 | $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name)); |
| 5575 | 5316 | $acf_content_parts[] = $field_label . ": " . $formatted_value; |
| 5576 | 5317 | } |
| 5577 | - | |
| 5578 | - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids); | |
| 5579 | 5318 | } |
| 5580 | 5319 | |
| 5581 | 5320 | if (!empty($acf_content_parts)) { |
| 5582 | 5321 | $final_content .= "\n\n" . implode("\n", $acf_content_parts); |
| 5583 | 5322 | } |
| 5584 | - | |
| 5585 | - // Gate the auto-sync PDF-extraction loop behind an opt-in option. | |
| 5586 | - // Mirrors the per-batch checkbox the manual content selector has; the | |
| 5587 | - // 25 MB size cap lives in the shared extractor so it applies in both | |
| 5588 | - // paths regardless. Default OFF — re-parsing every ACF PDF on every | |
| 5589 | - // editor save is expensive and most sites don't want it. | |
| 5590 | - $autosync_extract_acf_pdfs = get_option('mxchat_auto_sync_acf_pdfs', '0') === '1'; | |
| 5591 | - if ($autosync_extract_acf_pdfs && !empty($pdf_attachment_ids)) { | |
| 5592 | - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids))); | |
| 5593 | - $pdf_sections = array(); | |
| 5594 | - foreach ($pdf_attachment_ids as $att_id) { | |
| 5595 | - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id); | |
| 5596 | - if (!empty($pdf_text)) { | |
| 5597 | - $pdf_title = get_the_title($att_id); | |
| 5598 | - $pdf_url = wp_get_attachment_url($att_id); | |
| 5599 | - $header = 'PDF Attachment'; | |
| 5600 | - if (!empty($pdf_title)) { | |
| 5601 | - $header .= ': ' . $pdf_title; | |
| 5602 | - } | |
| 5603 | - if (!empty($pdf_url)) { | |
| 5604 | - $header .= ' (' . $pdf_url . ')'; | |
| 5605 | - } | |
| 5606 | - $pdf_sections[] = $header . "\n" . $pdf_text; | |
| 5607 | - } | |
| 5608 | - } | |
| 5609 | - if (!empty($pdf_sections)) { | |
| 5610 | - $final_content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections); | |
| 5611 | - } | |
| 5612 | - } | |
| 5613 | 5323 | } |
| 5614 | 5324 | |
| 5615 | 5325 | // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields) |
| 5616 | 5326 | $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id); |
| @@ -5717,14 +5427,12 @@ | ||
| 5717 | 5427 | if (!$should_sync) { |
| 5718 | 5428 | return; |
| 5719 | 5429 | } |
| 5720 | 5430 | |
| 5721 | - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing | |
| 5722 | - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the | |
| 5723 | - // real vector IDs stored under the original URL. | |
| 5724 | - $source_url = $this->mxchat_resolve_pre_trash_url($post_id); | |
| 5431 | + // Get the URL before post is deleted | |
| 5432 | + $source_url = get_permalink($post_id); | |
| 5725 | 5433 | if (!$source_url) { |
| 5726 | - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id); | |
| 5434 | + //error_log('MXChat: Failed to get permalink for post ' . $post_id); | |
| 5727 | 5435 | return; |
| 5728 | 5436 | } |
| 5729 | 5437 | |
| 5730 | 5438 | // Use chunk-aware deletion (handles both chunked and non-chunked content) |
| @@ -5732,36 +5440,56 @@ | ||
| 5732 | 5440 | |
| 5733 | 5441 | if (is_wp_error($delete_result)) { |
| 5734 | 5442 | //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message()); |
| 5735 | 5443 | } |
| 5444 | +} | |
| 5736 | 5445 | |
| 5737 | - delete_transient('mxchat_prev_url_' . $post_id); | |
| 5738 | - delete_transient('mxchat_prev_status_' . $post_id); | |
| 5739 | -} | |
| 5740 | 5446 | |
| 5741 | -/** | |
| 5742 | - * Resolve the source URL for a post being trashed/deleted. | |
| 5743 | - * | |
| 5744 | - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so | |
| 5745 | - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or | |
| 5746 | - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status | |
| 5747 | - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix. | |
| 5748 | - */ | |
| 5749 | -private function mxchat_resolve_pre_trash_url($post_id) { | |
| 5750 | - $previous_url = get_transient('mxchat_prev_url_' . $post_id); | |
| 5751 | - if (!empty($previous_url)) { | |
| 5752 | - return $previous_url; | |
| 5753 | - } | |
| 5447 | + /** | |
| 5448 | + * Deletes data from Pinecone using a source URL | |
| 5449 | + */ | |
| 5450 | + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) { | |
| 5451 | + $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; | |
| 5452 | + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; | |
| 5754 | 5453 | |
| 5755 | - $current = get_permalink($post_id); | |
| 5756 | - if (!$current) { | |
| 5757 | - return ''; | |
| 5758 | - } | |
| 5759 | - return preg_replace('#__trashed(/?)$#', '$1', $current); | |
| 5760 | -} | |
| 5454 | + if (empty($host) || empty($api_key)) { | |
| 5455 | + //error_log('MXChat: Pinecone deletion failed - missing configuration'); | |
| 5456 | + return false; | |
| 5457 | + } | |
| 5761 | 5458 | |
| 5459 | + $api_endpoint = "https://{$host}/vectors/delete"; | |
| 5460 | + $vector_id = md5($source_url); | |
| 5762 | 5461 | |
| 5462 | + $request_body = array( | |
| 5463 | + 'ids' => array($vector_id) | |
| 5464 | + ); | |
| 5763 | 5465 | |
| 5466 | + $response = wp_remote_post($api_endpoint, array( | |
| 5467 | + 'headers' => array( | |
| 5468 | + 'Api-Key' => $api_key, | |
| 5469 | + 'accept' => 'application/json', | |
| 5470 | + 'content-type' => 'application/json' | |
| 5471 | + ), | |
| 5472 | + 'body' => wp_json_encode($request_body), | |
| 5473 | + 'timeout' => 30 | |
| 5474 | + )); | |
| 5475 | + | |
| 5476 | + if (is_wp_error($response)) { | |
| 5477 | + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message()); | |
| 5478 | + return false; | |
| 5479 | + } | |
| 5480 | + | |
| 5481 | + $response_code = wp_remote_retrieve_response_code($response); | |
| 5482 | + if ($response_code !== 200) { | |
| 5483 | + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code); | |
| 5484 | + return false; | |
| 5485 | + } | |
| 5486 | + | |
| 5487 | + return true; | |
| 5488 | + } | |
| 5489 | + | |
| 5490 | + | |
| 5491 | + | |
| 5764 | 5492 | public function mxchat_handle_product_change($post_id, $post, $update) { |
| 5765 | 5493 | if ($post->post_type !== 'product') { |
| 5766 | 5494 | return; |
| 5767 | 5495 | } |
| @@ -5913,18 +5641,28 @@ | ||
| 5913 | 5641 | if (get_post_type($post_id) !== 'product') { |
| 5914 | 5642 | return; |
| 5915 | 5643 | } |
| 5916 | 5644 | |
| 5917 | - $source_url = $this->mxchat_resolve_pre_trash_url($post_id); | |
| 5918 | - if (!$source_url) { | |
| 5919 | - return; | |
| 5920 | - } | |
| 5645 | + $source_url = get_permalink($post_id); | |
| 5921 | 5646 | |
| 5922 | - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks) | |
| 5923 | - MxChat_Utils::delete_chunks_for_url($source_url, 'default'); | |
| 5647 | + // Check if Pinecone is enabled | |
| 5648 | + $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); | |
| 5649 | + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; | |
| 5924 | 5650 | |
| 5925 | - delete_transient('mxchat_prev_url_' . $post_id); | |
| 5926 | - delete_transient('mxchat_prev_status_' . $post_id); | |
| 5651 | + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { | |
| 5652 | + // Delete from Pinecone | |
| 5653 | + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); | |
| 5654 | + } else { | |
| 5655 | + // Delete from WordPress DB | |
| 5656 | + global $wpdb; | |
| 5657 | + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 5658 | + | |
| 5659 | + $wpdb->delete( | |
| 5660 | + $table_name, | |
| 5661 | + array('source_url' => $source_url), | |
| 5662 | + array('%s') | |
| 5663 | + ); | |
| 5664 | + } | |
| 5927 | 5665 | } |
| 5928 | 5666 | |
| 5929 | 5667 | /** |
| 5930 | 5668 | * Handle individual Pinecone content deletion |