2 Commits

Author SHA1 Message Date
95f7983e70 Add Google Analytics and Search Console integration
- Implemented Groq_AI_Google_Analytics_Data_Client for fetching GA4 data.
- Created Groq_AI_Google_Search_Console_Client for retrieving Search Console data.
- Added Google OAuth client for authentication with Google APIs.
- Enhanced Groq_AI_Settings_Manager to include Google OAuth settings.
- Introduced term context building methods in Groq_AI_Google_Context_Builder.
- Developed JavaScript functionality for term generation in the admin interface.
- Added methods for generating term prompts and handling responses.
- Improved error handling and response parsing for Google API interactions.
2026-01-16 17:48:34 +00:00
985f7dfbcd Refactor localization strings to use constant for text domain; add image context limit feature
- Updated localization strings in various classes to use the constant `GROQ_AI_PRODUCT_TEXT_DOMAIN` instead of hardcoded text domain.
- Introduced an image context limit setting in the settings manager and adjusted related methods to accommodate this new feature.
- Modified prompt builder to handle image context limit when building product context blocks and retrieving image payloads.
- Enhanced error handling and response structures to include new fields related to image context.
- Added support for title suggestions in the structured response from the AI.
2025-12-19 16:10:57 +00:00
20 changed files with 2604 additions and 203 deletions

View File

@@ -123,6 +123,36 @@
width: 100%; width: 100%;
} }
.groq-ai-title-suggestions {
border: 1px dashed #dcdcde;
border-radius: 4px;
padding: 8px;
background: #fefefe;
margin-top: 8px;
}
.groq-ai-title-suggestions__options {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 6px;
}
.groq-ai-title-suggestions__option {
display: flex;
align-items: flex-start;
gap: 8px;
}
.groq-ai-title-suggestions__option input[type='radio'] {
margin-top: 3px;
}
.groq-ai-title-suggestions__hint {
margin-top: 6px;
margin-bottom: 0;
}
.groq-ai-modal__raw { .groq-ai-modal__raw {
margin-top: 16px; margin-top: 16px;
} }

View File

@@ -28,6 +28,8 @@
rankMathAction: field.getAttribute('data-rankmath-action') || '', rankMathAction: field.getAttribute('data-rankmath-action') || '',
status: field.querySelector('.groq-ai-apply-status') || null, status: field.querySelector('.groq-ai-apply-status') || null,
statusTimer: null, statusTimer: null,
suggestionWrapper: field.querySelector('[data-title-suggestions]') || null,
suggestionOptions: field.querySelector('[data-title-suggestions-options]') || null,
}; };
}); });
@@ -92,8 +94,8 @@
statusField.setAttribute('data-status', type); statusField.setAttribute('data-status', type);
} }
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'groq-ai-product-text') : 'AI is bezig met schrijven...'; const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...';
const retryText = window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'groq-ai-product-text') : 'Probeer het opnieuw of pas je prompt/context aan.'; 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.';
function toggleLoading(isLoading) { function toggleLoading(isLoading) {
modal.classList.toggle('is-loading', isLoading); modal.classList.toggle('is-loading', isLoading);
@@ -120,6 +122,7 @@
jsonCopyButton.disabled = true; jsonCopyButton.disabled = true;
} }
resetFieldStatuses(); resetFieldStatuses();
clearTitleSuggestions();
fetch(GroqAIGenerator.ajaxUrl, { fetch(GroqAIGenerator.ajaxUrl, {
method: 'POST', method: 'POST',
@@ -142,6 +145,7 @@
entry.textarea.value = fields[key] || ''; entry.textarea.value = fields[key] || '';
} }
}); });
updateTitleSuggestions(fields.title_suggestions);
resultField.textContent = (json.data.raw || '').trim(); resultField.textContent = (json.data.raw || '').trim();
resultWrapper.hidden = false; resultWrapper.hidden = false;
if (jsonCopyButton) { if (jsonCopyButton) {
@@ -317,6 +321,8 @@
return ['textarea[name="rank_math_description"]']; return ['textarea[name="rank_math_description"]'];
case 'focus_keywords': case 'focus_keywords':
return ['input[name="rank_math_focus_keyword"]']; return ['input[name="rank_math_focus_keyword"]'];
case 'slug':
return ['#post_name', 'input[name="post_name"]', '#new-post-slug'];
default: default:
return []; return [];
} }
@@ -357,6 +363,89 @@
}); });
} }
function clearTitleSuggestions() {
const entry = resultFields.title;
if (!entry || !entry.suggestionWrapper || !entry.suggestionOptions) {
return;
}
entry.suggestionOptions.innerHTML = '';
entry.suggestionWrapper.hidden = true;
}
function updateTitleSuggestions(options) {
const entry = resultFields.title;
if (!entry || !entry.suggestionWrapper || !entry.suggestionOptions) {
return;
}
entry.suggestionOptions.innerHTML = '';
const sanitized = Array.isArray(options)
? options
.map((option) => (typeof option === 'string' ? option.trim() : ''))
.filter((option) => option.length > 0)
.slice(0, 3)
: [];
if (!sanitized.length) {
entry.suggestionWrapper.hidden = true;
return;
}
entry.suggestionWrapper.hidden = false;
const currentValue = entry.textarea ? entry.textarea.value.trim() : '';
const normalizedCurrent = currentValue.toLowerCase();
let selectedValue = '';
if (normalizedCurrent) {
const matched = sanitized.find((text) => text.toLowerCase() === normalizedCurrent);
if (matched) {
selectedValue = matched;
if (entry.textarea) {
entry.textarea.value = matched;
}
}
}
if (!selectedValue) {
selectedValue = sanitized[0];
if (entry.textarea) {
entry.textarea.value = sanitized[0];
}
}
const groupName = `groq-ai-title-option-${Date.now()}`;
sanitized.forEach((text, index) => {
const optionId = `${groupName}-${index}`;
const optionWrapper = document.createElement('label');
optionWrapper.className = 'groq-ai-title-suggestions__option';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = groupName;
radio.id = optionId;
radio.value = text;
if (text === selectedValue) {
radio.checked = true;
}
radio.addEventListener('change', () => {
if (entry.textarea) {
entry.textarea.value = text;
}
});
const textSpan = document.createElement('span');
textSpan.textContent = text;
optionWrapper.appendChild(radio);
optionWrapper.appendChild(textSpan);
entry.suggestionOptions.appendChild(optionWrapper);
});
}
function resetContextToggles() { function resetContextToggles() {
const defaults = GroqAIGenerator.contextDefaults || {}; const defaults = GroqAIGenerator.contextDefaults || {};
contextToggles.forEach((toggle) => { contextToggles.forEach((toggle) => {

102
assets/js/term-admin.js Normal file
View File

@@ -0,0 +1,102 @@
(function () {
if (!window.GroqAITermGenerator) {
return;
}
const form = document.getElementById('groq-ai-term-form');
if (!form) {
return;
}
const promptField = document.getElementById('groq-ai-term-prompt');
const outputField = document.getElementById('groq-ai-term-generated');
const rawField = document.getElementById('groq-ai-term-raw');
const statusField = document.getElementById('groq-ai-term-status');
const applyButton = document.getElementById('groq-ai-term-apply');
const includeTopProducts = document.getElementById('groq-ai-term-include-top-products');
const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit');
function setStatus(message, type) {
if (!statusField) {
return;
}
statusField.textContent = message || '';
statusField.setAttribute('data-status', type || '');
}
function setLoading(isLoading) {
form.classList.toggle('is-loading', !!isLoading);
const buttons = form.querySelectorAll('button, input[type="submit"]');
buttons.forEach((btn) => {
btn.disabled = !!isLoading;
});
}
function buildPayload(prompt) {
const payload = new URLSearchParams();
payload.append('action', 'groq_ai_generate_term_text');
payload.append('nonce', GroqAITermGenerator.nonce);
payload.append('taxonomy', GroqAITermGenerator.taxonomy);
payload.append('term_id', GroqAITermGenerator.termId);
payload.append('prompt', prompt);
payload.append('include_top_products', includeTopProducts && includeTopProducts.checked ? '1' : '0');
payload.append('top_products_limit', topProductsLimit ? String(topProductsLimit.value || '') : '10');
return payload;
}
if (applyButton) {
applyButton.addEventListener('click', () => {
const descriptionField = document.getElementById('description');
if (!descriptionField || !outputField) {
return;
}
descriptionField.value = outputField.value || '';
setStatus('Tekst ingevuld in het beschrijving-veld. Vergeet niet op "Opslaan" te klikken.', 'success');
});
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const prompt = promptField ? (promptField.value || '').trim() : '';
if (!prompt) {
setStatus('Vul eerst een prompt in.', 'error');
return;
}
setLoading(true);
setStatus('AI is bezig met schrijven...', 'loading');
if (rawField) {
rawField.textContent = '';
}
fetch(GroqAITermGenerator.ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: buildPayload(prompt).toString(),
})
.then((response) => response.json())
.then((json) => {
if (!json.success) {
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout';
throw new Error(errorMessage);
}
if (outputField) {
outputField.value = (json.data && json.data.description ? json.data.description : '').trim();
}
if (rawField) {
rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();
}
setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success');
})
.catch((error) => {
setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error');
})
.finally(() => {
setLoading(false);
});
});
})();

View File

@@ -26,7 +26,7 @@ services:
WORDPRESS_DB_USER: wordpress WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress WORDPRESS_DB_NAME: wordpress
WORDPRESS_DEBUG: 1 WORDPRESS_DEBUG: 0
WP_ENVIRONMENT_TYPE: local WP_ENVIRONMENT_TYPE: local
volumes: volumes:
- wordpress_data:/var/www/html - wordpress_data:/var/www/html

View File

@@ -2,8 +2,10 @@
/** /**
* 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.2.2 * Version: 1.4.0
* Author: SitiAI * Author: SitiAI
* Text Domain: siti-ai-product-content-generator
* Domain Path: /languages
*/ */
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
@@ -27,6 +29,14 @@ if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_VERSION' ) ) {
define( 'GROQ_AI_PRODUCT_TEXT_VERSION', $groq_ai_version ); define( 'GROQ_AI_PRODUCT_TEXT_VERSION', $groq_ai_version );
} }
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_DOMAIN' ) ) {
define( 'GROQ_AI_PRODUCT_TEXT_DOMAIN', 'siti-ai-product-content-generator' );
}
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN' ) ) {
define( 'GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN', 'groq-ai-product-text' );
}
if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) { if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
define( 'GROQ_AI_DEBUG_TRACE_ADDED', true ); define( 'GROQ_AI_DEBUG_TRACE_ADDED', true );
} }
@@ -44,6 +54,10 @@ require_once __DIR__ . '/includes/Services/Settings/class-groq-ai-settings-manag
require_once __DIR__ . '/includes/Services/Prompt/class-groq-ai-prompt-builder.php'; require_once __DIR__ . '/includes/Services/Prompt/class-groq-ai-prompt-builder.php';
require_once __DIR__ . '/includes/Services/Conversations/class-groq-ai-conversation-manager.php'; require_once __DIR__ . '/includes/Services/Conversations/class-groq-ai-conversation-manager.php';
require_once __DIR__ . '/includes/Services/Logging/class-groq-ai-generation-logger.php'; require_once __DIR__ . '/includes/Services/Logging/class-groq-ai-generation-logger.php';
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-oauth-client.php';
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-search-console-client.php';
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-analytics-data-client.php';
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-context-builder.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php';
@@ -63,6 +77,9 @@ final class Groq_AI_Product_Text_Plugin {
const CONVERSATION_OPTION_KEY = 'groq_ai_product_text_conversations'; const CONVERSATION_OPTION_KEY = 'groq_ai_product_text_conversations';
const MODELS_CACHE_OPTION_KEY = 'groq_ai_product_text_models'; const MODELS_CACHE_OPTION_KEY = 'groq_ai_product_text_models';
/** @var bool */
private $textdomain_loaded = false;
private static $instance = null; private static $instance = null;
/** @var Groq_AI_Service_Container */ /** @var Groq_AI_Service_Container */
@@ -91,8 +108,35 @@ final class Groq_AI_Product_Text_Plugin {
$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->product_ui = new Groq_AI_Product_Text_Product_UI( $this ); $this->product_ui = new Groq_AI_Product_Text_Product_UI( $this );
add_action( 'plugins_loaded', [ $this, 'maybe_load_textdomain_early' ], 0 );
add_action( 'init', [ $this, 'load_textdomain' ] );
add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] ); add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] );
add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] ); add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] );
add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 );
}
public function load_textdomain() {
if ( $this->textdomain_loaded ) {
return;
}
$relative_path = dirname( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) ) . '/languages';
load_plugin_textdomain( GROQ_AI_PRODUCT_TEXT_DOMAIN, false, $relative_path );
if ( defined( 'GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN' ) && GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN !== GROQ_AI_PRODUCT_TEXT_DOMAIN ) {
load_plugin_textdomain( GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN, false, $relative_path );
}
$this->textdomain_loaded = true;
}
public function maybe_load_textdomain_early() {
if ( did_action( 'init' ) ) {
return;
}
$this->load_textdomain();
} }
private function register_services() { private function register_services() {
@@ -140,10 +184,47 @@ final class Groq_AI_Product_Text_Plugin {
} }
); );
$this->container->set(
'google_oauth_client',
function () {
return new Groq_AI_Google_OAuth_Client();
}
);
$this->container->set(
'gsc_client',
function ( Groq_AI_Service_Container $container ) {
return new Groq_AI_Google_Search_Console_Client( $container->get( 'google_oauth_client' ) );
}
);
$this->container->set(
'ga_client',
function ( Groq_AI_Service_Container $container ) {
return new Groq_AI_Google_Analytics_Data_Client( $container->get( 'google_oauth_client' ) );
}
);
$this->container->set(
'google_context_builder',
function ( Groq_AI_Service_Container $container ) {
return new Groq_AI_Google_Context_Builder( $container->get( 'gsc_client' ), $container->get( 'ga_client' ) );
}
);
// Instantiate controller immediately so hooks are registered. // Instantiate controller immediately so hooks are registered.
$this->container->get( 'ajax_controller' ); $this->container->get( 'ajax_controller' );
} }
public function inject_google_term_context( $existing, $term, $settings ) {
$builder = $this->container->get( 'google_context_builder' );
if ( ! $builder ) {
return (string) $existing;
}
return $builder->build_term_google_context( $existing, $term, $settings );
}
public function get_option_key() { public function get_option_key() {
return self::OPTION_KEY; return self::OPTION_KEY;
} }
@@ -198,7 +279,7 @@ final class Groq_AI_Product_Text_Plugin {
?> ?>
<div class="notice notice-error"> <div class="notice notice-error">
<p> <p>
<?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', 'groq-ai-product-text' ); ?> <?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p> </p>
</div> </div>
<?php <?php
@@ -208,15 +289,15 @@ final class Groq_AI_Product_Text_Plugin {
$parts = []; $parts = [];
if ( ! empty( $settings['store_context'] ) ) { if ( ! empty( $settings['store_context'] ) ) {
$parts[] = sprintf( __( 'Winkelcontext: %s', 'groq-ai-product-text' ), $settings['store_context'] ); $parts[] = sprintf( __( 'Winkelcontext: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $settings['store_context'] );
} }
if ( ! empty( $settings['default_prompt'] ) ) { if ( ! empty( $settings['default_prompt'] ) ) {
$parts[] = sprintf( __( 'Standaard prompt: %s', 'groq-ai-product-text' ), $settings['default_prompt'] ); $parts[] = sprintf( __( 'Standaard prompt: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $settings['default_prompt'] );
} }
if ( empty( $parts ) ) { if ( empty( $parts ) ) {
return __( 'Nog geen promptinformatie opgeslagen.', 'groq-ai-product-text' ); return __( 'Nog geen promptinformatie opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} }
return implode( "\n\n", $parts ); return implode( "\n\n", $parts );
@@ -266,6 +347,10 @@ final class Groq_AI_Product_Text_Plugin {
return $this->get_settings_manager()->get_image_context_mode( $settings ); 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 should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) { public function should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) {
return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format(); return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
} }

View File

@@ -30,13 +30,13 @@ class Groq_AI_Logs_Table extends WP_List_Table {
public function get_columns() { public function get_columns() {
return [ return [
'created_at' => __( 'Datum', 'groq-ai-product-text' ), 'created_at' => __( 'Datum', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'user_id' => __( 'Gebruiker', 'groq-ai-product-text' ), 'user_id' => __( 'Gebruiker', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'post_title' => __( 'Product', 'groq-ai-product-text' ), 'post_title' => __( 'Product', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'provider' => __( 'Provider', 'groq-ai-product-text' ), 'provider' => __( 'Provider', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'model' => __( 'Model', 'groq-ai-product-text' ), 'model' => __( 'Model', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'status' => __( 'Status', 'groq-ai-product-text' ), 'status' => __( 'Status', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'tokens_total' => __( 'Tokens', 'groq-ai-product-text' ), 'tokens_total' => __( 'Tokens', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
]; ];
} }
@@ -114,7 +114,7 @@ class Groq_AI_Logs_Table extends WP_List_Table {
if ( ! $item['post_id'] ) { if ( ! $item['post_id'] ) {
return '—'; return '—';
} }
$title = $item['post_title'] ? $item['post_title'] : sprintf( __( 'Product #%d', 'groq-ai-product-text' ), (int) $item['post_id'] ); $title = $item['post_title'] ? $item['post_title'] : sprintf( __( 'Product #%d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (int) $item['post_id'] );
$link = get_edit_post_link( $item['post_id'] ); $link = get_edit_post_link( $item['post_id'] );
return $link ? sprintf( '<a href="%s">%s</a>', esc_url( $link ), esc_html( $title ) ) : esc_html( $title ); return $link ? sprintf( '<a href="%s">%s</a>', esc_url( $link ), esc_html( $title ) ) : esc_html( $title );
case 'user_id': case 'user_id':
@@ -131,7 +131,7 @@ class Groq_AI_Logs_Table extends WP_List_Table {
} }
public function no_items() { public function no_items() {
esc_html_e( 'Nog geen AI-logboeken gevonden.', 'groq-ai-product-text' ); esc_html_e( 'Nog geen AI-logboeken gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} }
protected function column_created_at( $item ) { protected function column_created_at( $item ) {

View File

@@ -14,7 +14,7 @@ class Groq_AI_Product_Text_Product_UI {
public function register_meta_box() { public function register_meta_box() {
add_meta_box( add_meta_box(
'groq-ai-generator-box', 'groq-ai-generator-box',
__( 'Gebruik AI', 'groq-ai-product-text' ), __( 'Gebruik AI', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_meta_box' ], [ $this, 'render_meta_box' ],
'product', 'product',
'side', 'side',
@@ -24,14 +24,14 @@ class Groq_AI_Product_Text_Product_UI {
public function render_meta_box() { public function render_meta_box() {
if ( ! current_user_can( 'edit_products' ) ) { if ( ! current_user_can( 'edit_products' ) ) {
echo '<p>' . esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', 'groq-ai-product-text' ) . '</p>'; echo '<p>' . esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) . '</p>';
return; return;
} }
?> ?>
<p><?php esc_html_e( 'Laat de geselecteerde AI een concepttekst genereren op basis van een prompt.', 'groq-ai-product-text' ); ?></p> <p><?php esc_html_e( 'Laat de geselecteerde AI een concepttekst genereren op basis van een prompt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<button type="button" class="button button-primary groq-ai-open-modal" data-target="groq-ai-modal"><?php esc_html_e( 'Gebruik AI', 'groq-ai-product-text' ); ?></button> <button type="button" class="button button-primary groq-ai-open-modal" data-target="groq-ai-modal"><?php esc_html_e( 'Gebruik AI', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<p class="description" style="margin-top:8px;"> <p class="description" style="margin-top:8px;">
<?php esc_html_e( 'Klik om een prompt in te voeren en een voorsteltekst te genereren. Plak het resultaat in de beschrijving of korte beschrijving.', 'groq-ai-product-text' ); ?> <?php esc_html_e( 'Klik om een prompt in te voeren en een voorsteltekst te genereren. Plak het resultaat in de beschrijving of korte beschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p> </p>
<?php <?php
} }
@@ -85,25 +85,25 @@ class Groq_AI_Product_Text_Product_UI {
?> ?>
<div id="groq-ai-modal" class="groq-ai-modal" aria-hidden="true"> <div id="groq-ai-modal" class="groq-ai-modal" aria-hidden="true">
<div class="groq-ai-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="groq-ai-modal-title"> <div class="groq-ai-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="groq-ai-modal-title">
<button type="button" class="groq-ai-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', 'groq-ai-product-text' ); ?>">&times;</button> <button type="button" class="groq-ai-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">&times;</button>
<div class="groq-ai-modal__dialog-inner"> <div class="groq-ai-modal__dialog-inner">
<h2 id="groq-ai-modal-title"><?php esc_html_e( 'Siti AI prompt', 'groq-ai-product-text' ); ?></h2> <h2 id="groq-ai-modal-title"><?php esc_html_e( 'Siti AI prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<form id="groq-ai-form"> <form id="groq-ai-form">
<label for="groq-ai-prompt" class="screen-reader-text"><?php esc_html_e( 'Prompt', 'groq-ai-product-text' ); ?></label> <label for="groq-ai-prompt" class="screen-reader-text"><?php esc_html_e( 'Prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label>
<textarea id="groq-ai-prompt" rows="6" placeholder="<?php esc_attr_e( 'Beschrijf hier wat de AI moet schrijven...', 'groq-ai-product-text' ); ?>"></textarea> <textarea id="groq-ai-prompt" rows="6" placeholder="<?php esc_attr_e( 'Beschrijf hier wat de AI moet schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>"></textarea>
<div class="groq-ai-modal__actions"> <div class="groq-ai-modal__actions">
<button type="submit" class="button button-primary"> <button type="submit" class="button button-primary">
<?php esc_html_e( 'Genereer tekst', 'groq-ai-product-text' ); ?> <?php esc_html_e( 'Genereer tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</button> </button>
</div> </div>
<div class="groq-ai-advanced-settings"> <div class="groq-ai-advanced-settings">
<button type="button" class="groq-ai-advanced-toggle" aria-expanded="false" aria-controls="groq-ai-advanced-panel"> <button type="button" class="groq-ai-advanced-toggle" aria-expanded="false" aria-controls="groq-ai-advanced-panel">
<span class="groq-ai-advanced-toggle__icon" aria-hidden="true"></span> <span class="groq-ai-advanced-toggle__icon" aria-hidden="true"></span>
<?php esc_html_e( 'Geavanceerde instellingen', 'groq-ai-product-text' ); ?> <?php esc_html_e( 'Geavanceerde instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</button> </button>
<div id="groq-ai-advanced-panel" class="groq-ai-context-options" hidden> <div id="groq-ai-advanced-panel" class="groq-ai-context-options" hidden>
<h3><?php esc_html_e( 'Gebruik deze productinformatie in de prompt', 'groq-ai-product-text' ); ?></h3> <h3><?php esc_html_e( 'Gebruik deze productinformatie in de prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<p class="description"><?php esc_html_e( 'Je kunt tijdelijk onderdelen uitzetten of weer inschakelen. Standaard zijn de opties aangevinkt zoals ingesteld op de instellingenpagina.', 'groq-ai-product-text' ); ?></p> <p class="description"><?php esc_html_e( 'Je kunt tijdelijk onderdelen uitzetten of weer inschakelen. Standaard zijn de opties aangevinkt zoals ingesteld op de instellingenpagina.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<div class="groq-ai-context-options__grid"> <div class="groq-ai-context-options__grid">
<?php <?php
$context_definitions = $this->plugin->get_context_field_definitions(); $context_definitions = $this->plugin->get_context_field_definitions();
@@ -126,81 +126,97 @@ class Groq_AI_Product_Text_Product_UI {
</div> </div>
</form> </form>
<div class="groq-ai-modal__result" hidden> <div class="groq-ai-modal__result" hidden>
<h3><?php esc_html_e( 'Resultaat', 'groq-ai-product-text' ); ?></h3> <h3><?php esc_html_e( 'Resultaat', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<div class="groq-ai-result-grid"> <div class="groq-ai-result-grid">
<div class="groq-ai-result-field" data-field="title" data-target-input="#title" data-label="<?php esc_attr_e( 'Producttitel', 'groq-ai-product-text' ); ?>"> <div class="groq-ai-result-field" data-field="title" data-target-input="#title" data-label="<?php esc_attr_e( 'Producttitel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Producttitel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="title"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="title"><?php esc_html_e( 'Vul titel in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<div class="groq-ai-title-suggestions" data-title-suggestions hidden>
<p class="groq-ai-title-suggestions__label"><?php esc_html_e( 'Kies je favoriete titelvoorstel:', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<div class="groq-ai-title-suggestions__options" data-title-suggestions-options></div>
<p class="description groq-ai-title-suggestions__hint"><?php esc_html_e( 'Je kunt de tekst hieronder altijd nog aanpassen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</div>
<textarea rows="2"></textarea>
</div>
<div class="groq-ai-result-field" data-field="slug" data-target-input="#slug" data-label="<?php esc_attr_e( 'Productslug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header"> <div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Producttitel', 'groq-ai-product-text' ); ?></strong> <strong><?php esc_html_e( 'Productslug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<div class="groq-ai-result-field__actions"> <div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="title"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button> <button type="button" class="button button-secondary groq-ai-copy-field" data-field="slug"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="title"><?php esc_html_e( 'Vul titel in', 'groq-ai-product-text' ); ?></button> <button type="button" class="button groq-ai-apply-field" data-field="slug"><?php esc_html_e( 'Vul slug in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span> <span class="groq-ai-apply-status" aria-hidden="true"></span>
</div> </div>
</div> </div>
<textarea rows="2"></textarea> <textarea rows="1"></textarea>
</div> </div>
<div class="groq-ai-result-field" data-field="short_description" data-target-input="#excerpt" data-label="<?php esc_attr_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?>"> <div class="groq-ai-result-field" data-field="short_description" data-target-input="#excerpt" data-label="<?php esc_attr_e( 'Korte beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header"> <div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?></strong> <strong><?php esc_html_e( 'Korte beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<div class="groq-ai-result-field__actions"> <div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="short_description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button> <button type="button" class="button button-secondary groq-ai-copy-field" data-field="short_description"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="short_description"><?php esc_html_e( 'Vul korte beschrijving in', 'groq-ai-product-text' ); ?></button> <button type="button" class="button groq-ai-apply-field" data-field="short_description"><?php esc_html_e( 'Vul korte beschrijving in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span> <span class="groq-ai-apply-status" aria-hidden="true"></span>
</div> </div>
</div> </div>
<textarea rows="3"></textarea> <textarea rows="3"></textarea>
</div> </div>
<div class="groq-ai-result-field" data-field="description" data-target-input="#content" data-label="<?php esc_attr_e( 'Beschrijving', 'groq-ai-product-text' ); ?>"> <div class="groq-ai-result-field" data-field="description" data-target-input="#content" data-label="<?php esc_attr_e( 'Beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header"> <div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Beschrijving', 'groq-ai-product-text' ); ?></strong> <strong><?php esc_html_e( 'Beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<div class="groq-ai-result-field__actions"> <div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button> <button type="button" class="button button-secondary groq-ai-copy-field" data-field="description"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="description"><?php esc_html_e( 'Vul beschrijving in', 'groq-ai-product-text' ); ?></button> <button type="button" class="button groq-ai-apply-field" data-field="description"><?php esc_html_e( 'Vul beschrijving in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span> <span class="groq-ai-apply-status" aria-hidden="true"></span>
</div> </div>
</div> </div>
<textarea rows="6"></textarea> <textarea rows="6"></textarea>
</div> </div>
<?php if ( $rankmath_enabled ) : ?> <?php if ( $rankmath_enabled ) : ?>
<div class="groq-ai-result-field" data-field="meta_title" data-target-input="#rank_math_title" data-rankmath-action="updateTitle" data-label="<?php esc_attr_e( 'Rank Math meta titel', 'groq-ai-product-text' ); ?>"> <div class="groq-ai-result-field" data-field="meta_title" data-target-input="#rank_math_title" data-rankmath-action="updateTitle" data-label="<?php esc_attr_e( 'Rank Math meta titel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header"> <div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Rank Math meta titel', 'groq-ai-product-text' ); ?></strong> <strong><?php esc_html_e( 'Rank Math meta titel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<div class="groq-ai-result-field__actions"> <div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_title"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button> <button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_title"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="meta_title"><?php esc_html_e( 'Vul meta titel in', 'groq-ai-product-text' ); ?></button> <button type="button" class="button groq-ai-apply-field" data-field="meta_title"><?php esc_html_e( 'Vul meta titel in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span> <span class="groq-ai-apply-status" aria-hidden="true"></span>
</div> </div>
</div> </div>
<textarea rows="2"></textarea> <textarea rows="2"></textarea>
</div> </div>
<div class="groq-ai-result-field" data-field="meta_description" data-target-input="#rank_math_description" data-rankmath-action="updateDescription" data-label="<?php esc_attr_e( 'Rank Math meta description', 'groq-ai-product-text' ); ?>"> <div class="groq-ai-result-field" data-field="meta_description" data-target-input="#rank_math_description" data-rankmath-action="updateDescription" data-label="<?php esc_attr_e( 'Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header"> <div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Rank Math meta description', 'groq-ai-product-text' ); ?></strong> <strong><?php esc_html_e( 'Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<div class="groq-ai-result-field__actions"> <div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button> <button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_description"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="meta_description"><?php esc_html_e( 'Vul meta description in', 'groq-ai-product-text' ); ?></button> <button type="button" class="button groq-ai-apply-field" data-field="meta_description"><?php esc_html_e( 'Vul meta description in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span> <span class="groq-ai-apply-status" aria-hidden="true"></span>
</div> </div>
</div> </div>
<textarea rows="3"></textarea> <textarea rows="3"></textarea>
</div> </div>
<div class="groq-ai-result-field" data-field="focus_keywords" data-target-input="#rank_math_focus_keyword" data-rankmath-action="updateKeywords" data-label="<?php esc_attr_e( 'Rank Math focus keyphrase', 'groq-ai-product-text' ); ?>"> <div class="groq-ai-result-field" data-field="focus_keywords" data-target-input="#rank_math_focus_keyword" data-rankmath-action="updateKeywords" data-label="<?php esc_attr_e( 'Rank Math focus keyphrase', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header"> <div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Rank Math focus keyphrase', 'groq-ai-product-text' ); ?></strong> <strong><?php esc_html_e( 'Rank Math focus keyphrase', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<div class="groq-ai-result-field__actions"> <div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="focus_keywords"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button> <button type="button" class="button button-secondary groq-ai-copy-field" data-field="focus_keywords"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="focus_keywords"><?php esc_html_e( 'Vul focus keyphrase in', 'groq-ai-product-text' ); ?></button> <button type="button" class="button groq-ai-apply-field" data-field="focus_keywords"><?php esc_html_e( 'Vul focus keyphrase in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span> <span class="groq-ai-apply-status" aria-hidden="true"></span>
</div> </div>
</div> </div>
<textarea rows="2" placeholder="<?php esc_attr_e( 'bijv. luxe massage apparaat, wellness cadeau', 'groq-ai-product-text' ); ?>"></textarea> <textarea rows="2" placeholder="<?php esc_attr_e( 'bijv. luxe massage apparaat, wellness cadeau', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>"></textarea>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="groq-ai-modal__raw"> <div class="groq-ai-modal__raw">
<h4><?php esc_html_e( 'Ruwe JSON-output', 'groq-ai-product-text' ); ?></h4> <h4><?php esc_html_e( 'Ruwe JSON-output', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h4>
<pre id="groq-ai-output"></pre> <pre id="groq-ai-output"></pre>
<button type="button" class="button groq-ai-copy-json"><?php esc_html_e( 'Kopieer JSON', 'groq-ai-product-text' ); ?></button> <button type="button" class="button groq-ai-copy-json"><?php esc_html_e( 'Kopieer JSON', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
</div> </div>
</div> </div>
<div class="groq-ai-modal__status" aria-live="polite"></div> <div class="groq-ai-modal__status" aria-live="polite"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,113 @@ class Groq_AI_Ajax_Controller {
add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] ); add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] );
add_action( 'wp_ajax_groq_ai_refresh_models', [ $this, 'handle_refresh_models' ] ); add_action( 'wp_ajax_groq_ai_refresh_models', [ $this, 'handle_refresh_models' ] );
add_action( 'wp_ajax_groq_ai_generate_term_text', [ $this, 'handle_generate_term_text' ] );
}
public function handle_generate_term_text() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
}
check_ajax_referer( 'groq_ai_generate_term', 'nonce' );
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
$term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0;
$include_top_products = ! empty( $_POST['include_top_products'] );
$top_products_limit = isset( $_POST['top_products_limit'] ) ? absint( $_POST['top_products_limit'] ) : 10;
$top_products_limit = max( 1, min( 25, $top_products_limit ) );
if ( '' === $prompt || '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
wp_send_json_error( [ 'message' => __( 'Prompt, taxonomy en term_id zijn verplicht.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
}
$term = get_term( $term_id, $taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
wp_send_json_error( [ 'message' => __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
}
$settings = $this->plugin->get_settings();
$provider_manager = $this->plugin->get_provider_manager();
$provider_key = $settings['provider'];
$provider = $provider_manager->get_provider( $provider_key );
if ( ! $provider ) {
$provider = $provider_manager->get_provider( 'groq' );
$provider_key = 'groq';
}
$conversation_id = $this->plugin->get_conversation_manager()->ensure_id( $provider_key, $settings['store_context'] );
$prompt_builder = $this->plugin->get_prompt_builder();
$system_prompt = method_exists( $prompt_builder, 'build_term_system_prompt' )
? $prompt_builder->build_term_system_prompt( $settings, $conversation_id, $term )
: $prompt_builder->build_system_prompt( $settings, $conversation_id );
$model = $this->plugin->get_selected_model( $provider, $settings );
$context_block = '';
if ( method_exists( $prompt_builder, 'build_term_context_block' ) ) {
$context_block = $prompt_builder->build_term_context_block(
$term,
[
'include_top_products' => $include_top_products,
'top_products_limit' => $top_products_limit,
],
$settings
);
}
$prompt_with_context = method_exists( $prompt_builder, 'prepend_term_context_to_prompt' )
? $prompt_builder->prepend_term_context_to_prompt( $prompt, $context_block )
: $prompt_builder->prepend_context_to_prompt( $prompt, $context_block );
$response_format = null;
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
if ( $use_response_format && method_exists( $prompt_builder, 'get_term_response_format_definition' ) ) {
$response_format = $prompt_builder->get_term_response_format_definition( $settings );
$final_prompt = $prompt_with_context;
} elseif ( method_exists( $prompt_builder, 'append_term_response_instructions' ) ) {
$final_prompt = $prompt_builder->append_term_response_instructions( $prompt_with_context, $settings );
} else {
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
}
$result = $provider->generate_content(
[
'prompt' => $final_prompt,
'system_prompt' => $system_prompt,
'model' => $model,
'settings' => $settings,
'temperature' => 0.7,
'conversation_id' => $conversation_id,
'response_format' => $response_format,
]
);
if ( is_wp_error( $result ) ) {
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
}
$response_text = $this->extract_content_text( $result );
$parsed = null;
if ( method_exists( $prompt_builder, 'parse_term_structured_response' ) ) {
$parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings );
}
if ( ! is_array( $parsed ) ) {
$parsed = [
'description' => trim( (string) $response_text ),
];
}
wp_send_json_success(
[
'description' => isset( $parsed['description'] ) ? $parsed['description'] : '',
'raw' => $response_text,
]
);
} }
public function handle_generate_text() { public function handle_generate_text() {
if ( ! current_user_can( 'edit_products' ) ) { if ( ! current_user_can( 'edit_products' ) ) {
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', 'groq-ai-product-text' ) ], 403 ); wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
} }
check_ajax_referer( 'groq_ai_generate', 'nonce' ); check_ajax_referer( 'groq_ai_generate', 'nonce' );
@@ -37,6 +139,7 @@ class Groq_AI_Ajax_Controller {
$model = $this->plugin->get_selected_model( $provider, $settings ); $model = $this->plugin->get_selected_model( $provider, $settings );
$context_fields = $prompt_builder->parse_context_fields_from_request( isset( $_POST['context_fields'] ) ? $_POST['context_fields'] : '', $settings ); $context_fields = $prompt_builder->parse_context_fields_from_request( isset( $_POST['context_fields'] ) ? $_POST['context_fields'] : '', $settings );
$image_context_mode = $this->plugin->get_image_context_mode( $settings ); $image_context_mode = $this->plugin->get_image_context_mode( $settings );
$image_context_limit = $this->plugin->get_image_context_limit( $settings );
if ( 'none' === $image_context_mode ) { if ( 'none' === $image_context_mode ) {
$context_fields['images'] = false; $context_fields['images'] = false;
@@ -44,7 +147,8 @@ class Groq_AI_Ajax_Controller {
$image_context_enabled = ! empty( $context_fields['images'] ); $image_context_enabled = ! empty( $context_fields['images'] );
$use_base64_payloads = $image_context_enabled && 'base64' === $image_context_mode && $provider->supports_image_context(); $use_base64_payloads = $image_context_enabled && 'base64' === $image_context_mode && $provider->supports_image_context();
$image_context_count = $image_context_enabled ? $prompt_builder->get_product_image_count( $post_id ) : 0; $total_image_count = $image_context_enabled ? $prompt_builder->get_product_image_count( $post_id ) : 0;
$image_context_count = $image_context_enabled ? min( $image_context_limit, $total_image_count ) : 0;
$prompt_image_mode = 'none'; $prompt_image_mode = 'none';
if ( $image_context_enabled ) { if ( $image_context_enabled ) {
@@ -59,17 +163,19 @@ class Groq_AI_Ajax_Controller {
} }
} }
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode ); $product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode, $image_context_limit );
$image_context_payloads = []; $image_context_payloads = [];
if ( $use_base64_payloads ) { if ( $use_base64_payloads ) {
$image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id ); $image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id, $image_context_limit );
} }
$prompt_with_context = $prompt_builder->prepend_context_to_prompt( $prompt, $product_context_text ); $prompt_with_context = $prompt_builder->prepend_context_to_prompt( $prompt, $product_context_text );
$image_context_meta = [ $image_context_meta = [
'requested_mode' => $image_context_mode, 'requested_mode' => $image_context_mode,
'effective_mode' => $prompt_image_mode, 'effective_mode' => $prompt_image_mode,
'available' => $image_context_count, 'limit' => $image_context_limit,
'available' => $total_image_count,
'used' => $image_context_count,
'base64_sent' => $use_base64_payloads ? count( $image_context_payloads ) : 0, 'base64_sent' => $use_base64_payloads ? count( $image_context_payloads ) : 0,
]; ];
@@ -160,7 +266,7 @@ class Groq_AI_Ajax_Controller {
public function handle_refresh_models() { public function handle_refresh_models() {
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Geen toestemming.', 'groq-ai-product-text' ) ], 403 ); wp_send_json_error( [ 'message' => __( 'Geen toestemming.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
} }
check_ajax_referer( 'groq_ai_refresh_models', 'nonce' ); check_ajax_referer( 'groq_ai_refresh_models', 'nonce' );
@@ -169,13 +275,13 @@ class Groq_AI_Ajax_Controller {
$api_key = isset( $_POST['apiKey'] ) ? sanitize_text_field( wp_unslash( $_POST['apiKey'] ) ) : ''; $api_key = isset( $_POST['apiKey'] ) ? sanitize_text_field( wp_unslash( $_POST['apiKey'] ) ) : '';
if ( empty( $provider_key ) || empty( $api_key ) ) { if ( empty( $provider_key ) || empty( $api_key ) ) {
wp_send_json_error( [ 'message' => __( 'Provider en API-sleutel zijn verplicht.', 'groq-ai-product-text' ) ], 400 ); wp_send_json_error( [ 'message' => __( 'Provider en API-sleutel zijn verplicht.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
} }
$provider = $this->plugin->get_provider_manager()->get_provider( $provider_key ); $provider = $this->plugin->get_provider_manager()->get_provider( $provider_key );
if ( ! $provider || ! $provider->supports_live_models() ) { if ( ! $provider || ! $provider->supports_live_models() ) {
wp_send_json_error( [ 'message' => __( 'Deze aanbieder ondersteunt het ophalen van modellen niet.', 'groq-ai-product-text' ) ], 400 ); wp_send_json_error( [ 'message' => __( 'Deze aanbieder ondersteunt het ophalen van modellen niet.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
} }
$result = $provider->fetch_live_models( $api_key ); $result = $provider->fetch_live_models( $api_key );

View File

@@ -16,7 +16,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
public function fetch_live_models( $api_key ) { public function fetch_live_models( $api_key ) {
$endpoint = $this->get_models_endpoint(); $endpoint = $this->get_models_endpoint();
if ( empty( $endpoint ) ) { if ( empty( $endpoint ) ) {
return new WP_Error( 'groq_ai_models_endpoint_missing', __( 'Geen model-endpoint beschikbaar voor deze aanbieder.', 'groq-ai-product-text' ) ); return new WP_Error( 'groq_ai_models_endpoint_missing', __( 'Geen model-endpoint beschikbaar voor deze aanbieder.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
$response = wp_remote_get( $response = wp_remote_get(
@@ -41,7 +41,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
} }
if ( empty( $body['data'] ) || ! is_array( $body['data'] ) ) { if ( empty( $body['data'] ) || ! is_array( $body['data'] ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) ); return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
$models = []; $models = [];
@@ -52,7 +52,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
} }
if ( empty( $models ) ) { if ( empty( $models ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) ); return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
return $models; return $models;
@@ -66,7 +66,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
$api_key = $this->get_api_key( $settings ); $api_key = $this->get_api_key( $settings );
if ( empty( $api_key ) ) { if ( empty( $api_key ) ) {
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', 'groq-ai-product-text' ), $this->get_label() ) ); return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() ) );
} }
$messages = [ $messages = [
@@ -116,7 +116,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
if ( empty( $body['choices'][0]['message']['content'] ) ) { if ( empty( $body['choices'][0]['message']['content'] ) ) {
return new WP_Error( return new WP_Error(
'groq_ai_empty_response', 'groq_ai_empty_response',
sprintf( __( 'Geen antwoord ontvangen van %s.', 'groq-ai-product-text' ), $this->get_label() ) sprintf( __( 'Geen antwoord ontvangen van %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() )
); );
} }

View File

@@ -6,7 +6,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
} }
public function get_label() { public function get_label() {
return __( 'Google AI (Gemini)', 'groq-ai-product-text' ); return __( 'Google AI (Gemini)', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} }
public function get_default_model() { public function get_default_model() {
@@ -61,7 +61,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
} }
if ( empty( $body['models'] ) || ! is_array( $body['models'] ) ) { if ( empty( $body['models'] ) || ! is_array( $body['models'] ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) ); return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
$models = []; $models = [];
@@ -73,7 +73,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
} }
if ( empty( $models ) ) { if ( empty( $models ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) ); return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
return $models; return $models;
@@ -87,7 +87,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
$api_key = isset( $settings[ $this->get_option_key() ] ) ? $settings[ $this->get_option_key() ] : ''; $api_key = isset( $settings[ $this->get_option_key() ] ) ? $settings[ $this->get_option_key() ] : '';
if ( empty( $api_key ) ) { if ( empty( $api_key ) ) {
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', 'groq-ai-product-text' ), $this->get_label() ) ); return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() ) );
} }
$endpoint = add_query_arg( $endpoint = add_query_arg(
@@ -123,7 +123,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
$parts[] = [ $parts[] = [
'text' => sprintf( 'text' => sprintf(
/* translators: %s: image label */ /* translators: %s: image label */
__( 'Contextafbeelding: %s', 'groq-ai-product-text' ), __( 'Contextafbeelding: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$label $label
), ),
]; ];
@@ -181,7 +181,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
if ( empty( $body['candidates'][0]['content']['parts'] ) ) { if ( empty( $body['candidates'][0]['content']['parts'] ) ) {
return new WP_Error( return new WP_Error(
'groq_ai_empty_response', 'groq_ai_empty_response',
sprintf( __( 'Geen antwoord ontvangen van %s.', 'groq-ai-product-text' ), $this->get_label() ) sprintf( __( 'Geen antwoord ontvangen van %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() )
); );
} }

View File

@@ -6,7 +6,7 @@ class Groq_AI_Provider_Groq extends Groq_AI_Abstract_OpenAI_Provider {
} }
public function get_label() { public function get_label() {
return __( 'Groq', 'groq-ai-product-text' ); return __( 'Groq', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} }
public function get_default_model() { public function get_default_model() {

View File

@@ -6,7 +6,7 @@ class Groq_AI_Provider_OpenAI extends Groq_AI_Abstract_OpenAI_Provider {
} }
public function get_label() { public function get_label() {
return __( 'OpenAI', 'groq-ai-product-text' ); return __( 'OpenAI', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} }
public function get_default_model() { public function get_default_model() {

View File

@@ -0,0 +1,182 @@
<?php
class Groq_AI_Google_Analytics_Data_Client {
/** @var Groq_AI_Google_OAuth_Client */
private $oauth;
public function __construct( Groq_AI_Google_OAuth_Client $oauth ) {
$this->oauth = $oauth;
}
/**
* Simple connectivity check for GA4 Data API.
*
* @param array $settings
* @param string $property_id
* @param string $start_date
* @param string $end_date
* @return array|WP_Error
*/
public function get_property_sessions_summary( $settings, $property_id, $start_date, $end_date ) {
$property_id = trim( (string) $property_id );
if ( '' === $property_id ) {
return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$token = $this->oauth->get_access_token( $settings );
if ( is_wp_error( $token ) ) {
return $token;
}
$endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport';
$body = [
'dateRanges' => [
[
'startDate' => $start_date,
'endDate' => $end_date,
],
],
'metrics' => [
[ 'name' => 'sessions' ],
[ 'name' => 'engagedSessions' ],
],
'limit' => 1,
];
$response = wp_remote_post(
$endpoint,
[
'timeout' => 20,
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $body ),
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$raw_body = wp_remote_retrieve_body( $response );
$data = json_decode( (string) $raw_body, true );
if ( 200 !== $status_code || ! is_array( $data ) ) {
return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
$sessions = 0;
$engaged = 0;
foreach ( $rows as $row ) {
$metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : [];
if ( isset( $metric_values[0]['value'] ) ) {
$sessions += absint( $metric_values[0]['value'] );
}
if ( isset( $metric_values[1]['value'] ) ) {
$engaged += absint( $metric_values[1]['value'] );
}
}
return [
'sessions' => $sessions,
'engagedSessions' => $engaged,
];
}
/**
* Returns approximate GA4 sessions for a landing page path.
*
* @param array $settings
* @param string $property_id
* @param string $page_path e.g. /product-category/foo/
* @param string $start_date YYYY-MM-DD
* @param string $end_date YYYY-MM-DD
* @return array|WP_Error
*/
public function get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date ) {
$property_id = trim( (string) $property_id );
$page_path = trim( (string) $page_path );
if ( '' === $property_id || '' === $page_path ) {
return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID of page path ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$token = $this->oauth->get_access_token( $settings );
if ( is_wp_error( $token ) ) {
return $token;
}
$endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport';
$body = [
'dateRanges' => [
[
'startDate' => $start_date,
'endDate' => $end_date,
],
],
'dimensions' => [
[ 'name' => 'landingPagePlusQueryString' ],
],
'metrics' => [
[ 'name' => 'sessions' ],
[ 'name' => 'engagedSessions' ],
],
'dimensionFilter' => [
'filter' => [
'fieldName' => 'landingPagePlusQueryString',
'stringFilter' => [
'matchType' => 'CONTAINS',
'value' => $page_path,
],
],
],
'limit' => 5,
];
$response = wp_remote_post(
$endpoint,
[
'timeout' => 20,
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $body ),
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$raw_body = wp_remote_retrieve_body( $response );
$data = json_decode( (string) $raw_body, true );
if ( 200 !== $status_code || ! is_array( $data ) ) {
return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
$sessions = 0;
$engaged = 0;
foreach ( $rows as $row ) {
$metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : [];
if ( isset( $metric_values[0]['value'] ) ) {
$sessions += absint( $metric_values[0]['value'] );
}
if ( isset( $metric_values[1]['value'] ) ) {
$engaged += absint( $metric_values[1]['value'] );
}
}
return [
'sessions' => $sessions,
'engagedSessions' => $engaged,
];
}
}

View File

@@ -0,0 +1,113 @@
<?php
class Groq_AI_Google_Context_Builder {
/** @var Groq_AI_Google_Search_Console_Client */
private $gsc;
/** @var Groq_AI_Google_Analytics_Data_Client */
private $ga;
public function __construct( Groq_AI_Google_Search_Console_Client $gsc, Groq_AI_Google_Analytics_Data_Client $ga ) {
$this->gsc = $gsc;
$this->ga = $ga;
}
/**
* @param string $existing
* @param WP_Term $term
* @param array $settings
* @return string
*/
public function build_term_google_context( $existing, $term, $settings ) {
if ( ! $term || ! is_object( $term ) ) {
return (string) $existing;
}
$enabled_gsc = ! empty( $settings['google_enable_gsc'] );
$enabled_ga = ! empty( $settings['google_enable_ga'] );
if ( ! $enabled_gsc && ! $enabled_ga ) {
return (string) $existing;
}
$term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
$taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
$range_days = 28;
$end_date = gmdate( 'Y-m-d' );
$start_date = gmdate( 'Y-m-d', time() - ( $range_days * DAY_IN_SECONDS ) );
$term_link = get_term_link( $term );
if ( is_wp_error( $term_link ) ) {
$term_link = '';
}
$page_path = '';
if ( is_string( $term_link ) && '' !== $term_link ) {
$parts = wp_parse_url( $term_link );
if ( is_array( $parts ) && isset( $parts['path'] ) ) {
$page_path = (string) $parts['path'];
}
}
$cache_key = 'groq_ai_google_term_ctx_' . md5( $taxonomy . '|' . $term_id . '|' . $start_date . '|' . $end_date );
$cached = get_transient( $cache_key );
if ( is_string( $cached ) && '' !== $cached ) {
return trim( (string) $existing . "\n\n" . $cached );
}
$lines = [];
$lines[] = sprintf(
/* translators: %d: days */
__( 'Google data (laatste %d dagen):', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$range_days
);
if ( $enabled_gsc ) {
$site_url = isset( $settings['google_gsc_site_url'] ) ? trim( (string) $settings['google_gsc_site_url'] ) : '';
if ( '' !== $site_url && '' !== $term_link ) {
$queries = $this->gsc->get_top_queries_for_page( $settings, $site_url, $term_link, $start_date, $end_date, 10 );
if ( is_wp_error( $queries ) ) {
$lines[] = __( 'Search Console: kon queries niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} elseif ( empty( $queries ) ) {
$lines[] = __( 'Search Console: geen query data gevonden voor deze pagina.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} else {
$lines[] = __( 'Search Console top zoekopdrachten (query → clicks/impr):', GROQ_AI_PRODUCT_TEXT_DOMAIN );
foreach ( $queries as $row ) {
$q = isset( $row['query'] ) ? (string) $row['query'] : '';
$c = isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0;
$i = isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0;
if ( '' === $q ) {
continue;
}
$lines[] = sprintf( '- %s → %d/%d', $q, (int) round( $c ), (int) round( $i ) );
}
}
}
}
if ( $enabled_ga ) {
$property_id = isset( $settings['google_ga4_property_id'] ) ? trim( (string) $settings['google_ga4_property_id'] ) : '';
if ( '' !== $property_id && '' !== $page_path ) {
$stats = $this->ga->get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date );
if ( is_wp_error( $stats ) ) {
$lines[] = __( 'Analytics: kon sessies niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} else {
$sessions = isset( $stats['sessions'] ) ? absint( $stats['sessions'] ) : 0;
$engaged = isset( $stats['engagedSessions'] ) ? absint( $stats['engagedSessions'] ) : 0;
$lines[] = sprintf( __( 'Analytics (GA4): sessies ~%1$d, engaged sessies ~%2$d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $sessions, $engaged );
}
}
}
// If we only have the header, skip.
if ( count( $lines ) <= 1 ) {
return (string) $existing;
}
$context = implode( "\n", $lines );
set_transient( $cache_key, $context, 15 * MINUTE_IN_SECONDS );
return trim( (string) $existing . "\n\n" . $context );
}
}

View File

@@ -0,0 +1,101 @@
<?php
class Groq_AI_Google_OAuth_Client {
/**
* @param array $settings
* @return string|WP_Error
*/
public function get_access_token( $settings ) {
$client_id = isset( $settings['google_oauth_client_id'] ) ? trim( (string) $settings['google_oauth_client_id'] ) : '';
$client_secret = isset( $settings['google_oauth_client_secret'] ) ? trim( (string) $settings['google_oauth_client_secret'] ) : '';
$refresh_token = isset( $settings['google_oauth_refresh_token'] ) ? trim( (string) $settings['google_oauth_refresh_token'] ) : '';
if ( '' === $client_id || '' === $client_secret || '' === $refresh_token ) {
return new WP_Error( 'groq_ai_google_oauth_missing', __( 'Google OAuth is niet (volledig) geconfigureerd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$cache_key = 'groq_ai_google_access_token_' . md5( $client_id . '|' . $refresh_token );
$cached = get_transient( $cache_key );
if ( is_string( $cached ) && '' !== $cached ) {
return $cached;
}
$response = wp_remote_post(
'https://oauth2.googleapis.com/token',
[
'timeout' => 20,
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
],
'body' => [
'client_id' => $client_id,
'client_secret' => $client_secret,
'refresh_token' => $refresh_token,
'grant_type' => 'refresh_token',
],
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( (string) $body, true );
if ( 200 !== $status_code || ! is_array( $data ) ) {
return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Google token refresh mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$access_token = isset( $data['access_token'] ) ? sanitize_text_field( (string) $data['access_token'] ) : '';
$expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0;
if ( '' === $access_token ) {
return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Geen access token ontvangen van Google.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$ttl = max( 60, $expires_in - 60 );
set_transient( $cache_key, $access_token, $ttl );
return $access_token;
}
/**
* Diagnostics helper: returns scopes for a given access token.
*
* @param string $access_token
* @return array|WP_Error { 'scope' => string, 'expires_in' => int }
*/
public function get_access_token_info( $access_token ) {
$access_token = trim( (string) $access_token );
if ( '' === $access_token ) {
return new WP_Error( 'groq_ai_google_tokeninfo_missing', __( 'Geen access token om te inspecteren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$response = wp_remote_get(
add_query_arg( [ 'access_token' => $access_token ], 'https://oauth2.googleapis.com/tokeninfo' ),
[ 'timeout' => 15 ]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( (string) $body, true );
if ( 200 !== $status_code || ! is_array( $data ) ) {
return new WP_Error( 'groq_ai_google_tokeninfo_failed', __( 'Kon tokeninfo niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$scope = isset( $data['scope'] ) ? trim( (string) $data['scope'] ) : '';
$expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0;
return [
'scope' => $scope,
'expires_in' => $expires_in,
];
}
}

View File

@@ -0,0 +1,195 @@
<?php
class Groq_AI_Google_Search_Console_Client {
/** @var Groq_AI_Google_OAuth_Client */
private $oauth;
public function __construct( Groq_AI_Google_OAuth_Client $oauth ) {
$this->oauth = $oauth;
}
/**
* @param int $status_code
* @param string $raw_body
* @return WP_Error
*/
private function build_http_error( $status_code, $raw_body ) {
$status_code = absint( $status_code );
$raw_body = (string) $raw_body;
$message = __( 'Search Console API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
$details = '';
$data = json_decode( $raw_body, true );
if ( is_array( $data ) ) {
// Google APIs often respond with: { error: { code, message, status, details/errors } }
$err = isset( $data['error'] ) && is_array( $data['error'] ) ? $data['error'] : [];
$google_message = isset( $err['message'] ) ? trim( (string) $err['message'] ) : '';
$google_status = isset( $err['status'] ) ? trim( (string) $err['status'] ) : '';
if ( '' !== $google_status || '' !== $google_message ) {
$details = trim( $google_status . ( $google_status && $google_message ? ': ' : '' ) . $google_message );
}
}
if ( '' !== $details ) {
$message = sprintf(
/* translators: 1: HTTP status, 2: details */
__( 'Search Console API call mislukt (HTTP %1$d): %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$status_code,
$details
);
} else {
$message = sprintf(
/* translators: %d: HTTP status */
__( 'Search Console API call mislukt (HTTP %d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$status_code
);
}
return new WP_Error( 'groq_ai_gsc_error', $message );
}
/**
* @param array $settings
* @return array|WP_Error Array of siteUrl strings.
*/
public function list_sites( $settings ) {
$token = $this->oauth->get_access_token( $settings );
if ( is_wp_error( $token ) ) {
return $token;
}
$response = wp_remote_get(
'https://searchconsole.googleapis.com/webmasters/v3/sites',
[
'timeout' => 20,
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
],
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$raw_body = wp_remote_retrieve_body( $response );
$data = json_decode( (string) $raw_body, true );
if ( 200 !== $status_code || ! is_array( $data ) ) {
return $this->build_http_error( $status_code, $raw_body );
}
$entries = isset( $data['siteEntry'] ) && is_array( $data['siteEntry'] ) ? $data['siteEntry'] : [];
$sites = [];
foreach ( $entries as $entry ) {
if ( ! is_array( $entry ) ) {
continue;
}
$site_url = isset( $entry['siteUrl'] ) ? trim( (string) $entry['siteUrl'] ) : '';
if ( '' !== $site_url ) {
$sites[] = $site_url;
}
}
$sites = array_values( array_unique( $sites ) );
sort( $sites, SORT_NATURAL | SORT_FLAG_CASE );
return $sites;
}
/**
* @param array $settings
* @param string $site_url
* @param string $page_url
* @param string $start_date YYYY-MM-DD
* @param string $end_date YYYY-MM-DD
* @param int $limit
* @return array|WP_Error
*/
public function get_top_queries_for_page( $settings, $site_url, $page_url, $start_date, $end_date, $limit = 10 ) {
$site_url = trim( (string) $site_url );
$page_url = trim( (string) $page_url );
$limit = max( 1, min( 25, absint( $limit ) ) );
if ( '' === $site_url || '' === $page_url ) {
return new WP_Error( 'groq_ai_gsc_missing', __( 'Search Console site URL of pagina URL ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$token = $this->oauth->get_access_token( $settings );
if ( is_wp_error( $token ) ) {
return $token;
}
$endpoint = 'https://searchconsole.googleapis.com/webmasters/v3/sites/' . rawurlencode( $site_url ) . '/searchAnalytics/query';
$body = [
'startDate' => $start_date,
'endDate' => $end_date,
'dimensions' => [ 'query' ],
'rowLimit' => $limit,
'dimensionFilterGroups' => [
[
'filters' => [
[
'dimension' => 'page',
'operator' => 'equals',
'expression' => $page_url,
],
],
],
],
'aggregationType' => 'auto',
];
$response = wp_remote_post(
$endpoint,
[
'timeout' => 20,
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'body' => wp_json_encode( $body ),
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
$raw_body = wp_remote_retrieve_body( $response );
$data = json_decode( (string) $raw_body, true );
if ( 200 !== $status_code || ! is_array( $data ) ) {
return $this->build_http_error( $status_code, $raw_body );
}
$rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
$result = [];
foreach ( $rows as $row ) {
if ( ! is_array( $row ) ) {
continue;
}
$keys = isset( $row['keys'] ) && is_array( $row['keys'] ) ? $row['keys'] : [];
$query = isset( $keys[0] ) ? sanitize_text_field( (string) $keys[0] ) : '';
if ( '' === $query ) {
continue;
}
$result[] = [
'query' => $query,
'clicks' => isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0,
'impressions' => isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0,
'ctr' => isset( $row['ctr'] ) ? (float) $row['ctr'] : 0.0,
'position' => isset( $row['position'] ) ? (float) $row['position'] : 0.0,
];
}
return $result;
}
}

View File

@@ -13,17 +13,43 @@ class Groq_AI_Prompt_Builder {
public function build_system_prompt( $settings, $conversation_id ) { public function build_system_prompt( $settings, $conversation_id ) {
$context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : ''; $context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : '';
$base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft overtuigende productbeschrijvingen.', 'groq-ai-product-text' ); $base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft overtuigende productbeschrijvingen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
if ( $context ) { if ( $context ) {
$base_instruction = sprintf( $base_instruction = sprintf(
__( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende context indien beschikbaar: %s', 'groq-ai-product-text' ), __( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende context indien beschikbaar: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$context $context
); );
} }
return sprintf( return sprintf(
__( 'Conversatie-ID: %1$s. %2$s', 'groq-ai-product-text' ), __( 'Conversatie-ID: %1$s. %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$conversation_id,
$base_instruction
);
}
public function build_term_system_prompt( $settings, $conversation_id, $term ) {
$context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : '';
$term_name = is_object( $term ) && isset( $term->name ) ? (string) $term->name : '';
$base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft SEO-vriendelijke categorie- en merkpagina teksten.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
if ( $context ) {
$base_instruction = sprintf(
__( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende winkelcontext indien beschikbaar: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$context
);
}
if ( '' !== $term_name ) {
$base_instruction .= ' ' . sprintf(
__( 'Je schrijft nu voor de term: %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$term_name
);
}
return sprintf(
__( 'Conversatie-ID: %1$s. %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$conversation_id, $conversation_id,
$base_instruction $base_instruction
); );
@@ -46,7 +72,7 @@ class Groq_AI_Prompt_Builder {
public function parse_structured_response( $raw, $settings = null ) { public function parse_structured_response( $raw, $settings = null ) {
if ( empty( $raw ) ) { if ( empty( $raw ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', 'groq-ai-product-text' ) ); return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
$clean = trim( $raw ); $clean = trim( $raw );
@@ -58,7 +84,7 @@ class Groq_AI_Prompt_Builder {
$decoded = json_decode( $clean, true ); $decoded = json_decode( $clean, true );
if ( ! is_array( $decoded ) ) { if ( ! is_array( $decoded ) ) {
return new WP_Error( 'groq_ai_parse_error', __( 'Kon de AI-respons niet als JSON lezen. Probeer het opnieuw.', 'groq-ai-product-text' ) ); return new WP_Error( 'groq_ai_parse_error', __( 'Kon de AI-respons niet als JSON lezen. Probeer het opnieuw.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
$fields = [ $fields = [
@@ -67,6 +93,40 @@ class Groq_AI_Prompt_Builder {
'description' => trim( (string) ( $decoded['description'] ?? '' ) ), 'description' => trim( (string) ( $decoded['description'] ?? '' ) ),
]; ];
$title_suggestions = [];
if ( isset( $decoded['title_suggestions'] ) && is_array( $decoded['title_suggestions'] ) ) {
foreach ( $decoded['title_suggestions'] as $suggestion ) {
$suggestion = sanitize_text_field( (string) $suggestion );
$suggestion = trim( preg_replace( '/\s+/', ' ', $suggestion ) );
if ( '' === $suggestion ) {
continue;
}
$title_suggestions[] = $suggestion;
if ( count( $title_suggestions ) >= 3 ) {
break;
}
}
}
if ( empty( $title_suggestions ) && '' !== $fields['title'] ) {
$title_suggestions[] = $fields['title'];
}
if ( '' === $fields['title'] && ! empty( $title_suggestions ) ) {
$fields['title'] = $title_suggestions[0];
}
$fields['title_suggestions'] = $title_suggestions;
$slug_value = isset( $decoded['slug'] ) ? sanitize_title( $decoded['slug'] ) : '';
if ( '' === $slug_value && '' !== $fields['title'] ) {
$slug_value = sanitize_title( $fields['title'] );
}
$fields['slug'] = $slug_value;
if ( $this->settings_manager->is_module_enabled( 'rankmath', $settings ) ) { if ( $this->settings_manager->is_module_enabled( 'rankmath', $settings ) ) {
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings ); $keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
$focus_keywords = []; $focus_keywords = [];
@@ -98,8 +158,22 @@ class Groq_AI_Prompt_Builder {
$fields['focus_keywords'] = implode( ', ', $focus_keywords ); $fields['focus_keywords'] = implode( ', ', $focus_keywords );
} }
if ( implode( '', $fields ) === '' ) { $primary_values = [
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen bruikbare velden.', 'groq-ai-product-text' ) ); $fields['title'],
$fields['short_description'],
$fields['description'],
];
$has_primary_content = false;
foreach ( $primary_values as $value ) {
if ( '' !== trim( (string) $value ) ) {
$has_primary_content = true;
break;
}
}
if ( ! $has_primary_content ) {
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen bruikbare velden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
} }
return $fields; return $fields;
@@ -125,7 +199,7 @@ class Groq_AI_Prompt_Builder {
return $normalized; return $normalized;
} }
public function build_product_context_block( $post_id, $fields, $image_mode = 'url' ) { public function build_product_context_block( $post_id, $fields, $image_mode = 'url', $image_limit = 3 ) {
$post_id = absint( $post_id ); $post_id = absint( $post_id );
if ( ! $post_id ) { if ( ! $post_id ) {
@@ -137,35 +211,35 @@ class Groq_AI_Prompt_Builder {
if ( ! empty( $fields['title'] ) ) { if ( ! empty( $fields['title'] ) ) {
$title = get_the_title( $post_id ); $title = get_the_title( $post_id );
if ( $title ) { if ( $title ) {
$parts[] = sprintf( __( 'Titel: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $title ) ); $parts[] = sprintf( __( 'Titel: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( $title ) );
} }
} }
if ( ! empty( $fields['short_description'] ) ) { if ( ! empty( $fields['short_description'] ) ) {
$excerpt = get_post_field( 'post_excerpt', $post_id ); $excerpt = get_post_field( 'post_excerpt', $post_id );
if ( $excerpt ) { if ( $excerpt ) {
$parts[] = sprintf( __( 'Korte beschrijving: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $excerpt ) ); $parts[] = sprintf( __( 'Korte beschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( $excerpt ) );
} }
} }
if ( ! empty( $fields['description'] ) ) { if ( ! empty( $fields['description'] ) ) {
$content = get_post_field( 'post_content', $post_id ); $content = get_post_field( 'post_content', $post_id );
if ( $content ) { if ( $content ) {
$parts[] = sprintf( __( 'Beschrijving: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $content ) ); $parts[] = sprintf( __( 'Beschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( $content ) );
} }
} }
if ( ! empty( $fields['attributes'] ) ) { if ( ! empty( $fields['attributes'] ) ) {
$attributes = $this->get_product_attributes_text( $post_id ); $attributes = $this->get_product_attributes_text( $post_id );
if ( $attributes ) { if ( $attributes ) {
$parts[] = sprintf( __( 'Attributen: %s', 'groq-ai-product-text' ), $attributes ); $parts[] = sprintf( __( 'Attributen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $attributes );
} }
} }
if ( ! empty( $fields['images'] ) && 'url' === $image_mode ) { if ( ! empty( $fields['images'] ) && 'url' === $image_mode ) {
$images = $this->get_product_images_text( $post_id ); $images = $this->get_product_images_text( $post_id, $image_limit );
if ( $images ) { if ( $images ) {
$parts[] = sprintf( __( 'Afbeeldingen: %s', 'groq-ai-product-text' ), $images ); $parts[] = sprintf( __( 'Afbeeldingen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $images );
} }
} }
@@ -179,31 +253,77 @@ class Groq_AI_Prompt_Builder {
return $prompt; return $prompt;
} }
$intro = __( 'Gebruik de volgende productcontext bij het schrijven:', 'groq-ai-product-text' ); $intro = __( 'Gebruik de volgende productcontext bij het schrijven:', GROQ_AI_PRODUCT_TEXT_DOMAIN );
return $intro . "\n" . $context . "\n\n" . $prompt; return $intro . "\n" . $context . "\n\n" . $prompt;
} }
public function get_response_format_definition( $settings = null ) { public function build_term_context_block( $term, $options = [], $settings = null ) {
if ( ! $term || ! is_object( $term ) ) {
return '';
}
$taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
$term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
if ( '' === $taxonomy || ! $term_id ) {
return '';
}
$include_top_products = ! empty( $options['include_top_products'] );
$top_products_limit = isset( $options['top_products_limit'] ) ? absint( $options['top_products_limit'] ) : 10;
$top_products_limit = max( 1, min( 25, $top_products_limit ) );
$parts = [];
$parts[] = sprintf( __( 'Term: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->name ) );
if ( isset( $term->slug ) && '' !== (string) $term->slug ) {
$parts[] = sprintf( __( 'Slug: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), sanitize_title( (string) $term->slug ) );
}
if ( isset( $term->count ) ) {
$parts[] = sprintf( __( 'Aantal producten: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (string) absint( $term->count ) );
}
if ( isset( $term->description ) && '' !== trim( (string) $term->description ) ) {
$parts[] = sprintf( __( 'Huidige omschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->description ) );
}
if ( $include_top_products ) {
$top_products = $this->get_top_products_for_term( $taxonomy, $term_id, $top_products_limit );
if ( ! empty( $top_products ) ) {
$lines = [];
foreach ( $top_products as $product_row ) {
$lines[] = sprintf( '- %s', $product_row );
}
$parts[] = __( 'Top verkochte producten (indicatief):', GROQ_AI_PRODUCT_TEXT_DOMAIN ) . "\n" . implode( "\n", $lines );
}
}
$google_context = apply_filters( 'groq_ai_term_google_context', '', $term, $settings );
$google_context = trim( (string) $google_context );
if ( '' !== $google_context ) {
$parts[] = $google_context;
}
return implode( "\n\n", array_filter( $parts ) );
}
public function prepend_term_context_to_prompt( $prompt, $context ) {
$context = trim( (string) $context );
if ( '' === $context ) {
return $prompt;
}
$intro = __( 'Gebruik de volgende categorie/term-context bij het schrijven:', GROQ_AI_PRODUCT_TEXT_DOMAIN );
return $intro . "\n" . $context . "\n\n" . $prompt;
}
public function get_term_response_format_definition( $settings = null ) {
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings ); $rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings ); $keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings ); $title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings ); $desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
$properties = [ $properties = [
'title' => [ 'description' => [
'type' => 'string', 'type' => 'string',
'description' => __( 'Korte, overtuigende producttitel in het Nederlands.', 'groq-ai-product-text' ), 'description' => __( 'HTML-omschrijving voor de categorie/term met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 3,
],
'short_description' => [
'type' => 'string',
'description' => __( "Korte HTML-beschrijving in <p>-tags (maximaal 2 alinea's).", 'groq-ai-product-text' ),
'minLength' => 10,
],
'description' => [
'type' => 'string',
'description' => __( 'Uitgebreide HTML-productbeschrijving met paragrafen en eventueel lijsten.', 'groq-ai-product-text' ),
'minLength' => 20, 'minLength' => 20,
], ],
]; ];
@@ -212,8 +332,7 @@ class Groq_AI_Prompt_Builder {
$properties['meta_title'] = [ $properties['meta_title'] = [
'type' => 'string', 'type' => 'string',
'description' => sprintf( 'description' => sprintf(
/* translators: 1: maximum character count, 2: maximum pixels */ __( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
60, 60,
$title_pixels $title_pixels
), ),
@@ -222,8 +341,7 @@ class Groq_AI_Prompt_Builder {
$properties['meta_description'] = [ $properties['meta_description'] = [
'type' => 'string', 'type' => 'string',
'description' => sprintf( 'description' => sprintf(
/* translators: 1: maximum character count, 2: maximum pixels */ __( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
160, 160,
$desc_pixels $desc_pixels
), ),
@@ -231,7 +349,7 @@ class Groq_AI_Prompt_Builder {
]; ];
$properties['focus_keywords'] = [ $properties['focus_keywords'] = [
'type' => 'array', 'type' => 'array',
'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', 'groq-ai-product-text' ), 'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'maxItems' => max( 1, $keyword_limit ), 'maxItems' => max( 1, $keyword_limit ),
'items' => [ 'items' => [
'type' => 'string', 'type' => 'string',
@@ -243,7 +361,217 @@ class Groq_AI_Prompt_Builder {
$schema = [ $schema = [
'type' => 'object', 'type' => 'object',
'properties' => $properties, 'properties' => $properties,
'required' => [ 'title', 'short_description', 'description' ], 'required' => [ 'description' ],
'additionalProperties' => false,
];
return [
'type' => 'json_schema',
'json_schema' => [
'name' => 'groq_ai_term_text',
'schema' => $schema,
],
];
}
public function append_term_response_instructions( $prompt, $settings ) {
$instructions = (string) ( $this->get_term_structured_response_instructions( $settings ) ?? '' );
$prompt = trim( (string) $prompt );
if ( '' === $instructions ) {
return $prompt;
}
if ( false !== strpos( $prompt, $instructions ) ) {
return $prompt;
}
return $prompt . "\n\n" . $instructions;
}
public function parse_term_structured_response( $raw, $settings = null ) {
if ( empty( $raw ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$clean = trim( (string) $raw );
if ( preg_match( '/```(?:json)?\s*(.*?)```/is', $clean, $matches ) ) {
$clean = trim( $matches[1] );
}
$decoded = json_decode( $clean, true );
if ( ! is_array( $decoded ) ) {
// Fallback: treat as plain text.
return [
'description' => trim( (string) $raw ),
];
}
$description = isset( $decoded['description'] ) ? trim( (string) $decoded['description'] ) : '';
if ( '' === $description ) {
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen description veld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$result = [
'description' => $description,
];
if ( isset( $decoded['meta_title'] ) ) {
$result['meta_title'] = $this->truncate_meta_field( (string) $decoded['meta_title'], 60 );
}
if ( isset( $decoded['meta_description'] ) ) {
$result['meta_description'] = $this->truncate_meta_field( (string) $decoded['meta_description'], 160 );
}
if ( isset( $decoded['focus_keywords'] ) ) {
if ( is_array( $decoded['focus_keywords'] ) ) {
$keywords = [];
foreach ( $decoded['focus_keywords'] as $kw ) {
$kw = trim( (string) $kw );
if ( '' !== $kw ) {
$keywords[] = $kw;
}
}
$keywords = array_values( array_unique( $keywords ) );
$result['focus_keywords'] = implode( ', ', $keywords );
}
}
return $result;
}
private function get_term_structured_response_instructions( $settings = null ) {
$schema_parts = [
'"description":"..."',
];
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
if ( $rankmath_enabled ) {
$schema_parts[] = '"meta_title":"..."';
$schema_parts[] = '"meta_description":"..."';
$schema_parts[] = '"focus_keywords":["...","..."]';
}
$json_structure = '{' . implode( ',', $schema_parts ) . '}';
$instruction = sprintf(
__( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \n voor regeleinden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$json_structure
);
$instruction .= ' ' . __( 'Zorg dat description geldige HTML bevat (gebruik minimaal <p>-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
return $instruction;
}
private function get_top_products_for_term( $taxonomy, $term_id, $limit = 10 ) {
$taxonomy = sanitize_key( (string) $taxonomy );
$term_id = absint( $term_id );
$limit = max( 1, min( 25, absint( $limit ) ) );
$query = new WP_Query(
[
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $limit,
'no_found_rows' => true,
'meta_key' => 'total_sales',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'tax_query' => [
[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => [ $term_id ],
],
],
]
);
$rows = [];
if ( $query->have_posts() ) {
foreach ( $query->posts as $post ) {
$title = isset( $post->post_title ) ? wp_strip_all_tags( (string) $post->post_title ) : '';
$rows[] = $title;
}
}
wp_reset_postdata();
return array_values( array_filter( $rows ) );
}
public function get_response_format_definition( $settings = null ) {
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
$properties = [
'title_suggestions' => [
'type' => 'array',
'description' => __( 'Exact drie korte producttitelvoorstellen in het Nederlands. Kies de beste ook als title.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minItems' => 3,
'maxItems' => 3,
'items' => [
'type' => 'string',
'minLength' => 3,
'maxLength' => 120,
],
],
'title' => [
'type' => 'string',
'description' => __( 'Korte, overtuigende producttitel in het Nederlands.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 3,
],
'slug' => [
'type' => 'string',
'description' => __( 'Productslug voor de URL (alleen kleine letters, cijfers en koppeltekens).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 3,
'pattern' => '^[a-z0-9\\-]+$',
],
'short_description' => [
'type' => 'string',
'description' => __( "Korte HTML-beschrijving in <p>-tags (maximaal 2 alinea's).", GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 10,
],
'description' => [
'type' => 'string',
'description' => __( 'Uitgebreide HTML-productbeschrijving met paragrafen en eventueel lijsten.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 20,
],
];
if ( $rankmath_enabled ) {
$properties['meta_title'] = [
'type' => 'string',
'description' => sprintf(
/* translators: 1: maximum character count, 2: maximum pixels */
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
60,
$title_pixels
),
'maxLength' => 120,
];
$properties['meta_description'] = [
'type' => 'string',
'description' => sprintf(
/* translators: 1: maximum character count, 2: maximum pixels */
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
160,
$desc_pixels
),
'maxLength' => 320,
];
$properties['focus_keywords'] = [
'type' => 'array',
'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'maxItems' => max( 1, $keyword_limit ),
'items' => [
'type' => 'string',
'minLength' => 1,
],
];
}
$schema = [
'type' => 'object',
'properties' => $properties,
'required' => [ 'title_suggestions', 'title', 'slug', 'short_description', 'description' ],
'additionalProperties' => false, 'additionalProperties' => false,
]; ];
@@ -258,7 +586,9 @@ class Groq_AI_Prompt_Builder {
private function get_structured_response_instructions( $settings = null ) { private function get_structured_response_instructions( $settings = null ) {
$schema_parts = [ $schema_parts = [
'"title_suggestions":["...","...","..."]',
'"title":"..."', '"title":"..."',
'"slug":"..."',
'"short_description":"..."', '"short_description":"..."',
'"description":"..."', '"description":"..."',
]; ];
@@ -274,7 +604,7 @@ class Groq_AI_Prompt_Builder {
$instruction = sprintf( $instruction = sprintf(
/* translators: %s: JSON structure example */ /* translators: %s: JSON structure example */
__( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \\n voor regeleinden. Zorg dat zowel short_description als description nooit leeg zijn.', 'groq-ai-product-text' ), __( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \\n voor regeleinden. Zorg dat zowel short_description als description nooit leeg zijn.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$json_structure $json_structure
); );
@@ -284,14 +614,16 @@ class Groq_AI_Prompt_Builder {
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings ); $desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
$instruction .= ' ' . sprintf( $instruction .= ' ' . sprintf(
/* translators: 1: focus keyword limit, 2: meta title pixel limit, 3: meta description pixel limit */ /* translators: 1: focus keyword limit, 2: meta title pixel limit, 3: meta description pixel limit */
__( 'Beperk meta_title tot maximaal 60 tekens en %2$d pixels en meta_description tot maximaal 160 tekens en %3$d pixels. Lever maximaal %1$d focuskeywords in het focus_keywords-array (korte termen zonder hashtag of extra tekst).', 'groq-ai-product-text' ), __( 'Beperk meta_title tot maximaal 60 tekens en %2$d pixels en meta_description tot maximaal 160 tekens en %3$d pixels. Lever maximaal %1$d focuskeywords in het focus_keywords-array (korte termen zonder hashtag of extra tekst).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$keyword_limit, $keyword_limit,
$title_pixels, $title_pixels,
$desc_pixels $desc_pixels
); );
} }
$instruction .= ' ' . __( 'Zorg dat short_description en description geldige HTML bevatten (gebruik minimaal <p>-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', 'groq-ai-product-text' ); $instruction .= ' ' . __( 'Lever exact drie verschillende titelvoorstellen in title_suggestions en kopieer de beste keuze naar title.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
$instruction .= ' ' . __( 'Zorg dat short_description en description geldige HTML bevatten (gebruik minimaal <p>-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
$instruction .= ' ' . __( 'Maak de slug URL-vriendelijk, gebruik alleen kleine letters, cijfers en koppeltekens en geen spaties.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
return $instruction; return $instruction;
} }
@@ -358,9 +690,14 @@ class Groq_AI_Prompt_Builder {
return implode( '; ', $lines ); return implode( '; ', $lines );
} }
private function get_product_images_text( $post_id ) { private function get_product_images_text( $post_id, $limit = 3 ) {
$limit = max( 0, (int) $limit );
$image_ids = $this->get_product_image_ids( $post_id ); $image_ids = $this->get_product_image_ids( $post_id );
if ( $limit > 0 ) {
$image_ids = array_slice( $image_ids, 0, $limit );
}
if ( empty( $image_ids ) ) { if ( empty( $image_ids ) ) {
return ''; return '';
} }
@@ -380,7 +717,13 @@ class Groq_AI_Prompt_Builder {
} }
public function get_product_image_payloads( $post_id, $limit = 3, $max_filesize = 1572864 ) { public function get_product_image_payloads( $post_id, $limit = 3, $max_filesize = 1572864 ) {
$image_ids = array_slice( $this->get_product_image_ids( $post_id ), 0, max( 1, (int) $limit ) ); $limit = max( 0, (int) $limit );
if ( $limit <= 0 ) {
return [];
}
$image_ids = array_slice( $this->get_product_image_ids( $post_id ), 0, $limit );
if ( empty( $image_ids ) ) { if ( empty( $image_ids ) ) {
return []; return [];
@@ -476,7 +819,7 @@ class Groq_AI_Prompt_Builder {
$label = trim( wp_strip_all_tags( (string) $label ) ); $label = trim( wp_strip_all_tags( (string) $label ) );
if ( '' === $label ) { if ( '' === $label ) {
$label = sprintf( __( 'Afbeelding %d', 'groq-ai-product-text' ), $position ); $label = sprintf( __( 'Afbeelding %d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $position );
} }
$path = get_attached_file( $attachment_id ); $path = get_attached_file( $attachment_id );

View File

@@ -35,9 +35,19 @@ class Groq_AI_Settings_Manager {
'groq_api_key' => '', 'groq_api_key' => '',
'openai_api_key' => '', 'openai_api_key' => '',
'google_api_key' => '', 'google_api_key' => '',
'google_oauth_client_id' => '',
'google_oauth_client_secret' => '',
'google_oauth_refresh_token' => '',
'google_oauth_connected_email' => '',
'google_oauth_connected_at' => 0,
'google_enable_gsc' => true,
'google_enable_ga' => true,
'google_gsc_site_url' => '',
'google_ga4_property_id' => '',
'context_fields' => $this->get_default_context_fields(), 'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(), 'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url', 'image_context_mode' => 'url',
'image_context_limit' => 3,
'response_format_compat' => false, 'response_format_compat' => false,
]; ];
@@ -59,6 +69,9 @@ class Groq_AI_Settings_Manager {
$settings['image_context_mode'] = 'url'; $settings['image_context_mode'] = 'url';
} }
$limit = isset( $settings['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $settings['image_context_limit'] ) : 3;
$settings['image_context_limit'] = $limit;
return $settings; return $settings;
} }
@@ -77,9 +90,19 @@ class Groq_AI_Settings_Manager {
'groq_api_key' => '', 'groq_api_key' => '',
'openai_api_key' => '', 'openai_api_key' => '',
'google_api_key' => '', 'google_api_key' => '',
'google_oauth_client_id' => '',
'google_oauth_client_secret' => '',
'google_oauth_refresh_token' => '',
'google_oauth_connected_email' => '',
'google_oauth_connected_at' => 0,
'google_enable_gsc' => true,
'google_enable_ga' => true,
'google_gsc_site_url' => '',
'google_ga4_property_id' => '',
'context_fields' => $this->get_default_context_fields(), 'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(), 'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url', 'image_context_mode' => 'url',
'image_context_limit' => 3,
'response_format_compat' => false, 'response_format_compat' => false,
]; ];
@@ -104,6 +127,8 @@ class Groq_AI_Settings_Manager {
$image_mode = 'url'; $image_mode = 'url';
} }
$image_limit = isset( $input['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $input['image_context_limit'] ) : $defaults['image_context_limit'];
$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 ) {
@@ -120,8 +145,18 @@ class Groq_AI_Settings_Manager {
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ), 'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ), 'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
'google_api_key' => sanitize_text_field( $input['google_api_key'] ), 'google_api_key' => sanitize_text_field( $input['google_api_key'] ),
'google_oauth_client_id' => sanitize_text_field( $input['google_oauth_client_id'] ),
'google_oauth_client_secret' => sanitize_text_field( $input['google_oauth_client_secret'] ),
'google_oauth_refresh_token' => sanitize_text_field( $input['google_oauth_refresh_token'] ),
'google_oauth_connected_email' => sanitize_text_field( $input['google_oauth_connected_email'] ),
'google_oauth_connected_at' => absint( $input['google_oauth_connected_at'] ),
'google_enable_gsc' => ! empty( $raw_input['google_enable_gsc'] ),
'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ),
'google_gsc_site_url' => esc_url_raw( (string) $input['google_gsc_site_url'] ),
'google_ga4_property_id' => sanitize_text_field( (string) $input['google_ga4_property_id'] ),
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ), 'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
'image_context_mode' => $image_mode, 'image_context_mode' => $image_mode,
'image_context_limit' => $image_limit,
'context_fields' => $context_fields, 'context_fields' => $context_fields,
'modules' => $this->sanitize_modules_settings( 'modules' => $this->sanitize_modules_settings(
$modules_posted ? $raw_input['modules'] : [], $modules_posted ? $raw_input['modules'] : [],
@@ -136,28 +171,28 @@ class Groq_AI_Settings_Manager {
if ( null === $this->context_field_definitions ) { if ( null === $this->context_field_definitions ) {
$this->context_field_definitions = [ $this->context_field_definitions = [
'title' => [ 'title' => [
'label' => __( 'Producttitel', 'groq-ai-product-text' ), 'label' => __( 'Producttitel', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voeg de huidige producttitel toe als context.', 'groq-ai-product-text' ), 'description' => __( 'Voeg de huidige producttitel toe als context.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => true, 'default' => true,
], ],
'short_description' => [ 'short_description' => [
'label' => __( 'Korte beschrijving', 'groq-ai-product-text' ), 'label' => __( 'Korte beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', 'groq-ai-product-text' ), 'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => true, 'default' => true,
], ],
'description' => [ 'description' => [
'label' => __( 'Volledige beschrijving', 'groq-ai-product-text' ), 'label' => __( 'Volledige beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', 'groq-ai-product-text' ), 'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => true, 'default' => true,
], ],
'attributes' => [ 'attributes' => [
'label' => __( 'Attributen', 'groq-ai-product-text' ), 'label' => __( 'Attributen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', 'groq-ai-product-text' ), 'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => false, 'default' => false,
], ],
'images' => [ 'images' => [
'label' => __( 'Afbeeldingen', 'groq-ai-product-text' ), 'label' => __( 'Afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', 'groq-ai-product-text' ), 'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => false, 'default' => false,
], ],
]; ];
@@ -268,6 +303,16 @@ class Groq_AI_Settings_Manager {
return in_array( $mode, $allowed_modes, true ) ? $mode : 'url'; return in_array( $mode, $allowed_modes, true ) ? $mode : 'url';
} }
public function get_image_context_limit( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
$limit = isset( $settings['image_context_limit'] ) ? $settings['image_context_limit'] : 3;
return $this->sanitize_image_context_limit_value( $limit );
}
public function is_response_format_compat_enabled( $settings = null ) { public function is_response_format_compat_enabled( $settings = null ) {
if ( null === $settings ) { if ( null === $settings ) {
$settings = $this->all(); $settings = $this->all();
@@ -341,4 +386,14 @@ class Groq_AI_Settings_Manager {
return $result; return $result;
} }
private function sanitize_image_context_limit_value( $value ) {
$limit = absint( $value );
if ( $limit <= 0 ) {
$limit = 1;
}
return min( 10, $limit );
}
} }

0
languages/.gitkeep Normal file
View File