Add Google and Groq AI providers, enhance provider manager, and implement conversation and logging services

- Introduced `Groq_AI_Provider_Google` and `Groq_AI_Provider_Groq` classes for handling AI interactions with Google and Groq respectively.
- Enhanced `Groq_AI_Provider_Manager` to register and manage multiple AI providers.
- Implemented `Groq_AI_Conversation_Manager` for managing conversation IDs and context hashes.
- Added `Groq_AI_Generation_Logger` for logging AI generation events and managing log tables.
- Developed `Groq_AI_Prompt_Builder` for constructing prompts and processing AI responses.
- Established `Groq_AI_Settings_Manager` for managing plugin settings, including context fields and module configurations.
This commit is contained in:
Roberto Guagliardo
2025-12-05 23:58:15 +01:00
commit 5171f93a93
26 changed files with 4040 additions and 0 deletions

102
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: Build & Release Plugin
on:
workflow_dispatch:
inputs:
release_notes:
description: 'Optionele release-opmerkingen'
required: false
push:
branches:
- main
paths:
- 'groq-ai-product-text.php'
- 'includes/**'
- 'assets/**'
- 'README.md'
- '.github/workflows/release.yml'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine plugin version
id: meta
run: |
VERSION=$(grep -E "^\s*\*\s*Version:" -m 1 groq-ai-product-text.php | sed -E 's/.*Version:\s*//')
VERSION=$(echo "$VERSION" | tr -d '\r')
if [ -z "$VERSION" ]; then
echo "::error::Kon pluginversie niet bepalen."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check if tag exists
id: tagcheck
run: |
TAG="v${{ steps.meta.outputs.version }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Tag bestaat al workflow afronden
if: steps.tagcheck.outputs.exists == 'true'
run: |
echo "Tag v${{ steps.meta.outputs.version }} bestaat al. Release wordt overgeslagen."
- name: Build distributie-zip
if: steps.tagcheck.outputs.exists == 'false'
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
SLUG="siti-ai-product-content-generator"
BUILD_ROOT="$RUNNER_TEMP/build"
DEST_DIR="$BUILD_ROOT/$SLUG"
mkdir -p "$DEST_DIR"
rsync -a ./ "$DEST_DIR" \
--exclude '.git/' \
--exclude '.github/' \
--exclude 'docker/' \
--exclude 'docs/' \
--exclude 'dist/' \
--exclude 'docker-compose.yml' \
--exclude 'PLAN.md'
mkdir -p dist
ZIP_PATH="dist/${SLUG}-${VERSION}.zip"
(cd "$BUILD_ROOT" && zip -r "$GITHUB_WORKSPACE/$ZIP_PATH" "$SLUG")
echo "asset_path=$ZIP_PATH" >> "$GITHUB_OUTPUT"
echo "asset_name=${SLUG}-${VERSION}.zip" >> "$GITHUB_OUTPUT"
- name: Stel release-body samen
if: steps.tagcheck.outputs.exists == 'false'
id: releasebody
run: |
if [ -n "$RELEASE_NOTES" ]; then
echo "text=$RELEASE_NOTES" >> "$GITHUB_OUTPUT"
else
echo "text=Automatische release op basis van versie ${{ steps.meta.outputs.version }}." >> "$GITHUB_OUTPUT"
fi
env:
RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
- name: Maak GitHub release
if: steps.tagcheck.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.meta.outputs.version }}
name: Siti AI Product Teksten v${{ steps.meta.outputs.version }}
body: ${{ steps.releasebody.outputs.text }}
generate_release_notes: true
files: ${{ steps.package.outputs.asset_path }}

20
.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# OS cruft
.DS_Store
Thumbs.db
# IDE / tooling
.idea/
.vscode/
*.code-workspace
# Dependencies & builds
node_modules/
vendor/
dist/
# Logs / env
*.log
.env*
# Docker artifacts
docker/wordpress/wp-content/

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# SitiAI Product Teksten (WordPress plugin)
Deze repository bevat de WordPress plugin waarmee productteksten via SitiAI kunnen worden gegenereerd. De plugincode leeft volledig in deze map en kan daarom veilig via git beheerd worden.
## Plugin installeren en gebruiken
### Systeemeisen
- WordPress 6.4 of hoger.
- WooCommerce (de plugin controleert dit en deactiveert zichzelf als WooCommerce ontbreekt).
- Minimaal één API-sleutel voor Groq, OpenAI of Google Gemini.
- (Optioneel) Rank Math SEO wanneer je de extra SEO-velden wilt gebruiken.
### Installatie
1. Download de nieuwste release (`siti-ai-product-content-generator-x.y.z.zip`) vanaf de [GitHub Releases](https://github.com/SitiWeb/siti-ai-product-content-generator/releases) of gebruik het zip-bestand dat door de workflow in `dist/` wordt geplaatst.
2. Ga in WordPress naar **Plugins → Nieuwe plugin → Plugin uploaden** en upload het zipbestand. Je kunt de map ook handmatig naar `wp-content/plugins/` uploaden.
3. Activeer **SitiAI Product Teksten** en controleer dat WooCommerce actief is.
### Configuratie
1. Navigeer naar **Instellingen → Siti AI**.
2. Kies een AI-aanbieder, vul de bijbehorende API-sleutel in en (optioneel) klik op **Live modellen ophalen** om beschikbare modellen te laden.
3. Stel een standaard prompt en winkelcontext in zodat het AI-venster vooraf gevuld is.
4. Selecteer welke productvelden standaard als context dienen (titel, beschrijvingen, attributen, …).
5. Gebruik de knop **Ga naar modules** om bijvoorbeeld de Rank Math integratie aan of uit te zetten en de limieten aan te passen.
6. Via **Bekijk AI-logboek** zie je alle eerdere generaties inclusief foutmeldingen of token usage.
### Productteksten genereren
1. Open een product in WooCommerce en gebruik de meta-box **Gebruik AI** om de modal te openen.
2. Vul (of hergebruik) een prompt, kies welke contextvelden meegestuurd worden en klik op **Genereer tekst**.
3. De resultaten verschijnen per veld (titel, korte beschrijving, beschrijving en indien geactiveerd Rank Math velden). Gebruik **Kopieer** of **Vul … in** om velden direct over te nemen.
4. Via de geavanceerde sectie kun je contextvelden tijdelijk uitschakelen; dit heeft alleen effect voor de huidige generatie.
5. Iedere generatie wordt opgeslagen in het AI-logboek zodat je binnen WordPress kunt terugzoeken wat er is gebeurd.
## Ontwikkelvereisten
- Docker Desktop of Docker Engine + Docker Compose v2
## Ontwikkelen in de Docker omgeving
1. Start de containers (WordPress + MariaDB + phpMyAdmin):
```bash
docker compose up --build -d
```
2. Open http://localhost:8080 om de WordPress installatie te doorlopen. Gebruik `db` als host en de volgende databasegegevens:
- database: `wordpress`
- gebruiker: `wordpress`
- wachtwoord: `wordpress`
3. Activeer in het WordPress dashboard de plugin **SitiAI Product Teksten** (deze repository wordt in de container gemount naar `wp-content/plugins/siti-ai-product-content-generator`).
### Handige commando's
- Shell in de WordPress container om bijvoorbeeld `wp` CLI of git te draaien binnen de container:
```bash
docker compose exec wordpress bash
```
- WP-CLI is al aanwezig:
```bash
docker compose exec wordpress wp plugin list
```
- Bekijk de database via phpMyAdmin op http://localhost:8081 (gebruik dezelfde DB-gebruiker/WW als hierboven).
- Containers stoppen:
```bash
docker compose down
```
## Werken met git
De pluginbestanden blijven op de host staan en worden alleen als bind-mount in de container gebruikt. Daardoor kun je git gewoon op je machine gebruiken:
```bash
git status
git add .
git commit -m "Beschrijf je wijziging"
git push origin <branch>
```
Je kunt optioneel vanuit de container git gebruiken (zelfde codepad) wanneer je liever binnen Docker werkt.
## Tips
- De databank (`db_data`) en WordPress bestanden (`wordpress_data`) worden in Docker volumes opgeslagen zodat je data behouden blijft tussen sessies.
- Wil je helemaal opnieuw beginnen? Voer `docker compose down -v` uit om de volumes te verwijderen.
## Releasen via GitHub Actions
De workflow `.github/workflows/release.yml` bouwt automatisch een distributie-zip van de plugin, maakt een git-tag (`vX.Y.Z`) op basis van de versie in `groq-ai-product-text.php` en publiceert een GitHub Release met het zipbestand als asset.
1. Werk de `Version`-header in `groq-ai-product-text.php` bij en commit de wijzigingen.
2. Push naar `main` of start handmatig de workflow **Build & Release Plugin** via **Actions → Run workflow** (optioneel met extra release notes).
3. De workflow slaat releases over wanneer een tag met dezelfde versie al bestaat.

178
SitiWebUpdater.php Normal file
View File

@@ -0,0 +1,178 @@
<?php
class SitiWebUpdater {
private $file;
private $plugin;
private $basename;
private $active;
private $username;
private $repository;
private $authorize_token;
private $github_response;
public function __construct( $file ) {
$this->file = $file;
add_action( 'admin_init', array( $this, 'set_plugin_properties' ) );
return $this;
}
public function set_plugin_properties() {
$this->plugin = get_plugin_data( $this->file );
$this->basename = plugin_basename( $this->file );
$this->active = is_plugin_active( $this->basename );
}
public function set_username( $username ) {
$this->username = $username;
}
public function set_repository( $repository ) {
$this->repository = $repository;
}
public function authorize( $token ) {
$this->authorize_token = $token;
}
private function get_repository_info() {
if ( is_null( $this->github_response ) ) { // Do we have a response?
$args = array();
$request_uri = sprintf( 'https://api.github.com/repos/%s/%s/releases', $this->username, $this->repository ); // Build URI
$args = array();
if( $this->authorize_token ) { // Is there an access token?
$args['headers']['Authorization'] = "bearer {$this->authorize_token}"; // Set the headers
}
$response = json_decode( wp_remote_retrieve_body( wp_remote_get( $request_uri, $args ) ), true ); // Get JSON and parse it
if( is_array( $response ) ) { // If it is an array
$response = current( $response ); // Get the first item
}
$this->github_response = $response; // Set it to our property
}
}
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);
add_filter( 'upgrader_post_install', array( $this, 'after_install' ), 10, 3 );
// Add Authorization Token to download_package
add_filter( 'upgrader_pre_download',
function() {
add_filter( 'http_request_args', [ $this, 'download_package' ], 15, 2 );
return false; // upgrader_pre_download filter default return value.
}
);
}
public function modify_transient( $transient ) {
if( property_exists( $transient, 'checked') ) { // Check if transient has a checked property
if( $checked = $transient->checked ) { // Did Wordpress check for updates?
$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
if( $out_of_date ) {
$new_files = $this->github_response['zipball_url']; // Get the ZIP
$slug = current( explode('/', $this->basename ) ); // Create valid slug
$plugin = array( // setup our plugin info
'url' => $this->plugin["PluginURI"],
'slug' => $slug,
'package' => $new_files,
'new_version' => $this->github_response['tag_name']
);
$transient->response[$this->basename] = (object) $plugin; // Return it in response
}
}
}
return $transient; // Return filtered transient
}
public function plugin_popup( $result, $action, $args ) {
if( ! empty( $args->slug ) ) { // If there is a slug
if( $args->slug == current( explode( '/' , $this->basename ) ) ) { // And it's our slug
$this->get_repository_info(); // Get our repo info
// Set it to an array
$plugin = array(
'name' => $this->plugin["Name"],
'slug' => $this->basename,
'requires' => '5.1',
'tested' => '6.4.3',
'rating' => '100.0',
'num_ratings' => '10823',
'downloaded' => '14249',
'added' => '2016-01-05',
'version' => $this->github_response['tag_name'],
'author' => $this->plugin["AuthorName"],
'author_profile' => $this->plugin["AuthorURI"],
'last_updated' => $this->github_response['published_at'],
'homepage' => $this->plugin["PluginURI"],
'short_description' => $this->plugin["Description"],
'sections' => array(
'Description' => $this->plugin["Description"],
'Updates' => $this->github_response['body'],
),
'download_link' => $this->github_response['zipball_url']
);
return (object) $plugin; // Return the data
}
}
return $result; // Otherwise return default
}
public function download_package( $args, $url ) {
if ( null !== $args['filename'] ) {
if( $this->authorize_token ) {
$args = array_merge( $args, array( "headers" => array( "Authorization" => "token {$this->authorize_token}" ) ) );
}
}
remove_filter( 'http_request_args', [ $this, 'download_package' ] );
return $args;
}
public function after_install( $response, $hook_extra, $result ) {
global $wp_filesystem; // Get global FS object
$install_directory = plugin_dir_path( $this->file ); // Our plugin directory
$wp_filesystem->move( $result['destination'], $install_directory ); // Move files to the plugin dir
$result['destination'] = $install_directory; // Set the destination for the rest of the stack
if ( $this->active ) { // If it was active
activate_plugin( $this->basename ); // Reactivate
}
return $result;
}
}

236
assets/css/admin.css Normal file
View File

@@ -0,0 +1,236 @@
.groq-ai-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: none;
align-items: center;
justify-content: center;
z-index: 100000;
}
.groq-ai-modal.is-open {
display: flex;
}
.groq-ai-modal__dialog {
background: #fff;
max-width: 640px;
width: 90%;
padding: 24px;
border-radius: 6px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
position: relative;
max-height: 90vh;
overflow: hidden;
}
.groq-ai-modal__dialog-inner {
max-height: calc(90vh - 20px);
overflow-y: auto;
padding-right: 8px;
}
.groq-ai-modal__close {
position: absolute;
top: 12px;
right: 12px;
border: none;
background: transparent;
font-size: 24px;
cursor: pointer;
}
.groq-ai-modal textarea {
width: 100%;
margin-top: 8px;
}
.groq-ai-modal__context {
background: #f4f4f4;
padding: 12px;
border-radius: 4px;
margin-bottom: 12px;
font-size: 13px;
}
.groq-ai-modal__actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.groq-ai-context-options {
margin-top: 0;
padding: 12px;
border-top: 1px solid #dcdcde;
background: #f9f9f9;
}
.groq-ai-context-options__grid {
display: flex;
flex-direction: column;
gap: 10px;
}
.groq-ai-context-option {
display: flex;
gap: 10px;
align-items: flex-start;
}
.groq-ai-context-option input[type='checkbox'] {
margin-top: 4px;
}
.groq-ai-modal__result {
margin-top: 16px;
background: #fafafa;
border: 1px solid #e5e5e5;
padding: 12px;
border-radius: 4px;
max-height: none;
overflow: visible;
}
.groq-ai-result-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.groq-ai-result-field {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
padding: 10px;
}
.groq-ai-result-field__header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.groq-ai-result-field__actions {
display: flex;
gap: 6px;
align-items: center;
}
.groq-ai-result-field textarea {
margin-top: 8px;
width: 100%;
}
.groq-ai-modal__raw {
margin-top: 16px;
}
.groq-ai-modal__raw pre {
background: #1e1e1e;
color: #f8f8f2;
padding: 12px;
border-radius: 4px;
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.groq-ai-modal__status {
margin-top: 10px;
font-size: 13px;
}
.groq-ai-modal__status[data-status='error'] {
color: #b32d2e;
}
.groq-ai-modal__status[data-status='success'] {
color: #008a20;
}
.groq-ai-modal.is-loading .groq-ai-modal__dialog::after {
content: '';
position: absolute;
top: 12px;
left: 12px;
width: 18px;
height: 18px;
border: 2px solid #2271b1;
border-right-color: transparent;
border-radius: 50%;
animation: groq-spin 0.8s linear infinite;
}
@keyframes groq-spin {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
.wrap .groq-ai-prompt-helper {
margin-top: 30px;
background: #fff;
padding: 20px;
border: 1px solid #ccd0d4;
border-radius: 6px;
}
.groq-ai-advanced-settings {
margin-top: 16px;
border: 1px solid #dcdcde;
border-radius: 4px;
background: #fff;
}
.groq-ai-advanced-toggle {
width: 100%;
background: none;
border: none;
padding: 10px 14px;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #1d2327;
cursor: pointer;
}
.groq-ai-advanced-toggle__icon {
display: inline-block;
width: 12px;
height: 12px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
transition: transform 0.2s ease;
}
.groq-ai-advanced-toggle.is-open .groq-ai-advanced-toggle__icon {
transform: rotate(-135deg);
}
.groq-ai-apply-status {
min-width: 16px;
font-weight: 700;
font-size: 16px;
line-height: 1;
opacity: 0;
transition: opacity 0.2s ease;
}
.groq-ai-apply-status.is-success {
color: #1b9a3c;
opacity: 1;
}
.groq-ai-apply-status.is-error {
color: #b32d2e;
opacity: 1;
}

31
assets/css/settings.css Normal file
View File

@@ -0,0 +1,31 @@
.groq-ai-model-field select {
min-width: 300px;
}
.groq-ai-model-custom {
margin-top: 10px;
}
#groq-ai-refresh-models {
margin-top: 10px;
}
#groq-ai-refresh-models-status {
margin-top: 6px;
font-size: 13px;
}
#groq-ai-refresh-models-status[data-status='error'] {
color: #b32d2e;
}
#groq-ai-refresh-models-status[data-status='success'] {
color: #008a20;
}
.groq-ai-context-defaults label {
display: flex;
gap: 8px;
align-items: center;
margin-top: 8px;
}

381
assets/js/admin.js Normal file
View File

@@ -0,0 +1,381 @@
(function ($) {
const modal = document.getElementById('groq-ai-modal');
if (!modal) {
return;
}
const openButtons = document.querySelectorAll('.groq-ai-open-modal');
const closeButton = modal.querySelector('.groq-ai-modal__close');
const form = document.getElementById('groq-ai-form');
const promptField = document.getElementById('groq-ai-prompt');
const statusField = modal.querySelector('.groq-ai-modal__status');
const resultWrapper = modal.querySelector('.groq-ai-modal__result');
const resultField = document.getElementById('groq-ai-output');
const jsonCopyButton = modal.querySelector('.groq-ai-copy-json');
const contextToggles = modal.querySelectorAll('.groq-ai-context-toggle');
const resultFields = {};
modal.querySelectorAll('.groq-ai-result-field').forEach((field) => {
const key = field.getAttribute('data-field');
if (!key) {
return;
}
resultFields[key] = {
key,
container: field,
textarea: field.querySelector('textarea'),
target: field.getAttribute('data-target-input') || '',
label: field.getAttribute('data-label') || key,
rankMathAction: field.getAttribute('data-rankmath-action') || '',
status: field.querySelector('.groq-ai-apply-status') || null,
statusTimer: null,
};
});
const advancedToggle = modal.querySelector('.groq-ai-advanced-toggle');
const advancedPanel = document.getElementById('groq-ai-advanced-panel');
function setAdvancedState(isOpen) {
if (!advancedToggle || !advancedPanel) {
return;
}
advancedToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
advancedToggle.classList.toggle('is-open', isOpen);
advancedPanel.hidden = !isOpen;
}
if (advancedToggle && advancedPanel) {
advancedToggle.addEventListener('click', () => {
const expanded = advancedToggle.getAttribute('aria-expanded') === 'true';
setAdvancedState(!expanded);
});
setAdvancedState(false);
}
function openModal() {
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
if (promptField && !promptField.value && GroqAIGenerator.defaultPrompt) {
promptField.value = GroqAIGenerator.defaultPrompt;
}
resetContextToggles();
setTimeout(() => promptField.focus(), 50);
}
function closeModal() {
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
statusField.textContent = '';
}
openButtons.forEach((button) => {
button.addEventListener('click', openModal);
});
if (closeButton) {
closeButton.addEventListener('click', closeModal);
}
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keyup', (event) => {
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
closeModal();
}
});
function setStatus(message, type = '') {
statusField.textContent = message;
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.';
function toggleLoading(isLoading) {
modal.classList.toggle('is-loading', isLoading);
if (isLoading) {
setStatus(loadingText, 'loading');
}
}
if (form) {
form.addEventListener('submit', (event) => {
event.preventDefault();
const prompt = promptField.value.trim();
const payload = new URLSearchParams();
payload.append('action', 'groq_ai_generate_text');
payload.append('nonce', GroqAIGenerator.nonce);
payload.append('prompt', prompt);
payload.append('post_id', GroqAIGenerator.postId || 0);
payload.append('context_fields', JSON.stringify(collectContextSelection()));
toggleLoading(true);
resultWrapper.hidden = true;
if (jsonCopyButton) {
jsonCopyButton.disabled = true;
}
resetFieldStatuses();
fetch(GroqAIGenerator.ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: payload.toString(),
})
.then((response) => response.json())
.then((json) => {
if (!json.success) {
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout';
throw new Error(errorMessage);
}
const fields = json.data.fields || {};
Object.keys(resultFields).forEach((key) => {
const entry = resultFields[key];
if (entry && entry.textarea) {
entry.textarea.value = fields[key] || '';
}
});
resultField.textContent = (json.data.raw || '').trim();
resultWrapper.hidden = false;
if (jsonCopyButton) {
jsonCopyButton.disabled = false;
}
setStatus('Structuur gegenereerd. Kopieer of vul velden in.', 'success');
})
.catch((error) => {
const message = error && error.message ? error.message : 'Er ging iets mis bij het genereren.';
setStatus(loadingText, 'error');
const fullMessage = `${loadingText} ${message}. ${retryText}`;
statusField.textContent = fullMessage;
})
.finally(() => {
toggleLoading(false);
});
});
}
function copyToClipboard(text) {
if (!text) {
return Promise.reject();
}
if (navigator.clipboard) {
return navigator.clipboard.writeText(text);
}
return new Promise((resolve, reject) => {
const temp = document.createElement('textarea');
temp.value = text;
document.body.appendChild(temp);
temp.select();
try {
document.execCommand('copy');
resolve();
} catch (err) {
reject(err);
} finally {
document.body.removeChild(temp);
}
});
}
function applyRankMathField(action, value) {
if (!action || !window.wp || !window.wp.data || typeof window.wp.data.dispatch !== 'function') {
return false;
}
const dispatcher = window.wp.data.dispatch('rank-math');
if (!dispatcher || typeof dispatcher[action] !== 'function') {
return false;
}
try {
dispatcher[action](value);
return true;
} catch (error) {
if (window.console && console.warn) {
console.warn('[GroqAI] Rank Math veld kon niet worden bijgewerkt', error);
}
}
return false;
}
function setFieldStatus(fieldKey, state) {
const entry = resultFields[fieldKey];
if (!entry || !entry.status) {
return;
}
if (entry.statusTimer) {
clearTimeout(entry.statusTimer);
entry.statusTimer = null;
}
entry.status.textContent = '';
entry.status.classList.remove('is-success', 'is-error');
if (!state) {
return;
}
if (state === 'success') {
entry.status.textContent = '✓';
entry.status.classList.add('is-success');
} else if (state === 'error') {
entry.status.textContent = '!';
entry.status.classList.add('is-error');
}
entry.statusTimer = setTimeout(() => {
setFieldStatus(fieldKey, null);
}, 4000);
}
function resetFieldStatuses() {
Object.keys(resultFields).forEach((key) => setFieldStatus(key, null));
}
function shouldUseTinyMCE(selector) {
return selector === '#content' || selector === '#excerpt';
}
function applyTinyMCEContent(selector, value) {
if (!shouldUseTinyMCE(selector) || !window.tinymce) {
return false;
}
const editorId = selector.startsWith('#') ? selector.substring(1) : selector;
const editor = window.tinymce.get(editorId);
if (!editor) {
return false;
}
try {
editor.setContent(value);
editor.save();
return true;
} catch (error) {
if (window.console && console.warn) {
console.warn('[GroqAI] TinyMCE update mislukt voor', selector, error);
}
}
return false;
}
function applyToTarget(fieldKey) {
const entry = resultFields[fieldKey];
if (!entry) {
return;
}
const value = entry.textarea ? entry.textarea.value : '';
let applied = false;
const fallbackSelectors = getFallbackSelectors(fieldKey);
const allSelectors = [];
if (entry.target) {
allSelectors.push(entry.target);
}
Array.prototype.push.apply(allSelectors, fallbackSelectors);
for (let i = 0; i < allSelectors.length && !applied; i += 1) {
const selector = allSelectors[i];
if (shouldUseTinyMCE(selector)) {
applied = applyTinyMCEContent(selector, value);
if (applied) {
break;
}
}
const target = document.querySelector(selector);
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) {
target.value = value;
target.dispatchEvent(new Event('input', { bubbles: true }));
target.dispatchEvent(new Event('change', { bubbles: true }));
applied = true;
}
if (!applied && shouldUseTinyMCE(selector)) {
applied = applyTinyMCEContent(selector, value);
}
}
if (!applied && entry.rankMathAction) {
applied = applyRankMathField(entry.rankMathAction, value);
}
if (applied) {
setStatus(entry.label + ' ingevuld.', 'success');
setFieldStatus(fieldKey, 'success');
} else {
setStatus('Kon het veld niet automatisch invullen.', 'error');
setFieldStatus(fieldKey, 'error');
}
}
function getFallbackSelectors(fieldKey) {
switch (fieldKey) {
case 'meta_title':
return ['input[name="rank_math_title"]'];
case 'meta_description':
return ['textarea[name="rank_math_description"]'];
case 'focus_keywords':
return ['input[name="rank_math_focus_keyword"]'];
default:
return [];
}
}
modal.addEventListener('click', (event) => {
if (event.target.classList.contains('groq-ai-copy-field')) {
const field = event.target.getAttribute('data-field');
const entry = resultFields[field];
if (!entry || !entry.textarea) {
return;
}
copyToClipboard(entry.textarea.value)
.then(() => {
setStatus(entry.label + ' gekopieerd naar het klembord.', 'success');
})
.catch(() => {
setStatus('Kopiëren mislukt.', 'error');
});
}
if (event.target.classList.contains('groq-ai-apply-field')) {
const field = event.target.getAttribute('data-field');
applyToTarget(field);
}
});
if (jsonCopyButton) {
jsonCopyButton.addEventListener('click', () => {
const text = resultField ? resultField.textContent.trim() : '';
copyToClipboard(text)
.then(() => {
setStatus('JSON gekopieerd naar het klembord.', 'success');
})
.catch(() => {
setStatus('Kopiëren mislukt.', 'error');
});
});
}
function resetContextToggles() {
const defaults = GroqAIGenerator.contextDefaults || {};
contextToggles.forEach((toggle) => {
const key = toggle.getAttribute('data-field');
if (!key) {
return;
}
const state = Object.prototype.hasOwnProperty.call(defaults, key) ? !!defaults[key] : true;
toggle.checked = state;
});
}
function collectContextSelection() {
const selected = [];
contextToggles.forEach((toggle) => {
if (toggle.checked) {
selected.push(toggle.getAttribute('data-field'));
}
});
return selected;
}
})(jQuery);

194
assets/js/settings.js Normal file
View File

@@ -0,0 +1,194 @@
(function () {
function onReady(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
} else {
callback();
}
}
onReady(function () {
const data = window.GroqAISettingsData || {};
const optionKey = data.optionKey || '';
if (!optionKey) {
return;
}
const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]');
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');
let currentModelValue = (modelSelect && modelSelect.dataset.currentModel) || data.currentModel || '';
function toggleProviderRows() {
if (!data.providerRows || !providerSelect) {
return;
}
Object.keys(data.providerRows).forEach(function (provider) {
const rowId = data.providerRows[provider];
let row = rowId ? document.getElementById(rowId) : null;
if (!row) {
const field = document.querySelector('[data-provider-row="' + provider + '"]');
if (field) {
row = field.closest('tr') || field;
}
}
if (!row) {
return;
}
const target = row.tagName && row.tagName.toLowerCase() === 'tr' ? row : row.closest('tr') || row;
target.style.display = provider === providerSelect.value ? '' : 'none';
});
}
function buildModelOptions() {
if (!modelSelect || !data.providers) {
return;
}
const provider = providerSelect ? providerSelect.value : data.currentProvider;
const providerData = data.providers[provider];
if (!providerData) {
return;
}
const models = Array.isArray(providerData.models) ? providerData.models : [];
const frag = document.createDocumentFragment();
const placeholder = document.createElement('option');
placeholder.value = '';
const defaultLabel = providerData.default_label || (data.placeholders && data.placeholders.selectModel) || 'Selecteer een model via "Live modellen ophalen"';
placeholder.textContent = defaultLabel;
frag.appendChild(placeholder);
let hasCurrent = false;
models.forEach(function (model) {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
if (model === currentModelValue) {
hasCurrent = true;
}
frag.appendChild(option);
});
if (currentModelValue && !hasCurrent) {
const extraOption = document.createElement('option');
extraOption.value = currentModelValue;
extraOption.textContent = currentModelValue;
frag.appendChild(extraOption);
}
modelSelect.innerHTML = '';
modelSelect.appendChild(frag);
modelSelect.value = currentModelValue || '';
modelSelect.dataset.currentModel = currentModelValue || '';
}
function setRefreshStatus(message, type) {
if (!refreshStatus) {
return;
}
refreshStatus.textContent = message || '';
refreshStatus.dataset.status = type || '';
}
function updateRefreshButtonVisibility() {
if (!refreshButton) {
return;
}
const provider = providerSelect ? providerSelect.value : data.currentProvider;
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
const supports = providerData && providerData.supports_live;
refreshButton.style.display = supports ? '' : 'none';
if (!supports) {
setRefreshStatus('', '');
}
}
function handleModelChange() {
if (!modelSelect) {
return;
}
currentModelValue = modelSelect.value;
modelSelect.dataset.currentModel = currentModelValue;
}
function handleProviderChange() {
currentModelValue = '';
if (modelSelect) {
modelSelect.dataset.currentModel = '';
}
buildModelOptions();
handleModelChange();
toggleProviderRows();
updateRefreshButtonVisibility();
}
function handleRefreshModels() {
if (!refreshButton || !data.ajaxUrl) {
return;
}
const provider = providerSelect ? providerSelect.value : data.currentProvider;
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
if (!providerData || !providerData.supports_live) {
setRefreshStatus('Deze aanbieder ondersteunt dit niet.', 'error');
return;
}
const keyField = document.querySelector('[data-provider-row="' + provider + '"] input');
const apiKey = keyField ? keyField.value.trim() : '';
if (!apiKey) {
setRefreshStatus('Vul eerst de API-sleutel in.', 'error');
return;
}
refreshButton.disabled = true;
setRefreshStatus('Modellen worden opgehaald…', 'loading');
const payload = new URLSearchParams();
payload.append('action', 'groq_ai_refresh_models');
payload.append('nonce', data.refreshNonce || '');
payload.append('provider', provider);
payload.append('apiKey', apiKey);
fetch(data.ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: payload.toString(),
})
.then((response) => response.json())
.then((json) => {
if (!json.success || !json.data || !Array.isArray(json.data.models)) {
throw new Error((json.data && json.data.message) || 'Onbekende fout');
}
data.providers[provider].models = json.data.models;
buildModelOptions();
setRefreshStatus('Modellen bijgewerkt.', 'success');
})
.catch((error) => {
setRefreshStatus(error.message || 'Ophalen mislukt.', 'error');
})
.finally(() => {
refreshButton.disabled = false;
});
}
if (modelSelect) {
modelSelect.addEventListener('change', handleModelChange);
}
if (providerSelect) {
providerSelect.addEventListener('change', handleProviderChange);
}
if (refreshButton) {
refreshButton.addEventListener('click', handleRefreshModels);
}
buildModelOptions();
handleModelChange();
toggleProviderRows();
updateRefreshButtonVisibility();
});
})();

49
docker-compose.yml Normal file
View File

@@ -0,0 +1,49 @@
services:
db:
image: mariadb:10.11
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
MYSQL_ROOT_PASSWORD: supersecret
volumes:
- db_data:/var/lib/mysql
ports:
- "3307:3306"
wordpress:
build:
context: .
dockerfile: docker/wordpress/Dockerfile
depends_on:
- db
ports:
- "8082:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DEBUG: 1
WP_ENVIRONMENT_TYPE: local
volumes:
- wordpress_data:/var/www/html
- ./:/var/www/html/wp-content/plugins/siti-ai-product-content-generator
restart: unless-stopped
phpmyadmin:
image: phpmyadmin/phpmyadmin:5
depends_on:
- db
ports:
- "8085:80"
environment:
PMA_HOST: db
PMA_USER: wordpress
PMA_PASSWORD: wordpress
volumes:
db_data:
wordpress_data:

View File

@@ -0,0 +1,13 @@
FROM wordpress:6.9-php8.2
# Install handy tooling (git, mariadb client, wp-cli) for local development.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git less vim unzip mariadb-client \
&& rm -rf /var/lib/apt/lists/* \
&& curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x /usr/local/bin/wp
WORKDIR /var/www/html
# Match the default Linux UID/GID to avoid permission headaches with bind mounts.
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data

335
groq-ai-product-text.php Normal file
View File

@@ -0,0 +1,335 @@
<?php
/**
* Plugin Name: SitiAI Product Teksten
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
* Version: 1.1.0
* Author: SitiAI
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_FILE' ) ) {
define( 'GROQ_AI_PRODUCT_TEXT_FILE', __FILE__ );
}
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_VERSION' ) ) {
$groq_ai_plugin_data = get_file_data(
__FILE__,
[
'Version' => 'Version',
],
false
);
$groq_ai_version = isset( $groq_ai_plugin_data['Version'] ) && $groq_ai_plugin_data['Version'] ? $groq_ai_plugin_data['Version'] : '1.0.0';
define( 'GROQ_AI_PRODUCT_TEXT_VERSION', $groq_ai_version );
}
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-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';
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-openai.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-google.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-manager.php';
require_once __DIR__ . '/includes/Services/Settings/class-groq-ai-settings-manager.php';
require_once __DIR__ . '/includes/Services/Prompt/class-groq-ai-prompt-builder.php';
require_once __DIR__ . '/includes/Services/Conversations/class-groq-ai-conversation-manager.php';
require_once __DIR__ . '/includes/Services/Logging/class-groq-ai-generation-logger.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php';
if( ! class_exists( 'SitiWebUpdater' ) ){
include_once( plugin_dir_path( __FILE__ ) . 'SitiWebUpdater.php' );
}
$updater = new SitiWebUpdater( __FILE__ );
$updater->set_username( 'SitiWeb' );
$updater->set_repository( 'siti-ai-product-content-generator' );
$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';
private static $instance = null;
/** @var Groq_AI_Service_Container */
private $container;
/** @var */
private $settings_page;
/** @var Groq_AI_Product_Text_Product_UI */
private $product_ui;
/** @var bool */
private $missing_wc_notice = false;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->register_services();
$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_create_logs_table' ] );
add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] );
}
private function register_services() {
$this->container = new Groq_AI_Service_Container();
$this->container->set(
'provider_manager',
function () {
return new Groq_AI_Provider_Manager();
}
);
$this->container->set(
'settings_manager',
function ( Groq_AI_Service_Container $container ) {
return new Groq_AI_Settings_Manager( self::OPTION_KEY, $container->get( 'provider_manager' ) );
}
);
$this->container->set(
'prompt_builder',
function ( Groq_AI_Service_Container $container ) {
return new Groq_AI_Prompt_Builder( $container->get( 'settings_manager' ) );
}
);
$this->container->set(
'conversation_manager',
function () {
return new Groq_AI_Conversation_Manager( self::CONVERSATION_OPTION_KEY );
}
);
$this->container->set(
'generation_logger',
function () {
return new Groq_AI_Generation_Logger();
}
);
$this->container->set(
'ajax_controller',
function () {
return new Groq_AI_Ajax_Controller( $this );
}
);
// Instantiate controller immediately so hooks are registered.
$this->container->get( 'ajax_controller' );
}
public function get_option_key() {
return self::OPTION_KEY;
}
public function get_provider_manager() {
return $this->container->get( 'provider_manager' );
}
public function get_settings_manager() {
return $this->container->get( 'settings_manager' );
}
public function get_prompt_builder() {
return $this->container->get( 'prompt_builder' );
}
public function get_conversation_manager() {
return $this->container->get( 'conversation_manager' );
}
public function get_generation_logger() {
return $this->container->get( 'generation_logger' );
}
public function get_settings() {
return $this->get_settings_manager()->all();
}
public function sanitize_settings( $input ) {
return $this->get_settings_manager()->sanitize( $input );
}
public function maybe_deactivate_if_woocommerce_missing() {
if ( $this->is_woocommerce_active() ) {
return;
}
if ( ! function_exists( 'deactivate_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
deactivate_plugins( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) );
$this->missing_wc_notice = true;
add_action( 'admin_notices', [ $this, 'render_missing_wc_notice' ] );
}
public function render_missing_wc_notice() {
if ( ! $this->missing_wc_notice ) {
return;
}
?>
<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' ); ?>
</p>
</div>
<?php
}
public function build_prompt_template_preview( $settings ) {
$parts = [];
if ( ! empty( $settings['store_context'] ) ) {
$parts[] = sprintf( __( 'Winkelcontext: %s', 'groq-ai-product-text' ), $settings['store_context'] );
}
if ( ! empty( $settings['default_prompt'] ) ) {
$parts[] = sprintf( __( 'Standaard prompt: %s', 'groq-ai-product-text' ), $settings['default_prompt'] );
}
if ( empty( $parts ) ) {
return __( 'Nog geen promptinformatie opgeslagen.', 'groq-ai-product-text' );
}
return implode( "\n\n", $parts );
}
public function get_context_field_definitions() {
return $this->get_settings_manager()->get_context_field_definitions();
}
public function get_default_modules_settings() {
return $this->get_settings_manager()->get_default_modules_settings();
}
public function get_default_context_fields() {
return $this->get_settings_manager()->get_default_context_fields();
}
public function normalize_context_fields( $fields ) {
return $this->get_settings_manager()->normalize_context_fields( $fields );
}
public function get_module_config( $module, $settings = null ) {
return $this->get_settings_manager()->get_module_config( $module, $settings );
}
public function is_module_enabled( $module, $settings = null ) {
return $this->get_settings_manager()->is_module_enabled( $module, $settings );
}
public function get_rankmath_focus_keyword_limit( $settings = null ) {
return $this->get_settings_manager()->get_rankmath_focus_keyword_limit( $settings );
}
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
return $this->get_settings_manager()->get_rankmath_meta_title_pixel_limit( $settings );
}
public function get_rankmath_meta_description_pixel_limit( $settings = null ) {
return $this->get_settings_manager()->get_rankmath_meta_description_pixel_limit( $settings );
}
public function is_response_format_compat_enabled( $settings = null ) {
return $this->get_settings_manager()->is_response_format_compat_enabled( $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();
}
public function is_rankmath_active() {
if ( class_exists( 'RankMath' ) ) {
return true;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'seo-by-rank-math/rank-math.php' );
}
public function is_woocommerce_active() {
if ( class_exists( 'WooCommerce' ) ) {
return true;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'woocommerce/woocommerce.php' );
}
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
return ! empty( $settings['model'] ) ? $settings['model'] : $provider->get_default_model();
}
public function log_debug( $message, $context = [] ) {
$this->get_generation_logger()->log_debug( $message, $context );
}
private function extract_content_text( $result ) {
if ( is_array( $result ) && isset( $result['content'] ) ) {
return (string) $result['content'];
}
return (string) $result;
}
public function maybe_create_logs_table() {
$this->get_generation_logger()->maybe_create_table();
}
public static function activate() {
$logger = new Groq_AI_Generation_Logger();
$logger->create_table();
}
}
register_activation_hook( GROQ_AI_PRODUCT_TEXT_FILE, [ 'Groq_AI_Product_Text_Plugin', 'activate' ] );
Groq_AI_Product_Text_Plugin::instance();

View File

@@ -0,0 +1,160 @@
<?php
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
class Groq_AI_Logs_Table extends WP_List_Table {
/** @var Groq_AI_Product_Text_Plugin */
private $plugin;
/** @var string */
private $table;
/** @var string */
private $posts_table;
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
$this->plugin = $plugin;
global $wpdb;
$this->table = $wpdb->prefix . 'groq_ai_generation_logs';
$this->posts_table = $wpdb->posts;
parent::__construct(
[
'singular' => 'groq_ai_log',
'plural' => 'groq_ai_logs',
'ajax' => false,
]
);
}
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' ),
];
}
protected function get_sortable_columns() {
return [
'created_at' => [ 'created_at', true ],
'provider' => [ 'provider', false ],
'model' => [ 'model', false ],
'status' => [ 'status', false ],
];
}
protected function get_default_primary_column_name() {
return 'created_at';
}
public function prepare_items() {
global $wpdb;
$per_page = 20;
$current_page = $this->get_pagenum();
$offset = ( $current_page - 1 ) * $per_page;
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at';
if ( ! $orderby ) {
$orderby = 'created_at';
}
$order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC';
$order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';
$search = isset( $_REQUEST['s'] ) ? wp_unslash( trim( $_REQUEST['s'] ) ) : '';
$where = '1=1';
$params = [];
if ( $search ) {
$like = '%' . $wpdb->esc_like( $search ) . '%';
$where .= ' AND (provider LIKE %s OR model LIKE %s OR prompt LIKE %s OR response LIKE %s OR error_message LIKE %s )';
$params = array_merge( $params, [ $like, $like, $like, $like, $like ] );
}
$total_query = "SELECT COUNT(*) FROM {$this->table} l LEFT JOIN {$this->posts_table} p ON p.ID = l.post_id WHERE {$where}";
$total_items = (int) $wpdb->get_var(
$params ? $wpdb->prepare( $total_query, $params ) : $total_query
);
$query = "SELECT l.*, p.post_title FROM {$this->table} l LEFT JOIN {$this->posts_table} p ON p.ID = l.post_id WHERE {$where} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
$params_with_limits = array_merge( $params, [ $per_page, $offset ] );
$this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns() ];
$this->items = $params
? $wpdb->get_results( $wpdb->prepare( $query, $params_with_limits ), ARRAY_A )
: $wpdb->get_results( $wpdb->prepare( $query, $per_page, $offset ), ARRAY_A );
$this->set_pagination_args(
[
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil( $total_items / $per_page ),
]
);
}
protected function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'created_at':
return esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
case 'provider':
case 'model':
case 'status':
return esc_html( $item[ $column_name ] );
case 'tokens_total':
return isset( $item['tokens_total'] ) ? absint( $item['tokens_total'] ) : '—';
case 'post_title':
if ( ! $item['post_id'] ) {
return '—';
}
$title = $item['post_title'] ? $item['post_title'] : sprintf( __( 'Product #%d', 'groq-ai-product-text' ), (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':
if ( empty( $item['user_id'] ) ) {
return '—';
}
$user = get_userdata( $item['user_id'] );
return $user ? esc_html( $user->display_name ) : (int) $item['user_id'];
case 'error_message':
return $item['error_message'] ? esc_html( $item['error_message'] ) : '—';
default:
return isset( $item[ $column_name ] ) ? esc_html( $item[ $column_name ] ) : '';
}
}
public function no_items() {
esc_html_e( 'Nog geen AI-logboeken gevonden.', 'groq-ai-product-text' );
}
protected function column_created_at( $item ) {
$date = esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
$payload = [
'created_at' => $item['created_at'],
'user' => $this->column_default( $item, 'user_id' ),
'post_title' => $item['post_title'],
'provider' => $item['provider'],
'model' => $item['model'],
'status' => $item['status'],
'tokens_prompt' => isset( $item['tokens_prompt'] ) ? (int) $item['tokens_prompt'] : null,
'tokens_completion' => isset( $item['tokens_completion'] ) ? (int) $item['tokens_completion'] : null,
'tokens_total' => isset( $item['tokens_total'] ) ? (int) $item['tokens_total'] : null,
'prompt' => $item['prompt'],
'response' => $item['response'],
'error_message' => $item['error_message'],
];
$encoded = esc_attr( wp_json_encode( $payload ) );
return sprintf(
'<a href="#" class="groq-ai-log-row" data-groq-log="%s">%s</a>',
$encoded,
$date
);
}
}

View File

@@ -0,0 +1,212 @@
<?php
class Groq_AI_Product_Text_Product_UI {
private $plugin;
public function __construct( $plugin ) {
$this->plugin = $plugin;
add_action( 'add_meta_boxes', [ $this, 'register_meta_box' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_action( 'admin_footer', [ $this, 'render_modal_markup' ] );
}
public function register_meta_box() {
add_meta_box(
'groq-ai-generator-box',
__( 'Gebruik AI', 'groq-ai-product-text' ),
[ $this, 'render_meta_box' ],
'product',
'side',
'high'
);
}
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>';
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 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' ); ?>
</p>
<?php
}
public function enqueue_admin_assets( $hook ) {
$screen = get_current_screen();
if ( $screen && 'product' === $screen->post_type && in_array( $screen->base, [ 'post', 'post-new' ], true ) ) {
wp_enqueue_style(
'groq-ai-admin',
plugins_url( 'assets/css/admin.css', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION
);
wp_enqueue_script(
'groq-ai-admin',
plugins_url( 'assets/js/admin.js', GROQ_AI_PRODUCT_TEXT_FILE ),
[ 'jquery' ],
GROQ_AI_PRODUCT_TEXT_VERSION,
true
);
global $post;
$post_id = ( $post && isset( $post->ID ) ) ? (int) $post->ID : 0;
$settings = $this->plugin->get_settings();
wp_localize_script(
'groq-ai-admin',
'GroqAIGenerator',
[
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'groq_ai_generate' ),
'defaultPrompt' => $settings['default_prompt'],
'postId' => $post_id,
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
]
);
}
}
public function render_modal_markup() {
$screen = get_current_screen();
if ( ! $screen || 'product' !== $screen->post_type ) {
return;
}
$settings = $this->plugin->get_settings();
$rankmath_enabled = $this->plugin->is_rankmath_active() && $this->plugin->is_module_enabled( 'rankmath', $settings );
?>
<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>
<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>
<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>
<div class="groq-ai-modal__actions">
<button type="submit" class="button button-primary">
<?php esc_html_e( 'Genereer tekst', 'groq-ai-product-text' ); ?>
</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' ); ?>
</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>
<div class="groq-ai-context-options__grid">
<?php
$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 ) :
$checked = ! empty( $context_defaults[ $context_key ] );
?>
<label class="groq-ai-context-option">
<input type="checkbox" class="groq-ai-context-toggle" data-field="<?php echo esc_attr( $context_key ); ?>" <?php checked( $checked ); ?> />
<div>
<strong><?php echo esc_html( $context_info['label'] ); ?></strong>
<?php if ( ! empty( $context_info['description'] ) ) : ?>
<p class="description"><?php echo esc_html( $context_info['description'] ); ?></p>
<?php endif; ?>
</div>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</form>
<div class="groq-ai-modal__result" hidden>
<h3><?php esc_html_e( 'Resultaat', 'groq-ai-product-text' ); ?></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__header">
<strong><?php esc_html_e( 'Producttitel', 'groq-ai-product-text' ); ?></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>
<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="short_description" data-target-input="#excerpt" data-label="<?php esc_attr_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?></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>
<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__header">
<strong><?php esc_html_e( 'Beschrijving', 'groq-ai-product-text' ); ?></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>
<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__header">
<strong><?php esc_html_e( 'Rank Math meta titel', 'groq-ai-product-text' ); ?></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>
<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__header">
<strong><?php esc_html_e( 'Rank Math meta description', 'groq-ai-product-text' ); ?></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>
<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__header">
<strong><?php esc_html_e( 'Rank Math focus keyphrase', 'groq-ai-product-text' ); ?></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>
<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>
</div>
<?php endif; ?>
</div>
<div class="groq-ai-modal__raw">
<h4><?php esc_html_e( 'Ruwe JSON-output', 'groq-ai-product-text' ); ?></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>
</div>
</div>
<div class="groq-ai-modal__status" aria-live="polite"></div>
</div>
</div>
</div>
<?php
}
}

View File

@@ -0,0 +1,569 @@
<?php
class Groq_AI_Product_Text_Settings_Page {
private $plugin;
private $provider_manager;
public function __construct( $plugin, Groq_AI_Provider_Manager $provider_manager ) {
$this->plugin = $plugin;
$this->provider_manager = $provider_manager;
add_action( 'admin_menu', [ $this, 'register_settings_pages' ] );
add_action( 'admin_init', [ $this, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_settings_assets' ] );
add_action( 'admin_head', [ $this, 'hide_menu_links' ] );
}
public function register_settings_pages() {
add_options_page(
__( 'Siti AI Productteksten', 'groq-ai-product-text' ),
__( 'Siti AI', 'groq-ai-product-text' ),
'manage_options',
'groq-ai-product-text',
[ $this, 'render_settings_page' ]
);
add_submenu_page(
'options-general.php',
__( 'Siti AI Modules', 'groq-ai-product-text' ),
__( 'Siti AI Modules', 'groq-ai-product-text' ),
'manage_options',
'groq-ai-product-text-modules',
[ $this, 'render_modules_page' ]
);
add_submenu_page(
'options-general.php',
__( 'Siti AI AI-logboek', 'groq-ai-product-text' ),
__( 'Siti AI AI-logboek', 'groq-ai-product-text' ),
'manage_options',
'groq-ai-product-text-logs',
[ $this, 'render_logs_page' ]
);
}
public function hide_menu_links() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<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"] {
display: none !important;
}
</style>
<?php
}
public function register_settings() {
register_setting( 'groq_ai_product_text_group', $this->plugin->get_option_key(), [ $this->plugin, 'sanitize_settings' ] );
add_settings_section(
'groq_ai_product_text_general',
__( 'Algemene instellingen', 'groq-ai-product-text' ),
'__return_false',
'groq-ai-product-text'
);
add_settings_field(
'groq_ai_provider',
__( 'AI-aanbieder', 'groq-ai-product-text' ),
[ $this, 'render_provider_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_model',
__( 'Model', 'groq-ai-product-text' ),
[ $this, 'render_model_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
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() ),
[ $this, 'render_provider_api_key_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general',
[
'provider' => $provider,
]
);
}
add_settings_field(
'groq_ai_store_context',
__( 'Winkelcontext', 'groq-ai-product-text' ),
[ $this, 'render_store_context_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_default_prompt',
__( 'Standaard prompt', 'groq-ai-product-text' ),
[ $this, 'render_default_prompt_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_context_fields',
__( 'Standaard productcontext', 'groq-ai-product-text' ),
[ $this, 'render_context_fields_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_response_format_compat',
__( 'Response-format compatibiliteit', 'groq-ai-product-text' ),
[ $this, 'render_response_format_compat_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_section(
'groq_ai_product_text_modules_rankmath',
__( 'Rank Math SEO', 'groq-ai-product-text' ),
'__return_false',
'groq-ai-product-text-modules'
);
add_settings_field(
'groq_ai_module_rankmath',
__( 'Rank Math SEO', 'groq-ai-product-text' ),
[ $this, 'render_rankmath_module_field' ],
'groq-ai-product-text-modules',
'groq_ai_product_text_modules_rankmath'
);
}
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->plugin->get_settings();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Siti AI Productteksten', 'groq-ai-product-text' ); ?></h1>
<p style="margin-bottom:16px;">
<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' ); ?>
</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' ); ?>
</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>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
do_settings_sections( 'groq-ai-product-text' );
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
}
public function render_modules_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<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>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
do_settings_sections( 'groq-ai-product-text-modules' );
submit_button();
?>
</form>
</div>
<?php
}
public function render_logs_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$logs_table = new Groq_AI_Logs_Table( $this->plugin );
$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>
<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->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>
<div class="groq-ai-log-modal__content">
<h2 id="groq-ai-log-modal-title"><?php esc_html_e( 'Logdetails', 'groq-ai-product-text' ); ?></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>
<textarea id="groq-ai-log-prompt" readonly rows="6"></textarea>
</label>
<label>
<span><?php esc_html_e( 'Response', 'groq-ai-product-text' ); ?></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>
<span id="groq-ai-log-tokens-prompt">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Tokens response', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-tokens-completion">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Tokens totaal', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-tokens-total">—</span>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.groq-ai-log-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.65);display:none;align-items:center;justify-content:center;z-index:100000;}
.groq-ai-log-modal.is-open{display:flex;}
.groq-ai-log-modal__dialog{background:#fff;max-width:900px;width:90%;padding:20px;box-shadow:0 10px 40px rgba(0,0,0,0.3);position:relative;}
.groq-ai-log-modal__close{position:absolute;top:10px;right:10px;border:none;background:transparent;font-size:24px;cursor:pointer;}
.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-row{display:inline-block;}
</style>
<script>
(function(){
const modal=document.getElementById('groq-ai-log-modal');
if(!modal){return;}
const closeBtn=modal.querySelector('.groq-ai-log-modal__close');
const promptField=document.getElementById('groq-ai-log-prompt');
const responseField=document.getElementById('groq-ai-log-response');
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 meta=document.querySelector('.groq-ai-log-meta');
function openModal(data){
if(!data){return;}
if(promptField){promptField.value=data.prompt||'';}
if(responseField){responseField.value=data.response||'';}
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:'—';}
if(meta){
meta.textContent=(data.provider||'')+' • '+(data.model||'')+' • '+(data.post_title||'')+' • '+(data.status||'');
}
modal.classList.add('is-open');
modal.setAttribute('aria-hidden','false');
}
function closeModal(){
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden','true');
}
document.addEventListener('click',function(e){
const link=e.target.closest('.groq-ai-log-row');
if(link){
e.preventDefault();
let payload=link.getAttribute('data-groq-log');
if(payload){
try{
const data=JSON.parse(payload);
openModal(data);
}catch(err){
console.error('Invalid log payload',err);
}
}
}
if(e.target===modal){
closeModal();
}
});
if(closeBtn){
closeBtn.addEventListener('click',closeModal);
}
document.addEventListener('keyup',function(e){
if(e.key==='Escape' && modal.classList.contains('is-open')){
closeModal();
}
});
})();
</script>
<?php
}
public function render_provider_field() {
$settings = $this->plugin->get_settings();
$providers = $this->provider_manager->get_providers();
?>
<select name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[provider]">
<?php foreach ( $providers as $provider ) : ?>
<option value="<?php echo esc_attr( $provider->get_key() ); ?>" <?php selected( $settings['provider'], $provider->get_key() ); ?>>
<?php echo esc_html( $provider->get_label() ); ?>
</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>
<?php
}
public function render_model_field() {
$settings = $this->plugin->get_settings();
$current_model = $settings['model'];
$current_provider = $settings['provider'];
?>
<div class="groq-ai-model-field">
<select
id="groq-ai-model-select"
class="groq-ai-model-select"
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>
</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>
<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' ); ?>
</button>
<p id="groq-ai-refresh-models-status" class="description" aria-live="polite"></p>
</div>
<?php
}
public function render_provider_api_key_field( $args ) {
$settings = $this->plugin->get_settings();
/** @var Groq_AI_Provider_Interface $provider */
$provider = $args['provider'];
$field = $provider->get_option_key();
$provider_key = $provider->get_key();
?>
<div class="groq-ai-provider-field" data-provider-row="<?php echo esc_attr( $provider_key ); ?>">
<input type="password" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[<?php echo esc_attr( $field ); ?>]" value="<?php echo esc_attr( $settings[ $field ] ); ?>" class="regular-text" autocomplete="off" />
<p class="description">
<?php
printf(
/* translators: %s: provider name */
esc_html__( 'Voeg hier de API-sleutel voor %s toe.', 'groq-ai-product-text' ),
esc_html( $provider->get_label() )
);
?>
</p>
</div>
<?php
}
public function render_store_context_field() {
$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>
<?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>
<?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();
$definitions = $this->plugin->get_context_field_definitions();
?>
<div class="groq-ai-context-defaults">
<?php foreach ( $definitions as $key => $definition ) :
$checked = ! empty( $values[ $key ] );
?>
<label>
<input type="checkbox" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[context_fields][<?php echo esc_attr( $key ); ?>]" value="1" <?php checked( $checked ); ?> />
<strong><?php echo esc_html( $definition['label'] ); ?></strong>
</label>
<?php if ( ! empty( $definition['description'] ) ) : ?>
<p class="description" style="margin-top:-8px;margin-bottom:12px;">
<?php echo esc_html( $definition['description'] ); ?>
</p>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php
}
public function render_response_format_compat_field() {
$settings = $this->plugin->get_settings();
$is_enabled = ! empty( $settings['response_format_compat'] );
?>
<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' ); ?>
</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' ); ?>
</p>
<?php
}
public function render_rankmath_module_field() {
$settings = $this->plugin->get_settings();
$defaults = $this->plugin->get_default_modules_settings();
$modules = isset( $settings['modules'] ) ? $settings['modules'] : $defaults;
$config = isset( $modules['rankmath'] ) ? $modules['rankmath'] : ( $defaults['rankmath'] ?? [] );
$rankmath_active = $this->plugin->is_rankmath_active();
$enabled = $rankmath_active && ! empty( $config['enabled'] );
$keyword_limit = isset( $config['focus_keyword_limit'] ) ? absint( $config['focus_keyword_limit'] ) : ( $defaults['rankmath']['focus_keyword_limit'] ?? 3 );
$keyword_limit = $keyword_limit > 0 ? $keyword_limit : 3;
$title_pixels = isset( $config['meta_title_pixel_limit'] ) ? absint( $config['meta_title_pixel_limit'] ) : ( $defaults['rankmath']['meta_title_pixel_limit'] ?? 580 );
$title_pixels = $title_pixels > 0 ? $title_pixels : 580;
$pixel_limit = isset( $config['meta_description_pixel_limit'] ) ? absint( $config['meta_description_pixel_limit'] ) : ( $defaults['rankmath']['meta_description_pixel_limit'] ?? 920 );
$pixel_limit = $pixel_limit > 0 ? $pixel_limit : 920;
$rankmath_active = $this->plugin->is_rankmath_active();
?>
<div class="groq-ai-module-field">
<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' ); ?>
</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' );
} else {
esc_html_e( 'Wanneer ingeschakeld worden extra velden in de AI-modal getoond en automatisch gekoppeld aan Rank Math.', 'groq-ai-product-text' );
}
?>
</p>
<label for="groq-ai-rankmath-keywords">
<?php esc_html_e( 'Aantal focus keywords om te genereren', 'groq-ai-product-text' ); ?>
</label>
<input
type="number"
id="groq-ai-rankmath-keywords"
min="1"
max="99"
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;"
<?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' ); ?>
</p>
<label for="groq-ai-rankmath-title-pixels">
<?php esc_html_e( 'Maximale meta title breedte (pixels)', 'groq-ai-product-text' ); ?>
</label>
<input
type="number"
id="groq-ai-rankmath-title-pixels"
min="1"
max="1200"
step="1"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][meta_title_pixel_limit]"
value="<?php echo esc_attr( $title_pixels ); ?>"
style="width: 100px;"
<?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' ); ?>
</p>
<label for="groq-ai-rankmath-pixels">
<?php esc_html_e( 'Maximale meta description breedte (pixels)', 'groq-ai-product-text' ); ?>
</label>
<input
type="number"
id="groq-ai-rankmath-pixels"
min="1"
max="2000"
step="1"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][meta_description_pixel_limit]"
value="<?php echo esc_attr( $pixel_limit ); ?>"
style="width: 100px;"
<?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' ); ?>
</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 ) ) {
return;
}
wp_enqueue_style(
'groq-ai-settings',
plugins_url( 'assets/css/admin.css', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION
);
wp_enqueue_style(
'groq-ai-settings-extra',
plugins_url( 'assets/css/settings.css', GROQ_AI_PRODUCT_TEXT_FILE ),
[ 'groq-ai-settings' ],
GROQ_AI_PRODUCT_TEXT_VERSION
);
wp_enqueue_script(
'groq-ai-settings',
plugins_url( 'assets/js/settings.js', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION,
true
);
$current_settings = $this->plugin->get_settings();
$data = [
'optionKey' => $this->plugin->get_option_key(),
'providers' => [],
'currentProvider' => $current_settings['provider'],
'currentModel' => $current_settings['model'],
'providerRows' => [],
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'refreshNonce' => wp_create_nonce( 'groq_ai_refresh_models' ),
'placeholders' => [
'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', 'groq-ai-product-text' ),
],
];
foreach ( $this->provider_manager->get_providers() as $provider ) {
$data['providers'][ $provider->get_key() ] = [
'default_label' => sprintf( __( 'Gebruik standaardmodel (%s)', 'groq-ai-product-text' ), $provider->get_default_model() ),
'models' => [],
'supports_live' => $provider->supports_live_models(),
];
$data['providerRows'][ $provider->get_key() ] = 'groq_ai_api_key_' . $provider->get_key();
}
wp_localize_script( 'groq-ai-settings', 'GroqAISettingsData', $data );
}
}

View File

@@ -0,0 +1,21 @@
<?php
interface Groq_AI_Provider_Interface {
public function get_key();
public function get_label();
public function get_default_model();
public function get_available_models();
public function get_option_key();
public function generate_content( array $args );
public function supports_live_models();
public function fetch_live_models( $api_key );
public function supports_response_format();
}

View File

@@ -0,0 +1,156 @@
<?php
class Groq_AI_Ajax_Controller {
/** @var Groq_AI_Product_Text_Plugin */
private $plugin;
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
$this->plugin = $plugin;
add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] );
add_action( 'wp_ajax_groq_ai_refresh_models', [ $this, 'handle_refresh_models' ] );
}
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 );
}
check_ajax_referer( 'groq_ai_generate', 'nonce' );
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
$settings = $this->plugin->get_settings();
$provider_manager = $this->plugin->get_provider_manager();
$provider_key = $settings['provider'];
$provider = $provider_manager->get_provider( $provider_key );
if ( ! $provider ) {
$provider = $provider_manager->get_provider( 'groq' );
$provider_key = 'groq';
}
$conversation_id = $this->plugin->get_conversation_manager()->ensure_id( $provider_key, $settings['store_context'] );
$prompt_builder = $this->plugin->get_prompt_builder();
$system_prompt = $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 );
$prompt_with_context = $prompt_builder->prepend_context_to_prompt( $prompt, $product_context_text );
$response_format = null;
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
if ( $use_response_format ) {
$response_format = $prompt_builder->get_response_format_definition( $settings );
$final_prompt = $prompt_with_context;
} else {
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
}
$result = $provider->generate_content(
[
'prompt' => $final_prompt,
'system_prompt' => $system_prompt,
'model' => $model,
'settings' => $settings,
'temperature' => 0.7,
'conversation_id' => $conversation_id,
'response_format' => $response_format,
]
);
if ( is_wp_error( $result ) ) {
$this->plugin->get_generation_logger()->log_generation_event(
[
'provider' => $provider_key,
'model' => $model,
'prompt' => $final_prompt,
'response' => '',
'usage' => [],
'post_id' => $post_id,
'status' => 'error',
'error_message' => $result->get_error_message(),
]
);
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
}
$response_text = $this->extract_content_text( $result );
$response_usage = is_array( $result ) && isset( $result['usage'] ) ? $result['usage'] : [];
$response = $prompt_builder->parse_structured_response( $response_text, $settings );
if ( is_wp_error( $response ) ) {
$this->plugin->get_generation_logger()->log_generation_event(
[
'provider' => $provider_key,
'model' => $model,
'prompt' => $final_prompt,
'response' => $response_text,
'usage' => $response_usage,
'post_id' => $post_id,
'status' => 'error',
'error_message' => $response->get_error_message(),
]
);
wp_send_json_error( [ 'message' => $response->get_error_message() ], 500 );
}
$this->plugin->get_generation_logger()->log_generation_event(
[
'provider' => $provider_key,
'model' => $model,
'prompt' => $final_prompt,
'response' => $response_text,
'usage' => $response_usage,
'post_id' => $post_id,
'status' => 'success',
]
);
wp_send_json_success(
[
'fields' => $response,
'raw' => $response_text,
]
);
}
public function handle_refresh_models() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Geen toestemming.', 'groq-ai-product-text' ) ], 403 );
}
check_ajax_referer( 'groq_ai_refresh_models', 'nonce' );
$provider_key = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '';
$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 );
}
$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 );
}
$result = $provider->fetch_live_models( $api_key );
if ( is_wp_error( $result ) ) {
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
}
wp_send_json_success( [ 'models' => array_values( array_unique( $result ) ) ] );
}
private function extract_content_text( $result ) {
if ( is_array( $result ) && isset( $result['content'] ) ) {
return (string) $result['content'];
}
return (string) $result;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* Lightweight container voor Groq AI plugin services.
*
* Doel:
* - Centraliseren van service creatie en dependency sharing.
* - Mogelijke vervanging voor de huidige singleton/inline instanties in groq-ai-product-text.php.
*/
class Groq_AI_Service_Container {
/** @var array<string,mixed> */
private $services = [];
/**
* Registreer een service factory.
*
* @param string $key
* @param callable $factory
*/
public function set( $key, callable $factory ) {
$this->services[ $key ] = $factory;
}
/**
* Haal een service op en initialiseer deze lazy.
*
* @param string $key
* @return mixed
*/
public function get( $key ) {
if ( ! isset( $this->services[ $key ] ) ) {
return null;
}
if ( is_callable( $this->services[ $key ] ) ) {
$this->services[ $key ] = call_user_func( $this->services[ $key ], $this );
}
return $this->services[ $key ];
}
}

View File

@@ -0,0 +1,141 @@
<?php
abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Interface {
public function get_available_models() {
return [];
}
public function supports_response_format() {
return true;
}
public function supports_live_models() {
return true;
}
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' ) );
}
$response = wp_remote_get(
$endpoint,
[
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
],
'timeout' => 20,
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['error']['message'] ) ) {
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
}
if ( empty( $body['data'] ) || ! is_array( $body['data'] ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
}
$models = [];
foreach ( $body['data'] as $model ) {
if ( ! empty( $model['id'] ) ) {
$models[] = sanitize_text_field( $model['id'] );
}
}
if ( empty( $models ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
}
return $models;
}
public function generate_content( array $args ) {
$settings = isset( $args['settings'] ) ? (array) $args['settings'] : [];
$prompt = isset( $args['prompt'] ) ? $args['prompt'] : '';
$system_prompt = isset( $args['system_prompt'] ) ? $args['system_prompt'] : '';
$model = ! empty( $args['model'] ) ? $args['model'] : $this->get_default_model();
$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() ) );
}
$messages = [
[
'role' => 'system',
'content' => $system_prompt,
],
[
'role' => 'user',
'content' => $prompt,
],
];
$request_body = [
'model' => $model,
'messages' => $messages,
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
'max_tokens' => 1024,
];
if ( ! empty( $args['response_format'] ) ) {
$request_body['response_format'] = $args['response_format'];
}
$response = wp_remote_post(
$this->get_endpoint(),
[
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $request_body ),
'timeout' => isset( $args['timeout'] ) ? (int) $args['timeout'] : 60,
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['error']['message'] ) ) {
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
}
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() )
);
}
$content = trim( $body['choices'][0]['message']['content'] );
$usage = isset( $body['usage'] ) && is_array( $body['usage'] ) ? $body['usage'] : [];
return [
'content' => $content,
'usage' => $usage,
'raw_response' => $body,
];
}
abstract protected function get_endpoint();
abstract protected function get_models_endpoint();
protected function get_api_key( $settings ) {
$field = $this->get_option_key();
return isset( $settings[ $field ] ) ? $settings[ $field ] : '';
}
}

View File

@@ -0,0 +1,158 @@
<?php
class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
public function get_key() {
return 'google';
}
public function get_label() {
return __( 'Google AI (Gemini)', 'groq-ai-product-text' );
}
public function get_default_model() {
return 'gemini-1.5-flash';
}
public function get_available_models() {
return [
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-pro',
];
}
public function get_option_key() {
return 'google_api_key';
}
public function supports_live_models() {
return true;
}
public function supports_response_format() {
return false;
}
public function fetch_live_models( $api_key ) {
$endpoint = add_query_arg(
[ 'key' => $api_key, 'pageSize' => 100 ],
'https://generativelanguage.googleapis.com/v1beta/models'
);
$response = wp_remote_get(
$endpoint,
[
'timeout' => 20,
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['error']['message'] ) ) {
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
}
if ( empty( $body['models'] ) || ! is_array( $body['models'] ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
}
$models = [];
foreach ( $body['models'] as $model ) {
if ( ! empty( $model['name'] ) ) {
$parts = explode( '/', $model['name'] );
$models[] = sanitize_text_field( end( $parts ) );
}
}
if ( empty( $models ) ) {
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
}
return $models;
}
public function generate_content( array $args ) {
$settings = isset( $args['settings'] ) ? (array) $args['settings'] : [];
$prompt = isset( $args['prompt'] ) ? $args['prompt'] : '';
$system_prompt = isset( $args['system_prompt'] ) ? $args['system_prompt'] : '';
$model = ! empty( $args['model'] ) ? $args['model'] : $this->get_default_model();
$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() ) );
}
$endpoint = add_query_arg(
'key',
$api_key,
sprintf( 'https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent', rawurlencode( $model ) )
);
$payload = [
'contents' => [
[
'role' => 'user',
'parts' => [
[
'text' => $system_prompt . "\n\n" . $prompt,
],
],
],
],
'generationConfig' => [
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
'maxOutputTokens' => 1024,
],
];
$response = wp_remote_post(
$endpoint,
[
'headers' => [
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $payload ),
'timeout' => isset( $args['timeout'] ) ? (int) $args['timeout'] : 60,
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['error']['message'] ) ) {
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
}
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() )
);
}
$parts = $body['candidates'][0]['content']['parts'];
$texts = [];
foreach ( $parts as $part ) {
if ( isset( $part['text'] ) ) {
$texts[] = $part['text'];
}
}
$content = trim( implode( "\n\n", array_filter( $texts ) ) );
$usage = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : [];
return [
'content' => $content,
'usage' => $usage,
'raw_response' => $body,
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
class Groq_AI_Provider_Groq extends Groq_AI_Abstract_OpenAI_Provider {
public function get_key() {
return 'groq';
}
public function get_label() {
return __( 'Groq', 'groq-ai-product-text' );
}
public function get_default_model() {
return 'llama3-70b-8192';
}
public function get_available_models() {
return [
'llama3-70b-8192',
'llama3-8b-8192',
'mixtral-8x7b-32768',
'gemma-7b-it',
];
}
public function get_option_key() {
return 'groq_api_key';
}
protected function get_endpoint() {
return 'https://api.groq.com/openai/v1/chat/completions';
}
protected function get_models_endpoint() {
return 'https://api.groq.com/openai/v1/models';
}
}

View File

@@ -0,0 +1,24 @@
<?php
class Groq_AI_Provider_Manager {
/** @var Groq_AI_Provider_Interface[] */
private $providers = [];
public function __construct() {
$this->register_provider( new Groq_AI_Provider_Groq() );
$this->register_provider( new Groq_AI_Provider_OpenAI() );
$this->register_provider( new Groq_AI_Provider_Google() );
}
public function register_provider( Groq_AI_Provider_Interface $provider ) {
$this->providers[ $provider->get_key() ] = $provider;
}
public function get_providers() {
return $this->providers;
}
public function get_provider( $key ) {
return isset( $this->providers[ $key ] ) ? $this->providers[ $key ] : null;
}
}

View File

@@ -0,0 +1,36 @@
<?php
class Groq_AI_Provider_OpenAI extends Groq_AI_Abstract_OpenAI_Provider {
public function get_key() {
return 'openai';
}
public function get_label() {
return __( 'OpenAI', 'groq-ai-product-text' );
}
public function get_default_model() {
return 'gpt-4o-mini';
}
public function get_available_models() {
return [
'gpt-4o',
'gpt-4o-mini',
'gpt-4.1-mini',
'gpt-3.5-turbo',
];
}
public function get_option_key() {
return 'openai_api_key';
}
protected function get_endpoint() {
return 'https://api.openai.com/v1/chat/completions';
}
protected function get_models_endpoint() {
return 'https://api.openai.com/v1/models';
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Beheert conversatie-ID's per provider + context-hash.
*
* Verplaatst vanuit groq-ai-product-text.php:
* - ensure_conversation_id()
* - get/save_conversation_states()
* - get_context_hash()
*/
class Groq_AI_Conversation_Manager {
/** @var string */
private $option_key;
public function __construct( $option_key ) {
$this->option_key = $option_key;
}
/**
* Retourneert of creëert een conversatie-ID.
*
* @param string $provider_key
* @param string $store_context
* @return string
*/
public function ensure_id( $provider_key, $store_context ) {
$states = $this->get_states();
$context_hash = $this->get_context_hash( $store_context );
if ( isset( $states[ $provider_key ]['hash'], $states[ $provider_key ]['id'] ) && $states[ $provider_key ]['hash'] === $context_hash ) {
return $states[ $provider_key ]['id'];
}
$conversation_id = wp_generate_uuid4();
$states[ $provider_key ] = [
'hash' => $context_hash,
'id' => $conversation_id,
];
$this->save_states( $states );
return $conversation_id;
}
/**
* @return array
*/
private function get_states() {
$states = get_option( $this->option_key, [] );
return is_array( $states ) ? $states : [];
}
/**
* @param array $states
* @return void
*/
private function save_states( $states ) {
update_option( $this->option_key, $states, false );
}
/**
* @param string $store_context
* @return string
*/
private function get_context_hash( $store_context ) {
return md5( wp_json_encode( trim( (string) $store_context ) ) );
}
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* Loggingservice voor AI-generaties en DB-tabellen.
*
* Te importeren logica:
* - log_generation_event()
* - log_debug()
* - get_logs_table_name()/create_logs_table()/maybe_create_logs_table()
*/
class Groq_AI_Generation_Logger {
const OPTION_TABLE_CREATED = 'groq_ai_logs_table_created';
/** @var WC_Logger|null */
private $woo_logger = null;
/** @var bool|null */
private $logs_table_exists = null;
public function log_generation_event( array $args ) {
if ( ! $this->logs_table_exists() ) {
return;
}
global $wpdb;
$table = $this->get_logs_table_name();
$usage = isset( $args['usage'] ) && is_array( $args['usage'] ) ? $args['usage'] : [];
$prompt_tokens = isset( $usage['prompt_tokens'] ) ? absint( $usage['prompt_tokens'] ) : null;
$completion_tokens = isset( $usage['completion_tokens'] ) ? absint( $usage['completion_tokens'] ) : null;
$total_tokens = isset( $usage['total_tokens'] ) ? absint( $usage['total_tokens'] ) : null;
$wpdb->insert(
$table,
[
'created_at' => current_time( 'mysql' ),
'user_id' => get_current_user_id(),
'post_id' => isset( $args['post_id'] ) ? absint( $args['post_id'] ) : 0,
'provider' => isset( $args['provider'] ) ? sanitize_text_field( $args['provider'] ) : '',
'model' => isset( $args['model'] ) ? sanitize_text_field( $args['model'] ) : '',
'prompt' => isset( $args['prompt'] ) ? $args['prompt'] : '',
'response' => isset( $args['response'] ) ? $args['response'] : '',
'tokens_prompt' => $prompt_tokens,
'tokens_completion' => $completion_tokens,
'tokens_total' => $total_tokens,
'status' => isset( $args['status'] ) ? sanitize_text_field( $args['status'] ) : 'success',
'error_message' => isset( $args['error_message'] ) ? $args['error_message'] : '',
'usage_json' => ! empty( $usage ) ? wp_json_encode( $usage ) : null,
]
);
}
public function log_debug( $message, $context = [] ) {
if ( class_exists( 'WC_Logger' ) ) {
if ( ! $this->woo_logger ) {
$this->woo_logger = wc_get_logger();
}
if ( $this->woo_logger ) {
$context_string = ! empty( $context ) ? ' ' . wp_json_encode( $context ) : '';
$this->woo_logger->debug( '[GroqAI] ' . $message . $context_string, [ 'source' => 'groq-ai-product-text' ] );
return;
}
}
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
$entry = '[GroqAI] ' . $message;
if ( ! empty( $context ) ) {
$entry .= ' ' . wp_json_encode( $context );
}
error_log( $entry );
}
}
public function maybe_create_table() {
if ( get_option( self::OPTION_TABLE_CREATED ) ) {
$this->logs_table_exists = true;
return;
}
$this->create_table();
}
public function create_table() {
global $wpdb;
$table = $this->get_logs_table_name();
$charset_collate = $wpdb->get_charset_collate();
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$sql = "CREATE TABLE {$table} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
created_at datetime NOT NULL,
user_id bigint(20) unsigned DEFAULT NULL,
post_id bigint(20) unsigned DEFAULT NULL,
provider varchar(50) NOT NULL,
model varchar(100) NOT NULL,
prompt longtext NOT NULL,
response longtext DEFAULT NULL,
tokens_prompt int unsigned DEFAULT NULL,
tokens_completion int unsigned DEFAULT NULL,
tokens_total int unsigned DEFAULT NULL,
status varchar(20) NOT NULL,
error_message text DEFAULT NULL,
usage_json longtext DEFAULT NULL,
PRIMARY KEY (id),
KEY provider (provider),
KEY post_id (post_id)
) {$charset_collate};";
dbDelta( $sql );
$this->logs_table_exists = true;
update_option( self::OPTION_TABLE_CREATED, 1 );
}
private function get_logs_table_name() {
global $wpdb;
return $wpdb->prefix . 'groq_ai_generation_logs';
}
private function logs_table_exists() {
if ( null !== $this->logs_table_exists ) {
return $this->logs_table_exists;
}
global $wpdb;
$table = $this->get_logs_table_name();
$result = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
$this->logs_table_exists = ( $result === $table );
return $this->logs_table_exists;
}
}

View File

@@ -0,0 +1,353 @@
<?php
/**
* Bouwt prompts, verwerkt responses en verzamelt context.
*/
class Groq_AI_Prompt_Builder {
/** @var Groq_AI_Settings_Manager */
private $settings_manager;
public function __construct( Groq_AI_Settings_Manager $settings_manager ) {
$this->settings_manager = $settings_manager;
}
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' );
if ( $context ) {
$base_instruction = sprintf(
__( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende context indien beschikbaar: %s', 'groq-ai-product-text' ),
$context
);
}
return sprintf(
__( 'Conversatie-ID: %1$s. %2$s', 'groq-ai-product-text' ),
$conversation_id,
$base_instruction
);
}
public function append_response_instructions( $prompt, $settings ) {
$instructions = (string) ( $this->get_structured_response_instructions( $settings ) ?? '' );
$prompt = trim( (string) $prompt );
if ( '' === $instructions ) {
return $prompt;
}
if ( false !== strpos( $prompt, $instructions ) ) {
return $prompt;
}
return $prompt . "\n\n" . $instructions;
}
public function parse_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' ) );
}
$clean = trim( $raw );
if ( preg_match( '/```(?:json)?\s*(.*?)```/is', $clean, $matches ) ) {
$clean = trim( $matches[1] );
}
$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' ) );
}
$fields = [
'title' => trim( (string) ( $decoded['title'] ?? '' ) ),
'short_description' => trim( (string) ( $decoded['short_description'] ?? '' ) ),
'description' => trim( (string) ( $decoded['description'] ?? '' ) ),
];
if ( $this->settings_manager->is_module_enabled( 'rankmath', $settings ) ) {
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
$focus_keywords = [];
$raw_keyword_set = isset( $decoded['focus_keywords'] ) ? $decoded['focus_keywords'] : [];
if ( is_array( $raw_keyword_set ) ) {
foreach ( $raw_keyword_set as $keyword ) {
$keyword = trim( (string) $keyword );
if ( '' !== $keyword ) {
$focus_keywords[] = $keyword;
}
}
} elseif ( is_string( $raw_keyword_set ) ) {
$parts = preg_split( '/[,\\n]+/', $raw_keyword_set );
if ( is_array( $parts ) ) {
foreach ( $parts as $part ) {
$part = trim( (string) $part );
if ( '' !== $part ) {
$focus_keywords[] = $part;
}
}
}
}
$focus_keywords = array_slice( array_unique( $focus_keywords ), 0, $keyword_limit );
$fields['meta_title'] = $this->truncate_meta_field( (string) ( $decoded['meta_title'] ?? '' ), 60 );
$fields['meta_description'] = $this->truncate_meta_field( (string) ( $decoded['meta_description'] ?? '' ), 160 );
$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' ) );
}
return $fields;
}
public function parse_context_fields_from_request( $raw, $settings ) {
if ( empty( $raw ) ) {
return $settings['context_fields'];
}
$decoded = json_decode( wp_unslash( $raw ), true );
if ( ! is_array( $decoded ) ) {
return $settings['context_fields'];
}
$normalized = $this->settings_manager->normalize_context_fields( $decoded );
if ( ! array_filter( $normalized ) ) {
return $settings['context_fields'];
}
return $normalized;
}
public function build_product_context_block( $post_id, $fields ) {
$post_id = absint( $post_id );
if ( ! $post_id ) {
return '';
}
$parts = [];
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 ) );
}
}
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 ) );
}
}
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 ) );
}
}
if ( ! empty( $fields['attributes'] ) ) {
$attributes = $this->get_product_attributes_text( $post_id );
if ( $attributes ) {
$parts[] = sprintf( __( 'Attributen: %s', 'groq-ai-product-text' ), $attributes );
}
}
return implode( "\n\n", array_filter( $parts ) );
}
public function prepend_context_to_prompt( $prompt, $context ) {
$context = trim( (string) $context );
if ( '' === $context ) {
return $prompt;
}
$intro = __( 'Gebruik de volgende productcontext bij het schrijven:', 'groq-ai-product-text' );
return $intro . "\n" . $context . "\n\n" . $prompt;
}
public function get_response_format_definition( $settings = null ) {
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
$properties = [
'title' => [
'type' => 'string',
'description' => __( 'Korte, overtuigende producttitel in het Nederlands.', 'groq-ai-product-text' ),
'minLength' => 3,
],
'short_description' => [
'type' => 'string',
'description' => __( "Korte HTML-beschrijving in <p>-tags (maximaal 2 alinea's).", 'groq-ai-product-text' ),
'minLength' => 10,
],
'description' => [
'type' => 'string',
'description' => __( 'Uitgebreide HTML-productbeschrijving met paragrafen en eventueel lijsten.', 'groq-ai-product-text' ),
'minLength' => 20,
],
];
if ( $rankmath_enabled ) {
$properties['meta_title'] = [
'type' => 'string',
'description' => sprintf(
/* translators: 1: maximum character count, 2: maximum pixels */
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
60,
$title_pixels
),
'maxLength' => 120,
];
$properties['meta_description'] = [
'type' => 'string',
'description' => sprintf(
/* translators: 1: maximum character count, 2: maximum pixels */
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
160,
$desc_pixels
),
'maxLength' => 320,
];
$properties['focus_keywords'] = [
'type' => 'array',
'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', 'groq-ai-product-text' ),
'maxItems' => max( 1, $keyword_limit ),
'items' => [
'type' => 'string',
'minLength' => 1,
],
];
}
$schema = [
'type' => 'object',
'properties' => $properties,
'required' => [ 'title', 'short_description', 'description' ],
'additionalProperties' => false,
];
return [
'type' => 'json_schema',
'json_schema' => [
'name' => 'groq_ai_product_text',
'schema' => $schema,
],
];
}
private function get_structured_response_instructions( $settings = null ) {
$schema_parts = [
'"title":"..."',
'"short_description":"..."',
'"description":"..."',
];
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
if ( $rankmath_enabled ) {
$schema_parts[] = '"meta_title":"..."';
$schema_parts[] = '"meta_description":"..."';
$schema_parts[] = '"focus_keywords":["...","..."]';
}
$json_structure = '{' . implode( ',', $schema_parts ) . '}';
$instruction = sprintf(
/* 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' ),
$json_structure
);
if ( $rankmath_enabled ) {
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
$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' ),
$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' );
return $instruction;
}
private function truncate_meta_field( $text, $limit ) {
$text = trim( (string) $text );
if ( '' === $text || $limit <= 0 ) {
return '';
}
if ( function_exists( 'mb_strlen' ) ) {
if ( mb_strlen( $text ) <= $limit ) {
return $text;
}
return mb_substr( $text, 0, $limit );
}
if ( strlen( $text ) <= $limit ) {
return $text;
}
return substr( $text, 0, $limit );
}
private function get_product_attributes_text( $post_id ) {
if ( ! function_exists( 'wc_get_product' ) ) {
return '';
}
$product = wc_get_product( $post_id );
if ( ! $product ) {
return '';
}
$attributes = $product->get_attributes();
if ( empty( $attributes ) ) {
return '';
}
$lines = [];
foreach ( $attributes as $attribute ) {
if ( $attribute->is_taxonomy() ) {
$terms = wc_get_product_terms( $post_id, $attribute->get_name(), [ 'fields' => 'names' ] );
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $terms ) );
$label = wc_attribute_label( $attribute->get_name() );
} else {
$options = $attribute->get_options();
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $options ) );
$label = sanitize_text_field( $attribute->get_name() );
}
$value = trim( $value );
if ( '' !== $value ) {
$lines[] = sprintf( '%s: %s', $label, $value );
}
}
return implode( '; ', $lines );
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* Beheert alle plugininstellingen: ophalen, standaardiseren en sanitizen.
*/
class Groq_AI_Settings_Manager {
/** @var string */
private $option_key;
/** @var Groq_AI_Provider_Manager */
private $provider_manager;
/** @var array|null */
private $context_field_definitions = null;
/** @var array|null */
private $default_modules = null;
public function __construct( $option_key, Groq_AI_Provider_Manager $provider_manager ) {
$this->option_key = $option_key;
$this->provider_manager = $provider_manager;
}
/**
* Geeft samengestelde instellingen terug (voormalige get_settings()).
*
* @return array
*/
public function all() {
$defaults = [
'provider' => 'groq',
'model' => '',
'store_context' => '',
'default_prompt' => '',
'groq_api_key' => '',
'openai_api_key' => '',
'google_api_key' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'response_format_compat' => false,
];
$settings = get_option( $this->option_key, [] );
$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'] : [] );
return $settings;
}
/**
* Sanitizelogica voor register_setting callback.
*
* @param array $input
* @return array
*/
public function sanitize( $input ) {
$base_defaults = [
'provider' => 'groq',
'model' => '',
'store_context' => '',
'default_prompt' => '',
'groq_api_key' => '',
'openai_api_key' => '',
'google_api_key' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'response_format_compat' => false,
];
$current_settings = $this->all();
$defaults = wp_parse_args( $current_settings, $base_defaults );
$raw_input = (array) $input;
$input = wp_parse_args( $raw_input, $defaults );
$context_posted = array_key_exists( 'context_fields', $raw_input );
$modules_posted = array_key_exists( 'modules', $raw_input );
$provider = sanitize_text_field( $input['provider'] );
if ( ! $this->provider_manager->get_provider( $provider ) ) {
$provider = 'groq';
}
return [
'provider' => $provider,
'model' => sanitize_text_field( $input['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'] ),
'modules' => $this->sanitize_modules_settings(
$modules_posted ? $raw_input['modules'] : [],
$defaults['modules'],
isset( $current_settings['modules'] ) ? (array) $current_settings['modules'] : $this->get_default_modules_settings(),
$modules_posted
),
];
}
public function get_context_field_definitions() {
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' ),
'default' => true,
],
'short_description' => [
'label' => __( 'Korte beschrijving', 'groq-ai-product-text' ),
'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', 'groq-ai-product-text' ),
'default' => true,
],
'description' => [
'label' => __( 'Volledige beschrijving', 'groq-ai-product-text' ),
'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', 'groq-ai-product-text' ),
'default' => true,
],
'attributes' => [
'label' => __( 'Attributen', 'groq-ai-product-text' ),
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', 'groq-ai-product-text' ),
'default' => false,
],
];
}
return $this->context_field_definitions;
}
public function get_default_context_fields() {
$definitions = $this->get_context_field_definitions();
$defaults = [];
foreach ( $definitions as $key => $data ) {
$defaults[ $key ] = ! empty( $data['default'] );
}
return $defaults;
}
public function normalize_context_fields( $fields ) {
$definitions = $this->get_context_field_definitions();
$normalized = [];
foreach ( $definitions as $key => $data ) {
$normalized[ $key ] = false;
}
if ( ! is_array( $fields ) ) {
return $normalized;
}
foreach ( $fields as $key => $value ) {
if ( is_int( $key ) ) {
$key = sanitize_text_field( $value );
$value = true;
}
if ( array_key_exists( $key, $normalized ) ) {
$normalized[ $key ] = (bool) $value;
}
}
return $normalized;
}
public function get_default_modules_settings() {
if ( null === $this->default_modules ) {
$this->default_modules = [
'rankmath' => [
'enabled' => true,
'focus_keyword_limit' => 3,
'meta_title_pixel_limit' => 580,
'meta_description_pixel_limit' => 920,
],
];
}
return $this->default_modules;
}
public function get_module_config( $module, $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
$defaults = $this->get_default_modules_settings();
$modules = isset( $settings['modules'] ) && is_array( $settings['modules'] ) ? $settings['modules'] : [];
$config = isset( $modules[ $module ] ) ? (array) $modules[ $module ] : [];
return wp_parse_args( $config, isset( $defaults[ $module ] ) ? $defaults[ $module ] : [] );
}
public function is_module_enabled( $module, $settings = null ) {
$config = $this->get_module_config( $module, $settings );
return ! empty( $config['enabled'] );
}
public function get_rankmath_focus_keyword_limit( $settings = null ) {
$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 ) );
}
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
$config = $this->get_module_config( 'rankmath', $settings );
$value = isset( $config['meta_title_pixel_limit'] ) ? absint( $config['meta_title_pixel_limit'] ) : 580;
return max( 200, min( 1200, $value ) );
}
public function get_rankmath_meta_description_pixel_limit( $settings = null ) {
$config = $this->get_module_config( 'rankmath', $settings );
$value = isset( $config['meta_description_pixel_limit'] ) ? absint( $config['meta_description_pixel_limit'] ) : 920;
return max( 200, min( 2000, $value ) );
}
public function is_response_format_compat_enabled( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
return ! empty( $settings['response_format_compat'] );
}
/**
* @param array|null $modules
* @param array|null $defaults
* @param array|null $current
* @param bool $is_posted
* @return array
*/
private function sanitize_modules_settings( $modules, $defaults = null, $current = null, $is_posted = false ) {
$module_defaults = $this->get_default_modules_settings();
if ( ! is_array( $defaults ) ) {
$defaults = $module_defaults;
}
if ( ! is_array( $current ) ) {
$current = $defaults;
}
$current = wp_parse_args( $current, $defaults );
if ( ! is_array( $modules ) ) {
$modules = [];
}
if ( ! $is_posted ) {
$clean = [];
foreach ( $module_defaults as $module_key => $module_default_config ) {
$raw = isset( $modules[ $module_key ] ) ? (array) $modules[ $module_key ] : [];
$clean[ $module_key ] = wp_parse_args( $raw, isset( $current[ $module_key ] ) ? $current[ $module_key ] : $module_default_config );
}
return $clean;
}
$result = $current;
foreach ( $module_defaults as $module_key => $module_default_config ) {
$raw = isset( $modules[ $module_key ] ) ? (array) $modules[ $module_key ] : [];
$current_config = isset( $current[ $module_key ] ) ? (array) $current[ $module_key ] : $module_default_config;
$result[ $module_key ]['enabled'] = isset( $raw['enabled'] ) ? (bool) $raw['enabled'] : ( isset( $current_config['enabled'] ) ? (bool) $current_config['enabled'] : false );
if ( 'rankmath' === $module_key ) {
$limit = isset( $raw['focus_keyword_limit'] ) ? absint( $raw['focus_keyword_limit'] ) : ( isset( $current_config['focus_keyword_limit'] ) ? absint( $current_config['focus_keyword_limit'] ) : $module_default_config['focus_keyword_limit'] );
if ( $limit <= 0 ) {
$limit = $module_default_config['focus_keyword_limit'];
}
$result[ $module_key ]['focus_keyword_limit'] = max( 1, min( 10, $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 ) {
$title_pixel_limit = $module_default_config['meta_title_pixel_limit'];
}
$result[ $module_key ]['meta_title_pixel_limit'] = max( 200, min( 1200, $title_pixel_limit ) );
$pixel_limit = isset( $raw['meta_description_pixel_limit'] ) ? absint( $raw['meta_description_pixel_limit'] ) : ( isset( $current_config['meta_description_pixel_limit'] ) ? absint( $current_config['meta_description_pixel_limit'] ) : $module_default_config['meta_description_pixel_limit'] );
if ( $pixel_limit <= 0 ) {
$pixel_limit = $module_default_config['meta_description_pixel_limit'];
}
$result[ $module_key ]['meta_description_pixel_limit'] = max( 200, min( 2000, $pixel_limit ) );
}
}
return $result;
}
}