2 Commits

7 changed files with 311 additions and 9 deletions

View File

@@ -13,6 +13,7 @@
const resultField = document.getElementById('groq-ai-output');
const jsonCopyButton = modal.querySelector('.groq-ai-copy-json');
const contextToggles = modal.querySelectorAll('.groq-ai-context-toggle');
const attributeToggles = modal.querySelectorAll('.groq-ai-attribute-toggle');
const resultFields = {};
modal.querySelectorAll('.groq-ai-result-field').forEach((field) => {
const key = field.getAttribute('data-field');
@@ -60,6 +61,7 @@
promptField.value = GroqAIGenerator.defaultPrompt;
}
resetContextToggles();
resetAttributeToggles();
setTimeout(() => promptField.focus(), 50);
}
@@ -115,6 +117,7 @@
payload.append('prompt', prompt);
payload.append('post_id', GroqAIGenerator.postId || 0);
payload.append('context_fields', JSON.stringify(collectContextSelection()));
payload.append('attribute_includes', JSON.stringify(collectAttributeSelection()));
toggleLoading(true);
resultWrapper.hidden = true;
@@ -458,6 +461,20 @@
});
}
function resetAttributeToggles() {
const defaults = Array.isArray(GroqAIGenerator.attributeIncludesDefaults)
? GroqAIGenerator.attributeIncludesDefaults
: [];
attributeToggles.forEach((toggle) => {
const key = toggle.getAttribute('data-attribute');
if (!key) {
return;
}
toggle.checked = defaults.includes(key);
});
}
function collectContextSelection() {
const selected = [];
contextToggles.forEach((toggle) => {
@@ -467,4 +484,18 @@
});
return selected;
}
function collectAttributeSelection() {
const selected = [];
attributeToggles.forEach((toggle) => {
if (!toggle.checked) {
return;
}
const key = toggle.getAttribute('data-attribute');
if (key) {
selected.push(key);
}
});
return selected;
}
})(jQuery);

View File

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

View File

@@ -59,6 +59,9 @@ class Groq_AI_Product_Text_Product_UI {
$post_id = ( $post && isset( $post->ID ) ) ? (int) $post->ID : 0;
$settings = $this->plugin->get_settings();
$attribute_defaults = isset( $settings['product_attribute_includes'] ) && is_array( $settings['product_attribute_includes'] )
? array_values( array_unique( array_map( 'sanitize_key', $settings['product_attribute_includes'] ) ) )
: [];
wp_localize_script(
'groq-ai-admin',
@@ -69,6 +72,7 @@ class Groq_AI_Product_Text_Product_UI {
'defaultPrompt' => $settings['default_prompt'],
'postId' => $post_id,
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
'attributeIncludesDefaults' => $attribute_defaults,
]
);
}
@@ -82,6 +86,7 @@ class Groq_AI_Product_Text_Product_UI {
$settings = $this->plugin->get_settings();
$rankmath_enabled = $this->plugin->is_rankmath_active() && $this->plugin->is_module_enabled( 'rankmath', $settings );
$attribute_options = $this->get_product_attribute_include_options();
?>
<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">
@@ -109,6 +114,9 @@ class Groq_AI_Product_Text_Product_UI {
$context_definitions = $this->plugin->get_context_field_definitions();
$context_defaults = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();
foreach ( $context_definitions as $context_key => $context_info ) :
if ( 'attributes' === $context_key ) {
continue;
}
$checked = ! empty( $context_defaults[ $context_key ] );
?>
<label class="groq-ai-context-option">
@@ -122,6 +130,23 @@ class Groq_AI_Product_Text_Product_UI {
</label>
<?php endforeach; ?>
</div>
<h3 style="margin-top:16px;"><?php esc_html_e( 'Attributen meesturen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<p class="description"><?php esc_html_e( 'Selecteer welke productattributen je mee wilt geven aan de AI. Dit vervangt de oude alles-of-niets optie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php if ( empty( $attribute_options ) ) : ?>
<p class="description"><?php esc_html_e( 'Geen WooCommerce-attributen gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<div class="groq-ai-context-options__grid">
<?php foreach ( $attribute_options as $attr_key => $attr_label ) : ?>
<label class="groq-ai-context-option">
<input type="checkbox" class="groq-ai-attribute-toggle" data-attribute="<?php echo esc_attr( $attr_key ); ?>" />
<div>
<strong><?php echo esc_html( $attr_label ); ?></strong>
</div>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</form>
@@ -225,4 +250,39 @@ class Groq_AI_Product_Text_Product_UI {
</div>
<?php
}
private function get_product_attribute_include_options() {
$options = [
'__custom__' => __( 'Custom attributen (niet-taxonomie)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
];
if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
$taxonomies = wc_get_attribute_taxonomies();
if ( is_array( $taxonomies ) ) {
foreach ( $taxonomies as $attr ) {
$name = isset( $attr->attribute_name ) ? sanitize_key( (string) $attr->attribute_name ) : '';
$label = isset( $attr->attribute_label ) ? sanitize_text_field( (string) $attr->attribute_label ) : '';
if ( '' === $name ) {
continue;
}
$taxonomy = 'pa_' . $name;
if ( '' === $label ) {
$label = function_exists( 'wc_attribute_label' ) ? wc_attribute_label( $taxonomy ) : $taxonomy;
}
$options[ $taxonomy ] = $label;
}
}
}
if ( count( $options ) > 1 ) {
$fixed = [
'__custom__' => $options['__custom__'],
];
unset( $options['__custom__'] );
asort( $options, SORT_NATURAL | SORT_FLAG_CASE );
$options = $fixed + $options;
}
return $options;
}
}

View File

@@ -689,6 +689,14 @@ class Groq_AI_Product_Text_Settings_Page {
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_product_attribute_includes',
__( 'Productattributen meesturen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_product_attribute_includes_field' ],
'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_DOMAIN ),
@@ -1636,6 +1644,9 @@ class Groq_AI_Product_Text_Settings_Page {
?>
<div class="groq-ai-context-defaults">
<?php foreach ( $definitions as $key => $definition ) :
if ( 'attributes' === $key ) {
continue;
}
$checked = ! empty( $values[ $key ] );
?>
<label>
@@ -1652,6 +1663,74 @@ class Groq_AI_Product_Text_Settings_Page {
<?php
}
public function render_product_attribute_includes_field() {
$settings = $this->plugin->get_settings();
$values = isset( $settings['product_attribute_includes'] ) && is_array( $settings['product_attribute_includes'] )
? $settings['product_attribute_includes']
: [];
$values = array_values( array_unique( array_map( 'sanitize_key', $values ) ) );
$options = $this->get_product_attribute_include_options();
?>
<div class="groq-ai-attribute-includes">
<p class="description" style="margin-top:0;">
<?php esc_html_e( 'Selecteer welke productattributen je als context mee wilt sturen naar de AI. Als je niets selecteert, worden attributen niet meegestuurd (tenzij je dit eerder al had ingeschakeld via de oude instelling).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<?php if ( empty( $options ) ) : ?>
<p class="description">
<?php esc_html_e( 'Geen WooCommerce-attributen gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<?php else : ?>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px;">
<?php foreach ( $options as $key => $label ) :
$checked = in_array( $key, $values, true );
?>
<label style="display:flex;gap:8px;align-items:flex-start;">
<input type="checkbox" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[product_attribute_includes][]" value="<?php echo esc_attr( $key ); ?>" <?php checked( $checked ); ?> />
<span><?php echo esc_html( $label ); ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php
}
private function get_product_attribute_include_options() {
$options = [
'__custom__' => __( 'Custom attributen (niet-taxonomie)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
];
if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
$taxonomies = wc_get_attribute_taxonomies();
if ( is_array( $taxonomies ) ) {
foreach ( $taxonomies as $attr ) {
$name = isset( $attr->attribute_name ) ? sanitize_key( (string) $attr->attribute_name ) : '';
$label = isset( $attr->attribute_label ) ? sanitize_text_field( (string) $attr->attribute_label ) : '';
if ( '' === $name ) {
continue;
}
$taxonomy = 'pa_' . $name;
if ( '' === $label ) {
$label = function_exists( 'wc_attribute_label' ) ? wc_attribute_label( $taxonomy ) : $taxonomy;
}
$options[ $taxonomy ] = $label;
}
}
}
if ( count( $options ) > 1 ) {
$fixed = [
'__custom__' => $options['__custom__'],
];
unset( $options['__custom__'] );
asort( $options, SORT_NATURAL | SORT_FLAG_CASE );
$options = $fixed + $options;
}
return $options;
}
public function render_response_format_compat_field() {
$settings = $this->plugin->get_settings();
$is_enabled = ! empty( $settings['response_format_compat'] );

View File

@@ -143,6 +143,23 @@ 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 );
if ( array_key_exists( 'attribute_includes', $_POST ) ) {
$attribute_includes = [];
$attribute_raw = (string) wp_unslash( $_POST['attribute_includes'] );
$decoded = json_decode( $attribute_raw, true );
if ( is_array( $decoded ) ) {
foreach ( $decoded as $value ) {
$key = sanitize_key( (string) $value );
if ( '' === $key ) {
continue;
}
if ( in_array( $key, [ '__custom__', '__all__' ], true ) || 0 === strpos( $key, 'pa_' ) ) {
$attribute_includes[] = $key;
}
}
}
$settings['product_attribute_includes'] = array_values( array_unique( $attribute_includes ) );
}
$image_context_mode = $this->plugin->get_image_context_mode( $settings );
$image_context_limit = $this->plugin->get_image_context_limit( $settings );
@@ -168,7 +185,7 @@ class Groq_AI_Ajax_Controller {
}
}
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode, $image_context_limit );
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode, $image_context_limit, $settings );
$image_context_payloads = [];
if ( $use_base64_payloads ) {
$image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id, $image_context_limit );

View File

@@ -338,7 +338,7 @@ class Groq_AI_Prompt_Builder {
return $normalized;
}
public function build_product_context_block( $post_id, $fields, $image_mode = 'url', $image_limit = 3 ) {
public function build_product_context_block( $post_id, $fields, $image_mode = 'url', $image_limit = 3, $settings = null ) {
$post_id = absint( $post_id );
if ( ! $post_id ) {
@@ -368,8 +368,14 @@ class Groq_AI_Prompt_Builder {
}
}
if ( ! empty( $fields['attributes'] ) ) {
$attributes = $this->get_product_attributes_text( $post_id );
$attribute_includes = [];
if ( is_array( $settings ) && isset( $settings['product_attribute_includes'] ) && is_array( $settings['product_attribute_includes'] ) ) {
$attribute_includes = array_values( array_unique( array_map( 'sanitize_key', $settings['product_attribute_includes'] ) ) );
}
$include_attributes = ! empty( $attribute_includes ) || ! empty( $fields['attributes'] );
if ( $include_attributes ) {
$attributes = $this->get_product_attributes_text( $post_id, $attribute_includes );
if ( $attributes ) {
$parts[] = sprintf( __( 'Attributen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $attributes );
}
@@ -382,9 +388,66 @@ class Groq_AI_Prompt_Builder {
}
}
if ( ! empty( $fields['brands'] ) ) {
$brands_context = $this->get_product_brand_context_text( $post_id );
if ( '' !== $brands_context ) {
$parts[] = sprintf( __( 'Merken: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $brands_context );
}
}
return implode( "\n\n", array_filter( $parts ) );
}
private function get_product_brand_context_text( $post_id ) {
$post_id = absint( $post_id );
$taxonomy = $this->detect_brand_taxonomy();
if ( ! $post_id || '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
return '';
}
$terms = get_the_terms( $post_id, $taxonomy );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return '';
}
$entries = [];
foreach ( $terms as $term ) {
if ( ! $term || ! is_object( $term ) ) {
continue;
}
$name = isset( $term->name ) ? trim( wp_strip_all_tags( (string) $term->name ) ) : '';
if ( '' === $name ) {
continue;
}
$description = isset( $term->description ) ? trim( wp_strip_all_tags( (string) $term->description ) ) : '';
if ( '' !== $description ) {
$entries[] = sprintf( '%s - %s', $name, $description );
} else {
$entries[] = $name;
}
}
$entries = array_values( array_unique( array_filter( $entries ) ) );
if ( empty( $entries ) ) {
return '';
}
$context = implode( '; ', $entries );
/**
* Filters the product brand context string added to prompts.
*
* @param string $context
* @param int $post_id
* @param array $terms
* @param string $taxonomy
*/
return (string) apply_filters( 'groq_ai_product_brand_context', $context, $post_id, $terms, $taxonomy );
}
public function prepend_context_to_prompt( $prompt, $context ) {
$context = trim( (string) $context );
@@ -833,7 +896,7 @@ class Groq_AI_Prompt_Builder {
return substr( $text, 0, $limit );
}
private function get_product_attributes_text( $post_id ) {
private function get_product_attributes_text( $post_id, $attribute_includes = [] ) {
if ( ! function_exists( 'wc_get_product' ) ) {
return '';
}
@@ -850,14 +913,27 @@ class Groq_AI_Prompt_Builder {
return '';
}
$attribute_includes = is_array( $attribute_includes ) ? array_values( array_unique( array_map( 'sanitize_key', $attribute_includes ) ) ) : [];
$include_all = empty( $attribute_includes ) || in_array( '__all__', $attribute_includes, true );
$include_custom = $include_all || in_array( '__custom__', $attribute_includes, true );
$lines = [];
foreach ( $attributes as $attribute ) {
if ( $attribute->is_taxonomy() ) {
$terms = wc_get_product_terms( $post_id, $attribute->get_name(), [ 'fields' => 'names' ] );
$taxonomy_name = sanitize_key( (string) $attribute->get_name() );
if ( ! $include_all && ! in_array( $taxonomy_name, $attribute_includes, true ) ) {
continue;
}
$terms = wc_get_product_terms( $post_id, $taxonomy_name, [ 'fields' => 'names' ] );
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $terms ) );
$label = wc_attribute_label( $attribute->get_name() );
$label = wc_attribute_label( $taxonomy_name );
} else {
if ( ! $include_custom ) {
continue;
}
$options = $attribute->get_options();
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $options ) );
$label = sanitize_text_field( $attribute->get_name() );

View File

@@ -33,6 +33,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '',
'default_prompt' => '',
'max_output_tokens' => 2048,
'product_attribute_includes' => [],
'term_bottom_description_meta_key' => '',
'groq_api_key' => '',
'openai_api_key' => '',
@@ -74,6 +75,10 @@ class Groq_AI_Settings_Manager {
$limit = isset( $settings['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $settings['image_context_limit'] ) : 3;
$settings['image_context_limit'] = $limit;
$settings['product_attribute_includes'] = $this->sanitize_product_attribute_includes(
isset( $settings['product_attribute_includes'] ) ? $settings['product_attribute_includes'] : []
);
return $settings;
}
@@ -90,6 +95,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '',
'default_prompt' => '',
'max_output_tokens' => 2048,
'product_attribute_includes' => [],
'term_bottom_description_meta_key' => '',
'groq_api_key' => '',
'openai_api_key' => '',
@@ -151,6 +157,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,
'product_attribute_includes' => $this->sanitize_product_attribute_includes( isset( $raw_input['product_attribute_includes'] ) ? $raw_input['product_attribute_includes'] : [] ),
'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ),
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
@@ -177,6 +184,33 @@ class Groq_AI_Settings_Manager {
];
}
private function sanitize_product_attribute_includes( $value ) {
if ( ! is_array( $value ) ) {
return [];
}
$clean = [];
foreach ( $value as $item ) {
$item = sanitize_key( (string) $item );
if ( '' === $item ) {
continue;
}
// Allow special tokens and attribute taxonomies.
if ( in_array( $item, [ '__all__', '__custom__' ], true ) || 0 === strpos( $item, 'pa_' ) ) {
$clean[] = $item;
}
}
$clean = array_values( array_unique( $clean ) );
// Hard cap to avoid overly large option payloads.
if ( count( $clean ) > 200 ) {
$clean = array_slice( $clean, 0, 200 );
}
return $clean;
}
public function get_context_field_definitions() {
if ( null === $this->context_field_definitions ) {
$this->context_field_definitions = [
@@ -200,6 +234,11 @@ class Groq_AI_Settings_Manager {
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => false,
],
'brands' => [
'label' => __( 'Merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voegt gekoppelde productmerken toe (detecteert WooCommerce merk-taxonomieën).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'default' => true,
],
'images' => [
'label' => __( 'Afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
@@ -227,7 +266,7 @@ class Groq_AI_Settings_Manager {
$normalized = [];
foreach ( $definitions as $key => $data ) {
$normalized[ $key ] = false;
$normalized[ $key ] = ! empty( $data['default'] );
}
if ( ! is_array( $fields ) ) {