5 Commits

Author SHA1 Message Date
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
cf7ee6b86e fix: Update version to 1.2.2 in groq-ai-product-text.php and ensure plugin data is loaded in SitiWebUpdater 2025-12-11 21:23:31 +00:00
1d19b36493 fix: Update version to 1.2.1 in groq-ai-product-text.php and refactor version retrieval in SitiWebUpdater 2025-12-11 21:13:25 +00:00
732c7ad393 feat: Update plugin to version 1.2.0 with new image context features
- Added support for image context in product prompts, allowing images to be sent as URLs or Base64.
- Introduced a new settings page for managing prompt configurations.
- Implemented caching for allowed models per provider to enhance performance.
- Enhanced logging to include image context usage details.
- Added model exclusions management to prevent the use of specific models.
- Updated AJAX controller to handle image context in requests.
- Refactored prompt builder to support image context in prompts.
2025-12-11 20:01:46 +00:00
0a605cf165 Bump version to 1.1.1 and remove unused error handler code 2025-12-05 23:57:37 +00:00
19 changed files with 1214 additions and 211 deletions

View File

@@ -22,12 +22,17 @@ class SitiWebUpdater {
$this->file = $file;
$this->set_plugin_properties();
add_action( 'admin_init', array( $this, 'set_plugin_properties' ) );
return $this;
}
public function set_plugin_properties() {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$this->plugin = get_plugin_data( $this->file );
$this->basename = plugin_basename( $this->file );
$this->active = is_plugin_active( $this->basename );
@@ -66,6 +71,14 @@ class SitiWebUpdater {
}
}
private function get_latest_version_from_response() {
if ( empty( $this->github_response['tag_name'] ) ) {
return null;
}
return ltrim( $this->github_response['tag_name'], 'vV' );
}
public function initialize() {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'modify_transient' ), 10, 1 );
add_filter( 'plugins_api', array( $this, 'plugin_popup' ), 10, 3);
@@ -88,7 +101,13 @@ class SitiWebUpdater {
$this->get_repository_info(); // Get the repo info
$out_of_date = version_compare( $this->github_response['tag_name'], $checked[ $this->basename ], 'gt' ); // Check if we're out of date
$latest_version = $this->get_latest_version_from_response();
if ( null === $latest_version ) {
return $transient;
}
$out_of_date = version_compare( $latest_version, $checked[ $this->basename ], 'gt' ); // Check if we're out of date
if( $out_of_date ) {
@@ -100,7 +119,7 @@ class SitiWebUpdater {
'url' => $this->plugin["PluginURI"],
'slug' => $slug,
'package' => $new_files,
'new_version' => $this->github_response['tag_name']
'new_version' => $latest_version
);
$transient->response[$this->basename] = (object) $plugin; // Return it in response
@@ -118,6 +137,11 @@ class SitiWebUpdater {
if( $args->slug == current( explode( '/' , $this->basename ) ) ) { // And it's our slug
$this->get_repository_info(); // Get our repo info
$latest_version = $this->get_latest_version_from_response();
if ( null === $latest_version ) {
return $result;
}
// Set it to an array
$plugin = array(
@@ -129,7 +153,7 @@ class SitiWebUpdater {
'num_ratings' => '10823',
'downloaded' => '14249',
'added' => '2016-01-05',
'version' => $this->github_response['tag_name'],
'version' => $latest_version,
'author' => $this->plugin["AuthorName"],
'author_profile' => $this->plugin["AuthorURI"],
'last_updated' => $this->github_response['published_at'],

View File

@@ -123,6 +123,36 @@
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 {
margin-top: 16px;
}

View File

@@ -28,6 +28,8 @@
rankMathAction: field.getAttribute('data-rankmath-action') || '',
status: field.querySelector('.groq-ai-apply-status') || 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);
}
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'groq-ai-product-text') : '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 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.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.';
function toggleLoading(isLoading) {
modal.classList.toggle('is-loading', isLoading);
@@ -120,6 +122,7 @@
jsonCopyButton.disabled = true;
}
resetFieldStatuses();
clearTitleSuggestions();
fetch(GroqAIGenerator.ajaxUrl, {
method: 'POST',
@@ -142,6 +145,7 @@
entry.textarea.value = fields[key] || '';
}
});
updateTitleSuggestions(fields.title_suggestions);
resultField.textContent = (json.data.raw || '').trim();
resultWrapper.hidden = false;
if (jsonCopyButton) {
@@ -317,6 +321,8 @@
return ['textarea[name="rank_math_description"]'];
case 'focus_keywords':
return ['input[name="rank_math_focus_keyword"]'];
case 'slug':
return ['#post_name', 'input[name="post_name"]', '#new-post-slug'];
default:
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() {
const defaults = GroqAIGenerator.contextDefaults || {};
contextToggles.forEach((toggle) => {

View File

@@ -18,6 +18,7 @@
const modelSelect = document.getElementById('groq-ai-model-select');
const refreshButton = document.getElementById('groq-ai-refresh-models');
const refreshStatus = document.getElementById('groq-ai-refresh-models-status');
const excludedModels = data.excludedModels || {};
let currentModelValue = (modelSelect && modelSelect.dataset.currentModel) || data.currentModel || '';
function toggleProviderRows() {
@@ -42,6 +43,32 @@
});
}
function isModelAllowed(model, providerOverride) {
if (!model) {
return true;
}
const provider = providerOverride || (providerSelect ? providerSelect.value : data.currentProvider);
if (!provider || !excludedModels[provider]) {
return true;
}
return excludedModels[provider].indexOf(model) === -1;
}
function ensureCurrentModelAllowed(providerOverride) {
if (!currentModelValue) {
return;
}
if (!isModelAllowed(currentModelValue, providerOverride)) {
currentModelValue = '';
if (modelSelect) {
modelSelect.dataset.currentModel = '';
}
}
}
function buildModelOptions() {
if (!modelSelect || !data.providers) {
return;
@@ -53,6 +80,8 @@
return;
}
ensureCurrentModelAllowed(provider);
const models = Array.isArray(providerData.models) ? providerData.models : [];
const frag = document.createDocumentFragment();
const placeholder = document.createElement('option');
@@ -63,6 +92,9 @@
let hasCurrent = false;
models.forEach(function (model) {
if (!isModelAllowed(model, provider)) {
return;
}
const option = document.createElement('option');
option.value = model;
option.textContent = model;
@@ -72,7 +104,7 @@
frag.appendChild(option);
});
if (currentModelValue && !hasCurrent) {
if (currentModelValue && !hasCurrent && isModelAllowed(currentModelValue, provider)) {
const extraOption = document.createElement('option');
extraOption.value = currentModelValue;
extraOption.textContent = currentModelValue;

View File

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

View File

@@ -2,8 +2,10 @@
/**
* Plugin Name: SitiAI Product Teksten
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
* Version: 1.1.0
* Version: 1.3.0
* Author: SitiAI
* Text Domain: siti-ai-product-content-generator
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -27,29 +29,20 @@ if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_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 ) {
define( 'GROQ_AI_DEBUG_TRACE_ADDED', true );
set_error_handler(
function ( $errno, $errstr, $errfile, $errline ) {
$target_lines = [
'/wp-includes/functions.php:7291',
'/wp-includes/functions.php:2187',
];
foreach ( $target_lines as $needle ) {
if ( false !== strpos( $errfile . ':' . $errline, $needle ) ) {
error_log( '[GroqAI Debug] ' . $errstr . ' | Stack: ' . wp_debug_backtrace_summary( null, 0, true ) );
break;
}
}
return false;
}
);
}
require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php';
require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-abstract-openai-provider.php';
@@ -78,6 +71,10 @@ $updater->initialize();
final class Groq_AI_Product_Text_Plugin {
const OPTION_KEY = 'groq_ai_product_text_settings';
const CONVERSATION_OPTION_KEY = 'groq_ai_product_text_conversations';
const MODELS_CACHE_OPTION_KEY = 'groq_ai_product_text_models';
/** @var bool */
private $textdomain_loaded = false;
private static $instance = null;
@@ -107,10 +104,36 @@ final class Groq_AI_Product_Text_Plugin {
$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 );
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( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] );
}
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() {
$this->container = new Groq_AI_Service_Container();
@@ -214,7 +237,7 @@ final class Groq_AI_Product_Text_Plugin {
?>
<div class="notice notice-error">
<p>
<?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
</div>
<?php
@@ -224,15 +247,15 @@ final class Groq_AI_Product_Text_Plugin {
$parts = [];
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'] ) ) {
$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 ) ) {
return __( 'Nog geen promptinformatie opgeslagen.', 'groq-ai-product-text' );
return __( 'Nog geen promptinformatie opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
}
return implode( "\n\n", $parts );
@@ -278,6 +301,14 @@ final class Groq_AI_Product_Text_Plugin {
return $this->get_settings_manager()->is_response_format_compat_enabled( $settings );
}
public function get_image_context_mode( $settings = null ) {
return $this->get_settings_manager()->get_image_context_mode( $settings );
}
public function get_image_context_limit( $settings = null ) {
return $this->get_settings_manager()->get_image_context_limit( $settings );
}
public function should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) {
return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
}
@@ -307,7 +338,78 @@ final class Groq_AI_Product_Text_Plugin {
}
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
return ! empty( $settings['model'] ) ? $settings['model'] : $provider->get_default_model();
$provider_key = $provider->get_key();
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
if ( '' === $model ) {
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
if ( '' !== $default ) {
return $default;
}
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
if ( ! empty( $available ) ) {
return $available[0];
}
}
return $model;
}
public function get_cached_models_for_provider( $provider ) {
$provider = sanitize_key( (string) $provider );
$cache = $this->get_models_cache();
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
}
public function update_cached_models_for_provider( $provider, $models ) {
$provider = sanitize_key( (string) $provider );
$models = $this->sanitize_models_list( $models );
$cache = $this->get_models_cache();
$cache[ $provider ] = $models;
update_option( self::MODELS_CACHE_OPTION_KEY, $cache );
return $models;
}
private function get_models_cache() {
$cache = get_option( self::MODELS_CACHE_OPTION_KEY, [] );
if ( ! is_array( $cache ) ) {
$cache = [];
}
foreach ( $cache as $provider => $models ) {
$cache[ $provider ] = $this->sanitize_models_list( $models );
}
return $cache;
}
private function sanitize_models_list( $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
$models = array_values( array_unique( $models ) );
if ( ! empty( $models ) ) {
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
}
return $models;
}
public function log_debug( $message, $context = [] ) {

View File

@@ -30,13 +30,13 @@ class Groq_AI_Logs_Table extends WP_List_Table {
public function get_columns() {
return [
'created_at' => __( 'Datum', 'groq-ai-product-text' ),
'user_id' => __( 'Gebruiker', 'groq-ai-product-text' ),
'post_title' => __( 'Product', 'groq-ai-product-text' ),
'provider' => __( 'Provider', 'groq-ai-product-text' ),
'model' => __( 'Model', 'groq-ai-product-text' ),
'status' => __( 'Status', 'groq-ai-product-text' ),
'tokens_total' => __( 'Tokens', 'groq-ai-product-text' ),
'created_at' => __( 'Datum', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'user_id' => __( 'Gebruiker', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'post_title' => __( 'Product', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'provider' => __( 'Provider', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'model' => __( 'Model', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'status' => __( 'Status', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'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'] ) {
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'] );
return $link ? sprintf( '<a href="%s">%s</a>', esc_url( $link ), esc_html( $title ) ) : esc_html( $title );
case 'user_id':
@@ -131,11 +131,12 @@ class Groq_AI_Logs_Table extends WP_List_Table {
}
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 ) {
$date = esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
$usage = $this->get_usage_meta( $item );
$payload = [
'created_at' => $item['created_at'],
'user' => $this->column_default( $item, 'user_id' ),
@@ -149,6 +150,7 @@ class Groq_AI_Logs_Table extends WP_List_Table {
'prompt' => $item['prompt'],
'response' => $item['response'],
'error_message' => $item['error_message'],
'image_context' => isset( $usage['image_context'] ) ? $usage['image_context'] : null,
];
$encoded = esc_attr( wp_json_encode( $payload ) );
return sprintf(
@@ -157,4 +159,14 @@ class Groq_AI_Logs_Table extends WP_List_Table {
$date
);
}
private function get_usage_meta( $item ) {
if ( empty( $item['usage_json'] ) ) {
return [];
}
$data = json_decode( $item['usage_json'], true );
return is_array( $data ) ? $data : [];
}
}

View File

@@ -14,7 +14,7 @@ class Groq_AI_Product_Text_Product_UI {
public function register_meta_box() {
add_meta_box(
'groq-ai-generator-box',
__( 'Gebruik AI', 'groq-ai-product-text' ),
__( 'Gebruik AI', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_meta_box' ],
'product',
'side',
@@ -24,14 +24,14 @@ class Groq_AI_Product_Text_Product_UI {
public function render_meta_box() {
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;
}
?>
<p><?php esc_html_e( 'Laat de geselecteerde AI een concepttekst genereren op basis van een prompt.', 'groq-ai-product-text' ); ?></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>
<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_DOMAIN ); ?></button>
<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>
<?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 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">
<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">
<label for="groq-ai-prompt" class="screen-reader-text"><?php esc_html_e( 'Prompt', 'groq-ai-product-text' ); ?></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>
<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_DOMAIN ); ?>"></textarea>
<div class="groq-ai-modal__actions">
<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>
</div>
<div class="groq-ai-advanced-settings">
<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>
<?php esc_html_e( 'Geavanceerde instellingen', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Geavanceerde instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</button>
<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>
<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>
<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_DOMAIN ); ?></p>
<div class="groq-ai-context-options__grid">
<?php
$context_definitions = $this->plugin->get_context_field_definitions();
@@ -126,81 +126,97 @@ class Groq_AI_Product_Text_Product_UI {
</div>
</form>
<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-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' ); ?></strong>
<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' ); ?></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 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="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="slug" data-target-input="#slug" data-label="<?php esc_attr_e( 'Productslug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?></strong>
<strong><?php esc_html_e( 'Productslug', 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="short_description"><?php esc_html_e( 'Kopieer', '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' ); ?></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="slug"><?php esc_html_e( 'Vul slug in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="1"></textarea>
</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_DOMAIN ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Korte beschrijving', 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="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_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="3"></textarea>
</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">
<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">
<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 groq-ai-apply-field" data-field="description"><?php esc_html_e( 'Vul beschrijving in', '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_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="6"></textarea>
</div>
<?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">
<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">
<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 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 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_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="2"></textarea>
</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">
<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">
<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 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 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_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="3"></textarea>
</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">
<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">
<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 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 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_DOMAIN ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</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>
<?php endif; ?>
</div>
<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>
<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 class="groq-ai-modal__status" aria-live="polite"></div>

View File

@@ -16,8 +16,8 @@ class Groq_AI_Product_Text_Settings_Page {
public function register_settings_pages() {
add_options_page(
__( 'Siti AI Productteksten', 'groq-ai-product-text' ),
__( 'Siti AI', 'groq-ai-product-text' ),
__( 'Siti AI Productteksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text',
[ $this, 'render_settings_page' ]
@@ -25,8 +25,8 @@ class Groq_AI_Product_Text_Settings_Page {
add_submenu_page(
'options-general.php',
__( 'Siti AI Modules', 'groq-ai-product-text' ),
__( 'Siti AI Modules', 'groq-ai-product-text' ),
__( 'Siti AI Modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI Modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-modules',
[ $this, 'render_modules_page' ]
@@ -34,13 +34,22 @@ class Groq_AI_Product_Text_Settings_Page {
add_submenu_page(
'options-general.php',
__( 'Siti AI AI-logboek', 'groq-ai-product-text' ),
__( 'Siti AI AI-logboek', 'groq-ai-product-text' ),
__( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-logs',
[ $this, 'render_logs_page' ]
);
add_submenu_page(
'options-general.php',
__( 'Siti AI Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-prompts',
[ $this, 'render_prompt_settings_page' ]
);
}
public function hide_menu_links() {
@@ -50,7 +59,8 @@ class Groq_AI_Product_Text_Settings_Page {
?>
<style>
#adminmenu a[href="options-general.php?page=groq-ai-product-text-modules"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-logs"] {
#adminmenu a[href="options-general.php?page=groq-ai-product-text-logs"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-prompts"] {
display: none !important;
}
</style>
@@ -62,14 +72,14 @@ class Groq_AI_Product_Text_Settings_Page {
add_settings_section(
'groq_ai_product_text_general',
__( 'Algemene instellingen', 'groq-ai-product-text' ),
__( 'Algemene instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'__return_false',
'groq-ai-product-text'
);
add_settings_field(
'groq_ai_provider',
__( 'AI-aanbieder', 'groq-ai-product-text' ),
__( 'AI-aanbieder', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_provider_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
@@ -77,7 +87,7 @@ class Groq_AI_Product_Text_Settings_Page {
add_settings_field(
'groq_ai_model',
__( 'Model', 'groq-ai-product-text' ),
__( 'Model', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_model_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
@@ -86,7 +96,7 @@ class Groq_AI_Product_Text_Settings_Page {
foreach ( $this->provider_manager->get_providers() as $provider ) {
add_settings_field(
'groq_ai_api_key_' . $provider->get_key(),
sprintf( __( '%s API-sleutel', 'groq-ai-product-text' ), $provider->get_label() ),
sprintf( __( '%s API-sleutel', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $provider->get_label() ),
[ $this, 'render_provider_api_key_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general',
@@ -96,54 +106,114 @@ class Groq_AI_Product_Text_Settings_Page {
);
}
add_settings_section(
'groq_ai_product_text_prompts',
__( 'Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'__return_false',
'groq-ai-product-text-prompts'
);
add_settings_field(
'groq_ai_store_context',
__( 'Winkelcontext', 'groq-ai-product-text' ),
__( 'Winkelcontext', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_store_context_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_default_prompt',
__( 'Standaard prompt', 'groq-ai-product-text' ),
__( 'Standaard prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_default_prompt_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_context_fields',
__( 'Standaard productcontext', 'groq-ai-product-text' ),
__( 'Standaard productcontext', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_context_fields_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_response_format_compat',
__( 'Response-format compatibiliteit', 'groq-ai-product-text' ),
__( 'Response-format compatibiliteit', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_response_format_compat_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_image_context_mode',
__( 'Afbeeldingen toevoegen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_image_context_mode_field' ],
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_image_context_limit',
__( 'Maximaal aantal afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_image_context_limit_field' ],
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_section(
'groq_ai_product_text_modules_rankmath',
__( 'Rank Math SEO', 'groq-ai-product-text' ),
__( 'Rank Math SEO', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'__return_false',
'groq-ai-product-text-modules'
);
add_settings_field(
'groq_ai_module_rankmath',
__( 'Rank Math SEO', 'groq-ai-product-text' ),
__( 'Rank Math SEO', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_rankmath_module_field' ],
'groq-ai-product-text-modules',
'groq_ai_product_text_modules_rankmath'
);
}
public function render_image_context_mode_field() {
$settings = $this->plugin->get_settings();
$mode = isset( $settings['image_context_mode'] ) ? $settings['image_context_mode'] : 'url';
$options = [
'none' => __( 'Nee, geen afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'url' => __( 'Ja, voeg afbeeldings-URLs toe aan de prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'base64' => __( 'Ja, verstuur afbeeldingen als Base64 (indien ondersteund)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
];
?>
<select name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[image_context_mode]">
<?php foreach ( $options as $value => $label ) : ?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $mode, $value ); ?>><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
<p class="description">
<?php esc_html_e( 'Bepaal hoe productafbeeldingen worden meegestuurd: helemaal niet, als URLs in de prompt of als Base64-bijlagen voor modellen die beeldcontext ondersteunen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<?php
}
public function render_image_context_limit_field() {
$settings = $this->plugin->get_settings();
$limit = $this->plugin->get_image_context_limit( $settings );
?>
<input type="number"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[image_context_limit]"
min="1"
max="10"
step="1"
value="<?php echo esc_attr( $limit ); ?>"
class="small-text" />
<p class="description">
<?php esc_html_e( 'Stel hier het maximum aantal productafbeeldingen in dat wordt meegestuurd (we beginnen bij de uitgelichte afbeelding, gevolgd door de galerij).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<?php
}
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
@@ -152,16 +222,19 @@ class Groq_AI_Product_Text_Settings_Page {
$settings = $this->plugin->get_settings();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Siti AI Productteksten', 'groq-ai-product-text' ); ?></h1>
<h1><?php esc_html_e( 'Siti AI Productteksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p style="margin-bottom:16px;">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=groq-ai-product-text-prompts' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=groq-ai-product-text-modules' ) ); ?>" class="button button-secondary">
<?php esc_html_e( 'Ga naar modules', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Ga naar modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=groq-ai-product-text-logs' ) ); ?>" class="button">
<?php esc_html_e( 'Bekijk AI-logboek', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Bekijk AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</a>
</p>
<p><?php esc_html_e( 'Kies je AI-aanbieder, stel de juiste API-sleutel en het gewenste model in en beheer optionele winkelcontext of standaard prompt.', 'groq-ai-product-text' ); ?></p>
<p><?php esc_html_e( 'Kies je AI-aanbieder, stel de juiste API-sleutel en het gewenste model in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
@@ -169,11 +242,6 @@ class Groq_AI_Product_Text_Settings_Page {
submit_button();
?>
</form>
<div class="groq-ai-prompt-helper">
<h2><?php esc_html_e( 'Prompt generator', 'groq-ai-product-text' ); ?></h2>
<p><?php esc_html_e( 'Gebruik deze velden om belangrijke informatie voor de AI bij te houden (bijvoorbeeld tone of voice, USPs of doelgroepen). Voeg ze toe aan je prompt met kopiëren en plakken.', 'groq-ai-product-text' ); ?></p>
<textarea class="large-text" rows="6" readonly><?php echo esc_textarea( $this->plugin->build_prompt_template_preview( $settings ) ); ?></textarea>
</div>
</div>
<?php
}
@@ -185,8 +253,8 @@ class Groq_AI_Product_Text_Settings_Page {
?>
<div class="wrap">
<h1><?php esc_html_e( 'Siti AI Modules', 'groq-ai-product-text' ); ?></h1>
<p><?php esc_html_e( 'Beheer aparte integraties zoals Rank Math. Het uitschakelen van een module verwijdert de bijbehorende AI-uitvoer automatisch uit de productmodal.', 'groq-ai-product-text' ); ?></p>
<h1><?php esc_html_e( 'Siti AI Modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p><?php esc_html_e( 'Beheer aparte integraties zoals Rank Math. Het uitschakelen van een module verwijdert de bijbehorende AI-uitvoer automatisch uit de productmodal.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
@@ -198,6 +266,37 @@ class Groq_AI_Product_Text_Settings_Page {
<?php
}
public function render_prompt_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->plugin->get_settings();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p style="margin-bottom:16px;">
<a href="<?php echo esc_url( admin_url( 'options-general.php?page=groq-ai-product-text' ) ); ?>" class="button button-secondary">
<?php esc_html_e( 'Terug naar algemene instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</a>
</p>
<p><?php esc_html_e( 'Beheer hier de winkelcontext, standaardprompt, productcontext en response-format instellingen. Deze keuzes bepalen hoe elke prompt richting de AI wordt opgebouwd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
do_settings_sections( 'groq-ai-product-text-prompts' );
submit_button();
?>
</form>
<div class="groq-ai-prompt-helper">
<h2><?php esc_html_e( 'Prompt generator', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<p><?php esc_html_e( 'Gebruik deze velden om belangrijke informatie voor de AI bij te houden (bijvoorbeeld tone of voice, USPs of doelgroepen). Voeg ze toe aan je prompt met kopiëren en plakken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<textarea class="large-text" rows="6" readonly><?php echo esc_textarea( $this->plugin->build_prompt_template_preview( $settings ) ); ?></textarea>
</div>
</div>
<?php
}
public function render_logs_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
@@ -207,43 +306,57 @@ class Groq_AI_Product_Text_Settings_Page {
$logs_table->prepare_items();
?>
<div class="wrap">
<h1><?php esc_html_e( 'AI-logboek', 'groq-ai-product-text' ); ?></h1>
<p><?php esc_html_e( 'Bekijk recente AI-generaties inclusief status, gebruiker en tokens.', 'groq-ai-product-text' ); ?></p>
<h1><?php esc_html_e( 'AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p><?php esc_html_e( 'Bekijk recente AI-generaties inclusief status, gebruiker en tokens.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<form method="get">
<input type="hidden" name="page" value="groq-ai-product-text-logs" />
<?php $logs_table->search_box( __( 'Zoek logs', 'groq-ai-product-text' ), 'groq-ai-logs' ); ?>
<?php $logs_table->search_box( __( 'Zoek logs', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'groq-ai-logs' ); ?>
<?php $logs_table->display(); ?>
</form>
</div>
<div id="groq-ai-log-modal" class="groq-ai-log-modal" aria-hidden="true">
<div class="groq-ai-log-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="groq-ai-log-modal-title">
<button type="button" class="groq-ai-log-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', 'groq-ai-product-text' ); ?>">&times;</button>
<button type="button" class="groq-ai-log-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">&times;</button>
<div class="groq-ai-log-modal__content">
<h2 id="groq-ai-log-modal-title"><?php esc_html_e( 'Logdetails', 'groq-ai-product-text' ); ?></h2>
<h2 id="groq-ai-log-modal-title"><?php esc_html_e( 'Logdetails', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<p class="description groq-ai-log-meta"></p>
<div class="groq-ai-log-fields">
<label>
<span><?php esc_html_e( 'Prompt', 'groq-ai-product-text' ); ?></span>
<span><?php esc_html_e( 'Prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></span>
<textarea id="groq-ai-log-prompt" readonly rows="6"></textarea>
</label>
<label>
<span><?php esc_html_e( 'Response', 'groq-ai-product-text' ); ?></span>
<span><?php esc_html_e( 'Response', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></span>
<textarea id="groq-ai-log-response" readonly rows="6"></textarea>
</label>
<div class="groq-ai-log-tokens">
<div>
<strong><?php esc_html_e( 'Tokens prompt', 'groq-ai-product-text' ); ?></strong>
<strong><?php esc_html_e( 'Tokens prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<span id="groq-ai-log-tokens-prompt">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Tokens response', 'groq-ai-product-text' ); ?></strong>
<strong><?php esc_html_e( 'Tokens response', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<span id="groq-ai-log-tokens-completion">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Tokens totaal', 'groq-ai-product-text' ); ?></strong>
<strong><?php esc_html_e( 'Tokens totaal', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<span id="groq-ai-log-tokens-total">—</span>
</div>
</div>
<div class="groq-ai-log-images">
<div>
<strong><?php esc_html_e( 'Afbeeldingsmodus', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<span id="groq-ai-log-images-mode">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Beschikbare afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<span id="groq-ai-log-images-available">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Base64 meegestuurd', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
<span id="groq-ai-log-images-base64">—</span>
</div>
</div>
</div>
</div>
</div>
@@ -256,6 +369,7 @@ class Groq_AI_Product_Text_Settings_Page {
.groq-ai-log-fields label{display:block;margin-bottom:15px;}
.groq-ai-log-fields textarea{width:100%;}
.groq-ai-log-tokens{display:flex;gap:20px;margin-top:10px;}
.groq-ai-log-images{display:flex;gap:20px;margin-top:10px;}
.groq-ai-log-row{display:inline-block;}
</style>
<script>
@@ -268,6 +382,9 @@ class Groq_AI_Product_Text_Settings_Page {
const tokensPrompt=document.getElementById('groq-ai-log-tokens-prompt');
const tokensCompletion=document.getElementById('groq-ai-log-tokens-completion');
const tokensTotal=document.getElementById('groq-ai-log-tokens-total');
const imagesMode=document.getElementById('groq-ai-log-images-mode');
const imagesAvailable=document.getElementById('groq-ai-log-images-available');
const imagesBase64=document.getElementById('groq-ai-log-images-base64');
const meta=document.querySelector('.groq-ai-log-meta');
function openModal(data){
if(!data){return;}
@@ -276,6 +393,22 @@ class Groq_AI_Product_Text_Settings_Page {
if(tokensPrompt){tokensPrompt.textContent=Number.isFinite(data.tokens_prompt)?data.tokens_prompt:'—';}
if(tokensCompletion){tokensCompletion.textContent=Number.isFinite(data.tokens_completion)?data.tokens_completion:'—';}
if(tokensTotal){tokensTotal.textContent=Number.isFinite(data.tokens_total)?data.tokens_total:'—';}
const imageContext=data.image_context||null;
if(imagesMode){
let mode='—';
if(imageContext){
mode=imageContext.effective_mode||imageContext.requested_mode||'—';
}
imagesMode.textContent=mode||'—';
}
if(imagesAvailable){
const available=imageContext&&Number.isFinite(imageContext.available)?imageContext.available:'—';
imagesAvailable.textContent=available;
}
if(imagesBase64){
const base64=imageContext&&Number.isFinite(imageContext.base64_sent)?imageContext.base64_sent:'—';
imagesBase64.textContent=base64;
}
if(meta){
meta.textContent=(data.provider||'')+' • '+(data.model||'')+' • '+(data.post_title||'')+' • '+(data.status||'');
}
@@ -328,7 +461,7 @@ class Groq_AI_Product_Text_Settings_Page {
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Bepaal welke AI-dienst wordt aangesproken wanneer je teksten genereert.', 'groq-ai-product-text' ); ?></p>
<p class="description"><?php esc_html_e( 'Bepaal welke AI-dienst wordt aangesproken wanneer je teksten genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php
}
@@ -344,11 +477,11 @@ class Groq_AI_Product_Text_Settings_Page {
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[model]"
data-current-model="<?php echo esc_attr( $current_model ); ?>"
>
<option value=""><?php esc_html_e( 'Selecteer een model via "Live modellen ophalen"', 'groq-ai-product-text' ); ?></option>
<option value=""><?php esc_html_e( 'Selecteer een model via "Live modellen ophalen"', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></option>
</select>
<p class="description"><?php esc_html_e( 'Gebruik de knop hieronder om rechtstreeks via het API-endpoint beschikbare modellen op te halen. Zonder een live lijst blijft de selectie leeg.', 'groq-ai-product-text' ); ?></p>
<p class="description"><?php esc_html_e( 'Gebruik de knop hieronder om rechtstreeks via het API-endpoint beschikbare modellen op te halen. Zonder een live lijst blijft de selectie leeg.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<button type="button" class="button" id="groq-ai-refresh-models" style="margin-top:10px;">
<?php esc_html_e( 'Live modellen ophalen', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Live modellen ophalen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</button>
<p id="groq-ai-refresh-models-status" class="description" aria-live="polite"></p>
</div>
@@ -368,7 +501,7 @@ class Groq_AI_Product_Text_Settings_Page {
<?php
printf(
/* translators: %s: provider name */
esc_html__( 'Voeg hier de API-sleutel voor %s toe.', 'groq-ai-product-text' ),
esc_html__( 'Voeg hier de API-sleutel voor %s toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
esc_html( $provider->get_label() )
);
?>
@@ -381,15 +514,15 @@ class Groq_AI_Product_Text_Settings_Page {
$settings = $this->plugin->get_settings();
?>
<textarea name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[store_context]" class="large-text" rows="4"><?php echo esc_textarea( $settings['store_context'] ); ?></textarea>
<p class="description"><?php esc_html_e( 'Beschrijf het merk, de tone of voice en andere relevante winkelinformatie.', 'groq-ai-product-text' ); ?></p>
<p class="description"><?php esc_html_e( 'Beschrijf het merk, de tone of voice en andere relevante winkelinformatie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php
}
public function render_default_prompt_field() {
$settings = $this->plugin->get_settings();
?>
<textarea name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[default_prompt]" class="large-text" rows="4" placeholder="<?php esc_attr_e( 'Bijvoorbeeld: Schrijf een overtuigende productbeschrijving met nadruk op kwaliteit en levertijd.', 'groq-ai-product-text' ); ?>"><?php echo esc_textarea( $settings['default_prompt'] ); ?></textarea>
<p class="description"><?php esc_html_e( 'Deze tekst verschijnt vooraf ingevuld in de AI-popup, maar kan per product worden aangepast.', 'groq-ai-product-text' ); ?></p>
<textarea name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[default_prompt]" class="large-text" rows="4" placeholder="<?php esc_attr_e( 'Bijvoorbeeld: Schrijf een overtuigende productbeschrijving met nadruk op kwaliteit en levertijd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>"><?php echo esc_textarea( $settings['default_prompt'] ); ?></textarea>
<p class="description"><?php esc_html_e( 'Deze tekst verschijnt vooraf ingevuld in de AI-popup, maar kan per product worden aangepast.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php
}
@@ -422,10 +555,10 @@ class Groq_AI_Product_Text_Settings_Page {
?>
<label>
<input type="checkbox" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[response_format_compat]" value="1" <?php checked( $is_enabled ); ?> />
<?php esc_html_e( 'Compatibele modus inschakelen (instructies toevoegen aan de prompt).', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Compatibele modus inschakelen (instructies toevoegen aan de prompt).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Standaard gebruikt de plugin de response_format-functie van aanbieders zoals Groq en OpenAI voor gegarandeerde JSON-uitvoer. Schakel deze optie alleen in wanneer je problemen ervaart met oudere modellen of eigen integraties die deze functie niet ondersteunen.', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Standaard gebruikt de plugin de response_format-functie van aanbieders zoals Groq en OpenAI voor gegarandeerde JSON-uitvoer. Schakel deze optie alleen in wanneer je problemen ervaart met oudere modellen of eigen integraties die deze functie niet ondersteunen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<?php
}
@@ -449,19 +582,19 @@ class Groq_AI_Product_Text_Settings_Page {
<input type="hidden" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][enabled]" value="0" />
<label>
<input type="checkbox" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][enabled]" value="1" <?php checked( $enabled ); ?> <?php disabled( ! $rankmath_active ); ?> />
<?php esc_html_e( 'Activeer Rank Math integratie (meta title, meta description en focus keywords genereren).', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Activeer Rank Math integratie (meta title, meta description en focus keywords genereren).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</label>
<p class="description" style="margin-top:4px;">
<?php
if ( ! $rankmath_active ) {
esc_html_e( 'Installeer en activeer Rank Math om deze opties te gebruiken. Velden zijn momenteel alleen-lezen.', 'groq-ai-product-text' );
esc_html_e( 'Installeer en activeer Rank Math om deze opties te gebruiken. Velden zijn momenteel alleen-lezen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} else {
esc_html_e( 'Wanneer ingeschakeld worden extra velden in de AI-modal getoond en automatisch gekoppeld aan Rank Math.', 'groq-ai-product-text' );
esc_html_e( 'Wanneer ingeschakeld worden extra velden in de AI-modal getoond en automatisch gekoppeld aan Rank Math.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
}
?>
</p>
<label for="groq-ai-rankmath-keywords">
<?php esc_html_e( 'Aantal focus keywords om te genereren', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Aantal focus keywords om te genereren', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</label>
<input
type="number"
@@ -474,10 +607,10 @@ class Groq_AI_Product_Text_Settings_Page {
<?php disabled( ! $rankmath_active ); ?>
/>
<p class="description">
<?php esc_html_e( 'Bepaal hoeveel zoekwoorden de AI maximaal mag teruggeven (bijvoorbeeld 3).', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Bepaal hoeveel zoekwoorden de AI maximaal mag teruggeven (bijvoorbeeld 3).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<label for="groq-ai-rankmath-title-pixels">
<?php esc_html_e( 'Maximale meta title breedte (pixels)', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Maximale meta title breedte (pixels)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</label>
<input
type="number"
@@ -491,10 +624,10 @@ class Groq_AI_Product_Text_Settings_Page {
<?php disabled( ! $rankmath_active ); ?>
/>
<p class="description">
<?php esc_html_e( 'Bepaal hoe breed (in pixels) de meta title maximaal mag zijn volgens de SERP-richtlijnen.', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Bepaal hoe breed (in pixels) de meta title maximaal mag zijn volgens de SERP-richtlijnen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<label for="groq-ai-rankmath-pixels">
<?php esc_html_e( 'Maximale meta description breedte (pixels)', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Maximale meta description breedte (pixels)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</label>
<input
type="number"
@@ -508,14 +641,14 @@ class Groq_AI_Product_Text_Settings_Page {
<?php disabled( ! $rankmath_active ); ?>
/>
<p class="description">
<?php esc_html_e( 'Gebruik het SERP-voorbeeld als referentie. De AI krijgt door dat de meta description deze pixelbreedte niet mag overschrijden.', 'groq-ai-product-text' ); ?>
<?php esc_html_e( 'Gebruik het SERP-voorbeeld als referentie. De AI krijgt door dat de meta description deze pixelbreedte niet mag overschrijden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
</div>
<?php
}
public function enqueue_settings_assets( $hook ) {
if ( ! in_array( $hook, [ 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules' ], true ) ) {
if ( ! in_array( $hook, [ 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules', 'settings_page_groq-ai-product-text-prompts' ], true ) ) {
return;
}
@@ -550,15 +683,19 @@ class Groq_AI_Product_Text_Settings_Page {
'providerRows' => [],
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'refreshNonce' => wp_create_nonce( 'groq_ai_refresh_models' ),
'excludedModels' => Groq_AI_Model_Exclusions::get_all(),
'placeholders' => [
'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', 'groq-ai-product-text' ),
'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
];
foreach ( $this->provider_manager->get_providers() as $provider ) {
$provider_key = $provider->get_key();
$cached_models = $this->plugin->get_cached_models_for_provider( $provider_key );
$cached_models = Groq_AI_Model_Exclusions::filter_models( $provider_key, $cached_models );
$data['providers'][ $provider->get_key() ] = [
'default_label' => sprintf( __( 'Gebruik standaardmodel (%s)', 'groq-ai-product-text' ), $provider->get_default_model() ),
'models' => [],
'default_label' => sprintf( __( 'Gebruik standaardmodel (%s)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $provider->get_default_model() ),
'models' => $cached_models,
'supports_live' => $provider->supports_live_models(),
];
$data['providerRows'][ $provider->get_key() ] = 'groq_ai_api_key_' . $provider->get_key();

View File

@@ -18,4 +18,6 @@ interface Groq_AI_Provider_Interface {
public function fetch_live_models( $api_key );
public function supports_response_format();
public function supports_image_context();
}

View File

@@ -13,7 +13,7 @@ class Groq_AI_Ajax_Controller {
public function handle_generate_text() {
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' );
@@ -36,9 +36,47 @@ class Groq_AI_Ajax_Controller {
$system_prompt = $prompt_builder->build_system_prompt( $settings, $conversation_id );
$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 );
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields );
$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 ) {
$context_fields['images'] = false;
}
$image_context_enabled = ! empty( $context_fields['images'] );
$use_base64_payloads = $image_context_enabled && 'base64' === $image_context_mode && $provider->supports_image_context();
$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';
if ( $image_context_enabled ) {
if ( $use_base64_payloads ) {
$prompt_image_mode = 'base64';
} else {
$prompt_image_mode = 'url';
}
if ( 'base64' === $image_context_mode && ! $provider->supports_image_context() ) {
$prompt_image_mode = 'url';
}
}
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode, $image_context_limit );
$image_context_payloads = [];
if ( $use_base64_payloads ) {
$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 );
$image_context_meta = [
'requested_mode' => $image_context_mode,
'effective_mode' => $prompt_image_mode,
'limit' => $image_context_limit,
'available' => $total_image_count,
'used' => $image_context_count,
'base64_sent' => $use_base64_payloads ? count( $image_context_payloads ) : 0,
];
$response_format = null;
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
if ( $use_response_format ) {
@@ -57,6 +95,7 @@ class Groq_AI_Ajax_Controller {
'temperature' => 0.7,
'conversation_id' => $conversation_id,
'response_format' => $response_format,
'image_context' => $image_context_payloads,
]
);
@@ -67,7 +106,9 @@ class Groq_AI_Ajax_Controller {
'model' => $model,
'prompt' => $final_prompt,
'response' => '',
'usage' => [],
'usage' => [
'image_context' => $image_context_meta,
],
'post_id' => $post_id,
'status' => 'error',
'error_message' => $result->get_error_message(),
@@ -78,6 +119,10 @@ class Groq_AI_Ajax_Controller {
$response_text = $this->extract_content_text( $result );
$response_usage = is_array( $result ) && isset( $result['usage'] ) ? $result['usage'] : [];
if ( ! is_array( $response_usage ) ) {
$response_usage = [];
}
$response_usage['image_context'] = $image_context_meta;
$response = $prompt_builder->parse_structured_response( $response_text, $settings );
@@ -119,7 +164,7 @@ class Groq_AI_Ajax_Controller {
public function handle_refresh_models() {
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' );
@@ -128,13 +173,13 @@ class Groq_AI_Ajax_Controller {
$api_key = isset( $_POST['apiKey'] ) ? sanitize_text_field( wp_unslash( $_POST['apiKey'] ) ) : '';
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 );
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 );
@@ -143,7 +188,10 @@ class Groq_AI_Ajax_Controller {
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
}
wp_send_json_success( [ 'models' => array_values( array_unique( $result ) ) ] );
$models = Groq_AI_Model_Exclusions::filter_models( $provider_key, array_values( array_unique( $result ) ) );
$models = $this->plugin->update_cached_models_for_provider( $provider_key, $models );
wp_send_json_success( [ 'models' => $models ] );
}
private function extract_content_text( $result ) {

View File

@@ -0,0 +1,155 @@
<?php
final class Groq_AI_Model_Exclusions {
private const DEFAULT_EXCLUSIONS = [
'groq' => [
'playai-tts-arabic',
'moonshotai/kimi-k2-instruct',
'meta-llama/llama-prompt-guard-2-22m',
'groq/compound-mini',
'meta-llama/llama-guard-4-12b',
'openai/gpt-oss-20b',
'groq/compound',
'openai/gpt-oss-safeguard-20b',
'whisper-large-v3-turbo',
'meta-llama/llama-4-scout-17b-16e-instruct',
'allam-2-7b',
'playai-tts',
'moonshotai/kimi-k2-instruct-0905',
'whisper-large-v3',
'meta-llama/llama-prompt-guard-2-86m',
],
'openai' => [],
'google' => [
'embedding-gecko-001',
'embedding-001',
'text-embedding-004',
'gemini-embedding-exp-03-07',
'gemini-embedding-exp',
'gemini-embedding-001',
'gemini-2.5-flash-image-preview',
'gemini-2.5-flash-image',
'gemini-2.5-flash-preview-tts',
'gemini-2.5-pro-preview-tts',
'gemini-2.5-flash-native-audio-latest',
'gemini-2.5-flash-native-audio-preview-09-2025',
'gemini-2.5-computer-use-preview-10-2025',
'gemini-3-pro-image-preview',
'nano-banana-pro-preview',
'gemini-robotics-er-1.5-preview',
'deep-research-pro-preview-12-2025',
'aqa',
'imagen-4.0-generate-preview-06-06',
'imagen-4.0-ultra-generate-preview-06-06',
'imagen-4.0-generate-001',
'imagen-4.0-ultra-generate-001',
'imagen-4.0-fast-generate-001',
'veo-2.0-generate-001',
'veo-3.0-generate-001',
'veo-3.0-fast-generate-001',
'veo-3.1-generate-preview',
'veo-3.1-fast-generate-preview',
],
];
/**
* Geeft de volledige lijst met uitgesloten modellen terug, gegroepeerd per aanbieder.
*
* @return array<string, string[]>
*/
public static function get_all() {
$list = apply_filters( 'groq_ai_model_exclusions', self::DEFAULT_EXCLUSIONS );
if ( ! is_array( $list ) ) {
$list = [];
}
$normalized = [];
foreach ( $list as $provider => $models ) {
$normalized[ self::normalize_provider( $provider ) ] = self::normalize_models_list( $models );
}
return $normalized;
}
/**
* @param string $provider
* @return string[]
*/
public static function get_for_provider( $provider ) {
$provider = self::normalize_provider( $provider );
$list = self::get_all();
return isset( $list[ $provider ] ) ? $list[ $provider ] : [];
}
public static function is_excluded( $provider, $model ) {
if ( '' === $model ) {
return false;
}
$model = sanitize_text_field( $model );
$provider = self::normalize_provider( $provider );
$list = self::get_for_provider( $provider );
return in_array( $model, $list, true );
}
/**
* @param string $provider
* @param string $model
* @return string
*/
public static function ensure_allowed( $provider, $model ) {
if ( self::is_excluded( $provider, $model ) ) {
return '';
}
return $model;
}
/**
* @param string $provider
* @param array $models
* @return array
*/
public static function filter_models( $provider, $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$provider = self::normalize_provider( $provider );
return array_values(
array_filter(
array_map(
'sanitize_text_field',
$models
),
function ( $model ) use ( $provider ) {
return ! self::is_excluded( $provider, $model );
}
)
);
}
private static function normalize_provider( $provider ) {
return sanitize_key( (string) $provider );
}
private static function normalize_models_list( $models ) {
if ( ! is_array( $models ) ) {
$models = [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
return array_values( array_unique( $models ) );
}
}

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 ) {
$endpoint = $this->get_models_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(
@@ -41,7 +41,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
}
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 = [];
@@ -52,7 +52,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
}
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;
@@ -66,7 +66,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
$api_key = $this->get_api_key( $settings );
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 = [
@@ -116,7 +116,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
if ( empty( $body['choices'][0]['message']['content'] ) ) {
return new WP_Error(
'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() )
);
}
@@ -138,4 +138,8 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
$field = $this->get_option_key();
return isset( $settings[ $field ] ) ? $settings[ $field ] : '';
}
public function supports_image_context() {
return false;
}
}

View File

@@ -6,7 +6,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
}
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() {
@@ -33,6 +33,10 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
return false;
}
public function supports_image_context() {
return true;
}
public function fetch_live_models( $api_key ) {
$endpoint = add_query_arg(
[ 'key' => $api_key, 'pageSize' => 100 ],
@@ -57,7 +61,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
}
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 = [];
@@ -69,7 +73,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
}
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;
@@ -83,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() ] : '';
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(
@@ -92,15 +96,59 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
sprintf( 'https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent', rawurlencode( $model ) )
);
$image_context = isset( $args['image_context'] ) && is_array( $args['image_context'] ) ? $args['image_context'] : [];
$parts = [];
if ( '' !== trim( (string) $system_prompt ) ) {
$parts[] = [
'text' => $system_prompt,
];
}
if ( '' !== trim( (string) $prompt ) ) {
$parts[] = [
'text' => $prompt,
];
}
if ( ! empty( $image_context ) ) {
foreach ( $image_context as $image ) {
if ( empty( $image['data'] ) ) {
continue;
}
$label = isset( $image['label'] ) ? trim( (string) $image['label'] ) : '';
if ( '' !== $label ) {
$parts[] = [
'text' => sprintf(
/* translators: %s: image label */
__( 'Contextafbeelding: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$label
),
];
}
$parts[] = [
'inline_data' => [
'mime_type' => ! empty( $image['mime_type'] ) ? $image['mime_type'] : 'image/jpeg',
'data' => $image['data'],
],
];
}
}
if ( empty( $parts ) ) {
$parts[] = [
'text' => $prompt,
];
}
$payload = [
'contents' => [
[
'role' => 'user',
'parts' => [
[
'text' => $system_prompt . "\n\n" . $prompt,
],
],
'parts' => $parts,
],
],
'generationConfig' => [
@@ -133,7 +181,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
if ( empty( $body['candidates'][0]['content']['parts'] ) ) {
return new WP_Error(
'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() {
return __( 'Groq', 'groq-ai-product-text' );
return __( 'Groq', GROQ_AI_PRODUCT_TEXT_DOMAIN );
}
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() {
return __( 'OpenAI', 'groq-ai-product-text' );
return __( 'OpenAI', GROQ_AI_PRODUCT_TEXT_DOMAIN );
}
public function get_default_model() {

View File

@@ -13,17 +13,17 @@ class Groq_AI_Prompt_Builder {
public function build_system_prompt( $settings, $conversation_id ) {
$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 ) {
$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
);
}
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
);
@@ -46,7 +46,7 @@ class Groq_AI_Prompt_Builder {
public function parse_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' ) );
return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$clean = trim( $raw );
@@ -58,7 +58,7 @@ class Groq_AI_Prompt_Builder {
$decoded = json_decode( $clean, true );
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 = [
@@ -67,6 +67,40 @@ class Groq_AI_Prompt_Builder {
'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 ) ) {
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
$focus_keywords = [];
@@ -98,8 +132,22 @@ class Groq_AI_Prompt_Builder {
$fields['focus_keywords'] = implode( ', ', $focus_keywords );
}
if ( implode( '', $fields ) === '' ) {
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen bruikbare velden.', 'groq-ai-product-text' ) );
$primary_values = [
$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;
@@ -125,7 +173,7 @@ class Groq_AI_Prompt_Builder {
return $normalized;
}
public function build_product_context_block( $post_id, $fields ) {
public function build_product_context_block( $post_id, $fields, $image_mode = 'url', $image_limit = 3 ) {
$post_id = absint( $post_id );
if ( ! $post_id ) {
@@ -137,28 +185,35 @@ class Groq_AI_Prompt_Builder {
if ( ! empty( $fields['title'] ) ) {
$title = get_the_title( $post_id );
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'] ) ) {
$excerpt = get_post_field( 'post_excerpt', $post_id );
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'] ) ) {
$content = get_post_field( 'post_content', $post_id );
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'] ) ) {
$attributes = $this->get_product_attributes_text( $post_id );
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 ) {
$images = $this->get_product_images_text( $post_id, $image_limit );
if ( $images ) {
$parts[] = sprintf( __( 'Afbeeldingen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $images );
}
}
@@ -172,7 +227,7 @@ class Groq_AI_Prompt_Builder {
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;
}
@@ -184,19 +239,36 @@ class Groq_AI_Prompt_Builder {
$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' ),
'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' ),
'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' ),
'description' => __( 'Uitgebreide HTML-productbeschrijving met paragrafen en eventueel lijsten.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 20,
],
];
@@ -206,7 +278,7 @@ class Groq_AI_Prompt_Builder {
'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' ),
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
60,
$title_pixels
),
@@ -216,7 +288,7 @@ class Groq_AI_Prompt_Builder {
'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' ),
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
160,
$desc_pixels
),
@@ -224,7 +296,7 @@ class Groq_AI_Prompt_Builder {
];
$properties['focus_keywords'] = [
'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 ),
'items' => [
'type' => 'string',
@@ -236,7 +308,7 @@ class Groq_AI_Prompt_Builder {
$schema = [
'type' => 'object',
'properties' => $properties,
'required' => [ 'title', 'short_description', 'description' ],
'required' => [ 'title_suggestions', 'title', 'slug', 'short_description', 'description' ],
'additionalProperties' => false,
];
@@ -251,7 +323,9 @@ class Groq_AI_Prompt_Builder {
private function get_structured_response_instructions( $settings = null ) {
$schema_parts = [
'"title_suggestions":["...","...","..."]',
'"title":"..."',
'"slug":"..."',
'"short_description":"..."',
'"description":"..."',
];
@@ -267,7 +341,7 @@ class Groq_AI_Prompt_Builder {
$instruction = sprintf(
/* 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
);
@@ -277,14 +351,16 @@ class Groq_AI_Prompt_Builder {
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
$instruction .= ' ' . sprintf(
/* 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,
$title_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;
}
@@ -350,4 +426,155 @@ class Groq_AI_Prompt_Builder {
return implode( '; ', $lines );
}
private function get_product_images_text( $post_id, $limit = 3 ) {
$limit = max( 0, (int) $limit );
$image_ids = $this->get_product_image_ids( $post_id );
if ( $limit > 0 ) {
$image_ids = array_slice( $image_ids, 0, $limit );
}
if ( empty( $image_ids ) ) {
return '';
}
$entries = [];
foreach ( $image_ids as $index => $attachment_id ) {
$descriptor = $this->describe_product_image( $attachment_id, $index + 1 );
if ( ! $descriptor ) {
continue;
}
$entries[] = sprintf( '%s - %s', $descriptor['label'], $descriptor['url'] );
}
return implode( '; ', array_filter( $entries ) );
}
public function get_product_image_payloads( $post_id, $limit = 3, $max_filesize = 1572864 ) {
$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 ) ) {
return [];
}
$payloads = [];
foreach ( $image_ids as $index => $attachment_id ) {
$descriptor = $this->describe_product_image( $attachment_id, $index + 1 );
if ( ! $descriptor || empty( $descriptor['path'] ) ) {
continue;
}
$path = $descriptor['path'];
if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
continue;
}
$filesize = filesize( $path );
if ( false !== $filesize && $filesize > $max_filesize ) {
continue;
}
$data = @file_get_contents( $path );
if ( false === $data ) {
continue;
}
$payloads[] = [
'attachment_id' => $attachment_id,
'label' => $descriptor['label'],
'mime_type' => $descriptor['mime_type'],
'data' => base64_encode( $data ),
'url' => $descriptor['url'],
];
}
return $payloads;
}
public function get_product_image_count( $post_id ) {
return count( $this->get_product_image_ids( $post_id ) );
}
private function get_product_image_ids( $post_id ) {
$post_id = absint( $post_id );
if ( ! $post_id ) {
return [];
}
$image_ids = [];
$featured_id = get_post_thumbnail_id( $post_id );
if ( $featured_id ) {
$image_ids[] = $featured_id;
}
$gallery_ids = [];
if ( function_exists( 'wc_get_product' ) ) {
$product = wc_get_product( $post_id );
if ( $product ) {
$gallery_ids = (array) $product->get_gallery_image_ids();
}
}
if ( empty( $gallery_ids ) ) {
$raw_gallery = get_post_meta( $post_id, '_product_image_gallery', true );
if ( is_string( $raw_gallery ) && '' !== trim( $raw_gallery ) ) {
$gallery_ids = array_filter( array_map( 'absint', explode( ',', $raw_gallery ) ) );
}
}
if ( ! empty( $gallery_ids ) ) {
$image_ids = array_merge( $image_ids, $gallery_ids );
}
return array_values( array_unique( array_filter( array_map( 'absint', $image_ids ) ) ) );
}
private function describe_product_image( $attachment_id, $position ) {
$url = wp_get_attachment_url( $attachment_id );
if ( ! $url ) {
return null;
}
$label = trim( (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) );
if ( '' === $label ) {
$label = get_the_title( $attachment_id );
}
$label = trim( wp_strip_all_tags( (string) $label ) );
if ( '' === $label ) {
$label = sprintf( __( 'Afbeelding %d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $position );
}
$path = get_attached_file( $attachment_id );
$mime = get_post_mime_type( $attachment_id );
if ( ! $mime && $path ) {
$mime = wp_get_image_mime( $path );
}
if ( $mime && 0 !== strpos( $mime, 'image/' ) ) {
$mime = '';
}
return [
'attachment_id' => $attachment_id,
'label' => $label,
'url' => esc_url_raw( $url ),
'path' => $path,
'mime_type' => $mime ? $mime : 'image/jpeg',
];
}
}

View File

@@ -37,6 +37,8 @@ class Groq_AI_Settings_Manager {
'google_api_key' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url',
'image_context_limit' => 3,
'response_format_compat' => false,
];
@@ -44,6 +46,22 @@ class Groq_AI_Settings_Manager {
$settings = wp_parse_args( (array) $settings, $defaults );
$settings['context_fields'] = $this->normalize_context_fields( isset( $settings['context_fields'] ) ? $settings['context_fields'] : [] );
$settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] );
$settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' );
$image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
if ( 'none' === $image_mode ) {
$settings['context_fields']['images'] = false;
$settings['image_context_mode'] = 'none';
} elseif ( in_array( $image_mode, [ 'url', 'base64' ], true ) ) {
$settings['context_fields']['images'] = true;
$settings['image_context_mode'] = $image_mode;
} else {
$settings['context_fields']['images'] = true;
$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;
}
@@ -65,6 +83,8 @@ class Groq_AI_Settings_Manager {
'google_api_key' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url',
'image_context_limit' => 3,
'response_format_compat' => false,
];
@@ -80,16 +100,37 @@ class Groq_AI_Settings_Manager {
$provider = 'groq';
}
$model = sanitize_text_field( $input['model'] );
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider, $model );
$image_mode = isset( $input['image_context_mode'] ) ? sanitize_text_field( $input['image_context_mode'] ) : $defaults['image_context_mode'];
$allowed_modes = [ 'none', 'base64', 'url' ];
if ( ! in_array( $image_mode, $allowed_modes, true ) ) {
$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'] );
if ( 'none' === $image_mode ) {
$context_fields['images'] = false;
} else {
$context_fields['images'] = true;
}
return [
'provider' => $provider,
'model' => sanitize_text_field( $input['model'] ),
'model' => $model,
'store_context' => sanitize_textarea_field( $input['store_context'] ),
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
'google_api_key' => sanitize_text_field( $input['google_api_key'] ),
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
'context_fields' => $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] ),
'image_context_mode' => $image_mode,
'image_context_limit' => $image_limit,
'context_fields' => $context_fields,
'modules' => $this->sanitize_modules_settings(
$modules_posted ? $raw_input['modules'] : [],
$defaults['modules'],
@@ -103,23 +144,28 @@ class Groq_AI_Settings_Manager {
if ( null === $this->context_field_definitions ) {
$this->context_field_definitions = [
'title' => [
'label' => __( 'Producttitel', 'groq-ai-product-text' ),
'description' => __( 'Voeg de huidige producttitel toe als context.', 'groq-ai-product-text' ),
'label' => __( 'Producttitel', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voeg de huidige producttitel toe als context.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => true,
],
'short_description' => [
'label' => __( 'Korte beschrijving', 'groq-ai-product-text' ),
'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', 'groq-ai-product-text' ),
'label' => __( 'Korte beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => true,
],
'description' => [
'label' => __( 'Volledige beschrijving', 'groq-ai-product-text' ),
'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', 'groq-ai-product-text' ),
'label' => __( 'Volledige beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => true,
],
'attributes' => [
'label' => __( 'Attributen', 'groq-ai-product-text' ),
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', 'groq-ai-product-text' ),
'label' => __( 'Attributen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => false,
],
'images' => [
'label' => __( 'Afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => false,
],
];
@@ -219,6 +265,27 @@ class Groq_AI_Settings_Manager {
return max( 200, min( 2000, $value ) );
}
public function get_image_context_mode( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
$mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
$allowed_modes = [ 'none', 'base64', '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 ) {
if ( null === $settings ) {
$settings = $this->all();
@@ -292,4 +359,14 @@ class Groq_AI_Settings_Manager {
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