1 Commits

6 changed files with 194 additions and 27 deletions

View File

@@ -47,11 +47,18 @@
if (applyButton) {
applyButton.addEventListener('click', () => {
const descriptionField = document.getElementById('description');
if (!descriptionField || !outputField) {
const bottomDescriptionField = document.getElementById('groq-ai-term-bottom-description');
if (!outputField) {
return;
}
descriptionField.value = outputField.value || '';
setStatus('Tekst ingevuld in het beschrijving-veld. Vergeet niet op "Opslaan" te klikken.', 'success');
if (bottomDescriptionField) {
bottomDescriptionField.value = outputField.value || '';
} else if (descriptionField) {
descriptionField.value = outputField.value || '';
}
setStatus('Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', 'success');
});
}
@@ -69,6 +76,10 @@
rawField.textContent = '';
}
if (outputField) {
outputField.value = '';
}
fetch(GroqAITermGenerator.ajaxUrl, {
method: 'POST',
headers: {
@@ -84,7 +95,8 @@
}
if (outputField) {
outputField.value = (json.data && json.data.description ? json.data.description : '').trim();
const text = json.data && json.data.description ? json.data.description : '';
outputField.value = String(text).trim();
}
if (rawField) {
rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();

View File

@@ -2,7 +2,7 @@
/**
* Plugin Name: SitiAI Product Teksten
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
* Version: 1.4.2
* Version: 1.4.3
* Author: SitiAI
* Text Domain: siti-ai-product-content-generator
* Domain Path: /languages

View File

@@ -319,6 +319,12 @@ class Groq_AI_Product_Text_Settings_Page {
$term_label = ( 'product_cat' === $taxonomy ) ? __( 'Categorie', GROQ_AI_PRODUCT_TEXT_DOMAIN ) : __( 'Term', GROQ_AI_PRODUCT_TEXT_DOMAIN );
$word_count = $this->count_words( $term->description );
$meta_prompt = get_term_meta( $term_id, 'groq_ai_term_custom_prompt', true );
$settings = $this->plugin->get_settings();
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings );
$bottom_description = '';
if ( '' !== $bottom_meta_key ) {
$bottom_description = (string) get_term_meta( $term_id, $bottom_meta_key, true );
}
$default_prompt = (string) $meta_prompt;
if ( '' === trim( $default_prompt ) ) {
$default_prompt = __( 'Schrijf een SEO-vriendelijke categorieomschrijving in het Nederlands. Gebruik duidelijke tussenkoppen en <p>-tags. Voeg geen prijsinformatie toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
@@ -354,6 +360,23 @@ class Groq_AI_Product_Text_Settings_Page {
<p class="description"><?php esc_html_e( 'Dit is de standaard WordPress term-omschrijving (wordt o.a. gebruikt op categorie/merk paginas).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<?php if ( '' !== $bottom_meta_key ) : ?>
<tr>
<th scope="row"><label for="groq-ai-term-bottom-description"><?php esc_html_e( 'Omschrijving (onderaan)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea name="groq_ai_term_bottom_description" id="groq-ai-term-bottom-description" rows="8" class="large-text"><?php echo esc_textarea( (string) $bottom_description ); ?></textarea>
<p class="description">
<?php
printf(
/* translators: %s: meta key */
esc_html__( 'Dit veld wordt opgeslagen in term meta (%s) en wordt onderaan de pagina getoond via LiveBetter customfields.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
esc_html( $bottom_meta_key )
);
?>
</p>
</td>
</tr>
<?php endif; ?>
<tr>
<th scope="row"><label for="groq-ai-term-custom-prompt"><?php esc_html_e( 'Prompt (optioneel, per term)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
@@ -384,10 +407,18 @@ class Groq_AI_Product_Text_Settings_Page {
<textarea id="groq-ai-term-prompt" class="large-text" rows="5"><?php echo esc_textarea( $default_prompt ); ?></textarea>
<p>
<button type="submit" class="button button-primary"><?php esc_html_e( 'Genereer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button" id="groq-ai-term-apply"><?php esc_html_e( 'Zet in omschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button" id="groq-ai-term-apply">
<?php
if ( '' !== $bottom_meta_key ) {
esc_html_e( 'Zet in onderaan-veld', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} else {
esc_html_e( 'Zet in omschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN );
}
?>
</button>
</p>
<div id="groq-ai-term-status" class="description" aria-live="polite"></div>
<h3><?php esc_html_e( 'Gegenereerde tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<h3><?php esc_html_e( 'Gegenereerde tekst (onderaan)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<textarea id="groq-ai-term-generated" class="large-text" rows="10"></textarea>
<h3><?php esc_html_e( 'Ruwe JSON-output', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<pre id="groq-ai-term-raw" style="background:#fff;border:1px solid #ddd;padding:12px;max-height:240px;overflow:auto;"></pre>
@@ -396,6 +427,17 @@ class Groq_AI_Product_Text_Settings_Page {
<?php
}
private function resolve_term_bottom_description_meta_key( $term, $settings ) {
$default_key = '';
if ( is_array( $settings ) && isset( $settings['term_bottom_description_meta_key'] ) ) {
$default_key = sanitize_key( (string) $settings['term_bottom_description_meta_key'] );
}
$key = apply_filters( 'groq_ai_term_bottom_description_meta_key', $default_key, $term, $settings );
$key = sanitize_key( (string) $key );
return $key;
}
public function handle_save_term_content() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Geen toestemming.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
@@ -406,6 +448,7 @@ class Groq_AI_Product_Text_Settings_Page {
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
$term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0;
$description = isset( $_POST['description'] ) ? wp_kses_post( wp_unslash( $_POST['description'] ) ) : '';
$bottom_description = isset( $_POST['groq_ai_term_bottom_description'] ) ? wp_kses_post( wp_unslash( $_POST['groq_ai_term_bottom_description'] ) ) : '';
$custom_prompt = isset( $_POST['groq_ai_term_custom_prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_term_custom_prompt'] ) ) : '';
if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
@@ -423,6 +466,14 @@ class Groq_AI_Product_Text_Settings_Page {
if ( ! is_wp_error( $result ) ) {
update_term_meta( $term_id, 'groq_ai_term_custom_prompt', $custom_prompt );
$settings = $this->plugin->get_settings();
$term = get_term( $term_id, $taxonomy );
if ( $term && ! is_wp_error( $term ) ) {
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings );
if ( '' !== $bottom_meta_key ) {
update_term_meta( $term_id, $bottom_meta_key, $bottom_description );
}
}
}
wp_safe_redirect( $this->get_term_page_url( $taxonomy, $term_id ) );
@@ -554,6 +605,14 @@ class Groq_AI_Product_Text_Settings_Page {
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_term_bottom_description_meta_key',
__( 'Term-veld (onderaan) meta key', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_term_bottom_description_meta_key_field' ],
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_context_fields',
__( 'Standaard productcontext', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
@@ -1486,6 +1545,22 @@ class Groq_AI_Product_Text_Settings_Page {
<?php
}
public function render_term_bottom_description_meta_key_field() {
$settings = $this->plugin->get_settings();
$value = isset( $settings['term_bottom_description_meta_key'] ) ? (string) $settings['term_bottom_description_meta_key'] : '';
?>
<input type="text"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[term_bottom_description_meta_key]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
placeholder="bijv. bottom_description"
/>
<p class="description">
<?php esc_html_e( 'Dit is de term meta key van het extra customfields-veld dat onderaan de categorie/merk pagina wordt getoond (LiveBetter customfields). Laat leeg om alleen de standaard term-omschrijving te gebruiken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<?php
}
public function render_context_fields_field() {
$settings = $this->plugin->get_settings();
$values = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();
@@ -1560,7 +1635,7 @@ class Groq_AI_Product_Text_Settings_Page {
type="number"
id="groq-ai-rankmath-keywords"
min="1"
max="99"
max="100"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][focus_keyword_limit]"
value="<?php echo esc_attr( $keyword_limit ); ?>"
style="width: 80px;"

View File

@@ -105,9 +105,20 @@ class Groq_AI_Ajax_Controller {
];
}
$default_bottom_key = isset( $settings['term_bottom_description_meta_key'] ) ? sanitize_key( (string) $settings['term_bottom_description_meta_key'] ) : '';
$bottom_meta_key = apply_filters( 'groq_ai_term_bottom_description_meta_key', $default_bottom_key, $term, $settings );
$bottom_meta_key = sanitize_key( (string) $bottom_meta_key );
$has_bottom_field = ( '' !== $bottom_meta_key );
$top_description = isset( $parsed['description'] ) ? (string) $parsed['description'] : '';
$bottom_description = isset( $parsed['bottom_description'] ) ? (string) $parsed['bottom_description'] : '';
$apply_text = $has_bottom_field ? ( '' !== $bottom_description ? $bottom_description : $top_description ) : $top_description;
wp_send_json_success(
[
'description' => isset( $parsed['description'] ) ? $parsed['description'] : '',
'top_description' => $top_description,
'bottom_description' => $has_bottom_field ? $apply_text : $bottom_description,
'description' => $apply_text,
'raw' => $response_text,
]
);

View File

@@ -424,6 +424,15 @@ class Groq_AI_Prompt_Builder {
$parts[] = sprintf( __( 'Huidige omschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->description ) );
}
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings );
if ( '' !== $bottom_meta_key && $term_id ) {
$bottom = (string) get_term_meta( $term_id, $bottom_meta_key, true );
$bottom = trim( wp_strip_all_tags( $bottom ) );
if ( '' !== $bottom ) {
$parts[] = sprintf( __( 'Huidige omschrijving (onderaan): %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $bottom );
}
}
if ( $include_top_products ) {
$top_products = $this->get_top_products_for_term( $taxonomy, $term_id, $top_products_limit );
if ( ! empty( $top_products ) ) {
@@ -465,13 +474,27 @@ class Groq_AI_Prompt_Builder {
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
$properties = [
'description' => [
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( null, $settings );
$use_bottom_field = ( '' !== $bottom_meta_key );
$properties = [];
$required = [];
if ( $use_bottom_field ) {
$properties['bottom_description'] = [
'type' => 'string',
'description' => __( 'HTML-omschrijving voor de categorie/term met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Uitgebreide HTML-omschrijving (helemaal onderaan) met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 20,
],
];
];
$required[] = 'bottom_description';
} else {
$properties['description'] = [
'type' => 'string',
'description' => __( 'HTML-omschrijving (WordPress term description) met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'minLength' => 20,
];
$required[] = 'description';
}
if ( $rankmath_enabled ) {
$properties['meta_title'] = [
@@ -506,7 +529,7 @@ class Groq_AI_Prompt_Builder {
$schema = [
'type' => 'object',
'properties' => $properties,
'required' => [ 'description' ],
'required' => $required,
'additionalProperties' => false,
];
@@ -549,14 +572,37 @@ class Groq_AI_Prompt_Builder {
];
}
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( null, $settings );
$use_bottom_field = ( '' !== $bottom_meta_key );
$description = isset( $decoded['description'] ) ? trim( (string) $decoded['description'] ) : '';
if ( '' === $description ) {
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen description veld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
$top = isset( $decoded['top_description'] ) ? trim( (string) $decoded['top_description'] ) : '';
$bottom = isset( $decoded['bottom_description'] ) ? trim( (string) $decoded['bottom_description'] ) : '';
if ( $use_bottom_field ) {
if ( '' === $bottom ) {
$bottom = '' !== $description ? $description : $top;
}
if ( '' === $bottom ) {
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen bottom_description veld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
} else {
if ( '' === $description ) {
$description = '' !== $top ? $top : $bottom;
}
if ( '' === $description ) {
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen description veld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
}
$result = [
'description' => $description,
];
$result = [];
if ( $use_bottom_field ) {
$result['bottom_description'] = $bottom;
// For backwards compatibility with existing UI, keep `description` alias.
$result['description'] = $bottom;
} else {
$result['description'] = $description;
}
if ( isset( $decoded['meta_title'] ) ) {
$result['meta_title'] = $this->truncate_meta_field( (string) $decoded['meta_title'], 60 );
@@ -582,9 +628,15 @@ class Groq_AI_Prompt_Builder {
}
private function get_term_structured_response_instructions( $settings = null ) {
$schema_parts = [
'"description":"..."',
];
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( null, $settings );
$use_bottom_field = ( '' !== $bottom_meta_key );
$schema_parts = [];
if ( $use_bottom_field ) {
$schema_parts[] = '"bottom_description":"..."';
} else {
$schema_parts[] = '"description":"..."';
}
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
if ( $rankmath_enabled ) {
@@ -600,11 +652,25 @@ class Groq_AI_Prompt_Builder {
$json_structure
);
$instruction .= ' ' . __( 'Zorg dat description geldige HTML bevat (gebruik minimaal <p>-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
$instruction .= ' ' . __( 'Als in de context een sectie "Interne links" staat, verwerk dan 25 van deze links natuurlijk in de description als HTML-links (<a href="URL">Anker</a>).', GROQ_AI_PRODUCT_TEXT_DOMAIN );
if ( $use_bottom_field ) {
$instruction .= ' ' . __( 'Zorg dat bottom_description geldige HTML bevat. Dit is de tekst die helemaal onderaan de pagina komt.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} else {
$instruction .= ' ' . __( 'Zorg dat description geldige HTML bevat.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
}
$instruction .= ' ' . __( 'Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
$instruction .= ' ' . __( 'Als in de context een sectie "Interne links" staat, verwerk dan 25 van deze links natuurlijk in de hoofdtekst als HTML-links (<a href="URL">Anker</a>).', GROQ_AI_PRODUCT_TEXT_DOMAIN );
return $instruction;
}
private function resolve_term_bottom_description_meta_key( $term = null, $settings = null ) {
$default_key = '';
if ( is_array( $settings ) && isset( $settings['term_bottom_description_meta_key'] ) ) {
$default_key = sanitize_key( (string) $settings['term_bottom_description_meta_key'] );
}
$key = apply_filters( 'groq_ai_term_bottom_description_meta_key', $default_key, $term, $settings );
return sanitize_key( (string) $key );
}
private function get_top_products_for_term( $taxonomy, $term_id, $limit = 10 ) {
$taxonomy = sanitize_key( (string) $taxonomy );
$term_id = absint( $term_id );

View File

@@ -33,6 +33,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '',
'default_prompt' => '',
'max_output_tokens' => 2048,
'term_bottom_description_meta_key' => '',
'groq_api_key' => '',
'openai_api_key' => '',
'google_api_key' => '',
@@ -89,6 +90,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '',
'default_prompt' => '',
'max_output_tokens' => 2048,
'term_bottom_description_meta_key' => '',
'groq_api_key' => '',
'openai_api_key' => '',
'google_api_key' => '',
@@ -149,6 +151,7 @@ class Groq_AI_Settings_Manager {
'store_context' => sanitize_textarea_field( $input['store_context'] ),
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
'max_output_tokens' => $max_output_tokens,
'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ),
'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'] ),
@@ -282,7 +285,7 @@ class Groq_AI_Settings_Manager {
$config = $this->get_module_config( 'rankmath', $settings );
$limit = isset( $config['focus_keyword_limit'] ) ? absint( $config['focus_keyword_limit'] ) : 3;
return max( 1, min( 10, $limit ) );
return max( 1, min( 100, $limit ) );
}
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
@@ -375,7 +378,7 @@ class Groq_AI_Settings_Manager {
if ( $limit <= 0 ) {
$limit = $module_default_config['focus_keyword_limit'];
}
$result[ $module_key ]['focus_keyword_limit'] = max( 1, min( 10, $limit ) );
$result[ $module_key ]['focus_keyword_limit'] = max( 1, min( 100, $limit ) );
$title_pixel_limit = isset( $raw['meta_title_pixel_limit'] ) ? absint( $raw['meta_title_pixel_limit'] ) : ( isset( $current_config['meta_title_pixel_limit'] ) ? absint( $current_config['meta_title_pixel_limit'] ) : $module_default_config['meta_title_pixel_limit'] );
if ( $title_pixel_limit <= 0 ) {