Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71424567bf | |||
| 82da1180da | |||
| 2c749eebd5 | |||
|
|
04a0c8be37 | ||
| 03727b9828 | |||
|
|
987139bca7 | ||
| 773db7407c | |||
|
|
a4da6e3e00 | ||
| ec08c79aab | |||
| 4a789363c5 | |||
|
|
c279ea7247 | ||
| 1e2214dd74 | |||
| 70001df941 | |||
|
|
072e2d109f | ||
| 1015627822 | |||
| 8f1fc80835 | |||
| b73d858ca7 | |||
| aa4bfa33c0 | |||
| 0b9cd99fb0 | |||
| eba8df9962 | |||
| f2a39e4660 | |||
| 2f0a44706b | |||
| 10a46f8668 | |||
| db981ba4a6 | |||
|
|
aa757710c9 | ||
| 1d587ce2d1 | |||
| 0aaa7087a9 | |||
|
|
68bce5006d | ||
| 4383982fb2 | |||
|
|
5f1dcaf352 | ||
| 62c688dba4 | |||
|
|
42332635ba | ||
| 9ae516b77c | |||
| 4ff96d98e0 | |||
| 6cff0b6f58 | |||
| 26aabdb2d8 | |||
| 3e74bcbf3a | |||
| 051db0febc |
151
.github/workflows/release.yml
vendored
151
.github/workflows/release.yml
vendored
@@ -26,6 +26,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
- name: Determine plugin version
|
- name: Determine plugin version
|
||||||
id: meta
|
id: meta
|
||||||
@@ -38,11 +39,54 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Genereer manifest.json uit plugin header
|
||||||
|
run: |
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
with open('groq-ai-product-text.php', 'r', encoding='utf-8') as handle:
|
||||||
|
content = handle.read()
|
||||||
|
|
||||||
|
header_match = re.search(r'/\*\*(.*?)\*/', content, re.S)
|
||||||
|
header = header_match.group(1) if header_match else ''
|
||||||
|
|
||||||
|
def read_field(label: str) -> str:
|
||||||
|
match = re.search(r'^\s*\*\s*' + re.escape(label) + r'\s*:\s*(.+)$', header, re.M)
|
||||||
|
return match.group(1).strip() if match else ''
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
'plugin_name': read_field('Plugin Name'),
|
||||||
|
'description': read_field('Description'),
|
||||||
|
'version': read_field('Version'),
|
||||||
|
'author': read_field('Author'),
|
||||||
|
'author_url': read_field('Author URI'),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open('manifest.json', 'w', encoding='utf-8') as handle:
|
||||||
|
json.dump(manifest, handle, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print('manifest.json aangemaakt/bijgewerkt')
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Commit & push manifest.json
|
||||||
|
run: |
|
||||||
|
if git diff --quiet -- manifest.json; then
|
||||||
|
echo "manifest.json ongewijzigd; geen commit nodig."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add manifest.json
|
||||||
|
git commit -m "chore: update manifest.json"
|
||||||
|
git push
|
||||||
|
|
||||||
- name: Check if tag exists
|
- name: Check if tag exists
|
||||||
id: tagcheck
|
id: tagcheck
|
||||||
run: |
|
run: |
|
||||||
TAG="v${{ steps.meta.outputs.version }}"
|
TAG="v${{ steps.meta.outputs.version }}"
|
||||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "refs/tags/$TAG$"; then
|
||||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||||
@@ -57,20 +101,26 @@ jobs:
|
|||||||
if: steps.tagcheck.outputs.exists == 'false'
|
if: steps.tagcheck.outputs.exists == 'false'
|
||||||
id: package
|
id: package
|
||||||
run: |
|
run: |
|
||||||
|
if ! command -v zip >/dev/null 2>&1; then
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y zip
|
||||||
|
fi
|
||||||
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
SLUG="siti-ai-product-content-generator"
|
SLUG="siti-ai-product-content-generator"
|
||||||
BUILD_ROOT="$RUNNER_TEMP/build"
|
BUILD_ROOT="$RUNNER_TEMP/build"
|
||||||
DEST_DIR="$BUILD_ROOT/$SLUG"
|
DEST_DIR="$BUILD_ROOT/$SLUG"
|
||||||
mkdir -p "$DEST_DIR"
|
mkdir -p "$DEST_DIR"
|
||||||
|
|
||||||
rsync -a ./ "$DEST_DIR" \
|
tar -cf - \
|
||||||
--exclude '.git/' \
|
--exclude='.git' \
|
||||||
--exclude '.github/' \
|
--exclude='.github' \
|
||||||
--exclude 'docker/' \
|
--exclude='docker' \
|
||||||
--exclude 'docs/' \
|
--exclude='docs' \
|
||||||
--exclude 'dist/' \
|
--exclude='dist' \
|
||||||
--exclude 'docker-compose.yml' \
|
--exclude='docker-compose.yml' \
|
||||||
--exclude 'PLAN.md'
|
--exclude='PLAN.md' \
|
||||||
|
. | tar -xf - -C "$DEST_DIR"
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
ZIP_PATH="dist/${SLUG}-${VERSION}.zip"
|
ZIP_PATH="dist/${SLUG}-${VERSION}.zip"
|
||||||
@@ -79,24 +129,71 @@ jobs:
|
|||||||
echo "asset_path=$ZIP_PATH" >> "$GITHUB_OUTPUT"
|
echo "asset_path=$ZIP_PATH" >> "$GITHUB_OUTPUT"
|
||||||
echo "asset_name=${SLUG}-${VERSION}.zip" >> "$GITHUB_OUTPUT"
|
echo "asset_name=${SLUG}-${VERSION}.zip" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Stel release-body samen
|
- name: Maak Gitea release
|
||||||
if: steps.tagcheck.outputs.exists == 'false'
|
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:
|
env:
|
||||||
RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
RELEASE_SERVER_URL: ${{ vars.RELEASE_SERVER_URL }}
|
||||||
|
RELEASE_REPOSITORY: ${{ vars.RELEASE_REPOSITORY }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="v$VERSION"
|
||||||
|
ASSET_PATH="${{ steps.package.outputs.asset_path }}"
|
||||||
|
ASSET_NAME="${{ steps.package.outputs.asset_name }}"
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_TOKEN" ]; then
|
||||||
|
echo "::error::RELEASE_TOKEN ontbreekt. Voeg deze secret toe om releases te kunnen maken."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVER_URL="${RELEASE_SERVER_URL:-${GITHUB_SERVER_URL}}"
|
||||||
|
REPO="${RELEASE_REPOSITORY:-${GITHUB_REPOSITORY}}"
|
||||||
|
if [ -z "$SERVER_URL" ] || [ -z "$REPO" ]; then
|
||||||
|
echo "::error::Kan server of repository niet bepalen. Stel RELEASE_SERVER_URL en RELEASE_REPOSITORY in."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
API_URL="${SERVER_URL%/}/api/v1"
|
||||||
|
|
||||||
|
export TAG
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"tag_name": os.environ["TAG"],
|
||||||
|
"name": f"Siti AI Product Teksten {os.environ['TAG']}",
|
||||||
|
"body": f"Automatische release op basis van versie {os.environ['TAG'][1:]}",
|
||||||
|
"target_commitish": os.environ.get("GITHUB_SHA") or os.environ.get("GITEA_SHA", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open("release.json", "w", encoding="utf-8") as handle:
|
||||||
|
json.dump(payload, handle, ensure_ascii=False)
|
||||||
|
PY
|
||||||
|
|
||||||
|
curl -sS -X POST "$API_URL/repos/$REPO/releases" \
|
||||||
|
-H "Authorization: token $RELEASE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @release.json \
|
||||||
|
-o release-response.json
|
||||||
|
|
||||||
|
RELEASE_ID=$(python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
|
||||||
|
with open('release-response.json', 'r', encoding='utf-8') as handle:
|
||||||
|
data = json.load(handle)
|
||||||
|
|
||||||
|
print(data.get('id', ''))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "::error::Kon release-ID niet bepalen. Response: $(cat release-response.json)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -sS -X POST "$API_URL/repos/$REPO/releases/$RELEASE_ID/assets?name=$ASSET_NAME" \
|
||||||
|
-H "Authorization: token $RELEASE_TOKEN" \
|
||||||
|
-H "Content-Type: application/zip" \
|
||||||
|
--data-binary "@$ASSET_PATH"
|
||||||
|
|
||||||
- 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
1
.phpunit.result.cache
Normal 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
49
LICENSE_INTEGRATION.md
Normal 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.
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
assets/images/plugin-icon.svg
Normal file
11
assets/images/plugin-icon.svg
Normal 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 |
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
16
composer.json
Normal 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
1815
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.9.7
|
||||||
* 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 ) {
|
||||||
return $this->container->get( 'provider_manager' );
|
switch ( $name ) {
|
||||||
}
|
case 'get_provider_manager':
|
||||||
|
return $this->container->get( 'provider_manager' );
|
||||||
public function get_settings_manager() {
|
case 'get_settings_manager':
|
||||||
return $this->container->get( 'settings_manager' );
|
return $this->container->get( 'settings_manager' );
|
||||||
}
|
case 'get_prompt_builder':
|
||||||
|
return $this->container->get( 'prompt_builder' );
|
||||||
public function get_prompt_builder() {
|
case 'get_conversation_manager':
|
||||||
return $this->container->get( 'prompt_builder' );
|
return $this->container->get( 'conversation_manager' );
|
||||||
}
|
case 'get_generation_logger':
|
||||||
|
return $this->container->get( 'generation_logger' );
|
||||||
public function get_conversation_manager() {
|
case 'get_settings':
|
||||||
return $this->container->get( 'conversation_manager' );
|
return $this->container->get( 'settings_manager' )->all();
|
||||||
}
|
case 'sanitize_settings':
|
||||||
|
return $this->container->get( 'settings_manager' )->sanitize( $arguments[0] ?? [] );
|
||||||
public function get_generation_logger() {
|
case 'get_context_field_definitions':
|
||||||
return $this->container->get( 'generation_logger' );
|
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();
|
||||||
public function get_settings() {
|
case 'get_default_context_fields':
|
||||||
return $this->get_settings_manager()->all();
|
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] ?? [] );
|
||||||
public function sanitize_settings( $input ) {
|
case 'get_module_config':
|
||||||
return $this->get_settings_manager()->sanitize( $input );
|
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 );
|
||||||
public function maybe_deactivate_if_woocommerce_missing() {
|
case 'get_rankmath_focus_keyword_limit':
|
||||||
if ( $this->is_woocommerce_active() ) {
|
return $this->container->get( 'settings_manager' )->get_rankmath_focus_keyword_limit( $arguments[0] ?? null );
|
||||||
return;
|
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] ?? [] );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! function_exists( 'deactivate_plugins' ) ) {
|
throw new BadMethodCallException( sprintf( 'Method %s does not exist.', $name ) );
|
||||||
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();
|
||||||
|
|||||||
46
includes/Admin/class-groq-ai-admin-base.php
Normal file
46
includes/Admin/class-groq-ai-admin-base.php
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
includes/Admin/class-groq-ai-brands-admin.php
Normal file
177
includes/Admin/class-groq-ai-brands-admin.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
includes/Admin/class-groq-ai-categories-admin.php
Normal file
119
includes/Admin/class-groq-ai-categories-admin.php
Normal 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 ),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
includes/Admin/class-groq-ai-logs-admin.php
Normal file
180
includes/Admin/class-groq-ai-logs-admin.php
Normal 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">← <?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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
262
includes/Admin/class-groq-ai-settings-renderer.php
Normal file
262
includes/Admin/class-groq-ai-settings-renderer.php
Normal 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 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
538
includes/Admin/class-groq-ai-term-admin-base.php
Normal file
538
includes/Admin/class-groq-ai-term-admin-base.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,21 +230,28 @@ 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(
|
||||||
[
|
[
|
||||||
'provider' => $provider_key,
|
'provider' => $provider_key,
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
'prompt' => $final_prompt,
|
'prompt' => $final_prompt,
|
||||||
'response' => '',
|
'response' => '',
|
||||||
'usage' => $usage_meta,
|
'usage' => $usage_meta,
|
||||||
'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,
|
||||||
);
|
]
|
||||||
}
|
);
|
||||||
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,20 +267,21 @@ class Groq_AI_Ajax_Controller {
|
|||||||
$parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings );
|
$parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings );
|
||||||
}
|
}
|
||||||
if ( is_wp_error( $parsed ) ) {
|
if ( is_wp_error( $parsed ) ) {
|
||||||
if ( $logger ) {
|
if ( $logger ) {
|
||||||
$logger->log_generation_event(
|
$logger->log_generation_event(
|
||||||
[
|
[
|
||||||
'provider' => $provider_key,
|
'provider' => $provider_key,
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
'prompt' => $final_prompt,
|
'prompt' => $final_prompt,
|
||||||
'response' => $response_text,
|
'response' => $response_text,
|
||||||
'usage' => $response_usage,
|
'usage' => $response_usage,
|
||||||
'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,
|
||||||
);
|
]
|
||||||
}
|
);
|
||||||
|
}
|
||||||
return $parsed;
|
return $parsed;
|
||||||
}
|
}
|
||||||
if ( ! is_array( $parsed ) ) {
|
if ( ! is_array( $parsed ) ) {
|
||||||
@@ -263,19 +290,20 @@ class Groq_AI_Ajax_Controller {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $logger ) {
|
if ( $logger ) {
|
||||||
$logger->log_generation_event(
|
$logger->log_generation_event(
|
||||||
[
|
[
|
||||||
'provider' => $provider_key,
|
'provider' => $provider_key,
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
'prompt' => $final_prompt,
|
'prompt' => $final_prompt,
|
||||||
'response' => $response_text,
|
'response' => $response_text,
|
||||||
'usage' => $response_usage,
|
'usage' => $response_usage,
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'post_id' => 0,
|
'post_id' => 0,
|
||||||
]
|
'parameters' => $logged_parameters,
|
||||||
);
|
]
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'top_description' => isset( $parsed['top_description'] ) ? $parsed['top_description'] : ( isset( $parsed['description'] ) ? $parsed['description'] : '' ),
|
'top_description' => isset( $parsed['top_description'] ) ? $parsed['top_description'] : ( isset( $parsed['description'] ) ? $parsed['description'] : '' ),
|
||||||
@@ -407,9 +435,22 @@ class Groq_AI_Ajax_Controller {
|
|||||||
|
|
||||||
check_ajax_referer( 'groq_ai_generate', 'nonce' );
|
check_ajax_referer( 'groq_ai_generate', 'nonce' );
|
||||||
|
|
||||||
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
includes/Core/class-groq-ai-compatibility-service.php
Normal file
58
includes/Core/class-groq-ai-compatibility-service.php
Normal 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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
28
includes/Core/class-groq-ai-log-scheduler.php
Normal file
28
includes/Core/class-groq-ai-log-scheduler.php
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
78
includes/Core/class-groq-ai-model-service.php
Normal file
78
includes/Core/class-groq-ai-model-service.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,16 +221,132 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
181
includes/SitiLicenseValidator.php
Normal file
181
includes/SitiLicenseValidator.php
Normal 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' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
463
includes/SitiWebUpdater2.php
Normal file
463
includes/SitiWebUpdater2.php
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
<?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 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'] = $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 );
|
||||||
|
$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->plugin_response['version'];
|
||||||
|
$current_version = $this->plugin['Version'];
|
||||||
|
|
||||||
|
if ( 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->plugin_response['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->plugin_response['version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$license_data = $this->get_license_data();
|
||||||
|
if ( isset( $license_data['pluginVersion'] ) && $license_data['pluginVersion'] ) {
|
||||||
|
return $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
|
||||||
|
}
|
||||||
|
}
|
||||||
202
languages/siti-ai-product-content-generator.pot
Normal file
202
languages/siti-ai-product-content-generator.pot
Normal 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
7
manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"plugin_name": "SitiAI Product Teksten",
|
||||||
|
"description": "Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.",
|
||||||
|
"version": "1.9.6",
|
||||||
|
"author": "Roberto Guagliardo | SitiWeb",
|
||||||
|
"author_url": "https://sitiweb.nl/"
|
||||||
|
}
|
||||||
13
phpunit.xml
Normal file
13
phpunit.xml
Normal 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>
|
||||||
17
tests/ModelExclusionsTest.php
Normal file
17
tests/ModelExclusionsTest.php
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
102
tests/ProviderRequestBuilderTest.php
Normal file
102
tests/ProviderRequestBuilderTest.php
Normal 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'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
112
tests/SettingsManagerTest.php
Normal file
112
tests/SettingsManagerTest.php
Normal 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
67
tests/TermSaveTest.php
Normal 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
258
tests/bootstrap.php
Normal 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';
|
||||||
Reference in New Issue
Block a user