| @@ -56,11 +56,12 @@ | ||
| 56 | 56 | add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries')); |
| 57 | 57 | add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps')); |
| 58 | 58 | add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries')); |
| 59 | 59 | add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries')); |
| 60 | - add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content')); | |
| 61 | - add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content')); | |
| 62 | 60 | |
| 61 | + // Hook for content deletion | |
| 62 | + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1); | |
| 63 | + | |
| 63 | 64 | // WordPress post management hooks |
| 64 | 65 | add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2); |
| 65 | 66 | add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3); |
| 66 | 67 | add_action('before_delete_post', array($this, 'mxchat_handle_post_delete')); |
| @@ -160,13 +161,9 @@ | ||
| 160 | 161 | public function mxchat_is_pdf_url($url, $response) { |
| 161 | 162 | $content_type = wp_remote_retrieve_header($response, 'content-type'); |
| 162 | 163 | $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION)); |
| 163 | 164 | |
| 164 | - // Check Content-Disposition header for .pdf filename (Google Drive sends this) | |
| 165 | - $disposition = wp_remote_retrieve_header($response, 'content-disposition'); | |
| 166 | - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false; | |
| 167 | - | |
| 168 | - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition; | |
| 165 | + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf'; | |
| 169 | 166 | } |
| 170 | 167 | |
| 171 | 168 | |
| 172 | 169 | public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') { |
| @@ -623,269 +620,8 @@ | ||
| 623 | 620 | } |
| 624 | 621 | } |
| 625 | 622 | |
| 626 | 623 | |
| 627 | -/** | |
| 628 | - * AJAX: Get full content for editing — reassembles chunks if needed. | |
| 629 | - * Works for both WordPress DB and Pinecone entries. | |
| 630 | - */ | |
| 631 | -public function ajax_mxchat_get_entry_content() { | |
| 632 | - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce'); | |
| 633 | - | |
| 634 | - if ( ! current_user_can('manage_options') ) { | |
| 635 | - wp_send_json_error( array( 'message' => 'Permission denied.' ) ); | |
| 636 | - } | |
| 637 | - | |
| 638 | - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : ''; | |
| 639 | - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0; | |
| 640 | - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress'; | |
| 641 | - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; | |
| 642 | - | |
| 643 | - if ( $data_source === 'pinecone' ) { | |
| 644 | - // Pinecone: fetch vectors by source_url, reassemble chunks | |
| 645 | - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id ); | |
| 646 | - } else { | |
| 647 | - // WordPress DB | |
| 648 | - $content = $this->get_wordpress_entry_content( $source_url, $entry_id ); | |
| 649 | - } | |
| 650 | - | |
| 651 | - if ( is_wp_error( $content ) ) { | |
| 652 | - wp_send_json_error( array( 'message' => $content->get_error_message() ) ); | |
| 653 | - } | |
| 654 | - | |
| 655 | - wp_send_json_success( $content ); | |
| 656 | -} | |
| 657 | - | |
| 658 | -/** | |
| 659 | - * Get content from WordPress DB — reassembles chunks by source_url. | |
| 660 | - */ | |
| 661 | -private function get_wordpress_entry_content( $source_url, $entry_id ) { | |
| 662 | - global $wpdb; | |
| 663 | - $table = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 664 | - | |
| 665 | - // If we have a source_url, check for chunks | |
| 666 | - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) { | |
| 667 | - $rows = $wpdb->get_results( $wpdb->prepare( | |
| 668 | - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC", | |
| 669 | - $source_url | |
| 670 | - ) ); | |
| 671 | - | |
| 672 | - if ( $rows && count( $rows ) > 1 ) { | |
| 673 | - // Multiple rows = chunked. Reassemble. | |
| 674 | - $chunks = array(); | |
| 675 | - foreach ( $rows as $row ) { | |
| 676 | - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content ); | |
| 677 | - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks ); | |
| 678 | - $chunks[ $index ] = $parsed['text']; | |
| 679 | - } | |
| 680 | - ksort( $chunks ); | |
| 681 | - return array( | |
| 682 | - 'content' => implode( "\n\n", $chunks ), | |
| 683 | - 'source_url' => $source_url, | |
| 684 | - 'is_chunked' => true, | |
| 685 | - 'chunk_count' => count( $chunks ), | |
| 686 | - 'content_type' => $rows[0]->content_type, | |
| 687 | - ); | |
| 688 | - } elseif ( $rows && count( $rows ) === 1 ) { | |
| 689 | - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content ); | |
| 690 | - return array( | |
| 691 | - 'content' => $parsed['text'], | |
| 692 | - 'source_url' => $source_url, | |
| 693 | - 'entry_id' => $rows[0]->id, | |
| 694 | - 'is_chunked' => false, | |
| 695 | - 'content_type' => $rows[0]->content_type, | |
| 696 | - ); | |
| 697 | - } | |
| 698 | - } | |
| 699 | - | |
| 700 | - // Fallback: fetch by ID | |
| 701 | - if ( $entry_id > 0 ) { | |
| 702 | - $row = $wpdb->get_row( $wpdb->prepare( | |
| 703 | - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d", | |
| 704 | - $entry_id | |
| 705 | - ) ); | |
| 706 | - if ( $row ) { | |
| 707 | - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content ); | |
| 708 | - return array( | |
| 709 | - 'content' => $parsed['text'], | |
| 710 | - 'source_url' => $row->source_url, | |
| 711 | - 'entry_id' => $row->id, | |
| 712 | - 'is_chunked' => false, | |
| 713 | - 'content_type' => $row->content_type, | |
| 714 | - ); | |
| 715 | - } | |
| 716 | - } | |
| 717 | - | |
| 718 | - return new WP_Error( 'not_found', 'Entry not found.' ); | |
| 719 | -} | |
| 720 | - | |
| 721 | -/** | |
| 722 | - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks. | |
| 723 | - */ | |
| 724 | -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) { | |
| 725 | - if ( ! class_exists('MxChat_Pinecone_Manager') ) { | |
| 726 | - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' ); | |
| 727 | - } | |
| 728 | - | |
| 729 | - // Get Pinecone config | |
| 730 | - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) { | |
| 731 | - $pinecone_options = get_option('mxchat_pinecone_addon_options'); | |
| 732 | - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; | |
| 733 | - $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; | |
| 734 | - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; | |
| 735 | - } else { | |
| 736 | - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); | |
| 737 | - $api_key = $bot_config['api_key'] ?? ''; | |
| 738 | - $host = $bot_config['host'] ?? ''; | |
| 739 | - $namespace = $bot_config['namespace'] ?? ''; | |
| 740 | - } | |
| 741 | - | |
| 742 | - if ( empty($host) || empty($api_key) ) { | |
| 743 | - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' ); | |
| 744 | - } | |
| 745 | - | |
| 746 | - // List vectors with the source_url prefix | |
| 747 | - $base_id = md5( $source_url ); | |
| 748 | - $vector_ids = array( $base_id ); | |
| 749 | - | |
| 750 | - // Find chunk vectors | |
| 751 | - $list_url = "https://{$host}/vectors/list"; | |
| 752 | - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 ); | |
| 753 | - if ( ! empty($namespace) ) { | |
| 754 | - $list_body['namespace'] = $namespace; | |
| 755 | - } | |
| 756 | - | |
| 757 | - $list_resp = wp_remote_post( $list_url, array( | |
| 758 | - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), | |
| 759 | - 'body' => wp_json_encode( $list_body ), | |
| 760 | - 'timeout' => 15, | |
| 761 | - ) ); | |
| 762 | - | |
| 763 | - if ( ! is_wp_error($list_resp) ) { | |
| 764 | - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true ); | |
| 765 | - if ( ! empty($list_data['vectors']) ) { | |
| 766 | - foreach ( $list_data['vectors'] as $v ) { | |
| 767 | - $vector_ids[] = $v['id']; | |
| 768 | - } | |
| 769 | - } | |
| 770 | - } | |
| 771 | - | |
| 772 | - // Fetch vectors with metadata | |
| 773 | - $fetch_url = "https://{$host}/vectors/fetch"; | |
| 774 | - $fetch_body = array( 'ids' => $vector_ids ); | |
| 775 | - if ( ! empty($namespace) ) { | |
| 776 | - $fetch_body['namespace'] = $namespace; | |
| 777 | - } | |
| 778 | - | |
| 779 | - $fetch_resp = wp_remote_post( $fetch_url, array( | |
| 780 | - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), | |
| 781 | - 'body' => wp_json_encode( $fetch_body ), | |
| 782 | - 'timeout' => 15, | |
| 783 | - ) ); | |
| 784 | - | |
| 785 | - if ( is_wp_error($fetch_resp) ) { | |
| 786 | - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' ); | |
| 787 | - } | |
| 788 | - | |
| 789 | - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true ); | |
| 790 | - $vectors = $fetch_data['vectors'] ?? array(); | |
| 791 | - | |
| 792 | - if ( empty($vectors) ) { | |
| 793 | - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' ); | |
| 794 | - } | |
| 795 | - | |
| 796 | - // Reassemble chunks | |
| 797 | - $chunks = array(); | |
| 798 | - $content_type = 'content'; | |
| 799 | - foreach ( $vectors as $vid => $vector ) { | |
| 800 | - $meta = $vector['metadata'] ?? array(); | |
| 801 | - $text = $meta['text'] ?? ''; | |
| 802 | - $index = $meta['chunk_index'] ?? 0; | |
| 803 | - $content_type = $meta['type'] ?? 'content'; | |
| 804 | - $chunks[ intval($index) ] = $text; | |
| 805 | - } | |
| 806 | - ksort( $chunks ); | |
| 807 | - | |
| 808 | - return array( | |
| 809 | - 'content' => implode( "\n\n", $chunks ), | |
| 810 | - 'source_url' => $source_url, | |
| 811 | - 'is_chunked' => count($chunks) > 1, | |
| 812 | - 'chunk_count' => count($chunks), | |
| 813 | - 'content_type' => $content_type, | |
| 814 | - ); | |
| 815 | -} | |
| 816 | - | |
| 817 | -/** | |
| 818 | - * AJAX: Save edited content — re-chunks and re-embeds as needed. | |
| 819 | - * Works for both WordPress DB and Pinecone entries. | |
| 820 | - */ | |
| 821 | -public function ajax_mxchat_save_entry_content() { | |
| 822 | - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce'); | |
| 823 | - | |
| 824 | - if ( ! current_user_can('manage_options') ) { | |
| 825 | - wp_send_json_error( array( 'message' => 'Permission denied.' ) ); | |
| 826 | - } | |
| 827 | - | |
| 828 | - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : ''; | |
| 829 | - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0; | |
| 830 | - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : ''; | |
| 831 | - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress'; | |
| 832 | - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; | |
| 833 | - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content'; | |
| 834 | - | |
| 835 | - if ( empty($content) ) { | |
| 836 | - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) ); | |
| 837 | - } | |
| 838 | - | |
| 839 | - // Get the embedding API key | |
| 840 | - $options = get_option('mxchat_options', array()); | |
| 841 | - $api_key = ''; | |
| 842 | - | |
| 843 | - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) { | |
| 844 | - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id); | |
| 845 | - $api_key = $bot_options['api_key'] ?? ''; | |
| 846 | - } | |
| 847 | - if ( empty($api_key) ) { | |
| 848 | - $api_key = $options['api_key'] ?? ''; | |
| 849 | - } | |
| 850 | - | |
| 851 | - global $wpdb; | |
| 852 | - $table = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 853 | - | |
| 854 | - // If source_url is empty but we have an entry_id, look it up | |
| 855 | - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) { | |
| 856 | - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) ); | |
| 857 | - if ( $row && ! empty($row->source_url) ) { | |
| 858 | - $source_url = $row->source_url; | |
| 859 | - } | |
| 860 | - } | |
| 861 | - | |
| 862 | - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first | |
| 863 | - // so submit_content_to_db creates a replacement instead of a duplicate | |
| 864 | - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries | |
| 865 | - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0; | |
| 866 | - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) { | |
| 867 | - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) ); | |
| 868 | - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier | |
| 869 | - // instead of reusing the shared URL (which would mass-delete other entries with the same URL) | |
| 870 | - if ( $is_legacy_manual ) { | |
| 871 | - $source_url = ''; | |
| 872 | - } | |
| 873 | - } | |
| 874 | - | |
| 875 | - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB | |
| 876 | - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id); | |
| 877 | - | |
| 878 | - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store | |
| 879 | - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type ); | |
| 880 | - | |
| 881 | - if ( is_wp_error($result) ) { | |
| 882 | - wp_send_json_error( array( 'message' => $result->get_error_message() ) ); | |
| 883 | - } | |
| 884 | - | |
| 885 | - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) ); | |
| 886 | -} | |
| 887 | - | |
| 888 | 624 | public function mxchat_get_pdf_processing_status($pdf_url) { |
| 889 | 625 | $pdf_url = esc_url_raw($pdf_url); |
| 890 | 626 | $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url))); |
| 891 | 627 | |
| @@ -947,22 +683,9 @@ | ||
| 947 | 683 | exit; |
| 948 | 684 | } |
| 949 | 685 | |
| 950 | 686 | $submitted_url = esc_url_raw($_POST['sitemap_url']); |
| 951 | - | |
| 952 | - // Convert Google Drive sharing URLs to direct download URLs | |
| 953 | - if ( strpos($submitted_url, 'drive.google.com') !== false ) { | |
| 954 | - $file_id = ''; | |
| 955 | - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) { | |
| 956 | - $file_id = $m[1]; | |
| 957 | - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) { | |
| 958 | - $file_id = $m[1]; | |
| 959 | - } | |
| 960 | - if ( ! empty($file_id) ) { | |
| 961 | - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id; | |
| 962 | - } | |
| 963 | - } | |
| 964 | - | |
| 687 | + | |
| 965 | 688 | // Get bot_id from form submission |
| 966 | 689 | $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; |
| 967 | 690 | |
| 968 | 691 | // Get bot-specific options and validate API key |
| @@ -990,18 +713,10 @@ | ||
| 990 | 713 | wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); |
| 991 | 714 | exit; |
| 992 | 715 | } |
| 993 | 716 | |
| 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 | - )); | |
| 717 | + // Fetch URL | |
| 718 | + $response = wp_remote_get($submitted_url, array('timeout' => 30)); | |
| 1004 | 719 | |
| 1005 | 720 | if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { |
| 1006 | 721 | $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response); |
| 1007 | 722 | set_transient('mxchat_admin_notice_error', |
| @@ -1908,10 +1623,8 @@ | ||
| 1908 | 1623 | 'preview_length' => $preview_length, |
| 1909 | 1624 | 'source_url' => $entry->source_url, |
| 1910 | 1625 | 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0, |
| 1911 | 1626 | 'chunk_metadata' => $chunk_metadata, |
| 1912 | - 'bot_id' => $entry->bot_id ?? 'default', | |
| 1913 | - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'), | |
| 1914 | 1627 | 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce') |
| 1915 | 1628 | ); |
| 1916 | 1629 | } |
| 1917 | 1630 | |
| @@ -2117,22 +1830,10 @@ | ||
| 2117 | 1830 | <?php else : ?> |
| 2118 | 1831 | <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span> |
| 2119 | 1832 | <?php endif; ?> |
| 2120 | 1833 | </td> |
| 2121 | - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;"> | |
| 2122 | - <?php if ($data_source !== 'pinecone') : ?> | |
| 1834 | + <td class="mxchat-actions-cell" style="padding: 12px 16px;"> | |
| 2123 | 1835 | <button type="button" |
| 2124 | - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn" | |
| 2125 | - data-source-url="<?php echo esc_attr($source_url); ?>" | |
| 2126 | - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>" | |
| 2127 | - data-data-source="<?php echo esc_attr($data_source); ?>" | |
| 2128 | - data-bot-id="<?php echo esc_attr($current_bot_id); ?>" | |
| 2129 | - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>" | |
| 2130 | - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>"> | |
| 2131 | - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span> | |
| 2132 | - </button> | |
| 2133 | - <?php endif; ?> | |
| 2134 | - <button type="button" | |
| 2135 | 1836 | class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group" |
| 2136 | 1837 | data-source-url="<?php echo esc_attr($source_url); ?>" |
| 2137 | 1838 | data-chunk-count="<?php echo esc_attr($chunk_count); ?>" |
| 2138 | 1839 | data-data-source="<?php echo esc_attr($data_source); ?>" |
| @@ -2585,22 +2286,10 @@ | ||
| 2585 | 2286 | <?php else : ?> |
| 2586 | 2287 | <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span> |
| 2587 | 2288 | <?php endif; ?> |
| 2588 | 2289 | </td> |
| 2589 | - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;"> | |
| 2590 | - <?php if ($data_source !== 'pinecone') : ?> | |
| 2290 | + <td class="mxchat-actions-cell" style="padding: 12px 16px;"> | |
| 2591 | 2291 | <button type="button" |
| 2592 | - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn" | |
| 2593 | - data-source-url="<?php echo esc_attr($source_url); ?>" | |
| 2594 | - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>" | |
| 2595 | - data-data-source="<?php echo esc_attr($data_source); ?>" | |
| 2596 | - data-bot-id="<?php echo esc_attr($current_bot_id); ?>" | |
| 2597 | - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>" | |
| 2598 | - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>"> | |
| 2599 | - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span> | |
| 2600 | - </button> | |
| 2601 | - <?php endif; ?> | |
| 2602 | - <button type="button" | |
| 2603 | 2292 | class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group" |
| 2604 | 2293 | data-source-url="<?php echo esc_attr($source_url); ?>" |
| 2605 | 2294 | data-chunk-count="<?php echo esc_attr($chunk_count); ?>" |
| 2606 | 2295 | data-data-source="<?php echo esc_attr($data_source); ?>" |
| @@ -2730,19 +2419,9 @@ | ||
| 2730 | 2419 | <?php else : ?> |
| 2731 | 2420 | <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span> |
| 2732 | 2421 | <?php endif; ?> |
| 2733 | 2422 | </td> |
| 2734 | - <td style="padding: 12px 16px; white-space: nowrap;"> | |
| 2735 | - <button type="button" | |
| 2736 | - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn" | |
| 2737 | - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>" | |
| 2738 | - data-entry-id="<?php echo esc_attr($prompt->id); ?>" | |
| 2739 | - data-data-source="<?php echo esc_attr($data_source); ?>" | |
| 2740 | - data-bot-id="<?php echo esc_attr($current_bot_id); ?>" | |
| 2741 | - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>" | |
| 2742 | - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>"> | |
| 2743 | - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span> | |
| 2744 | - </button> | |
| 2423 | + <td style="padding: 12px 16px;"> | |
| 2745 | 2424 | <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-wordpress" data-entry-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_wordpress_prompt_nonce'); ?>" style="color: var(--mxch-error);"> |
| 2746 | 2425 | <span class="dashicons dashicons-trash" style="font-size: 14px;"></span> |
| 2747 | 2426 | </button> |
| 2748 | 2427 | </td> |
| @@ -2835,12 +2514,11 @@ | ||
| 2835 | 2514 | foreach ($primary_indexes as $path => $source) { |
| 2836 | 2515 | $url = trailingslashit($site_url) . $path; |
| 2837 | 2516 | |
| 2838 | 2517 | $response = wp_remote_head($url, array( |
| 2839 | - 'timeout' => 10, | |
| 2518 | + 'timeout' => 3, // Short timeout | |
| 2840 | 2519 | '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', | |
| 2520 | + 'redirection' => 1 | |
| 2843 | 2521 | )); |
| 2844 | 2522 | |
| 2845 | 2523 | if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { |
| 2846 | 2524 | // Found a sitemap index - parse it to get sub-sitemaps |
| @@ -2898,15 +2576,10 @@ | ||
| 2898 | 2576 | private function parse_sitemap_index($url) { |
| 2899 | 2577 | $sub_sitemaps = array(); |
| 2900 | 2578 | |
| 2901 | 2579 | $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 | - ), | |
| 2580 | + 'timeout' => 5, | |
| 2581 | + 'sslverify' => false | |
| 2909 | 2582 | )); |
| 2910 | 2583 | |
| 2911 | 2584 | if (is_wp_error($response)) { |
| 2912 | 2585 | return $sub_sitemaps; |
| @@ -2957,15 +2630,10 @@ | ||
| 2957 | 2630 | * Get URL count from a sitemap |
| 2958 | 2631 | */ |
| 2959 | 2632 | private function get_sitemap_url_count($url) { |
| 2960 | 2633 | $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 | - ), | |
| 2634 | + 'timeout' => 10, | |
| 2635 | + 'sslverify' => false | |
| 2968 | 2636 | )); |
| 2969 | 2637 | |
| 2970 | 2638 | if (is_wp_error($response)) { |
| 2971 | 2639 | return 0; |
| @@ -2988,11 +2656,10 @@ | ||
| 2988 | 2656 | $sitemaps = array(); |
| 2989 | 2657 | $robots_url = trailingslashit($site_url) . 'robots.txt'; |
| 2990 | 2658 | |
| 2991 | 2659 | $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', | |
| 2660 | + 'timeout' => 5, | |
| 2661 | + 'sslverify' => false | |
| 2995 | 2662 | )); |
| 2996 | 2663 | |
| 2997 | 2664 | if (is_wp_error($response)) { |
| 2998 | 2665 | return $sitemaps; |
| @@ -3482,22 +3149,9 @@ | ||
| 3482 | 3149 | } |
| 3483 | 3150 | |
| 3484 | 3151 | // Get bot_id from request |
| 3485 | 3152 | $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 | - | |
| 3153 | + | |
| 3500 | 3154 | // Process only ONE post at a time to avoid request size issues |
| 3501 | 3155 | $post_id = reset($post_ids); |
| 3502 | 3156 | $post = get_post($post_id); |
| 3503 | 3157 | |
| @@ -3603,57 +3257,23 @@ | ||
| 3603 | 3257 | } |
| 3604 | 3258 | |
| 3605 | 3259 | // ADD ACF FIELDS SUPPORT |
| 3606 | 3260 | $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id); |
| 3607 | - $pdf_extracted_count = 0; | |
| 3608 | 3261 | if (!empty($acf_fields)) { |
| 3609 | 3262 | $acf_content_parts = array(); |
| 3610 | - $pdf_attachment_ids = array(); | |
| 3611 | - | |
| 3263 | + | |
| 3612 | 3264 | foreach ($acf_fields as $field_name => $field_value) { |
| 3613 | 3265 | $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id); |
| 3614 | - | |
| 3266 | + | |
| 3615 | 3267 | if (!empty($formatted_value)) { |
| 3616 | 3268 | $field_label = ucwords(str_replace('_', ' ', $field_name)); |
| 3617 | 3269 | $acf_content_parts[] = $field_label . ": " . $formatted_value; |
| 3618 | 3270 | } |
| 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 | 3271 | } |
| 3627 | - | |
| 3272 | + | |
| 3628 | 3273 | if (!empty($acf_content_parts)) { |
| 3629 | 3274 | $content .= "\n\n" . implode("\n", $acf_content_parts); |
| 3630 | 3275 | } |
| 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 | 3276 | } |
| 3657 | 3277 | |
| 3658 | 3278 | // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields) |
| 3659 | 3279 | $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id); |
| @@ -3783,9 +3403,8 @@ | ||
| 3783 | 3403 | 'title' => $post->post_title, |
| 3784 | 3404 | 'operation_type' => $operation_type, |
| 3785 | 3405 | 'vector_id' => $vector_id, |
| 3786 | 3406 | 'acf_fields_found' => $acf_field_count, |
| 3787 | - 'pdf_extracted_count' => (int) $pdf_extracted_count, | |
| 3788 | 3407 | 'content_preview' => substr($content, 0, 100) . '...', |
| 3789 | 3408 | 'bot_id' => $bot_id |
| 3790 | 3409 | )); |
| 3791 | 3410 | exit; |
| @@ -4200,20 +3819,9 @@ | ||
| 4200 | 3819 | |
| 4201 | 3820 | // Get bot-specific options |
| 4202 | 3821 | $bot_options = $this->get_bot_options($bot_id); |
| 4203 | 3822 | $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 | - | |
| 3823 | + | |
| 4216 | 3824 | $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; |
| 4217 | 3825 | //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model); |
| 4218 | 3826 | |
| 4219 | 3827 | // Determine provider and endpoint |
| @@ -5131,197 +4739,8 @@ | ||
| 5131 | 4739 | return implode(', ', array_filter($text_parts)); |
| 5132 | 4740 | } |
| 5133 | 4741 | |
| 5134 | 4742 | /** |
| 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 | 4743 | * Handle ACF save - fires after ACF fields are saved |
| 5325 | 4744 | * This ensures ACF field data is available when syncing to knowledge base |
| 5326 | 4745 | */ |
| 5327 | 4746 | public function mxchat_handle_acf_save($post_id) { |
| @@ -5429,33 +4848,40 @@ | ||
| 5429 | 4848 | // If the post was previously published but is now not published, remove from knowledge base |
| 5430 | 4849 | if ($previous_status === 'publish' && $post->post_status !== 'publish') { |
| 5431 | 4850 | // Use the stored URL from when it was published, or fall back to current permalink |
| 5432 | 4851 | $source_url = $previous_url ?: get_permalink($post_id); |
| 4852 | + | |
| 4853 | + if ($source_url) { | |
| 4854 | + // Check if Pinecone is enabled | |
| 4855 | + $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); | |
| 4856 | + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; | |
| 5433 | 4857 | |
| 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'); | |
| 4858 | + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { | |
| 4859 | + // Delete from Pinecone | |
| 4860 | + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); | |
| 4861 | + } else { | |
| 4862 | + // Delete from WordPress DB | |
| 4863 | + global $wpdb; | |
| 4864 | + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 4865 | + | |
| 4866 | + $result = $wpdb->delete( | |
| 4867 | + $table_name, | |
| 4868 | + array('source_url' => $source_url), | |
| 4869 | + array('%s') | |
| 4870 | + ); | |
| 4871 | + } | |
| 5437 | 4872 | } |
| 5438 | - | |
| 4873 | + | |
| 5439 | 4874 | // Clean up the transients and exit early |
| 5440 | 4875 | delete_transient($previous_status_key); |
| 5441 | 4876 | delete_transient($previous_url_key); |
| 5442 | 4877 | return; |
| 5443 | 4878 | } |
| 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 | - | |
| 4879 | + | |
| 5454 | 4880 | // Store the current status for next time (if this is an update) |
| 5455 | 4881 | if ($update) { |
| 5456 | 4882 | set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS); |
| 5457 | - | |
| 4883 | + | |
| 5458 | 4884 | // If the post is currently published, also store its URL |
| 5459 | 4885 | if ($post->post_status === 'publish') { |
| 5460 | 4886 | $current_url = get_permalink($post_id); |
| 5461 | 4887 | set_transient($previous_url_key, $current_url, DAY_IN_SECONDS); |
| @@ -5564,9 +4990,8 @@ | ||
| 5564 | 4990 | // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior) |
| 5565 | 4991 | $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id); |
| 5566 | 4992 | if (!empty($acf_fields)) { |
| 5567 | 4993 | $acf_content_parts = array(); |
| 5568 | - $pdf_attachment_ids = array(); | |
| 5569 | 4994 | |
| 5570 | 4995 | foreach ($acf_fields as $field_name => $field_value) { |
| 5571 | 4996 | $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id); |
| 5572 | 4997 | if (!empty($formatted_value)) { |
| @@ -5573,44 +4998,13 @@ | ||
| 5573 | 4998 | // Convert field name to readable label |
| 5574 | 4999 | $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name)); |
| 5575 | 5000 | $acf_content_parts[] = $field_label . ": " . $formatted_value; |
| 5576 | 5001 | } |
| 5577 | - | |
| 5578 | - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids); | |
| 5579 | 5002 | } |
| 5580 | 5003 | |
| 5581 | 5004 | if (!empty($acf_content_parts)) { |
| 5582 | 5005 | $final_content .= "\n\n" . implode("\n", $acf_content_parts); |
| 5583 | 5006 | } |
| 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 | 5007 | } |
| 5614 | 5008 | |
| 5615 | 5009 | // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields) |
| 5616 | 5010 | $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id); |
| @@ -5717,14 +5111,12 @@ | ||
| 5717 | 5111 | if (!$should_sync) { |
| 5718 | 5112 | return; |
| 5719 | 5113 | } |
| 5720 | 5114 | |
| 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); | |
| 5115 | + // Get the URL before post is deleted | |
| 5116 | + $source_url = get_permalink($post_id); | |
| 5725 | 5117 | if (!$source_url) { |
| 5726 | - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id); | |
| 5118 | + //error_log('MXChat: Failed to get permalink for post ' . $post_id); | |
| 5727 | 5119 | return; |
| 5728 | 5120 | } |
| 5729 | 5121 | |
| 5730 | 5122 | // Use chunk-aware deletion (handles both chunked and non-chunked content) |
| @@ -5732,36 +5124,56 @@ | ||
| 5732 | 5124 | |
| 5733 | 5125 | if (is_wp_error($delete_result)) { |
| 5734 | 5126 | //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message()); |
| 5735 | 5127 | } |
| 5128 | +} | |
| 5736 | 5129 | |
| 5737 | - delete_transient('mxchat_prev_url_' . $post_id); | |
| 5738 | - delete_transient('mxchat_prev_status_' . $post_id); | |
| 5739 | -} | |
| 5740 | 5130 | |
| 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 | - } | |
| 5131 | + /** | |
| 5132 | + * Deletes data from Pinecone using a source URL | |
| 5133 | + */ | |
| 5134 | + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) { | |
| 5135 | + $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; | |
| 5136 | + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; | |
| 5754 | 5137 | |
| 5755 | - $current = get_permalink($post_id); | |
| 5756 | - if (!$current) { | |
| 5757 | - return ''; | |
| 5758 | - } | |
| 5759 | - return preg_replace('#__trashed(/?)$#', '$1', $current); | |
| 5760 | -} | |
| 5138 | + if (empty($host) || empty($api_key)) { | |
| 5139 | + //error_log('MXChat: Pinecone deletion failed - missing configuration'); | |
| 5140 | + return false; | |
| 5141 | + } | |
| 5761 | 5142 | |
| 5143 | + $api_endpoint = "https://{$host}/vectors/delete"; | |
| 5144 | + $vector_id = md5($source_url); | |
| 5762 | 5145 | |
| 5146 | + $request_body = array( | |
| 5147 | + 'ids' => array($vector_id) | |
| 5148 | + ); | |
| 5763 | 5149 | |
| 5150 | + $response = wp_remote_post($api_endpoint, array( | |
| 5151 | + 'headers' => array( | |
| 5152 | + 'Api-Key' => $api_key, | |
| 5153 | + 'accept' => 'application/json', | |
| 5154 | + 'content-type' => 'application/json' | |
| 5155 | + ), | |
| 5156 | + 'body' => wp_json_encode($request_body), | |
| 5157 | + 'timeout' => 30 | |
| 5158 | + )); | |
| 5159 | + | |
| 5160 | + if (is_wp_error($response)) { | |
| 5161 | + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message()); | |
| 5162 | + return false; | |
| 5163 | + } | |
| 5164 | + | |
| 5165 | + $response_code = wp_remote_retrieve_response_code($response); | |
| 5166 | + if ($response_code !== 200) { | |
| 5167 | + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code); | |
| 5168 | + return false; | |
| 5169 | + } | |
| 5170 | + | |
| 5171 | + return true; | |
| 5172 | + } | |
| 5173 | + | |
| 5174 | + | |
| 5175 | + | |
| 5764 | 5176 | public function mxchat_handle_product_change($post_id, $post, $update) { |
| 5765 | 5177 | if ($post->post_type !== 'product') { |
| 5766 | 5178 | return; |
| 5767 | 5179 | } |
| @@ -5913,18 +5325,28 @@ | ||
| 5913 | 5325 | if (get_post_type($post_id) !== 'product') { |
| 5914 | 5326 | return; |
| 5915 | 5327 | } |
| 5916 | 5328 | |
| 5917 | - $source_url = $this->mxchat_resolve_pre_trash_url($post_id); | |
| 5918 | - if (!$source_url) { | |
| 5919 | - return; | |
| 5920 | - } | |
| 5329 | + $source_url = get_permalink($post_id); | |
| 5921 | 5330 | |
| 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'); | |
| 5331 | + // Check if Pinecone is enabled | |
| 5332 | + $pinecone_options = get_option('mxchat_pinecone_addon_options', array()); | |
| 5333 | + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1'; | |
| 5924 | 5334 | |
| 5925 | - delete_transient('mxchat_prev_url_' . $post_id); | |
| 5926 | - delete_transient('mxchat_prev_status_' . $post_id); | |
| 5335 | + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) { | |
| 5336 | + // Delete from Pinecone | |
| 5337 | + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options); | |
| 5338 | + } else { | |
| 5339 | + // Delete from WordPress DB | |
| 5340 | + global $wpdb; | |
| 5341 | + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 5342 | + | |
| 5343 | + $wpdb->delete( | |
| 5344 | + $table_name, | |
| 5345 | + array('source_url' => $source_url), | |
| 5346 | + array('%s') | |
| 5347 | + ); | |
| 5348 | + } | |
| 5927 | 5349 | } |
| 5928 | 5350 | |
| 5929 | 5351 | /** |
| 5930 | 5352 | * Handle individual Pinecone content deletion |
| @@ -7335,27 +6757,13 @@ | ||
| 7335 | 6757 | |
| 7336 | 6758 | $result = false; |
| 7337 | 6759 | $error_message = ''; |
| 7338 | 6760 | |
| 7339 | - // Read item directly from DB to get queue_id and preserve special chars in item_data | |
| 7340 | - // (POST round-trip through JS mangles characters like apostrophes in URLs) | |
| 7341 | - $db_item = $wpdb->get_row($wpdb->prepare( | |
| 7342 | - "SELECT queue_id, item_data FROM $table_name WHERE id = %d", | |
| 7343 | - $item_id | |
| 7344 | - )); | |
| 7345 | - $item_queue_id = $db_item ? $db_item->queue_id : ''; | |
| 7346 | - if ($db_item && !empty($db_item->item_data)) { | |
| 7347 | - $db_data = json_decode($db_item->item_data, true); | |
| 7348 | - if (is_array($db_data)) { | |
| 7349 | - $item_data = $db_data; | |
| 7350 | - } | |
| 7351 | - } | |
| 7352 | - | |
| 7353 | 6761 | switch ($item_type) { |
| 7354 | 6762 | case 'url': |
| 7355 | - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id); | |
| 6763 | + $result = $this->mxchat_process_queue_url($item_data, $bot_id); | |
| 7356 | 6764 | break; |
| 7357 | - | |
| 6765 | + | |
| 7358 | 6766 | case 'pdf_page': |
| 7359 | 6767 | $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id); |
| 7360 | 6768 | break; |
| 7361 | 6769 | |
| @@ -7363,37 +6771,11 @@ | ||
| 7363 | 6771 | throw new Exception('Unknown item type: ' . $item_type); |
| 7364 | 6772 | } |
| 7365 | 6773 | |
| 7366 | 6774 | if (is_wp_error($result)) { |
| 7367 | - $error_code = $result->get_error_code(); | |
| 7368 | - // Content errors (empty page, sanitization) are permanent — retrying won't help | |
| 7369 | - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found'); | |
| 7370 | - if (in_array($error_code, $permanent_codes)) { | |
| 7371 | - // Mark as permanently failed — set attempts = max_attempts so it won't be retried | |
| 7372 | - $current_item = $wpdb->get_row($wpdb->prepare( | |
| 7373 | - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id | |
| 7374 | - )); | |
| 7375 | - $wpdb->update( | |
| 7376 | - $table_name, | |
| 7377 | - array( | |
| 7378 | - 'status' => 'failed', | |
| 7379 | - 'error_message' => $result->get_error_message(), | |
| 7380 | - 'attempts' => $current_item ? $current_item->max_attempts : 3 | |
| 7381 | - ), | |
| 7382 | - array('id' => $item_id), | |
| 7383 | - array('%s', '%s', '%d'), | |
| 7384 | - array('%d') | |
| 7385 | - ); | |
| 7386 | - wp_send_json_error(array( | |
| 7387 | - 'message' => $result->get_error_message(), | |
| 7388 | - 'permanent_failure' => true, | |
| 7389 | - 'item_id' => $item_id | |
| 7390 | - )); | |
| 7391 | - return; | |
| 7392 | - } | |
| 7393 | 6775 | throw new Exception($result->get_error_message()); |
| 7394 | 6776 | } |
| 7395 | - | |
| 6777 | + | |
| 7396 | 6778 | if ($result === false) { |
| 7397 | 6779 | throw new Exception('Processing returned false - item may be empty or invalid'); |
| 7398 | 6780 | } |
| 7399 | 6781 | |
| @@ -7469,9 +6851,9 @@ | ||
| 7469 | 6851 | |
| 7470 | 6852 | /** |
| 7471 | 6853 | * Process a URL from the queue |
| 7472 | 6854 | */ |
| 7473 | -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') { | |
| 6855 | +private function mxchat_process_queue_url($item_data, $bot_id = 'default') { | |
| 7474 | 6856 | $url = isset($item_data['url']) ? $item_data['url'] : ''; |
| 7475 | 6857 | |
| 7476 | 6858 | if (empty($url)) { |
| 7477 | 6859 | return new WP_Error('invalid_url', 'URL is empty'); |
| @@ -7517,11 +6899,10 @@ | ||
| 7517 | 6899 | // If WooCommerce extraction failed, fall through to HTML extraction |
| 7518 | 6900 | } |
| 7519 | 6901 | |
| 7520 | 6902 | // Fetch URL content (fallback for non-products or when WooCommerce extraction fails) |
| 7521 | - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf'); | |
| 7522 | 6903 | $response = wp_remote_get($url, array( |
| 7523 | - 'timeout' => $is_likely_pdf ? 120 : 30, | |
| 6904 | + 'timeout' => 30, | |
| 7524 | 6905 | 'redirection' => 5, |
| 7525 | 6906 | 'user-agent' => 'MxChat/1.0' |
| 7526 | 6907 | )); |
| 7527 | 6908 | |
| @@ -7530,16 +6911,11 @@ | ||
| 7530 | 6911 | } |
| 7531 | 6912 | |
| 7532 | 6913 | $response_code = wp_remote_retrieve_response_code($response); |
| 7533 | 6914 | if ($response_code !== 200) { |
| 7534 | - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url); | |
| 6915 | + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error'); | |
| 7535 | 6916 | } |
| 7536 | 6917 | |
| 7537 | - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline | |
| 7538 | - if ($this->mxchat_is_pdf_url($url, $response)) { | |
| 7539 | - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id); | |
| 7540 | - } | |
| 7541 | - | |
| 7542 | 6918 | $html = wp_remote_retrieve_body($response); |
| 7543 | 6919 | |
| 7544 | 6920 | if (empty($html)) { |
| 7545 | 6921 | return new WP_Error('empty_response', 'Empty response body'); |
| @@ -7567,180 +6943,8 @@ | ||
| 7567 | 6943 | return $result; |
| 7568 | 6944 | } |
| 7569 | 6945 | |
| 7570 | 6946 | /** |
| 7571 | - * Expand a PDF URL into per-page queue items using the standard PDF pipeline. | |
| 7572 | - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count, | |
| 7573 | - * and adds pdf_page items to the same queue so they process with full progress tracking. | |
| 7574 | - */ | |
| 7575 | -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') { | |
| 7576 | - set_time_limit(120); // PDFs need extra time for download + parsing | |
| 7577 | - | |
| 7578 | - $upload_dir = wp_upload_dir(); | |
| 7579 | - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf'); | |
| 7580 | - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename; | |
| 7581 | - | |
| 7582 | - $response_body = wp_remote_retrieve_body($response); | |
| 7583 | - if (empty($response_body)) { | |
| 7584 | - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url); | |
| 7585 | - } | |
| 7586 | - | |
| 7587 | - if (!wp_mkdir_p(dirname($pdf_path))) { | |
| 7588 | - return new WP_Error('dir_error', 'Failed to create upload directory'); | |
| 7589 | - } | |
| 7590 | - | |
| 7591 | - file_put_contents($pdf_path, $response_body); | |
| 7592 | - | |
| 7593 | - if (!file_exists($pdf_path)) { | |
| 7594 | - return new WP_Error('save_error', 'Failed to save PDF file'); | |
| 7595 | - } | |
| 7596 | - | |
| 7597 | - try { | |
| 7598 | - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path); | |
| 7599 | - | |
| 7600 | - if ($total_pages === false || $total_pages < 1) { | |
| 7601 | - wp_delete_file($pdf_path); | |
| 7602 | - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url); | |
| 7603 | - } | |
| 7604 | - | |
| 7605 | - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base | |
| 7606 | - $pages = array(); | |
| 7607 | - for ($i = 1; $i <= $total_pages; $i++) { | |
| 7608 | - $pages[] = array( | |
| 7609 | - 'pdf_path' => $pdf_path, | |
| 7610 | - 'pdf_url' => $pdf_url, | |
| 7611 | - 'page_number' => $i, | |
| 7612 | - 'total_pages' => $total_pages | |
| 7613 | - ); | |
| 7614 | - } | |
| 7615 | - | |
| 7616 | - // Add pdf_page items to the SAME queue so the JS picks them up automatically | |
| 7617 | - if (!empty($queue_id)) { | |
| 7618 | - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id); | |
| 7619 | - } else { | |
| 7620 | - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow) | |
| 7621 | - $new_queue_id = 'pdf_' . md5($pdf_url . time()); | |
| 7622 | - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id); | |
| 7623 | - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url); | |
| 7624 | - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf'); | |
| 7625 | - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages); | |
| 7626 | - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id); | |
| 7627 | - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path); | |
| 7628 | - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql')); | |
| 7629 | - } | |
| 7630 | - | |
| 7631 | - if ($queued_count === 0) { | |
| 7632 | - wp_delete_file($pdf_path); | |
| 7633 | - return new WP_Error('queue_error', 'Failed to add PDF pages to queue'); | |
| 7634 | - } | |
| 7635 | - | |
| 7636 | - // Return true so the original URL item is marked complete | |
| 7637 | - // The new pdf_page items will be processed in subsequent batches | |
| 7638 | - return true; | |
| 7639 | - | |
| 7640 | - } catch (Exception $e) { | |
| 7641 | - if (file_exists($pdf_path)) { | |
| 7642 | - wp_delete_file($pdf_path); | |
| 7643 | - } | |
| 7644 | - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage()); | |
| 7645 | - } | |
| 7646 | -} | |
| 7647 | - | |
| 7648 | -/** | |
| 7649 | - * Legacy: Process a PDF URL inline during sitemap queue processing. | |
| 7650 | - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only. | |
| 7651 | - */ | |
| 7652 | -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') { | |
| 7653 | - set_time_limit(120); // PDFs need more time — downloading + parsing all pages | |
| 7654 | - | |
| 7655 | - $upload_dir = wp_upload_dir(); | |
| 7656 | - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf'); | |
| 7657 | - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename; | |
| 7658 | - | |
| 7659 | - $response_body = wp_remote_retrieve_body($response); | |
| 7660 | - if (empty($response_body)) { | |
| 7661 | - return new WP_Error('empty_pdf', 'Empty PDF response'); | |
| 7662 | - } | |
| 7663 | - | |
| 7664 | - if (!wp_mkdir_p(dirname($pdf_path))) { | |
| 7665 | - return new WP_Error('dir_error', 'Failed to create upload directory'); | |
| 7666 | - } | |
| 7667 | - | |
| 7668 | - file_put_contents($pdf_path, $response_body); | |
| 7669 | - | |
| 7670 | - if (!file_exists($pdf_path)) { | |
| 7671 | - return new WP_Error('save_error', 'Failed to save PDF file'); | |
| 7672 | - } | |
| 7673 | - | |
| 7674 | - try { | |
| 7675 | - mxchat_load_pdf_parser(); | |
| 7676 | - $parser = new \Smalot\PdfParser\Parser(); | |
| 7677 | - $pdf = $parser->parseFile($pdf_path); | |
| 7678 | - $pages = $pdf->getPages(); | |
| 7679 | - $total_pages = count($pages); | |
| 7680 | - | |
| 7681 | - if ($total_pages < 1) { | |
| 7682 | - wp_delete_file($pdf_path); | |
| 7683 | - return new WP_Error('no_pages', 'PDF has no pages'); | |
| 7684 | - } | |
| 7685 | - | |
| 7686 | - $processed = 0; | |
| 7687 | - $skipped_pages = array(); | |
| 7688 | - | |
| 7689 | - for ($i = 0; $i < $total_pages; $i++) { | |
| 7690 | - $page_num = $i + 1; | |
| 7691 | - $text = $pages[$i]->getText(); | |
| 7692 | - if (empty($text)) { | |
| 7693 | - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding'; | |
| 7694 | - continue; | |
| 7695 | - } | |
| 7696 | - | |
| 7697 | - $sanitized = $this->mxchat_sanitize_content_for_api($text); | |
| 7698 | - if (empty($sanitized)) { | |
| 7699 | - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content'; | |
| 7700 | - continue; | |
| 7701 | - } | |
| 7702 | - | |
| 7703 | - $metadata = array( | |
| 7704 | - 'document_type' => 'pdf', | |
| 7705 | - 'total_pages' => $total_pages, | |
| 7706 | - 'current_page' => $page_num, | |
| 7707 | - 'source_url' => $pdf_url, | |
| 7708 | - ); | |
| 7709 | - | |
| 7710 | - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized; | |
| 7711 | - $page_url = esc_url($pdf_url . '#page=' . $page_num); | |
| 7712 | - | |
| 7713 | - MxChat_Utils::submit_content_to_db( | |
| 7714 | - $content_with_metadata, | |
| 7715 | - $page_url, | |
| 7716 | - $api_key, | |
| 7717 | - null, | |
| 7718 | - $bot_id, | |
| 7719 | - 'pdf' | |
| 7720 | - ); | |
| 7721 | - | |
| 7722 | - $processed++; | |
| 7723 | - } | |
| 7724 | - | |
| 7725 | - // Clean up the temp PDF file | |
| 7726 | - wp_delete_file($pdf_path); | |
| 7727 | - | |
| 7728 | - if (!empty($skipped_pages)) { | |
| 7729 | - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages)); | |
| 7730 | - } | |
| 7731 | - | |
| 7732 | - return $processed > 0 ? true : false; | |
| 7733 | - | |
| 7734 | - } catch (Exception $e) { | |
| 7735 | - if (file_exists($pdf_path)) { | |
| 7736 | - wp_delete_file($pdf_path); | |
| 7737 | - } | |
| 7738 | - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage()); | |
| 7739 | - } | |
| 7740 | -} | |
| 7741 | - | |
| 7742 | -/** | |
| 7743 | 6947 | * Extract WooCommerce product content including pricing |
| 7744 | 6948 | * |
| 7745 | 6949 | * @param string $url The product URL |
| 7746 | 6950 | * @return string|false Product content with pricing, or false if not found |
| @@ -7894,17 +7098,18 @@ | ||
| 7894 | 7098 | return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF'); |
| 7895 | 7099 | } |
| 7896 | 7100 | |
| 7897 | 7101 | $text = $pages[$page_number - 1]->getText(); |
| 7898 | - | |
| 7102 | + | |
| 7899 | 7103 | if (empty($text)) { |
| 7900 | - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding'); | |
| 7104 | + // Not an error - just an empty page | |
| 7105 | + return false; | |
| 7901 | 7106 | } |
| 7902 | - | |
| 7107 | + | |
| 7903 | 7108 | $sanitized = $this->mxchat_sanitize_content_for_api($text); |
| 7904 | - | |
| 7109 | + | |
| 7905 | 7110 | if (empty($sanitized)) { |
| 7906 | - return new WP_Error('empty_after_sanitization', 'Page ' . $page_number . ': Text was extracted but contained only special characters, control codes, or unsupported content that was removed during cleanup'); | |
| 7111 | + return false; | |
| 7907 | 7112 | } |
| 7908 | 7113 | |
| 7909 | 7114 | // Create metadata |
| 7910 | 7115 | $metadata = array( |
| @@ -8008,16 +7213,17 @@ | ||
| 8008 | 7213 | |
| 8009 | 7214 | // Calculate percentage |
| 8010 | 7215 | $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0; |
| 8011 | 7216 | |
| 8012 | - // Get failed items details (include all failed items, not just those that exhausted retries) | |
| 7217 | + // Get failed items details | |
| 8013 | 7218 | $failed_items = array(); |
| 8014 | 7219 | if ($failed > 0) { |
| 8015 | 7220 | $failed_items = $wpdb->get_results($wpdb->prepare( |
| 8016 | - "SELECT item_type, item_data, error_message, attempts | |
| 8017 | - FROM $table_name | |
| 8018 | - WHERE queue_id = %s | |
| 7221 | + "SELECT item_type, item_data, error_message, attempts | |
| 7222 | + FROM $table_name | |
| 7223 | + WHERE queue_id = %s | |
| 8019 | 7224 | AND status = 'failed' |
| 7225 | + AND attempts >= max_attempts | |
| 8020 | 7226 | ORDER BY id DESC |
| 8021 | 7227 | LIMIT 50", |
| 8022 | 7228 | $queue_id |
| 8023 | 7229 | )); |