46 Commits

Author SHA1 Message Date
actions-bot
e02d29c49e chore: update manifest.json 2026-02-06 18:47:29 +00:00
5ed94f79d3 chore: bump version to 1.10.0 and enhance model selection functionality
Some checks failed
Build & Release Plugin / release (push) Failing after 10s
2026-02-06 18:47:21 +00:00
actions-bot
ee1873568d chore: update manifest.json 2026-02-01 15:44:21 +00:00
cf0269f421 chore: bump version to 1.9.9
All checks were successful
Build & Release Plugin / release (push) Successful in 10s
2026-02-01 15:44:14 +00:00
a352c53139 chore: add release workflow for plugin build and deployment 2026-02-01 15:43:42 +00:00
github-actions[bot]
b33d92a80e chore: update manifest.json 2026-02-01 04:46:37 +00:00
2a36fa7e31 chore: bump version to 1.9.8
All checks were successful
Build & Release Plugin / release (push) Successful in 14s
2026-02-01 04:46:27 +00:00
github-actions[bot]
4b05d67a82 chore: update manifest.json 2026-02-01 04:37:32 +00:00
71424567bf chore: bump version to 1.9.7
All checks were successful
Build & Release Plugin / release (push) Successful in 15s
2026-02-01 04:36:30 +00:00
82da1180da fix: include owner and repository in license verification request
All checks were successful
Build & Release Plugin / release (push) Successful in 6s
2026-02-01 04:36:06 +00:00
2c749eebd5 fix: enhance license validation logic in SitiWebUpdater2
All checks were successful
Build & Release Plugin / release (push) Successful in 5s
2026-02-01 04:10:01 +00:00
github-actions[bot]
04a0c8be37 chore: update manifest.json 2026-02-01 04:02:03 +00:00
03727b9828 chore: bump version to 1.9.6
All checks were successful
Build & Release Plugin / release (push) Successful in 16s
2026-02-01 04:01:54 +00:00
github-actions[bot]
987139bca7 chore: update manifest.json 2026-02-01 03:59:23 +00:00
773db7407c chore: bump version to 1.9.5
Some checks failed
Build & Release Plugin / release (push) Failing after 9s
2026-02-01 03:59:14 +00:00
github-actions[bot]
a4da6e3e00 chore: update manifest.json 2026-02-01 03:55:48 +00:00
ec08c79aab feat: update version to 1.9.4 and enhance license validation handling
All checks were successful
Build & Release Plugin / release (push) Successful in 20s
2026-02-01 03:55:36 +00:00
4a789363c5 feat: enhance SitiWebUpdater2 with global license management and automatic updates
All checks were successful
Build & Release Plugin / release (push) Successful in 12s
2026-02-01 03:46:14 +00:00
github-actions[bot]
c279ea7247 chore: update manifest.json 2026-02-01 03:05:10 +00:00
1e2214dd74 chore: bump version to 1.9.3
All checks were successful
Build & Release Plugin / release (push) Successful in 28s
2026-02-01 03:04:53 +00:00
70001df941 feat: implement new license validation and updater system with SitiWebUpdater2 2026-02-01 03:04:38 +00:00
github-actions[bot]
072e2d109f chore: update manifest.json 2026-02-01 01:23:44 +00:00
1015627822 chore: bump version to 1.9.2
All checks were successful
Build & Release Plugin / release (push) Successful in 10s
2026-02-01 01:23:38 +00:00
8f1fc80835 fix: update release workflow to handle API response and improve error reporting
All checks were successful
Build & Release Plugin / release (push) Successful in 4s
2026-02-01 01:22:50 +00:00
b73d858ca7 feat: implement Gitea release creation with asset upload
Some checks failed
Build & Release Plugin / release (push) Failing after 8s
2026-02-01 01:22:10 +00:00
aa4bfa33c0 fix: remove sudo from zip installation in release workflow
Some checks failed
Build & Release Plugin / release (push) Failing after 9s
2026-02-01 01:13:22 +00:00
0b9cd99fb0 fix: ensure zip is installed before building distribution package
Some checks failed
Build & Release Plugin / release (push) Failing after 4s
2026-02-01 01:12:52 +00:00
eba8df9962 refactor: replace rsync with tar for packaging assets in release workflow
Some checks failed
Build & Release Plugin / release (push) Failing after 4s
2026-02-01 01:12:09 +00:00
f2a39e4660 refactor: update release workflow to improve tagging and asset packaging
Some checks failed
Build & Release Plugin / release (push) Failing after 4s
2026-02-01 01:11:15 +00:00
2f0a44706b fix: improve tag existence check by using local tags
Some checks failed
Generate Manifest & Tag / tag (push) Failing after 3s
2026-02-01 01:01:41 +00:00
10a46f8668 Merge branch 'main' of 192.168.1.206:roberto/siti-ai-product-content-generator
Some checks failed
Generate Manifest & Tag / tag (push) Failing after 3s
2026-02-01 01:00:33 +00:00
db981ba4a6 refactor: rename workflow to 'Generate Manifest & Tag' and simplify release process 2026-02-01 01:00:31 +00:00
github-actions[bot]
aa757710c9 chore: update manifest.json 2026-02-01 00:56:40 +00:00
1d587ce2d1 Merge branch 'main' of 192.168.1.206:roberto/siti-ai-product-content-generator
Some checks failed
Build & Release Plugin / release (push) Failing after 5s
2026-02-01 00:56:35 +00:00
0aaa7087a9 chore: downgrade version to 1.9.1 in plugin header 2026-02-01 00:56:09 +00:00
github-actions[bot]
68bce5006d chore: update manifest.json 2026-02-01 00:45:58 +00:00
4383982fb2 feat: update version to 1.9.2 and set custom API base URL in SitiWebUpdater
Some checks failed
Build & Release Plugin / release (push) Failing after 5s
2026-02-01 00:44:55 +00:00
github-actions[bot]
5f1dcaf352 chore: update manifest.json 2026-01-31 23:57:25 +00:00
62c688dba4 chore: bump version to 1.9.1 in plugin header 2026-01-31 23:01:39 +00:00
github-actions[bot]
42332635ba chore: update manifest.json 2026-01-31 19:42:48 +00:00
9ae516b77c feat: Add automated commit and push for manifest.json updates 2026-01-31 19:42:38 +00:00
4ff96d98e0 feat: Generate manifest.json from plugin header information 2026-01-31 19:40:26 +00:00
6cff0b6f58 Add core classes and tests for Groq AI compatibility, logging, and model services
- Implement Groq_AI_Compatibility_Service to manage WooCommerce dependency and admin notices.
- Create Groq_AI_Log_Scheduler for scheduled log cleanup based on settings.
- Develop Groq_AI_Model_Service for model selection and caching.
- Add language translations in POT file for Dutch.
- Set up PHPUnit configuration and bootstrap for testing.
- Implement unit tests for model exclusions, provider request building, settings management, and term saving functionality.
2026-01-31 17:48:46 +00:00
26aabdb2d8 Add admin classes for AI logs and settings management
- Introduced Groq_AI_Logs_Admin class to manage AI logs in the admin panel, including log viewing and detail rendering.
- Created Groq_AI_Settings_Renderer class for rendering settings fields in a structured manner.
- Implemented Groq_AI_Term_Admin_Base class to handle term-related functionalities, including term page registration and bulk actions for term descriptions.
- Enhanced term management with AJAX support for generating term descriptions and handling Rank Math integration.
2026-01-31 16:02:13 +00:00
3e74bcbf3a feat: Enhance logging by including request payload in generation responses 2026-01-31 14:58:01 +00:00
051db0febc feat: Add Google safety settings and enhance logging for AI generation events 2026-01-31 14:00:59 +00:00
40 changed files with 5981 additions and 1759 deletions

View File

@@ -0,0 +1,25 @@
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/**'
- 'languages/**'
jobs:
release:
uses: roberto/ci-workflows/.gitea/workflows/wp-plugin-release.yml@c6393ed47258d6f040ceeed3994b17b7faa3ef23
with:
main_file: groq-ai-product-text.php
slug: siti-ai-product-content-generator
release_body: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_notes || '' }}
secrets:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -1,102 +0,0 @@
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 }}

1
.phpunit.result.cache Normal file
View File

@@ -0,0 +1 @@
{"version":1,"defects":{"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":3,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":4,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":4,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":4,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":4},"times":{"ModelExclusionsTest::test_ensure_allowed_blocks_excluded_model":0.002,"ModelExclusionsTest::test_filter_models_removes_excluded_entries":0,"SettingsManagerTest::test_logs_retention_days_sanitized_and_capped":0,"SettingsManagerTest::test_logs_retention_days_allows_zero":0,"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":0,"SettingsManagerTest::test_sanitize_accepts_all_settings_keys":0,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":0,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":0,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":0,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":0}}

49
LICENSE_INTEGRATION.md Normal file
View File

@@ -0,0 +1,49 @@
# SitiWebUpdater2 - Globale Plugin Updater
Deze class gebruikt een globale registry zodat alle plugins die deze updater gebruiken hun licenties op één centrale pagina beheren.
## Gebruik
### Basis setup (per plugin)
```php
require_once 'path/to/SitiWebUpdater2.php';
$updater = new SitiWebUpdater2( __FILE__ );
$updater->set_owner( 'jouw-github-username' );
$updater->set_repository( 'jouw-plugin-repo' );
$updater->initialize();
```
### Automatische features
De updater doet alles automatisch:
1. **Globale license pagina**: Voegt één instellingen pagina toe onder Instellingen > Plugin Licenties
2. **Admin notices**: Toont rood bericht per plugin als licentie ongeldig is
3. **Dagelijkse checks**: Controleert dagelijks alle licenties
4. **Auto-updates**: Controleert op updates voor alle plugins
### Centrale licentie pagina
Alle plugins die SitiWebUpdater2 gebruiken verschijnen automatisch op de centrale pagina `Instellingen > Plugin Licenties`, waar je alle licentiecodes in één keer kunt beheren.
## Configuratie
- **Owner/Repo**: Stel in via `set_owner()` en `set_repository()`
- **API URL**: Standaard `https://plugins.robert.ooo`, aanpasbaar met `set_api_base_url()`
- **License key**: Wordt opgeslagen in eigen option key per plugin
## Option Keys (per plugin)
- `siti_updater_{plugin_slug}_license_key`: De licentiecode
- `siti_updater_{plugin_slug}_license_data`: Gecachte licentie data
- `siti_updater_{plugin_slug}_last_check`: Timestamp laatste check
## Cron Jobs (per plugin)
- `siti_updater_daily_check_siti_updater_{plugin_slug}_`: Dagelijkse licentie verificatie
## Herbruikbaarheid
Deze class is volledig herbruikbaar in meerdere plugins. Elke plugin registreert zichzelf automatisch in de globale registry en verschijnt op de centrale licentie pagina.

View File

@@ -1,202 +0,0 @@
<?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;
$this->set_plugin_properties();
add_action( 'admin_init', array( $this, 'set_plugin_properties' ) );
return $this;
}
public function set_plugin_properties() {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$this->plugin = get_plugin_data( $this->file, false, false );
$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
}
}
private function get_latest_version_from_response() {
if ( empty( $this->github_response['tag_name'] ) ) {
return null;
}
return ltrim( $this->github_response['tag_name'], 'vV' );
}
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
$latest_version = $this->get_latest_version_from_response();
if ( null === $latest_version ) {
return $transient;
}
$out_of_date = version_compare( $latest_version, $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' => $latest_version
);
$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
$latest_version = $this->get_latest_version_from_response();
if ( null === $latest_version ) {
return $result;
}
// 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' => $latest_version,
'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;
}
}

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<defs>
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6C38FF"/>
<stop offset="100%" stop-color="#00C7C7"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="48" fill="url(#g)"/>
<path fill="#ffffff" d="M68 176c-8.8 0-16-7.2-16-16v-64c0-8.8 7.2-16 16-16h56c8.8 0 16 7.2 16 16v12h-24v-4c0-3.3-2.7-6-6-6H84c-3.3 0-6 2.7-6 6v44c0 3.3 2.7 6 6 6h26c3.3 0 6-2.7 6-6v-4h24v12c0 8.8-7.2 16-16 16H68zm120-32h-48v-32h48v-16l32 32-32 32v-16z"/>
<text x="128" y="210" text-anchor="middle" font-family="'Montserrat', Arial, sans-serif" font-size="28" fill="#ffffff" opacity="0.9">SitiAI</text>
</svg>

After

Width:  |  Height:  |  Size: 735 B

View File

@@ -96,8 +96,35 @@
statusField.setAttribute('data-status', type); statusField.setAttribute('data-status', type);
} }
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...'; const localized = (window.GroqAIGenerator && GroqAIGenerator.strings) || {};
const retryText = window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.'; const loadingText = localized.loading || (window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...');
const retryText = localized.retry || (window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.');
const errorDefaultText = localized.errorDefault || (window.wp && wp.i18n ? wp.i18n.__('Er ging iets mis bij het genereren.', 'siti-ai-product-content-generator') : 'Er ging iets mis bij het genereren.');
const errorUnknownText = localized.errorUnknown || (window.wp && wp.i18n ? wp.i18n.__('Onbekende fout.', 'siti-ai-product-content-generator') : 'Onbekende fout.');
const successText = localized.success || (window.wp && wp.i18n ? wp.i18n.__('Structuur gegenereerd. Kopieer of vul velden in.', 'siti-ai-product-content-generator') : 'Structuur gegenereerd. Kopieer of vul velden in.');
const fieldAppliedText = localized.fieldApplied || (window.wp && wp.i18n ? wp.i18n.__('%s ingevuld.', 'siti-ai-product-content-generator') : '%s ingevuld.');
const fieldApplyErrorText = localized.fieldApplyError || (window.wp && wp.i18n ? wp.i18n.__('Kon het veld niet automatisch invullen.', 'siti-ai-product-content-generator') : 'Kon het veld niet automatisch invullen.');
const fieldCopiedText = localized.fieldCopied || (window.wp && wp.i18n ? wp.i18n.__('%s gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : '%s gekopieerd naar het klembord.');
const jsonCopiedText = localized.jsonCopied || (window.wp && wp.i18n ? wp.i18n.__('JSON gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : 'JSON gekopieerd naar het klembord.');
const copyFailedText = localized.copyFailed || (window.wp && wp.i18n ? wp.i18n.__('Kopiëren mislukt.', 'siti-ai-product-content-generator') : 'Kopiëren mislukt.');
function formatString(template, values) {
if (!template) {
return '';
}
let autoIndex = 0;
return template.replace(/%(\d+\$)?s/g, (match, position) => {
let valueIndex;
if (position) {
valueIndex = parseInt(position, 10) - 1;
} else {
valueIndex = autoIndex;
autoIndex += 1;
}
const replacement = values[valueIndex];
return typeof replacement === 'undefined' ? '' : String(replacement);
});
}
function toggleLoading(isLoading) { function toggleLoading(isLoading) {
modal.classList.toggle('is-loading', isLoading); modal.classList.toggle('is-loading', isLoading);
@@ -137,7 +164,7 @@
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
if (!json.success) { if (!json.success) {
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout'; const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText;
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -154,13 +181,12 @@
if (jsonCopyButton) { if (jsonCopyButton) {
jsonCopyButton.disabled = false; jsonCopyButton.disabled = false;
} }
setStatus('Structuur gegenereerd. Kopieer of vul velden in.', 'success'); setStatus(successText, 'success');
}) })
.catch((error) => { .catch((error) => {
const message = error && error.message ? error.message : 'Er ging iets mis bij het genereren.'; const message = error && error.message ? error.message : errorDefaultText;
setStatus(loadingText, 'error'); const fullMessage = `${errorDefaultText} ${message}. ${retryText}`;
const fullMessage = `${loadingText} ${message}. ${retryText}`; setStatus(fullMessage, 'error');
statusField.textContent = fullMessage;
}) })
.finally(() => { .finally(() => {
toggleLoading(false); toggleLoading(false);
@@ -308,10 +334,10 @@
} }
if (applied) { if (applied) {
setStatus(entry.label + ' ingevuld.', 'success'); setStatus(formatString(fieldAppliedText, [entry.label]), 'success');
setFieldStatus(fieldKey, 'success'); setFieldStatus(fieldKey, 'success');
} else { } else {
setStatus('Kon het veld niet automatisch invullen.', 'error'); setStatus(fieldApplyErrorText, 'error');
setFieldStatus(fieldKey, 'error'); setFieldStatus(fieldKey, 'error');
} }
} }
@@ -340,10 +366,10 @@
} }
copyToClipboard(entry.textarea.value) copyToClipboard(entry.textarea.value)
.then(() => { .then(() => {
setStatus(entry.label + ' gekopieerd naar het klembord.', 'success'); setStatus(formatString(fieldCopiedText, [entry.label]), 'success');
}) })
.catch(() => { .catch(() => {
setStatus('Kopiëren mislukt.', 'error'); setStatus(copyFailedText, 'error');
}); });
} }
@@ -358,10 +384,10 @@
const text = resultField ? resultField.textContent.trim() : ''; const text = resultField ? resultField.textContent.trim() : '';
copyToClipboard(text) copyToClipboard(text)
.then(() => { .then(() => {
setStatus('JSON gekopieerd naar het klembord.', 'success'); setStatus(jsonCopiedText, 'success');
}) })
.catch(() => { .catch(() => {
setStatus('Kopiëren mislukt.', 'error'); setStatus(copyFailedText, 'error');
}); });
}); });
} }

View File

@@ -14,6 +14,14 @@
return; return;
} }
const strings = data.strings || {};
const providerUnsupportedText = strings.providerUnsupported || 'Deze aanbieder ondersteunt dit niet.';
const apiKeyRequiredText = strings.apiKeyRequired || 'Vul eerst de API-sleutel in.';
const loadingModelsText = strings.loadingModels || 'Modellen worden opgehaald…';
const errorUnknownText = strings.errorUnknown || 'Onbekende fout';
const successModelsText = strings.successModels || 'Modellen bijgewerkt.';
const errorFetchText = strings.errorFetch || 'Ophalen mislukt.';
const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]'); const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]');
const modelSelect = document.getElementById('groq-ai-model-select'); const modelSelect = document.getElementById('groq-ai-model-select');
const refreshButton = document.getElementById('groq-ai-refresh-models'); const refreshButton = document.getElementById('groq-ai-refresh-models');
@@ -164,19 +172,19 @@
const provider = providerSelect ? providerSelect.value : data.currentProvider; const provider = providerSelect ? providerSelect.value : data.currentProvider;
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null; const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
if (!providerData || !providerData.supports_live) { if (!providerData || !providerData.supports_live) {
setRefreshStatus('Deze aanbieder ondersteunt dit niet.', 'error'); setRefreshStatus(providerUnsupportedText, 'error');
return; return;
} }
const keyField = document.querySelector('[data-provider-row="' + provider + '"] input'); const keyField = document.querySelector('[data-provider-row="' + provider + '"] input');
const apiKey = keyField ? keyField.value.trim() : ''; const apiKey = keyField ? keyField.value.trim() : '';
if (!apiKey) { if (!apiKey) {
setRefreshStatus('Vul eerst de API-sleutel in.', 'error'); setRefreshStatus(apiKeyRequiredText, 'error');
return; return;
} }
refreshButton.disabled = true; refreshButton.disabled = true;
setRefreshStatus('Modellen worden opgehaald…', 'loading'); setRefreshStatus(loadingModelsText, 'loading');
const payload = new URLSearchParams(); const payload = new URLSearchParams();
payload.append('action', 'groq_ai_refresh_models'); payload.append('action', 'groq_ai_refresh_models');
@@ -194,14 +202,14 @@
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
if (!json.success || !json.data || !Array.isArray(json.data.models)) { if (!json.success || !json.data || !Array.isArray(json.data.models)) {
throw new Error((json.data && json.data.message) || 'Onbekende fout'); throw new Error((json.data && json.data.message) || errorUnknownText);
} }
data.providers[provider].models = json.data.models; data.providers[provider].models = json.data.models;
buildModelOptions(); buildModelOptions();
setRefreshStatus('Modellen bijgewerkt.', 'success'); setRefreshStatus(successModelsText, 'success');
}) })
.catch((error) => { .catch((error) => {
setRefreshStatus(error.message || 'Ophalen mislukt.', 'error'); setRefreshStatus(error.message || errorFetchText, 'error');
}) })
.finally(() => { .finally(() => {
refreshButton.disabled = false; refreshButton.disabled = false;

View File

@@ -20,6 +20,14 @@
const includeTopProducts = document.getElementById('groq-ai-term-include-top-products'); const includeTopProducts = document.getElementById('groq-ai-term-include-top-products');
const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit'); const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit');
const strings = (window.GroqAITermGenerator && GroqAITermGenerator.strings) || {};
const promptRequiredText = strings.promptRequired || 'Vul eerst een prompt in.';
const loadingText = strings.loading || 'AI is bezig met schrijven...';
const successText = strings.success || 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.';
const applySuccessText = strings.applySuccess || 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.';
const errorDefaultText = strings.errorDefault || 'Er ging iets mis bij het genereren.';
const errorUnknownText = strings.errorUnknown || 'Onbekende fout';
function setStatus(message, type) { function setStatus(message, type) {
if (!statusField) { if (!statusField) {
return; return;
@@ -75,7 +83,7 @@
rankmathKeywordsField.value = outputFocusKeywordsField.value || ''; rankmathKeywordsField.value = outputFocusKeywordsField.value || '';
} }
setStatus('Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', 'success'); setStatus(applySuccessText, 'success');
}); });
} }
@@ -83,12 +91,12 @@
event.preventDefault(); event.preventDefault();
const prompt = promptField ? (promptField.value || '').trim() : ''; const prompt = promptField ? (promptField.value || '').trim() : '';
if (!prompt) { if (!prompt) {
setStatus('Vul eerst een prompt in.', 'error'); setStatus(promptRequiredText, 'error');
return; return;
} }
setLoading(true); setLoading(true);
setStatus('AI is bezig met schrijven...', 'loading'); setStatus(loadingText, 'loading');
if (rawField) { if (rawField) {
rawField.textContent = ''; rawField.textContent = '';
} }
@@ -109,7 +117,7 @@
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
if (!json.success) { if (!json.success) {
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout'; const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText;
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -137,10 +145,10 @@
rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim(); rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();
} }
setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success'); setStatus(successText, 'success');
}) })
.catch((error) => { .catch((error) => {
setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error'); setStatus(error && error.message ? error.message : errorDefaultText, 'error');
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);

View File

@@ -11,6 +11,13 @@
const strings = data.strings || {}; const strings = data.strings || {};
const allowRegenerate = !!data.allowRegenerate; const allowRegenerate = !!data.allowRegenerate;
const unknownErrorText = strings.unknownError || 'Onbekende fout';
const unknownTermText = strings.unknownTerm || 'Onbekende term.';
const confirmStopFallbackText = strings.confirmStopFallback || 'Stoppen?';
const logErrorDefaultText = strings.logErrorDefault || '%1$s: %2$s';
const logSuccessDefaultText = strings.logSuccessDefault || '%1$s gevuld.';
const regenerateErrorDefaultText = strings.regenerateErrorDefault || '%1$s mislukt: %2$s';
const regenerateDoneDefaultText = strings.regenerateDoneDefault || '%s is bijgewerkt.';
const terms = (Array.isArray(data.terms) ? data.terms : []) const terms = (Array.isArray(data.terms) ? data.terms : [])
.map((term) => { .map((term) => {
const id = parseInt(term.id, 10); const id = parseInt(term.id, 10);
@@ -146,19 +153,19 @@
function handleResponse(term, json, context) { function handleResponse(term, json, context) {
if (!json || !json.success) { if (!json || !json.success) {
const errorMessage = (json && json.data && json.data.message) || 'Onbekende fout'; const errorMessage = (json && json.data && json.data.message) || unknownErrorText;
appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, errorMessage]), 'error'); appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, errorMessage]), 'error');
if (context === 'single') { if (context === 'single') {
setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, errorMessage]), 'error'); setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [term.name || term.id, errorMessage]), 'error');
} }
return false; return false;
} }
const words = json.data && typeof json.data.words !== 'undefined' ? parseInt(json.data.words, 10) : term.words; const words = json.data && typeof json.data.words !== 'undefined' ? parseInt(json.data.words, 10) : term.words;
markTermCompleted(term, Number.isFinite(words) ? words : term.words); markTermCompleted(term, Number.isFinite(words) ? words : term.words);
appendLog(formatString(strings.logSuccess || '%1$s gevuld.', [term.name || term.id, term.words]), 'success'); appendLog(formatString(strings.logSuccess || logSuccessDefaultText, [term.name || term.id, term.words]), 'success');
if (context === 'single') { if (context === 'single') {
setStatus(formatString(strings.regenerateDone || '%s is bijgewerkt.', [term.name || term.id]), 'success'); setStatus(formatString(strings.regenerateDone || regenerateDoneDefaultText, [term.name || term.id]), 'success');
} }
return true; return true;
} }
@@ -233,7 +240,7 @@
if (!isRunning) { if (!isRunning) {
return; return;
} }
const confirmation = strings.confirmStop ? window.confirm(strings.confirmStop) : window.confirm('Stoppen?'); const confirmation = strings.confirmStop ? window.confirm(strings.confirmStop) : window.confirm(confirmStopFallbackText);
if (confirmation) { if (confirmation) {
abortRequested = true; abortRequested = true;
} }
@@ -251,7 +258,7 @@
const termId = parseInt(button.getAttribute('data-term-id'), 10); const termId = parseInt(button.getAttribute('data-term-id'), 10);
const term = termMap.get(termId); const term = termMap.get(termId);
if (!term) { if (!term) {
setStatus('Onbekende term.', 'error'); setStatus(unknownTermText, 'error');
return; return;
} }
if (strings.confirmRegenerate) { if (strings.confirmRegenerate) {
@@ -270,9 +277,9 @@
handleResponse(term, json, 'single'); handleResponse(term, json, 'single');
}) })
.catch((error) => { .catch((error) => {
const message = error && error.message ? error.message : 'Onbekende fout'; const message = error && error.message ? error.message : unknownErrorText;
appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, message]), 'error'); appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, message]), 'error');
setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, message]), 'error'); setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [term.name || term.id, message]), 'error');
}) })
.finally(() => { .finally(() => {
button.disabled = false; button.disabled = false;

16
composer.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "sitiweb/siti-ai-product-content-generator",
"description": "SitiAI Product Teksten WordPress plugin",
"type": "wordpress-plugin",
"require-dev": {
"phpunit/phpunit": "^9.6"
},
"autoload-dev": {
"classmap": [
"includes/"
]
},
"scripts": {
"test": "phpunit"
}
}

1815
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,9 @@
/** /**
* Plugin Name: SitiAI Product Teksten * Plugin Name: SitiAI Product Teksten
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce. * Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
* Version: 1.7.0 * Version: 1.10.0
* Author: SitiAI * Author: Roberto Guagliardo | SitiWeb
* Author URI: https://sitiweb.nl/
* Text Domain: siti-ai-product-content-generator * Text Domain: siti-ai-product-content-generator
* Domain Path: /languages * Domain Path: /languages
*/ */
@@ -44,6 +45,9 @@ if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEB
require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php'; require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-compatibility-service.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-model-service.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-log-scheduler.php';
require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.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-abstract-openai-provider.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php'; require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php';
@@ -58,16 +62,22 @@ require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-oauth-cli
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-search-console-client.php'; require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-search-console-client.php';
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-analytics-data-client.php'; require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-analytics-data-client.php';
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-context-builder.php'; require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-context-builder.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-admin-base.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-term-admin-base.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-categories-admin.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-brands-admin.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-admin.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-renderer.php';
if( ! class_exists( 'SitiWebUpdater' ) ){ if( ! class_exists( 'SitiWebUpdater2' ) ){
include_once( plugin_dir_path( __FILE__ ) . 'SitiWebUpdater.php' ); include_once( plugin_dir_path( __FILE__ ) . 'includes/SitiWebUpdater2.php' );
} }
$updater = new SitiWebUpdater( __FILE__ ); $updater = new SitiWebUpdater2( __FILE__ );
$updater->set_username( 'SitiWeb' ); $updater->set_owner( 'roberto' );
$updater->set_repository( 'siti-ai-product-content-generator' ); $updater->set_repository( 'siti-ai-product-content-generator' );
$updater->initialize(); $updater->initialize();
@@ -85,14 +95,29 @@ final class Groq_AI_Product_Text_Plugin {
/** @var Groq_AI_Service_Container */ /** @var Groq_AI_Service_Container */
private $container; private $container;
/** @var */ /** @var Groq_AI_Product_Text_Settings_Page */
private $settings_page; private $settings_page;
/** @var Groq_AI_Categories_Admin */
private $categories_admin;
/** @var Groq_AI_Brands_Admin */
private $brands_admin;
/** @var Groq_AI_Logs_Admin */
private $logs_admin;
/** @var Groq_AI_Product_Text_Product_UI */ /** @var Groq_AI_Product_Text_Product_UI */
private $product_ui; private $product_ui;
/** @var bool */ /** @var Groq_AI_Compatibility_Service */
private $missing_wc_notice = false; private $compatibility_service;
/** @var Groq_AI_Model_Service */
private $model_service;
/** @var Groq_AI_Log_Scheduler */
private $log_scheduler;
public static function instance() { public static function instance() {
if ( null === self::$instance ) { if ( null === self::$instance ) {
@@ -104,13 +129,22 @@ final class Groq_AI_Product_Text_Plugin {
private function __construct() { private function __construct() {
$this->register_services(); $this->register_services();
$this->compatibility_service = new Groq_AI_Compatibility_Service();
$this->model_service = new Groq_AI_Model_Service();
$this->log_scheduler = new Groq_AI_Log_Scheduler( $this->get_settings_manager(), $this->get_generation_logger() );
$this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() ); $this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() );
$this->categories_admin = new Groq_AI_Categories_Admin( $this );
$this->brands_admin = new Groq_AI_Brands_Admin( $this );
$this->logs_admin = new Groq_AI_Logs_Admin( $this );
$this->product_ui = new Groq_AI_Product_Text_Product_UI( $this ); $this->product_ui = new Groq_AI_Product_Text_Product_UI( $this );
add_action( 'init', [ $this, 'load_textdomain' ] ); add_action( 'init', [ $this, 'load_textdomain' ] );
add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] ); $logger = $this->container->get( 'generation_logger' );
add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] ); add_action( 'plugins_loaded', [ $logger, 'maybe_create_table' ] );
add_action( 'load-plugins.php', [ $this->compatibility_service, 'maybe_deactivate_if_woocommerce_missing' ] );
add_action( 'init', [ $this->log_scheduler, 'ensure_logs_cleanup_schedule' ] );
add_action( 'groq_ai_cleanup_logs', [ $this->log_scheduler, 'cleanup_logs' ] );
add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 ); add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 );
} }
@@ -220,60 +254,84 @@ final class Groq_AI_Product_Text_Plugin {
return self::OPTION_KEY; return self::OPTION_KEY;
} }
public function get_provider_manager() { public function __call( $name, $arguments ) {
switch ( $name ) {
case 'get_provider_manager':
return $this->container->get( 'provider_manager' ); return $this->container->get( 'provider_manager' );
} case 'get_settings_manager':
public function get_settings_manager() {
return $this->container->get( 'settings_manager' ); return $this->container->get( 'settings_manager' );
} case 'get_prompt_builder':
public function get_prompt_builder() {
return $this->container->get( 'prompt_builder' ); return $this->container->get( 'prompt_builder' );
} case 'get_conversation_manager':
public function get_conversation_manager() {
return $this->container->get( 'conversation_manager' ); return $this->container->get( 'conversation_manager' );
} case 'get_generation_logger':
public function get_generation_logger() {
return $this->container->get( 'generation_logger' ); return $this->container->get( 'generation_logger' );
case 'get_settings':
return $this->container->get( 'settings_manager' )->all();
case 'sanitize_settings':
return $this->container->get( 'settings_manager' )->sanitize( $arguments[0] ?? [] );
case 'get_context_field_definitions':
return $this->container->get( 'settings_manager' )->get_context_field_definitions();
case 'get_default_modules_settings':
return $this->container->get( 'settings_manager' )->get_default_modules_settings();
case 'get_default_context_fields':
return $this->container->get( 'settings_manager' )->get_default_context_fields();
case 'normalize_context_fields':
return $this->container->get( 'settings_manager' )->normalize_context_fields( $arguments[0] ?? [] );
case 'get_module_config':
return $this->container->get( 'settings_manager' )->get_module_config( $arguments[0] ?? '', $arguments[1] ?? null );
case 'is_module_enabled':
return $this->container->get( 'settings_manager' )->is_module_enabled( $arguments[0] ?? '', $arguments[1] ?? null );
case 'get_rankmath_focus_keyword_limit':
return $this->container->get( 'settings_manager' )->get_rankmath_focus_keyword_limit( $arguments[0] ?? null );
case 'get_rankmath_meta_title_pixel_limit':
return $this->container->get( 'settings_manager' )->get_rankmath_meta_title_pixel_limit( $arguments[0] ?? null );
case 'get_rankmath_meta_description_pixel_limit':
return $this->container->get( 'settings_manager' )->get_rankmath_meta_description_pixel_limit( $arguments[0] ?? null );
case 'is_response_format_compat_enabled':
return $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $arguments[0] ?? null );
case 'get_image_context_mode':
return $this->container->get( 'settings_manager' )->get_image_context_mode( $arguments[0] ?? null );
case 'get_image_context_limit':
return $this->container->get( 'settings_manager' )->get_image_context_limit( $arguments[0] ?? null );
case 'get_term_top_description_char_limit':
return $this->container->get( 'settings_manager' )->get_term_top_description_char_limit( $arguments[0] ?? null );
case 'get_term_bottom_description_char_limit':
return $this->container->get( 'settings_manager' )->get_term_bottom_description_char_limit( $arguments[0] ?? null );
case 'get_google_safety_settings':
return $this->container->get( 'settings_manager' )->get_google_safety_settings( $arguments[0] ?? null );
case 'get_google_safety_categories':
return $this->container->get( 'settings_manager' )->get_google_safety_categories();
case 'get_google_safety_thresholds':
return $this->container->get( 'settings_manager' )->get_google_safety_thresholds();
case 'get_loggable_settings_snapshot':
return $this->container->get( 'settings_manager' )->get_loggable_settings_snapshot( $arguments[0] ?? null );
case 'create_settings_renderer':
$values = $arguments[0] ?? null;
if ( null === $values ) {
$values = $this->container->get( 'settings_manager' )->all();
}
return new Groq_AI_Settings_Renderer( self::OPTION_KEY, $values );
case 'should_use_response_format':
$provider = $arguments[0] ?? null;
$settings = $arguments[1] ?? null;
if ( ! $provider instanceof Groq_AI_Provider_Interface ) {
return false;
}
return ! $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
case 'is_rankmath_active':
return $this->compatibility_service->is_rankmath_active();
case 'is_woocommerce_active':
return $this->compatibility_service->is_woocommerce_active();
case 'get_selected_model':
return $this->model_service->get_selected_model( $arguments[0], $arguments[1] ?? [] );
case 'get_cached_models_for_provider':
return $this->model_service->get_cached_models_for_provider( $arguments[0] ?? '' );
case 'update_cached_models_for_provider':
return $this->model_service->update_cached_models_for_provider( $arguments[0] ?? '', $arguments[1] ?? [] );
} }
public function get_settings() { throw new BadMethodCallException( sprintf( 'Method %s does not exist.', $name ) );
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_DOMAIN ); ?>
</p>
</div>
<?php
} }
public function build_prompt_template_preview( $settings ) { public function build_prompt_template_preview( $settings ) {
@@ -294,179 +352,6 @@ final class Groq_AI_Product_Text_Plugin {
return implode( "\n\n", $parts ); 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 get_image_context_mode( $settings = null ) {
return $this->get_settings_manager()->get_image_context_mode( $settings );
}
public function get_image_context_limit( $settings = null ) {
return $this->get_settings_manager()->get_image_context_limit( $settings );
}
public function get_term_top_description_char_limit( $settings = null ) {
return $this->get_settings_manager()->get_term_top_description_char_limit( $settings );
}
public function get_term_bottom_description_char_limit( $settings = null ) {
return $this->get_settings_manager()->get_term_bottom_description_char_limit( $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 ) {
$provider_key = $provider->get_key();
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
if ( '' === $model ) {
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
if ( '' !== $default ) {
return $default;
}
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
if ( ! empty( $available ) ) {
return $available[0];
}
}
return $model;
}
public function get_cached_models_for_provider( $provider ) {
$provider = sanitize_key( (string) $provider );
$cache = $this->get_models_cache();
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
}
public function update_cached_models_for_provider( $provider, $models ) {
$provider = sanitize_key( (string) $provider );
$models = $this->sanitize_models_list( $models );
$cache = $this->get_models_cache();
$cache[ $provider ] = $models;
update_option( self::MODELS_CACHE_OPTION_KEY, $cache );
return $models;
}
private function get_models_cache() {
$cache = get_option( self::MODELS_CACHE_OPTION_KEY, [] );
if ( ! is_array( $cache ) ) {
$cache = [];
}
foreach ( $cache as $provider => $models ) {
$cache[ $provider ] = $this->sanitize_models_list( $models );
}
return $cache;
}
private function sanitize_models_list( $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
$models = array_values( array_unique( $models ) );
if ( ! empty( $models ) ) {
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
}
return $models;
}
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() { public static function activate() {
$logger = new Groq_AI_Generation_Logger(); $logger = new Groq_AI_Generation_Logger();

View File

@@ -0,0 +1,46 @@
<?php
abstract class Groq_AI_Admin_Base {
/** @var Groq_AI_Product_Text_Plugin */
protected $plugin;
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
$this->plugin = $plugin;
}
protected function get_page_url( $slug = 'groq-ai-product-text', $args = [] ) {
$slug = sanitize_key( (string) $slug );
$url = add_query_arg(
[
'page' => $slug,
],
admin_url( 'options-general.php' )
);
if ( ! empty( $args ) ) {
$url = add_query_arg( $args, $url );
}
return $url;
}
protected function current_user_can_manage() {
return current_user_can( 'manage_options' );
}
protected function enqueue_admin_styles() {
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
);
}
}

View File

@@ -0,0 +1,177 @@
<?php
class Groq_AI_Brands_Admin extends Groq_AI_Term_Admin_Base {
private $brand_taxonomy = null;
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
parent::__construct( $plugin );
add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_brand_assets' ] );
}
public function register_menu_pages() {
add_submenu_page(
'options-general.php',
__( 'Siti AI Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI Merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-brands',
[ $this, 'render_brands_overview_page' ]
);
$this->register_term_page();
}
public function render_brands_overview_page() {
if ( ! $this->current_user_can_manage() ) {
return;
}
$taxonomy = $this->detect_brand_taxonomy();
if ( '' === $taxonomy ) {
?>
<div class="wrap">
<h1><?php esc_html_e( 'Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p><?php esc_html_e( 'Geen merk-taxonomie gevonden. Installeer/activeer een merken-plugin of stel een taxonomie in via de filter groq_ai_brand_taxonomy.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</div>
<?php
return;
}
$overview = $this->get_term_overview_data( $taxonomy );
$rows = isset( $overview['rows'] ) ? $overview['rows'] : [];
$empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0;
?>
<div class="wrap">
<h1><?php esc_html_e( 'Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p>
<?php
printf(
/* translators: %s: taxonomy key */
esc_html__( 'Gedetecteerde merk-taxonomie: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
esc_html( $taxonomy )
);
?>
</p>
<?php $this->render_term_bulk_panel( __( 'merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?>
<p class="description"><?php esc_html_e( 'Gebruik de knop "Genereer opnieuw" in de tabel om bestaande merkteksten opnieuw laten schrijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<table class="widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Merk', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Slug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Producten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Woorden (omschrijving)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Acties', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan="5"><?php esc_html_e( 'Geen merken gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<?php
$row_classes = [ 'groq-ai-term-row' ];
if ( empty( $row['has_description'] ) ) {
$row_classes[] = 'groq-ai-term-missing';
}
$link = isset( $row['url'] ) ? $row['url'] : '';
$count = isset( $row['count'] ) ? (int) $row['count'] : 0;
$words = isset( $row['words'] ) ? (int) $row['words'] : 0;
?>
<tr class="<?php echo esc_attr( implode( ' ', $row_classes ) ); ?>" data-groq-ai-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
<td>
<a href="<?php echo esc_url( $link ); ?>"><strong><?php echo esc_html( isset( $row['name'] ) ? $row['name'] : '' ); ?></strong></a>
</td>
<td><?php echo esc_html( isset( $row['slug'] ) ? $row['slug'] : '' ); ?></td>
<td><?php echo esc_html( (string) $count ); ?></td>
<td class="groq-ai-word-cell"><span class="groq-ai-word-count"><?php echo esc_html( (string) $words ); ?></span></td>
<td class="groq-ai-term-actions">
<button type="button" class="button button-secondary groq-ai-regenerate-term" data-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
<?php esc_html_e( 'Genereer opnieuw', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
public function enqueue_brand_assets( $hook ) {
if ( 0 !== strpos( (string) $hook, 'settings_page_groq-ai-product-text-brands' ) ) {
return;
}
$this->enqueue_admin_styles();
$taxonomy = $this->detect_brand_taxonomy();
if ( '' === $taxonomy ) {
return;
}
wp_enqueue_script(
'groq-ai-term-bulk',
plugins_url( 'assets/js/term-bulk.js', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION,
true
);
$this->localize_term_bulk_script(
$taxonomy,
[
'allowRegenerate' => true,
'strings' => [
'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde merken bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusProgress' => __( 'Merk %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusDone' => __( 'Klaar! %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusStopped' => __( 'Bulk generatie gestopt. %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusEmpty' => __( 'Geen merken zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? Het huidige merk kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'confirmRegenerate' => __( 'Wil je %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een merk opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
]
);
}
private function detect_brand_taxonomy() {
if ( null !== $this->brand_taxonomy ) {
return $this->brand_taxonomy;
}
$candidates = [
'product_brand',
'pwb-brand',
'yith_product_brand',
'berocket_brand',
];
if ( taxonomy_exists( 'pa_brand' ) ) {
array_unshift( $candidates, 'pa_brand' );
}
$candidates = apply_filters( 'groq_ai_brand_taxonomy_candidates', $candidates );
$found = '';
foreach ( $candidates as $tax ) {
$tax = sanitize_key( (string) $tax );
if ( $tax && taxonomy_exists( $tax ) ) {
$found = $tax;
break;
}
}
$found = apply_filters( 'groq_ai_brand_taxonomy', $found );
$this->brand_taxonomy = sanitize_key( (string) $found );
return $this->brand_taxonomy;
}
}

View File

@@ -0,0 +1,119 @@
<?php
class Groq_AI_Categories_Admin extends Groq_AI_Term_Admin_Base {
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
parent::__construct( $plugin );
add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_category_assets' ] );
}
public function register_menu_pages() {
add_submenu_page(
'options-general.php',
__( 'Siti AI Categorie teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI Categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-categories',
[ $this, 'render_categories_overview_page' ]
);
$this->register_term_page();
}
public function render_categories_overview_page() {
if ( ! $this->current_user_can_manage() ) {
return;
}
$taxonomy = 'product_cat';
$overview = $this->get_term_overview_data( $taxonomy );
$rows = isset( $overview['rows'] ) ? $overview['rows'] : [];
$empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0;
?>
<div class="wrap">
<h1><?php esc_html_e( 'Categorie teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p><?php esc_html_e( 'Klik op een categorie om teksten te genereren en instellingen te beheren. De tabel toont de huidige woordlengte van de categorie-omschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php $this->render_term_bulk_panel( __( 'categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?>
<table class="widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Categorie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Slug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Producten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Woorden (omschrijving)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<th><?php esc_html_e( 'Acties', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan="5"><?php esc_html_e( 'Geen categorieën gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<?php
$row_classes = [ 'groq-ai-term-row' ];
if ( empty( $row['has_description'] ) ) {
$row_classes[] = 'groq-ai-term-missing';
}
$link = isset( $row['url'] ) ? $row['url'] : '';
$count = isset( $row['count'] ) ? (int) $row['count'] : 0;
$words = isset( $row['words'] ) ? (int) $row['words'] : 0;
?>
<tr class="<?php echo esc_attr( implode( ' ', $row_classes ) ); ?>" data-groq-ai-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
<td>
<a href="<?php echo esc_url( $link ); ?>"><strong><?php echo esc_html( isset( $row['name'] ) ? $row['name'] : '' ); ?></strong></a>
</td>
<td><?php echo esc_html( isset( $row['slug'] ) ? $row['slug'] : '' ); ?></td>
<td><?php echo esc_html( (string) $count ); ?></td>
<td class="groq-ai-word-cell"><span class="groq-ai-word-count"><?php echo esc_html( (string) $words ); ?></span></td>
<td class="groq-ai-term-actions">
<button type="button" class="button button-secondary groq-ai-regenerate-term" data-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
<?php esc_html_e( 'Genereer opnieuw', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
public function enqueue_category_assets( $hook ) {
if ( 0 !== strpos( (string) $hook, 'settings_page_groq-ai-product-text-categories' ) ) {
return;
}
$this->enqueue_admin_styles();
wp_enqueue_script(
'groq-ai-term-bulk',
plugins_url( 'assets/js/term-bulk.js', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION,
true
);
$this->localize_term_bulk_script(
'product_cat',
[
'allowRegenerate' => true,
'strings' => [
'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde categorieën bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusProgress' => __( 'Categorie %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusDone' => __( 'Klaar! %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusStopped' => __( 'Bulk generatie gestopt. %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'statusEmpty' => __( 'Geen categorieën zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? De huidige categorie kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'confirmRegenerate' => __( 'Wil je categorie %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een categorie opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
]
);
}
}

View File

@@ -0,0 +1,180 @@
<?php
class Groq_AI_Logs_Admin extends Groq_AI_Admin_Base {
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
parent::__construct( $plugin );
add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
}
public function register_menu_pages() {
add_submenu_page(
'options-general.php',
__( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-logs',
[ $this, 'render_logs_page' ]
);
add_submenu_page(
'options-general.php',
__( 'Siti AI Log detail', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI Log detail', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-log',
[ $this, 'render_log_detail_page' ]
);
}
public function render_logs_page() {
if ( ! $this->current_user_can_manage() ) {
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_DOMAIN ); ?></h1>
<form method="get">
<input type="hidden" name="page" value="groq-ai-product-text-logs" />
<?php $logs_table->search_box( __( 'Zoek logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'groq-ai-logs' ); ?>
<?php $logs_table->display(); ?>
</form>
</div>
<?php
}
public function render_log_detail_page() {
if ( ! $this->current_user_can_manage() ) {
return;
}
$log_id = isset( $_GET['log_id'] ) ? absint( $_GET['log_id'] ) : 0;
$back_url = $this->get_page_url( 'groq-ai-product-text-logs' );
$log = null;
if ( $log_id ) {
global $wpdb;
$table = $wpdb->prefix . 'groq_ai_generation_logs';
$query = $wpdb->prepare(
"SELECT l.*, p.post_title FROM {$table} l LEFT JOIN {$wpdb->posts} p ON p.ID = l.post_id WHERE l.id = %d",
$log_id
);
$log = $wpdb->get_row( $query, ARRAY_A );
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'Logdetail', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p>
<a href="<?php echo esc_url( $back_url ); ?>" class="button">&larr; <?php esc_html_e( 'Terug naar logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></a>
</p>
<?php if ( ! $log ) : ?>
<p><?php esc_html_e( 'Log niet gevonden of verwijderd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class="widefat striped" style="margin-top:16px;">
<tbody>
<tr>
<th><?php esc_html_e( 'Datum', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php echo esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $log['created_at'] ) ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Gebruiker', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td>
<?php
if ( $log['user_id'] ) {
$user = get_userdata( $log['user_id'] );
echo $user ? esc_html( $user->display_name ) : esc_html( (string) $log['user_id'] );
} else {
echo '—';
}
?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Product', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td>
<?php
if ( $log['post_id'] ) {
$link = get_edit_post_link( $log['post_id'] );
$title = $log['post_title'] ? $log['post_title'] : sprintf( __( 'Product #%d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (int) $log['post_id'] );
echo $link ? '<a href="' . esc_url( $link ) . '">' . esc_html( $title ) . '</a>' : esc_html( $title );
} else {
echo '—';
}
?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Provider', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php echo esc_html( $log['provider'] ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Model', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php echo esc_html( $log['model'] ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Status', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php echo esc_html( $log['status'] ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Tokens', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td>
<?php
printf(
esc_html__( 'Prompt: %1$s — Completion: %2$s — Totaal: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
isset( $log['tokens_prompt'] ) ? number_format_i18n( (int) $log['tokens_prompt'] ) : '—',
isset( $log['tokens_completion'] ) ? number_format_i18n( (int) $log['tokens_completion'] ) : '—',
isset( $log['tokens_total'] ) ? number_format_i18n( (int) $log['tokens_total'] ) : '—'
);
?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Foutmelding', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php echo $log['error_message'] ? esc_html( $log['error_message'] ) : '—'; ?></td>
</tr>
</tbody>
</table>
<h2><?php esc_html_e( 'Prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<pre style="background:#fff;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
<?php echo esc_html( $log['prompt'] ); ?>
</pre>
<h2><?php esc_html_e( 'AI-respons', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<pre style="background:#f9f9f9;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
<?php echo esc_html( $log['response'] ); ?>
</pre>
<?php if ( ! empty( $log['request_json'] ) ) :
$request_params = json_decode( $log['request_json'], true );
$request_params = is_array( $request_params ) ? $request_params : [];
if ( ! empty( $request_params ) ) :
$request_pretty = wp_json_encode( $request_params, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
$request_pretty = $request_pretty ? $request_pretty : wp_json_encode( $request_params );
?>
<h2><?php esc_html_e( 'Request parameters', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<pre style="background:#fff;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
<?php echo esc_html( $request_pretty ); ?>
</pre>
<?php endif; endif; ?>
<?php if ( ! empty( $log['usage_json'] ) ) :
$usage_meta = json_decode( $log['usage_json'], true );
$usage_meta = is_array( $usage_meta ) ? $usage_meta : [];
if ( ! empty( $usage_meta ) ) :
$usage_pretty = wp_json_encode( $usage_meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
$usage_pretty = $usage_pretty ? $usage_pretty : wp_json_encode( $usage_meta );
?>
<h2><?php esc_html_e( 'Usage metadata', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<pre style="background:#f6f7f7;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
<?php echo esc_html( $usage_pretty ); ?>
</pre>
<?php endif; endif; ?>
<?php endif; ?>
</div>
<?php
}
}

View File

@@ -60,10 +60,14 @@ class Groq_AI_Logs_Table extends WP_List_Table {
$current_page = $this->get_pagenum(); $current_page = $this->get_pagenum();
$offset = ( $current_page - 1 ) * $per_page; $offset = ( $current_page - 1 ) * $per_page;
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at'; $allowed_orderby = [
if ( ! $orderby ) { 'created_at' => 'created_at',
$orderby = 'created_at'; 'provider' => 'provider',
} 'model' => 'model',
'status' => 'status',
];
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_key( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at';
$orderby = isset( $allowed_orderby[ $orderby ] ) ? $allowed_orderby[ $orderby ] : 'created_at';
$order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC'; $order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC';
$order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC'; $order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';

View File

@@ -73,6 +73,18 @@ class Groq_AI_Product_Text_Product_UI {
'postId' => $post_id, 'postId' => $post_id,
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(), 'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
'attributeIncludesDefaults' => $attribute_defaults, 'attributeIncludesDefaults' => $attribute_defaults,
'strings' => [
'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'retry' => __( 'Probeer het opnieuw of pas je prompt/context aan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorUnknown' => __( 'Onbekende fout.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'success' => __( 'Structuur gegenereerd. Kopieer of vul velden in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'fieldApplied' => __( '%s ingevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'fieldApplyError' => __( 'Kon het veld niet automatisch invullen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'fieldCopied' => __( '%s gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'jsonCopied' => __( 'JSON gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'copyFailed' => __( 'Kopiëren mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
] ]
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
<?php
class Groq_AI_Settings_Renderer {
/** @var string */
private $option_key;
/** @var array */
private $values = [];
public function __construct( $option_key, $values = [] ) {
$this->option_key = $option_key;
$this->set_values( $values );
}
public function set_values( $values ) {
$this->values = is_array( $values ) ? $values : [];
}
public function open_table( $args = [] ) {
$defaults = [
'class' => 'form-table',
'role' => 'presentation',
];
$args = wp_parse_args( $args, $defaults );
printf( '<table %s>', $this->build_attr_string( $args ) );
}
public function close_table() {
echo '</table>';
}
public function field( $args ) {
$defaults = [
'key' => '',
'name' => '',
'id' => '',
'label' => '',
'description' => '',
'type' => 'text',
'placeholder' => '',
'options' => [],
'attributes' => [],
'default' => '',
'value' => null,
'renderer' => null,
'row_attributes' => [],
'row_class' => '',
];
$args = wp_parse_args( $args, $defaults );
if ( '' === $args['name'] && '' !== $args['key'] ) {
$args['name'] = $this->build_field_name( $args['key'] );
}
if ( '' === $args['id'] && '' !== $args['key'] ) {
$args['id'] = $this->build_field_id( $args['key'] );
}
if ( null === $args['value'] && '' !== $args['key'] ) {
$args['value'] = $this->get_value( $args['key'], $args['default'] );
}
if ( ! isset( $args['attributes']['id'] ) && '' !== $args['id'] ) {
$args['attributes']['id'] = $args['id'];
}
$type = $args['type'];
$row_attributes = $this->prepare_row_attributes( $args );
$row_attr_string = $row_attributes ? ' ' . $this->build_attr_string( $row_attributes ) : '';
echo '<tr' . $row_attr_string . '>';
$this->render_label_cell( $args );
echo '<td>';
if ( is_callable( $args['renderer'] ) ) {
call_user_func( $args['renderer'], $args, $this );
} else {
switch ( $type ) {
case 'textarea':
$this->render_textarea( $args );
break;
case 'password':
$this->render_input( 'password', $args );
break;
case 'number':
$this->render_input( 'number', $args );
break;
case 'select':
$this->render_select( $args );
break;
case 'checkbox':
$this->render_checkbox( $args );
break;
case 'toggle':
$this->render_toggle( $args );
break;
default:
$this->render_input( 'text', $args );
}
}
$this->render_description( $args['description'] );
echo '</td>';
echo '</tr>';
}
private function render_label_cell( $args ) {
$label = $args['label'];
$id = $args['id'];
echo '<th scope="row">';
if ( '' !== $label ) {
printf( '<label for="%s">%s</label>', esc_attr( $id ), esc_html( $label ) );
}
echo '</th>';
}
private function render_input( $type, $args ) {
$attributes = $this->prepare_input_attributes( $args );
printf( '<input type="%s" %s />', esc_attr( $type ), $attributes );
}
private function render_textarea( $args ) {
$attributes = $this->prepare_input_attributes( $args, [ 'rows' => 4, 'class' => 'large-text' ] );
printf( '<textarea %s>%s</textarea>', $attributes, esc_textarea( $args['value'] ) );
}
private function render_select( $args ) {
$attributes = $this->prepare_input_attributes( $args );
printf( '<select %s>', $attributes );
foreach ( (array) $args['options'] as $value => $label ) {
printf( '<option value="%s" %s>%s</option>', esc_attr( $value ), selected( $args['value'], $value, false ), esc_html( $label ) );
}
echo '</select>';
}
private function render_checkbox( $args ) {
$value = ! empty( $args['value'] );
$attributes = $this->prepare_input_attributes( $args, [ 'class' => '' ] );
printf( '<label><input type="checkbox" %s %s /> %s</label>', $attributes, checked( $value, true, false ), esc_html( $args['checkbox_label'] ?? '' ) );
}
private function render_toggle( $args ) {
$value = ! empty( $args['value'] );
$attributes = $this->prepare_input_attributes( $args, [ 'class' => '' ] );
printf( '<label class="groq-ai-toggle"><input type="checkbox" %s %s /> <span class="groq-ai-toggle__slider"></span> %s</label>', $attributes, checked( $value, true, false ), esc_html( $args['checkbox_label'] ?? '' ) );
}
private function render_description( $text ) {
$text = trim( (string) $text );
if ( '' === $text ) {
return;
}
printf( '<p class="description">%s</p>', wp_kses_post( $text ) );
}
private function prepare_input_attributes( $args, $defaults = [] ) {
$attributes = wp_parse_args( $args['attributes'], $defaults );
$attributes['name'] = $args['name'];
if ( ! isset( $attributes['id'] ) ) {
$attributes['id'] = $args['id'];
}
if ( '' !== $args['placeholder'] ) {
$attributes['placeholder'] = $args['placeholder'];
}
if ( ! isset( $attributes['class'] ) ) {
$attributes['class'] = 'regular-text';
}
if ( ! in_array( $args['type'], [ 'checkbox', 'toggle', 'select', 'textarea' ], true ) ) {
$attributes['value'] = $args['value'];
}
return $this->build_attr_string( $attributes );
}
private function prepare_row_attributes( $args ) {
$attributes = [];
if ( isset( $args['row_attributes'] ) && is_array( $args['row_attributes'] ) ) {
$attributes = $args['row_attributes'];
}
$row_class = isset( $args['row_class'] ) ? trim( (string) $args['row_class'] ) : '';
if ( '' !== $row_class ) {
if ( isset( $attributes['class'] ) ) {
$attributes['class'] .= ' ' . $row_class;
} else {
$attributes['class'] = $row_class;
}
}
return array_filter(
$attributes,
function ( $value ) {
return '' !== $value || 0 === $value || '0' === $value;
}
);
}
private function build_attr_string( $attributes ) {
$buffer = [];
foreach ( $attributes as $key => $value ) {
if ( '' === $value && 0 !== $value && '0' !== $value ) {
continue;
}
$buffer[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) );
}
return implode( ' ', $buffer );
}
private function build_field_name( $key ) {
$segments = $this->split_key( $key );
$name = $this->option_key;
foreach ( $segments as $segment ) {
$name .= '[' . $segment . ']';
}
return $name;
}
private function build_field_id( $key ) {
$segments = $this->split_key( $key );
return 'groq-ai-' . implode( '-', $segments );
}
private function get_value( $key, $default = '' ) {
$segments = $this->split_key( $key );
$value = $this->values;
foreach ( $segments as $segment ) {
if ( is_array( $value ) && array_key_exists( $segment, $value ) ) {
$value = $value[ $segment ];
} else {
return $default;
}
}
return $value;
}
private function split_key( $key ) {
if ( is_array( $key ) ) {
return $key;
}
$key = trim( (string) $key );
if ( '' === $key ) {
return [];
}
return array_map( 'sanitize_key', explode( '.', $key ) );
}
}

View File

@@ -0,0 +1,538 @@
<?php
abstract class Groq_AI_Term_Admin_Base extends Groq_AI_Admin_Base {
protected $term_overview_cache = [];
private static $term_page_registered = false;
private static $term_handler_registered = false;
private static $term_assets_hook_registered = false;
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
parent::__construct( $plugin );
$this->ensure_term_handler_registered();
$this->ensure_term_assets_hook();
}
protected function register_term_page() {
if ( self::$term_page_registered ) {
return;
}
add_submenu_page(
'options-general.php',
__( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
__( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'manage_options',
'groq-ai-product-text-term',
[ $this, 'render_term_generator_page' ]
);
self::$term_page_registered = true;
}
protected function render_term_bulk_panel( $label_plural, $empty_count ) {
$label_plural = (string) $label_plural;
?>
<div class="groq-ai-bulk-panel">
<p>
<?php
if ( $empty_count > 0 ) {
printf(
/* translators: 1: amount, 2: label plural (e.g. categorieën) */
esc_html__( 'Er zijn %1$d %2$s zonder omschrijving. Klik op de knop hieronder om automatisch teksten te genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
(int) $empty_count,
esc_html( $label_plural )
);
} else {
printf(
esc_html__( 'Alle %s hebben al een omschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
esc_html( $label_plural )
);
}
?>
</p>
<p class="groq-ai-bulk-actions">
<?php
$button_label = sprintf(
esc_html__( 'Genereer teksten voor lege %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
$label_plural
);
?>
<button type="button" class="button button-primary" id="groq-ai-bulk-generate"><?php echo esc_html( $button_label ); ?></button>
<button type="button" class="button" id="groq-ai-bulk-cancel" hidden><?php esc_html_e( 'Stop bulk generatie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
</p>
<div id="groq-ai-bulk-status" class="description"></div>
<ol id="groq-ai-bulk-log" class="groq-ai-bulk-log"></ol>
</div>
<?php
}
protected function localize_term_bulk_script( $taxonomy, $overrides = [] ) {
$overview = $this->get_term_overview_data( $taxonomy );
$rows = isset( $overview['rows'] ) ? $overview['rows'] : [];
$terms = [];
foreach ( $rows as $row ) {
$terms[] = [
'id' => isset( $row['id'] ) ? (int) $row['id'] : 0,
'name' => isset( $row['name'] ) ? (string) $row['name'] : '',
'slug' => isset( $row['slug'] ) ? (string) $row['slug'] : '',
'count' => isset( $row['count'] ) ? (int) $row['count'] : 0,
'words' => isset( $row['words'] ) ? (int) $row['words'] : 0,
'hasDescription' => ! empty( $row['has_description'] ),
];
}
$defaults = [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'groq_ai_bulk_generate_terms' ),
'taxonomy' => $taxonomy,
'terms' => $terms,
'allowRegenerate' => false,
'strings' => [
'unknownError' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'unknownTerm' => __( 'Onbekende term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'confirmStopFallback' => __( 'Stoppen?', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logErrorDefault' => __( '%1$s: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'logSuccessDefault' => __( '%1$s gevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateErrorDefault' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'regenerateDoneDefault' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
];
$config = wp_parse_args( $overrides, $defaults );
$override_strings = isset( $overrides['strings'] ) && is_array( $overrides['strings'] ) ? $overrides['strings'] : [];
$config['strings'] = array_merge( $defaults['strings'], $override_strings );
wp_localize_script( 'groq-ai-term-bulk', 'GroqAITermBulk', $config );
}
protected function get_term_overview_data( $taxonomy ) {
$taxonomy = sanitize_key( (string) $taxonomy );
if ( isset( $this->term_overview_cache[ $taxonomy ] ) ) {
return $this->term_overview_cache[ $taxonomy ];
}
$rows = [];
$empty_rows = [];
if ( '' !== $taxonomy && taxonomy_exists( $taxonomy ) ) {
$terms = get_terms(
[
'taxonomy' => $taxonomy,
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
'number' => 0,
]
);
if ( is_wp_error( $terms ) ) {
$terms = [];
}
foreach ( $terms as $term ) {
if ( ! $term || ! is_object( $term ) || empty( $term->term_id ) ) {
continue;
}
$words = $this->count_words( isset( $term->description ) ? $term->description : '' );
$has_description = $words > 0;
$row = [
'id' => absint( $term->term_id ),
'name' => (string) $term->name,
'slug' => (string) $term->slug,
'count' => isset( $term->count ) ? absint( $term->count ) : 0,
'words' => $words,
'has_description' => $has_description,
'url' => $this->get_term_page_url( $taxonomy, $term->term_id ),
];
$rows[] = $row;
if ( ! $has_description ) {
$empty_rows[] = $row;
}
}
}
$data = [
'rows' => $rows,
'empty_rows' => $empty_rows,
'empty_count' => count( $empty_rows ),
];
$this->term_overview_cache[ $taxonomy ] = $data;
return $data;
}
private function count_words( $text ) {
$text = wp_strip_all_tags( (string) $text );
$text = trim( preg_replace( '/\s+/u', ' ', $text ) );
if ( '' === $text ) {
return 0;
}
if ( preg_match_all( '/\pL[\pL\pN\']*/u', $text, $matches ) ) {
return count( $matches[0] );
}
return 0;
}
protected function get_term_page_url( $taxonomy, $term_id ) {
return add_query_arg(
[
'page' => 'groq-ai-product-text-term',
'taxonomy' => sanitize_key( (string) $taxonomy ),
'term_id' => absint( $term_id ),
],
admin_url( 'options-general.php' )
);
}
public function enqueue_term_assets( $hook ) {
if ( 0 !== strpos( (string) $hook, 'settings_page_groq-ai-product-text-term' ) ) {
return;
}
$this->enqueue_admin_styles();
wp_enqueue_script(
'groq-ai-term-admin',
plugins_url( 'assets/js/term-admin.js', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION,
true
);
$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '';
$term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0;
wp_localize_script(
'groq-ai-term-admin',
'GroqAITermGenerator',
[
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'groq_ai_generate_term' ),
'taxonomy' => $taxonomy,
'termId' => $term_id,
'strings' => [
'promptRequired' => __( 'Vul eerst een prompt in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'success' => __( 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'applySuccess' => __( 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'errorUnknown' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
]
);
}
public function render_term_generator_page() {
if ( ! $this->current_user_can_manage() ) {
return;
}
$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '';
$term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0;
if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
?>
<div class="wrap">
<h1><?php esc_html_e( 'Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p><?php esc_html_e( 'Ongeldige term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</div>
<?php
return;
}
$term = get_term( $term_id, $taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
?>
<div class="wrap">
<h1><?php esc_html_e( 'Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
<p><?php esc_html_e( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</div>
<?php
return;
}
$term_notice = isset( $_GET['groq_ai_term_notice'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_term_notice'] ) ) : '';
$term_notice_status = isset( $_GET['groq_ai_term_status'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_term_status'] ) ) : 'success';
$term_notice_message = '';
if ( isset( $_GET['groq_ai_term_notice_message'] ) ) {
$term_notice_message = sanitize_text_field( rawurldecode( wp_unslash( $_GET['groq_ai_term_notice_message'] ) ) );
}
if ( $term_notice && '' === $term_notice_message ) {
if ( 'saved' === $term_notice ) {
$term_notice_message = __( 'Term succesvol opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
} else {
$term_notice_message = __( 'Actie voltooid.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
}
}
$term_label = ( 'product_cat' === $taxonomy ) ? __( 'Categorie', GROQ_AI_PRODUCT_TEXT_DOMAIN ) : __( 'Term', GROQ_AI_PRODUCT_TEXT_DOMAIN );
$word_count = $this->count_words( $term->description );
$meta_prompt = get_term_meta( $term_id, 'groq_ai_term_custom_prompt', true );
$settings = $this->plugin->get_settings();
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings );
$effective_bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description';
$bottom_description = (string) get_term_meta( $term_id, $effective_bottom_meta_key, true );
$rankmath_module_enabled = $this->plugin->is_module_enabled( 'rankmath', $settings );
$rankmath_active = $this->plugin->is_rankmath_active();
$rankmath_title = '';
$rankmath_description = '';
$rankmath_focus_keywords = '';
if ( $rankmath_module_enabled ) {
$rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings );
$rankmath_title = (string) get_term_meta( $term_id, $rankmath_keys['title'], true );
$rankmath_description = (string) get_term_meta( $term_id, $rankmath_keys['description'], true );
$rankmath_focus_keywords = (string) get_term_meta( $term_id, $rankmath_keys['focus_keyword'], true );
}
$default_prompt = $this->get_term_prompt_text( $term, $meta_prompt );
?>
<div class="wrap">
<h1>
<?php echo esc_html( $term_label ); ?>: <?php echo esc_html( $term->name ); ?>
</h1>
<?php if ( $term_notice ) : ?>
<?php $notice_class = ( 'error' === $term_notice_status ) ? 'notice notice-error' : 'notice notice-success'; ?>
<div class="<?php echo esc_attr( $notice_class ); ?>">
<p><?php echo esc_html( $term_notice_message ); ?></p>
</div>
<?php endif; ?>
<table class="form-table" role="presentation">
<tr>
<th><?php esc_html_e( 'Taxonomie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php echo esc_html( $taxonomy ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Huidige woordtelling', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
<td><?php echo esc_html( (string) $word_count ); ?></td>
</tr>
</table>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" id="groq-ai-term-form">
<?php wp_nonce_field( 'groq_ai_save_term_content' ); ?>
<input type="hidden" name="action" value="groq_ai_save_term_content" />
<input type="hidden" name="taxonomy" value="<?php echo esc_attr( $taxonomy ); ?>" />
<input type="hidden" name="term_id" value="<?php echo esc_attr( $term_id ); ?>" />
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="groq-ai-term-description"><?php esc_html_e( 'Omschrijving (top description)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea id="groq-ai-term-description" class="large-text" rows="8" name="description"><?php echo esc_textarea( $term->description ); ?></textarea>
<p class="description"><?php esc_html_e( 'Bovenste omschrijving van de term. Wordt op de term-archive bovenaan getoond.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="groq-ai-term-bottom"><?php esc_html_e( 'Onderste omschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea id="groq-ai-term-bottom" class="large-text" rows="10" name="groq_ai_term_bottom_description"><?php echo esc_textarea( $bottom_description ); ?></textarea>
<p class="description"><?php esc_html_e( 'Wordt onderaan op de term-archive geplaatst. Laat leeg wanneer je dit niet wilt gebruiken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="groq-ai-term-custom-prompt"><?php esc_html_e( 'Eigen prompt (optioneel)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea id="groq-ai-term-custom-prompt" class="large-text" rows="5" name="groq_ai_term_custom_prompt"><?php echo esc_textarea( $meta_prompt ); ?></textarea>
<p class="description"><?php esc_html_e( 'Overschrijft de standaard prompt alleen voor deze term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<?php if ( $rankmath_module_enabled ) : ?>
<tr>
<th scope="row"><label for="groq-ai-rankmath-title"><?php esc_html_e( 'Rank Math meta title', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea id="groq-ai-rankmath-title" class="large-text" rows="2" name="groq_ai_rankmath_meta_title" <?php disabled( ! $rankmath_active ); ?>><?php echo esc_textarea( $rankmath_title ); ?></textarea>
<p class="description"><?php esc_html_e( 'Wordt opgeslagen in Rank Math. Alleen beschikbaar als Rank Math actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="groq-ai-rankmath-description"><?php esc_html_e( 'Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea id="groq-ai-rankmath-description" class="large-text" rows="3" name="groq_ai_rankmath_meta_description" <?php disabled( ! $rankmath_active ); ?>><?php echo esc_textarea( $rankmath_description ); ?></textarea>
</td>
</tr>
<tr>
<th scope="row"><label for="groq-ai-rankmath-keywords"><?php esc_html_e( 'Rank Math focus keywords', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea id="groq-ai-rankmath-keywords" class="large-text" rows="2" name="groq_ai_rankmath_focus_keywords" <?php disabled( ! $rankmath_active ); ?>><?php echo esc_textarea( $rankmath_focus_keywords ); ?></textarea>
</td>
</tr>
<?php endif; ?>
</table>
<?php submit_button( __( 'Term opslaan', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); ?>
</form>
<hr />
<form id="groq-ai-term-generator" action="javascript:void(0);">
<h2><?php esc_html_e( 'AI-term generator', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
<p><?php esc_html_e( 'Gebruik de AI om automatisch teksten te genereren. Pas deze aan voordat je opslaat.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
<textarea id="groq-ai-term-prompt" class="large-text" rows="5"><?php echo esc_textarea( $default_prompt ); ?></textarea>
<p>
<button type="submit" class="button button-primary"><?php esc_html_e( 'Genereer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
<button type="button" class="button" id="groq-ai-term-apply"><?php esc_html_e( 'Zet in velden', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
</p>
<div id="groq-ai-term-status" class="description" aria-live="polite"></div>
<h3><?php esc_html_e( 'Gegenereerde tekst (omschrijving, 1 alinea)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<textarea id="groq-ai-term-generated-top" class="large-text" rows="6"></textarea>
<h3><?php esc_html_e( 'Gegenereerde tekst (onderaan)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<textarea id="groq-ai-term-generated-bottom" class="large-text" rows="10"></textarea>
<?php if ( $rankmath_module_enabled ) : ?>
<h3><?php esc_html_e( 'Gegenereerde Rank Math meta title', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<textarea id="groq-ai-term-generated-meta-title" class="large-text" rows="2"></textarea>
<h3><?php esc_html_e( 'Gegenereerde Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<textarea id="groq-ai-term-generated-meta-description" class="large-text" rows="3"></textarea>
<h3><?php esc_html_e( 'Gegenereerde Rank Math focus keywords', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<textarea id="groq-ai-term-generated-focus-keywords" class="large-text" rows="2"></textarea>
<?php endif; ?>
<h3><?php esc_html_e( 'Ruwe JSON-output', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
<pre id="groq-ai-term-raw" style="background:#fff;border:1px solid #ddd;padding:12px;max-height:240px;overflow:auto;"></pre>
</form>
</div>
<?php
}
public function handle_save_term_content() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), '', [ 'response' => 403 ] );
}
check_admin_referer( 'groq_ai_save_term_content' );
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
$term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0;
if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
$this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Ongeldige term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' );
}
$term = get_term( $term_id, $taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
$this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' );
}
$description = isset( $_POST['description'] ) ? wp_kses_post( wp_unslash( $_POST['description'] ) ) : '';
$bottom_description = isset( $_POST['groq_ai_term_bottom_description'] ) ? wp_kses_post( wp_unslash( $_POST['groq_ai_term_bottom_description'] ) ) : '';
$custom_prompt = isset( $_POST['groq_ai_term_custom_prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_term_custom_prompt'] ) ) : '';
$update = wp_update_term(
$term_id,
$taxonomy,
[
'description' => $description,
]
);
if ( is_wp_error( $update ) ) {
$this->redirect_with_term_notice( $taxonomy, $term_id, 'error', $update->get_error_message(), 'error' );
}
$settings = $this->plugin->get_settings();
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings );
$bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description';
update_term_meta( $term_id, $bottom_meta_key, $bottom_description );
if ( '' === trim( $custom_prompt ) ) {
delete_term_meta( $term_id, 'groq_ai_term_custom_prompt' );
} else {
update_term_meta( $term_id, 'groq_ai_term_custom_prompt', $custom_prompt );
}
if ( isset( $_POST['groq_ai_term_bottom_description'] ) && $bottom_meta_key !== 'groq_ai_term_bottom_description' ) {
update_term_meta( $term_id, 'groq_ai_term_bottom_description', $bottom_description );
}
if ( $this->plugin->is_module_enabled( 'rankmath', $settings ) ) {
$rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings );
$rankmath_title = isset( $_POST['groq_ai_rankmath_meta_title'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_meta_title'] ) ) : '';
$rankmath_description = isset( $_POST['groq_ai_rankmath_meta_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_rankmath_meta_description'] ) ) : '';
$rankmath_keywords = isset( $_POST['groq_ai_rankmath_focus_keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_focus_keywords'] ) ) : '';
update_term_meta( $term_id, $rankmath_keys['title'], $rankmath_title );
update_term_meta( $term_id, $rankmath_keys['description'], $rankmath_description );
update_term_meta( $term_id, $rankmath_keys['focus_keyword'], $rankmath_keywords );
}
$this->redirect_with_term_notice( $taxonomy, $term_id, 'saved', __( 'Term opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
private function resolve_term_bottom_description_meta_key( $term, $settings ) {
$default_key = '';
if ( is_array( $settings ) && isset( $settings['term_bottom_description_meta_key'] ) ) {
$default_key = sanitize_key( (string) $settings['term_bottom_description_meta_key'] );
}
$key = apply_filters( 'groq_ai_term_bottom_description_meta_key', $default_key, $term, $settings );
$key = sanitize_key( (string) $key );
return $key;
}
private function resolve_rankmath_term_meta_keys( $term, $settings ) {
$keys = [
'title' => 'rank_math_title',
'description' => 'rank_math_description',
'focus_keyword' => 'rank_math_focus_keyword',
];
$keys = apply_filters( 'groq_ai_rankmath_term_meta_keys', $keys, $term, $settings );
if ( ! is_array( $keys ) ) {
$keys = [];
}
return [
'title' => isset( $keys['title'] ) ? sanitize_key( (string) $keys['title'] ) : 'rank_math_title',
'description' => isset( $keys['description'] ) ? sanitize_key( (string) $keys['description'] ) : 'rank_math_description',
'focus_keyword' => isset( $keys['focus_keyword'] ) ? sanitize_key( (string) $keys['focus_keyword'] ) : 'rank_math_focus_keyword',
];
}
private function get_term_prompt_text( $term, $custom_prompt = null ) {
$prompt = ( null !== $custom_prompt ) ? $custom_prompt : '';
if ( null === $custom_prompt && $term && isset( $term->term_id ) ) {
$prompt = get_term_meta( $term->term_id, 'groq_ai_term_custom_prompt', true );
}
$prompt = trim( (string) $prompt );
if ( '' !== $prompt ) {
return $prompt;
}
$default_prompt = __( 'Schrijf een SEO-vriendelijke categorieomschrijving in het Nederlands. Gebruik duidelijke tussenkoppen en <p>-tags. Voeg geen prijsinformatie toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
return apply_filters( 'groq_ai_default_term_prompt', $default_prompt, $term );
}
private function redirect_with_term_notice( $taxonomy, $term_id, $type, $message = '', $status = 'success' ) {
$url = ( $taxonomy && $term_id ) ? $this->get_term_page_url( $taxonomy, $term_id ) : $this->get_page_url( 'groq-ai-product-text-categories' );
$args = [
'groq_ai_term_notice' => sanitize_key( (string) $type ),
'groq_ai_term_status' => sanitize_key( (string) $status ),
];
if ( '' !== $message ) {
$args['groq_ai_term_notice_message'] = rawurlencode( (string) $message );
}
wp_safe_redirect( add_query_arg( $args, $url ) );
exit;
}
private function ensure_term_handler_registered() {
if ( self::$term_handler_registered ) {
return;
}
add_action( 'admin_post_groq_ai_save_term_content', [ $this, 'handle_save_term_content' ] );
self::$term_handler_registered = true;
}
private function ensure_term_assets_hook() {
if ( self::$term_assets_hook_registered ) {
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_term_assets' ] );
self::$term_assets_hook_registered = true;
}
}

View File

@@ -198,7 +198,26 @@ class Groq_AI_Ajax_Controller {
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings ); $final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
} }
$request_parameters = $this->build_request_parameters_snapshot(
$settings,
[
'provider' => $provider_key,
'conversation_id' => $conversation_id,
'temperature' => 0.7,
'response_format_mode' => $use_response_format ? 'structured' : 'prompt',
'response_format_definition' => $response_format,
'term_context' => [
'term_id' => $term_id,
'taxonomy' => $taxonomy,
],
'term_options' => $usage_meta['term_options'],
'origin' => $origin,
'google_safety_settings' => isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [],
]
);
$model = $this->plugin->get_selected_model( $provider, $settings ); $model = $this->plugin->get_selected_model( $provider, $settings );
$request_parameters['model'] = $model;
$result = $provider->generate_content( $result = $provider->generate_content(
[ [
'prompt' => $final_prompt, 'prompt' => $final_prompt,
@@ -211,6 +230,12 @@ class Groq_AI_Ajax_Controller {
] ]
); );
$logged_parameters = $request_parameters;
if ( is_array( $result ) && isset( $result['request_payload'] ) ) {
$logged_parameters['http_request'] = $result['request_payload'];
unset( $result['request_payload'] );
}
if ( is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) {
if ( $logger ) { if ( $logger ) {
$logger->log_generation_event( $logger->log_generation_event(
@@ -223,6 +248,7 @@ class Groq_AI_Ajax_Controller {
'status' => 'error', 'status' => 'error',
'error_message' => $result->get_error_message(), 'error_message' => $result->get_error_message(),
'post_id' => 0, 'post_id' => 0,
'parameters' => $logged_parameters,
] ]
); );
} }
@@ -252,6 +278,7 @@ class Groq_AI_Ajax_Controller {
'status' => 'error', 'status' => 'error',
'error_message' => $parsed->get_error_message(), 'error_message' => $parsed->get_error_message(),
'post_id' => 0, 'post_id' => 0,
'parameters' => $logged_parameters,
] ]
); );
} }
@@ -273,6 +300,7 @@ class Groq_AI_Ajax_Controller {
'usage' => $response_usage, 'usage' => $response_usage,
'status' => 'success', 'status' => 'success',
'post_id' => 0, 'post_id' => 0,
'parameters' => $logged_parameters,
] ]
); );
} }
@@ -410,6 +438,19 @@ class Groq_AI_Ajax_Controller {
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : ''; $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( ! $post_id ) {
wp_send_json_error( [ 'message' => __( 'Post-ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
}
$post = get_post( $post_id );
if ( ! $post || is_wp_error( $post ) || 'product' !== $post->post_type ) {
wp_send_json_error( [ 'message' => __( 'Product niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming om dit product te bewerken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
}
$settings = $this->plugin->get_settings(); $settings = $this->plugin->get_settings();
$provider_manager = $this->plugin->get_provider_manager(); $provider_manager = $this->plugin->get_provider_manager();
$provider_key = $settings['provider']; $provider_key = $settings['provider'];
@@ -492,6 +533,23 @@ class Groq_AI_Ajax_Controller {
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings ); $final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
} }
$request_parameters = $this->build_request_parameters_snapshot(
$settings,
[
'provider' => $provider_key,
'model' => $model,
'post_id' => $post_id,
'conversation_id' => $conversation_id,
'temperature' => 0.7,
'response_format_mode' => $use_response_format ? 'structured' : 'prompt',
'response_format_definition' => $response_format,
'context_fields' => $context_fields,
'attribute_includes' => isset( $settings['product_attribute_includes'] ) ? $settings['product_attribute_includes'] : [],
'image_context' => $image_context_meta,
'google_safety_settings' => isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [],
]
);
$result = $provider->generate_content( $result = $provider->generate_content(
[ [
'prompt' => $final_prompt, 'prompt' => $final_prompt,
@@ -505,6 +563,12 @@ class Groq_AI_Ajax_Controller {
] ]
); );
$logged_parameters = $request_parameters;
if ( is_array( $result ) && isset( $result['request_payload'] ) ) {
$logged_parameters['http_request'] = $result['request_payload'];
unset( $result['request_payload'] );
}
if ( is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) {
$this->plugin->get_generation_logger()->log_generation_event( $this->plugin->get_generation_logger()->log_generation_event(
[ [
@@ -518,6 +582,7 @@ class Groq_AI_Ajax_Controller {
'post_id' => $post_id, 'post_id' => $post_id,
'status' => 'error', 'status' => 'error',
'error_message' => $result->get_error_message(), 'error_message' => $result->get_error_message(),
'parameters' => $logged_parameters,
] ]
); );
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 ); wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
@@ -543,6 +608,7 @@ class Groq_AI_Ajax_Controller {
'post_id' => $post_id, 'post_id' => $post_id,
'status' => 'error', 'status' => 'error',
'error_message' => $response->get_error_message(), 'error_message' => $response->get_error_message(),
'parameters' => $logged_parameters,
] ]
); );
wp_send_json_error( [ 'message' => $response->get_error_message() ], 500 ); wp_send_json_error( [ 'message' => $response->get_error_message() ], 500 );
@@ -557,6 +623,7 @@ class Groq_AI_Ajax_Controller {
'usage' => $response_usage, 'usage' => $response_usage,
'post_id' => $post_id, 'post_id' => $post_id,
'status' => 'success', 'status' => 'success',
'parameters' => $logged_parameters,
] ]
); );
@@ -607,4 +674,16 @@ class Groq_AI_Ajax_Controller {
return (string) $result; return (string) $result;
} }
private function build_request_parameters_snapshot( $settings, array $additional = [] ) {
$snapshot = [
'settings' => $this->plugin->get_loggable_settings_snapshot( $settings ),
];
foreach ( $additional as $key => $value ) {
$snapshot[ $key ] = $value;
}
return $snapshot;
}
} }

View File

@@ -0,0 +1,58 @@
<?php
class Groq_AI_Compatibility_Service {
/** @var bool */
private $missing_wc_notice = false;
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_DOMAIN ); ?>
</p>
</div>
<?php
}
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' );
}
}

View File

@@ -0,0 +1,28 @@
<?php
class Groq_AI_Log_Scheduler {
/** @var Groq_AI_Settings_Manager */
private $settings_manager;
/** @var Groq_AI_Generation_Logger */
private $logger;
public function __construct( Groq_AI_Settings_Manager $settings_manager, Groq_AI_Generation_Logger $logger ) {
$this->settings_manager = $settings_manager;
$this->logger = $logger;
}
public function ensure_logs_cleanup_schedule() {
if ( wp_next_scheduled( 'groq_ai_cleanup_logs' ) ) {
return;
}
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'groq_ai_cleanup_logs' );
}
public function cleanup_logs() {
$settings = $this->settings_manager->all();
$retention_days = $this->settings_manager->get_logs_retention_days( $settings );
$this->logger->cleanup_old_logs( $retention_days );
}
}

View File

@@ -0,0 +1,78 @@
<?php
class Groq_AI_Model_Service {
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
$provider_key = $provider->get_key();
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
if ( '' === $model ) {
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
if ( '' !== $default ) {
return $default;
}
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
if ( ! empty( $available ) ) {
return $available[0];
}
}
return $model;
}
public function get_cached_models_for_provider( $provider ) {
$provider = sanitize_key( (string) $provider );
$cache = $this->get_models_cache();
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
}
public function update_cached_models_for_provider( $provider, $models ) {
$provider = sanitize_key( (string) $provider );
$models = $this->sanitize_models_list( $models );
$cache = $this->get_models_cache();
$cache[ $provider ] = $models;
update_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, $cache );
return $models;
}
private function get_models_cache() {
$cache = get_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, [] );
if ( ! is_array( $cache ) ) {
$cache = [];
}
foreach ( $cache as $provider => $models ) {
$cache[ $provider ] = $this->sanitize_models_list( $models );
}
return $cache;
}
private function sanitize_models_list( $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
$models = array_values( array_unique( $models ) );
if ( ! empty( $models ) ) {
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
}
return $models;
}
}

View File

@@ -100,8 +100,10 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
$request_body['response_format'] = $args['response_format']; $request_body['response_format'] = $args['response_format'];
} }
$endpoint = $this->get_endpoint();
$response = wp_remote_post( $response = wp_remote_post(
$this->get_endpoint(), $endpoint,
[ [
'headers' => [ 'headers' => [
'Authorization' => 'Bearer ' . $api_key, 'Authorization' => 'Bearer ' . $api_key,
@@ -140,6 +142,10 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
'content' => $content, 'content' => $content,
'usage' => $usage, 'usage' => $usage,
'raw_response' => $body, 'raw_response' => $body,
'request_payload' => [
'url' => $endpoint,
'body' => $request_body,
],
]; ];
} }

View File

@@ -30,7 +30,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
} }
public function supports_response_format() { public function supports_response_format() {
return false; return true;
} }
public function supports_image_context() { public function supports_image_context() {
@@ -153,6 +153,18 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
} }
$max_tokens = max( 128, min( 8192, $max_tokens ) ); $max_tokens = max( 128, min( 8192, $max_tokens ) );
$generation_config = [
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
'maxOutputTokens' => $max_tokens,
];
$response_format = isset( $args['response_format'] ) ? $args['response_format'] : null;
$schema_payload = $this->prepare_response_schema_payload( $response_format );
if ( ! empty( $schema_payload ) ) {
$generation_config['responseMimeType'] = 'application/json';
$generation_config['responseJsonSchema'] = $schema_payload;
}
$payload = [ $payload = [
'contents' => [ 'contents' => [
[ [
@@ -160,12 +172,17 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
'parts' => $parts, 'parts' => $parts,
], ],
], ],
'generationConfig' => [ 'generationConfig' => $generation_config,
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
'maxOutputTokens' => $max_tokens,
],
]; ];
$safety_settings_payload = $this->build_safety_settings_payload(
isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : []
);
if ( ! empty( $safety_settings_payload ) ) {
$payload['safetySettings'] = $safety_settings_payload;
}
$response = wp_remote_post( $response = wp_remote_post(
$endpoint, $endpoint,
[ [
@@ -204,7 +221,11 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
} }
$content = trim( implode( "\n\n", array_filter( $texts ) ) ); $content = trim( implode( "\n\n", array_filter( $texts ) ) );
$usage = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : []; $usage_metadata = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : [];
$usage = $usage_metadata;
if ( ! empty( $usage_metadata ) ) {
$usage = array_merge( $usage, $this->map_usage_metadata_counts( $usage_metadata ) );
}
$finish_reason = isset( $body['candidates'][0]['finishReason'] ) ? sanitize_text_field( (string) $body['candidates'][0]['finishReason'] ) : ''; $finish_reason = isset( $body['candidates'][0]['finishReason'] ) ? sanitize_text_field( (string) $body['candidates'][0]['finishReason'] ) : '';
if ( '' !== $finish_reason ) { if ( '' !== $finish_reason ) {
$usage['finish_reason'] = $finish_reason; $usage['finish_reason'] = $finish_reason;
@@ -214,6 +235,118 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
'content' => $content, 'content' => $content,
'usage' => $usage, 'usage' => $usage,
'raw_response' => $body, 'raw_response' => $body,
'request_payload' => [
'url' => $endpoint,
'body' => $payload,
],
]; ];
} }
private function build_safety_settings_payload( $settings ) {
if ( empty( $settings ) || ! is_array( $settings ) ) {
return [];
}
$categories = class_exists( 'Groq_AI_Settings_Manager' ) ? array_keys( Groq_AI_Settings_Manager::get_google_safety_categories_list() ) : [];
$thresholds = class_exists( 'Groq_AI_Settings_Manager' ) ? array_keys( Groq_AI_Settings_Manager::get_google_safety_thresholds_list() ) : [];
if ( empty( $categories ) || empty( $thresholds ) ) {
return [];
}
$payload = [];
foreach ( $settings as $category => $threshold ) {
$category = sanitize_text_field( (string) $category );
$threshold = sanitize_text_field( (string) $threshold );
if ( ! in_array( $category, $categories, true ) || ! in_array( $threshold, $thresholds, true ) ) {
continue;
}
$payload[] = [
'category' => $category,
'threshold' => $threshold,
];
}
return $payload;
}
private function prepare_response_schema_payload( $response_format ) {
if ( empty( $response_format ) || ! is_array( $response_format ) ) {
return [];
}
if ( isset( $response_format['type'] ) && 'json_schema' === $response_format['type'] ) {
if ( isset( $response_format['json_schema']['schema'] ) && is_array( $response_format['json_schema']['schema'] ) ) {
return $this->sanitize_schema_definition( $response_format['json_schema']['schema'] );
}
if ( isset( $response_format['schema'] ) && is_array( $response_format['schema'] ) ) {
return $this->sanitize_schema_definition( $response_format['schema'] );
}
}
return [];
}
private function sanitize_schema_definition( $schema ) {
if ( ! is_array( $schema ) ) {
return [];
}
$encoded = wp_json_encode( $schema );
if ( ! $encoded ) {
return [];
}
$decoded = json_decode( $encoded, true );
if ( ! is_array( $decoded ) ) {
return [];
}
$this->remove_disallowed_schema_keys( $decoded );
return $decoded;
}
private function remove_disallowed_schema_keys( array &$schema ) {
$disallowed = [ 'additionalProperties' ];
foreach ( $schema as $key => &$value ) {
if ( in_array( $key, $disallowed, true ) ) {
unset( $schema[ $key ] );
continue;
}
if ( is_array( $value ) ) {
$this->remove_disallowed_schema_keys( $value );
}
}
unset( $value );
}
private function map_usage_metadata_counts( $metadata ) {
if ( ! is_array( $metadata ) ) {
return [];
}
$mapped = [];
if ( isset( $metadata['promptTokenCount'] ) ) {
$mapped['prompt_tokens'] = absint( $metadata['promptTokenCount'] );
}
if ( isset( $metadata['candidatesTokenCount'] ) ) {
$mapped['completion_tokens'] = absint( $metadata['candidatesTokenCount'] );
}
if ( isset( $metadata['totalTokenCount'] ) ) {
$mapped['total_tokens'] = absint( $metadata['totalTokenCount'] );
}
return $mapped;
}
} }

View File

@@ -26,9 +26,32 @@ class Groq_AI_Generation_Logger {
$table = $this->get_logs_table_name(); $table = $this->get_logs_table_name();
$usage = isset( $args['usage'] ) && is_array( $args['usage'] ) ? $args['usage'] : []; $usage = isset( $args['usage'] ) && is_array( $args['usage'] ) ? $args['usage'] : [];
$prompt_tokens = isset( $usage['prompt_tokens'] ) ? absint( $usage['prompt_tokens'] ) : null; $parameters = isset( $args['parameters'] ) && is_array( $args['parameters'] ) ? $args['parameters'] : [];
$completion_tokens = isset( $usage['completion_tokens'] ) ? absint( $usage['completion_tokens'] ) : null; $prompt_tokens = $this->extract_usage_token_value(
$total_tokens = isset( $usage['total_tokens'] ) ? absint( $usage['total_tokens'] ) : null; $usage,
[
'prompt_tokens',
'promptTokenCount',
'input_tokens',
'inputTokenCount',
]
);
$completion_tokens = $this->extract_usage_token_value(
$usage,
[
'completion_tokens',
'output_tokens',
'candidatesTokenCount',
'outputTokenCount',
]
);
$total_tokens = $this->extract_usage_token_value(
$usage,
[
'total_tokens',
'totalTokenCount',
]
);
$wpdb->insert( $wpdb->insert(
$table, $table,
@@ -46,6 +69,7 @@ class Groq_AI_Generation_Logger {
'status' => isset( $args['status'] ) ? sanitize_text_field( $args['status'] ) : 'success', 'status' => isset( $args['status'] ) ? sanitize_text_field( $args['status'] ) : 'success',
'error_message' => isset( $args['error_message'] ) ? $args['error_message'] : '', 'error_message' => isset( $args['error_message'] ) ? $args['error_message'] : '',
'usage_json' => ! empty( $usage ) ? wp_json_encode( $usage ) : null, 'usage_json' => ! empty( $usage ) ? wp_json_encode( $usage ) : null,
'request_json' => ! empty( $parameters ) ? wp_json_encode( $parameters ) : null,
] ]
); );
} }
@@ -77,6 +101,7 @@ class Groq_AI_Generation_Logger {
public function maybe_create_table() { public function maybe_create_table() {
if ( get_option( self::OPTION_TABLE_CREATED ) ) { if ( get_option( self::OPTION_TABLE_CREATED ) ) {
$this->logs_table_exists = true; $this->logs_table_exists = true;
$this->maybe_upgrade_table_schema();
return; return;
} }
@@ -106,6 +131,7 @@ class Groq_AI_Generation_Logger {
status varchar(20) NOT NULL, status varchar(20) NOT NULL,
error_message text DEFAULT NULL, error_message text DEFAULT NULL,
usage_json longtext DEFAULT NULL, usage_json longtext DEFAULT NULL,
request_json longtext DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY provider (provider), KEY provider (provider),
KEY post_id (post_id) KEY post_id (post_id)
@@ -117,6 +143,40 @@ class Groq_AI_Generation_Logger {
update_option( self::OPTION_TABLE_CREATED, 1 ); update_option( self::OPTION_TABLE_CREATED, 1 );
} }
public function cleanup_old_logs( $retention_days ) {
$retention_days = absint( $retention_days );
if ( $retention_days <= 0 || ! $this->logs_table_exists() ) {
return;
}
$cutoff = time() - ( $retention_days * DAY_IN_SECONDS );
$cutoff = gmdate( 'Y-m-d H:i:s', $cutoff );
global $wpdb;
$table = $this->get_logs_table_name();
$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) );
}
private function extract_usage_token_value( $usage, $keys ) {
foreach ( (array) $keys as $key ) {
if ( isset( $usage[ $key ] ) ) {
return absint( $usage[ $key ] );
}
}
return null;
}
private function maybe_upgrade_table_schema() {
global $wpdb;
$table = $this->get_logs_table_name();
$column = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM {$table} LIKE %s", 'request_json' ) );
if ( ! $column ) {
$this->create_table();
}
}
private function get_logs_table_name() { private function get_logs_table_name() {
global $wpdb; global $wpdb;

View File

@@ -33,6 +33,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '', 'store_context' => '',
'default_prompt' => '', 'default_prompt' => '',
'max_output_tokens' => 2048, 'max_output_tokens' => 2048,
'logs_retention_days' => 90,
'product_attribute_includes' => [], 'product_attribute_includes' => [],
'term_bottom_description_meta_key' => '', 'term_bottom_description_meta_key' => '',
'groq_api_key' => '', 'groq_api_key' => '',
@@ -47,6 +48,7 @@ class Groq_AI_Settings_Manager {
'google_enable_ga' => true, 'google_enable_ga' => true,
'google_gsc_site_url' => '', 'google_gsc_site_url' => '',
'google_ga4_property_id' => '', 'google_ga4_property_id' => '',
'google_safety_settings' => [],
'context_fields' => $this->get_default_context_fields(), 'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(), 'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url', 'image_context_mode' => 'url',
@@ -60,7 +62,10 @@ class Groq_AI_Settings_Manager {
$settings = wp_parse_args( (array) $settings, $defaults ); $settings = wp_parse_args( (array) $settings, $defaults );
$settings['context_fields'] = $this->normalize_context_fields( isset( $settings['context_fields'] ) ? $settings['context_fields'] : [] ); $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'] : [] ); $settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] );
$settings['google_safety_settings'] = $this->sanitize_google_safety_settings( isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [] );
$settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' ); $settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' );
$logs_retention_days = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90;
$settings['logs_retention_days'] = max( 0, min( 3650, $logs_retention_days ) );
$image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url'; $image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
if ( 'none' === $image_mode ) { if ( 'none' === $image_mode ) {
@@ -106,6 +111,7 @@ class Groq_AI_Settings_Manager {
'store_context' => '', 'store_context' => '',
'default_prompt' => '', 'default_prompt' => '',
'max_output_tokens' => 2048, 'max_output_tokens' => 2048,
'logs_retention_days' => 90,
'product_attribute_includes' => [], 'product_attribute_includes' => [],
'term_bottom_description_meta_key' => '', 'term_bottom_description_meta_key' => '',
'groq_api_key' => '', 'groq_api_key' => '',
@@ -120,6 +126,7 @@ class Groq_AI_Settings_Manager {
'google_enable_ga' => true, 'google_enable_ga' => true,
'google_gsc_site_url' => '', 'google_gsc_site_url' => '',
'google_ga4_property_id' => '', 'google_ga4_property_id' => '',
'google_safety_settings' => [],
'context_fields' => $this->get_default_context_fields(), 'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(), 'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url', 'image_context_mode' => 'url',
@@ -156,6 +163,10 @@ class Groq_AI_Settings_Manager {
// Keep within sane bounds across providers. // Keep within sane bounds across providers.
$max_output_tokens = max( 128, min( 8192, $max_output_tokens ) ); $max_output_tokens = max( 128, min( 8192, $max_output_tokens ) );
$logs_retention_days = isset( $input['logs_retention_days'] ) ? (int) $input['logs_retention_days'] : (int) $defaults['logs_retention_days'];
// 0 = keep indefinitely, otherwise cap at 10 years.
$logs_retention_days = max( 0, min( 3650, $logs_retention_days ) );
$context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] ); $context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] );
if ( 'none' === $image_mode ) { if ( 'none' === $image_mode ) {
@@ -179,6 +190,7 @@ class Groq_AI_Settings_Manager {
'store_context' => sanitize_textarea_field( $input['store_context'] ), 'store_context' => sanitize_textarea_field( $input['store_context'] ),
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ), 'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
'max_output_tokens' => $max_output_tokens, 'max_output_tokens' => $max_output_tokens,
'logs_retention_days' => $logs_retention_days,
'product_attribute_includes' => $this->sanitize_product_attribute_includes( isset( $raw_input['product_attribute_includes'] ) ? $raw_input['product_attribute_includes'] : [] ), 'product_attribute_includes' => $this->sanitize_product_attribute_includes( isset( $raw_input['product_attribute_includes'] ) ? $raw_input['product_attribute_includes'] : [] ),
'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ), 'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ),
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ), 'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
@@ -193,6 +205,7 @@ class Groq_AI_Settings_Manager {
'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ), 'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ),
'google_gsc_site_url' => esc_url_raw( (string) $input['google_gsc_site_url'] ), 'google_gsc_site_url' => esc_url_raw( (string) $input['google_gsc_site_url'] ),
'google_ga4_property_id' => sanitize_text_field( (string) $input['google_ga4_property_id'] ), 'google_ga4_property_id' => sanitize_text_field( (string) $input['google_ga4_property_id'] ),
'google_safety_settings' => $this->sanitize_google_safety_settings( isset( $raw_input['google_safety_settings'] ) ? $raw_input['google_safety_settings'] : [] ),
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ), 'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
'image_context_mode' => $image_mode, 'image_context_mode' => $image_mode,
'image_context_limit' => $image_limit, 'image_context_limit' => $image_limit,
@@ -422,6 +435,104 @@ class Groq_AI_Settings_Manager {
return $this->sanitize_term_description_char_limit_value( $value, 1200 ); return $this->sanitize_term_description_char_limit_value( $value, 1200 );
} }
public function get_google_safety_settings( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
return $this->sanitize_google_safety_settings( isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [] );
}
public function get_google_safety_categories() {
return self::get_google_safety_categories_list();
}
public function get_google_safety_thresholds() {
return self::get_google_safety_thresholds_list();
}
public function get_logs_retention_days( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
$value = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90;
return max( 0, min( 3650, $value ) );
}
public function get_loggable_settings_snapshot( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
$allowed_keys = [
'store_context',
'default_prompt',
'max_output_tokens',
'logs_retention_days',
'product_attribute_includes',
'context_fields',
'modules',
'image_context_mode',
'image_context_limit',
'response_format_compat',
'term_top_description_char_limit',
'term_bottom_description_char_limit',
'term_bottom_description_meta_key',
'google_safety_settings',
'google_enable_gsc',
'google_enable_ga',
'google_gsc_site_url',
'google_ga4_property_id',
];
$snapshot = [];
foreach ( $allowed_keys as $key ) {
if ( array_key_exists( $key, $settings ) ) {
$snapshot[ $key ] = $settings[ $key ];
}
}
return $snapshot;
}
public static function get_google_safety_categories_list() {
return [
'HARM_CATEGORY_HARASSMENT' => [
'label' => __( 'Harassment & intimidatie', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Detecteert bedreigingen en pesterijen in de output.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
'HARM_CATEGORY_HATE_SPEECH' => [
'label' => __( 'Haatspraak', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Beperkt discriminerende of denigrerende taal.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
'HARM_CATEGORY_SEXUALLY_EXPLICIT' => [
'label' => __( 'Seksueel expliciet', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Filtert beschrijvingen van seksuele handelingen of fetish-content.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
'HARM_CATEGORY_DANGEROUS_CONTENT' => [
'label' => __( 'Gevaarlijke activiteiten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Voorkomt instructies rond geweld, wapens of gevaarlijke middelen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
'HARM_CATEGORY_CIVIC_INTEGRITY' => [
'label' => __( 'Civieke integriteit', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'description' => __( 'Vermindert desinformatie rond verkiezingen en burgerprocessen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
],
];
}
public static function get_google_safety_thresholds_list() {
return [
'' => __( 'Google standaard (niet meesturen)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'HARM_BLOCK_THRESHOLD_UNSPECIFIED' => __( 'Onbekende drempel (laat Google beslissen)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'BLOCK_LOW_AND_ABOVE' => __( 'Blokkeer lage ernst en hoger', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'BLOCK_MEDIUM_AND_ABOVE' => __( 'Blokkeer middel en hoger', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'BLOCK_ONLY_HIGH' => __( 'Blokkeer alleen hoge ernst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
'BLOCK_NONE' => __( 'Sta alles toe (geen blokkade)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
];
}
public function is_response_format_compat_enabled( $settings = null ) { public function is_response_format_compat_enabled( $settings = null ) {
if ( null === $settings ) { if ( null === $settings ) {
$settings = $this->all(); $settings = $this->all();
@@ -505,4 +616,27 @@ class Groq_AI_Settings_Manager {
return min( 10, $limit ); return min( 10, $limit );
} }
private function sanitize_google_safety_settings( $settings ) {
if ( ! is_array( $settings ) ) {
return [];
}
$categories = array_keys( self::get_google_safety_categories_list() );
$thresholds = array_keys( self::get_google_safety_thresholds_list() );
$clean = [];
foreach ( $settings as $category => $threshold ) {
$category = sanitize_text_field( (string) $category );
$threshold = sanitize_text_field( (string) $threshold );
if ( '' === $threshold || ! in_array( $category, $categories, true ) || ! in_array( $threshold, $thresholds, true ) ) {
continue;
}
$clean[ $category ] = $threshold;
}
return $clean;
}
} }

View File

@@ -0,0 +1,181 @@
<?php
/**
* Siti License Validator
*
* Validates licenses against the Siti Plugin Repo API at plugins.robert.ooo
*/
class SitiLicenseValidator {
private $api_base_url = 'https://plugins.robert.ooo';
private $license_key;
private $hostname;
private $plugin_version;
public function __construct( $license_key = null, $hostname = null, $plugin_version = null ) {
$this->license_key = $license_key ?: get_option( 'siti_license_key' );
$this->hostname = $hostname ?: parse_url( home_url(), PHP_URL_HOST );
$this->plugin_version = $plugin_version;
}
/**
* Set the license key
*/
public function set_license_key( $key ) {
$this->license_key = $key;
}
/**
* Set the API base URL
*/
public function set_api_base_url( $url ) {
$this->api_base_url = rtrim( $url, '/' );
}
/**
* Set the hostname
*/
public function set_hostname( $hostname ) {
$this->hostname = $hostname;
}
/**
* Set the plugin version for reporting purposes
*/
public function set_plugin_version( $version ) {
$this->plugin_version = $version;
}
/**
* Verify the license
*
* @return array|WP_Error License data on success, WP_Error on failure
*/
public function verify() {
if ( empty( $this->license_key ) ) {
return new WP_Error( 'missing_license', 'Geen licentiecode ingesteld.' );
}
if ( empty( $this->hostname ) ) {
return new WP_Error( 'missing_hostname', 'Kon hostname niet bepalen.' );
}
$response = wp_remote_post( $this->api_base_url . '/api/licenses/verify', array(
'headers' => array(
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array(
'key' => $this->license_key,
'hostname' => $this->hostname,
'currentVersion' => $this->plugin_version,
) ),
'timeout' => 15,
) );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $body ) ) {
return new WP_Error( 'verification_failed', 'Onverwacht antwoord van licentieserver.' );
}
$has_payload = isset( $body['license'] ) || isset( $body['pluginVersion'] ) || isset( $body['version'] );
if ( 200 !== $code ) {
if ( $has_payload ) {
$this->store_license_payload( $body );
}
$error_message = isset( $body['error'] ) ? $body['error'] : 'Licentie verificatie mislukt.';
return new WP_Error( 'verification_failed', $error_message );
}
if ( empty( $body['valid'] ) ) {
if ( $has_payload ) {
$this->store_license_payload( $body );
}
return new WP_Error( 'invalid_license', 'Licentie is ongeldig.' );
}
$this->store_license_payload( $body );
return $body;
}
/**
* Get stored license data
*/
public function get_license_data() {
return get_option( 'siti_license_data', array() );
}
/**
* Check if license is valid (cached)
*/
public function is_valid() {
$data = $this->get_license_data();
return ! empty( $data ) && isset( $data['key'] );
}
/**
* Get the latest version from license data
*/
public function get_latest_version() {
$data = $this->get_license_data();
return $data['pluginVersion'] ?? null;
}
/**
* Download a specific version
*
* @param string $version Version to download (e.g., 'v1.2.3' or 'latest')
* @return string|WP_Error Download URL or error
*/
public function get_download_url( $version = 'latest' ) {
if ( empty( $this->license_key ) || empty( $this->hostname ) ) {
return new WP_Error( 'missing_credentials', 'Licentie of hostname ontbreekt.' );
}
// For now, return the API download endpoint
// In practice, you might want to proxy this through WordPress
return $this->api_base_url . '/api/licenses/download?key=' . urlencode( $this->license_key ) . '&hostname=' . urlencode( $this->hostname ) . '&version=' . urlencode( $version );
}
/**
* Persist payload data locally for UI purposes
*/
private function store_license_payload( $body ) {
$license_data = array();
if ( isset( $body['license'] ) && is_array( $body['license'] ) ) {
$license_data = $body['license'];
}
if ( empty( $license_data['key'] ) ) {
$license_data['key'] = $this->license_key;
}
$version = null;
if ( ! empty( $body['pluginVersion'] ) ) {
$version = $body['pluginVersion'];
} elseif ( ! empty( $body['version'] ) ) {
$version = $body['version'];
}
if ( $version ) {
$license_data['pluginVersion'] = $version;
}
if ( empty( $license_data ) ) {
return;
}
update_option( 'siti_license_data', $license_data );
update_option( 'siti_license_last_check', current_time( 'mysql' ) );
}
}

View File

@@ -0,0 +1,510 @@
<?php
/**
* SitiWebUpdater2
*
* Updates plugins via the Siti Plugin Repo API instead of GitHub
* Fully self-contained with built-in license management
*/
class SitiWebUpdater2
{
private static $instances = array();
private static $global_settings_registered = false;
private $file;
private $plugin;
private $basename;
private $active;
private $owner;
private $repository;
private $license_key;
private $api_base_url = 'https://plugins.robert.ooo';
private $plugin_response;
private $option_prefix;
public function __construct($file)
{
$this->file = $file;
$this->option_prefix = 'siti_updater_' . sanitize_key(dirname(plugin_basename($this->file))) . '_';
$this->set_plugin_properties();
// Register this instance globally
self::$instances[$this->option_prefix] = $this;
add_action('admin_init', array($this, 'set_plugin_properties'));
add_action('admin_notices', array($this, 'admin_notices'));
add_action('siti_updater_daily_check_' . $this->option_prefix, array($this, 'daily_license_check'));
if (!wp_next_scheduled('siti_updater_daily_check_' . $this->option_prefix)) {
wp_schedule_event(time(), 'daily', 'siti_updater_daily_check_' . $this->option_prefix);
}
return $this;
}
public function initialize()
{
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_updates'));
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
// Register global settings page only once
if (!self::$global_settings_registered) {
add_action('admin_menu', array($this, 'add_global_settings_page'));
self::$global_settings_registered = true;
}
}
public function set_plugin_properties()
{
if (!function_exists('get_plugin_data')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$this->plugin = get_plugin_data($this->file, false, false);
$this->basename = plugin_basename($this->file);
$this->active = is_plugin_active($this->basename);
}
public static function get_instances()
{
return self::$instances;
}
public function get_plugin_data()
{
return $this->plugin;
}
public function set_owner($owner)
{
$this->owner = $owner;
}
public function set_repository($repository)
{
$this->repository = $repository;
}
public function set_license_key($key)
{
$sanitized_key = sanitize_text_field((string) $key);
$stored_key = get_option($this->option_prefix . 'license_key', '');
if ($stored_key !== $sanitized_key) {
$this->clear_cached_license_data();
}
$this->license_key = $sanitized_key;
update_option($this->option_prefix . 'license_key', $sanitized_key);
}
public function get_license_key()
{
if (null === $this->license_key) {
$this->license_key = get_option($this->option_prefix . 'license_key', '');
}
return $this->license_key;
}
public function get_license_data()
{
return $this->get_stored_license_data();
}
public function set_api_base_url($api_base_url)
{
$this->api_base_url = rtrim((string) $api_base_url, '/');
}
private function normalize_version($version)
{
if (!is_string($version)) {
return $version;
}
$version = trim($version);
if ('' === $version) {
return $version;
}
if (0 === stripos($version, 'v')) {
$version = ltrim($version, 'vV');
}
return $version;
}
private function get_stored_license_data()
{
$data = get_option($this->option_prefix . 'license_data', array());
return is_array($data) ? $data : array();
}
private function clear_cached_license_data()
{
delete_option($this->option_prefix . 'license_data');
delete_option($this->option_prefix . 'last_check');
}
private function store_license_data($body)
{
if (!is_array($body)) {
$body = array();
}
$license_data = array();
if (isset($body['license']) && is_array($body['license'])) {
$license_data = $body['license'];
}
if (empty($license_data['key'])) {
$license_data['key'] = $this->get_license_key();
}
$license_data['licenseValid'] = !empty($body['valid']);
$version = null;
if (isset($body['pluginVersion']) && $body['pluginVersion']) {
$version = $body['pluginVersion'];
} elseif (isset($body['version']) && $body['version']) {
$version = $body['version'];
} elseif (isset($license_data['pluginVersion']) && $license_data['pluginVersion']) {
$version = $license_data['pluginVersion'];
}
if ($version) {
$license_data['pluginVersion'] = $this->normalize_version($version);
}
update_option($this->option_prefix . 'license_data', $license_data);
update_option($this->option_prefix . 'last_check', current_time('mysql'));
}
private function get_plugin_info()
{
if (is_null($this->plugin_response)) {
$request_uri = sprintf('%s/api/plugins/%s/%s', $this->api_base_url, $this->owner, $this->repository);
$response = wp_remote_get($request_uri, array('timeout' => 15));
if (is_wp_error($response)) {
$this->plugin_response = false;
return;
}
$code = wp_remote_retrieve_response_code($response);
if (200 !== $code) {
$this->plugin_response = false;
return;
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (is_array($body) && isset($body['version'])) {
$body['version'] = $this->normalize_version($body['version']);
}
$this->plugin_response = $body;
}
}
public function check_for_updates($transient)
{
if (empty($transient->checked)) {
return $transient;
}
$this->get_plugin_info();
if (false === $this->plugin_response || empty($this->plugin_response['version'])) {
return $transient;
}
$remote_version = $this->normalize_version($this->plugin_response['version']);
$current_version = $this->normalize_version($this->plugin['Version']);
if ($remote_version && version_compare($remote_version, $current_version, '>')) {
$download_url = $this->get_download_url('latest');
if (!is_wp_error($download_url)) {
$transient->response[$this->basename] = (object) array(
'slug' => dirname($this->basename),
'new_version' => $remote_version,
'package' => $download_url,
'tested' => get_bloginfo('version'),
'url' => $this->plugin_response['homepage'] ?? '',
);
}
}
return $transient;
}
public function plugin_info($result, $action, $args)
{
if ('plugin_information' !== $action || $args->slug !== dirname($this->basename)) {
return $result;
}
$this->get_plugin_info();
if (false === $this->plugin_response) {
return $result;
}
return (object) array(
'name' => $this->plugin_response['name'] ?? $this->plugin['Name'],
'slug' => $args->slug,
'version' => $this->normalize_version($this->plugin_response['version'] ?? $this->plugin['Version']),
'author' => $this->plugin_response['author'] ?? $this->plugin['Author'],
'author_profile' => $this->plugin_response['author_url'] ?? $this->plugin['AuthorURI'],
'homepage' => $this->plugin_response['homepage'] ?? '',
'requires' => $this->plugin_response['requires'] ?? '',
'tested' => $this->plugin_response['tested'] ?? get_bloginfo('version'),
'downloaded' => $this->plugin_response['downloads'] ?? 0,
'last_updated' => $this->plugin_response['last_updated'] ?? '',
'sections' => array(
'description' => $this->plugin_response['description'] ?? $this->plugin['Description'],
'changelog' => $this->plugin_response['changelog'] ?? '',
),
'download_link' => $this->get_download_url('latest'),
);
}
private function get_download_url($version = 'latest')
{
$license_key = $this->get_license_key();
if (empty($license_key)) {
return new WP_Error('no_license', 'Geen licentie ingesteld');
}
$hostname = parse_url(home_url(), PHP_URL_HOST);
return $this->api_base_url . '/api/licenses/download?key=' . urlencode($license_key) . '&hostname=' . urlencode($hostname) . '&version=' . urlencode($version);
}
public function verify_license()
{
$license_key = $this->get_license_key();
if (empty($license_key)) {
return new WP_Error('missing_license', 'Geen licentiecode ingesteld.');
}
$hostname = parse_url(home_url(), PHP_URL_HOST);
$current_version = isset($this->plugin['Version']) ? $this->plugin['Version'] : null;
$response = wp_remote_post($this->api_base_url . '/api/licenses/verify', array(
'headers' => array(
'Content-Type' => 'application/json',
),
'body' => wp_json_encode(array(
'key' => $license_key,
'hostname' => $hostname,
'currentVersion' => $current_version,
'owner' => $this->owner,
'repository' => $this->repository,
)),
'timeout' => 15,
));
if (is_wp_error($response)) {
return $response;
}
$code = wp_remote_retrieve_response_code($response);
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($body)) {
$this->clear_cached_license_data();
return new WP_Error('verification_failed', 'Onverwacht antwoord van licentieserver.');
}
$has_license_payload = isset($body['license']) || isset($body['pluginVersion']) || isset($body['version']);
if (200 !== $code) {
if ($has_license_payload) {
$this->store_license_data($body);
} else {
$this->clear_cached_license_data();
}
$error_message = isset($body['error']) ? $body['error'] : 'Licentie verificatie mislukt.';
return new WP_Error('verification_failed', $error_message);
}
if (empty($body['valid'])) {
if ($has_license_payload) {
$this->store_license_data($body);
} else {
$this->clear_cached_license_data();
}
return new WP_Error('invalid_license', 'Licentie is ongeldig.');
}
$this->store_license_data($body);
return $body;
}
public function is_license_valid()
{
$data = $this->get_license_data();
if (empty($data) || empty($data['key'])) {
return false;
}
if (isset($data['licenseValid'])) {
return (bool) $data['licenseValid'];
}
return true;
}
public function get_latest_known_version()
{
$this->get_plugin_info();
if (!empty($this->plugin_response['version'])) {
return $this->normalize_version($this->plugin_response['version']);
}
$license_data = $this->get_license_data();
if (isset($license_data['pluginVersion']) && $license_data['pluginVersion']) {
return $this->normalize_version($license_data['pluginVersion']);
}
return null;
}
public function admin_notices()
{
if (!current_user_can('manage_options')) {
return;
}
if (!$this->is_license_valid()) {
$settings_url = admin_url('options-general.php?page=siti-plugin-licenses');
echo '<div class="notice notice-error"><p>';
printf(
esc_html__('%s: Licentie ongeldig of niet ingesteld. Ga naar %s om je licentie in te voeren.', 'siti-updater'),
esc_html($this->plugin['Name']),
'<a href="' . esc_url($settings_url) . '">' . esc_html__('plugin licenties', 'siti-updater') . '</a>'
);
echo '</p></div>';
}
}
public function daily_license_check()
{
$result = $this->verify_license();
if (is_wp_error($result)) {
error_log($this->plugin['Name'] . ' License check failed: ' . $result->get_error_message());
}
}
public function add_global_settings_page()
{
add_options_page(
__('Plugin Licenties', 'siti-updater'),
__('Plugin Licenties', 'siti-updater'),
'manage_options',
'siti-plugin-licenses',
array($this, 'render_global_settings_page')
);
}
public function render_global_settings_page()
{
if (!current_user_can('manage_options')) {
return;
}
if (isset($_POST['siti_updater_save']) && wp_verify_nonce($_POST['_wpnonce'], 'siti_updater_save')) {
foreach (self::$instances as $prefix => $instance) {
$key = $prefix . 'license_key';
if (isset($_POST[$key])) {
$instance->set_license_key(sanitize_text_field($_POST[$key]));
$result = $instance->verify_license();
if (is_wp_error($result)) {
$plugin_data = $instance->get_plugin_data();
add_settings_error('siti_updater', 'license_error_' . $prefix, $plugin_data['Name'] . ': ' . $result->get_error_message());
} else {
$plugin_data = $instance->get_plugin_data();
add_settings_error('siti_updater', 'license_success_' . $prefix, $plugin_data['Name'] . ': ' . __('Licentie succesvol opgeslagen en geverifieerd.', 'siti-updater'), 'updated');
}
}
}
}
?>
<div class="wrap">
<h1><?php esc_html_e('Plugin Licenties', 'siti-updater'); ?></h1>
<p class="description">
<?php esc_html_e('Beheer licentiecodes voor al je plugins die het Siti update systeem gebruiken.', 'siti-updater'); ?>
</p>
<?php settings_errors('siti_updater'); ?>
<form method="post" action="">
<?php wp_nonce_field('siti_updater_save'); ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php esc_html_e('Plugin', 'siti-updater'); ?></th>
<th><?php esc_html_e('Licentiecode', 'siti-updater'); ?></th>
<th><?php esc_html_e('Geldig', 'siti-updater'); ?></th>
<th><?php esc_html_e('Huidige versie', 'siti-updater'); ?></th>
<th><?php esc_html_e('Nieuwste versie', 'siti-updater'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach (self::$instances as $prefix => $instance):
$license_data = $instance->get_license_data();
$is_valid = $instance->is_license_valid();
$plugin_data = $instance->get_plugin_data();
$current_version = $plugin_data['Version'];
$latest_known = $instance->get_latest_known_version();
$latest_version = $latest_known ? $latest_known : (isset($license_data['pluginVersion']) ? $license_data['pluginVersion'] : '-');
?>
<tr>
<td>
<strong><?php echo esc_html($plugin_data['Name']); ?></strong>
<br>
<small><?php echo esc_html($plugin_data['Description']); ?></small>
</td>
<td>
<input type="text" name="<?php echo esc_attr($prefix . 'license_key'); ?>"
value="<?php echo esc_attr($instance->get_license_key()); ?>" class="regular-text"
placeholder="<?php esc_attr_e('Voer licentiecode in...', 'siti-updater'); ?>" />
<p class="description">
<?php printf(esc_html__('Licentie voor %s. Verkrijg je code van plugins.robert.ooo', 'siti-updater'), $plugin_data['Name']); ?>
</p>
</td>
<td>
<span class="license-status <?php echo $is_valid ? 'valid' : 'invalid'; ?>">
<?php echo $is_valid ? '<span style="color: green;">✓ ' . esc_html__('Ja', 'siti-updater') . '</span>' : '<span style="color: red;">✗ ' . esc_html__('Nee', 'siti-updater') . '</span>'; ?>
</span>
</td>
<td>
<?php echo esc_html($current_version); ?>
</td>
<td>
<?php echo esc_html($latest_version); ?>
<?php if ($latest_version && '-' !== $latest_version && version_compare($latest_version, $current_version, '>')): ?>
<br><small
style="color: orange;"><?php esc_html_e('Update beschikbaar!', 'siti-updater'); ?></small>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="submit">
<input type="submit" name="siti_updater_save" class="button button-primary"
value="<?php esc_attr_e('Licenties opslaan', 'siti-updater'); ?>" />
</p>
</form>
</div>
<?php
}
}

View File

@@ -0,0 +1,202 @@
msgid ""
msgstr ""
"Project-Id-Version: SitiAI Product Teksten\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-31 00:00+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Domain: siti-ai-product-content-generator\n"
#: assets/js/settings.js:166
msgid "Deze aanbieder ondersteunt dit niet."
msgstr ""
#: assets/js/settings.js:173
msgid "Vul eerst de API-sleutel in."
msgstr ""
#: assets/js/settings.js:177
msgid "Modellen worden opgehaald…"
msgstr ""
#: assets/js/settings.js:197
msgid "Onbekende fout"
msgstr ""
#: assets/js/settings.js:202
msgid "Modellen bijgewerkt."
msgstr ""
#: assets/js/settings.js:205
msgid "Ophalen mislukt."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:844
msgid "Deze aanbieder ondersteunt dit niet."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:845
msgid "Vul eerst de API-sleutel in."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:846
msgid "Modellen worden opgehaald…"
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:847
msgid "Onbekende fout"
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:848
msgid "Modellen bijgewerkt."
msgstr ""
#: includes/Admin/class-groq-ai-settings-page.php:849
msgid "Ophalen mislukt."
msgstr ""
#: assets/js/term-admin.js:24
msgid "Vul eerst een prompt in."
msgstr ""
#: assets/js/term-admin.js:25
msgid "AI is bezig met schrijven..."
msgstr ""
#: assets/js/term-admin.js:26
msgid "Tekst gegenereerd. Je kunt hem toepassen en opslaan."
msgstr ""
#: assets/js/term-admin.js:27
msgid "Tekst ingevuld. Vergeet niet op \"Opslaan\" te klikken."
msgstr ""
#: assets/js/term-admin.js:28
msgid "Er ging iets mis bij het genereren."
msgstr ""
#: assets/js/term-admin.js:119
msgid "Onbekende fout"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:212
msgid "Onbekende fout"
msgstr ""
#: assets/js/admin.js:100
msgid "AI is bezig met schrijven..."
msgstr ""
#: assets/js/admin.js:101
msgid "Probeer het opnieuw of pas je prompt/context aan."
msgstr ""
#: assets/js/admin.js:102
msgid "Er ging iets mis bij het genereren."
msgstr ""
#: assets/js/admin.js:103
msgid "Onbekende fout."
msgstr ""
#: assets/js/admin.js:104
msgid "Structuur gegenereerd. Kopieer of vul velden in."
msgstr ""
#: assets/js/admin.js:105
msgid "%s ingevuld."
msgstr ""
#: assets/js/admin.js:106
msgid "Kon het veld niet automatisch invullen."
msgstr ""
#: assets/js/admin.js:107
msgid "%s gekopieerd naar het klembord."
msgstr ""
#: assets/js/admin.js:108
msgid "JSON gekopieerd naar het klembord."
msgstr ""
#: assets/js/admin.js:109
msgid "Kopiëren mislukt."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:84
msgid "%s ingevuld."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:85
msgid "Kon het veld niet automatisch invullen."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:86
msgid "%s gekopieerd naar het klembord."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:87
msgid "JSON gekopieerd naar het klembord."
msgstr ""
#: includes/Admin/class-groq-ai-product-ui.php:88
msgid "Kopiëren mislukt."
msgstr ""
#: assets/js/term-bulk.js:149
msgid "Onbekende fout"
msgstr ""
#: assets/js/term-bulk.js:150
msgid "%1$s: %2$s"
msgstr ""
#: assets/js/term-bulk.js:152
msgid "%1$s mislukt: %2$s"
msgstr ""
#: assets/js/term-bulk.js:159
msgid "%1$s gevuld."
msgstr ""
#: assets/js/term-bulk.js:161
msgid "%s is bijgewerkt."
msgstr ""
#: assets/js/term-bulk.js:236
msgid "Stoppen?"
msgstr ""
#: assets/js/term-bulk.js:254
msgid "Onbekende term."
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:101
msgid "Onbekende fout"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:102
msgid "Onbekende term."
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:103
msgid "Stoppen?"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:104
msgid "%1$s: %2$s"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:105
msgid "%1$s gevuld."
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:106
msgid "%1$s mislukt: %2$s"
msgstr ""
#: includes/Admin/class-groq-ai-term-admin-base.php:107
msgid "%s is bijgewerkt."
msgstr ""

7
manifest.json Normal file
View File

@@ -0,0 +1,7 @@
{
"plugin_name": "SitiAI Product Teksten",
"description": "Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.",
"version": "1.10.0",
"author": "Roberto Guagliardo | SitiWeb",
"author_url": "https://sitiweb.nl/"
}

13
phpunit.xml Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="tests/bootstrap.php"
colors="true"
failOnWarning="false"
failOnRisky="false"
>
<testsuites>
<testsuite name="Plugin Tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,17 @@
<?php
use PHPUnit\Framework\TestCase;
class ModelExclusionsTest extends TestCase {
public function test_ensure_allowed_blocks_excluded_model() {
$blocked = Groq_AI_Model_Exclusions::ensure_allowed( 'groq', 'whisper-large-v3' );
$this->assertSame( '', $blocked );
}
public function test_filter_models_removes_excluded_entries() {
$models = [ 'llama3-70b-8192', 'whisper-large-v3', 'mixtral-8x7b-32768' ];
$filtered = Groq_AI_Model_Exclusions::filter_models( 'groq', $models );
$this->assertSame( [ 'llama3-70b-8192', 'mixtral-8x7b-32768' ], $filtered );
}
}

View File

@@ -0,0 +1,102 @@
<?php
use PHPUnit\Framework\TestCase;
class ProviderRequestBuilderTest extends TestCase {
public function test_openai_request_payload_respects_settings() {
$provider = new Groq_AI_Provider_OpenAI();
$result = $provider->generate_content(
[
'prompt' => 'Hallo',
'system_prompt' => 'System',
'model' => 'gpt-4o-mini',
'settings' => [
'openai_api_key' => 'test-key',
'max_output_tokens' => 512,
],
'temperature' => 0.5,
'response_format' => [
'type' => 'json_object',
],
]
);
$this->assertIsArray( $result );
$payload = $result['request_payload']['body'];
$this->assertSame( 'gpt-4o-mini', $payload['model'] );
$this->assertSame( 0.5, $payload['temperature'] );
$this->assertSame( 512, $payload['max_tokens'] );
$this->assertSame( 'json_object', $payload['response_format']['type'] );
$this->assertSame( 'System', $payload['messages'][0]['content'] );
$this->assertSame( 'Hallo', $payload['messages'][1]['content'] );
}
public function test_groq_request_payload_uses_default_model_when_missing() {
$provider = new Groq_AI_Provider_Groq();
$result = $provider->generate_content(
[
'prompt' => 'Hallo',
'system_prompt' => 'System',
'settings' => [
'groq_api_key' => 'test-key',
],
]
);
$this->assertIsArray( $result );
$payload = $result['request_payload']['body'];
$this->assertSame( $provider->get_default_model(), $payload['model'] );
$this->assertSame( 'System', $payload['messages'][0]['content'] );
$this->assertSame( 'Hallo', $payload['messages'][1]['content'] );
}
public function test_google_request_payload_builds_schema_and_images() {
$provider = new Groq_AI_Provider_Google();
$result = $provider->generate_content(
[
'prompt' => 'Hallo',
'system_prompt' => 'System',
'model' => 'gemini-1.5-flash',
'settings' => [
'google_api_key' => 'test-key',
'max_output_tokens' => 256,
'google_safety_settings' => [
'HARM_CATEGORY_HARASSMENT' => 'BLOCK_LOW_AND_ABOVE',
],
],
'temperature' => 0.2,
'response_format' => [
'type' => 'json_schema',
'json_schema' => [
'schema' => [
'type' => 'object',
'properties' => [
'name' => [
'type' => 'string',
],
],
],
],
],
'image_context' => [
[
'label' => 'Image 1',
'mime_type' => 'image/png',
'data' => 'BASE64DATA',
],
],
]
);
$this->assertIsArray( $result );
$payload = $result['request_payload']['body'];
$this->assertSame( 0.2, $payload['generationConfig']['temperature'] );
$this->assertSame( 256, $payload['generationConfig']['maxOutputTokens'] );
$this->assertSame( 'application/json', $payload['generationConfig']['responseMimeType'] );
$this->assertArrayHasKey( 'responseJsonSchema', $payload['generationConfig'] );
$this->assertSame( 'System', $payload['contents'][0]['parts'][0]['text'] );
$this->assertSame( 'Hallo', $payload['contents'][0]['parts'][1]['text'] );
$this->assertSame( 'image/png', $payload['contents'][0]['parts'][3]['inline_data']['mime_type'] );
$this->assertSame( 'BASE64DATA', $payload['contents'][0]['parts'][3]['inline_data']['data'] );
}
}

View File

@@ -0,0 +1,112 @@
<?php
use PHPUnit\Framework\TestCase;
class SettingsManagerTest extends TestCase {
private function make_manager() {
$provider_manager = new Groq_AI_Provider_Manager();
return new Groq_AI_Settings_Manager( 'groq_ai_test_settings', $provider_manager );
}
public function test_logs_retention_days_sanitized_and_capped() {
$manager = $this->make_manager();
$result = $manager->sanitize( [
'logs_retention_days' => 5000,
] );
$this->assertSame( 3650, $result['logs_retention_days'] );
}
public function test_logs_retention_days_allows_zero() {
$manager = $this->make_manager();
$result = $manager->sanitize( [
'logs_retention_days' => 0,
] );
$this->assertSame( 0, $result['logs_retention_days'] );
}
public function test_logs_retention_days_negative_becomes_zero() {
$manager = $this->make_manager();
$result = $manager->sanitize( [
'logs_retention_days' => -5,
] );
$this->assertSame( 0, $result['logs_retention_days'] );
}
public function test_sanitize_accepts_all_settings_keys() {
$manager = $this->make_manager();
$context_fields = $manager->get_default_context_fields();
$modules = $manager->get_default_modules_settings();
$google_categories = Groq_AI_Settings_Manager::get_google_safety_categories_list();
$first_category = array_key_first( $google_categories );
$input = [
'provider' => 'openai',
'model' => 'gpt-4o-mini',
'store_context' => 'Test winkelcontext',
'default_prompt' => 'Schrijf een korte tekst',
'max_output_tokens' => 2048,
'logs_retention_days' => 30,
'product_attribute_includes' => [ '__all__', '__custom__', 'pa_color', 'invalid key' ],
'term_bottom_description_meta_key' => 'custom_bottom_key',
'groq_api_key' => 'groq-key',
'openai_api_key' => 'openai-key',
'google_api_key' => 'google-key',
'google_oauth_client_id' => 'client-id',
'google_oauth_client_secret' => 'client-secret',
'google_oauth_refresh_token' => 'refresh-token',
'google_oauth_connected_email' => 'user@example.com',
'google_oauth_connected_at' => 123456,
'google_enable_gsc' => true,
'google_enable_ga' => false,
'google_gsc_site_url' => 'https://example.com/',
'google_ga4_property_id' => '123456',
'google_safety_settings' => $first_category ? [ $first_category => 'BLOCK_LOW_AND_ABOVE' ] : [],
'context_fields' => $context_fields,
'modules' => $modules,
'image_context_mode' => 'base64',
'image_context_limit' => 5,
'response_format_compat' => true,
'term_top_description_char_limit' => 700,
'term_bottom_description_char_limit' => 1400,
];
$result = $manager->sanitize( $input );
$this->assertSame( 'openai', $result['provider'] );
$this->assertSame( 'gpt-4o-mini', $result['model'] );
$this->assertSame( 'Test winkelcontext', $result['store_context'] );
$this->assertSame( 'Schrijf een korte tekst', $result['default_prompt'] );
$this->assertSame( 2048, $result['max_output_tokens'] );
$this->assertSame( 30, $result['logs_retention_days'] );
$this->assertContains( '__all__', $result['product_attribute_includes'] );
$this->assertContains( '__custom__', $result['product_attribute_includes'] );
$this->assertContains( 'pa_color', $result['product_attribute_includes'] );
$this->assertSame( 'custom_bottom_key', $result['term_bottom_description_meta_key'] );
$this->assertSame( 'groq-key', $result['groq_api_key'] );
$this->assertSame( 'openai-key', $result['openai_api_key'] );
$this->assertSame( 'google-key', $result['google_api_key'] );
$this->assertSame( 'client-id', $result['google_oauth_client_id'] );
$this->assertSame( 'client-secret', $result['google_oauth_client_secret'] );
$this->assertSame( 'refresh-token', $result['google_oauth_refresh_token'] );
$this->assertSame( 'user@example.com', $result['google_oauth_connected_email'] );
$this->assertSame( 123456, $result['google_oauth_connected_at'] );
$this->assertTrue( $result['google_enable_gsc'] );
$this->assertFalse( $result['google_enable_ga'] );
$this->assertSame( 'https://example.com/', $result['google_gsc_site_url'] );
$this->assertSame( '123456', $result['google_ga4_property_id'] );
$this->assertIsArray( $result['google_safety_settings'] );
$this->assertIsArray( $result['context_fields'] );
$this->assertIsArray( $result['modules'] );
$this->assertSame( 'base64', $result['image_context_mode'] );
$this->assertSame( 5, $result['image_context_limit'] );
$this->assertTrue( $result['response_format_compat'] );
$this->assertSame( 700, $result['term_top_description_char_limit'] );
$this->assertSame( 1400, $result['term_bottom_description_char_limit'] );
}
}

67
tests/TermSaveTest.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
use PHPUnit\Framework\TestCase;
class TermSaveTest extends TestCase {
protected function setUp(): void {
$GLOBALS['wp_term_updates'] = [];
$GLOBALS['wp_term_meta_updates'] = [];
$GLOBALS['wp_filters'] = [];
}
public function test_save_term_generation_result_saves_descriptions_and_filtered_meta_key() {
$plugin = new class {
public function get_settings() {
return [ 'term_bottom_description_meta_key' => '' ];
}
public function is_module_enabled( $module, $settings = null ) {
return false;
}
};
$controller_ref = new ReflectionClass( Groq_AI_Ajax_Controller::class );
$controller = $controller_ref->newInstanceWithoutConstructor();
$plugin_prop = $controller_ref->getProperty( 'plugin' );
$plugin_prop->setAccessible( true );
$plugin_prop->setValue( $controller, $plugin );
add_filter(
'groq_ai_term_bottom_description_meta_key',
function ( $default_key ) {
return 'Custom Key';
},
10,
3
);
$term = (object) [
'term_id' => 12,
'taxonomy' => 'product_cat',
'name' => 'Test',
'description' => '',
];
$result = [
'top_description' => '<p>Dit is een test.</p>',
'bottom_description' => '<p>Onderste tekst.</p>',
];
$settings = $plugin->get_settings();
$method = $controller_ref->getMethod( 'save_term_generation_result' );
$method->setAccessible( true );
$saved = $method->invoke( $controller, $term, $result, $settings );
$this->assertIsArray( $saved );
$this->assertSame( 4, $saved['words'] );
$this->assertCount( 1, $GLOBALS['wp_term_updates'] );
$this->assertSame( 12, $GLOBALS['wp_term_updates'][0]['term_id'] );
$this->assertSame( 'product_cat', $GLOBALS['wp_term_updates'][0]['taxonomy'] );
$this->assertSame( '<p>Dit is een test.</p>', $GLOBALS['wp_term_updates'][0]['args']['description'] );
$this->assertArrayHasKey( 12, $GLOBALS['wp_term_meta_updates'] );
$this->assertArrayHasKey( 'customkey', $GLOBALS['wp_term_meta_updates'][12] );
$this->assertSame( '<p>Onderste tekst.</p>', $GLOBALS['wp_term_meta_updates'][12]['customkey'] );
}
}

258
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,258 @@
<?php
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_DOMAIN' ) ) {
define( 'GROQ_AI_PRODUCT_TEXT_DOMAIN', 'siti-ai-product-content-generator' );
}
if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
define( 'HOUR_IN_SECONDS', 3600 );
}
if ( ! defined( 'DAY_IN_SECONDS' ) ) {
define( 'DAY_IN_SECONDS', 86400 );
}
if ( ! class_exists( 'WP_Error' ) ) {
class WP_Error {
private $message;
public function __construct( $code = '', $message = '' ) {
$this->message = (string) $message;
}
public function get_error_message() {
return $this->message;
}
}
}
if ( ! function_exists( 'is_wp_error' ) ) {
function is_wp_error( $thing ) {
return $thing instanceof WP_Error;
}
}
if ( ! function_exists( '__' ) ) {
function __( $text ) {
return $text;
}
}
if ( ! function_exists( 'wp_parse_args' ) ) {
function wp_parse_args( $args, $defaults = [] ) {
if ( is_object( $args ) ) {
$args = get_object_vars( $args );
}
if ( ! is_array( $args ) ) {
$args = [];
}
return array_merge( $defaults, $args );
}
}
if ( ! function_exists( 'sanitize_text_field' ) ) {
function sanitize_text_field( $text ) {
return trim( (string) $text );
}
}
if ( ! function_exists( 'sanitize_textarea_field' ) ) {
function sanitize_textarea_field( $text ) {
return trim( (string) $text );
}
}
if ( ! function_exists( 'sanitize_key' ) ) {
function sanitize_key( $key ) {
$key = strtolower( (string) $key );
return preg_replace( '/[^a-z0-9_\-]/', '', $key );
}
}
if ( ! function_exists( 'absint' ) ) {
function absint( $number ) {
return abs( (int) $number );
}
}
if ( ! function_exists( 'esc_url_raw' ) ) {
function esc_url_raw( $url ) {
return (string) $url;
}
}
if ( ! function_exists( 'add_filter' ) ) {
function add_filter( $tag, $callback, $priority = 10, $accepted_args = 1 ) {
$GLOBALS['wp_filters'][ $tag ][ $priority ][] = [
'callback' => $callback,
'accepted_args' => (int) $accepted_args,
];
}
}
if ( ! function_exists( 'apply_filters' ) ) {
function apply_filters( $tag, $value ) {
$args = func_get_args();
if ( empty( $GLOBALS['wp_filters'][ $tag ] ) ) {
return $value;
}
ksort( $GLOBALS['wp_filters'][ $tag ] );
foreach ( $GLOBALS['wp_filters'][ $tag ] as $callbacks ) {
foreach ( $callbacks as $filter ) {
$accepted = isset( $filter['accepted_args'] ) ? (int) $filter['accepted_args'] : 1;
$call_args = array_slice( $args, 0, max( 1, $accepted ) );
$call_args[0] = $value;
$value = call_user_func_array( $filter['callback'], $call_args );
$args[0] = $value;
}
}
return $value;
}
}
if ( ! function_exists( 'wp_kses_post' ) ) {
function wp_kses_post( $content ) {
return (string) $content;
}
}
if ( ! function_exists( 'wp_strip_all_tags' ) ) {
function wp_strip_all_tags( $text ) {
return strip_tags( (string) $text );
}
}
if ( ! function_exists( 'wp_update_term' ) ) {
function wp_update_term( $term_id, $taxonomy, $args = [] ) {
$GLOBALS['wp_term_updates'][] = [
'term_id' => (int) $term_id,
'taxonomy' => (string) $taxonomy,
'args' => $args,
];
return [ 'term_id' => (int) $term_id ];
}
}
if ( ! function_exists( 'update_term_meta' ) ) {
function update_term_meta( $term_id, $meta_key, $meta_value ) {
$term_id = (int) $term_id;
if ( ! isset( $GLOBALS['wp_term_meta_updates'][ $term_id ] ) ) {
$GLOBALS['wp_term_meta_updates'][ $term_id ] = [];
}
$GLOBALS['wp_term_meta_updates'][ $term_id ][ (string) $meta_key ] = $meta_value;
return true;
}
}
if ( ! function_exists( 'wp_json_encode' ) ) {
function wp_json_encode( $data, $options = 0, $depth = 512 ) {
return json_encode( $data, $options, $depth );
}
}
if ( ! function_exists( 'add_query_arg' ) ) {
function add_query_arg( $args, $url = '' ) {
if ( is_string( $args ) ) {
return $url;
}
$query = http_build_query( (array) $args );
$separator = strpos( $url, '?' ) === false ? '?' : '&';
return $url . $separator . $query;
}
}
if ( ! function_exists( 'wp_remote_post' ) ) {
function wp_remote_post( $url, $args = [] ) {
$GLOBALS['wp_last_http_request'] = [
'url' => $url,
'args' => $args,
];
$body = json_encode(
[
'choices' => [
[
'message' => [
'content' => 'ok',
],
'finish_reason' => 'stop',
],
],
'usage' => [
'prompt_tokens' => 10,
'completion_tokens' => 20,
'total_tokens' => 30,
],
'candidates' => [
[
'content' => [
'parts' => [
[ 'text' => 'ok' ],
],
],
'finishReason' => 'STOP',
],
],
'usageMetadata' => [
'promptTokenCount' => 10,
'candidatesTokenCount' => 20,
'totalTokenCount' => 30,
],
]
);
return [
'body' => $body,
'response' => [ 'code' => 200 ],
];
}
}
if ( ! function_exists( 'wp_remote_get' ) ) {
function wp_remote_get( $url, $args = [] ) {
$GLOBALS['wp_last_http_request'] = [
'url' => $url,
'args' => $args,
];
return [
'body' => json_encode( [ 'data' => [], 'models' => [] ] ),
'response' => [ 'code' => 200 ],
];
}
}
if ( ! function_exists( 'wp_remote_retrieve_body' ) ) {
function wp_remote_retrieve_body( $response ) {
return isset( $response['body'] ) ? $response['body'] : '';
}
}
if ( ! function_exists( 'wp_remote_retrieve_response_code' ) ) {
function wp_remote_retrieve_response_code( $response ) {
return isset( $response['response']['code'] ) ? (int) $response['response']['code'] : 0;
}
}
if ( ! function_exists( 'get_option' ) ) {
function get_option( $key, $default = false ) {
return isset( $GLOBALS['wp_options'][ $key ] ) ? $GLOBALS['wp_options'][ $key ] : $default;
}
}
if ( ! function_exists( 'update_option' ) ) {
function update_option( $key, $value ) {
$GLOBALS['wp_options'][ $key ] = $value;
return true;
}
}
require_once __DIR__ . '/../includes/Core/class-groq-ai-model-exclusions.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/Core/class-groq-ai-ajax-controller.php';