Plugin Directory

Changeset 3658740


Ignore:
Timestamp:
08/21/2026 09:59:19 AM (2 months ago)
Author:
fernandofas2909
Message:

implementation of Ai helper and minor changes

Location:
seo-auditor-tools
Files:
52 added
15 edited

Legend:

Unmodified
Added
Removed
  • seo-auditor-tools/trunk/assets/admin.js

    r3561078 r3658740  
    4646                .toggleClass('dashicons-visibility', !isHidden)
    4747                .toggleClass('dashicons-hidden', isHidden);
     48        });
     49
     50        // Read-only-on-load prevents password managers from treating API fields
     51        // as login-password fields. A deliberate user interaction unlocks them.
     52        $('.seoautopro-wrap').on('pointerdown keydown focus', '.seoautopro-sensitive-input[readonly]', function() {
     53            $(this).removeAttr('readonly');
    4854        });
    4955
    … …  
    686692        }
    687693
     694        // AI Helper provider, model, endpoint, and language controls.
     695        var aiProviderDataNode = document.getElementById('seoautopro-ai-provider-data');
     696        if (aiProviderDataNode) {
     697            var aiData = {};
     698            try {
     699                aiData = JSON.parse(aiProviderDataNode.textContent || '{}');
     700            } catch (error) {
     701                aiData = {};
     702            }
     703
     704            var providers = aiData.providers || {};
     705            var savedModels = aiData.models || {};
     706            var savedEndpoints = aiData.endpoints || {};
     707            var configuredKeys = aiData.configuredKeys || {};
     708            var $tier = $('#seoautopro-ai-tier');
     709            var $provider = $('#seoautopro-ai-provider');
     710            var $model = $('#seoautopro-ai-model');
     711
     712            function providerSupportsTier(definition, tier) {
     713                return definition && Array.isArray(definition.tiers) && definition.tiers.indexOf(tier) !== -1;
     714            }
     715
     716            function refreshProviders(keepCurrent) {
     717                var tier = $tier.val();
     718                var current = keepCurrent ? $provider.val() : '';
     719                $provider.empty();
     720                $.each(providers, function(id, definition) {
     721                    if (providerSupportsTier(definition, tier)) {
     722                        $('<option>').val(id).text(definition.label || id).appendTo($provider);
     723                    }
     724                });
     725                if (current && $provider.find('option[value="' + current + '"]').length) {
     726                    $provider.val(current);
     727                }
     728                refreshProviderFields(true);
     729            }
     730
     731            function refreshProviderFields(keepCurrent) {
     732                var id = $provider.val();
     733                var tier = $tier.val();
     734                var definition = providers[id] || {};
     735                var options = (definition.models && definition.models[tier]) || [];
     736                var current = keepCurrent ? (savedModels[id] || '') : '';
     737                var isListed = current && options.indexOf(current) !== -1;
     738                $model.empty();
     739                $.each(options, function(index, model) {
     740                    $('<option>').val(model).text(model === '__custom__' ? 'Custom model…' : model).appendTo($model);
     741                });
     742                if (current && isListed) {
     743                    $model.val(current);
     744                } else if (current && options.indexOf('__custom__') !== -1) {
     745                    $model.val('__custom__');
     746                    $('#seoautopro-ai-custom-model').val(current);
     747                }
     748
     749                var endpoint = savedEndpoints[id] || definition.endpoint || '';
     750                $('#seoautopro-ai-endpoint').val(endpoint);
     751                $('#seoautopro-ai-endpoint-row').toggle(Boolean(definition.endpoint_editable));
     752                $('#seoautopro-ai-api-key-row').toggle(Boolean(definition.api_key_required) || id === 'custom_openai');
     753                $('#seoautopro-ai-api-key').val('').attr('placeholder', configuredKeys[id] ? 'Key configured — leave blank to keep it' : 'Enter provider API key');
     754                refreshCustomModel();
     755            }
     756
     757            function refreshCustomModel() {
     758                $('#seoautopro-ai-custom-model-row').toggle($model.val() === '__custom__');
     759            }
     760
     761            $tier.on('change', function() { refreshProviders(false); });
     762            $provider.on('change', function() { refreshProviderFields(true); });
     763            $model.on('change', refreshCustomModel);
     764            $('#seoautopro-ai-language').on('change', function() {
     765                $('#seoautopro-ai-custom-language-row').toggle($(this).val() === '__custom__');
     766            });
     767        }
     768
    688769        function escapeHtml(text) {
    689770            if (!text) return '';
  • seo-auditor-tools/trunk/assets/admin.min.js

    r3561078 r3658740  
    4646                .toggleClass('dashicons-visibility', !isHidden)
    4747                .toggleClass('dashicons-hidden', isHidden);
     48        });
     49
     50        // Read-only-on-load prevents password managers from treating API fields
     51        // as login-password fields. A deliberate user interaction unlocks them.
     52        $('.seoautopro-wrap').on('pointerdown keydown focus', '.seoautopro-sensitive-input[readonly]', function() {
     53            $(this).removeAttr('readonly');
    4854        });
    4955
    … …  
    686692        }
    687693
     694        // AI Helper provider, model, endpoint, and language controls.
     695        var aiProviderDataNode = document.getElementById('seoautopro-ai-provider-data');
     696        if (aiProviderDataNode) {
     697            var aiData = {};
     698            try {
     699                aiData = JSON.parse(aiProviderDataNode.textContent || '{}');
     700            } catch (error) {
     701                aiData = {};
     702            }
     703
     704            var providers = aiData.providers || {};
     705            var savedModels = aiData.models || {};
     706            var savedEndpoints = aiData.endpoints || {};
     707            var configuredKeys = aiData.configuredKeys || {};
     708            var $tier = $('#seoautopro-ai-tier');
     709            var $provider = $('#seoautopro-ai-provider');
     710            var $model = $('#seoautopro-ai-model');
     711
     712            function providerSupportsTier(definition, tier) {
     713                return definition && Array.isArray(definition.tiers) && definition.tiers.indexOf(tier) !== -1;
     714            }
     715
     716            function refreshProviders(keepCurrent) {
     717                var tier = $tier.val();
     718                var current = keepCurrent ? $provider.val() : '';
     719                $provider.empty();
     720                $.each(providers, function(id, definition) {
     721                    if (providerSupportsTier(definition, tier)) {
     722                        $('<option>').val(id).text(definition.label || id).appendTo($provider);
     723                    }
     724                });
     725                if (current && $provider.find('option[value="' + current + '"]').length) {
     726                    $provider.val(current);
     727                }
     728                refreshProviderFields(true);
     729            }
     730
     731            function refreshProviderFields(keepCurrent) {
     732                var id = $provider.val();
     733                var tier = $tier.val();
     734                var definition = providers[id] || {};
     735                var options = (definition.models && definition.models[tier]) || [];
     736                var current = keepCurrent ? (savedModels[id] || '') : '';
     737                var isListed = current && options.indexOf(current) !== -1;
     738                $model.empty();
     739                $.each(options, function(index, model) {
     740                    $('<option>').val(model).text(model === '__custom__' ? 'Custom model…' : model).appendTo($model);
     741                });
     742                if (current && isListed) {
     743                    $model.val(current);
     744                } else if (current && options.indexOf('__custom__') !== -1) {
     745                    $model.val('__custom__');
     746                    $('#seoautopro-ai-custom-model').val(current);
     747                }
     748
     749                var endpoint = savedEndpoints[id] || definition.endpoint || '';
     750                $('#seoautopro-ai-endpoint').val(endpoint);
     751                $('#seoautopro-ai-endpoint-row').toggle(Boolean(definition.endpoint_editable));
     752                $('#seoautopro-ai-api-key-row').toggle(Boolean(definition.api_key_required) || id === 'custom_openai');
     753                $('#seoautopro-ai-api-key').val('').attr('placeholder', configuredKeys[id] ? 'Key configured — leave blank to keep it' : 'Enter provider API key');
     754                refreshCustomModel();
     755            }
     756
     757            function refreshCustomModel() {
     758                $('#seoautopro-ai-custom-model-row').toggle($model.val() === '__custom__');
     759            }
     760
     761            $tier.on('change', function() { refreshProviders(false); });
     762            $provider.on('change', function() { refreshProviderFields(true); });
     763            $model.on('change', refreshCustomModel);
     764            $('#seoautopro-ai-language').on('change', function() {
     765                $('#seoautopro-ai-custom-language-row').toggle($(this).val() === '__custom__');
     766            });
     767        }
     768
    688769        function escapeHtml(text) {
    689770            if (!text) return '';
  • seo-auditor-tools/trunk/assets/chatbot.css

    r3592712 r3658740  
    7878    padding: 12px 16px;
    7979    font-weight: 600;
     80    display: flex;
     81    align-items: center;
     82    justify-content: space-between;
     83    gap: 12px;
     84}
     85
     86.seoautopro-chatbot-refresh {
     87    appearance: none;
     88    width: 30px;
     89    height: 30px;
     90    background: transparent;
     91    color: inherit;
     92    font: inherit;
     93    font-size: 20px;
     94    line-height: 1;
     95    padding: 0;
     96    cursor: pointer;
     97    opacity: 0.9;
     98}
     99
     100.seoautopro-chatbot-refresh:hover,
     101.seoautopro-chatbot-refresh:focus-visible {
     102    opacity: 1;
     103    outline-offset: 2px;
    80104}
    81105
    … …  
    197221}
    198222
     223.seoautopro-chatbot-contact-actions {
     224    display: flex;
     225    gap: 8px;
     226}
     227
     228.seoautopro-chatbot-contact-actions button {
     229    flex: 1 1 0;
     230}
     231
     232.seoautopro-chatbot-contact .seoautopro-chatbot-continue {
     233    background: transparent;
     234    color: var(--seoautopro-chat-primary);
     235    border: 1px solid var(--seoautopro-chat-primary);
     236}
     237
    199238.seoautopro-chatbot-input button:disabled,
    200239.seoautopro-chatbot-contact button:disabled {
  • seo-auditor-tools/trunk/assets/chatbot.js

    r3592712 r3658740  
    1616        var $input = $inputForm.find('input');
    1717        var $contactForm = $chat.find('.seoautopro-chatbot-contact');
     18        var $refresh = $chat.find('.seoautopro-chatbot-refresh');
     19        var $continue = $chat.find('.seoautopro-chatbot-continue');
     20        var conversationId = 0;
    1821
    1922        function scrollToBottom() {
    … …  
    8992        }
    9093
     94        function startNewConversation() {
     95            conversationId += 1;
     96            $messages.empty();
     97            $input.val('');
     98            $contactForm.trigger('reset');
     99            $inputForm.find('button').prop('disabled', false);
     100            $contactForm.find('button').prop('disabled', false);
     101            hideContactForm();
     102            if (SEOAUTOPRO_CHATBOT.greeting) {
     103                appendMessage(SEOAUTOPRO_CHATBOT.greeting, 'bot');
     104            }
     105            $input.trigger('focus');
     106        }
     107
    91108        setOpen(false);
    92109
    … …  
    97114        $toggle.on('click', function() {
    98115            setOpen(!$window.hasClass('is-open'));
     116        });
     117
     118        $refresh.on('click', function() {
     119            startNewConversation();
     120        });
     121
     122        $continue.on('click', function() {
     123            hideContactForm();
     124            $input.trigger('focus');
    99125        });
    100126
    … …  
    107133
    108134            var $btn = $inputForm.find('button');
     135            var requestConversation = conversationId;
    109136            $btn.prop('disabled', true);
    110137            appendMessage(question, 'user');
    … …  
    117144                question: question
    118145            }).done(function(response) {
     146                if (requestConversation !== conversationId) {
     147                    return;
     148                }
    119149                if (!response || !response.success || !response.data) {
    120                     appendMessage('Sorry, something went wrong.', 'bot');
     150                    appendMessage(SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.', 'bot');
    121151                    showContactForm();
    122152                    return;
    … …  
    126156                appendSources(response.data.sources || []);
    127157                if (response.data.no_answer) {
    128                     appendMessage('Would you like to send a message to our support team for further assistance?', 'bot');
     158                    appendMessage(SEOAUTOPRO_CHATBOT.support_prompt || 'Would you like to send a message to our support team for further assistance?', 'bot');
    129159                    showContactForm();
    130160                } else {
    131161                    hideContactForm();
    132162                }
    133             }).fail(function() {
    134                 appendMessage('Sorry, something went wrong.', 'bot');
     163            }).fail(function(xhr) {
     164                if (requestConversation !== conversationId) {
     165                    return;
     166                }
     167                var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message
     168                    ? xhr.responseJSON.data.message
     169                    : (SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.');
     170                appendMessage(message, 'bot');
    135171                showContactForm();
    136172            }).always(function() {
    137                 $btn.prop('disabled', false);
     173                if (requestConversation === conversationId) {
     174                    $btn.prop('disabled', false);
     175                }
    138176            });
    139177        });
    … …  
    142180            event.preventDefault();
    143181
    144             var $btn = $contactForm.find('button');
     182            var $btn = $contactForm.find('button[type="submit"]');
     183            var requestConversation = conversationId;
    145184            $btn.prop('disabled', true);
    146185
    … …  
    156195
    157196            $.post(SEOAUTOPRO_CHATBOT.ajax_url, payload).done(function(response) {
     197                if (requestConversation !== conversationId) {
     198                    return;
     199                }
    158200                if (!response || !response.success || !response.data) {
    159201                    appendMessage('Unable to send your message. Please try again.', 'bot');
    … …  
    165207                $contactForm.trigger('reset');
    166208                hideContactForm();
    167             }).fail(function() {
    168                 appendMessage('Unable to send your message. Please try again.', 'bot');
     209            }).fail(function(xhr) {
     210                if (requestConversation !== conversationId) {
     211                    return;
     212                }
     213                var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message
     214                    ? xhr.responseJSON.data.message
     215                    : 'Unable to send your message. Please try again.';
     216                appendMessage(message, 'bot');
    169217            }).always(function() {
    170                 $btn.prop('disabled', false);
     218                if (requestConversation === conversationId) {
     219                    $btn.prop('disabled', false);
     220                }
    171221            });
    172222        });
  • seo-auditor-tools/trunk/assets/chatbot.min.css

    r3592712 r3658740  
    1 .seoautopro-chatbot {
    2     position: fixed;
    3     right: 24px;
    4     bottom: 24px;
    5     z-index: 9999;
    6     font-family: "Segoe UI", Arial, sans-serif;
    7     --seoautopro-chat-primary: #0b6b8f;
    8     --seoautopro-chat-bg: #ffffff;
    9     --seoautopro-chat-text: #1b2a36;
    10     --seoautopro-chat-bot: #f2f5f8;
    11     --seoautopro-chat-user: #0b6b8f;
    12     --seoautopro-chat-user-text: #ffffff;
    13 }
    14 
    15 .seoautopro-chatbot *,
    16 .seoautopro-chatbot *::before,
    17 .seoautopro-chatbot *::after {
    18     box-sizing: border-box;
    19 }
    20 
    21 .seoautopro-chatbot-toggle {
    22     background: var(--seoautopro-chat-primary);
    23     color: var(--seoautopro-chat-user-text);
    24     border: none;
    25     border-radius: 999px;
    26     padding: 10px 18px;
    27     font-weight: 600;
    28     cursor: pointer;
    29     box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
    30     display: inline-flex;
    31     align-items: center;
    32     gap: 8px;
    33 }
    34 
    35 .seoautopro-chatbot-toggle-icon {
    36     width: 20px;
    37     height: 20px;
    38     object-fit: contain;
    39     border-radius: 50%;
    40 }
    41 
    42 .seoautopro-chatbot-toggle--icon-only {
    43     width: 56px;
    44     height: 56px;
    45     padding: 0;
    46     border-radius: 50%;
    47     justify-content: center;
    48     overflow: hidden;
    49 }
    50 
    51 .seoautopro-chatbot-toggle--icon-only .seoautopro-chatbot-toggle-icon {
    52     width: 100%;
    53     height: 100%;
    54     object-fit: cover;
    55 }
    56 
    57 .seoautopro-chatbot-window {
    58     width: 320px;
    59     max-height: 70vh;
    60     min-height: 20vh;
    61     background: var(--seoautopro-chat-bg);
    62     color: var(--seoautopro-chat-text);
    63     border-radius: 16px;
    64     box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
    65     margin-top: 12px;
    66     display: none;
    67     overflow-y: auto;
    68     flex-direction: column;
    69 }
    70 
    71 .seoautopro-chatbot-window.is-open {
    72     display: flex;
    73 }
    74 
    75 .seoautopro-chatbot-header {
    76     background: var(--seoautopro-chat-primary);
    77     color: var(--seoautopro-chat-user-text);
    78     padding: 12px 16px;
    79     font-weight: 600;
    80 }
    81 
    82 .seoautopro-chatbot-messages {
    83     padding: 16px;
    84     overflow-y: auto;
    85     display: flex;
    86     flex-direction: column;
    87     gap: 10px;
    88     flex: 1 1 auto;
    89 }
    90 
    91 .seoautopro-chatbot-message {
    92     font-size: 13px;
    93     line-height: 1.5;
    94     padding: 10px 12px;
    95     border-radius: 12px;
    96     max-width: 85%;
    97     word-wrap: break-word;
    98     overflow-wrap: break-word;
    99 }
    100 
    101 .seoautopro-chatbot-message--bot {
    102     background: var(--seoautopro-chat-bot);
    103     color: var(--seoautopro-chat-text);
    104     align-self: flex-start;
    105 }
    106 
    107 .seoautopro-chatbot-message--user {
    108     background: var(--seoautopro-chat-user);
    109     color: var(--seoautopro-chat-user-text);
    110     align-self: flex-end;
    111 }
    112 
    113 .seoautopro-chatbot-results {
    114     display: flex;
    115     flex-direction: column;
    116     gap: 8px;
    117     width: 100%;
    118 }
    119 
    120 .seoautopro-chatbot-result-card {
    121     background: var(--seoautopro-chat-bot);
    122     border-radius: 10px;
    123     padding: 10px 12px;
    124     display: flex;
    125     flex-direction: column;
    126     gap: 4px;
    127 }
    128 
    129 .seoautopro-chatbot-result-title {
    130     font-size: 13px;
    131     font-weight: 600;
    132     color: var(--seoautopro-chat-primary);
    133     text-decoration: none;
    134     line-height: 1.3;
    135 }
    136 
    137 a.seoautopro-chatbot-result-title:hover {
    138     text-decoration: underline;
    139 }
    140 
    141 .seoautopro-chatbot-result-excerpt {
    142     font-size: 12px;
    143     line-height: 1.45;
    144     color: var(--seoautopro-chat-text);
    145     margin: 2px 0 0;
    146     opacity: 0.85;
    147 }
    148 
    149 .seoautopro-chatbot-sources {
    150     margin: 0;
    151     padding-left: 18px;
    152     font-size: 12px;
    153     color: #1b2a36;
    154 }
    155 
    156 .seoautopro-chatbot-input,
    157 .seoautopro-chatbot-contact {
    158     padding: 12px 16px 16px;
    159     border-top: 1px solid #e6edf2;
    160 }
    161 
    162 .seoautopro-chatbot-input {
    163     display: flex;
    164     gap: 8px;
    165 }
    166 
    167 .seoautopro-chatbot-input input {
    168     flex: 1;
    169     min-width: 0;
    170     padding: 8px 10px;
    171     border-radius: 8px;
    172     border: 1px solid #d2dbe3;
    173     font-size: 13px;
    174 }
    175 
    176 .seoautopro-chatbot-input button {
    177     background: var(--seoautopro-chat-primary);
    178     color: var(--seoautopro-chat-user-text);
    179     border: none;
    180     border-radius: 8px;
    181     padding: 8px 14px;
    182     font-weight: 600;
    183     cursor: pointer;
    184     white-space: nowrap;
    185     flex-shrink: 0;
    186 }
    187 
    188 .seoautopro-chatbot-contact button {
    189     background: var(--seoautopro-chat-primary);
    190     color: var(--seoautopro-chat-user-text);
    191     border: none;
    192     border-radius: 8px;
    193     padding: 10px 12px;
    194     font-weight: 600;
    195     cursor: pointer;
    196     width: 100%;
    197 }
    198 
    199 .seoautopro-chatbot-input button:disabled,
    200 .seoautopro-chatbot-contact button:disabled {
    201     opacity: 0.6;
    202     cursor: not-allowed;
    203 }
    204 
    205 .seoautopro-chatbot-contact {
    206     display: none;
    207     flex-direction: column;
    208     gap: 8px;
    209 }
    210 
    211 .seoautopro-chatbot-contact input,
    212 .seoautopro-chatbot-contact textarea {
    213     width: 100%;
    214     padding: 8px 10px;
    215     border-radius: 8px;
    216     border: 1px solid #d2dbe3;
    217     font-size: 13px;
    218     font-family: inherit;
    219 }
    220 
    221 .seoautopro-chatbot-contact textarea {
    222     resize: vertical;
    223 }
    224 
    225 .seoautopro-chatbot-contact p {
    226     margin: 0 0 6px;
    227     font-size: 12px;
    228     line-height: 1.4;
    229     color: var(--seoautopro-chat-text);
    230 }
    231 
    232 @media (max-width: 480px) {
    233     .seoautopro-chatbot {
    234         right: 16px;
    235         bottom: 16px;
    236     }
    237 
    238     .seoautopro-chatbot-window {
    239         width: calc(100vw - 32px);
    240         max-height: 70vh;
    241     }
    242 }
     1.seoautopro-chatbot{position:fixed;right:24px;bottom:24px;z-index:9999;font-family:"Segoe UI",Arial,sans-serif;--seoautopro-chat-primary:#0b6b8f;--seoautopro-chat-bg:#ffffff;--seoautopro-chat-text:#1b2a36;--seoautopro-chat-bot:#f2f5f8;--seoautopro-chat-user:#0b6b8f;--seoautopro-chat-user-text:#ffffff}.seoautopro-chatbot *,.seoautopro-chatbot ::after,.seoautopro-chatbot ::before{box-sizing:border-box}.seoautopro-chatbot-toggle{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);border:none;border-radius:999px;padding:10px 18px;font-weight:600;cursor:pointer;box-shadow:0 10px 24px rgba(0,0,0,.2);display:inline-flex;align-items:center;gap:8px}.seoautopro-chatbot-toggle-icon{width:20px;height:20px;object-fit:contain;border-radius:50%}.seoautopro-chatbot-toggle--icon-only{width:56px;height:56px;padding:0;border-radius:50%;justify-content:center;overflow:hidden}.seoautopro-chatbot-toggle--icon-only .seoautopro-chatbot-toggle-icon{width:100%;height:100%;object-fit:cover}.seoautopro-chatbot-window{width:320px;max-height:70vh;min-height:20vh;background:var(--seoautopro-chat-bg);color:var(--seoautopro-chat-text);border-radius:16px;box-shadow:0 16px 40px rgba(0,0,0,.2);margin-top:12px;display:none;overflow-y:auto;flex-direction:column}.seoautopro-chatbot-window.is-open{display:flex}.seoautopro-chatbot-header{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);padding:12px 16px;font-weight:600;display:flex;align-items:center;justify-content:space-between;gap:12px}.seoautopro-chatbot-refresh{appearance:none;width:30px;height:30px;background:0 0;color:inherit;font:inherit;font-size:20px;line-height:1;padding:0;cursor:pointer;opacity:.9}.seoautopro-chatbot-refresh:focus-visible,.seoautopro-chatbot-refresh:hover{opacity:1;outline-offset:2px}.seoautopro-chatbot-messages{padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:10px;flex:1 1 auto}.seoautopro-chatbot-message{font-size:13px;line-height:1.5;padding:10px 12px;border-radius:12px;max-width:85%;word-wrap:break-word;overflow-wrap:break-word}.seoautopro-chatbot-message--bot{background:var(--seoautopro-chat-bot);color:var(--seoautopro-chat-text);align-self:flex-start}.seoautopro-chatbot-message--user{background:var(--seoautopro-chat-user);color:var(--seoautopro-chat-user-text);align-self:flex-end}.seoautopro-chatbot-results{display:flex;flex-direction:column;gap:8px;width:100%}.seoautopro-chatbot-result-card{background:var(--seoautopro-chat-bot);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:4px}.seoautopro-chatbot-result-title{font-size:13px;font-weight:600;color:var(--seoautopro-chat-primary);text-decoration:none;line-height:1.3}a.seoautopro-chatbot-result-title:hover{text-decoration:underline}.seoautopro-chatbot-result-excerpt{font-size:12px;line-height:1.45;color:var(--seoautopro-chat-text);margin:2px 0 0;opacity:.85}.seoautopro-chatbot-sources{margin:0;padding-left:18px;font-size:12px;color:#1b2a36}.seoautopro-chatbot-contact,.seoautopro-chatbot-input{padding:12px 16px 16px;border-top:1px solid #e6edf2}.seoautopro-chatbot-input{display:flex;gap:8px}.seoautopro-chatbot-input input{flex:1;min-width:0;padding:8px 10px;border-radius:8px;border:1px solid #d2dbe3;font-size:13px}.seoautopro-chatbot-input button{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);border:none;border-radius:8px;padding:8px 14px;font-weight:600;cursor:pointer;white-space:nowrap;flex-shrink:0}.seoautopro-chatbot-contact button{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);border:none;border-radius:8px;padding:10px 12px;font-weight:600;cursor:pointer;width:100%}.seoautopro-chatbot-contact-actions{display:flex;gap:8px}.seoautopro-chatbot-contact-actions button{flex:1 1 0}.seoautopro-chatbot-contact .seoautopro-chatbot-continue{background:0 0;color:var(--seoautopro-chat-primary);border:1px solid var(--seoautopro-chat-primary)}.seoautopro-chatbot-contact button:disabled,.seoautopro-chatbot-input button:disabled{opacity:.6;cursor:not-allowed}.seoautopro-chatbot-contact{display:none;flex-direction:column;gap:8px}.seoautopro-chatbot-contact input,.seoautopro-chatbot-contact textarea{width:100%;padding:8px 10px;border-radius:8px;border:1px solid #d2dbe3;font-size:13px;font-family:inherit}.seoautopro-chatbot-contact textarea{resize:vertical}.seoautopro-chatbot-contact p{margin:0 0 6px;font-size:12px;line-height:1.4;color:var(--seoautopro-chat-text)}@media (max-width:480px){.seoautopro-chatbot{right:16px;bottom:16px}.seoautopro-chatbot-window{width:calc(100vw - 32px);max-height:70vh}}
  • seo-auditor-tools/trunk/assets/chatbot.min.js

    r3592712 r3658740  
    1616        var $input = $inputForm.find('input');
    1717        var $contactForm = $chat.find('.seoautopro-chatbot-contact');
     18        var $refresh = $chat.find('.seoautopro-chatbot-refresh');
     19        var $continue = $chat.find('.seoautopro-chatbot-continue');
     20        var conversationId = 0;
    1821
    1922        function scrollToBottom() {
    … …  
    8992        }
    9093
     94        function startNewConversation() {
     95            conversationId += 1;
     96            $messages.empty();
     97            $input.val('');
     98            $contactForm.trigger('reset');
     99            $inputForm.find('button').prop('disabled', false);
     100            $contactForm.find('button').prop('disabled', false);
     101            hideContactForm();
     102            if (SEOAUTOPRO_CHATBOT.greeting) {
     103                appendMessage(SEOAUTOPRO_CHATBOT.greeting, 'bot');
     104            }
     105            $input.trigger('focus');
     106        }
     107
    91108        setOpen(false);
    92109
    … …  
    97114        $toggle.on('click', function() {
    98115            setOpen(!$window.hasClass('is-open'));
     116        });
     117
     118        $refresh.on('click', function() {
     119            startNewConversation();
     120        });
     121
     122        $continue.on('click', function() {
     123            hideContactForm();
     124            $input.trigger('focus');
    99125        });
    100126
    … …  
    107133
    108134            var $btn = $inputForm.find('button');
     135            var requestConversation = conversationId;
    109136            $btn.prop('disabled', true);
    110137            appendMessage(question, 'user');
    … …  
    117144                question: question
    118145            }).done(function(response) {
     146                if (requestConversation !== conversationId) {
     147                    return;
     148                }
    119149                if (!response || !response.success || !response.data) {
    120                     appendMessage('Sorry, something went wrong.', 'bot');
     150                    appendMessage(SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.', 'bot');
    121151                    showContactForm();
    122152                    return;
    … …  
    126156                appendSources(response.data.sources || []);
    127157                if (response.data.no_answer) {
    128                     appendMessage('Would you like to send a message to our support team for further assistance?', 'bot');
     158                    appendMessage(SEOAUTOPRO_CHATBOT.support_prompt || 'Would you like to send a message to our support team for further assistance?', 'bot');
    129159                    showContactForm();
    130160                } else {
    131161                    hideContactForm();
    132162                }
    133             }).fail(function() {
    134                 appendMessage('Sorry, something went wrong.', 'bot');
     163            }).fail(function(xhr) {
     164                if (requestConversation !== conversationId) {
     165                    return;
     166                }
     167                var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message
     168                    ? xhr.responseJSON.data.message
     169                    : (SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.');
     170                appendMessage(message, 'bot');
    135171                showContactForm();
    136172            }).always(function() {
    137                 $btn.prop('disabled', false);
     173                if (requestConversation === conversationId) {
     174                    $btn.prop('disabled', false);
     175                }
    138176            });
    139177        });
    … …  
    142180            event.preventDefault();
    143181
    144             var $btn = $contactForm.find('button');
     182            var $btn = $contactForm.find('button[type="submit"]');
     183            var requestConversation = conversationId;
    145184            $btn.prop('disabled', true);
    146185
    … …  
    156195
    157196            $.post(SEOAUTOPRO_CHATBOT.ajax_url, payload).done(function(response) {
     197                if (requestConversation !== conversationId) {
     198                    return;
     199                }
    158200                if (!response || !response.success || !response.data) {
    159201                    appendMessage('Unable to send your message. Please try again.', 'bot');
    … …  
    165207                $contactForm.trigger('reset');
    166208                hideContactForm();
    167             }).fail(function() {
    168                 appendMessage('Unable to send your message. Please try again.', 'bot');
     209            }).fail(function(xhr) {
     210                if (requestConversation !== conversationId) {
     211                    return;
     212                }
     213                var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message
     214                    ? xhr.responseJSON.data.message
     215                    : 'Unable to send your message. Please try again.';
     216                appendMessage(message, 'bot');
    169217            }).always(function() {
    170                 $btn.prop('disabled', false);
     218                if (requestConversation === conversationId) {
     219                    $btn.prop('disabled', false);
     220                }
    171221            });
    172222        });
  • seo-auditor-tools/trunk/assets/editor.js

    r3645873 r3658740  
    1313    var TextControl = wp.components.TextControl;
    1414    var SelectControl = wp.components.SelectControl;
     15    var Button = wp.components.Button;
     16    var Notice = wp.components.Notice;
    1517    var useSelect = wp.data.useSelect;
    1618    var useDispatch = wp.data.useDispatch;
     19    var useState = wp.element.useState;
    1720    var el = wp.element.createElement;
    1821
    1922    var MetaPanel = function() {
    20         var meta = useSelect(function(select){
    21             return select('core/editor').getEditedPostAttribute('meta') || {};
     23        var editorState = useSelect(function(select){
     24            var editor = select('core/editor');
     25            return {
     26                meta: editor.getEditedPostAttribute('meta') || {},
     27                title: editor.getEditedPostAttribute('title') || '',
     28                content: editor.getEditedPostAttribute('content') || '',
     29                excerpt: editor.getEditedPostAttribute('excerpt') || ''
     30            };
    2231        });
    2332        var editPost = useDispatch('core/editor').editPost;
     33        var generatingState = useState(false);
     34        var keywordGeneratingState = useState(false);
     35        var noticeState = useState(null);
     36        var isGenerating = generatingState[0];
     37        var setIsGenerating = generatingState[1];
     38        var isGeneratingKeywords = keywordGeneratingState[0];
     39        var setIsGeneratingKeywords = keywordGeneratingState[1];
     40        var notice = noticeState[0];
     41        var setNotice = noticeState[1];
     42        var meta = editorState.meta;
    2443
    2544        var description = meta['_seoautopro_meta_description'] || '';
    … …  
    2847        var robotsFollow = meta['_seoautopro_robots_follow'] || '';
    2948        var isLite = !!SEOAUTOPRO_PAGE_META.lite;
     49
     50        var generateDescription = function() {
     51            if (isGenerating || !SEOAUTOPRO_PAGE_META.post_id) {
     52                return;
     53            }
     54
     55            setIsGenerating(true);
     56            setNotice(null);
     57
     58            var body = new window.FormData();
     59            body.append('action', 'seoautopro_ai_helper_editor_generate');
     60            body.append('nonce', SEOAUTOPRO_PAGE_META.nonce);
     61            body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id));
     62            body.append('title', editorState.title);
     63            body.append('content', editorState.content);
     64            body.append('excerpt', editorState.excerpt);
     65
     66            window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, {
     67                method: 'POST',
     68                credentials: 'same-origin',
     69                body: body
     70            }).then(function(response) {
     71                return response.json();
     72            }).then(function(response) {
     73                if (!response || !response.success || !response.data || !response.data.suggested_value) {
     74                    var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error;
     75                    throw new Error(message);
     76                }
     77
     78                editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_description: response.data.suggested_value }) });
     79                setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.generated });
     80            }).catch(function(error) {
     81                setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error });
     82            }).then(function() {
     83                setIsGenerating(false);
     84            });
     85        };
     86
     87        var generateKeywords = function() {
     88            if (isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id) {
     89                return;
     90            }
     91
     92            setIsGeneratingKeywords(true);
     93            setNotice(null);
     94
     95            var body = new window.FormData();
     96            body.append('action', 'seoautopro_ai_helper_editor_generate');
     97            body.append('nonce', SEOAUTOPRO_PAGE_META.nonce);
     98            body.append('task', 'keywords');
     99            body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id));
     100            body.append('title', editorState.title);
     101            body.append('content', editorState.content);
     102            body.append('excerpt', editorState.excerpt);
     103
     104            window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, {
     105                method: 'POST',
     106                credentials: 'same-origin',
     107                body: body
     108            }).then(function(response) {
     109                return response.json();
     110            }).then(function(response) {
     111                if (!response || !response.success || !response.data || !response.data.suggested_value) {
     112                    var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error;
     113                    throw new Error(message);
     114                }
     115
     116                editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_keywords: response.data.suggested_value }) });
     117                setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.keywords_generated });
     118            }).catch(function(error) {
     119                setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error });
     120            }).then(function() {
     121                setIsGeneratingKeywords(false);
     122            });
     123        };
    30124
    31125        var children = [
    … …  
    40134        ];
    41135
     136        children.push(
     137            el('div', { className: 'seoautopro-editor-ai-actions' },
     138                el(Button, {
     139                    isSecondary: true,
     140                    isBusy: isGenerating,
     141                    disabled: isGenerating || !SEOAUTOPRO_PAGE_META.post_id,
     142                    onClick: generateDescription
     143                }, isGenerating ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate),
     144                el('a', {
     145                    href: SEOAUTOPRO_PAGE_META.ai_url,
     146                    target: '_blank',
     147                    rel: 'noopener noreferrer',
     148                    style: { marginLeft: '10px' }
     149                }, SEOAUTOPRO_PAGE_META.strings.settings_label)
     150            )
     151        );
     152
     153        if (!SEOAUTOPRO_PAGE_META.post_id) {
     154            children.push(el('p', { className: 'components-base-control__help' }, SEOAUTOPRO_PAGE_META.strings.save_first));
     155        }
     156
     157        if (notice) {
     158            children.push(el(Notice, {
     159                status: notice.status,
     160                isDismissible: true,
     161                onRemove: function(){ setNotice(null); }
     162            }, notice.message));
     163        }
     164
    42165        if (!isLite) {
    43166            children.push(
    44167                el(TextControl, {
    45168                    label: 'Meta keywords',
    46                     help: 'Comma-separated keywords. Overrides global keywords for this page.',
     169                    help: SEOAUTOPRO_PAGE_META.strings.keywords_help,
    47170                    value: keywords,
    48171                    onChange: function(value){
    … …  
    50173                    }
    51174                }),
     175                el('div', { className: 'seoautopro-editor-ai-actions', style: { marginBottom: '16px' } },
     176                    el(Button, {
     177                        isSecondary: true,
     178                        isBusy: isGeneratingKeywords,
     179                        disabled: isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id,
     180                        onClick: generateKeywords
     181                    }, isGeneratingKeywords ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate_keywords)
     182                ),
    52183                el(SelectControl, {
    53184                    label: 'Indexing',
  • seo-auditor-tools/trunk/assets/editor.min.js

    r3645873 r3658740  
    1313    var TextControl = wp.components.TextControl;
    1414    var SelectControl = wp.components.SelectControl;
     15    var Button = wp.components.Button;
     16    var Notice = wp.components.Notice;
    1517    var useSelect = wp.data.useSelect;
    1618    var useDispatch = wp.data.useDispatch;
     19    var useState = wp.element.useState;
    1720    var el = wp.element.createElement;
    1821
    1922    var MetaPanel = function() {
    20         var meta = useSelect(function(select){
    21             return select('core/editor').getEditedPostAttribute('meta') || {};
     23        var editorState = useSelect(function(select){
     24            var editor = select('core/editor');
     25            return {
     26                meta: editor.getEditedPostAttribute('meta') || {},
     27                title: editor.getEditedPostAttribute('title') || '',
     28                content: editor.getEditedPostAttribute('content') || '',
     29                excerpt: editor.getEditedPostAttribute('excerpt') || ''
     30            };
    2231        });
    2332        var editPost = useDispatch('core/editor').editPost;
     33        var generatingState = useState(false);
     34        var keywordGeneratingState = useState(false);
     35        var noticeState = useState(null);
     36        var isGenerating = generatingState[0];
     37        var setIsGenerating = generatingState[1];
     38        var isGeneratingKeywords = keywordGeneratingState[0];
     39        var setIsGeneratingKeywords = keywordGeneratingState[1];
     40        var notice = noticeState[0];
     41        var setNotice = noticeState[1];
     42        var meta = editorState.meta;
    2443
    2544        var description = meta['_seoautopro_meta_description'] || '';
    … …  
    2847        var robotsFollow = meta['_seoautopro_robots_follow'] || '';
    2948        var isLite = !!SEOAUTOPRO_PAGE_META.lite;
     49
     50        var generateDescription = function() {
     51            if (isGenerating || !SEOAUTOPRO_PAGE_META.post_id) {
     52                return;
     53            }
     54
     55            setIsGenerating(true);
     56            setNotice(null);
     57
     58            var body = new window.FormData();
     59            body.append('action', 'seoautopro_ai_helper_editor_generate');
     60            body.append('nonce', SEOAUTOPRO_PAGE_META.nonce);
     61            body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id));
     62            body.append('title', editorState.title);
     63            body.append('content', editorState.content);
     64            body.append('excerpt', editorState.excerpt);
     65
     66            window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, {
     67                method: 'POST',
     68                credentials: 'same-origin',
     69                body: body
     70            }).then(function(response) {
     71                return response.json();
     72            }).then(function(response) {
     73                if (!response || !response.success || !response.data || !response.data.suggested_value) {
     74                    var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error;
     75                    throw new Error(message);
     76                }
     77
     78                editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_description: response.data.suggested_value }) });
     79                setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.generated });
     80            }).catch(function(error) {
     81                setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error });
     82            }).then(function() {
     83                setIsGenerating(false);
     84            });
     85        };
     86
     87        var generateKeywords = function() {
     88            if (isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id) {
     89                return;
     90            }
     91
     92            setIsGeneratingKeywords(true);
     93            setNotice(null);
     94
     95            var body = new window.FormData();
     96            body.append('action', 'seoautopro_ai_helper_editor_generate');
     97            body.append('nonce', SEOAUTOPRO_PAGE_META.nonce);
     98            body.append('task', 'keywords');
     99            body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id));
     100            body.append('title', editorState.title);
     101            body.append('content', editorState.content);
     102            body.append('excerpt', editorState.excerpt);
     103
     104            window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, {
     105                method: 'POST',
     106                credentials: 'same-origin',
     107                body: body
     108            }).then(function(response) {
     109                return response.json();
     110            }).then(function(response) {
     111                if (!response || !response.success || !response.data || !response.data.suggested_value) {
     112                    var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error;
     113                    throw new Error(message);
     114                }
     115
     116                editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_keywords: response.data.suggested_value }) });
     117                setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.keywords_generated });
     118            }).catch(function(error) {
     119                setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error });
     120            }).then(function() {
     121                setIsGeneratingKeywords(false);
     122            });
     123        };
    30124
    31125        var children = [
    … …  
    40134        ];
    41135
     136        children.push(
     137            el('div', { className: 'seoautopro-editor-ai-actions' },
     138                el(Button, {
     139                    isSecondary: true,
     140                    isBusy: isGenerating,
     141                    disabled: isGenerating || !SEOAUTOPRO_PAGE_META.post_id,
     142                    onClick: generateDescription
     143                }, isGenerating ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate),
     144                el('a', {
     145                    href: SEOAUTOPRO_PAGE_META.ai_url,
     146                    target: '_blank',
     147                    rel: 'noopener noreferrer',
     148                    style: { marginLeft: '10px' }
     149                }, SEOAUTOPRO_PAGE_META.strings.settings_label)
     150            )
     151        );
     152
     153        if (!SEOAUTOPRO_PAGE_META.post_id) {
     154            children.push(el('p', { className: 'components-base-control__help' }, SEOAUTOPRO_PAGE_META.strings.save_first));
     155        }
     156
     157        if (notice) {
     158            children.push(el(Notice, {
     159                status: notice.status,
     160                isDismissible: true,
     161                onRemove: function(){ setNotice(null); }
     162            }, notice.message));
     163        }
     164
    42165        if (!isLite) {
    43166            children.push(
    44167                el(TextControl, {
    45168                    label: 'Meta keywords',
    46                     help: 'Comma-separated keywords. Overrides global keywords for this page.',
     169                    help: SEOAUTOPRO_PAGE_META.strings.keywords_help,
    47170                    value: keywords,
    48171                    onChange: function(value){
    … …  
    50173                    }
    51174                }),
     175                el('div', { className: 'seoautopro-editor-ai-actions', style: { marginBottom: '16px' } },
     176                    el(Button, {
     177                        isSecondary: true,
     178                        isBusy: isGeneratingKeywords,
     179                        disabled: isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id,
     180                        onClick: generateKeywords
     181                    }, isGeneratingKeywords ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate_keywords)
     182                ),
    52183                el(SelectControl, {
    53184                    label: 'Indexing',
  • seo-auditor-tools/trunk/includes/class-seoautopro-admin.php

    r3645873 r3658740  
    606606                'key'    => 'crawl_limit',
    607607                'placeholder' => '50',
     608                'type'   => 'number',
     609                'min'    => 1,
     610                'max'    => 200,
    608611            )
    609612        );
    … …  
    619622                'key'    => 'crawl_depth',
    620623                'placeholder' => '2',
     624                'type'   => 'number',
     625                'min'    => 0,
     626                'max'    => 10,
    621627            )
    622628        );
    … …  
    632638                'key'    => 'crawl_timeout',
    633639                'placeholder' => '8',
     640                'type'   => 'number',
     641                'min'    => 1,
     642                'max'    => 60,
    634643            )
    635644        );
    … …  
    775784        $key         = $args['key'];
    776785        $placeholder = isset( $args['placeholder'] ) ? $args['placeholder'] : '';
     786        $type        = isset( $args['type'] ) && in_array( $args['type'], array( 'text', 'number', 'url', 'email' ), true ) ? $args['type'] : 'text';
     787        $min         = isset( $args['min'] ) ? $args['min'] : null;
     788        $max         = isset( $args['max'] ) ? $args['max'] : null;
    777789
    778790        $opts  = get_option( $option_name, array() );
    … …  
    787799        }
    788800        ?>
    789         <input type="text" class="regular-text" name="<?php echo esc_attr( $option_name . '[' . $key . ']' ); ?>" value="<?php echo esc_attr( $value ); ?>" placeholder="<?php echo esc_attr( $placeholder ); ?>" />
     801        <input
     802            type="<?php echo esc_attr( $type ); ?>"
     803            class="regular-text"
     804            name="<?php echo esc_attr( $option_name . '[' . $key . ']' ); ?>"
     805            value="<?php echo esc_attr( $value ); ?>"
     806            placeholder="<?php echo esc_attr( $placeholder ); ?>"
     807            autocomplete="off"
     808            data-form-type="other"
     809            data-lpignore="true"
     810            data-1p-ignore="true"
     811            <?php if ( null !== $min ) : ?>min="<?php echo esc_attr( $min ); ?>"<?php endif; ?>
     812            <?php if ( null !== $max ) : ?>max="<?php echo esc_attr( $max ); ?>"<?php endif; ?>
     813        />
    790814        <?php
    791815    }
    … …  
    802826                value="<?php echo esc_attr( $value ); ?>"
    803827                placeholder="<?php echo esc_attr( $placeholder ); ?>"
    804                 autocomplete="off"
     828                autocomplete="new-password"
     829                data-form-type="other"
     830                data-lpignore="true"
     831                data-1p-ignore="true"
     832                data-bwignore="true"
     833                autocapitalize="none"
     834                autocorrect="off"
    805835                spellcheck="false"
     836                readonly="readonly"
    806837            />
    807838            <button type="button" class="button seoautopro-sensitive-toggle" aria-label="<?php esc_attr_e( 'Show value', 'seo-auditor-tools' ); ?>" data-show-label="<?php esc_attr_e( 'Show value', 'seo-auditor-tools' ); ?>" data-hide-label="<?php esc_attr_e( 'Hide value', 'seo-auditor-tools' ); ?>">
    … …  
    10791110
    10801111            <h2><?php esc_html_e( 'Audit settings', 'seo-auditor-tools' ); ?></h2>
    1081             <form method="post" action="options.php" class="seoautopro-card seoautopro-form-card">
     1112            <form method="post" action="options.php" class="seoautopro-card seoautopro-form-card" autocomplete="off" data-form-type="other">
    10821113                <?php
    10831114                settings_fields( 'seoautopro_audit' );
    … …  
    13341365                                                <p class="seoautopro-issue-detail"><?php echo esc_html__( 'Verify:', 'seo-auditor-tools' ); ?> <?php echo esc_html( $issue['verify'] ); ?></p>
    13351366                                            <?php endif; ?>
     1367                                            <?php if ( ! empty( $issue['id'] ) && class_exists( 'SEOAUTOPRO_AI_Helper' ) && SEOAUTOPRO_AI_Helper::supports_issue( $issue['id'] ) ) : ?>
     1368                                                <p class="seoautopro-issue-detail">
     1369                                                    <a class="button button-primary" href="<?php echo esc_url( SEOAUTOPRO_AI_Helper::get_issue_url( $issue['id'] ) ); ?>"><?php esc_html_e( 'Open AI Helper', 'seo-auditor-tools' ); ?></a>
     1370                                                </p>
     1371                                            <?php endif; ?>
    13361372                                            <?php if ( ! empty( $resources ) ) : ?>
    13371373                                                <p class="seoautopro-issue-detail"><?php esc_html_e( 'Resources:', 'seo-auditor-tools' ); ?></p>
  • seo-auditor-tools/trunk/includes/class-seoautopro-analyzer.php

    r3638852 r3658740  
    106106            'accessibility_flags'      => 0,
    107107            'seo_flags'                => 0,
     108            'missing_meta_descriptions'=> 0,
    108109            'missing_h1_urls'          => array(),
    109110            'multiple_h1_urls'         => array(),
    … …  
    112113            'missing_size_urls'        => array(),
    113114            'empty_link_urls'          => array(),
     115            'missing_meta_description_urls' => array(),
    114116        );
    115117
    … …  
    126128
    127129        foreach ( $posts as $post_id ) {
     130            if ( trim( (string) get_post_meta( $post_id, '_seoautopro_meta_description', true ) ) === '' ) {
     131                $stats['missing_meta_descriptions']++;
     132                $stats['seo_flags']++;
     133                self::add_impacted_url( $stats['missing_meta_description_urls'], $post_id, $impact_limit );
     134            }
     135
    128136            $content = get_post_field( 'post_content', $post_id );
    129137            $content = apply_filters( 'the_content', $content );
    … …  
    159167                $has_missing_alt = false;
    160168                foreach ( $imgs[0] as $img_tag ) {
    161                     if ( ! preg_match( '/alt=["\'][^"\']*["\']/', $img_tag ) ) {
     169                    $has_alt = preg_match( '/\balt\s*=\s*(["\'])(.*?)\1/is', $img_tag, $alt_match ) && '' !== trim( html_entity_decode( $alt_match[2], ENT_QUOTES, 'UTF-8' ) );
     170                    $decorative = preg_match( '/\brole\s*=\s*(["\'])(?:presentation|none)\1/i', $img_tag ) || preg_match( '/\baria-hidden\s*=\s*(["\'])true\1/i', $img_tag );
     171                    if ( ! $has_alt && ! $decorative ) {
    162172                        $stats['images_missing_alt']++;
    163173                        $has_missing_alt = true;
    … …  
    229239                $stats['images_total']++;
    230240                $alt = $img->getAttribute( 'alt' );
    231                 if ( $alt === '' ) {
     241                $role = strtolower( $img->getAttribute( 'role' ) );
     242                $decorative = in_array( $role, array( 'presentation', 'none' ), true ) || 'true' === strtolower( $img->getAttribute( 'aria-hidden' ) );
     243                if ( $alt === '' && ! $decorative ) {
    232244                    $stats['images_missing_alt']++;
    233245                    $has_missing_alt = true;
    … …  
    472484
    473485        if ( $post_stats['total_posts'] > 0 ) {
    474             $seo_penalty = ( $post_stats['no_h1'] * 3 ) + ( $post_stats['multiple_h1'] * 3 );
     486            $seo_penalty = ( $post_stats['no_h1'] * 3 ) + ( $post_stats['multiple_h1'] * 3 ) + min( 20, intval( $post_stats['missing_meta_descriptions'] ) );
    475487            $seo_penalty = min( 80, $seo_penalty );
    476488            $seo_score   = max( 10, 100 - $seo_penalty );
    … …  
    526538                'sequence_issues'  => $post_stats['heading_sequence_issues'],
    527539            ),
     540            'missing_meta_descriptions' => isset( $post_stats['missing_meta_descriptions'] ) ? intval( $post_stats['missing_meta_descriptions'] ) : 0,
    528541            'images'                 => array(
    529542                'total'            => $post_stats['images_total'],
  • seo-auditor-tools/trunk/includes/class-seoautopro-chatbot.php

    r3645873 r3658740  
    1010
    1111    public static function register_settings() {
     12        self::migrate_legacy_documents();
     13
    1214        register_setting(
    1315            'seoautopro_chatbot',
    … …  
    3234
    3335        add_settings_field(
     36            'seoautopro_chatbot_ai_enabled',
     37            __( 'Grounded AI answers', 'seo-auditor-tools' ),
     38            array( __CLASS__, 'field_ai_enabled' ),
     39            'seoautopro-chatbot',
     40            'seoautopro_chatbot_main'
     41        );
     42
     43        add_settings_field(
     44            'seoautopro_chatbot_hourly_ip_limit',
     45            __( 'Questions per visitor/hour', 'seo-auditor-tools' ),
     46            array( __CLASS__, 'field_hourly_ip_limit' ),
     47            'seoautopro-chatbot',
     48            'seoautopro_chatbot_main'
     49        );
     50
     51        add_settings_field(
     52            'seoautopro_chatbot_daily_limit',
     53            __( 'Total questions/day', 'seo-auditor-tools' ),
     54            array( __CLASS__, 'field_daily_limit' ),
     55            'seoautopro-chatbot',
     56            'seoautopro_chatbot_main'
     57        );
     58
     59        add_settings_field(
    3460            'seoautopro_chatbot_email',
    3561            __( 'Support email', 'seo-auditor-tools' ),
    … …  
    138164        $clean = is_array( $input ) ? $input : array();
    139165        $clean['enabled'] = ! empty( $clean['enabled'] ) ? 1 : 0;
     166        $clean['ai_enabled'] = ! empty( $clean['ai_enabled'] ) ? 1 : 0;
     167        $clean['hourly_ip_limit'] = isset( $clean['hourly_ip_limit'] ) ? min( 100, max( 1, absint( $clean['hourly_ip_limit'] ) ) ) : 20;
     168        $clean['daily_limit'] = isset( $clean['daily_limit'] ) ? min( 10000, max( 1, absint( $clean['daily_limit'] ) ) ) : 500;
    140169        $clean['support_email'] = isset( $clean['support_email'] ) ? sanitize_email( $clean['support_email'] ) : '';
    141170        $clean['greeting'] = isset( $clean['greeting'] ) ? sanitize_textarea_field( $clean['greeting'] ) : '';
    … …  
    162191        $defaults = array(
    163192            'enabled' => 0,
     193            'ai_enabled' => 0,
     194            'hourly_ip_limit' => 20,
     195            'daily_limit' => 500,
    164196            'support_email' => get_option( 'admin_email' ),
    165197            'greeting' => __( 'Hello. How may I assist you today?', 'seo-auditor-tools' ),
    … …  
    189221    }
    190222
     223    public static function field_ai_enabled() {
     224        $settings = self::get_settings();
     225        ?>
     226        <label>
     227            <input type="checkbox" name="<?php echo esc_attr( self::SETTINGS_OPTION ); ?>[ai_enabled]" value="1" <?php checked( ! empty( $settings['ai_enabled'] ) ); ?> />
     228            <?php esc_html_e( 'Use the provider configured in AI Helper to compose answers from approved website sources.', 'seo-auditor-tools' ); ?>
     229        </label>
     230        <p class="description"><?php esc_html_e( 'The model receives only relevant public excerpts and approved document text. It cannot browse the web, and unsupported questions use the contact fallback.', 'seo-auditor-tools' ); ?></p>
     231        <?php
     232    }
     233
     234    public static function field_hourly_ip_limit() {
     235        $settings = self::get_settings();
     236        ?>
     237        <input type="number" class="small-text" min="1" max="100" name="<?php echo esc_attr( self::SETTINGS_OPTION ); ?>[hourly_ip_limit]" value="<?php echo esc_attr( $settings['hourly_ip_limit'] ); ?>" />
     238        <p class="description"><?php esc_html_e( 'Limits expensive or abusive requests. Visitor addresses are anonymised and hashed before the temporary counter is stored.', 'seo-auditor-tools' ); ?></p>
     239        <?php
     240    }
     241
     242    public static function field_daily_limit() {
     243        $settings = self::get_settings();
     244        ?>
     245        <input type="number" class="small-text" min="1" max="10000" name="<?php echo esc_attr( self::SETTINGS_OPTION ); ?>[daily_limit]" value="<?php echo esc_attr( $settings['daily_limit'] ); ?>" />
     246        <p class="description"><?php esc_html_e( 'Maximum chatbot questions across the website each UTC day.', 'seo-auditor-tools' ); ?></p>
     247        <?php
     248    }
     249
    191250    public static function field_support_email() {
    192251        $settings = self::get_settings();
    … …  
    300359        <div class="wrap seoautopro-wrap">
    301360            <h1><?php esc_html_e( 'Chatbot', 'seo-auditor-tools' ); ?></h1>
    302             <p><?php esc_html_e( 'Configure the chatbot and upload documents to improve responses.', 'seo-auditor-tools' ); ?></p>
     361            <p><?php esc_html_e( 'Configure a website-only assistant. It can use public published posts and pages plus documents you explicitly approve.', 'seo-auditor-tools' ); ?></p>
    303362
    304363            <?php if ( $status === 'uploaded' ) : ?>
    … …  
    306365            <?php elseif ( $status === 'deleted' ) : ?>
    307366                <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Document deleted.', 'seo-auditor-tools' ); ?></p></div>
     367            <?php elseif ( $status === 'updated' ) : ?>
     368                <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Document approval updated.', 'seo-auditor-tools' ); ?></p></div>
    308369            <?php elseif ( $status === 'error' ) : ?>
    309370                <div class="notice notice-error is-dismissible"><p><?php esc_html_e( 'Document upload failed.', 'seo-auditor-tools' ); ?></p></div>
    … …  
    320381            <div class="seoautopro-card seoautopro-form-card">
    321382                <h2><?php esc_html_e( 'Upload documents', 'seo-auditor-tools' ); ?></h2>
    322                 <p><?php esc_html_e( 'Upload PDF or DOC/DOCX files to add reference material for the chatbot.', 'seo-auditor-tools' ); ?></p>
     383                <p><?php esc_html_e( 'Upload PDF or DOC/DOCX reference material up to 5 MB. The plugin extracts limited plain text and does not retain a publicly downloadable copy of the original file.', 'seo-auditor-tools' ); ?></p>
    323384                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
    324385                    <?php wp_nonce_field( 'seoautopro_chatbot_upload' ); ?>
    325386                    <input type="hidden" name="action" value="seoautopro_chatbot_upload_doc" />
    326387                    <input type="file" name="seoautopro_chatbot_doc" accept=".pdf,.doc,.docx" required />
     388                    <label style="margin-left:12px;">
     389                        <input type="checkbox" name="approved" value="1" />
     390                        <?php esc_html_e( 'Approve extracted text for public chatbot answers', 'seo-auditor-tools' ); ?>
     391                    </label>
    327392                    <?php submit_button( __( 'Upload document', 'seo-auditor-tools' ), 'secondary', 'submit', false ); ?>
    328393                </form>
    … …  
    338403                            <tr>
    339404                                <th><?php esc_html_e( 'File', 'seo-auditor-tools' ); ?></th>
     405                                <th><?php esc_html_e( 'Chatbot access', 'seo-auditor-tools' ); ?></th>
    340406                                <th><?php esc_html_e( 'Uploaded', 'seo-auditor-tools' ); ?></th>
    341407                                <th><?php esc_html_e( 'Actions', 'seo-auditor-tools' ); ?></th>
    … …  
    346412                                <tr>
    347413                                    <td>
    348                                         <a href="<?php echo esc_url( $doc['url'] ); ?>" target="_blank" rel="noopener noreferrer">
    349                                             <?php echo esc_html( $doc['name'] ); ?>
    350                                         </a>
     414                                        <?php echo esc_html( isset( $doc['name'] ) ? $doc['name'] : __( 'Document', 'seo-auditor-tools' ) ); ?>
    351415                                    </td>
     416                                    <td><?php echo ! empty( $doc['approved'] ) ? esc_html__( 'Approved', 'seo-auditor-tools' ) : esc_html__( 'Not approved', 'seo-auditor-tools' ); ?></td>
    352417                                    <td><?php echo esc_html( date_i18n( get_option( 'date_format' ), intval( $doc['uploaded'] ) ) ); ?></td>
    353418                                    <td>
    354                                         <a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=seoautopro_chatbot_delete_doc&doc_id=' . intval( $doc['id'] ) ), 'seoautopro_chatbot_delete_doc' ) ); ?>"><?php esc_html_e( 'Delete', 'seo-auditor-tools' ); ?></a>
     419                                        <?php
     420                                        $doc_id = isset( $doc['id'] ) ? sanitize_text_field( (string) $doc['id'] ) : '';
     421                                        $toggle_url = add_query_arg(
     422                                            array(
     423                                                'action' => 'seoautopro_chatbot_toggle_doc',
     424                                                'doc_id' => $doc_id,
     425                                            ),
     426                                            admin_url( 'admin-post.php' )
     427                                        );
     428                                        $delete_url = add_query_arg(
     429                                            array(
     430                                                'action' => 'seoautopro_chatbot_delete_doc',
     431                                                'doc_id' => $doc_id,
     432                                            ),
     433                                            admin_url( 'admin-post.php' )
     434                                        );
     435                                        ?>
     436                                        <a href="<?php echo esc_url( wp_nonce_url( $toggle_url, 'seoautopro_chatbot_toggle_doc' ) ); ?>"><?php echo ! empty( $doc['approved'] ) ? esc_html__( 'Remove approval', 'seo-auditor-tools' ) : esc_html__( 'Approve', 'seo-auditor-tools' ); ?></a>
     437                                        | <a href="<?php echo esc_url( wp_nonce_url( $delete_url, 'seoautopro_chatbot_delete_doc' ) ); ?>"><?php esc_html_e( 'Delete', 'seo-auditor-tools' ); ?></a>
    355438                                    </td>
    356439                                </tr>
    … …  
    397480                'greeting' => $settings['greeting'],
    398481                'thank_you' => $settings['thank_you'],
     482                'error_message' => __( 'Sorry, something went wrong.', 'seo-auditor-tools' ),
     483                'support_prompt' => __( 'Would you like to send a message to our support team for further assistance?', 'seo-auditor-tools' ),
    399484            )
    400485        );
    … …  
    434519            </button>
    435520            <div id="seoautopro-chatbot-window" class="seoautopro-chatbot-window" aria-hidden="true">
    436                 <div class="seoautopro-chatbot-header"><?php echo esc_html( $settings['name'] ); ?></div>
     521                <div class="seoautopro-chatbot-header">
     522                    <span><?php echo esc_html( $settings['name'] ); ?></span>
     523                    <button type="button" class="seoautopro-chatbot-refresh" title="<?php esc_attr_e( 'Start a new conversation', 'seo-auditor-tools' ); ?>" aria-label="<?php esc_attr_e( 'Start a new conversation', 'seo-auditor-tools' ); ?>">&#8635;</button>
     524                </div>
    437525                <div class="seoautopro-chatbot-messages"></div>
    438526                <form class="seoautopro-chatbot-input">
    439                     <input type="text" placeholder="<?php esc_attr_e( 'Type your question...', 'seo-auditor-tools' ); ?>" required />
     527                    <input type="text" maxlength="300" autocomplete="off" placeholder="<?php esc_attr_e( 'Type your question...', 'seo-auditor-tools' ); ?>" required />
    440528                    <button type="submit"><?php esc_html_e( 'Send', 'seo-auditor-tools' ); ?></button>
    441529                </form>
    442530                <form class="seoautopro-chatbot-contact" style="display:none;">
    443                     <p><?php esc_html_e( 'We could not find an answer. Please leave your details and we will reply by email.', 'seo-auditor-tools' ); ?></p>
     531                    <p><?php esc_html_e( 'We could not find an answer. You can leave your details for an email reply or continue chatting.', 'seo-auditor-tools' ); ?></p>
    444532                    <input type="text" name="first_name" placeholder="<?php esc_attr_e( 'Name', 'seo-auditor-tools' ); ?>" required />
    445533                    <input type="text" name="last_name" placeholder="<?php esc_attr_e( 'Surname', 'seo-auditor-tools' ); ?>" required />
    … …  
    447535                    <input type="text" name="subject" placeholder="<?php esc_attr_e( 'Subject', 'seo-auditor-tools' ); ?>" required />
    448536                    <textarea name="message" rows="3" placeholder="<?php esc_attr_e( 'Message', 'seo-auditor-tools' ); ?>" required></textarea>
    449                     <button type="submit"><?php esc_html_e( 'Send message', 'seo-auditor-tools' ); ?></button>
     537                    <div class="seoautopro-chatbot-contact-actions">
     538                        <button type="button" class="seoautopro-chatbot-continue"><?php esc_html_e( 'Continue chat', 'seo-auditor-tools' ); ?></button>
     539                        <button type="submit"><?php esc_html_e( 'Send message', 'seo-auditor-tools' ); ?></button>
     540                    </div>
    450541                </form>
    451542            </div>
    … …  
    466557
    467558        $file = $_FILES['seoautopro_chatbot_doc'];
    468         if ( ! empty( $file['error'] ) || empty( $file['tmp_name'] ) ) {
     559        if ( ! empty( $file['error'] ) || empty( $file['tmp_name'] ) || empty( $file['size'] ) || intval( $file['size'] ) > 5 * MB_IN_BYTES || ! is_uploaded_file( $file['tmp_name'] ) ) {
    469560            self::redirect_with_status( 'error' );
    470561        }
    … …  
    475566        }
    476567
    477         $uploads = wp_upload_dir();
    478         $dir = trailingslashit( $uploads['basedir'] ) . 'seoautopro-chatbot';
    479         $url_base = trailingslashit( $uploads['baseurl'] ) . 'seoautopro-chatbot';
    480         if ( ! wp_mkdir_p( $dir ) ) {
     568        $text = self::extract_text( $file['tmp_name'], $ext );
     569        if ( $text === '' ) {
    481570            self::redirect_with_status( 'error' );
    482571        }
    483572
    484         $filename = wp_unique_filename( $dir, sanitize_file_name( $file['name'] ) );
    485         $dest = trailingslashit( $dir ) . $filename;
    486         if ( ! @move_uploaded_file( $file['tmp_name'], $dest ) ) {
    487             self::redirect_with_status( 'error' );
    488         }
    489 
    490         $text = self::extract_text( $dest, $ext );
    491573        $docs = get_option( self::DOC_OPTION, array() );
    492574        if ( ! is_array( $docs ) ) {
    … …  
    495577
    496578        $docs[] = array(
    497             'id' => time(),
    498             'name' => $file['name'],
    499             'path' => $dest,
    500             'url' => $url_base . '/' . $filename,
     579            'id' => wp_generate_uuid4(),
     580            'name' => sanitize_file_name( wp_unslash( $file['name'] ) ),
    501581            'ext' => $ext,
    502582            'text' => $text,
    503583            'uploaded' => time(),
     584            'approved' => ! empty( $_POST['approved'] ) ? 1 : 0,
    504585        );
    505586
    … …  
    515596        check_admin_referer( 'seoautopro_chatbot_delete_doc' );
    516597
    517         // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only param, sanitized inline.
    518         $doc_id = isset( $_GET['doc_id'] ) ? intval( wp_unslash( $_GET['doc_id'] ) ) : 0;
     598        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce checked above, sanitized inline.
     599        $doc_id = isset( $_GET['doc_id'] ) ? sanitize_text_field( wp_unslash( $_GET['doc_id'] ) ) : '';
    519600        $docs = get_option( self::DOC_OPTION, array() );
    520601        if ( ! is_array( $docs ) ) {
    … …  
    524605        $filtered = array();
    525606        foreach ( $docs as $doc ) {
    526             if ( intval( $doc['id'] ) === $doc_id ) {
    527                 if ( ! empty( $doc['path'] ) && file_exists( $doc['path'] ) ) {
    528                     @unlink( $doc['path'] );
    529                 }
     607            $stored_id = isset( $doc['id'] ) ? (string) $doc['id'] : '';
     608            if ( $stored_id !== '' && hash_equals( $stored_id, $doc_id ) ) {
     609                self::delete_legacy_file( $doc );
    530610                continue;
    531611            }
    … …  
    535615        update_option( self::DOC_OPTION, $filtered, false );
    536616        self::redirect_with_status( 'deleted' );
     617    }
     618
     619    public static function handle_toggle_doc() {
     620        if ( ! current_user_can( 'manage_options' ) ) {
     621            wp_die( esc_html__( 'Permission denied.', 'seo-auditor-tools' ) );
     622        }
     623
     624        check_admin_referer( 'seoautopro_chatbot_toggle_doc' );
     625
     626        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce checked above, sanitized inline.
     627        $doc_id = isset( $_GET['doc_id'] ) ? sanitize_text_field( wp_unslash( $_GET['doc_id'] ) ) : '';
     628        $docs = get_option( self::DOC_OPTION, array() );
     629        if ( ! is_array( $docs ) ) {
     630            $docs = array();
     631        }
     632
     633        foreach ( $docs as &$doc ) {
     634            $stored_id = isset( $doc['id'] ) ? (string) $doc['id'] : '';
     635            if ( $stored_id !== '' && hash_equals( $stored_id, $doc_id ) ) {
     636                $doc['approved'] = empty( $doc['approved'] ) ? 1 : 0;
     637                break;
     638            }
     639        }
     640        unset( $doc );
     641
     642        update_option( self::DOC_OPTION, $docs, false );
     643        self::redirect_with_status( 'updated' );
     644    }
     645
     646    public static function migrate_legacy_documents() {
     647        $docs = get_option( self::DOC_OPTION, array() );
     648        if ( ! is_array( $docs ) || empty( $docs ) ) {
     649            return;
     650        }
     651
     652        $changed = false;
     653        foreach ( $docs as &$doc ) {
     654            if ( ! array_key_exists( 'approved', $doc ) ) {
     655                $doc['approved'] = 0;
     656                $changed = true;
     657            }
     658            if ( isset( $doc['path'] ) || isset( $doc['url'] ) ) {
     659                self::delete_legacy_file( $doc );
     660                unset( $doc['path'], $doc['url'] );
     661                $changed = true;
     662            }
     663        }
     664        unset( $doc );
     665
     666        if ( $changed ) {
     667            update_option( self::DOC_OPTION, $docs, false );
     668        }
     669    }
     670
     671    private static function delete_legacy_file( $doc ) {
     672        if ( empty( $doc['path'] ) ) {
     673            return;
     674        }
     675
     676        $uploads = wp_upload_dir();
     677        $allowed_dir = realpath( trailingslashit( $uploads['basedir'] ) . 'seoautopro-chatbot' );
     678        $file_path = realpath( $doc['path'] );
     679        if ( ! $allowed_dir || ! $file_path ) {
     680            return;
     681        }
     682
     683        $allowed_prefix = trailingslashit( wp_normalize_path( $allowed_dir ) );
     684        $normalized_file = wp_normalize_path( $file_path );
     685        if ( strpos( $normalized_file, $allowed_prefix ) === 0 && is_file( $file_path ) ) {
     686            @unlink( $file_path );
     687        }
    537688    }
    538689
    … …  
    552703        check_ajax_referer( 'seoautopro-chatbot', 'nonce' );
    553704
     705        $settings = self::get_settings();
     706        if ( empty( $settings['enabled'] ) ) {
     707            wp_send_json_error( array( 'message' => __( 'The chatbot is currently unavailable.', 'seo-auditor-tools' ) ), 403 );
     708        }
     709
    554710        $question = isset( $_POST['question'] ) ? sanitize_text_field( wp_unslash( $_POST['question'] ) ) : '';
    555711        if ( $question === '' ) {
    556712            wp_send_json_error( array( 'message' => __( 'Please enter a question.', 'seo-auditor-tools' ) ) );
    557713        }
     714        if ( self::text_length( $question ) > 300 ) {
     715            wp_send_json_error( array( 'message' => __( 'Please keep questions under 300 characters.', 'seo-auditor-tools' ) ), 400 );
     716        }
     717
     718        $limit_error = self::check_rate_limit();
     719        if ( is_wp_error( $limit_error ) ) {
     720            wp_send_json_error( array( 'message' => $limit_error->get_error_message() ), 429 );
     721        }
    558722
    559723        $result = self::find_relevant_answers( $question );
    … …  
    563727    public static function ajax_contact() {
    564728        check_ajax_referer( 'seoautopro-chatbot', 'nonce' );
     729
     730        $settings = self::get_settings();
     731        if ( empty( $settings['enabled'] ) ) {
     732            wp_send_json_error( array( 'message' => __( 'The chatbot is currently unavailable.', 'seo-auditor-tools' ) ), 403 );
     733        }
     734        $contact_limit_error = self::check_rate_limit( 'contact', 5, 100 );
     735        if ( is_wp_error( $contact_limit_error ) ) {
     736            wp_send_json_error( array( 'message' => $contact_limit_error->get_error_message() ), 429 );
     737        }
    565738
    566739        $first = isset( $_POST['first_name'] ) ? sanitize_text_field( wp_unslash( $_POST['first_name'] ) ) : '';
    … …  
    578751        }
    579752
    580         $settings = self::get_settings();
    581753        $to = ! empty( $settings['support_email'] ) ? $settings['support_email'] : get_option( 'admin_email' );
    582754
    … …  
    603775
    604776        $items = array();
    605         $llms_text = self::get_llms_text();
    606         if ( $llms_text !== '' ) {
    607             $score = self::score_text( $llms_text, $terms );
    608             if ( $score > 0 ) {
    609                 $items[] = array(
    610                     'type' => 'llms',
    611                     'title' => __( 'Site summary', 'seo-auditor-tools' ),
    612                     'url' => '',
    613                     'excerpt' => self::extract_snippet( $llms_text, $terms ),
    614                     'score' => $score,
    615                 );
    616             }
    617         }
    618777        $posts = get_posts( array(
    619778            'post_type' => array( 'page', 'post' ),
    620779            'post_status' => 'publish',
    621780            'numberposts' => 200,
     781            'has_password' => false,
    622782        ) );
    623783
    624784        foreach ( $posts as $post ) {
     785            if ( 'publish' !== get_post_status( $post ) || ! empty( $post->post_password ) ) {
     786                continue;
     787            }
     788            if ( function_exists( 'is_post_publicly_viewable' ) && ! is_post_publicly_viewable( $post ) ) {
     789                continue;
     790            }
    625791            $text = self::clean_post_text( $post->post_title . ' ' . $post->post_content );
    626792            if ( strlen( $text ) < 20 ) {
    … …  
    643809        if ( is_array( $docs ) ) {
    644810            foreach ( $docs as $doc ) {
     811                if ( empty( $doc['approved'] ) ) {
     812                    continue;
     813                }
    645814                $text = isset( $doc['text'] ) ? $doc['text'] : '';
    646815                if ( $text === '' ) {
    … …  
    671840        } );
    672841
     842        $items = array_slice( $items, 0, 5 );
     843        foreach ( $items as $index => &$item ) {
     844            $item['id'] = 'source-' . ( $index + 1 );
     845        }
     846        unset( $item );
     847
     848        $settings = self::get_settings();
     849        if ( ! empty( $settings['ai_enabled'] ) && class_exists( 'SEOAUTOPRO_AI_Helper' ) ) {
     850            $ai_result = SEOAUTOPRO_AI_Helper::generate_grounded_chatbot_answer( $question, $items );
     851            if ( ! is_wp_error( $ai_result ) ) {
     852                if ( empty( $ai_result['supported'] ) ) {
     853                    return array(
     854                        'reply' => __( 'I am sorry, I could not find enough approved website information to answer that reliably.', 'seo-auditor-tools' ),
     855                        'no_answer' => true,
     856                    );
     857                }
     858
     859                $source_ids = isset( $ai_result['source_ids'] ) && is_array( $ai_result['source_ids'] ) ? $ai_result['source_ids'] : array();
     860                $cited_items = array_values( array_filter( $items, function( $item ) use ( $source_ids ) {
     861                    return in_array( $item['id'], $source_ids, true );
     862                } ) );
     863                if ( ! empty( $cited_items ) ) {
     864                    return array(
     865                        'reply' => $ai_result['reply'],
     866                        'sources' => $cited_items,
     867                        'no_answer' => false,
     868                        'ai_generated' => true,
     869                    );
     870                }
     871            }
     872        }
     873
    673874        $items = array_slice( $items, 0, 3 );
    674875        return array(
    … …  
    677878            'no_answer' => false,
    678879        );
     880    }
     881
     882    private static function check_rate_limit( $bucket = 'answer', $hourly_override = 0, $daily_override = 0 ) {
     883        $settings = self::get_settings();
     884        $hourly_limit = $hourly_override ? absint( $hourly_override ) : min( 100, max( 1, absint( $settings['hourly_ip_limit'] ) ) );
     885        $daily_limit = $daily_override ? absint( $daily_override ) : min( 10000, max( 1, absint( $settings['daily_limit'] ) ) );
     886        $bucket = sanitize_key( $bucket );
     887        $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
     888        if ( function_exists( 'wp_privacy_anonymize_ip' ) ) {
     889            $ip = wp_privacy_anonymize_ip( $ip );
     890        }
     891        $visitor_hash = hash_hmac( 'sha256', $ip, wp_salt( 'nonce' ) );
     892        $visitor_key = 'seoautopro_chat_' . $bucket . '_hour_' . substr( $visitor_hash, 0, 20 );
     893        $daily_key = 'seoautopro_chat_' . $bucket . '_day_' . gmdate( 'Ymd' );
     894        $visitor_count = absint( get_transient( $visitor_key ) );
     895        $daily_count = absint( get_transient( $daily_key ) );
     896
     897        if ( $visitor_count >= $hourly_limit || $daily_count >= $daily_limit ) {
     898            return new WP_Error( 'seoautopro_chatbot_rate_limit', __( 'The chatbot request limit has been reached. Please try again later.', 'seo-auditor-tools' ) );
     899        }
     900
     901        set_transient( $visitor_key, $visitor_count + 1, HOUR_IN_SECONDS );
     902        set_transient( $daily_key, $daily_count + 1, DAY_IN_SECONDS + HOUR_IN_SECONDS );
     903        return true;
     904    }
     905
     906    private static function text_length( $text ) {
     907        return function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text );
    679908    }
    680909
    … …  
    723952        }
    724953
    725         return $url;
     954        $result_host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
     955        $site_host = strtolower( (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ) );
     956        if ( '' === $result_host || '' === $site_host || $result_host !== $site_host ) {
     957            return '';
     958        }
     959
     960        return esc_url_raw( $url );
    726961    }
    727962
    … …  
    7911026            $zip = new ZipArchive();
    7921027            if ( $zip->open( $path ) === true ) {
    793                 $data = $zip->getFromName( 'word/document.xml' );
    794                 if ( $data ) {
    795                     $text = wp_strip_all_tags( $data );
     1028                $entry = $zip->statName( 'word/document.xml' );
     1029                if ( is_array( $entry ) && ! empty( $entry['size'] ) && intval( $entry['size'] ) <= 2 * MB_IN_BYTES ) {
     1030                    $data = $zip->getFromName( 'word/document.xml' );
     1031                    if ( $data ) {
     1032                        $text = wp_strip_all_tags( $data );
     1033                    }
    7961034                }
    7971035                $zip->close();
  • seo-auditor-tools/trunk/includes/class-seoautopro-rules.php

    r3592695 r3658740  
    2121        $missing_size_urls = isset( $post_stats['missing_size_urls'] ) ? $post_stats['missing_size_urls'] : array();
    2222        $empty_link_urls = isset( $post_stats['empty_link_urls'] ) ? $post_stats['empty_link_urls'] : array();
     23        $missing_meta_description_urls = isset( $post_stats['missing_meta_description_urls'] ) ? $post_stats['missing_meta_description_urls'] : array();
     24
     25        if ( ! empty( $post_stats['missing_meta_descriptions'] ) ) {
     26            $issues[] = self::issue( array(
     27                'id'       => 'seo_missing_meta_description',
     28                'title'    => __( 'Pages missing a custom meta description', 'seo-auditor-tools' ),
     29                'category' => 'seo',
     30                'severity' => 'warning',
     31                'impact'   => 'SEO',
     32                'effort'   => 'easy',
     33                'evidence' => sprintf( _n( '%d page relies on a generated fallback description.', '%d pages rely on generated fallback descriptions.', $post_stats['missing_meta_descriptions'], 'seo-auditor-tools' ), $post_stats['missing_meta_descriptions'] ),
     34                'impacted' => $missing_meta_description_urls,
     35                'fix'      => __( 'Create a concise, page-specific meta description. AI Helper can prepare a proposal for your approval.', 'seo-auditor-tools' ),
     36                'verify'   => __( 'View the page source after applying the change and confirm the description meta tag contains the approved text.', 'seo-auditor-tools' ),
     37            ) );
     38        }
    2339
    2440        if ( ! empty( $post_stats['no_h1'] ) ) {
    … …  
    6581                'evidence' => sprintf( _n( '%d image is missing alt text.', '%d images are missing alt text.', $post_stats['images_missing_alt'], 'seo-auditor-tools' ), $post_stats['images_missing_alt'] ),
    6682                'impacted' => $missing_alt_urls,
    67                 'fix'      => __( 'Add a short description for each image (alt text). You can do this in the Media Library or in the image block settings.', 'seo-auditor-tools' ),
    68                 'verify'   => __( 'Open one of the listed pages and confirm the images have descriptions filled in.', 'seo-auditor-tools' ),
     83                'fix'      => __( 'Add concise, contextual alt text for informative images. AI Helper can prepare a proposal for images saved in page or post content; decorative images should keep an empty alt attribute.', 'seo-auditor-tools' ),
     84                'verify'   => __( 'Run the audit again and confirm informative images have useful alt text while decorative images retain an empty alt attribute.', 'seo-auditor-tools' ),
    6985            ) );
    7086        }
  • seo-auditor-tools/trunk/includes/class-seoautopro-settings-export.php

    r3592695 r3658740  
    8585            $data[ $key ] = get_option( $key, array() );
    8686        }
     87        if ( isset( $data['seoautopro_ai_helper_settings']['api_keys'] ) ) {
     88            unset( $data['seoautopro_ai_helper_settings']['api_keys'] );
     89        }
    8790        $data['seoautopro_exported_at'] = current_time( 'mysql' );
    8891        $data['seoautopro_version'] = defined( 'SEOAUTOPRO_VERSION' ) ? SEOAUTOPRO_VERSION : '';
    … …  
    105108            'seoautopro_llms_settings',
    106109            'seoautopro_chatbot_settings',
     110            'seoautopro_ai_helper_settings',
    107111        );
    108112    }
  • seo-auditor-tools/trunk/readme.txt

    r3645916 r3658740  
    66Tested up to: 7.0
    77Requires PHP: 7.4
    8 Stable tag: 2.0.2
     8Stable tag: 2.0.3
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1616== Description ==
    1717
    18 Scan your WordPress site for SEO issues and get actionable fixes. Includes sitemap, robots.txt, LLMS.txt, Open Graph, cookie consent, analytics, UTM builder, QR code generator, image tools, caching, minification, security monitor, redirection manager, .htaccess editor, and chatbot. All features free - no Pro version.
     18Scan your WordPress site for SEO issues and get actionable fixes. Includes AI Helper, sitemap, robots.txt, LLMS.txt, Open Graph, cookie consent, analytics, UTM builder, QR code generator, image tools, caching, minification, security monitor, redirection manager, .htaccess editor, and chatbot. All features free - no Pro version.
    1919
    2020**SEO Auditor Tools** is a complete, no-paywall SEO and site management toolkit for WordPress. Run an automated site audit that crawls your pages and returns a fully scored report across four categories - SEO, Accessibility, Performance, and Best Practices - with plain-language issue cards and actionable fixes for every finding.
    … …  
    5959* **QR Code Generator** - Create QR codes for any URL. Download as PNG or copy Base64 inline.
    6060* **Settings Export** - Export all plugin settings as JSON or CSV to migrate to another site in seconds.
     61* **AI Helper** - Use a self-hosted Ollama model to create structured meta-description proposals with preview, validation, approval, fix history, and undo.
    6162* **Security Monitor** - File integrity checks, login hardening, HTTP security headers, and brute-force protection.
    6263* **Image Tools** - Bulk add missing width/height dimensions, WebP conversion, regenerate thumbnails, and non-destructive image editing.
    … …  
    6566* **.htaccess Editor** - Safe .htaccess editing with automatic backup and one-click restore.
    6667* **Redirection Manager** - 301/302 redirect rules with import/export and first-login redirect support.
    67 * **Chatbot** - On-site assistant widget for visitor support, powered by your own uploaded documents.
     68* **Chatbot** - Grounded website assistant using only public published content and explicitly approved document text, with optional AI-generated answers and source citations.
    6869
    6970== Installation ==
    … …  
    99100Enter your free Google API key in **SEO Auditor Tools → Audit Settings**. The plugin will fetch Lighthouse scores when you run a scan and display them directly in the dashboard.
    100101
     102= Does AI Helper change content automatically? =
     103
     104No. AI Helper asks the configured provider for a structured proposal, validates it, and shows it to an administrator for review and editing. Nothing is written until the administrator explicitly approves it. Applied changes are recorded and can be undone if the target has not subsequently changed.
     105
     106= Where must Ollama run? =
     107
     108The default endpoint points to the same server that hosts WordPress. Ollama running on your personal computer is not reachable from a remotely hosted WordPress site unless you provide a deliberately secured connection. Trusted remote Ollama hosts can be enabled by a developer with the `seoautopro_ai_helper_allowed_ollama_hosts` filter.
     109
     110= Which AI providers can I use? =
     111
     112The Free list includes Ollama, Google Gemini, GroqCloud, OpenRouter, and a custom OpenAI-compatible endpoint. The Paid list includes Google Gemini, GroqCloud, OpenRouter, OpenAI, Anthropic Claude, and a custom OpenAI-compatible endpoint. A provider's own free-tier limits, model availability, pricing, and account requirements can change. You can always enter a custom model ID when a newer supported model is not yet listed.
     113
     114API keys can be saved through the AI Helper screen or kept out of the WordPress database by defining the matching `SEOAUTOPRO_AI_GEMINI_API_KEY`, `SEOAUTOPRO_AI_GROQ_API_KEY`, `SEOAUTOPRO_AI_OPENROUTER_API_KEY`, `SEOAUTOPRO_AI_OPENAI_API_KEY`, `SEOAUTOPRO_AI_ANTHROPIC_API_KEY`, or `SEOAUTOPRO_AI_CUSTOM_API_KEY` constant in wp-config.php. Saved keys are never displayed or included in Settings Export.
     115
     116= Can AI Helper suggest keywords and image alt text? =
     117
     118Yes. Keyword proposals use the selected page content and are saved separately from meta descriptions after administrator approval. The block editor also provides a Generate keywords with AI action. Google Search does not use the meta-keywords tag for indexing or ranking, so treat these phrases as editorial planning aids: https://developers.google.com/search/docs/crawling-indexing/special-tags
     119
     120The audit flags informative images with missing or empty alt text. AI Helper can propose contextual alt text for images stored directly in page or post content, using the page context, source filename, nearby text, and available attachment title or caption. Decorative images marked with `role="presentation"`, `role="none"`, or `aria-hidden="true"` are excluded. Every proposal must be reviewed; AI Helper does not infer details that are absent from the available context.
     121
     122= Can the chatbot reveal drafts, private pages, or password-protected content? =
     123
     124No. Chatbot retrieval is restricted to published, publicly viewable, non-password-protected posts and pages. Drafts, pending content, private content, password-protected content, and other non-public post statuses are excluded both when content is queried and again before a result is used.
     125
     126Uploaded document text is unavailable to visitors until an administrator explicitly approves it for chatbot answers. Original uploaded documents are not retained in a public downloads directory, document download links are never shown, and chatbot conversations are not stored by the plugin.
     127
     128= What does grounded AI do in the chatbot? =
     129
     130When enabled, the chatbot sends the visitor's question and a small set of relevant, approved website excerpts to the provider configured in AI Helper. The model is instructed to answer only from those excerpts, treat source content as data rather than instructions, and identify the excerpts supporting its answer. Unsupported questions use the contact fallback. If AI is disabled or temporarily unavailable, the chatbot can still show its most relevant approved source excerpts.
     131
    101132== External Services ==
    102133
    … …  
    132163
    133164Data sent: your site's homepage URL. No personal data is transmitted.
     165
     166= Ollama (optional) =
     167
     168AI Helper can connect to an Ollama endpoint configured by a site administrator. By default, only loopback hosts on the WordPress server are allowed. Remote hosts must be explicitly trusted with the `seoautopro_ai_helper_allowed_ollama_hosts` filter.
     169
     170* Service URL: configured by the site administrator; the default is http://127.0.0.1:11434/api/generate
     171* Ollama documentation: https://docs.ollama.com/
     172
     173Data sent: the selected page title, URL, excerpt, a limited plain-text content sample, current metadata, requested output language, and the generation instructions. For alt-text proposals, the image source URL, filename, nearby page text, and available attachment title or caption are also sent. When grounded chatbot AI is enabled, the visitor's question and up to five relevant excerpts from public pages or administrator-approved document text are sent. Draft, private, password-protected, and unapproved content is never included. The image file and original chatbot document files are not uploaded by SEO Auditor Tools. AI Helper never applies model output automatically.
     174
     175= Cloud AI providers (optional) =
     176
     177When selected and configured by an administrator, AI Helper can send generation requests to Google Gemini, GroqCloud, OpenRouter, OpenAI, or Anthropic Claude. These services are disabled until the administrator selects one and supplies any required API key. Provider accounts, quotas, free tiers, and charges are managed by the provider.
     178
     179* Google Gemini API: https://generativelanguage.googleapis.com/ — Terms: https://ai.google.dev/gemini-api/terms — Privacy: https://policies.google.com/privacy
     180* GroqCloud API: https://api.groq.com/ — Terms: https://groq.com/terms-of-use/ — Privacy: https://groq.com/privacy-policy/
     181* OpenRouter API: https://openrouter.ai/api/ — Terms: https://openrouter.ai/terms — Privacy: https://openrouter.ai/privacy
     182* OpenAI API: https://api.openai.com/ — Terms: https://openai.com/policies/terms-of-use/ — Privacy: https://openai.com/policies/privacy-policy/
     183* Anthropic API: https://api.anthropic.com/ — Terms: https://www.anthropic.com/legal/commercial-terms — Privacy: https://www.anthropic.com/legal/privacy
     184
     185Data sent: the selected page title, URL, excerpt, a limited plain-text content sample, current metadata, requested output language, and the generation instructions. For alt-text proposals, the image source URL, filename, nearby page text, and available attachment title or caption are also sent. When grounded chatbot AI is enabled, the visitor's question and up to five relevant excerpts from public pages or administrator-approved document text are sent. Draft, private, password-protected, and unapproved content is never included. The image file and original chatbot document files are not uploaded by SEO Auditor Tools. The configured API key is sent only to authenticate with the selected provider. AI Helper never applies model output automatically.
     186
     187= Custom OpenAI-compatible endpoint (optional) =
     188
     189An administrator can configure a third-party or self-hosted endpoint that implements the OpenAI chat-completions request and response format. Remote endpoints must use HTTPS; loopback endpoints may use HTTP. The endpoint operator's own terms and privacy policy apply.
     190
     191Data sent: the selected page title, URL, excerpt, a limited plain-text content sample, current metadata, requested output language, generation instructions, and an API key when configured. For alt-text proposals, the image source URL, filename, nearby page text, and available attachment title or caption are also sent. When grounded chatbot AI is enabled, the visitor's question and up to five relevant excerpts from public pages or administrator-approved document text are sent. Draft, private, password-protected, and unapproved content is never included. The image file and original chatbot document files are not uploaded by SEO Auditor Tools. AI Helper never applies model output automatically.
    134192
    135193== Screenshots ==
    … …  
    144202== Changelog ==
    145203
     204= 2.0.3 =
     205* Adds optional grounded AI chatbot answers using the provider configured in AI Helper, constrained to retrieved website excerpts with validated source citations and a safe unsupported-answer fallback.
     206* Restricts chatbot sources to published, publicly viewable, non-password-protected posts and pages; drafts, private pages, pending content, and password-protected content are excluded.
     207* Requires explicit administrator approval before extracted document text can appear in chatbot answers, removes document download links, and stops retaining original chatbot documents in public uploads.
     208* Adds anonymised per-visitor and site-wide chatbot request limits, a 300-character question limit, and no conversation-history storage.
     209* Adds a chatbot restart control and a Continue chat action so the optional contact form never traps visitors in a dead end.
     210* Expands AI Helper with Free/Paid selection and adapters for Ollama, Google Gemini, GroqCloud, OpenRouter, OpenAI, Anthropic Claude, and custom OpenAI-compatible endpoints.
     211* Adds provider-specific model choices, custom model IDs, API-key handling, a saved-connection test, and output-language selection with en-GB as the default.
     212* Keeps AI API keys write-only in the interface, supports wp-config.php constants, and excludes stored keys from JSON and CSV settings exports.
     213* Adds Generate with AI directly to the block editor meta-description panel; proposals use the current unsaved title, excerpt, and page content and remain unsaved until the editor approves the post update.
     214* Adds separate AI keyword proposals in AI Helper and a Generate keywords with AI action in the block editor, with unique-phrase validation and independent approval.
     215* Connects the missing-image-alt audit finding to AI Helper, where administrators can preview, edit, approve, verify, and undo contextual alt-text changes to a selected image in page or post content.
     216* Excludes explicitly decorative images from missing-alt findings and prevents stale image proposals from overwriting subsequently edited post content.
     217* Adds an audit finding for pages that rely on generated fallback meta descriptions.
     218* Generates structured meta-description proposals using limited page context and validates all output before it can be applied.
     219* Requires an administrator to preview and approve every AI-generated change; the model never writes directly to WordPress.
     220* Adds stale-preview conflict protection, durable fix history, per-change undo, cache invalidation, targeted verification, and queued re-auditing.
     221* Restricts Ollama to server-local endpoints by default, with an explicit filter for trusted remote hosts.
     222* Prevents browser password managers from placing WordPress login credentials in audit timeout and PageSpeed API-key fields.
     223
    146224= 2.0.2 =
    147225* Adds a preview-first bulk SEO metadata importer for Yoast SEO, Rank Math SEO, AIOSEO, and SEOPress.
    … …  
    150228* Requires supported source SEO plugins to be deactivated before import and defers SEO Auditor meta output while one remains active.
    151229* Includes rollback for the last completed import and preserves fields edited after import.
    152 
    153 = 2.0.3 =
    154230* Sitemap.xml, robots.txt, and llms.txt are now written directly to the WordPress installation root instead of the uploads directory.
    155231* The Sitemap, Robots.txt, and LLMS.txt tool pages now show the exact file location, public URL, and generation status.
    156232* Cache prewarming and chatbot content lookup now read sitemap.xml and llms.txt from their new root locations.
    157233* Successfully generating a root file removes its obsolete legacy copy from uploads/seo-auditor-tools.
    158 
    159 = 2.0.2 =
    160234* Automatically purges all generated SEO Auditor page-cache files whenever the installed plugin version changes.
    161235* Adds activation and WordPress updater safeguards so stale HTML and asset URLs cannot survive plugin updates.
    … …  
    192266== Upgrade Notice ==
    193267
     268= 2.0.3 =
     269Configure a free-tier, paid, or self-hosted provider under SEO Auditor Tools → AI Helper to generate controlled, reversible meta-description, keyword, and image alt-text proposals.
     270
    194271= 2.0.2 =
    195 Use the new importer in Open Graph & Meta Description to preview and migrate metadata after deactivating the source SEO plugin.
    196 
    197 = 2.0.3 =
    198 Regenerate sitemap.xml, robots.txt, and llms.txt once after updating so the files are created in the WordPress root and obsolete uploads copies are removed.
    199 
    200 = 2.0.2 =
    201 SEO Auditor now clears its complete page cache automatically after plugin updates, preventing stale pages and asset URLs from carrying over.
     272Includes metadata migration, root sitemap/robots/LLMS generation, and automatic cache purging after updates.
    202273
    203274= 2.0.1 =
  • seo-auditor-tools/trunk/seo-auditor-tools.php

    r3645916 r3658740  
    33Plugin Name: SEO Auditor Tools
    44Plugin URI: https://wordpress.org/plugins/seo-auditor-tools
    5 Description: All-in-one free SEO and site management toolkit for WordPress. Audit, Open Graph, sitemap, robots.txt, LLMS.txt, cookie consent, analytics, UTM builder, QR codes, image tools, cache, minify, security, redirections, .htaccess editor, and chatbot. All features free — no Pro version.
    6 Version: 2.0.2
     5Description: All-in-one free SEO and site management toolkit for WordPress. Audit, AI Helper, metadata, sitemap, robots.txt, LLMS.txt, performance, security, redirections, and more. All features free — no Pro version.
     6Version: 2.0.3
    77Author: fernandofas2909
    88Author URI: https://seoauditor.one
    … …  
    1818class SEOAUTOPRO_Plugin {
    1919
    20     const VERSION = '2.0.2';
     20    const VERSION = '2.0.3';
    2121    const VERSION_OPTION = 'seoautopro_version';
    2222    private static $instance = null;
    … …  
    5454        require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-cookie-banner.php';
    5555        require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-meta-importer.php';
     56        require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-ai-providers.php';
     57        require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-ai-helper.php';
    5658        require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-opengraph.php';
    5759        require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-tracking.php';
    … …  
    9799        add_action( 'admin_init', array( 'SEOAUTOPRO_Chatbot', 'register_settings' ) );
    98100        add_action( 'admin_init', array( 'SEOAUTOPRO_Redirection', 'maybe_handle_form_submission' ) );
     101        add_action( 'admin_init', array( 'SEOAUTOPRO_AI_Helper', 'register_settings' ) );
    99102        add_action( 'admin_notices', array( 'SEOAUTOPRO_Image_Tools', 'render_media_bulk_button' ) );
    100103        add_action( 'init', array( 'SEOAUTOPRO_OpenGraph', 'register_post_meta' ), 99 );
    … …  
    128131        add_action( 'admin_post_seoautopro_chatbot_upload_doc', array( 'SEOAUTOPRO_Chatbot', 'handle_upload_doc' ) );
    129132        add_action( 'admin_post_seoautopro_chatbot_delete_doc', array( 'SEOAUTOPRO_Chatbot', 'handle_delete_doc' ) );
     133        add_action( 'admin_post_seoautopro_chatbot_toggle_doc', array( 'SEOAUTOPRO_Chatbot', 'handle_toggle_doc' ) );
     134        add_action( 'admin_post_seoautopro_ai_helper_generate', array( 'SEOAUTOPRO_AI_Helper', 'handle_generate' ) );
     135        add_action( 'admin_post_seoautopro_ai_helper_test', array( 'SEOAUTOPRO_AI_Helper', 'handle_test_connection' ) );
     136        add_action( 'admin_post_seoautopro_ai_helper_apply', array( 'SEOAUTOPRO_AI_Helper', 'handle_apply' ) );
     137        add_action( 'admin_post_seoautopro_ai_helper_discard', array( 'SEOAUTOPRO_AI_Helper', 'handle_discard' ) );
     138        add_action( 'admin_post_seoautopro_ai_helper_undo', array( 'SEOAUTOPRO_AI_Helper', 'handle_undo' ) );
    130139
    131140        // Frontend output
    … …  
    195204        add_action( 'wp_ajax_seoautopro_meta_import_rollback_start', array( 'SEOAUTOPRO_Meta_Importer', 'ajax_rollback_start' ) );
    196205        add_action( 'wp_ajax_seoautopro_meta_import_rollback_batch', array( 'SEOAUTOPRO_Meta_Importer', 'ajax_rollback_batch' ) );
     206        add_action( 'wp_ajax_seoautopro_ai_helper_editor_generate', array( 'SEOAUTOPRO_AI_Helper', 'ajax_generate_for_editor' ) );
    197207        add_action( 'wp_ajax_seoautopro_generate_qr', array( 'SEOAUTOPRO_QR_Code', 'ajax_generate_qr' ) );
    198208        add_action( 'admin_post_seoautopro_download_qr', array( 'SEOAUTOPRO_QR_Code', 'handle_download_qr' ) );
    … …  
    210220        add_action( 'seoautopro_cache_gc', array( 'SEOAUTOPRO_Cache_Tools', 'handle_gc' ) );
    211221        add_action( 'seoautopro_minify_regenerate', array( 'SEOAUTOPRO_Minify_Tools', 'handle_regenerate' ), 10, 1 );
     222        add_action( 'seoautopro_ai_helper_reaudit', array( 'SEOAUTOPRO_AI_Helper', 'handle_scheduled_reaudit' ) );
    212223
    213224        // Security
    … …  
    516527            'seoautopro-chatbot',
    517528            array( 'SEOAUTOPRO_Chatbot', 'render_page' )
     529        );
     530
     531        add_submenu_page(
     532            'seoautopro-dashboard',
     533            __( 'AI Helper', 'seo-auditor-tools' ),
     534            __( 'AI Helper', 'seo-auditor-tools' ),
     535            $cap,
     536            'seoautopro-ai-helper',
     537            array( 'SEOAUTOPRO_AI_Helper', 'render_page' )
    518538        );
    519539
    … …  
    736756
    737757        if ( $is_editor_page ) {
     758            global $post;
     759            $editor_post_id = $post instanceof WP_Post ? absint( $post->ID ) : 0;
     760
    738761            wp_enqueue_script(
    739762                'seoautopro-editor',
    … …  
    747770                'seoautopro-editor',
    748771                'SEOAUTOPRO_PAGE_META',
    749                 array( 'enabled' => true )
     772                array(
     773                    'enabled'  => true,
     774                    'post_id'  => $editor_post_id,
     775                    'ajax_url' => admin_url( 'admin-ajax.php' ),
     776                    'nonce'    => wp_create_nonce( 'seoautopro-ai-editor' ),
     777                    'ai_url'   => admin_url( 'admin.php?page=seoautopro-ai-helper' ),
     778                    'strings'  => array(
     779                        'generate'       => __( 'Generate with AI', 'seo-auditor-tools' ),
     780                        'generating'     => __( 'Generating proposal…', 'seo-auditor-tools' ),
     781                        'generated'      => __( 'Proposal inserted. Review it, then save or update the page.', 'seo-auditor-tools' ),
     782                        'generate_keywords' => __( 'Generate keywords with AI', 'seo-auditor-tools' ),
     783                        'keywords_generated' => __( 'Keyword proposal inserted. Review it, then save or update the page.', 'seo-auditor-tools' ),
     784                        'keywords_help'  => __( 'Comma-separated editorial keywords. Google Search does not use the meta-keywords tag for ranking.', 'seo-auditor-tools' ),
     785                        'error'          => __( 'AI Helper could not generate a proposal.', 'seo-auditor-tools' ),
     786                        'save_first'     => __( 'Save the draft before generating a proposal.', 'seo-auditor-tools' ),
     787                        'settings_label' => __( 'AI Helper settings', 'seo-auditor-tools' ),
     788                    ),
     789                )
    750790            );
    751791        }
Note: See TracChangeset for help on using the changeset viewer.