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:
102
.github/workflows/release.yml
vendored
Normal file
102
.github/workflows/release.yml
vendored
Normal 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
20
.gitignore
vendored
Normal 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
93
README.md
Normal 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
178
SitiWebUpdater.php
Normal 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
236
assets/css/admin.css
Normal 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
31
assets/css/settings.css
Normal 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
381
assets/js/admin.js
Normal 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
194
assets/js/settings.js
Normal 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
49
docker-compose.yml
Normal 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:
|
||||
13
docker/wordpress/Dockerfile
Normal file
13
docker/wordpress/Dockerfile
Normal 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
335
groq-ai-product-text.php
Normal 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();
|
||||
160
includes/Admin/class-groq-ai-logs-table.php
Normal file
160
includes/Admin/class-groq-ai-logs-table.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
212
includes/Admin/class-groq-ai-product-ui.php
Normal file
212
includes/Admin/class-groq-ai-product-ui.php
Normal 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' ); ?>">×</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
|
||||
}
|
||||
}
|
||||
569
includes/Admin/class-groq-ai-settings-page.php
Normal file
569
includes/Admin/class-groq-ai-settings-page.php
Normal 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, USP’s 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' ); ?>">×</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 );
|
||||
}
|
||||
}
|
||||
21
includes/Contracts/interface-groq-ai-provider.php
Normal file
21
includes/Contracts/interface-groq-ai-provider.php
Normal 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();
|
||||
}
|
||||
156
includes/Core/class-groq-ai-ajax-controller.php
Normal file
156
includes/Core/class-groq-ai-ajax-controller.php
Normal 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;
|
||||
}
|
||||
}
|
||||
41
includes/Core/class-groq-ai-service-container.php
Normal file
41
includes/Core/class-groq-ai-service-container.php
Normal 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 ];
|
||||
}
|
||||
}
|
||||
141
includes/Providers/class-groq-ai-abstract-openai-provider.php
Normal file
141
includes/Providers/class-groq-ai-abstract-openai-provider.php
Normal 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 ] : '';
|
||||
}
|
||||
}
|
||||
158
includes/Providers/class-groq-ai-provider-google.php
Normal file
158
includes/Providers/class-groq-ai-provider-google.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
36
includes/Providers/class-groq-ai-provider-groq.php
Normal file
36
includes/Providers/class-groq-ai-provider-groq.php
Normal 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';
|
||||
}
|
||||
}
|
||||
24
includes/Providers/class-groq-ai-provider-manager.php
Normal file
24
includes/Providers/class-groq-ai-provider-manager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
36
includes/Providers/class-groq-ai-provider-openai.php
Normal file
36
includes/Providers/class-groq-ai-provider-openai.php
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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 ) ) );
|
||||
}
|
||||
}
|
||||
138
includes/Services/Logging/class-groq-ai-generation-logger.php
Normal file
138
includes/Services/Logging/class-groq-ai-generation-logger.php
Normal 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;
|
||||
}
|
||||
}
|
||||
353
includes/Services/Prompt/class-groq-ai-prompt-builder.php
Normal file
353
includes/Services/Prompt/class-groq-ai-prompt-builder.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
295
includes/Services/Settings/class-groq-ai-settings-manager.php
Normal file
295
includes/Services/Settings/class-groq-ai-settings-manager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user