diff --git a/assets/css/settings.css b/assets/css/settings.css index 440c472..18e409b 100644 --- a/assets/css/settings.css +++ b/assets/css/settings.css @@ -29,3 +29,63 @@ align-items: center; margin-top: 8px; } + +.groq-ai-bulk-panel { + margin: 16px 0; + padding: 16px; + background: #fff; + border: 1px solid #dcdcde; +} + +.groq-ai-bulk-panel .description { + margin-top: 8px; +} + +#groq-ai-bulk-status { + margin-top: 8px; +} + +#groq-ai-bulk-status[data-status='error'] { + color: #b32d2e; +} + +#groq-ai-bulk-status[data-status='success'] { + color: #008a20; +} + +.groq-ai-bulk-log { + margin: 12px 0 0; + padding-left: 18px; + max-height: 220px; + overflow-y: auto; + font-size: 13px; +} + +.groq-ai-bulk-log li { + margin-bottom: 4px; +} + +.groq-ai-bulk-log li[data-status='error'] { + color: #b32d2e; +} + +.groq-ai-bulk-log li[data-status='success'] { + color: #008a20; +} + +.groq-ai-term-row.groq-ai-term-missing td { + background: #fff8e5; +} + +.groq-ai-term-row.groq-ai-term-updated td { + animation: groqAiTermPulse 1.8s ease-out 1; +} + +@keyframes groqAiTermPulse { + from { + background-color: #e3f8eb; + } + to { + background-color: transparent; + } +} diff --git a/assets/js/category-bulk.js b/assets/js/category-bulk.js new file mode 100644 index 0000000..f717552 --- /dev/null +++ b/assets/js/category-bulk.js @@ -0,0 +1,212 @@ +(function () { + const data = window.GroqAICategoryBulk || {}; + const startButton = document.getElementById('groq-ai-bulk-generate'); + const stopButton = document.getElementById('groq-ai-bulk-cancel'); + const statusField = document.getElementById('groq-ai-bulk-status'); + const logList = document.getElementById('groq-ai-bulk-log'); + + if (!startButton || !data.ajaxUrl) { + return; + } + + let queue = []; + let totalCount = 0; + let processed = 0; + let successes = 0; + let isRunning = false; + let abortRequested = false; + + function formatString(template, values) { + if (!template) { + return ''; + } + let autoIndex = 0; + return template.replace(/%(\d+\$)?[sd]/g, (match, position) => { + let valueIndex; + if (position) { + valueIndex = parseInt(position, 10) - 1; + } else { + valueIndex = autoIndex; + autoIndex += 1; + } + const replacement = values[valueIndex]; + return typeof replacement === 'undefined' ? '' : String(replacement); + }); + } + + function setStatus(message, type) { + if (!statusField) { + return; + } + statusField.textContent = message || ''; + statusField.dataset.status = type || ''; + } + + function appendLog(message, type) { + if (!logList || !message) { + return; + } + const item = document.createElement('li'); + item.textContent = message; + item.dataset.status = type || ''; + logList.appendChild(item); + } + + function resetLog() { + if (!logList) { + return; + } + logList.innerHTML = ''; + } + + function toggleButtons(running) { + isRunning = running; + startButton.disabled = running; + if (stopButton) { + stopButton.hidden = !running; + } + } + + function getPendingTerms() { + if (!Array.isArray(data.terms)) { + return []; + } + return data.terms.filter((term) => !term.processed); + } + + function updateRow(termId, words) { + const row = document.querySelector('[data-groq-ai-term-id="' + termId + '"]'); + if (!row) { + return; + } + row.classList.remove('groq-ai-term-missing'); + row.classList.add('groq-ai-term-updated'); + const wordCell = row.querySelector('.groq-ai-word-count'); + if (wordCell) { + wordCell.textContent = String(typeof words === 'number' ? words : wordCell.textContent); + } + } + + function finish(state) { + const summaryTemplate = + state === 'done' + ? data.strings && data.strings.statusDone + : state === 'stopped' + ? data.strings && data.strings.statusStopped + : ''; + + const summary = summaryTemplate + ? formatString(summaryTemplate, [successes]) + : ''; + + const statusType = state === 'done' ? 'success' : state === 'stopped' ? 'info' : ''; + setStatus(summary, statusType); + toggleButtons(false); + queue = []; + totalCount = 0; + abortRequested = false; + } + + function processNext() { + if (abortRequested) { + finish('stopped'); + return; + } + + if (!queue.length) { + finish('done'); + return; + } + + const term = queue.shift(); + const position = processed + 1; + const progressTemplate = data.strings && data.strings.statusProgress; + if (progressTemplate) { + setStatus(formatString(progressTemplate, [position, totalCount, term.name || '']), 'loading'); + } + + const payload = new URLSearchParams(); + payload.append('action', 'groq_ai_bulk_generate_terms'); + payload.append('nonce', data.nonce || ''); + payload.append('taxonomy', data.taxonomy || 'product_cat'); + payload.append('term_id', term.id); + + fetch(data.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: payload.toString(), + }) + .then((response) => response.json()) + .then((json) => { + if (!json.success) { + const errorMessage = (json.data && json.data.message) || 'Onbekende fout'; + appendLog(formatString((data.strings && data.strings.logError) || '%1$s: %2$s', [term.name || term.id, errorMessage]), 'error'); + return; + } + + term.processed = true; + successes += 1; + const words = json.data && typeof json.data.words !== 'undefined' ? json.data.words : 0; + updateRow(term.id, words); + appendLog(formatString((data.strings && data.strings.logSuccess) || '%1$s gevuld.', [term.name || term.id, words]), 'success'); + }) + .catch((error) => { + appendLog( + formatString((data.strings && data.strings.logError) || '%1$s: %2$s', [term.name || term.id, error && error.message ? error.message : 'Onbekende fout']), + 'error' + ); + }) + .finally(() => { + processed += 1; + if (abortRequested) { + finish('stopped'); + } else { + processNext(); + } + }); + } + + function startBulk() { + if (isRunning) { + return; + } + + const pending = getPendingTerms(); + if (!pending.length) { + setStatus((data.strings && data.strings.statusEmpty) || '', 'info'); + return; + } + + queue = pending.slice(); + totalCount = queue.length; + processed = 0; + successes = 0; + abortRequested = false; + resetLog(); + toggleButtons(true); + setStatus((data.strings && data.strings.statusIdle) || '', 'info'); + processNext(); + } + + startButton.addEventListener('click', startBulk); + + if (stopButton) { + stopButton.addEventListener('click', () => { + if (!isRunning) { + return; + } + const confirmation = ! (data.strings && data.strings.confirmStop) + ? window.confirm('Stoppen?') + : window.confirm(data.strings.confirmStop); + if (confirmation) { + abortRequested = true; + } + }); + } + + if (!Array.isArray(data.terms) || !data.terms.length) { + setStatus((data.strings && data.strings.statusEmpty) || '', 'info'); + } +})(); diff --git a/groq-ai-product-text.php b/groq-ai-product-text.php index d922ba0..da817d9 100644 --- a/groq-ai-product-text.php +++ b/groq-ai-product-text.php @@ -2,7 +2,7 @@ /** * Plugin Name: SitiAI Product Teksten * Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce. - * Version: 1.5.0 + * Version: 1.6.0 * Author: SitiAI * Text Domain: siti-ai-product-content-generator * Domain Path: /languages diff --git a/includes/Admin/class-groq-ai-settings-page.php b/includes/Admin/class-groq-ai-settings-page.php index 164e4d8..fb58200 100644 --- a/includes/Admin/class-groq-ai-settings-page.php +++ b/includes/Admin/class-groq-ai-settings-page.php @@ -173,10 +173,44 @@ class Groq_AI_Product_Text_Settings_Page { if ( is_wp_error( $terms ) ) { $terms = []; } + + $word_map = []; + $empty_terms = []; + foreach ( $terms as $term ) { + if ( ! $term || ! is_object( $term ) ) { + continue; + } + $word_count = $this->count_words( isset( $term->description ) ? $term->description : '' ); + $word_map[ $term->term_id ] = $word_count; + if ( 0 === $word_count ) { + $empty_terms[] = $term; + } + } ?>
+ +
++ + +
+ +| name ); ?> | slug ); ?> | - | + | + + + + |