Plugin Directory


Ignore:
Timestamp:
10/17/2026 01:47:11 PM (13 days ago)
Author:
mxchat
Message:

Release 3.2.26 - security hardening, new models (GPT-6.1 Sol, Claude 5.5 family, Grok 4.7, Gemini 4 Argon, DeepSeek Flash), automatic moves off discontinued models, token usage in Transcripts, page-builder and ACF Blocks imports

File:
1 edited

Legend:

Unmodified
Added
Removed
  • mxchat-basic/trunk/js/content-selector.js

    r3724923 r3751971  
    3636    $closeButtons.on('click', function() {
    3737        $modal.hide();
     38        // No word count keeps reading pages behind a closed modal (M6).
     39        wordCountRun++;
     40        wordQueue = [];
    3841    });
    3942// Handle import option box clicks (for non-WordPress options)
    … …  
    171174    $loading.show();
    172175    $contentList.find('.mxchat-kb-content-item').remove();
     176    wordCountRun++; // a word count still arriving for the previous list is dropped
     177    wordQueue = [];
    173178   
    174179    const data = {
    … …  
    199204                if (items.length > 0) {
    200205                    renderContentItems(items);
     206                    countPendingWords(items);
    201207                    renderPagination(parseInt(response.data.current_page), parseInt(response.data.total_pages));
    202208                   
    … …  
    225231}
    226232
     233// Word counts the list could not show yet (M6): a page whose text the import reads from its
     234// published page is counted on the server after the list is shown, a few rows per request.
     235// One request is in flight at a time, across list reloads too (each count reads a page from
     236// this site, so small hosting plans are not asked for several at once). A newer list replaces
     237// the queue and an answer for an older list is dropped. A row keeps its saved count if
     238// counting fails. Counting pauses while selected content is imported, and the import waits
     239// for a count still in flight: on a host with two PHP workers, a count and an import that
     240// both read a page from the site would leave neither read a worker to answer it.
     241let wordCountRun = 0;
     242let wordQueue = [];
     243let wordBusy = false;
     244let wordPaused = false;
     245let wordIdle = [];
     246
     247function pauseWordCounts(then) {
     248    wordPaused = true;
     249    if (wordBusy) {
     250        wordIdle.push(then);
     251    } else {
     252        then();
     253    }
     254}
     255
     256function resumeWordCounts() {
     257    wordPaused = false;
     258    if (!wordBusy) {
     259        pumpWordCounts();
     260    }
     261}
     262
     263function countingLabel() {
     264    return (mxchatSelector.i18n && mxchatSelector.i18n.countingWords) || 'Counting words…';
     265}
     266
     267function showWords(id, words) {
     268    const item = allItems.find(function(i) { return i.id === id; });
     269    if (item) {
     270        if (words !== null && words !== undefined) { item.word_count = words; }
     271        item.word_count_pending = false;
     272    }
     273    const shown = item ? item.word_count : words;
     274    if (shown !== null && shown !== undefined) {
     275        $contentList.find('.mxchat-kb-content-item[data-id="' + id + '"] .mxchat-kb-content-words').text(shown + ' words');
     276    }
     277}
     278
     279function countPendingWords(items) {
     280    wordQueue = items.filter(function(item) { return item.word_count_pending; }).map(function(item) { return item.id; });
     281    if (!wordBusy && !wordPaused) {
     282        pumpWordCounts();
     283    }
     284}
     285
     286function pumpWordCounts() {
     287    if (wordPaused || wordQueue.length === 0) {
     288        wordBusy = false;
     289        const waiting = wordIdle;
     290        wordIdle = [];
     291        waiting.forEach(function(then) { then(); });
     292        return;
     293    }
     294    wordBusy = true;
     295    const run = wordCountRun;
     296    const batch = wordQueue.splice(0, 4);
     297    $.ajax({
     298        url: mxchatSelector.ajaxurl,
     299        method: 'POST',
     300        dataType: 'json',
     301        data: { action: 'mxchat_count_indexed_words', nonce: mxchatSelector.nonce, post_ids: batch }
     302    }).done(function(response) {
     303        if (run !== wordCountRun) {
     304            return;
     305        }
     306        const counts = (response && response.success && response.data && response.data.counts) || null;
     307        if (!counts) {
     308            batch.forEach(function(id) { showWords(id, null); });
     309            return;
     310        }
     311        // Ids the server had no time for go back to the front of the queue.
     312        const missed = [];
     313        batch.forEach(function(id) {
     314            if (Object.prototype.hasOwnProperty.call(counts, id)) { showWords(id, counts[id]); } else { missed.push(id); }
     315        });
     316        if (missed.length === batch.length) {
     317            missed.forEach(function(id) { showWords(id, null); }); // never ask again for nothing
     318        } else {
     319            wordQueue.unshift.apply(wordQueue, missed);
     320        }
     321    }).fail(function() {
     322        if (run === wordCountRun) {
     323            batch.forEach(function(id) { showWords(id, null); });
     324        }
     325    }).always(pumpWordCounts);
     326}
     327
    227328// Helper function to display appropriate "no results" message
    228329function displayNoResults(processedStatus) {
    … …  
    240341}
    241342   
     343    // Server text in the list (titles, excerpts, dates, labels, links) is shown as text,
     344    // never parsed as markup (M7g). Titles and excerpts come with WordPress's display
     345    // entities (’, &): those are decoded first, in an inert textarea, so a
     346    // title reads exactly as it does in the Posts list.
     347    const escapeText = function(text) { return $('<div>').text(String(text == null ? '' : text)).html().replace(/"/g, '&quot;').replace(/'/g, '&#039;'); };
     348    const decodeEntities = function(text) { const area = document.createElement('textarea'); area.innerHTML = String(text == null ? '' : text); return area.value; };
     349    const plainText = function(text) { return escapeText(decodeEntities(text)); };
     350
    242351    // Render content items
    243352    function renderContentItems(items) {
    … …  
    264373                    <div class="mxchat-kb-content-details">
    265374                        <div class="mxchat-kb-content-title">
    266                             <a href="${item.permalink}" target="_blank">${item.title}</a>
     375                            <a href="${escapeText(item.permalink)}" target="_blank">${plainText(item.title)}</a>
    267376                            <span class="${badgeClass}">${badgeText}</span>
    268                             ${isProcessed ? '<span class="mxchat-kb-last-updated">Last updated: ' + item.processed_date + '</span>' : ''}
     377                            ${isProcessed ? '<span class="mxchat-kb-last-updated">Last updated: ' + plainText(item.processed_date) + '</span>' : ''}
    269378                        </div>
    270379                        <div class="mxchat-kb-content-meta">
    271                             <span class="mxchat-kb-content-type">${item.type_label || item.type}</span>
    272                             <span class="mxchat-kb-content-date">${item.date}</span>
    273                             <span class="mxchat-kb-content-words">${item.size_label ? item.size_label : item.word_count + ' words'}</span>
     380                            <span class="mxchat-kb-content-type">${plainText(item.type_label || item.type)}</span>
     381                            <span class="mxchat-kb-content-date">${plainText(item.date)}</span>
     382                            <span class="mxchat-kb-content-words">${plainText(item.size_label ? item.size_label : (item.word_count_pending ? countingLabel() : item.word_count + ' words'))}</span>
    274383                        </div>
    275                         <div class="mxchat-kb-content-excerpt">${item.excerpt}</div>
     384                        <div class="mxchat-kb-content-excerpt">${plainText(item.excerpt)}</div>
    276385                    </div>
    277386                </div>
    … …  
    487596    let updated = 0;
    488597    let failed = 0;
     598    let skipped = 0;
    489599    const results = {
    490600        success: [],
    491601        updated: [],
    492         failed: []
     602        failed: [],
     603        skipped: []
    493604    };
     605    // Strings for posts a developer filter leaves out (plan 9dd0da). Server text in the
     606    // results (titles, reasons, failure messages) is always escaped (M7b).
     607    const i18n = mxchatSelector.i18n || {};
     608    const escapeHtml = function(text) { return $('<div>').text(String(text)).html(); };
    494609   
    495610    // UPDATED: Get current bot_id for WordPress content processing
    … …  
    567682            dataType: 'json',
    568683            success: function(response) {
    569                 if (response.success) {
     684                if (response.success && response.data && response.data.skipped) {
     685                    // A developer filter excluded this post: a skip with its reason, not a failure.
     686                    skipped++;
     687                    results.skipped.push({
     688                        id: postId,
     689                        title: response.data.title || ('ID: ' + postId),
     690                        reason: response.data.reason || ''
     691                    });
     692                    $progressModal.find('.mxchat-kb-current-item').text(
     693                        (i18n.skippedItem || 'Skipped "%1$s": %2$s')
     694                            .replace('%1$s', response.data.title || ('ID: ' + postId))
     695                            .replace('%2$s', response.data.reason || '')
     696                    );
     697                } else if (response.success) {
    570698                    if (isUpdate) {
    571699                        updated++;
    … …  
    627755        // Remove progress modal
    628756        $progressModal.remove();
     757        resumeWordCounts();
    629758
    630759        // Determine notification type based on results
    … …  
    634763        } else if (failed > 0) {
    635764            notificationClass = processed > 0 || updated > 0 ? 'warning' : 'error';
     765        } else if (skipped > 0 && processed === 0 && updated === 0) {
     766            notificationClass = 'info';
    636767        }
    637768
    … …  
    653784        } else if (updated > 0) {
    654785            resultHTML += 'Updated ' + updated + ' items successfully';
     786        } else if (skipped > 0) {
     787            resultHTML += escapeHtml((i18n.onlySkipped || 'Nothing was imported: %d skipped').replace('%d', skipped));
    655788        } else {
    656789            resultHTML += 'No items were processed successfully';
    657790        }
    658791
     792        if (skipped > 0 && (wasAborted || processed > 0 || updated > 0)) {
     793            resultHTML += ', ' + escapeHtml((i18n.skippedCount || '%d skipped').replace('%d', skipped));
     794        }
     795
    659796        if (failed > 0 && !wasAborted) {
    660797            resultHTML += ' with ' + failed + ' failures';
    … …  
    663800        resultHTML += '</h4>';
    664801       
    665         // Add details if there were failures
     802        // Add details if there were failures or skips
     803        if (failed > 0 || skipped > 0) {
     804            resultHTML += '<div class="mxchat-kb-results-details">';
     805        }
    666806        if (failed > 0) {
    667             resultHTML += '<div class="mxchat-kb-results-details">';
    668807            resultHTML += '<h5>Failed Items:</h5><ul>';
    669808           
    670809            results.failed.forEach(function(item) {
    671                 resultHTML += '<li><strong>ID: ' + item.id + '</strong>: ' + item.error + '</li>';
     810                resultHTML += '<li><strong>ID: ' + escapeHtml(item.id) + '</strong>: ' + escapeHtml(item.error) + '</li>';
    672811            });
    673812           
    674             resultHTML += '</ul></div>';
     813            resultHTML += '</ul>';
     814        }
     815        if (skipped > 0) {
     816            resultHTML += '<h5 class="mxchat-kb-results-skipped">' + escapeHtml(i18n.skippedHeading || 'Skipped') + '</h5><ul>';
     817            results.skipped.forEach(function(item) {
     818                resultHTML += '<li><strong>' + escapeHtml(item.title) + '</strong>: ' + escapeHtml(item.reason) + '</li>';
     819            });
     820            resultHTML += '</ul>';
     821        }
     822        if (failed > 0 || skipped > 0) {
     823            resultHTML += '</div>';
    675824        }
    676825       
    … …  
    704853    }
    705854   
    706     // Start processing the first post
    707     processNext(0);
     855    // Start processing the first post once no word count is reading a page (M6)
     856    pauseWordCounts(function() {
     857        processNext(0);
     858    });
    708859});
    709860
    … …  
    836987                    ((typeof mxchatPromptsAdmin !== 'undefined' && mxchatPromptsAdmin.prompts_setting_nonce) ? mxchatPromptsAdmin.prompts_setting_nonce : '');
    837988
    838         function escapeText(text) { return $('<div>').text(String(text == null ? '' : text)).html(); }
     989        function escapeText(text) { return $('<div>').text(String(text == null ? '' : text)).html().replace(/"/g, '&quot;').replace(/'/g, '&#039;'); }
    839990        function notice(kind, text) {
    840991            $result.html('<div class="mxch-notice mxch-notice-' + kind + '" style="margin: 12px 0;"><span>' + escapeText(text) + '</span></div>').show();
Note: See TracChangeset for help on using the changeset viewer.