2 Commits

6 changed files with 213 additions and 3 deletions

View File

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

View File

@@ -165,6 +165,8 @@ class Groq_AI_Product_Text_Settings_Page {
[ [
'taxonomy' => 'product_cat', 'taxonomy' => 'product_cat',
'hide_empty' => false, 'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
'number' => 0, 'number' => 0,
] ]
); );
@@ -230,6 +232,8 @@ class Groq_AI_Product_Text_Settings_Page {
[ [
'taxonomy' => $taxonomy, 'taxonomy' => $taxonomy,
'hide_empty' => false, 'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
'number' => 0, 'number' => 0,
] ]
); );
@@ -542,6 +546,14 @@ class Groq_AI_Product_Text_Settings_Page {
'groq_ai_product_text_prompts' 'groq_ai_product_text_prompts'
); );
add_settings_field(
'groq_ai_max_output_tokens',
__( 'Max output tokens', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
[ $this, 'render_max_output_tokens_field' ],
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field( add_settings_field(
'groq_ai_context_fields', 'groq_ai_context_fields',
__( 'Standaard productcontext', GROQ_AI_PRODUCT_TEXT_DOMAIN ), __( 'Standaard productcontext', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
@@ -1455,6 +1467,25 @@ class Groq_AI_Product_Text_Settings_Page {
<?php <?php
} }
public function render_max_output_tokens_field() {
$settings = $this->plugin->get_settings();
$value = isset( $settings['max_output_tokens'] ) ? absint( $settings['max_output_tokens'] ) : 2048;
$value = max( 128, min( 8192, $value ) );
?>
<input type="number"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[max_output_tokens]"
min="128"
max="8192"
step="128"
value="<?php echo esc_attr( (string) $value ); ?>"
class="small-text"
/>
<p class="description">
<?php esc_html_e( 'Limiet voor lengte van het AI-antwoord. Als teksten afgekapt worden, zet dit hoger (kost vaak wel meer tokens).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</p>
<?php
}
public function render_context_fields_field() { public function render_context_fields_field() {
$settings = $this->plugin->get_settings(); $settings = $this->plugin->get_settings();
$values = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(); $values = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();

View File

@@ -80,11 +80,20 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
], ],
]; ];
$max_tokens = isset( $args['max_tokens'] ) ? absint( $args['max_tokens'] ) : 0;
if ( $max_tokens <= 0 ) {
$max_tokens = isset( $settings['max_output_tokens'] ) ? absint( $settings['max_output_tokens'] ) : 0;
}
if ( $max_tokens <= 0 ) {
$max_tokens = 2048;
}
$max_tokens = max( 128, min( 8192, $max_tokens ) );
$request_body = [ $request_body = [
'model' => $model, 'model' => $model,
'messages' => $messages, 'messages' => $messages,
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7, 'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
'max_tokens' => 1024, 'max_tokens' => $max_tokens,
]; ];
if ( ! empty( $args['response_format'] ) ) { if ( ! empty( $args['response_format'] ) ) {
@@ -122,6 +131,10 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
$content = trim( $body['choices'][0]['message']['content'] ); $content = trim( $body['choices'][0]['message']['content'] );
$usage = isset( $body['usage'] ) && is_array( $body['usage'] ) ? $body['usage'] : []; $usage = isset( $body['usage'] ) && is_array( $body['usage'] ) ? $body['usage'] : [];
$finish_reason = isset( $body['choices'][0]['finish_reason'] ) ? sanitize_text_field( (string) $body['choices'][0]['finish_reason'] ) : '';
if ( '' !== $finish_reason ) {
$usage['finish_reason'] = $finish_reason;
}
return [ return [
'content' => $content, 'content' => $content,

View File

@@ -144,6 +144,15 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
]; ];
} }
$max_tokens = isset( $args['max_tokens'] ) ? absint( $args['max_tokens'] ) : 0;
if ( $max_tokens <= 0 ) {
$max_tokens = isset( $settings['max_output_tokens'] ) ? absint( $settings['max_output_tokens'] ) : 0;
}
if ( $max_tokens <= 0 ) {
$max_tokens = 2048;
}
$max_tokens = max( 128, min( 8192, $max_tokens ) );
$payload = [ $payload = [
'contents' => [ 'contents' => [
[ [
@@ -153,7 +162,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
], ],
'generationConfig' => [ 'generationConfig' => [
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7, 'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
'maxOutputTokens' => 1024, 'maxOutputTokens' => $max_tokens,
], ],
]; ];
@@ -196,6 +205,10 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
$content = trim( implode( "\n\n", array_filter( $texts ) ) ); $content = trim( implode( "\n\n", array_filter( $texts ) ) );
$usage = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : []; $usage = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : [];
$finish_reason = isset( $body['candidates'][0]['finishReason'] ) ? sanitize_text_field( (string) $body['candidates'][0]['finishReason'] ) : '';
if ( '' !== $finish_reason ) {
$usage['finish_reason'] = $finish_reason;
}
return [ return [
'content' => $content, 'content' => $content,

View File

@@ -55,6 +55,145 @@ class Groq_AI_Prompt_Builder {
); );
} }
private function detect_brand_taxonomy() {
$candidates = [
'product_brand',
'pwb-brand',
'yith_product_brand',
'berocket_brand',
];
if ( taxonomy_exists( 'pa_brand' ) ) {
array_unshift( $candidates, 'pa_brand' );
}
$candidates = apply_filters( 'groq_ai_brand_taxonomy_candidates', $candidates );
$found = '';
foreach ( $candidates as $tax ) {
$tax = sanitize_key( (string) $tax );
if ( $tax && taxonomy_exists( $tax ) ) {
$found = $tax;
break;
}
}
$found = apply_filters( 'groq_ai_brand_taxonomy', $found );
return sanitize_key( (string) $found );
}
private function get_internal_link_suggestions( $taxonomy, $current_term_id, $limit = 10 ) {
$taxonomy = sanitize_key( (string) $taxonomy );
$current_term_id = absint( $current_term_id );
$limit = max( 0, min( 50, absint( $limit ) ) );
if ( '' === $taxonomy || $limit <= 0 || ! taxonomy_exists( $taxonomy ) ) {
return [];
}
$cache_key = 'groq_ai_internal_links_' . $taxonomy;
$cached = get_transient( $cache_key );
if ( is_array( $cached ) ) {
$all = $cached;
} else {
$terms = get_terms(
[
'taxonomy' => $taxonomy,
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
'number' => 0,
]
);
if ( is_wp_error( $terms ) ) {
$terms = [];
}
$all = [];
foreach ( (array) $terms as $t ) {
if ( ! $t || ! is_object( $t ) || empty( $t->term_id ) ) {
continue;
}
$link = get_term_link( $t );
if ( is_wp_error( $link ) || ! is_string( $link ) || '' === $link ) {
continue;
}
$name = isset( $t->name ) ? trim( wp_strip_all_tags( (string) $t->name ) ) : '';
if ( '' === $name ) {
continue;
}
$all[] = [
'term_id' => absint( $t->term_id ),
'name' => $name,
'url' => esc_url_raw( $link ),
];
}
set_transient( $cache_key, $all, HOUR_IN_SECONDS );
}
$suggestions = [];
foreach ( $all as $row ) {
if ( ! is_array( $row ) ) {
continue;
}
$tid = isset( $row['term_id'] ) ? absint( $row['term_id'] ) : 0;
if ( $current_term_id && $tid === $current_term_id ) {
continue;
}
$name = isset( $row['name'] ) ? (string) $row['name'] : '';
$url = isset( $row['url'] ) ? (string) $row['url'] : '';
if ( '' === $name || '' === $url ) {
continue;
}
$suggestions[] = [
'name' => $name,
'url' => $url,
];
if ( count( $suggestions ) >= $limit ) {
break;
}
}
return $suggestions;
}
private function build_internal_links_context( $term ) {
if ( ! $term || ! is_object( $term ) ) {
return '';
}
$current_tax = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
$current_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
$links = [];
// Categories.
if ( taxonomy_exists( 'product_cat' ) ) {
$links = array_merge( $links, $this->get_internal_link_suggestions( 'product_cat', 'product_cat' === $current_tax ? $current_id : 0, 10 ) );
}
// Brands.
$brand_tax = $this->detect_brand_taxonomy();
if ( '' !== $brand_tax ) {
$links = array_merge( $links, $this->get_internal_link_suggestions( $brand_tax, $brand_tax === $current_tax ? $current_id : 0, 10 ) );
}
if ( empty( $links ) ) {
return '';
}
$lines = [];
$lines[] = __( 'Interne links (gebruik 25 relevante links in de tekst, als HTML: <a href="URL">Anker</a>):', GROQ_AI_PRODUCT_TEXT_DOMAIN );
foreach ( $links as $link ) {
$name = isset( $link['name'] ) ? (string) $link['name'] : '';
$url = isset( $link['url'] ) ? (string) $link['url'] : '';
if ( '' === $name || '' === $url ) {
continue;
}
$lines[] = sprintf( '- %s → %s', $name, $url );
}
return implode( "\n", $lines );
}
public function append_response_instructions( $prompt, $settings ) { public function append_response_instructions( $prompt, $settings ) {
$instructions = (string) ( $this->get_structured_response_instructions( $settings ) ?? '' ); $instructions = (string) ( $this->get_structured_response_instructions( $settings ) ?? '' );
$prompt = trim( (string) $prompt ); $prompt = trim( (string) $prompt );
@@ -296,6 +435,12 @@ class Groq_AI_Prompt_Builder {
} }
} }
$internal_links = $this->build_internal_links_context( $term );
$internal_links = trim( (string) $internal_links );
if ( '' !== $internal_links ) {
$parts[] = $internal_links;
}
$google_context = apply_filters( 'groq_ai_term_google_context', '', $term, $settings ); $google_context = apply_filters( 'groq_ai_term_google_context', '', $term, $settings );
$google_context = trim( (string) $google_context ); $google_context = trim( (string) $google_context );
if ( '' !== $google_context ) { if ( '' !== $google_context ) {
@@ -456,6 +601,7 @@ class Groq_AI_Prompt_Builder {
); );
$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 .= ' ' . __( '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 );
return $instruction; return $instruction;
} }

View File

@@ -32,6 +32,7 @@ class Groq_AI_Settings_Manager {
'model' => '', 'model' => '',
'store_context' => '', 'store_context' => '',
'default_prompt' => '', 'default_prompt' => '',
'max_output_tokens' => 2048,
'groq_api_key' => '', 'groq_api_key' => '',
'openai_api_key' => '', 'openai_api_key' => '',
'google_api_key' => '', 'google_api_key' => '',
@@ -87,6 +88,7 @@ class Groq_AI_Settings_Manager {
'model' => '', 'model' => '',
'store_context' => '', 'store_context' => '',
'default_prompt' => '', 'default_prompt' => '',
'max_output_tokens' => 2048,
'groq_api_key' => '', 'groq_api_key' => '',
'openai_api_key' => '', 'openai_api_key' => '',
'google_api_key' => '', 'google_api_key' => '',
@@ -129,6 +131,10 @@ class Groq_AI_Settings_Manager {
$image_limit = isset( $input['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $input['image_context_limit'] ) : $defaults['image_context_limit']; $image_limit = isset( $input['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $input['image_context_limit'] ) : $defaults['image_context_limit'];
$max_output_tokens = isset( $input['max_output_tokens'] ) ? absint( $input['max_output_tokens'] ) : absint( $defaults['max_output_tokens'] );
// Keep within sane bounds across providers.
$max_output_tokens = max( 128, min( 8192, $max_output_tokens ) );
$context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] ); $context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] );
if ( 'none' === $image_mode ) { if ( 'none' === $image_mode ) {
@@ -142,6 +148,7 @@ class Groq_AI_Settings_Manager {
'model' => $model, 'model' => $model,
'store_context' => sanitize_textarea_field( $input['store_context'] ), 'store_context' => sanitize_textarea_field( $input['store_context'] ),
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ), 'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
'max_output_tokens' => $max_output_tokens,
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ), 'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ), 'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
'google_api_key' => sanitize_text_field( $input['google_api_key'] ), 'google_api_key' => sanitize_text_field( $input['google_api_key'] ),