PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.8
MxChat – AI Chatbot & Content Generation for WordPress v3.1.8
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | admin/class-knowledge-manager.php +93 -335 3.2.103.1.8 View file →
@@ -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'));
@@ -3482,22 +3485,9 @@
3482 3485 }
3483 3486
3484 3487 // Get bot_id from request
3485 3488 $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 -
3489 +
3500 3490 // Process only ONE post at a time to avoid request size issues
3501 3491 $post_id = reset($post_ids);
3502 3492 $post = get_post($post_id);
3503 3493
@@ -3603,57 +3593,23 @@
3603 3593 }
3604 3594
3605 3595 // ADD ACF FIELDS SUPPORT
3606 3596 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3607 - $pdf_extracted_count = 0;
3608 3597 if (!empty($acf_fields)) {
3609 3598 $acf_content_parts = array();
3610 - $pdf_attachment_ids = array();
3611 -
3599 +
3612 3600 foreach ($acf_fields as $field_name => $field_value) {
3613 3601 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3614 -
3602 +
3615 3603 if (!empty($formatted_value)) {
3616 3604 $field_label = ucwords(str_replace('_', ' ', $field_name));
3617 3605 $acf_content_parts[] = $field_label . ": " . $formatted_value;
3618 3606 }
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 3607 }
3627 -
3608 +
3628 3609 if (!empty($acf_content_parts)) {
3629 3610 $content .= "\n\n" . implode("\n", $acf_content_parts);
3630 3611 }
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 3612 }
3657 3613
3658 3614 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3659 3615 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -3783,9 +3739,8 @@
3783 3739 'title' => $post->post_title,
3784 3740 'operation_type' => $operation_type,
3785 3741 'vector_id' => $vector_id,
3786 3742 'acf_fields_found' => $acf_field_count,
3787 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
3788 3743 'content_preview' => substr($content, 0, 100) . '...',
3789 3744 'bot_id' => $bot_id
3790 3745 ));
3791 3746 exit;
@@ -4200,20 +4155,9 @@
4200 4155
4201 4156 // Get bot-specific options
4202 4157 $bot_options = $this->get_bot_options($bot_id);
4203 4158 $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 -
4159 +
4216 4160 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4217 4161 //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
4218 4162
4219 4163 // Determine provider and endpoint
@@ -5131,197 +5075,8 @@
5131 5075 return implode(', ', array_filter($text_parts));
5132 5076 }
5133 5077
5134 5078 /**
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 5079 * Handle ACF save - fires after ACF fields are saved
5325 5080 * This ensures ACF field data is available when syncing to knowledge base
5326 5081 */
5327 5082 public function mxchat_handle_acf_save($post_id) {
@@ -5429,33 +5184,40 @@
5429 5184 // If the post was previously published but is now not published, remove from knowledge base
5430 5185 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5431 5186 // Use the stored URL from when it was published, or fall back to current permalink
5432 5187 $source_url = $previous_url ?: get_permalink($post_id);
5188 +
5189 + if ($source_url) {
5190 + // Check if Pinecone is enabled
5191 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5192 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5433 5193
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');
5194 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5195 + // Delete from Pinecone
5196 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5197 + } else {
5198 + // Delete from WordPress DB
5199 + global $wpdb;
5200 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5201 +
5202 + $result = $wpdb->delete(
5203 + $table_name,
5204 + array('source_url' => $source_url),
5205 + array('%s')
5206 + );
5207 + }
5437 5208 }
5438 -
5209 +
5439 5210 // Clean up the transients and exit early
5440 5211 delete_transient($previous_status_key);
5441 5212 delete_transient($previous_url_key);
5442 5213 return;
5443 5214 }
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 -
5215 +
5454 5216 // Store the current status for next time (if this is an update)
5455 5217 if ($update) {
5456 5218 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5457 -
5219 +
5458 5220 // If the post is currently published, also store its URL
5459 5221 if ($post->post_status === 'publish') {
5460 5222 $current_url = get_permalink($post_id);
5461 5223 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
@@ -5564,9 +5326,8 @@
5564 5326 // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5565 5327 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5566 5328 if (!empty($acf_fields)) {
5567 5329 $acf_content_parts = array();
5568 - $pdf_attachment_ids = array();
5569 5330
5570 5331 foreach ($acf_fields as $field_name => $field_value) {
5571 5332 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5572 5333 if (!empty($formatted_value)) {
@@ -5573,44 +5334,13 @@
5573 5334 // Convert field name to readable label
5574 5335 $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5575 5336 $acf_content_parts[] = $field_label . ": " . $formatted_value;
5576 5337 }
5577 -
5578 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
5579 5338 }
5580 5339
5581 5340 if (!empty($acf_content_parts)) {
5582 5341 $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5583 5342 }
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 5343 }
5614 5344
5615 5345 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5616 5346 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -5717,14 +5447,12 @@
5717 5447 if (!$should_sync) {
5718 5448 return;
5719 5449 }
5720 5450
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);
5451 + // Get the URL before post is deleted
5452 + $source_url = get_permalink($post_id);
5725 5453 if (!$source_url) {
5726 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5454 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5727 5455 return;
5728 5456 }
5729 5457
5730 5458 // Use chunk-aware deletion (handles both chunked and non-chunked content)
@@ -5732,36 +5460,56 @@
5732 5460
5733 5461 if (is_wp_error($delete_result)) {
5734 5462 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5735 5463 }
5464 +}
5736 5465
5737 - delete_transient('mxchat_prev_url_' . $post_id);
5738 - delete_transient('mxchat_prev_status_' . $post_id);
5739 -}
5740 5466
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 - }
5467 + /**
5468 + * Deletes data from Pinecone using a source URL
5469 + */
5470 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
5471 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5472 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5754 5473
5755 - $current = get_permalink($post_id);
5756 - if (!$current) {
5757 - return '';
5758 - }
5759 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5760 -}
5474 + if (empty($host) || empty($api_key)) {
5475 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
5476 + return false;
5477 + }
5761 5478
5479 + $api_endpoint = "https://{$host}/vectors/delete";
5480 + $vector_id = md5($source_url);
5762 5481
5482 + $request_body = array(
5483 + 'ids' => array($vector_id)
5484 + );
5763 5485
5486 + $response = wp_remote_post($api_endpoint, array(
5487 + 'headers' => array(
5488 + 'Api-Key' => $api_key,
5489 + 'accept' => 'application/json',
5490 + 'content-type' => 'application/json'
5491 + ),
5492 + 'body' => wp_json_encode($request_body),
5493 + 'timeout' => 30
5494 + ));
5495 +
5496 + if (is_wp_error($response)) {
5497 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
5498 + return false;
5499 + }
5500 +
5501 + $response_code = wp_remote_retrieve_response_code($response);
5502 + if ($response_code !== 200) {
5503 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
5504 + return false;
5505 + }
5506 +
5507 + return true;
5508 + }
5509 +
5510 +
5511 +
5764 5512 public function mxchat_handle_product_change($post_id, $post, $update) {
5765 5513 if ($post->post_type !== 'product') {
5766 5514 return;
5767 5515 }
@@ -5913,18 +5661,28 @@
5913 5661 if (get_post_type($post_id) !== 'product') {
5914 5662 return;
5915 5663 }
5916 5664
5917 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5918 - if (!$source_url) {
5919 - return;
5920 - }
5665 + $source_url = get_permalink($post_id);
5921 5666
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');
5667 + // Check if Pinecone is enabled
5668 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5669 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5924 5670
5925 - delete_transient('mxchat_prev_url_' . $post_id);
5926 - delete_transient('mxchat_prev_status_' . $post_id);
5671 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5672 + // Delete from Pinecone
5673 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5674 + } else {
5675 + // Delete from WordPress DB
5676 + global $wpdb;
5677 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5678 +
5679 + $wpdb->delete(
5680 + $table_name,
5681 + array('source_url' => $source_url),
5682 + array('%s')
5683 + );
5684 + }
5927 5685 }
5928 5686
5929 5687 /**
5930 5688 * Handle individual Pinecone content deletion