Add core classes and tests for Groq AI compatibility, logging, and model services

- Implement Groq_AI_Compatibility_Service to manage WooCommerce dependency and admin notices.
- Create Groq_AI_Log_Scheduler for scheduled log cleanup based on settings.
- Develop Groq_AI_Model_Service for model selection and caching.
- Add language translations in POT file for Dutch.
- Set up PHPUnit configuration and bootstrap for testing.
- Implement unit tests for model exclusions, provider request building, settings management, and term saving functionality.
This commit is contained in:
2026-01-31 17:48:46 +00:00
parent 26aabdb2d8
commit 6cff0b6f58
25 changed files with 3131 additions and 368 deletions

1
.phpunit.result.cache Normal file
View File

@@ -0,0 +1 @@
{"version":1,"defects":{"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":3,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":4,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":4,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":4,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":4},"times":{"ModelExclusionsTest::test_ensure_allowed_blocks_excluded_model":0.002,"ModelExclusionsTest::test_filter_models_removes_excluded_entries":0,"SettingsManagerTest::test_logs_retention_days_sanitized_and_capped":0,"SettingsManagerTest::test_logs_retention_days_allows_zero":0,"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":0,"SettingsManagerTest::test_sanitize_accepts_all_settings_keys":0,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":0,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":0,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":0,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":0}}

View File

@@ -96,8 +96,35 @@
statusField.setAttribute('data-status', type); statusField.setAttribute('data-status', type);
} }
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...'; const localized = (window.GroqAIGenerator && GroqAIGenerator.strings) || {};
const retryText = window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.'; const loadingText = localized.loading || (window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...');
const retryText = localized.retry || (window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.');
const errorDefaultText = localized.errorDefault || (window.wp && wp.i18n ? wp.i18n.__('Er ging iets mis bij het genereren.', 'siti-ai-product-content-generator') : 'Er ging iets mis bij het genereren.');
const errorUnknownText = localized.errorUnknown || (window.wp && wp.i18n ? wp.i18n.__('Onbekende fout.', 'siti-ai-product-content-generator') : 'Onbekende fout.');
const successText = localized.success || (window.wp && wp.i18n ? wp.i18n.__('Structuur gegenereerd. Kopieer of vul velden in.', 'siti-ai-product-content-generator') : 'Structuur gegenereerd. Kopieer of vul velden in.');
const fieldAppliedText = localized.fieldApplied || (window.wp && wp.i18n ? wp.i18n.__('%s ingevuld.', 'siti-ai-product-content-generator') : '%s ingevuld.');
const fieldApplyErrorText = localized.fieldApplyError || (window.wp && wp.i18n ? wp.i18n.__('Kon het veld niet automatisch invullen.', 'siti-ai-product-content-generator') : 'Kon het veld niet automatisch invullen.');
const fieldCopiedText = localized.fieldCopied || (window.wp && wp.i18n ? wp.i18n.__('%s gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : '%s gekopieerd naar het klembord.');
const jsonCopiedText = localized.jsonCopied || (window.wp && wp.i18n ? wp.i18n.__('JSON gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : 'JSON gekopieerd naar het klembord.');
const copyFailedText = localized.copyFailed || (window.wp && wp.i18n ? wp.i18n.__('Kopiëren mislukt.', 'siti-ai-product-content-generator') : 'Kopiëren mislukt.');
function formatString(template, values) {
if (!template) {
return '';
}
let autoIndex = 0;
return template.replace(/%(\d+\$)?s/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 toggleLoading(isLoading) { function toggleLoading(isLoading) {
modal.classList.toggle('is-loading', isLoading); modal.classList.toggle('is-loading', isLoading);
@@ -137,7 +164,7 @@
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
if (!json.success) { if (!json.success) {
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout'; const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText;
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -154,13 +181,12 @@
if (jsonCopyButton) { if (jsonCopyButton) {
jsonCopyButton.disabled = false; jsonCopyButton.disabled = false;
} }
setStatus('Structuur gegenereerd. Kopieer of vul velden in.', 'success'); setStatus(successText, 'success');
}) })
.catch((error) => { .catch((error) => {
const message = error && error.message ? error.message : 'Er ging iets mis bij het genereren.'; const message = error && error.message ? error.message : errorDefaultText;
setStatus(loadingText, 'error'); const fullMessage = `${errorDefaultText} ${message}. ${retryText}`;
const fullMessage = `${loadingText} ${message}. ${retryText}`; setStatus(fullMessage, 'error');
statusField.textContent = fullMessage;
}) })
.finally(() => { .finally(() => {
toggleLoading(false); toggleLoading(false);
@@ -308,10 +334,10 @@
} }
if (applied) { if (applied) {
setStatus(entry.label + ' ingevuld.', 'success'); setStatus(formatString(fieldAppliedText, [entry.label]), 'success');
setFieldStatus(fieldKey, 'success'); setFieldStatus(fieldKey, 'success');
} else { } else {
setStatus('Kon het veld niet automatisch invullen.', 'error'); setStatus(fieldApplyErrorText, 'error');
setFieldStatus(fieldKey, 'error'); setFieldStatus(fieldKey, 'error');
} }
} }
@@ -340,10 +366,10 @@
} }
copyToClipboard(entry.textarea.value) copyToClipboard(entry.textarea.value)
.then(() => { .then(() => {
setStatus(entry.label + ' gekopieerd naar het klembord.', 'success'); setStatus(formatString(fieldCopiedText, [entry.label]), 'success');
}) })
.catch(() => { .catch(() => {
setStatus('Kopiëren mislukt.', 'error'); setStatus(copyFailedText, 'error');
}); });
} }
@@ -358,10 +384,10 @@
const text = resultField ? resultField.textContent.trim() : ''; const text = resultField ? resultField.textContent.trim() : '';
copyToClipboard(text) copyToClipboard(text)
.then(() => { .then(() => {
setStatus('JSON gekopieerd naar het klembord.', 'success'); setStatus(jsonCopiedText, 'success');
}) })
.catch(() => { .catch(() => {
setStatus('Kopiëren mislukt.', 'error'); setStatus(copyFailedText, 'error');
}); });
}); });
} }

View File

@@ -14,6 +14,14 @@
return; return;
} }
const strings = data.strings || {};
const providerUnsupportedText = strings.providerUnsupported || 'Deze aanbieder ondersteunt dit niet.';
const apiKeyRequiredText = strings.apiKeyRequired || 'Vul eerst de API-sleutel in.';
const loadingModelsText = strings.loadingModels || 'Modellen worden opgehaald…';
const errorUnknownText = strings.errorUnknown || 'Onbekende fout';
const successModelsText = strings.successModels || 'Modellen bijgewerkt.';
const errorFetchText = strings.errorFetch || 'Ophalen mislukt.';
const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]'); const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]');
const modelSelect = document.getElementById('groq-ai-model-select'); const modelSelect = document.getElementById('groq-ai-model-select');
const refreshButton = document.getElementById('groq-ai-refresh-models'); const refreshButton = document.getElementById('groq-ai-refresh-models');
@@ -164,19 +172,19 @@
const provider = providerSelect ? providerSelect.value : data.currentProvider; const provider = providerSelect ? providerSelect.value : data.currentProvider;
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null; const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
if (!providerData || !providerData.supports_live) { if (!providerData || !providerData.supports_live) {
setRefreshStatus('Deze aanbieder ondersteunt dit niet.', 'error'); setRefreshStatus(providerUnsupportedText, 'error');
return; return;
} }
const keyField = document.querySelector('[data-provider-row="' + provider + '"] input'); const keyField = document.querySelector('[data-provider-row="' + provider + '"] input');
const apiKey = keyField ? keyField.value.trim() : ''; const apiKey = keyField ? keyField.value.trim() : '';
if (!apiKey) { if (!apiKey) {
setRefreshStatus('Vul eerst de API-sleutel in.', 'error'); setRefreshStatus(apiKeyRequiredText, 'error');
return; return;
} }
refreshButton.disabled = true; refreshButton.disabled = true;
setRefreshStatus('Modellen worden opgehaald…', 'loading'); setRefreshStatus(loadingModelsText, 'loading');
const payload = new URLSearchParams(); const payload = new URLSearchParams();
payload.append('action', 'groq_ai_refresh_models'); payload.append('action', 'groq_ai_refresh_models');
@@ -194,14 +202,14 @@
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
if (!json.success || !json.data || !Array.isArray(json.data.models)) { if (!json.success || !json.data || !Array.isArray(json.data.models)) {
throw new Error((json.data && json.data.message) || 'Onbekende fout'); throw new Error((json.data && json.data.message) || errorUnknownText);
} }
data.providers[provider].models = json.data.models; data.providers[provider].models = json.data.models;
buildModelOptions(); buildModelOptions();
setRefreshStatus('Modellen bijgewerkt.', 'success'); setRefreshStatus(successModelsText, 'success');
}) })
.catch((error) => { .catch((error) => {
setRefreshStatus(error.message || 'Ophalen mislukt.', 'error'); setRefreshStatus(error.message || errorFetchText, 'error');
}) })
.finally(() => { .finally(() => {
refreshButton.disabled = false; refreshButton.disabled = false;

View File

@@ -20,6 +20,14 @@
const includeTopProducts = document.getElementById('groq-ai-term-include-top-products'); const includeTopProducts = document.getElementById('groq-ai-term-include-top-products');
const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit'); const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit');
const strings = (window.GroqAITermGenerator && GroqAITermGenerator.strings) || {};
const promptRequiredText = strings.promptRequired || 'Vul eerst een prompt in.';
const loadingText = strings.loading || 'AI is bezig met schrijven...';
const successText = strings.success || 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.';
const applySuccessText = strings.applySuccess || 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.';
const errorDefaultText = strings.errorDefault || 'Er ging iets mis bij het genereren.';
const errorUnknownText = strings.errorUnknown || 'Onbekende fout';
function setStatus(message, type) { function setStatus(message, type) {
if (!statusField) { if (!statusField) {
return; return;
@@ -75,7 +83,7 @@
rankmathKeywordsField.value = outputFocusKeywordsField.value || ''; rankmathKeywordsField.value = outputFocusKeywordsField.value || '';
} }
setStatus('Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', 'success'); setStatus(applySuccessText, 'success');
}); });
} }
@@ -83,12 +91,12 @@
event.preventDefault(); event.preventDefault();
const prompt = promptField ? (promptField.value || '').trim() : ''; const prompt = promptField ? (promptField.value || '').trim() : '';
if (!prompt) { if (!prompt) {
setStatus('Vul eerst een prompt in.', 'error'); setStatus(promptRequiredText, 'error');
return; return;
} }
setLoading(true); setLoading(true);
setStatus('AI is bezig met schrijven...', 'loading'); setStatus(loadingText, 'loading');
if (rawField) { if (rawField) {
rawField.textContent = ''; rawField.textContent = '';
} }
@@ -109,7 +117,7 @@
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
if (!json.success) { if (!json.success) {
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout'; const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText;
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -137,10 +145,10 @@
rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim(); rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();
} }
setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success'); setStatus(successText, 'success');
}) })
.catch((error) => { .catch((error) => {
setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error'); setStatus(error && error.message ? error.message : errorDefaultText, 'error');
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);

View File

@@ -11,6 +11,13 @@
const strings = data.strings || {}; const strings = data.strings || {};
const allowRegenerate = !!data.allowRegenerate; const allowRegenerate = !!data.allowRegenerate;
const unknownErrorText = strings.unknownError || 'Onbekende fout';
const unknownTermText = strings.unknownTerm || 'Onbekende term.';
const confirmStopFallbackText = strings.confirmStopFallback || 'Stoppen?';
const logErrorDefaultText = strings.logErrorDefault || '%1$s: %2$s';
const logSuccessDefaultText = strings.logSuccessDefault || '%1$s gevuld.';
const regenerateErrorDefaultText = strings.regenerateErrorDefault || '%1$s mislukt: %2$s';
const regenerateDoneDefaultText = strings.regenerateDoneDefault || '%s is bijgewerkt.';
const terms = (Array.isArray(data.terms) ? data.terms : []) const terms = (Array.isArray(data.terms) ? data.terms : [])
.map((term) => { .map((term) => {
const id = parseInt(term.id, 10); const id = parseInt(term.id, 10);
@@ -146,19 +153,19 @@
function handleResponse(term, json, context) { function handleResponse(term, json, context) {
if (!json || !json.success) { if (!json || !json.success) {
const errorMessage = (json && json.data && json.data.message) || 'Onbekende fout'; const errorMessage = (json && json.data && json.data.message) || unknownErrorText;
appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, errorMessage]), 'error'); appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, errorMessage]), 'error');
if (context === 'single') { if (context === 'single') {
setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, errorMessage]), 'error'); setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [term.name || term.id, errorMessage]), 'error');
} }
return false; return false;
} }
const words = json.data && typeof json.data.words !== 'undefined' ? parseInt(json.data.words, 10) : term.words; const words = json.data && typeof json.data.words !== 'undefined' ? parseInt(json.data.words, 10) : term.words;
markTermCompleted(term, Number.isFinite(words) ? words : term.words); markTermCompleted(term, Number.isFinite(words) ? words : term.words);
appendLog(formatString(strings.logSuccess || '%1$s gevuld.', [term.name || term.id, term.words]), 'success'); appendLog(formatString(strings.logSuccess || logSuccessDefaultText, [term.name || term.id, term.words]), 'success');
if (context === 'single') { if (context === 'single') {
setStatus(formatString(strings.regenerateDone || '%s is bijgewerkt.', [term.name || term.id]), 'success'); setStatus(formatString(strings.regenerateDone || regenerateDoneDefaultText, [term.name || term.id]), 'success');
} }
return true; return true;
} }
@@ -233,7 +240,7 @@
if (!isRunning) { if (!isRunning) {
return; return;
} }
const confirmation = strings.confirmStop ? window.confirm(strings.confirmStop) : window.confirm('Stoppen?'); const confirmation = strings.confirmStop ? window.confirm(strings.confirmStop) : window.confirm(confirmStopFallbackText);
if (confirmation) { if (confirmation) {
abortRequested = true; abortRequested = true;
} }
@@ -251,7 +258,7 @@
const termId = parseInt(button.getAttribute('data-term-id'), 10); const termId = parseInt(button.getAttribute('data-term-id'), 10);
const term = termMap.get(termId); const term = termMap.get(termId);
if (!term) { if (!term) {
setStatus('Onbekende term.', 'error'); setStatus(unknownTermText, 'error');
return; return;
} }
if (strings.confirmRegenerate) { if (strings.confirmRegenerate) {
@@ -270,9 +277,9 @@
handleResponse(term, json, 'single'); handleResponse(term, json, 'single');
}) })
.catch((error) => { .catch((error) => {
const message = error && error.message ? error.message : 'Onbekende fout'; const message = error && error.message ? error.message : unknownErrorText;
appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, message]), 'error'); appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, message]), 'error');
setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, message]), 'error'); setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [term.name || term.id, message]), 'error');
}) })
.finally(() => { .finally(() => {
button.disabled = false; button.disabled = false;

16
composer.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "sitiweb/siti-ai-product-content-generator",
"description": "SitiAI Product Teksten WordPress plugin",
"type": "wordpress-plugin",
"require-dev": {
"phpunit/phpunit": "^9.6"
},
"autoload-dev": {
"classmap": [
"includes/"
]
},
"scripts": {
"test": "phpunit"
}
}

1815
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,9 @@
/** /**
* Plugin Name: SitiAI Product Teksten * Plugin Name: SitiAI Product Teksten
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce. * Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
* Version: 1.8.0 * Version: 1.9.0
* Author: SitiAI * Author: Roberto Guagliardo | SitiWeb
* Author URI: https://sitiweb.nl/
* Text Domain: siti-ai-product-content-generator * Text Domain: siti-ai-product-content-generator
* Domain Path: /languages * Domain Path: /languages
*/ */
@@ -44,6 +45,9 @@ if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEB
require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-compatibility-service.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-model-service.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-log-scheduler.php';
require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.php'; require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-abstract-openai-provider.php'; require_once __DIR__ . '/includes/Providers/class-groq-ai-abstract-openai-provider.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php'; require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php';
@@ -106,8 +110,14 @@ final class Groq_AI_Product_Text_Plugin {
/** @var Groq_AI_Product_Text_Product_UI */ /** @var Groq_AI_Product_Text_Product_UI */
private $product_ui; private $product_ui;
/** @var bool */ /** @var Groq_AI_Compatibility_Service */
private $missing_wc_notice = false; private $compatibility_service;
/** @var Groq_AI_Model_Service */
private $model_service;
/** @var Groq_AI_Log_Scheduler */
private $log_scheduler;
public static function instance() { public static function instance() {
if ( null === self::$instance ) { if ( null === self::$instance ) {
@@ -119,6 +129,9 @@ final class Groq_AI_Product_Text_Plugin {
private function __construct() { private function __construct() {
$this->register_services(); $this->register_services();
$this->compatibility_service = new Groq_AI_Compatibility_Service();
$this->model_service = new Groq_AI_Model_Service();
$this->log_scheduler = new Groq_AI_Log_Scheduler( $this->get_settings_manager(), $this->get_generation_logger() );
$this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() ); $this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() );
$this->categories_admin = new Groq_AI_Categories_Admin( $this ); $this->categories_admin = new Groq_AI_Categories_Admin( $this );
@@ -127,8 +140,11 @@ final class Groq_AI_Product_Text_Plugin {
$this->product_ui = new Groq_AI_Product_Text_Product_UI( $this ); $this->product_ui = new Groq_AI_Product_Text_Product_UI( $this );
add_action( 'init', [ $this, 'load_textdomain' ] ); add_action( 'init', [ $this, 'load_textdomain' ] );
add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] ); $logger = $this->container->get( 'generation_logger' );
add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] ); add_action( 'plugins_loaded', [ $logger, 'maybe_create_table' ] );
add_action( 'load-plugins.php', [ $this->compatibility_service, 'maybe_deactivate_if_woocommerce_missing' ] );
add_action( 'init', [ $this->log_scheduler, 'ensure_logs_cleanup_schedule' ] );
add_action( 'groq_ai_cleanup_logs', [ $this->log_scheduler, 'cleanup_logs' ] );
add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 ); add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 );
} }
@@ -238,60 +254,84 @@ final class Groq_AI_Product_Text_Plugin {
return self::OPTION_KEY; return self::OPTION_KEY;
} }
public function get_provider_manager() { public function __call( $name, $arguments ) {
switch ( $name ) {
case 'get_provider_manager':
return $this->container->get( 'provider_manager' ); return $this->container->get( 'provider_manager' );
} case 'get_settings_manager':
public function get_settings_manager() {
return $this->container->get( 'settings_manager' ); return $this->container->get( 'settings_manager' );
} case 'get_prompt_builder':
public function get_prompt_builder() {
return $this->container->get( 'prompt_builder' ); return $this->container->get( 'prompt_builder' );
} case 'get_conversation_manager':
public function get_conversation_manager() {
return $this->container->get( 'conversation_manager' ); return $this->container->get( 'conversation_manager' );
} case 'get_generation_logger':
public function get_generation_logger() {
return $this->container->get( 'generation_logger' ); return $this->container->get( 'generation_logger' );
case 'get_settings':
return $this->container->get( 'settings_manager' )->all();
case 'sanitize_settings':
return $this->container->get( 'settings_manager' )->sanitize( $arguments[0] ?? [] );
case 'get_context_field_definitions':
return $this->container->get( 'settings_manager' )->get_context_field_definitions();
case 'get_default_modules_settings':
return $this->container->get( 'settings_manager' )->get_default_modules_settings();
case 'get_default_context_fields':
return $this->container->get( 'settings_manager' )->get_default_context_fields();
case 'normalize_context_fields':
return $this->container->get( 'settings_manager' )->normalize_context_fields( $arguments[0] ?? [] );
case 'get_module_config':
return $this->container->get( 'settings_manager' )->get_module_config( $arguments[0] ?? '', $arguments[1] ?? null );
case 'is_module_enabled':
return $this->container->get( 'settings_manager' )->is_module_enabled( $arguments[0] ?? '', $arguments[1] ?? null );
case 'get_rankmath_focus_keyword_limit':
return $this->container->get( 'settings_manager' )->get_rankmath_focus_keyword_limit( $arguments[0] ?? null );
case 'get_rankmath_meta_title_pixel_limit':
return $this->container->get( 'settings_manager' )->get_rankmath_meta_title_pixel_limit( $arguments[0] ?? null );
case 'get_rankmath_meta_description_pixel_limit':
return $this->container->get( 'settings_manager' )->get_rankmath_meta_description_pixel_limit( $arguments[0] ?? null );
case 'is_response_format_compat_enabled':
return $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $arguments[0] ?? null );
case 'get_image_context_mode':
return $this->container->get( 'settings_manager' )->get_image_context_mode( $arguments[0] ?? null );
case 'get_image_context_limit':
return $this->container->get( 'settings_manager' )->get_image_context_limit( $arguments[0] ?? null );
case 'get_term_top_description_char_limit':
return $this->container->get( 'settings_manager' )->get_term_top_description_char_limit( $arguments[0] ?? null );
case 'get_term_bottom_description_char_limit':
return $this->container->get( 'settings_manager' )->get_term_bottom_description_char_limit( $arguments[0] ?? null );
case 'get_google_safety_settings':
return $this->container->get( 'settings_manager' )->get_google_safety_settings( $arguments[0] ?? null );
case 'get_google_safety_categories':
return $this->container->get( 'settings_manager' )->get_google_safety_categories();
case 'get_google_safety_thresholds':
return $this->container->get( 'settings_manager' )->get_google_safety_thresholds();
case 'get_loggable_settings_snapshot':
return $this->container->get( 'settings_manager' )->get_loggable_settings_snapshot( $arguments[0] ?? null );
case 'create_settings_renderer':
$values = $arguments[0] ?? null;
if ( null === $values ) {
$values = $this->container->get( 'settings_manager' )->all();
}
return new Groq_AI_Settings_Renderer( self::OPTION_KEY, $values );
case 'should_use_response_format':
$provider = $arguments[0] ?? null;
$settings = $arguments[1] ?? null;
if ( ! $provider instanceof Groq_AI_Provider_Interface ) {
return false;
}
return ! $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
case 'is_rankmath_active':
return $this->compatibility_service->is_rankmath_active();
case 'is_woocommerce_active':
return $this->compatibility_service->is_woocommerce_active();
case 'get_selected_model':
return $this->model_service->get_selected_model( $arguments[0], $arguments[1] ?? [] );
case 'get_cached_models_for_provider':
return $this->model_service->get_cached_models_for_provider( $arguments[0] ?? '' );
case 'update_cached_models_for_provider':
return $this->model_service->update_cached_models_for_provider( $arguments[0] ?? '', $arguments[1] ?? [] );
} }
public function get_settings() { throw new BadMethodCallException( sprintf( 'Method %s does not exist.', $name ) );
return $this->get_settings_manager()->all();
}
public function sanitize_settings( $input ) {
return $this->get_settings_manager()->sanitize( $input );
}
public function maybe_deactivate_if_woocommerce_missing() {
if ( $this->is_woocommerce_active() ) {
return;
}
if ( ! function_exists( 'deactivate_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
deactivate_plugins( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) );
$this->missing_wc_notice = true;
add_action( 'admin_notices', [ $this, 'render_missing_wc_notice' ] );
}
public function render_missing_wc_notice() {
if ( ! $this->missing_wc_notice ) {
return;
}
?>
<div class="notice notice-error">
<p>
<?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
</div>
<?php
} }
public function build_prompt_template_preview( $settings ) { public function build_prompt_template_preview( $settings ) {
@@ -312,203 +352,6 @@ final class Groq_AI_Product_Text_Plugin {
return implode( "\n\n", $parts ); return implode( "\n\n", $parts );
} }
public function get_context_field_definitions() {
return $this->get_settings_manager()->get_context_field_definitions();
}
public function get_default_modules_settings() {
return $this->get_settings_manager()->get_default_modules_settings();
}
public function get_default_context_fields() {
return $this->get_settings_manager()->get_default_context_fields();
}
public function normalize_context_fields( $fields ) {
return $this->get_settings_manager()->normalize_context_fields( $fields );
}
public function get_module_config( $module, $settings = null ) {
return $this->get_settings_manager()->get_module_config( $module, $settings );
}
public function is_module_enabled( $module, $settings = null ) {
return $this->get_settings_manager()->is_module_enabled( $module, $settings );
}
public function get_rankmath_focus_keyword_limit( $settings = null ) {
return $this->get_settings_manager()->get_rankmath_focus_keyword_limit( $settings );
}
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
return $this->get_settings_manager()->get_rankmath_meta_title_pixel_limit( $settings );
}
public function get_rankmath_meta_description_pixel_limit( $settings = null ) {
return $this->get_settings_manager()->get_rankmath_meta_description_pixel_limit( $settings );
}
public function is_response_format_compat_enabled( $settings = null ) {
return $this->get_settings_manager()->is_response_format_compat_enabled( $settings );
}
public function get_image_context_mode( $settings = null ) {
return $this->get_settings_manager()->get_image_context_mode( $settings );
}
public function get_image_context_limit( $settings = null ) {
return $this->get_settings_manager()->get_image_context_limit( $settings );
}
public function get_term_top_description_char_limit( $settings = null ) {
return $this->get_settings_manager()->get_term_top_description_char_limit( $settings );
}
public function get_term_bottom_description_char_limit( $settings = null ) {
return $this->get_settings_manager()->get_term_bottom_description_char_limit( $settings );
}
public function get_google_safety_settings( $settings = null ) {
return $this->get_settings_manager()->get_google_safety_settings( $settings );
}
public function get_google_safety_categories() {
return $this->get_settings_manager()->get_google_safety_categories();
}
public function get_google_safety_thresholds() {
return $this->get_settings_manager()->get_google_safety_thresholds();
}
public function get_loggable_settings_snapshot( $settings = null ) {
return $this->get_settings_manager()->get_loggable_settings_snapshot( $settings );
}
public function create_settings_renderer( $values = null ) {
if ( null === $values ) {
$values = $this->get_settings();
}
return new Groq_AI_Settings_Renderer( self::OPTION_KEY, $values );
}
public function should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) {
return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
}
public function is_rankmath_active() {
if ( class_exists( 'RankMath' ) ) {
return true;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'seo-by-rank-math/rank-math.php' );
}
public function is_woocommerce_active() {
if ( class_exists( 'WooCommerce' ) ) {
return true;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'woocommerce/woocommerce.php' );
}
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
$provider_key = $provider->get_key();
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
if ( '' === $model ) {
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
if ( '' !== $default ) {
return $default;
}
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
if ( ! empty( $available ) ) {
return $available[0];
}
}
return $model;
}
public function get_cached_models_for_provider( $provider ) {
$provider = sanitize_key( (string) $provider );
$cache = $this->get_models_cache();
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
}
public function update_cached_models_for_provider( $provider, $models ) {
$provider = sanitize_key( (string) $provider );
$models = $this->sanitize_models_list( $models );
$cache = $this->get_models_cache();
$cache[ $provider ] = $models;
update_option( self::MODELS_CACHE_OPTION_KEY, $cache );
return $models;
}
private function get_models_cache() {
$cache = get_option( self::MODELS_CACHE_OPTION_KEY, [] );
if ( ! is_array( $cache ) ) {
$cache = [];
}
foreach ( $cache as $provider => $models ) {
$cache[ $provider ] = $this->sanitize_models_list( $models );
}
return $cache;
}
private function sanitize_models_list( $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
$models = array_values( array_unique( $models ) );
if ( ! empty( $models ) ) {
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
}
return $models;
}
public function log_debug( $message, $context = [] ) {
$this->get_generation_logger()->log_debug( $message, $context );
}
private function extract_content_text( $result ) {
if ( is_array( $result ) && isset( $result['content'] ) ) {
return (string) $result['content'];
}
return (string) $result;
}
public function maybe_create_logs_table() {
$this->get_generation_logger()->maybe_create_table();
}
public static function activate() { public static function activate() {
$logger = new Groq_AI_Generation_Logger(); $logger = new Groq_AI_Generation_Logger();

View File

@@ -60,10 +60,14 @@ class Groq_AI_Logs_Table extends WP_List_Table {
$current_page = $this->get_pagenum(); $current_page = $this->get_pagenum();
$offset = ( $current_page - 1 ) * $per_page; $offset = ( $current_page - 1 ) * $per_page;
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at'; $allowed_orderby = [
if ( ! $orderby ) { 'created_at' => 'created_at',
$orderby = 'created_at'; 'provider' => 'provider',
} 'model' => 'model',
'status' => 'status',
];
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_key( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at';
$orderby = isset( $allowed_orderby[ $orderby ] ) ? $allowed_orderby[ $orderby ] : 'created_at';
$order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC'; $order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC';
$order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC'; $order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';

View File

@@ -73,6 +73,18 @@ class Groq_AI_Product_Text_Product_UI {
'postId' => $post_id, 'postId' => $post_id,
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(), 'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
'attributeIncludesDefaults' => $attribute_defaults, 'attributeIncludesDefaults' => $attribute_defaults,
'strings' => [
'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'retry' => __( 'Probeer het opnieuw of pas je prompt/context aan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorUnknown' => __( 'Onbekende fout.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'success' => __( 'Structuur gegenereerd. Kopieer of vul velden in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'fieldApplied' => __( '%s ingevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'fieldApplyError' => __( 'Kon het veld niet automatisch invullen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'fieldCopied' => __( '%s gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'jsonCopied' => __( 'JSON gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'copyFailed' => __( 'Kopiëren mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
] ]
); );
} }

View File

@@ -125,17 +125,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
$brands_url = $this->get_page_url( 'groq-ai-product-text-brands' ); $brands_url = $this->get_page_url( 'groq-ai-product-text-brands' );
$prompt_preview = $this->plugin->build_prompt_template_preview( $settings ); $prompt_preview = $this->plugin->build_prompt_template_preview( $settings );
$google_notice = isset( $_GET['groq_ai_google_notice'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice'] ) ) : '';
$google_status = isset( $_GET['groq_ai_google_notice_status'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice_status'] ) ) : '';
$google_message = '';
if ( isset( $_GET['groq_ai_google_notice_message'] ) ) {
$google_message = sanitize_text_field( rawurldecode( wp_unslash( $_GET['groq_ai_google_notice_message'] ) ) );
}
$google_connected = ! empty( $settings['google_oauth_refresh_token'] );
$google_connected_email = isset( $settings['google_oauth_connected_email'] ) ? (string) $settings['google_oauth_connected_email'] : '';
$google_connected_at = isset( $settings['google_oauth_connected_at'] ) ? absint( $settings['google_oauth_connected_at'] ) : 0;
$oauth_redirect = add_query_arg( 'action', 'groq_ai_google_oauth_callback', admin_url( 'admin-post.php' ) );
$google_safety_settings = $this->plugin->get_google_safety_settings( $settings ); $google_safety_settings = $this->plugin->get_google_safety_settings( $settings );
$google_safety_categories = $this->plugin->get_google_safety_categories(); $google_safety_categories = $this->plugin->get_google_safety_categories();
$google_safety_thresholds = $this->plugin->get_google_safety_thresholds(); $google_safety_thresholds = $this->plugin->get_google_safety_thresholds();
@@ -145,7 +135,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e( 'Siti AI instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1> <h1><?php esc_html_e( 'Siti AI instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p class="description"> <p class="description">
<?php esc_html_e( 'Kies je AI-aanbieder, beheer API-sleutels en koppel optioneel Google Search Console/Analytics voor extra context.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?> <?php esc_html_e( 'Kies je AI-aanbieder en beheer API-sleutels voor de contentgeneratie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p> </p>
<p style="margin:16px 0; display:flex; flex-wrap:wrap; gap:8px;"> <p style="margin:16px 0; display:flex; flex-wrap:wrap; gap:8px;">
<a class="button" href="<?php echo esc_url( $prompt_url ); ?>"><?php esc_html_e( 'Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></a> <a class="button" href="<?php echo esc_url( $prompt_url ); ?>"><?php esc_html_e( 'Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></a>
@@ -155,12 +145,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
<a class="button" href="<?php echo esc_url( $brands_url ); ?>"><?php esc_html_e( 'Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></a> <a class="button" href="<?php echo esc_url( $brands_url ); ?>"><?php esc_html_e( 'Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></a>
</p> </p>
<?php settings_errors( $option_key ); ?> <?php settings_errors( $option_key ); ?>
<?php if ( $google_notice ) :
$class = ( 'error' === $google_status ) ? 'notice-error' : 'notice-success';
$google_message = '' !== $google_message ? $google_message : ( 'connected' === $google_notice ? __( 'Google OAuth is verbonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) : ( 'disconnected' === $google_notice ? __( 'Google OAuth is ontkoppeld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) : __( 'Google test afgerond.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ) );
?>
<div class="notice <?php echo esc_attr( $class ); ?>"><p><?php echo esc_html( $google_message ); ?></p></div>
<?php endif; ?>
<div style="margin:16px 0; padding:16px; background:#fff; border:1px solid #dcdcde;"> <div style="margin:16px 0; padding:16px; background:#fff; border:1px solid #dcdcde;">
<strong><?php esc_html_e( 'Huidige promptcontext', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong> <strong><?php esc_html_e( 'Huidige promptcontext', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<pre style="background:#f6f7f7; padding:12px; overflow:auto; margin-top:8px; white-space:pre-wrap;"><?php echo esc_html( $prompt_preview ); ?></pre> <pre style="background:#f6f7f7; padding:12px; overflow:auto; margin-top:8px; white-space:pre-wrap;"><?php echo esc_html( $prompt_preview ); ?></pre>
@@ -243,6 +228,18 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
'description' => __( 'Limitering van het aantal tokens per output voor compatibiliteit met verschillende modellen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'description' => __( 'Limitering van het aantal tokens per output voor compatibiliteit met verschillende modellen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
] ]
); );
$renderer->field(
[
'label' => __( 'Logboek retentie (dagen)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'key' => 'logs_retention_days',
'type' => 'number',
'attributes' => [
'min' => 0,
'max' => 3650,
],
'description' => __( 'Hoe lang logboekregels bewaard blijven. Zet op 0 om logs onbeperkt te bewaren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
]
);
$renderer->field( $renderer->field(
[ [
'label' => __( 'Term meta key (onderste tekst)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'label' => __( 'Term meta key (onderste tekst)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
@@ -262,8 +259,80 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
$renderer->close_table(); $renderer->close_table();
?> ?>
<p class="submit"><?php submit_button( __( 'Instellingen opslaan', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'primary', 'submit', false ); ?></p>
</form>
</div>
<?php
}
/**
* Register plugin settings with WordPress.
*/
public function register_settings() {
register_setting(
$this->plugin->get_option_key(),
$this->plugin->get_option_key(),
[ $this->plugin, 'sanitize_settings' ]
);
}
public function hide_menu_links() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<style>
#adminmenu a[href="options-general.php?page=groq-ai-product-text-modules"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-logs"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-prompts"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-term"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-log"] {
display: none !important;
}
</style>
<?php
}
public function render_modules_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$option_key = $this->plugin->get_option_key();
$settings = $this->plugin->get_settings();
$current_page = $this->get_page_url( 'groq-ai-product-text-modules' );
$oauth_redirect = add_query_arg( 'action', 'groq_ai_google_oauth_callback', admin_url( 'admin-post.php' ) );
$google_notice = isset( $_GET['groq_ai_google_notice'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice'] ) ) : '';
$google_status = isset( $_GET['groq_ai_google_notice_status'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_google_notice_status'] ) ) : '';
$google_message = '';
if ( isset( $_GET['groq_ai_google_notice_message'] ) ) {
$google_message = sanitize_text_field( rawurldecode( wp_unslash( $_GET['groq_ai_google_notice_message'] ) ) );
}
$google_connected = ! empty( $settings['google_oauth_refresh_token'] );
$google_connected_email = isset( $settings['google_oauth_connected_email'] ) ? (string) $settings['google_oauth_connected_email'] : '';
$google_connected_at = isset( $settings['google_oauth_connected_at'] ) ? absint( $settings['google_oauth_connected_at'] ) : 0;
?>
<div class="wrap">
<h1><?php esc_html_e( 'Siti AI modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p class="description"><?php esc_html_e( 'Schakel aanvullende integraties in en bepaal grenzen voor gegenereerde content.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php settings_errors( $option_key ); ?>
<?php if ( $google_notice ) :
$class = ( 'error' === $google_status ) ? 'notice-error' : 'notice-success';
$google_message = '' !== $google_message ? $google_message : ( 'connected' === $google_notice ? __( 'Google OAuth is verbonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) : ( 'disconnected' === $google_notice ? __( 'Google OAuth is ontkoppeld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) : __( 'Google test afgerond.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ) );
?>
<div class="notice <?php echo esc_attr( $class ); ?>"><p><?php echo esc_html( $google_message ); ?></p></div>
<?php endif; ?>
<form method="post" action="options.php">
<?php settings_fields( $option_key ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Rank Math integratie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php $this->render_rankmath_module_field(); ?></td>
</tr>
</table>
<h2><?php esc_html_e( 'Google Search Console & Analytics', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2> <h2><?php esc_html_e( 'Google Search Console & Analytics', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<?php <?php
$renderer = $this->plugin->create_settings_renderer( $settings );
$renderer->open_table(); $renderer->open_table();
$renderer->field( $renderer->field(
[ [
@@ -320,7 +389,7 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
); );
$renderer->close_table(); $renderer->close_table();
?> ?>
<p class="submit"><?php submit_button( __( 'Instellingen opslaan', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'primary', 'submit', false ); ?></p> <?php submit_button( __( 'Modules opslaan', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); ?>
</form> </form>
<div style="margin-top:24px; padding:16px; border:1px solid #dcdcde; background:#fff;"> <div style="margin-top:24px; padding:16px; border:1px solid #dcdcde; background:#fff;">
<h2><?php esc_html_e( 'Google verbinding', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2> <h2><?php esc_html_e( 'Google verbinding', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
@@ -364,59 +433,6 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
</div> </div>
<?php <?php
} }
/**
* Register plugin settings with WordPress.
*/
public function register_settings() {
register_setting(
$this->plugin->get_option_key(),
$this->plugin->get_option_key(),
[ $this->plugin, 'sanitize_settings' ]
);
}
public function hide_menu_links() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<style>
#adminmenu a[href="options-general.php?page=groq-ai-product-text-modules"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-logs"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-prompts"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-term"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-log"] {
display: none !important;
}
</style>
<?php
}
public function render_modules_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$option_key = $this->plugin->get_option_key();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Siti AI modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p class="description"><?php esc_html_e( 'Schakel aanvullende integraties in en bepaal grenzen voor gegenereerde content.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php settings_errors( $option_key ); ?>
<form method="post" action="options.php">
<?php settings_fields( $option_key ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Rank Math integratie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php $this->render_rankmath_module_field(); ?></td>
</tr>
</table>
<?php submit_button( __( 'Modules opslaan', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); ?>
</form>
</div>
<?php
}
public function render_prompt_settings_page() { public function render_prompt_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( 'manage_options' ) ) {
return; return;
@@ -837,6 +853,14 @@ class Groq_AI_Product_Text_Settings_Page extends Groq_AI_Admin_Base {
'placeholders' => [ 'placeholders' => [
'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
], ],
'strings' => [
'providerUnsupported' => __( 'Deze aanbieder ondersteunt dit niet.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'apiKeyRequired' => __( 'Vul eerst de API-sleutel in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'loadingModels' => __( 'Modellen worden opgehaald…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorUnknown' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'successModels' => __( 'Modellen bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorFetch' => __( 'Ophalen mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
]; ];
foreach ( $this->provider_manager->get_providers() as $provider ) { foreach ( $this->provider_manager->get_providers() as $provider ) {

View File

@@ -89,10 +89,20 @@ abstract class Groq_AI_Term_Admin_Base extends Groq_AI_Admin_Base {
'taxonomy' => $taxonomy, 'taxonomy' => $taxonomy,
'terms' => $terms, 'terms' => $terms,
'allowRegenerate' => false, 'allowRegenerate' => false,
'strings' => [], 'strings' => [
'unknownError' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'unknownTerm' => __( 'Onbekende term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'confirmStopFallback' => __( 'Stoppen?', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logErrorDefault' => __( '%1$s: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logSuccessDefault' => __( '%1$s gevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateErrorDefault' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateDoneDefault' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
]; ];
$config = wp_parse_args( $overrides, $defaults ); $config = wp_parse_args( $overrides, $defaults );
$override_strings = isset( $overrides['strings'] ) && is_array( $overrides['strings'] ) ? $overrides['strings'] : [];
$config['strings'] = array_merge( $defaults['strings'], $override_strings );
wp_localize_script( 'groq-ai-term-bulk', 'GroqAITermBulk', $config ); wp_localize_script( 'groq-ai-term-bulk', 'GroqAITermBulk', $config );
} }
@@ -207,6 +217,14 @@ abstract class Groq_AI_Term_Admin_Base extends Groq_AI_Admin_Base {
'nonce' => wp_create_nonce( 'groq_ai_generate_term' ), 'nonce' => wp_create_nonce( 'groq_ai_generate_term' ),
'taxonomy' => $taxonomy, 'taxonomy' => $taxonomy,
'termId' => $term_id, 'termId' => $term_id,
'strings' => [
'promptRequired' => __( 'Vul eerst een prompt in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'success' => __( 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'applySuccess' => __( 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorUnknown' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
] ]
); );
} }

View File

@@ -438,6 +438,19 @@ class Groq_AI_Ajax_Controller {
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : ''; $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( ! $post_id ) {
wp_send_json_error( [ 'message' => __( 'Post-ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
}
$post = get_post( $post_id );
if ( ! $post || is_wp_error( $post ) || 'product' !== $post->post_type ) {
wp_send_json_error( [ 'message' => __( 'Product niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming om dit product te bewerken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
}
$settings = $this->plugin->get_settings(); $settings = $this->plugin->get_settings();
$provider_manager = $this->plugin->get_provider_manager(); $provider_manager = $this->plugin->get_provider_manager();
$provider_key = $settings['provider']; $provider_key = $settings['provider'];

View File

@@ -0,0 +1,58 @@
<?php
class Groq_AI_Compatibility_Service {
/** @var bool */
private $missing_wc_notice = false;
public function maybe_deactivate_if_woocommerce_missing() {
if ( $this->is_woocommerce_active() ) {
return;
}
if ( ! function_exists( 'deactivate_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
deactivate_plugins( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) );
$this->missing_wc_notice = true;
add_action( 'admin_notices', [ $this, 'render_missing_wc_notice' ] );
}
public function render_missing_wc_notice() {
if ( ! $this->missing_wc_notice ) {
return;
}
?>
<div class="notice notice-error">
<p>
<?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
</div>
<?php
}
public function is_rankmath_active() {
if ( class_exists( 'RankMath' ) ) {
return true;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'seo-by-rank-math/rank-math.php' );
}
public function is_woocommerce_active() {
if ( class_exists( 'WooCommerce' ) ) {
return true;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'woocommerce/woocommerce.php' );
}
}

View File

@@ -0,0 +1,28 @@
<?php
class Groq_AI_Log_Scheduler {
/** @var Groq_AI_Settings_Manager */
private $settings_manager;
/** @var Groq_AI_Generation_Logger */
private $logger;
public function __construct( Groq_AI_Settings_Manager $settings_manager, Groq_AI_Generation_Logger $logger ) {
$this->settings_manager = $settings_manager;
$this->logger = $logger;
}
public function ensure_logs_cleanup_schedule() {
if ( wp_next_scheduled( 'groq_ai_cleanup_logs' ) ) {
return;
}
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'groq_ai_cleanup_logs' );
}
public function cleanup_logs() {
$settings = $this->settings_manager->all();
$retention_days = $this->settings_manager->get_logs_retention_days( $settings );
$this->logger->cleanup_old_logs( $retention_days );
}
}

View File

@@ -0,0 +1,78 @@
<?php
class Groq_AI_Model_Service {
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
$provider_key = $provider->get_key();
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
if ( '' === $model ) {
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
if ( '' !== $default ) {
return $default;
}
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
if ( ! empty( $available ) ) {
return $available[0];
}
}
return $model;
}
public function get_cached_models_for_provider( $provider ) {
$provider = sanitize_key( (string) $provider );
$cache = $this->get_models_cache();
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
}
public function update_cached_models_for_provider( $provider, $models ) {
$provider = sanitize_key( (string) $provider );
$models = $this->sanitize_models_list( $models );
$cache = $this->get_models_cache();
$cache[ $provider ] = $models;
update_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, $cache );
return $models;
}
private function get_models_cache() {
$cache = get_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, [] );
if ( ! is_array( $cache ) ) {
$cache = [];
}
foreach ( $cache as $provider => $models ) {
$cache[ $provider ] = $this->sanitize_models_list( $models );
}
return $cache;
}
private function sanitize_models_list( $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
$models = array_values( array_unique( $models ) );
if ( ! empty( $models ) ) {
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
}
return $models;
}
}

View File

@@ -143,6 +143,20 @@ class Groq_AI_Generation_Logger {
update_option( self::OPTION_TABLE_CREATED, 1 ); update_option( self::OPTION_TABLE_CREATED, 1 );
} }
public function cleanup_old_logs( $retention_days ) {
$retention_days = absint( $retention_days );
if ( $retention_days <= 0 || ! $this->logs_table_exists() ) {
return;
}
$cutoff = time() - ( $retention_days * DAY_IN_SECONDS );
$cutoff = gmdate( 'Y-m-d H:i:s', $cutoff );
global $wpdb;
$table = $this->get_logs_table_name();
$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) );
}
private function extract_usage_token_value( $usage, $keys ) { private function extract_usage_token_value( $usage, $keys ) {
foreach ( (array) $keys as $key ) { foreach ( (array) $keys as $key ) {
if ( isset( $usage[ $key ] ) ) { if ( isset( $usage[ $key ] ) ) {

View File

@@ -33,6 +33,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '', 'store_context' => '',
'default_prompt' => '', 'default_prompt' => '',
'max_output_tokens' => 2048, 'max_output_tokens' => 2048,
'logs_retention_days' => 90,
'product_attribute_includes' => [], 'product_attribute_includes' => [],
'term_bottom_description_meta_key' => '', 'term_bottom_description_meta_key' => '',
'groq_api_key' => '', 'groq_api_key' => '',
@@ -63,6 +64,8 @@ class Groq_AI_Settings_Manager {
$settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] ); $settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] );
$settings['google_safety_settings'] = $this->sanitize_google_safety_settings( isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [] ); $settings['google_safety_settings'] = $this->sanitize_google_safety_settings( isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [] );
$settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' ); $settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' );
$logs_retention_days = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90;
$settings['logs_retention_days'] = max( 0, min( 3650, $logs_retention_days ) );
$image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url'; $image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
if ( 'none' === $image_mode ) { if ( 'none' === $image_mode ) {
@@ -108,6 +111,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '', 'store_context' => '',
'default_prompt' => '', 'default_prompt' => '',
'max_output_tokens' => 2048, 'max_output_tokens' => 2048,
'logs_retention_days' => 90,
'product_attribute_includes' => [], 'product_attribute_includes' => [],
'term_bottom_description_meta_key' => '', 'term_bottom_description_meta_key' => '',
'groq_api_key' => '', 'groq_api_key' => '',
@@ -159,6 +163,10 @@ class Groq_AI_Settings_Manager {
// Keep within sane bounds across providers. // Keep within sane bounds across providers.
$max_output_tokens = max( 128, min( 8192, $max_output_tokens ) ); $max_output_tokens = max( 128, min( 8192, $max_output_tokens ) );
$logs_retention_days = isset( $input['logs_retention_days'] ) ? (int) $input['logs_retention_days'] : (int) $defaults['logs_retention_days'];
// 0 = keep indefinitely, otherwise cap at 10 years.
$logs_retention_days = max( 0, min( 3650, $logs_retention_days ) );
$context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] ); $context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] );
if ( 'none' === $image_mode ) { if ( 'none' === $image_mode ) {
@@ -182,6 +190,7 @@ class Groq_AI_Settings_Manager {
'store_context' => sanitize_textarea_field( $input['store_context'] ), 'store_context' => sanitize_textarea_field( $input['store_context'] ),
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ), 'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
'max_output_tokens' => $max_output_tokens, 'max_output_tokens' => $max_output_tokens,
'logs_retention_days' => $logs_retention_days,
'product_attribute_includes' => $this->sanitize_product_attribute_includes( isset( $raw_input['product_attribute_includes'] ) ? $raw_input['product_attribute_includes'] : [] ), 'product_attribute_includes' => $this->sanitize_product_attribute_includes( isset( $raw_input['product_attribute_includes'] ) ? $raw_input['product_attribute_includes'] : [] ),
'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ), 'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ),
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ), 'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
@@ -442,6 +451,15 @@ class Groq_AI_Settings_Manager {
return self::get_google_safety_thresholds_list(); return self::get_google_safety_thresholds_list();
} }
public function get_logs_retention_days( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
$value = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90;
return max( 0, min( 3650, $value ) );
}
public function get_loggable_settings_snapshot( $settings = null ) { public function get_loggable_settings_snapshot( $settings = null ) {
if ( null === $settings ) { if ( null === $settings ) {
$settings = $this->all(); $settings = $this->all();
@@ -451,6 +469,7 @@ class Groq_AI_Settings_Manager {
'store_context', 'store_context',
'default_prompt', 'default_prompt',
'max_output_tokens', 'max_output_tokens',
'logs_retention_days',
'product_attribute_includes', 'product_attribute_includes',
'context_fields', 'context_fields',
'modules', 'modules',

View File

@@ -0,0 +1,202 @@
msgid ""
msgstr ""
"Project-Id-Version: SitiAI Product Teksten\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-31 00:00+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Domain: siti-ai-product-content-generator\n"
#: assets/js/settings.js:166
msgid "Deze aanbieder ondersteunt dit niet."
msgstr ""
#: assets/js/settings.js:173
msgid "Vul eerst de API-sleutel in."
msgstr ""
#: assets/js/settings.js:177
msgid "Modellen worden opgehaald…"
msgstr ""
#: assets/js/settings.js:197
msgid "Onbekende fout"
msgstr ""
#: assets/js/settings.js:202
msgid "Modellen bijgewerkt."
msgstr ""
#: assets/js/settings.js:205
msgid "Ophalen mislukt."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:844
msgid "Deze aanbieder ondersteunt dit niet."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:845
msgid "Vul eerst de API-sleutel in."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:846
msgid "Modellen worden opgehaald…"
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:847
msgid "Onbekende fout"
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:848
msgid "Modellen bijgewerkt."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:849
msgid "Ophalen mislukt."
msgstr ""
#: assets/js/term-admin.js:24
msgid "Vul eerst een prompt in."
msgstr ""
#: assets/js/term-admin.js:25
msgid "AI is bezig met schrijven..."
msgstr ""
#: assets/js/term-admin.js:26
msgid "Tekst gegenereerd. Je kunt hem toepassen en opslaan."
msgstr ""
#: assets/js/term-admin.js:27
msgid "Tekst ingevuld. Vergeet niet op \"Opslaan\" te klikken."
msgstr ""
#: assets/js/term-admin.js:28
msgid "Er ging iets mis bij het genereren."
msgstr ""
#: assets/js/term-admin.js:119
msgid "Onbekende fout"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:212
msgid "Onbekende fout"
msgstr ""
#: assets/js/admin.js:100
msgid "AI is bezig met schrijven..."
msgstr ""
#: assets/js/admin.js:101
msgid "Probeer het opnieuw of pas je prompt/context aan."
msgstr ""
#: assets/js/admin.js:102
msgid "Er ging iets mis bij het genereren."
msgstr ""
#: assets/js/admin.js:103
msgid "Onbekende fout."
msgstr ""
#: assets/js/admin.js:104
msgid "Structuur gegenereerd. Kopieer of vul velden in."
msgstr ""
#: assets/js/admin.js:105
msgid "%s ingevuld."
msgstr ""
#: assets/js/admin.js:106
msgid "Kon het veld niet automatisch invullen."
msgstr ""
#: assets/js/admin.js:107
msgid "%s gekopieerd naar het klembord."
msgstr ""
#: assets/js/admin.js:108
msgid "JSON gekopieerd naar het klembord."
msgstr ""
#: assets/js/admin.js:109
msgid "Kopiëren mislukt."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:84
msgid "%s ingevuld."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:85
msgid "Kon het veld niet automatisch invullen."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:86
msgid "%s gekopieerd naar het klembord."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:87
msgid "JSON gekopieerd naar het klembord."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:88
msgid "Kopiëren mislukt."
msgstr ""
#: assets/js/term-bulk.js:149
msgid "Onbekende fout"
msgstr ""
#: assets/js/term-bulk.js:150
msgid "%1$s: %2$s"
msgstr ""
#: assets/js/term-bulk.js:152
msgid "%1$s mislukt: %2$s"
msgstr ""
#: assets/js/term-bulk.js:159
msgid "%1$s gevuld."
msgstr ""
#: assets/js/term-bulk.js:161
msgid "%s is bijgewerkt."
msgstr ""
#: assets/js/term-bulk.js:236
msgid "Stoppen?"
msgstr ""
#: assets/js/term-bulk.js:254
msgid "Onbekende term."
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:101
msgid "Onbekende fout"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:102
msgid "Onbekende term."
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:103
msgid "Stoppen?"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:104
msgid "%1$s: %2$s"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:105
msgid "%1$s gevuld."
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:106
msgid "%1$s mislukt: %2$s"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:107
msgid "%s is bijgewerkt."
msgstr ""

13
phpunit.xml Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="tests/bootstrap.php"
colors="true"
failOnWarning="false"
failOnRisky="false"
>
<testsuites>
<testsuite name="Plugin Tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,17 @@
<?php
use PHPUnit\Framework\TestCase;
class ModelExclusionsTest extends TestCase {
public function test_ensure_allowed_blocks_excluded_model() {
$blocked = Groq_AI_Model_Exclusions::ensure_allowed( 'groq', 'whisper-large-v3' );
$this->assertSame( '', $blocked );
}
public function test_filter_models_removes_excluded_entries() {
$models = [ 'llama3-70b-8192', 'whisper-large-v3', 'mixtral-8x7b-32768' ];
$filtered = Groq_AI_Model_Exclusions::filter_models( 'groq', $models );
$this->assertSame( [ 'llama3-70b-8192', 'mixtral-8x7b-32768' ], $filtered );
}
}

View File

@@ -0,0 +1,102 @@
<?php
use PHPUnit\Framework\TestCase;
class ProviderRequestBuilderTest extends TestCase {
public function test_openai_request_payload_respects_settings() {
$provider = new Groq_AI_Provider_OpenAI();
$result = $provider->generate_content(
[
'prompt' => 'Hallo',
'system_prompt' => 'System',
'model' => 'gpt-4o-mini',
'settings' => [
'openai_api_key' => 'test-key',
'max_output_tokens' => 512,
],
'temperature' => 0.5,
'response_format' => [
'type' => 'json_object',
],
]
);
$this->assertIsArray( $result );
$payload = $result['request_payload']['body'];
$this->assertSame( 'gpt-4o-mini', $payload['model'] );
$this->assertSame( 0.5, $payload['temperature'] );
$this->assertSame( 512, $payload['max_tokens'] );
$this->assertSame( 'json_object', $payload['response_format']['type'] );
$this->assertSame( 'System', $payload['messages'][0]['content'] );
$this->assertSame( 'Hallo', $payload['messages'][1]['content'] );
}
public function test_groq_request_payload_uses_default_model_when_missing() {
$provider = new Groq_AI_Provider_Groq();
$result = $provider->generate_content(
[
'prompt' => 'Hallo',
'system_prompt' => 'System',
'settings' => [
'groq_api_key' => 'test-key',
],
]
);
$this->assertIsArray( $result );
$payload = $result['request_payload']['body'];
$this->assertSame( $provider->get_default_model(), $payload['model'] );
$this->assertSame( 'System', $payload['messages'][0]['content'] );
$this->assertSame( 'Hallo', $payload['messages'][1]['content'] );
}
public function test_google_request_payload_builds_schema_and_images() {
$provider = new Groq_AI_Provider_Google();
$result = $provider->generate_content(
[
'prompt' => 'Hallo',
'system_prompt' => 'System',
'model' => 'gemini-1.5-flash',
'settings' => [
'google_api_key' => 'test-key',
'max_output_tokens' => 256,
'google_safety_settings' => [
'HARM_CATEGORY_HARASSMENT' => 'BLOCK_LOW_AND_ABOVE',
],
],
'temperature' => 0.2,
'response_format' => [
'type' => 'json_schema',
'json_schema' => [
'schema' => [
'type' => 'object',
'properties' => [
'name' => [
'type' => 'string',
],
],
],
],
],
'image_context' => [
[
'label' => 'Image 1',
'mime_type' => 'image/png',
'data' => 'BASE64DATA',
],
],
]
);
$this->assertIsArray( $result );
$payload = $result['request_payload']['body'];
$this->assertSame( 0.2, $payload['generationConfig']['temperature'] );
$this->assertSame( 256, $payload['generationConfig']['maxOutputTokens'] );
$this->assertSame( 'application/json', $payload['generationConfig']['responseMimeType'] );
$this->assertArrayHasKey( 'responseJsonSchema', $payload['generationConfig'] );
$this->assertSame( 'System', $payload['contents'][0]['parts'][0]['text'] );
$this->assertSame( 'Hallo', $payload['contents'][0]['parts'][1]['text'] );
$this->assertSame( 'image/png', $payload['contents'][0]['parts'][3]['inline_data']['mime_type'] );
$this->assertSame( 'BASE64DATA', $payload['contents'][0]['parts'][3]['inline_data']['data'] );
}
}

View File

@@ -0,0 +1,112 @@
<?php
use PHPUnit\Framework\TestCase;
class SettingsManagerTest extends TestCase {
private function make_manager() {
$provider_manager = new Groq_AI_Provider_Manager();
return new Groq_AI_Settings_Manager( 'groq_ai_test_settings', $provider_manager );
}
public function test_logs_retention_days_sanitized_and_capped() {
$manager = $this->make_manager();
$result = $manager->sanitize( [
'logs_retention_days' => 5000,
] );
$this->assertSame( 3650, $result['logs_retention_days'] );
}
public function test_logs_retention_days_allows_zero() {
$manager = $this->make_manager();
$result = $manager->sanitize( [
'logs_retention_days' => 0,
] );
$this->assertSame( 0, $result['logs_retention_days'] );
}
public function test_logs_retention_days_negative_becomes_zero() {
$manager = $this->make_manager();
$result = $manager->sanitize( [
'logs_retention_days' => -5,
] );
$this->assertSame( 0, $result['logs_retention_days'] );
}
public function test_sanitize_accepts_all_settings_keys() {
$manager = $this->make_manager();
$context_fields = $manager->get_default_context_fields();
$modules = $manager->get_default_modules_settings();
$google_categories = Groq_AI_Settings_Manager::get_google_safety_categories_list();
$first_category = array_key_first( $google_categories );
$input = [
'provider' => 'openai',
'model' => 'gpt-4o-mini',
'store_context' => 'Test winkelcontext',
'default_prompt' => 'Schrijf een korte tekst',
'max_output_tokens' => 2048,
'logs_retention_days' => 30,
'product_attribute_includes' => [ '__all__', '__custom__', 'pa_color', 'invalid key' ],
'term_bottom_description_meta_key' => 'custom_bottom_key',
'groq_api_key' => 'groq-key',
'openai_api_key' => 'openai-key',
'google_api_key' => 'google-key',
'google_oauth_client_id' => 'client-id',
'google_oauth_client_secret' => 'client-secret',
'google_oauth_refresh_token' => 'refresh-token',
'google_oauth_connected_email' => 'user@example.com',
'google_oauth_connected_at' => 123456,
'google_enable_gsc' => true,
'google_enable_ga' => false,
'google_gsc_site_url' => 'https://example.com/',
'google_ga4_property_id' => '123456',
'google_safety_settings' => $first_category ? [ $first_category => 'BLOCK_LOW_AND_ABOVE' ] : [],
'context_fields' => $context_fields,
'modules' => $modules,
'image_context_mode' => 'base64',
'image_context_limit' => 5,
'response_format_compat' => true,
'term_top_description_char_limit' => 700,
'term_bottom_description_char_limit' => 1400,
];
$result = $manager->sanitize( $input );
$this->assertSame( 'openai', $result['provider'] );
$this->assertSame( 'gpt-4o-mini', $result['model'] );
$this->assertSame( 'Test winkelcontext', $result['store_context'] );
$this->assertSame( 'Schrijf een korte tekst', $result['default_prompt'] );
$this->assertSame( 2048, $result['max_output_tokens'] );
$this->assertSame( 30, $result['logs_retention_days'] );
$this->assertContains( '__all__', $result['product_attribute_includes'] );
$this->assertContains( '__custom__', $result['product_attribute_includes'] );
$this->assertContains( 'pa_color', $result['product_attribute_includes'] );
$this->assertSame( 'custom_bottom_key', $result['term_bottom_description_meta_key'] );
$this->assertSame( 'groq-key', $result['groq_api_key'] );
$this->assertSame( 'openai-key', $result['openai_api_key'] );
$this->assertSame( 'google-key', $result['google_api_key'] );
$this->assertSame( 'client-id', $result['google_oauth_client_id'] );
$this->assertSame( 'client-secret', $result['google_oauth_client_secret'] );
$this->assertSame( 'refresh-token', $result['google_oauth_refresh_token'] );
$this->assertSame( 'user@example.com', $result['google_oauth_connected_email'] );
$this->assertSame( 123456, $result['google_oauth_connected_at'] );
$this->assertTrue( $result['google_enable_gsc'] );
$this->assertFalse( $result['google_enable_ga'] );
$this->assertSame( 'https://example.com/', $result['google_gsc_site_url'] );
$this->assertSame( '123456', $result['google_ga4_property_id'] );
$this->assertIsArray( $result['google_safety_settings'] );
$this->assertIsArray( $result['context_fields'] );
$this->assertIsArray( $result['modules'] );
$this->assertSame( 'base64', $result['image_context_mode'] );
$this->assertSame( 5, $result['image_context_limit'] );
$this->assertTrue( $result['response_format_compat'] );
$this->assertSame( 700, $result['term_top_description_char_limit'] );
$this->assertSame( 1400, $result['term_bottom_description_char_limit'] );
}
}

67
tests/TermSaveTest.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
use PHPUnit\Framework\TestCase;
class TermSaveTest extends TestCase {
protected function setUp(): void {
$GLOBALS['wp_term_updates'] = [];
$GLOBALS['wp_term_meta_updates'] = [];
$GLOBALS['wp_filters'] = [];
}
public function test_save_term_generation_result_saves_descriptions_and_filtered_meta_key() {
$plugin = new class {
public function get_settings() {
return [ 'term_bottom_description_meta_key' => '' ];
}
public function is_module_enabled( $module, $settings = null ) {
return false;
}
};
$controller_ref = new ReflectionClass( Groq_AI_Ajax_Controller::class );
$controller = $controller_ref->newInstanceWithoutConstructor();
$plugin_prop = $controller_ref->getProperty( 'plugin' );
$plugin_prop->setAccessible( true );
$plugin_prop->setValue( $controller, $plugin );
add_filter(
'groq_ai_term_bottom_description_meta_key',
function ( $default_key ) {
return 'Custom Key';
},
10,
3
);
$term = (object) [
'term_id' => 12,
'taxonomy' => 'product_cat',
'name' => 'Test',
'description' => '',
];
$result = [
'top_description' => '<p>Dit is een test.</p>',
'bottom_description' => '<p>Onderste tekst.</p>',
];
$settings = $plugin->get_settings();
$method = $controller_ref->getMethod( 'save_term_generation_result' );
$method->setAccessible( true );
$saved = $method->invoke( $controller, $term, $result, $settings );
$this->assertIsArray( $saved );
$this->assertSame( 4, $saved['words'] );
$this->assertCount( 1, $GLOBALS['wp_term_updates'] );
$this->assertSame( 12, $GLOBALS['wp_term_updates'][0]['term_id'] );
$this->assertSame( 'product_cat', $GLOBALS['wp_term_updates'][0]['taxonomy'] );
$this->assertSame( '<p>Dit is een test.</p>', $GLOBALS['wp_term_updates'][0]['args']['description'] );
$this->assertArrayHasKey( 12, $GLOBALS['wp_term_meta_updates'] );
$this->assertArrayHasKey( 'customkey', $GLOBALS['wp_term_meta_updates'][12] );
$this->assertSame( '<p>Onderste tekst.</p>', $GLOBALS['wp_term_meta_updates'][12]['customkey'] );
}
}

258
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,258 @@
<?php
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_DOMAIN' ) ) {
define( 'GROQ_AI_PRODUCT_TEXT_DOMAIN', 'siti-ai-product-content-generator' );
}
if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
define( 'HOUR_IN_SECONDS', 3600 );
}
if ( ! defined( 'DAY_IN_SECONDS' ) ) {
define( 'DAY_IN_SECONDS', 86400 );
}
if ( ! class_exists( 'WP_Error' ) ) {
class WP_Error {
private $message;
public function __construct( $code = '', $message = '' ) {
$this->message = (string) $message;
}
public function get_error_message() {
return $this->message;
}
}
}
if ( ! function_exists( 'is_wp_error' ) ) {
function is_wp_error( $thing ) {
return $thing instanceof WP_Error;
}
}
if ( ! function_exists( '__' ) ) {
function __( $text ) {
return $text;
}
}
if ( ! function_exists( 'wp_parse_args' ) ) {
function wp_parse_args( $args, $defaults = [] ) {
if ( is_object( $args ) ) {
$args = get_object_vars( $args );
}
if ( ! is_array( $args ) ) {
$args = [];
}
return array_merge( $defaults, $args );
}
}
if ( ! function_exists( 'sanitize_text_field' ) ) {
function sanitize_text_field( $text ) {
return trim( (string) $text );
}
}
if ( ! function_exists( 'sanitize_textarea_field' ) ) {
function sanitize_textarea_field( $text ) {
return trim( (string) $text );
}
}
if ( ! function_exists( 'sanitize_key' ) ) {
function sanitize_key( $key ) {
$key = strtolower( (string) $key );
return preg_replace( '/[^a-z0-9_\-]/', '', $key );
}
}
if ( ! function_exists( 'absint' ) ) {
function absint( $number ) {
return abs( (int) $number );
}
}
if ( ! function_exists( 'esc_url_raw' ) ) {
function esc_url_raw( $url ) {
return (string) $url;
}
}
if ( ! function_exists( 'add_filter' ) ) {
function add_filter( $tag, $callback, $priority = 10, $accepted_args = 1 ) {
$GLOBALS['wp_filters'][ $tag ][ $priority ][] = [
'callback' => $callback,
'accepted_args' => (int) $accepted_args,
];
}
}
if ( ! function_exists( 'apply_filters' ) ) {
function apply_filters( $tag, $value ) {
$args = func_get_args();
if ( empty( $GLOBALS['wp_filters'][ $tag ] ) ) {
return $value;
}
ksort( $GLOBALS['wp_filters'][ $tag ] );
foreach ( $GLOBALS['wp_filters'][ $tag ] as $callbacks ) {
foreach ( $callbacks as $filter ) {
$accepted = isset( $filter['accepted_args'] ) ? (int) $filter['accepted_args'] : 1;
$call_args = array_slice( $args, 0, max( 1, $accepted ) );
$call_args[0] = $value;
$value = call_user_func_array( $filter['callback'], $call_args );
$args[0] = $value;
}
}
return $value;
}
}
if ( ! function_exists( 'wp_kses_post' ) ) {
function wp_kses_post( $content ) {
return (string) $content;
}
}
if ( ! function_exists( 'wp_strip_all_tags' ) ) {
function wp_strip_all_tags( $text ) {
return strip_tags( (string) $text );
}
}
if ( ! function_exists( 'wp_update_term' ) ) {
function wp_update_term( $term_id, $taxonomy, $args = [] ) {
$GLOBALS['wp_term_updates'][] = [
'term_id' => (int) $term_id,
'taxonomy' => (string) $taxonomy,
'args' => $args,
];
return [ 'term_id' => (int) $term_id ];
}
}
if ( ! function_exists( 'update_term_meta' ) ) {
function update_term_meta( $term_id, $meta_key, $meta_value ) {
$term_id = (int) $term_id;
if ( ! isset( $GLOBALS['wp_term_meta_updates'][ $term_id ] ) ) {
$GLOBALS['wp_term_meta_updates'][ $term_id ] = [];
}
$GLOBALS['wp_term_meta_updates'][ $term_id ][ (string) $meta_key ] = $meta_value;
return true;
}
}
if ( ! function_exists( 'wp_json_encode' ) ) {
function wp_json_encode( $data, $options = 0, $depth = 512 ) {
return json_encode( $data, $options, $depth );
}
}
if ( ! function_exists( 'add_query_arg' ) ) {
function add_query_arg( $args, $url = '' ) {
if ( is_string( $args ) ) {
return $url;
}
$query = http_build_query( (array) $args );
$separator = strpos( $url, '?' ) === false ? '?' : '&';
return $url . $separator . $query;
}
}
if ( ! function_exists( 'wp_remote_post' ) ) {
function wp_remote_post( $url, $args = [] ) {
$GLOBALS['wp_last_http_request'] = [
'url' => $url,
'args' => $args,
];
$body = json_encode(
[
'choices' => [
[
'message' => [
'content' => 'ok',
],
'finish_reason' => 'stop',
],
],
'usage' => [
'prompt_tokens' => 10,
'completion_tokens' => 20,
'total_tokens' => 30,
],
'candidates' => [
[
'content' => [
'parts' => [
[ 'text' => 'ok' ],
],
],
'finishReason' => 'STOP',
],
],
'usageMetadata' => [
'promptTokenCount' => 10,
'candidatesTokenCount' => 20,
'totalTokenCount' => 30,
],
]
);
return [
'body' => $body,
'response' => [ 'code' => 200 ],
];
}
}
if ( ! function_exists( 'wp_remote_get' ) ) {
function wp_remote_get( $url, $args = [] ) {
$GLOBALS['wp_last_http_request'] = [
'url' => $url,
'args' => $args,
];
return [
'body' => json_encode( [ 'data' => [], 'models' => [] ] ),
'response' => [ 'code' => 200 ],
];
}
}
if ( ! function_exists( 'wp_remote_retrieve_body' ) ) {
function wp_remote_retrieve_body( $response ) {
return isset( $response['body'] ) ? $response['body'] : '';
}
}
if ( ! function_exists( 'wp_remote_retrieve_response_code' ) ) {
function wp_remote_retrieve_response_code( $response ) {
return isset( $response['response']['code'] ) ? (int) $response['response']['code'] : 0;
}
}
if ( ! function_exists( 'get_option' ) ) {
function get_option( $key, $default = false ) {
return isset( $GLOBALS['wp_options'][ $key ] ) ? $GLOBALS['wp_options'][ $key ] : $default;
}
}
if ( ! function_exists( 'update_option' ) ) {
function update_option( $key, $value ) {
$GLOBALS['wp_options'][ $key ] = $value;
return true;
}
}
require_once __DIR__ . '/../includes/Core/class-groq-ai-model-exclusions.php';
require_once __DIR__ . '/../includes/Contracts/interface-groq-ai-provider.php';
require_once __DIR__ . '/../includes/Providers/class-groq-ai-abstract-openai-provider.php';
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-groq.php';
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-openai.php';
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-google.php';
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-manager.php';
require_once __DIR__ . '/../includes/Services/Settings/class-groq-ai-settings-manager.php';
require_once __DIR__ . '/../includes/Core/class-groq-ai-ajax-controller.php';