Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ddd3f8104 | |||
| 9df41ca85c | |||
| 934cbf0f73 | |||
| 5cc6e869bf | |||
| 79d411f35a | |||
| 58a9b37ccf | |||
| 5b256f1374 | |||
| d878bb7805 | |||
| 43ddbddd11 | |||
| 7b9f26e966 | |||
| 6f488c5c6d | |||
| 1c4ef5e16a | |||
| 1bb10f4b45 | |||
| 95f7983e70 | |||
| 985f7dfbcd | |||
| cf7ee6b86e | |||
| 1d19b36493 |
53
AGENTS.md
Normal file
53
AGENTS.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# AGENTS – Repo handleiding
|
||||
|
||||
## Context & scope
|
||||
- Deze repository bevat de volledige WordPress-plugin **SitiAI Product Teksten** inclusief assets, taalbestanden en Docker-configs. Alle pluginbestanden leven hier (geen submodules).
|
||||
- Doel: AI-gestuurde content genereren voor WooCommerce-producten en -termen met ondersteuning voor Groq, OpenAI en Google Gemini plus Rank Math & Google integraties.
|
||||
- Belangrijkste entrypoints: `groq-ai-product-text.php` (bootstrap), `includes/` (services, admin UI, providers) en `assets/` (admin CSS/JS).
|
||||
|
||||
## Lokale ontwikkelworkflow
|
||||
1. Vereisten: Docker Desktop/Engine met Compose v2. Verdere tooling (npm, composer) is niet nodig; assets staan reeds gecompileerd.
|
||||
2. Start omgeving:
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
- WordPress: http://localhost:8082
|
||||
- phpMyAdmin: http://localhost:8085
|
||||
- MariaDB poort 3307 (db/user/pass = `wordpress`)
|
||||
3. WordPress installatie afronden via de browser, WooCommerce + deze plugin activeren.
|
||||
4. Handige commando’s:
|
||||
- `docker compose exec wordpress bash`
|
||||
- `docker compose exec wordpress wp plugin list`
|
||||
- `docker compose logs -f wordpress`
|
||||
- Stoppen/herinitialiseren: `docker compose down` / `docker compose down -v`
|
||||
5. Code staat buiten containers (bind mount). Gebruik git op de host (`git status`, `git commit`, …).
|
||||
|
||||
## Code style & patronen
|
||||
- Hanteer WordPress/PHP Coding Standards: tabs voor indent, `esc_html__`, `esc_attr__`, `wp_nonce_field`, etc. Tekstdomein = `siti-ai-product-content-generator`.
|
||||
- Alle adminstrings moeten vertaalbaar zijn via `__()`/`_e()`.
|
||||
- Integreer met bestaande services via `Groq_AI_Service_Container`; voeg nieuwe services via `$container->set()` in `groq-ai-product-text.php`.
|
||||
- Houd prompts/JSON-structuren consistent met `Groq_AI_Prompt_Builder`. Als je outputfields toevoegt, zorg ook voor updates in `get_structured_response_instructions()` en de UI (`assets/js/admin.js`).
|
||||
- AJAX-acties zitten in `Groq_AI_Ajax_Controller`; vervolgacties moeten capability checks, nonce-validatie en wp_send_json_* gebruiken.
|
||||
- Voor settings gebruik je altijd `Groq_AI_Settings_Manager` zodat defaults, sanitization en modules consistent blijven.
|
||||
- Houd rekening met de Rank Math module (optioneel) en Google OAuth flows (Search Console / GA clients). Voeg configuratie-opties toe via de bestaande adminpagina’s en filters.
|
||||
|
||||
## Testen & QA
|
||||
- Geen geautomatiseerde test-suite beschikbaar. Valideer wijzigingen door de Docker-WordPress te gebruiken.
|
||||
- PHP-lint: `docker compose exec wordpress php -l /var/www/html/wp-content/plugins/siti-ai-product-content-generator/<bestand>`.
|
||||
- Controleer AI-flows handmatig: productmodal, categorie/merk generator, bulk acties en AI-logboek.
|
||||
- Controleer database-migraties: logtabel `wp_groq_ai_generation_logs` wordt bij init aangemaakt. Gebruik WP-CLI of phpMyAdmin om schemawijzigingen te verifiëren.
|
||||
- Houd WooCommerce actief; de plugin deactiveert zichzelf als WooCommerce ontbreekt.
|
||||
|
||||
## Release & versiebeheer
|
||||
1. Pas `Version` (en eventueel `Stable tag`) aan in `groq-ai-product-text.php`.
|
||||
2. Commit veranderingen en push naar `main` of start handmatig de workflow **Build & Release Plugin** (GitHub Actions).
|
||||
3. Workflow bouwt zip, maakt tag `vX.Y.Z` en publiceert een release. Live sites krijgen updates via `SitiWebUpdater`.
|
||||
4. Bewaak backwards compatibility; logs en prompts worden opgeslagen in WordPress options/meta.
|
||||
|
||||
## Overige tips & valkuilen
|
||||
- `rg` is momenteel niet geïnstalleerd in deze omgeving; gebruik `grep`/`fd` voor zoekopdrachten.
|
||||
- Geef altijd capability-checks en nonce-validatie wanneer je nieuwe admin-acties toevoegt.
|
||||
- Filters ter beschikking: `groq_ai_brand_taxonomy`, `groq_ai_model_exclusions`, `groq_ai_term_google_context`, enz. Gebruik die in plaats van hardcodings.
|
||||
- Afbeeldingscontext kan `url`, `base64` of `none` zijn. Nieuwe providers moeten dit ondersteunen of duidelijk aangeven dat het unsupported is.
|
||||
- Denk aan caching (transients) zoals `Groq_AI_Google_Context_Builder` doet; intensieve API-calls moeten nooit in loops zonder caching draaien.
|
||||
- Logging (`Groq_AI_Generation_Logger`) is essentieel voor support. Als je nieuwe AI-calls toevoegt, log status, tokens en fouten daar.
|
||||
157
README.md
157
README.md
@@ -1,93 +1,110 @@
|
||||
# SitiAI Product Teksten (WordPress plugin)
|
||||
|
||||
Deze repository bevat de WordPress plugin waarmee productteksten via SitiAI kunnen worden gegenereerd. De plugincode leeft volledig in deze map en kan daarom veilig via git beheerd worden.
|
||||
SitiAI Product Teksten voegt een AI-gestuurde workflow toe aan WooCommerce zodat redacties product-, categorie- en merkteksten rechtstreeks binnen WordPress kunnen genereren. De plugin bundelt alle logica in deze repository (inclusief assets, taalbestanden en Docker-omgeving) en kan daardoor geheel via git beheerd en gedeployed worden.
|
||||
|
||||
## Plugin installeren en gebruiken
|
||||
## Functionaliteiten in vogelvlucht
|
||||
- **Multi-provider AI**: selecteer Groq, OpenAI of Google Gemini en laad live model-lijsten. `Groq_AI_Model_Exclusions` filtert ongeschikte modellen en elke provider declareert eigen endpoint, API-sleutel en resp. JSON capabilities.
|
||||
- **WooCommerce productmodal**: op de productbewerkscherm verschijnt de meta-box “Gebruik AI” met een modal waarin gebruikers prompts kunnen sturen, contextvelden (titel, beschrijvingen, attributen, merken, afbeeldingen) kunnen toggelen en resultaten per veld kunnen kopiëren of direct invullen.
|
||||
- **Categorie- en merkteksten**: uitgebreide beheerschermen voor `product_cat` en gedetecteerde merk-taxonomieën bevatten overzichten met woordtellingen, bulk-acties en een termgenerator die topverkopers, interne links en (optioneel) Google-data toevoegt. Output splitst in bovenste beschrijving, onderste beschrijving en – indien Rank Math actief – SEO-velden.
|
||||
- **Prompt builder & contextbeheer**: `Groq_AI_Prompt_Builder` bouwt system prompts op basis van winkelcontext, gefixeerde conversation ID’s en geselecteerde contextvelden. Productprompts eisen strikt JSON volgens `get_structured_response_instructions`; termprompts gebruiken desgewenst OpenAI/Groq response_format.
|
||||
- **Modules**: de Rank Math-module is standaard beschikbaar en bepaalt focuskeywordlimieten plus pixel-limieten voor meta title/description. Modules zijn uitbreidbaar via filters en krijgen een eigen instellingenpagina.
|
||||
- **Google-integratie**: OAuth 2.0 koppeling met Search Console en GA4 voegt queries, sessies en engaged sessions toe aan termcontext. Tokens worden ververst via `Groq_AI_Google_OAuth_Client` en resultaten gecachet (15 min).
|
||||
- **Logging & audits**: alle generaties worden opgeslagen in `wp_groq_ai_generation_logs` met prompt, response, status en token usage. Er zijn admin-schermen voor log-overzichten en detailpagina’s.
|
||||
- **Live updates**: `SitiWebUpdater` controleert GitHub releases (`SitiWeb/siti-ai-product-content-generator`) en verzorgt binnen WordPress één-klik updates inclusief re-activatie.
|
||||
|
||||
### Systeemeisen
|
||||
## Vereisten
|
||||
- WordPress 6.4+ en WooCommerce (de plugin deactiveert zichzelf zonder WooCommerce).
|
||||
- PHP 8.0+ (de Dockerfile gebruikt WordPress 6.9 op PHP 8.2).
|
||||
- Minstens één AI API-sleutel (Groq, OpenAI of Google Gemini). Je kunt sleutels voor meerdere providers opslaan en later wisselen.
|
||||
- Optioneel: Rank Math SEO (voor extra velden) en Google Cloud-project met Search Console & GA4 toegang voor OAuth.
|
||||
|
||||
- WordPress 6.4 of hoger.
|
||||
- WooCommerce (de plugin controleert dit en deactiveert zichzelf als WooCommerce ontbreekt).
|
||||
- Minimaal één API-sleutel voor Groq, OpenAI of Google Gemini.
|
||||
- (Optioneel) Rank Math SEO wanneer je de extra SEO-velden wilt gebruiken.
|
||||
|
||||
### Installatie
|
||||
|
||||
1. Download de nieuwste release (`siti-ai-product-content-generator-x.y.z.zip`) vanaf de [GitHub Releases](https://github.com/SitiWeb/siti-ai-product-content-generator/releases) of gebruik het zip-bestand dat door de workflow in `dist/` wordt geplaatst.
|
||||
2. Ga in WordPress naar **Plugins → Nieuwe plugin → Plugin uploaden** en upload het zipbestand. Je kunt de map ook handmatig naar `wp-content/plugins/` uploaden.
|
||||
## Installatie & activatie
|
||||
### Plugin installeren
|
||||
1. Download de laatste release (`siti-ai-product-content-generator-x.y.z.zip`) vanuit GitHub Releases of gebruik het zip-bestand dat door de workflow in `dist/` verschijnt.
|
||||
2. Upload via **Plugins → Nieuwe plugin → Plugin uploaden** of plaats de map onder `wp-content/plugins/`.
|
||||
3. Activeer **SitiAI Product Teksten** en controleer dat WooCommerce actief is.
|
||||
|
||||
### Configuratie
|
||||
|
||||
1. Navigeer naar **Instellingen → Siti AI**.
|
||||
2. Kies een AI-aanbieder, vul de bijbehorende API-sleutel in en (optioneel) klik op **Live modellen ophalen** om beschikbare modellen te laden.
|
||||
3. Stel een standaard prompt en winkelcontext in zodat het AI-venster vooraf gevuld is.
|
||||
4. Selecteer welke productvelden standaard als context dienen (titel, beschrijvingen, attributen, …).
|
||||
5. Gebruik de knop **Ga naar modules** om bijvoorbeeld de Rank Math integratie aan of uit te zetten en de limieten aan te passen.
|
||||
6. Via **Bekijk AI-logboek** zie je alle eerdere generaties inclusief foutmeldingen of token usage.
|
||||
### Basisconfiguratie
|
||||
1. Ga naar **Instellingen → Siti AI**.
|
||||
2. Kies een provider, stel het standaardmodel in (of laad live modellen via de knop), vul de bijbehorende API-sleutel in en kies optioneel andere aanbieders.
|
||||
3. Vul winkelcontext, standaardprompt, maximale outputtokens en gewenste contextvelden. Via **Prompt & context** beheer je defaults voor attributen, merkdetectie, beeldcontext (`none`, `url`, `base64`) en het maximale aantal afbeeldingen.
|
||||
4. Stel modules (Rank Math) in via **Instellingen → Siti AI → Modules** om keywordlimieten/pixellimieten te wijzigen en de module te activeren/deactiveren.
|
||||
5. (Optioneel) Koppel Google OAuth (client ID/secret + refresh token) en configureer Search Console site + GA4 property. Gebruik de ingebouwde verbindingstest om scopes te bevestigen.
|
||||
6. Gebruik op het tabblad **Prompt & context** de velden *Term omschrijving lengte* om de gewenste tekentaantallen voor de korte (top) en lange (bottom) categorie-/merktekst vast te leggen. De AI krijgt deze waardes met een marge van ±10%.
|
||||
|
||||
### Productteksten genereren
|
||||
1. Open een WooCommerce-product en klik in de meta-box op **Gebruik AI**.
|
||||
2. De modal toont de standaardprompt, contextselecties en (indien ingesteld) standaard attributen. Je kunt contextvelden tijdelijk toggelen zonder globale instellingen te wijzigen.
|
||||
3. Na **Genereer tekst** verschijnt output per veld: titel (inclusief drie suggesties), slug, korte beschrijving, beschrijving en – indien Rank Math module – meta title, meta description en focus keywords. Met de knoppen kun je de inhoud kopiëren of rechtstreeks invoegen in de corresponderende WordPress velden.
|
||||
4. Iedere call wordt gelogd (status, tokens, provider) en kan via het AI-logboek worden ingezien.
|
||||
5. Ajax-acties: `groq_ai_generate_text` verwerkt productprompts, `groq_ai_refresh_models` haalt provider-specifieke modellen op.
|
||||
|
||||
1. Open een product in WooCommerce en gebruik de meta-box **Gebruik AI** om de modal te openen.
|
||||
2. Vul (of hergebruik) een prompt, kies welke contextvelden meegestuurd worden en klik op **Genereer tekst**.
|
||||
3. De resultaten verschijnen per veld (titel, korte beschrijving, beschrijving en – indien geactiveerd – Rank Math velden). Gebruik **Kopieer** of **Vul … in** om velden direct over te nemen.
|
||||
4. Via de geavanceerde sectie kun je contextvelden tijdelijk uitschakelen; dit heeft alleen effect voor de huidige generatie.
|
||||
5. Iedere generatie wordt opgeslagen in het AI-logboek zodat je binnen WordPress kunt terugzoeken wat er is gebeurd.
|
||||
### Categorie- en merkteksten
|
||||
- Ga naar **Instellingen → Siti AI → Categorieën** of **Merken** om een overzicht te zien met productcounts en woordtellingen. Lege termen worden gemarkeerd.
|
||||
- Bovenaan staat een bulk-paneel dat een achtergrondproces start (`groq_ai_bulk_generate_terms`) voor lege termen; optioneel kun je bestaande teksten forceren.
|
||||
- Klik op een term om naar de generator te gaan. Daar kun je bovenste en onderste beschrijvingen, Rank Math velden en een term-specifieke prompt beheren. De knop **Genereer** roept `groq_ai_generate_term_text` aan, toont ruwe JSON-output en stelt je in staat de velden met één klik te vullen.
|
||||
- Context bevat: termnaam/slug/productcount, bestaande beschrijvingen (ook custom meta), topverkopende producten (max 25), automatische interne link-suggesties, merkcontext en – indien geactiveerd – Search Console queries en GA4 sessies.
|
||||
- Via de ingestelde tekentaantallen weet de AI hoeveel inhoud de korte en lange omschrijving ongeveer moeten bevatten; hij stuurt automatisch bij binnen ±10%.
|
||||
|
||||
## Ontwikkelvereisten
|
||||
### Modules & integraties
|
||||
- **Rank Math**: bepaalt of focuskeywords + meta velden worden getoond/bewaard bij zowel producten als termen. Limieten (keywords, pixelbreedtess) worden afgedwongen in de promptinstructies en validatie.
|
||||
- **Google-data**: caching en foutafhandeling gebeurt binnen de serviceclient. Errors verschijnen als WP notices en in het log. Zorg dat de redirect-URL (`/wp-admin/admin-post.php?action=groq_ai_google_oauth_callback`) in Google Cloud staat.
|
||||
- **Response-format compatibiliteit**: toggle onder Algemene instellingen om JSON Schema mode te forceren wanneer een provider geen native `response_format` ondersteunt.
|
||||
|
||||
- Docker Desktop of Docker Engine + Docker Compose v2
|
||||
### AI-logboek & troubleshooting
|
||||
- Via **Instellingen → Siti AI → AI-logboek** heb je een WP_List_Table met filters, zoekveld en pagination. Klik op een regel om de detailpagina te zien (prompt, response, tokens, foutmelding, gekoppeld product en gebruiker).
|
||||
- De `Groq_AI_Generation_Logger` creëert automatisch de DB-tabel bij `plugins_loaded`; bij ontbrekende tabellen kun je `WP_DEBUG` gebruiken om fouten te lezen.
|
||||
- Alle fouten worden ook verstuurd naar `WC_Logger` (indien aanwezig) met bron `groq-ai-product-text`.
|
||||
|
||||
## Ontwikkelen in de Docker omgeving
|
||||
## Hooks & extensies
|
||||
- `groq_ai_brand_taxonomy` / `groq_ai_brand_taxonomy_candidates`: overschrijf detectie van merk-taxonomie.
|
||||
- `groq_ai_product_brand_context`: wijzig de tekst die voor merkcontext wordt meegestuurd.
|
||||
- `groq_ai_term_google_context`: voeg extra analytics of SEO-data toe aan termcontext.
|
||||
- `groq_ai_model_exclusions`: pas geblokkeerde modellen per provider aan.
|
||||
- `groq_ai_prompt_default_context_fields`: stel andere standaardcontextvelden in.
|
||||
- `groq_ai_bulk_term_generation_options`: beïnvloed bulk-run opties (bijv. aantal top-producten).
|
||||
- Algemene WordPress filters zoals `plugin_action_links` of `admin_menu` kunnen gebruikt worden voor extra UI-knoppen; `Groq_AI_Service_Container` maakt het eenvoudig om services te vervangen of uit te breiden.
|
||||
|
||||
1. Start de containers (WordPress + MariaDB + phpMyAdmin):
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
2. Open http://localhost:8080 om de WordPress installatie te doorlopen. Gebruik `db` als host en de volgende databasegegevens:
|
||||
- database: `wordpress`
|
||||
- gebruiker: `wordpress`
|
||||
- wachtwoord: `wordpress`
|
||||
3. Activeer in het WordPress dashboard de plugin **SitiAI Product Teksten** (deze repository wordt in de container gemount naar `wp-content/plugins/siti-ai-product-content-generator`).
|
||||
|
||||
### Handige commando's
|
||||
|
||||
- Shell in de WordPress container om bijvoorbeeld `wp` CLI of git te draaien binnen de container:
|
||||
```bash
|
||||
docker compose exec wordpress bash
|
||||
```
|
||||
- WP-CLI is al aanwezig:
|
||||
```bash
|
||||
docker compose exec wordpress wp plugin list
|
||||
```
|
||||
- Bekijk de database via phpMyAdmin op http://localhost:8081 (gebruik dezelfde DB-gebruiker/WW als hierboven).
|
||||
- Containers stoppen:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Werken met git
|
||||
|
||||
De pluginbestanden blijven op de host staan en worden alleen als bind-mount in de container gebruikt. Daardoor kun je git gewoon op je machine gebruiken:
|
||||
## Lokale ontwikkeling
|
||||
### Voorwaarden
|
||||
- Docker Desktop of Docker Engine + Docker Compose v2.
|
||||
- Node of build tooling is niet vereist; alle assets staan reeds gecompileerd in `assets/`.
|
||||
|
||||
### Containers starten
|
||||
```bash
|
||||
git status
|
||||
git add .
|
||||
git commit -m "Beschrijf je wijziging"
|
||||
git push origin <branch>
|
||||
docker compose up --build -d
|
||||
```
|
||||
- WordPress: http://localhost:8082
|
||||
- phpMyAdmin: http://localhost:8085
|
||||
- MariaDB: poort 3307 (database, user, wachtwoord = `wordpress`)
|
||||
|
||||
Je kunt optioneel vanuit de container git gebruiken (zelfde codepad) wanneer je liever binnen Docker werkt.
|
||||
### WordPress initialiseren
|
||||
1. Bezoek http://localhost:8082 en volg de standaard WordPress-installatie (gebruik de `db` host en bovengenoemde databasegegevens).
|
||||
2. Log in, activeer WooCommerce (indien niet automatisch) en activeer daarna **SitiAI Product Teksten**. De plugin is als bind-mount aanwezig onder `wp-content/plugins/siti-ai-product-content-generator`.
|
||||
|
||||
## Tips
|
||||
### Handige commando’s
|
||||
- Shell binnen de WordPress-container: `docker compose exec wordpress bash`
|
||||
- WP-CLI gebruiken: `docker compose exec wordpress wp plugin list`
|
||||
- Logs volgen: `docker compose logs -f wordpress`
|
||||
- Containers stoppen: `docker compose down`
|
||||
- Helemaal opnieuw beginnen (verwijdert volumes): `docker compose down -v`
|
||||
|
||||
- De databank (`db_data`) en WordPress bestanden (`wordpress_data`) worden in Docker volumes opgeslagen zodat je data behouden blijft tussen sessies.
|
||||
- Wil je helemaal opnieuw beginnen? Voer `docker compose down -v` uit om de volumes te verwijderen.
|
||||
Alle code staat buiten de container, dus je kunt op de host `git status`, `git commit` etc. draaien. De Dockerfile (WordPress 6.9 / PHP 8.2) installeert hulpmiddelen zoals git, wp-cli en mariadb-client.
|
||||
|
||||
## Releasen via GitHub Actions
|
||||
## Release & updates
|
||||
1. Verhoog de `Version` header in `groq-ai-product-text.php` en commit de wijzigingen.
|
||||
2. Push naar `main` of start handmatig de GitHub Action **Build & Release Plugin**. De workflow creëert een distributie-zip, maakt tag `vX.Y.Z` en publiceert een GitHub Release met asset.
|
||||
3. Productiesites met de plugin krijgen een update-notificatie via `SitiWebUpdater` en kunnen vanuit het WordPress dashboard upgraden.
|
||||
|
||||
De workflow `.github/workflows/release.yml` bouwt automatisch een distributie-zip van de plugin, maakt een git-tag (`vX.Y.Z`) op basis van de versie in `groq-ai-product-text.php` en publiceert een GitHub Release met het zipbestand als asset.
|
||||
## Mappenstructuur
|
||||
- `groq-ai-product-text.php`: hoofdbestand dat services, providers, admin-schermen en hooks initialiseert.
|
||||
- `includes/Core`: service container, AJAX-controller en model-exclusiehulpen.
|
||||
- `includes/Admin`: instellingenpagina’s, meta-box UI, logboek en termoverzichten.
|
||||
- `includes/Providers`: implementaties voor Groq, OpenAI en Google (Gemini), inclusief live model listing en requestafhandeling.
|
||||
- `includes/Services`: gedeelde services zoals prompt builder, settings manager, conversatiebeheer, logging en Google-clients.
|
||||
- `assets/css` & `assets/js`: statische bestanden voor de WordPress admin experience.
|
||||
- `languages`: `.po/.mo` bestanden voor vertalingen.
|
||||
- `docker` + `docker-compose.yml`: lokale ontwikkelomgeving.
|
||||
- `snippets` & `assets/img`: aanvullende hulpmiddelen en marketingmateriaal.
|
||||
|
||||
1. Werk de `Version`-header in `groq-ai-product-text.php` bij en commit de wijzigingen.
|
||||
2. Push naar `main` of start handmatig de workflow **Build & Release Plugin** via **Actions → Run workflow** (optioneel met extra release notes).
|
||||
3. De workflow slaat releases over wanneer een tag met dezelfde versie al bestaat.
|
||||
Met deze README heb je een startpunt voor zowel functionele gebruikers (hoe gebruik ik de plugin) als ontwikkelaars (hoe werkt de codebase, hoe ontwikkel ik lokaal, hoe release ik). Vragen of verbeteringen? Open een issue of start een PR.
|
||||
|
||||
@@ -22,13 +22,18 @@ class SitiWebUpdater {
|
||||
|
||||
$this->file = $file;
|
||||
|
||||
$this->set_plugin_properties();
|
||||
add_action( 'admin_init', array( $this, 'set_plugin_properties' ) );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function set_plugin_properties() {
|
||||
$this->plugin = get_plugin_data( $this->file );
|
||||
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 );
|
||||
}
|
||||
@@ -66,6 +71,14 @@ class SitiWebUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -88,15 +101,13 @@ class SitiWebUpdater {
|
||||
|
||||
$this->get_repository_info(); // Get the repo info
|
||||
|
||||
if ( empty( $this->github_response ) || empty( $this->github_response['tag_name'] ) ) {
|
||||
$latest_version = $this->get_latest_version_from_response();
|
||||
|
||||
if ( null === $latest_version ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
if ( empty( $checked[ $this->basename ] ) ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$out_of_date = version_compare( $this->github_response['tag_name'], $checked[ $this->basename ], 'gt' ); // Check if we're out of date
|
||||
$out_of_date = version_compare( $latest_version, $checked[ $this->basename ], 'gt' ); // Check if we're out of date
|
||||
|
||||
if( $out_of_date ) {
|
||||
|
||||
@@ -108,7 +119,7 @@ class SitiWebUpdater {
|
||||
'url' => $this->plugin["PluginURI"],
|
||||
'slug' => $slug,
|
||||
'package' => $new_files,
|
||||
'new_version' => $this->github_response['tag_name']
|
||||
'new_version' => $latest_version
|
||||
);
|
||||
|
||||
$transient->response[$this->basename] = (object) $plugin; // Return it in response
|
||||
@@ -126,8 +137,9 @@ class SitiWebUpdater {
|
||||
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 ( empty( $this->github_response ) || empty( $this->github_response['tag_name'] ) ) {
|
||||
if ( null === $latest_version ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -141,7 +153,7 @@ class SitiWebUpdater {
|
||||
'num_ratings' => '10823',
|
||||
'downloaded' => '14249',
|
||||
'added' => '2016-01-05',
|
||||
'version' => $this->github_response['tag_name'],
|
||||
'version' => $latest_version,
|
||||
'author' => $this->plugin["AuthorName"],
|
||||
'author_profile' => $this->plugin["AuthorURI"],
|
||||
'last_updated' => $this->github_response['published_at'],
|
||||
|
||||
@@ -123,6 +123,36 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.groq-ai-title-suggestions {
|
||||
border: 1px dashed #dcdcde;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
background: #fefefe;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.groq-ai-title-suggestions__options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.groq-ai-title-suggestions__option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.groq-ai-title-suggestions__option input[type='radio'] {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.groq-ai-title-suggestions__hint {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.groq-ai-modal__raw {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@@ -29,3 +29,80 @@
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.groq-ai-bulk-panel {
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdcde;
|
||||
}
|
||||
|
||||
.groq-ai-bulk-panel .description {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.groq-ai-bulk-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#groq-ai-bulk-status {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#groq-ai-bulk-status[data-status='error'] {
|
||||
color: #b32d2e;
|
||||
}
|
||||
|
||||
#groq-ai-bulk-status[data-status='success'] {
|
||||
color: #008a20;
|
||||
}
|
||||
|
||||
.groq-ai-bulk-log {
|
||||
margin: 12px 0 0;
|
||||
padding-left: 18px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.groq-ai-bulk-log li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.groq-ai-bulk-log li[data-status='error'] {
|
||||
color: #b32d2e;
|
||||
}
|
||||
|
||||
.groq-ai-bulk-log li[data-status='success'] {
|
||||
color: #008a20;
|
||||
}
|
||||
|
||||
.groq-ai-term-row.groq-ai-term-missing td {
|
||||
background: #fff8e5;
|
||||
}
|
||||
|
||||
.groq-ai-term-row.groq-ai-term-updated td {
|
||||
animation: groqAiTermPulse 1.8s ease-out 1;
|
||||
}
|
||||
|
||||
@keyframes groqAiTermPulse {
|
||||
from {
|
||||
background-color: #e3f8eb;
|
||||
}
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.groq-ai-term-actions {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.groq-ai-regenerate-term.is-busy {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
const resultField = document.getElementById('groq-ai-output');
|
||||
const jsonCopyButton = modal.querySelector('.groq-ai-copy-json');
|
||||
const contextToggles = modal.querySelectorAll('.groq-ai-context-toggle');
|
||||
const attributeToggles = modal.querySelectorAll('.groq-ai-attribute-toggle');
|
||||
const resultFields = {};
|
||||
modal.querySelectorAll('.groq-ai-result-field').forEach((field) => {
|
||||
const key = field.getAttribute('data-field');
|
||||
@@ -28,6 +29,8 @@
|
||||
rankMathAction: field.getAttribute('data-rankmath-action') || '',
|
||||
status: field.querySelector('.groq-ai-apply-status') || null,
|
||||
statusTimer: null,
|
||||
suggestionWrapper: field.querySelector('[data-title-suggestions]') || null,
|
||||
suggestionOptions: field.querySelector('[data-title-suggestions-options]') || null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -58,6 +61,7 @@
|
||||
promptField.value = GroqAIGenerator.defaultPrompt;
|
||||
}
|
||||
resetContextToggles();
|
||||
resetAttributeToggles();
|
||||
setTimeout(() => promptField.focus(), 50);
|
||||
}
|
||||
|
||||
@@ -92,8 +96,8 @@
|
||||
statusField.setAttribute('data-status', type);
|
||||
}
|
||||
|
||||
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'groq-ai-product-text') : 'AI is bezig met schrijven...';
|
||||
const retryText = window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'groq-ai-product-text') : 'Probeer het opnieuw of pas je prompt/context aan.';
|
||||
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...';
|
||||
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.';
|
||||
|
||||
function toggleLoading(isLoading) {
|
||||
modal.classList.toggle('is-loading', isLoading);
|
||||
@@ -113,6 +117,7 @@
|
||||
payload.append('prompt', prompt);
|
||||
payload.append('post_id', GroqAIGenerator.postId || 0);
|
||||
payload.append('context_fields', JSON.stringify(collectContextSelection()));
|
||||
payload.append('attribute_includes', JSON.stringify(collectAttributeSelection()));
|
||||
|
||||
toggleLoading(true);
|
||||
resultWrapper.hidden = true;
|
||||
@@ -120,6 +125,7 @@
|
||||
jsonCopyButton.disabled = true;
|
||||
}
|
||||
resetFieldStatuses();
|
||||
clearTitleSuggestions();
|
||||
|
||||
fetch(GroqAIGenerator.ajaxUrl, {
|
||||
method: 'POST',
|
||||
@@ -142,6 +148,7 @@
|
||||
entry.textarea.value = fields[key] || '';
|
||||
}
|
||||
});
|
||||
updateTitleSuggestions(fields.title_suggestions);
|
||||
resultField.textContent = (json.data.raw || '').trim();
|
||||
resultWrapper.hidden = false;
|
||||
if (jsonCopyButton) {
|
||||
@@ -317,6 +324,8 @@
|
||||
return ['textarea[name="rank_math_description"]'];
|
||||
case 'focus_keywords':
|
||||
return ['input[name="rank_math_focus_keyword"]'];
|
||||
case 'slug':
|
||||
return ['#post_name', 'input[name="post_name"]', '#new-post-slug'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -357,6 +366,89 @@
|
||||
});
|
||||
}
|
||||
|
||||
function clearTitleSuggestions() {
|
||||
const entry = resultFields.title;
|
||||
if (!entry || !entry.suggestionWrapper || !entry.suggestionOptions) {
|
||||
return;
|
||||
}
|
||||
entry.suggestionOptions.innerHTML = '';
|
||||
entry.suggestionWrapper.hidden = true;
|
||||
}
|
||||
|
||||
function updateTitleSuggestions(options) {
|
||||
const entry = resultFields.title;
|
||||
if (!entry || !entry.suggestionWrapper || !entry.suggestionOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.suggestionOptions.innerHTML = '';
|
||||
|
||||
const sanitized = Array.isArray(options)
|
||||
? options
|
||||
.map((option) => (typeof option === 'string' ? option.trim() : ''))
|
||||
.filter((option) => option.length > 0)
|
||||
.slice(0, 3)
|
||||
: [];
|
||||
|
||||
if (!sanitized.length) {
|
||||
entry.suggestionWrapper.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
entry.suggestionWrapper.hidden = false;
|
||||
|
||||
const currentValue = entry.textarea ? entry.textarea.value.trim() : '';
|
||||
const normalizedCurrent = currentValue.toLowerCase();
|
||||
let selectedValue = '';
|
||||
|
||||
if (normalizedCurrent) {
|
||||
const matched = sanitized.find((text) => text.toLowerCase() === normalizedCurrent);
|
||||
if (matched) {
|
||||
selectedValue = matched;
|
||||
if (entry.textarea) {
|
||||
entry.textarea.value = matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedValue) {
|
||||
selectedValue = sanitized[0];
|
||||
if (entry.textarea) {
|
||||
entry.textarea.value = sanitized[0];
|
||||
}
|
||||
}
|
||||
|
||||
const groupName = `groq-ai-title-option-${Date.now()}`;
|
||||
|
||||
sanitized.forEach((text, index) => {
|
||||
const optionId = `${groupName}-${index}`;
|
||||
const optionWrapper = document.createElement('label');
|
||||
optionWrapper.className = 'groq-ai-title-suggestions__option';
|
||||
|
||||
const radio = document.createElement('input');
|
||||
radio.type = 'radio';
|
||||
radio.name = groupName;
|
||||
radio.id = optionId;
|
||||
radio.value = text;
|
||||
if (text === selectedValue) {
|
||||
radio.checked = true;
|
||||
}
|
||||
|
||||
radio.addEventListener('change', () => {
|
||||
if (entry.textarea) {
|
||||
entry.textarea.value = text;
|
||||
}
|
||||
});
|
||||
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.textContent = text;
|
||||
|
||||
optionWrapper.appendChild(radio);
|
||||
optionWrapper.appendChild(textSpan);
|
||||
entry.suggestionOptions.appendChild(optionWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
function resetContextToggles() {
|
||||
const defaults = GroqAIGenerator.contextDefaults || {};
|
||||
contextToggles.forEach((toggle) => {
|
||||
@@ -369,6 +461,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
function resetAttributeToggles() {
|
||||
const defaults = Array.isArray(GroqAIGenerator.attributeIncludesDefaults)
|
||||
? GroqAIGenerator.attributeIncludesDefaults
|
||||
: [];
|
||||
|
||||
attributeToggles.forEach((toggle) => {
|
||||
const key = toggle.getAttribute('data-attribute');
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
toggle.checked = defaults.includes(key);
|
||||
});
|
||||
}
|
||||
|
||||
function collectContextSelection() {
|
||||
const selected = [];
|
||||
contextToggles.forEach((toggle) => {
|
||||
@@ -378,4 +484,18 @@
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
function collectAttributeSelection() {
|
||||
const selected = [];
|
||||
attributeToggles.forEach((toggle) => {
|
||||
if (!toggle.checked) {
|
||||
return;
|
||||
}
|
||||
const key = toggle.getAttribute('data-attribute');
|
||||
if (key) {
|
||||
selected.push(key);
|
||||
}
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
})(jQuery);
|
||||
|
||||
149
assets/js/term-admin.js
Normal file
149
assets/js/term-admin.js
Normal file
@@ -0,0 +1,149 @@
|
||||
(function () {
|
||||
if (!window.GroqAITermGenerator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.getElementById('groq-ai-term-form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promptField = document.getElementById('groq-ai-term-prompt');
|
||||
const outputTopField = document.getElementById('groq-ai-term-generated-top');
|
||||
const outputBottomField = document.getElementById('groq-ai-term-generated-bottom');
|
||||
const outputMetaTitleField = document.getElementById('groq-ai-term-generated-meta-title');
|
||||
const outputMetaDescriptionField = document.getElementById('groq-ai-term-generated-meta-description');
|
||||
const outputFocusKeywordsField = document.getElementById('groq-ai-term-generated-focus-keywords');
|
||||
const rawField = document.getElementById('groq-ai-term-raw');
|
||||
const statusField = document.getElementById('groq-ai-term-status');
|
||||
const applyButton = document.getElementById('groq-ai-term-apply');
|
||||
const includeTopProducts = document.getElementById('groq-ai-term-include-top-products');
|
||||
const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit');
|
||||
|
||||
function setStatus(message, type) {
|
||||
if (!statusField) {
|
||||
return;
|
||||
}
|
||||
statusField.textContent = message || '';
|
||||
statusField.setAttribute('data-status', type || '');
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
form.classList.toggle('is-loading', !!isLoading);
|
||||
const buttons = form.querySelectorAll('button, input[type="submit"]');
|
||||
buttons.forEach((btn) => {
|
||||
btn.disabled = !!isLoading;
|
||||
});
|
||||
}
|
||||
|
||||
function buildPayload(prompt) {
|
||||
const payload = new URLSearchParams();
|
||||
payload.append('action', 'groq_ai_generate_term_text');
|
||||
payload.append('nonce', GroqAITermGenerator.nonce);
|
||||
payload.append('taxonomy', GroqAITermGenerator.taxonomy);
|
||||
payload.append('term_id', GroqAITermGenerator.termId);
|
||||
payload.append('prompt', prompt);
|
||||
payload.append('include_top_products', includeTopProducts && includeTopProducts.checked ? '1' : '0');
|
||||
payload.append('top_products_limit', topProductsLimit ? String(topProductsLimit.value || '') : '10');
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (applyButton) {
|
||||
applyButton.addEventListener('click', () => {
|
||||
const descriptionField = document.getElementById('description');
|
||||
const bottomDescriptionField = document.getElementById('groq-ai-term-bottom-description');
|
||||
const rankmathTitleField = document.getElementById('groq-ai-rankmath-title');
|
||||
const rankmathDescriptionField = document.getElementById('groq-ai-rankmath-description');
|
||||
const rankmathKeywordsField = document.getElementById('groq-ai-rankmath-keywords');
|
||||
if (!outputTopField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (descriptionField) {
|
||||
descriptionField.value = outputTopField.value || '';
|
||||
}
|
||||
if (bottomDescriptionField && outputBottomField) {
|
||||
bottomDescriptionField.value = outputBottomField.value || '';
|
||||
}
|
||||
if (rankmathTitleField && outputMetaTitleField) {
|
||||
rankmathTitleField.value = outputMetaTitleField.value || '';
|
||||
}
|
||||
if (rankmathDescriptionField && outputMetaDescriptionField) {
|
||||
rankmathDescriptionField.value = outputMetaDescriptionField.value || '';
|
||||
}
|
||||
if (rankmathKeywordsField && outputFocusKeywordsField) {
|
||||
rankmathKeywordsField.value = outputFocusKeywordsField.value || '';
|
||||
}
|
||||
|
||||
setStatus('Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const prompt = promptField ? (promptField.value || '').trim() : '';
|
||||
if (!prompt) {
|
||||
setStatus('Vul eerst een prompt in.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setStatus('AI is bezig met schrijven...', 'loading');
|
||||
if (rawField) {
|
||||
rawField.textContent = '';
|
||||
}
|
||||
|
||||
if (outputTopField) outputTopField.value = '';
|
||||
if (outputBottomField) outputBottomField.value = '';
|
||||
if (outputMetaTitleField) outputMetaTitleField.value = '';
|
||||
if (outputMetaDescriptionField) outputMetaDescriptionField.value = '';
|
||||
if (outputFocusKeywordsField) outputFocusKeywordsField.value = '';
|
||||
|
||||
fetch(GroqAITermGenerator.ajaxUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: buildPayload(prompt).toString(),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
if (!json.success) {
|
||||
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (outputTopField) {
|
||||
const top = json.data && (json.data.top_description || json.data.description) ? (json.data.top_description || json.data.description) : '';
|
||||
outputTopField.value = String(top).trim();
|
||||
}
|
||||
if (outputBottomField) {
|
||||
const bottom = json.data && json.data.bottom_description ? json.data.bottom_description : '';
|
||||
outputBottomField.value = String(bottom).trim();
|
||||
}
|
||||
if (outputMetaTitleField) {
|
||||
const metaTitle = json.data && json.data.meta_title ? json.data.meta_title : '';
|
||||
outputMetaTitleField.value = String(metaTitle).trim();
|
||||
}
|
||||
if (outputMetaDescriptionField) {
|
||||
const metaDescription = json.data && json.data.meta_description ? json.data.meta_description : '';
|
||||
outputMetaDescriptionField.value = String(metaDescription).trim();
|
||||
}
|
||||
if (outputFocusKeywordsField) {
|
||||
const keywords = json.data && json.data.focus_keywords ? json.data.focus_keywords : '';
|
||||
outputFocusKeywordsField.value = String(keywords).trim();
|
||||
}
|
||||
if (rawField) {
|
||||
rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();
|
||||
}
|
||||
|
||||
setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success');
|
||||
})
|
||||
.catch((error) => {
|
||||
setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
})();
|
||||
288
assets/js/term-bulk.js
Normal file
288
assets/js/term-bulk.js
Normal file
@@ -0,0 +1,288 @@
|
||||
(function () {
|
||||
const data = window.GroqAITermBulk || {};
|
||||
const startButton = document.getElementById('groq-ai-bulk-generate');
|
||||
const stopButton = document.getElementById('groq-ai-bulk-cancel');
|
||||
const statusField = document.getElementById('groq-ai-bulk-status');
|
||||
const logList = document.getElementById('groq-ai-bulk-log');
|
||||
|
||||
if (!data.ajaxUrl || !startButton || !statusField || !logList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strings = data.strings || {};
|
||||
const allowRegenerate = !!data.allowRegenerate;
|
||||
const terms = (Array.isArray(data.terms) ? data.terms : [])
|
||||
.map((term) => {
|
||||
const id = parseInt(term.id, 10);
|
||||
if (!Number.isFinite(id)) {
|
||||
return null;
|
||||
}
|
||||
const words = typeof term.words === 'number' ? term.words : parseInt(term.words, 10) || 0;
|
||||
const hasDescription = !!term.hasDescription;
|
||||
return {
|
||||
id,
|
||||
name: term.name || '',
|
||||
slug: term.slug || '',
|
||||
count: typeof term.count === 'number' ? term.count : parseInt(term.count, 10) || 0,
|
||||
words,
|
||||
hasDescription,
|
||||
needsGeneration: !hasDescription,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const termMap = new Map();
|
||||
terms.forEach((term) => termMap.set(term.id, term));
|
||||
|
||||
let queue = [];
|
||||
let totalCount = 0;
|
||||
let processed = 0;
|
||||
let successes = 0;
|
||||
let isRunning = false;
|
||||
let abortRequested = false;
|
||||
|
||||
function formatString(template, values) {
|
||||
if (!template) {
|
||||
return '';
|
||||
}
|
||||
let autoIndex = 0;
|
||||
return template.replace(/%(\d+\$)?[sd]/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 setStatus(message, type) {
|
||||
statusField.textContent = message || '';
|
||||
statusField.dataset.status = type || '';
|
||||
}
|
||||
|
||||
function appendLog(message, type) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const item = document.createElement('li');
|
||||
item.textContent = message;
|
||||
item.dataset.status = type || '';
|
||||
logList.appendChild(item);
|
||||
}
|
||||
|
||||
function resetLog() {
|
||||
logList.innerHTML = '';
|
||||
}
|
||||
|
||||
function toggleButtons(running) {
|
||||
isRunning = running;
|
||||
startButton.disabled = running;
|
||||
if (stopButton) {
|
||||
stopButton.hidden = !running;
|
||||
}
|
||||
}
|
||||
|
||||
function getPendingTerms() {
|
||||
return terms.filter((term) => term.needsGeneration);
|
||||
}
|
||||
|
||||
function updateRow(term) {
|
||||
const row = document.querySelector('[data-groq-ai-term-id="' + term.id + '"]');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.classList.remove('groq-ai-term-missing');
|
||||
row.classList.add('groq-ai-term-updated');
|
||||
const wordCell = row.querySelector('.groq-ai-word-count');
|
||||
if (wordCell) {
|
||||
wordCell.textContent = String(term.words);
|
||||
}
|
||||
}
|
||||
|
||||
function markTermCompleted(term, words) {
|
||||
term.hasDescription = true;
|
||||
term.needsGeneration = false;
|
||||
if (Number.isFinite(words)) {
|
||||
term.words = words;
|
||||
}
|
||||
updateRow(term);
|
||||
}
|
||||
|
||||
function finish(state) {
|
||||
const summaryTemplate = state === 'done' ? strings.statusDone : state === 'stopped' ? strings.statusStopped : '';
|
||||
const summary = summaryTemplate ? formatString(summaryTemplate, [successes]) : '';
|
||||
const statusType = state === 'done' ? 'success' : state === 'stopped' ? 'info' : '';
|
||||
setStatus(summary, statusType);
|
||||
toggleButtons(false);
|
||||
queue = [];
|
||||
totalCount = 0;
|
||||
processed = 0;
|
||||
successes = 0;
|
||||
abortRequested = false;
|
||||
}
|
||||
|
||||
function sendRequest(term, options = {}) {
|
||||
const payload = new URLSearchParams();
|
||||
payload.append('action', 'groq_ai_bulk_generate_terms');
|
||||
payload.append('nonce', data.nonce || '');
|
||||
payload.append('taxonomy', data.taxonomy || '');
|
||||
payload.append('term_id', term.id);
|
||||
if (options.force) {
|
||||
payload.append('force', '1');
|
||||
}
|
||||
|
||||
return fetch(data.ajaxUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: payload.toString(),
|
||||
}).then((response) => response.json());
|
||||
}
|
||||
|
||||
function handleResponse(term, json, context) {
|
||||
if (!json || !json.success) {
|
||||
const errorMessage = (json && json.data && json.data.message) || 'Onbekende fout';
|
||||
appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, errorMessage]), 'error');
|
||||
if (context === 'single') {
|
||||
setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, errorMessage]), 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const words = json.data && typeof json.data.words !== 'undefined' ? parseInt(json.data.words, 10) : term.words;
|
||||
markTermCompleted(term, Number.isFinite(words) ? words : term.words);
|
||||
appendLog(formatString(strings.logSuccess || '%1$s gevuld.', [term.name || term.id, term.words]), 'success');
|
||||
if (context === 'single') {
|
||||
setStatus(formatString(strings.regenerateDone || '%s is bijgewerkt.', [term.name || term.id]), 'success');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function processNext() {
|
||||
if (abortRequested) {
|
||||
finish('stopped');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!queue.length) {
|
||||
finish('done');
|
||||
return;
|
||||
}
|
||||
|
||||
const term = queue.shift();
|
||||
const progressTemplate = strings.statusProgress;
|
||||
if (progressTemplate) {
|
||||
setStatus(formatString(progressTemplate, [processed + 1, totalCount, term.name || '']), 'loading');
|
||||
}
|
||||
|
||||
sendRequest(term)
|
||||
.then((json) => {
|
||||
if (handleResponse(term, json, 'bulk')) {
|
||||
successes += 1;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
appendLog(
|
||||
formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, error && error.message ? error.message : 'Onbekende fout']),
|
||||
'error'
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
processed += 1;
|
||||
if (abortRequested) {
|
||||
finish('stopped');
|
||||
} else {
|
||||
processNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startBulk() {
|
||||
if (isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = getPendingTerms();
|
||||
if (!pending.length) {
|
||||
setStatus(strings.statusEmpty || '', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
queue = pending.slice();
|
||||
totalCount = queue.length;
|
||||
processed = 0;
|
||||
successes = 0;
|
||||
abortRequested = false;
|
||||
resetLog();
|
||||
toggleButtons(true);
|
||||
if (strings.statusIdle) {
|
||||
setStatus(strings.statusIdle, 'info');
|
||||
}
|
||||
processNext();
|
||||
}
|
||||
|
||||
startButton.addEventListener('click', startBulk);
|
||||
|
||||
if (stopButton) {
|
||||
stopButton.addEventListener('click', () => {
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
const confirmation = strings.confirmStop ? window.confirm(strings.confirmStop) : window.confirm('Stoppen?');
|
||||
if (confirmation) {
|
||||
abortRequested = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (allowRegenerate) {
|
||||
const buttons = document.querySelectorAll('.groq-ai-regenerate-term');
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
if (isRunning) {
|
||||
setStatus(strings.regenerateBlocked || '', 'error');
|
||||
return;
|
||||
}
|
||||
const termId = parseInt(button.getAttribute('data-term-id'), 10);
|
||||
const term = termMap.get(termId);
|
||||
if (!term) {
|
||||
setStatus('Onbekende term.', 'error');
|
||||
return;
|
||||
}
|
||||
if (strings.confirmRegenerate) {
|
||||
const confirmed = window.confirm(formatString(strings.confirmRegenerate, [term.name || term.id]));
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
button.classList.add('is-busy');
|
||||
button.disabled = true;
|
||||
if (strings.regenerateProgress) {
|
||||
setStatus(formatString(strings.regenerateProgress, [term.name || term.id]), 'loading');
|
||||
}
|
||||
sendRequest(term, { force: true })
|
||||
.then((json) => {
|
||||
handleResponse(term, json, 'single');
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error && error.message ? error.message : 'Onbekende fout';
|
||||
appendLog(formatString(strings.logError || '%1$s: %2$s', [term.name || term.id, message]), 'error');
|
||||
setStatus(formatString(strings.regenerateError || '%1$s mislukt: %2$s', [term.name || term.id, message]), 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
button.disabled = false;
|
||||
button.classList.remove('is-busy');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (getPendingTerms().length === 0) {
|
||||
setStatus(strings.statusEmpty || '', 'info');
|
||||
}
|
||||
})();
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
WORDPRESS_DB_USER: wordpress
|
||||
WORDPRESS_DB_PASSWORD: wordpress
|
||||
WORDPRESS_DB_NAME: wordpress
|
||||
WORDPRESS_DEBUG: 1
|
||||
WORDPRESS_DEBUG: 0
|
||||
WP_ENVIRONMENT_TYPE: local
|
||||
volumes:
|
||||
- wordpress_data:/var/www/html
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
/**
|
||||
* Plugin Name: SitiAI Product Teksten
|
||||
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
|
||||
* Version: 1.2.0
|
||||
* Version: 1.7.0
|
||||
* Author: SitiAI
|
||||
* Text Domain: siti-ai-product-content-generator
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
@@ -27,6 +29,14 @@ if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_VERSION' ) ) {
|
||||
define( 'GROQ_AI_PRODUCT_TEXT_VERSION', $groq_ai_version );
|
||||
}
|
||||
|
||||
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_DOMAIN' ) ) {
|
||||
define( 'GROQ_AI_PRODUCT_TEXT_DOMAIN', 'siti-ai-product-content-generator' );
|
||||
}
|
||||
|
||||
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN' ) ) {
|
||||
define( 'GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN', 'groq-ai-product-text' );
|
||||
}
|
||||
|
||||
if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
define( 'GROQ_AI_DEBUG_TRACE_ADDED', true );
|
||||
}
|
||||
@@ -44,6 +54,10 @@ require_once __DIR__ . '/includes/Services/Settings/class-groq-ai-settings-manag
|
||||
require_once __DIR__ . '/includes/Services/Prompt/class-groq-ai-prompt-builder.php';
|
||||
require_once __DIR__ . '/includes/Services/Conversations/class-groq-ai-conversation-manager.php';
|
||||
require_once __DIR__ . '/includes/Services/Logging/class-groq-ai-generation-logger.php';
|
||||
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-oauth-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-context-builder.php';
|
||||
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php';
|
||||
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php';
|
||||
require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php';
|
||||
@@ -63,6 +77,9 @@ final class Groq_AI_Product_Text_Plugin {
|
||||
const CONVERSATION_OPTION_KEY = 'groq_ai_product_text_conversations';
|
||||
const MODELS_CACHE_OPTION_KEY = 'groq_ai_product_text_models';
|
||||
|
||||
/** @var bool */
|
||||
private $textdomain_loaded = false;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var Groq_AI_Service_Container */
|
||||
@@ -91,8 +108,26 @@ final class Groq_AI_Product_Text_Plugin {
|
||||
$this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() );
|
||||
$this->product_ui = new Groq_AI_Product_Text_Product_UI( $this );
|
||||
|
||||
add_action( 'init', [ $this, 'load_textdomain' ] );
|
||||
add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] );
|
||||
add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] );
|
||||
add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 );
|
||||
}
|
||||
|
||||
public function load_textdomain() {
|
||||
if ( $this->textdomain_loaded ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relative_path = dirname( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) ) . '/languages';
|
||||
|
||||
load_plugin_textdomain( GROQ_AI_PRODUCT_TEXT_DOMAIN, false, $relative_path );
|
||||
|
||||
if ( defined( 'GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN' ) && GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN !== GROQ_AI_PRODUCT_TEXT_DOMAIN ) {
|
||||
load_plugin_textdomain( GROQ_AI_PRODUCT_TEXT_LEGACY_DOMAIN, false, $relative_path );
|
||||
}
|
||||
|
||||
$this->textdomain_loaded = true;
|
||||
}
|
||||
|
||||
private function register_services() {
|
||||
@@ -140,10 +175,47 @@ final class Groq_AI_Product_Text_Plugin {
|
||||
}
|
||||
);
|
||||
|
||||
$this->container->set(
|
||||
'google_oauth_client',
|
||||
function () {
|
||||
return new Groq_AI_Google_OAuth_Client();
|
||||
}
|
||||
);
|
||||
|
||||
$this->container->set(
|
||||
'gsc_client',
|
||||
function ( Groq_AI_Service_Container $container ) {
|
||||
return new Groq_AI_Google_Search_Console_Client( $container->get( 'google_oauth_client' ) );
|
||||
}
|
||||
);
|
||||
|
||||
$this->container->set(
|
||||
'ga_client',
|
||||
function ( Groq_AI_Service_Container $container ) {
|
||||
return new Groq_AI_Google_Analytics_Data_Client( $container->get( 'google_oauth_client' ) );
|
||||
}
|
||||
);
|
||||
|
||||
$this->container->set(
|
||||
'google_context_builder',
|
||||
function ( Groq_AI_Service_Container $container ) {
|
||||
return new Groq_AI_Google_Context_Builder( $container->get( 'gsc_client' ), $container->get( 'ga_client' ) );
|
||||
}
|
||||
);
|
||||
|
||||
// Instantiate controller immediately so hooks are registered.
|
||||
$this->container->get( 'ajax_controller' );
|
||||
}
|
||||
|
||||
public function inject_google_term_context( $existing, $term, $settings ) {
|
||||
$builder = $this->container->get( 'google_context_builder' );
|
||||
if ( ! $builder ) {
|
||||
return (string) $existing;
|
||||
}
|
||||
|
||||
return $builder->build_term_google_context( $existing, $term, $settings );
|
||||
}
|
||||
|
||||
public function get_option_key() {
|
||||
return self::OPTION_KEY;
|
||||
}
|
||||
@@ -198,7 +270,7 @@ final class Groq_AI_Product_Text_Plugin {
|
||||
?>
|
||||
<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' ); ?>
|
||||
<?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
|
||||
@@ -208,15 +280,15 @@ final class Groq_AI_Product_Text_Plugin {
|
||||
$parts = [];
|
||||
|
||||
if ( ! empty( $settings['store_context'] ) ) {
|
||||
$parts[] = sprintf( __( 'Winkelcontext: %s', 'groq-ai-product-text' ), $settings['store_context'] );
|
||||
$parts[] = sprintf( __( 'Winkelcontext: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $settings['store_context'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $settings['default_prompt'] ) ) {
|
||||
$parts[] = sprintf( __( 'Standaard prompt: %s', 'groq-ai-product-text' ), $settings['default_prompt'] );
|
||||
$parts[] = sprintf( __( 'Standaard prompt: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $settings['default_prompt'] );
|
||||
}
|
||||
|
||||
if ( empty( $parts ) ) {
|
||||
return __( 'Nog geen promptinformatie opgeslagen.', 'groq-ai-product-text' );
|
||||
return __( 'Nog geen promptinformatie opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
}
|
||||
|
||||
return implode( "\n\n", $parts );
|
||||
@@ -266,6 +338,18 @@ final class Groq_AI_Product_Text_Plugin {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ class Groq_AI_Logs_Table extends WP_List_Table {
|
||||
|
||||
public function get_columns() {
|
||||
return [
|
||||
'created_at' => __( 'Datum', 'groq-ai-product-text' ),
|
||||
'user_id' => __( 'Gebruiker', 'groq-ai-product-text' ),
|
||||
'post_title' => __( 'Product', 'groq-ai-product-text' ),
|
||||
'provider' => __( 'Provider', 'groq-ai-product-text' ),
|
||||
'model' => __( 'Model', 'groq-ai-product-text' ),
|
||||
'status' => __( 'Status', 'groq-ai-product-text' ),
|
||||
'tokens_total' => __( 'Tokens', 'groq-ai-product-text' ),
|
||||
'created_at' => __( 'Datum', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'user_id' => __( 'Gebruiker', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'post_title' => __( 'Product', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'provider' => __( 'Provider', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'model' => __( 'Model', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'status' => __( 'Status', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'tokens_total' => __( 'Tokens', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class Groq_AI_Logs_Table extends WP_List_Table {
|
||||
if ( ! $item['post_id'] ) {
|
||||
return '—';
|
||||
}
|
||||
$title = $item['post_title'] ? $item['post_title'] : sprintf( __( 'Product #%d', 'groq-ai-product-text' ), (int) $item['post_id'] );
|
||||
$title = $item['post_title'] ? $item['post_title'] : sprintf( __( 'Product #%d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (int) $item['post_id'] );
|
||||
$link = get_edit_post_link( $item['post_id'] );
|
||||
return $link ? sprintf( '<a href="%s">%s</a>', esc_url( $link ), esc_html( $title ) ) : esc_html( $title );
|
||||
case 'user_id':
|
||||
@@ -131,33 +131,20 @@ class Groq_AI_Logs_Table extends WP_List_Table {
|
||||
}
|
||||
|
||||
public function no_items() {
|
||||
esc_html_e( 'Nog geen AI-logboeken gevonden.', 'groq-ai-product-text' );
|
||||
esc_html_e( 'Nog geen AI-logboeken gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
}
|
||||
|
||||
protected function column_created_at( $item ) {
|
||||
$date = esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
|
||||
$usage = $this->get_usage_meta( $item );
|
||||
$payload = [
|
||||
'created_at' => $item['created_at'],
|
||||
'user' => $this->column_default( $item, 'user_id' ),
|
||||
'post_title' => $item['post_title'],
|
||||
'provider' => $item['provider'],
|
||||
'model' => $item['model'],
|
||||
'status' => $item['status'],
|
||||
'tokens_prompt' => isset( $item['tokens_prompt'] ) ? (int) $item['tokens_prompt'] : null,
|
||||
'tokens_completion' => isset( $item['tokens_completion'] ) ? (int) $item['tokens_completion'] : null,
|
||||
'tokens_total' => isset( $item['tokens_total'] ) ? (int) $item['tokens_total'] : null,
|
||||
'prompt' => $item['prompt'],
|
||||
'response' => $item['response'],
|
||||
'error_message' => $item['error_message'],
|
||||
'image_context' => isset( $usage['image_context'] ) ? $usage['image_context'] : null,
|
||||
];
|
||||
$encoded = esc_attr( wp_json_encode( $payload ) );
|
||||
return sprintf(
|
||||
'<a href="#" class="groq-ai-log-row" data-groq-log="%s">%s</a>',
|
||||
$encoded,
|
||||
$date
|
||||
$url = add_query_arg(
|
||||
[
|
||||
'page' => 'groq-ai-product-text-log',
|
||||
'log_id' => isset( $item['id'] ) ? (int) $item['id'] : 0,
|
||||
],
|
||||
admin_url( 'options-general.php' )
|
||||
);
|
||||
|
||||
return sprintf( '<a href="%s">%s</a>', esc_url( $url ), $date );
|
||||
}
|
||||
|
||||
private function get_usage_meta( $item ) {
|
||||
|
||||
@@ -14,7 +14,7 @@ class Groq_AI_Product_Text_Product_UI {
|
||||
public function register_meta_box() {
|
||||
add_meta_box(
|
||||
'groq-ai-generator-box',
|
||||
__( 'Gebruik AI', 'groq-ai-product-text' ),
|
||||
__( 'Gebruik AI', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
[ $this, 'render_meta_box' ],
|
||||
'product',
|
||||
'side',
|
||||
@@ -24,14 +24,14 @@ class Groq_AI_Product_Text_Product_UI {
|
||||
|
||||
public function render_meta_box() {
|
||||
if ( ! current_user_can( 'edit_products' ) ) {
|
||||
echo '<p>' . esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', 'groq-ai-product-text' ) . '</p>';
|
||||
echo '<p>' . esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) . '</p>';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<p><?php esc_html_e( 'Laat de geselecteerde AI een concepttekst genereren op basis van een prompt.', 'groq-ai-product-text' ); ?></p>
|
||||
<button type="button" class="button button-primary groq-ai-open-modal" data-target="groq-ai-modal"><?php esc_html_e( 'Gebruik AI', 'groq-ai-product-text' ); ?></button>
|
||||
<p><?php esc_html_e( 'Laat de geselecteerde AI een concepttekst genereren op basis van een prompt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||
<button type="button" class="button button-primary groq-ai-open-modal" data-target="groq-ai-modal"><?php esc_html_e( 'Gebruik AI', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<p class="description" style="margin-top:8px;">
|
||||
<?php esc_html_e( 'Klik om een prompt in te voeren en een voorsteltekst te genereren. Plak het resultaat in de beschrijving of korte beschrijving.', 'groq-ai-product-text' ); ?>
|
||||
<?php esc_html_e( 'Klik om een prompt in te voeren en een voorsteltekst te genereren. Plak het resultaat in de beschrijving of korte beschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
@@ -59,6 +59,9 @@ class Groq_AI_Product_Text_Product_UI {
|
||||
$post_id = ( $post && isset( $post->ID ) ) ? (int) $post->ID : 0;
|
||||
|
||||
$settings = $this->plugin->get_settings();
|
||||
$attribute_defaults = isset( $settings['product_attribute_includes'] ) && is_array( $settings['product_attribute_includes'] )
|
||||
? array_values( array_unique( array_map( 'sanitize_key', $settings['product_attribute_includes'] ) ) )
|
||||
: [];
|
||||
|
||||
wp_localize_script(
|
||||
'groq-ai-admin',
|
||||
@@ -69,6 +72,7 @@ class Groq_AI_Product_Text_Product_UI {
|
||||
'defaultPrompt' => $settings['default_prompt'],
|
||||
'postId' => $post_id,
|
||||
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
|
||||
'attributeIncludesDefaults' => $attribute_defaults,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -82,33 +86,37 @@ class Groq_AI_Product_Text_Product_UI {
|
||||
|
||||
$settings = $this->plugin->get_settings();
|
||||
$rankmath_enabled = $this->plugin->is_rankmath_active() && $this->plugin->is_module_enabled( 'rankmath', $settings );
|
||||
$attribute_options = $this->get_product_attribute_include_options();
|
||||
?>
|
||||
<div id="groq-ai-modal" class="groq-ai-modal" aria-hidden="true">
|
||||
<div class="groq-ai-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="groq-ai-modal-title">
|
||||
<button type="button" class="groq-ai-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', 'groq-ai-product-text' ); ?>">×</button>
|
||||
<button type="button" class="groq-ai-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">×</button>
|
||||
<div class="groq-ai-modal__dialog-inner">
|
||||
<h2 id="groq-ai-modal-title"><?php esc_html_e( 'Siti AI prompt', 'groq-ai-product-text' ); ?></h2>
|
||||
<h2 id="groq-ai-modal-title"><?php esc_html_e( 'Siti AI prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
|
||||
<form id="groq-ai-form">
|
||||
<label for="groq-ai-prompt" class="screen-reader-text"><?php esc_html_e( 'Prompt', 'groq-ai-product-text' ); ?></label>
|
||||
<textarea id="groq-ai-prompt" rows="6" placeholder="<?php esc_attr_e( 'Beschrijf hier wat de AI moet schrijven...', 'groq-ai-product-text' ); ?>"></textarea>
|
||||
<label for="groq-ai-prompt" class="screen-reader-text"><?php esc_html_e( 'Prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label>
|
||||
<textarea id="groq-ai-prompt" rows="6" placeholder="<?php esc_attr_e( 'Beschrijf hier wat de AI moet schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>"></textarea>
|
||||
<div class="groq-ai-modal__actions">
|
||||
<button type="submit" class="button button-primary">
|
||||
<?php esc_html_e( 'Genereer tekst', 'groq-ai-product-text' ); ?>
|
||||
<?php esc_html_e( 'Genereer tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="groq-ai-advanced-settings">
|
||||
<button type="button" class="groq-ai-advanced-toggle" aria-expanded="false" aria-controls="groq-ai-advanced-panel">
|
||||
<span class="groq-ai-advanced-toggle__icon" aria-hidden="true"></span>
|
||||
<?php esc_html_e( 'Geavanceerde instellingen', 'groq-ai-product-text' ); ?>
|
||||
<?php esc_html_e( 'Geavanceerde instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
|
||||
</button>
|
||||
<div id="groq-ai-advanced-panel" class="groq-ai-context-options" hidden>
|
||||
<h3><?php esc_html_e( 'Gebruik deze productinformatie in de prompt', 'groq-ai-product-text' ); ?></h3>
|
||||
<p class="description"><?php esc_html_e( 'Je kunt tijdelijk onderdelen uitzetten of weer inschakelen. Standaard zijn de opties aangevinkt zoals ingesteld op de instellingenpagina.', 'groq-ai-product-text' ); ?></p>
|
||||
<h3><?php esc_html_e( 'Gebruik deze productinformatie in de prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||
<p class="description"><?php esc_html_e( 'Je kunt tijdelijk onderdelen uitzetten of weer inschakelen. Standaard zijn de opties aangevinkt zoals ingesteld op de instellingenpagina.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||
<div class="groq-ai-context-options__grid">
|
||||
<?php
|
||||
$context_definitions = $this->plugin->get_context_field_definitions();
|
||||
$context_defaults = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();
|
||||
foreach ( $context_definitions as $context_key => $context_info ) :
|
||||
if ( 'attributes' === $context_key ) {
|
||||
continue;
|
||||
}
|
||||
$checked = ! empty( $context_defaults[ $context_key ] );
|
||||
?>
|
||||
<label class="groq-ai-context-option">
|
||||
@@ -122,85 +130,118 @@ class Groq_AI_Product_Text_Product_UI {
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top:16px;"><?php esc_html_e( 'Attributen meesturen', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||
<p class="description"><?php esc_html_e( 'Selecteer welke productattributen je mee wilt geven aan de AI. Dit vervangt de oude alles-of-niets optie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||
<?php if ( empty( $attribute_options ) ) : ?>
|
||||
<p class="description"><?php esc_html_e( 'Geen WooCommerce-attributen gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||
<?php else : ?>
|
||||
<div class="groq-ai-context-options__grid">
|
||||
<?php foreach ( $attribute_options as $attr_key => $attr_label ) : ?>
|
||||
<label class="groq-ai-context-option">
|
||||
<input type="checkbox" class="groq-ai-attribute-toggle" data-attribute="<?php echo esc_attr( $attr_key ); ?>" />
|
||||
<div>
|
||||
<strong><?php echo esc_html( $attr_label ); ?></strong>
|
||||
</div>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="groq-ai-modal__result" hidden>
|
||||
<h3><?php esc_html_e( 'Resultaat', 'groq-ai-product-text' ); ?></h3>
|
||||
<div class="groq-ai-result-grid">
|
||||
<div class="groq-ai-result-field" data-field="title" data-target-input="#title" data-label="<?php esc_attr_e( 'Producttitel', 'groq-ai-product-text' ); ?>">
|
||||
<h3><?php esc_html_e( 'Resultaat', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||
<div class="groq-ai-result-grid">
|
||||
<div class="groq-ai-result-field" data-field="title" data-target-input="#title" data-label="<?php esc_attr_e( 'Producttitel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
|
||||
<div class="groq-ai-result-field__header">
|
||||
<strong><?php esc_html_e( 'Producttitel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
|
||||
<div class="groq-ai-result-field__actions">
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="title"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="title"><?php esc_html_e( 'Vul titel in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<span class="groq-ai-apply-status" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="groq-ai-title-suggestions" data-title-suggestions hidden>
|
||||
<p class="groq-ai-title-suggestions__label"><?php esc_html_e( 'Kies je favoriete titelvoorstel:', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||
<div class="groq-ai-title-suggestions__options" data-title-suggestions-options></div>
|
||||
<p class="description groq-ai-title-suggestions__hint"><?php esc_html_e( 'Je kunt de tekst hieronder altijd nog aanpassen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||
</div>
|
||||
<textarea rows="2"></textarea>
|
||||
</div>
|
||||
<div class="groq-ai-result-field" data-field="slug" data-target-input="#slug" data-label="<?php esc_attr_e( 'Productslug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
|
||||
<div class="groq-ai-result-field__header">
|
||||
<strong><?php esc_html_e( 'Producttitel', 'groq-ai-product-text' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Productslug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
|
||||
<div class="groq-ai-result-field__actions">
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="title"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="title"><?php esc_html_e( 'Vul titel in', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="slug"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="slug"><?php esc_html_e( 'Vul slug in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<span class="groq-ai-apply-status" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea rows="2"></textarea>
|
||||
<textarea rows="1"></textarea>
|
||||
</div>
|
||||
<div class="groq-ai-result-field" data-field="short_description" data-target-input="#excerpt" data-label="<?php esc_attr_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?>">
|
||||
<div class="groq-ai-result-field" data-field="short_description" data-target-input="#excerpt" data-label="<?php esc_attr_e( 'Korte beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
|
||||
<div class="groq-ai-result-field__header">
|
||||
<strong><?php esc_html_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Korte beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
|
||||
<div class="groq-ai-result-field__actions">
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="short_description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="short_description"><?php esc_html_e( 'Vul korte beschrijving in', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="short_description"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="short_description"><?php esc_html_e( 'Vul korte beschrijving in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<span class="groq-ai-apply-status" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea rows="3"></textarea>
|
||||
</div>
|
||||
<div class="groq-ai-result-field" data-field="description" data-target-input="#content" data-label="<?php esc_attr_e( 'Beschrijving', 'groq-ai-product-text' ); ?>">
|
||||
<div class="groq-ai-result-field" data-field="description" data-target-input="#content" data-label="<?php esc_attr_e( 'Beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
|
||||
<div class="groq-ai-result-field__header">
|
||||
<strong><?php esc_html_e( 'Beschrijving', 'groq-ai-product-text' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
|
||||
<div class="groq-ai-result-field__actions">
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="description"><?php esc_html_e( 'Vul beschrijving in', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="description"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="description"><?php esc_html_e( 'Vul beschrijving in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<span class="groq-ai-apply-status" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea rows="6"></textarea>
|
||||
</div>
|
||||
<?php if ( $rankmath_enabled ) : ?>
|
||||
<div class="groq-ai-result-field" data-field="meta_title" data-target-input="#rank_math_title" data-rankmath-action="updateTitle" data-label="<?php esc_attr_e( 'Rank Math meta titel', 'groq-ai-product-text' ); ?>">
|
||||
<div class="groq-ai-result-field" data-field="meta_title" data-target-input="#rank_math_title" data-rankmath-action="updateTitle" data-label="<?php esc_attr_e( 'Rank Math meta titel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
|
||||
<div class="groq-ai-result-field__header">
|
||||
<strong><?php esc_html_e( 'Rank Math meta titel', 'groq-ai-product-text' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Rank Math meta titel', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
|
||||
<div class="groq-ai-result-field__actions">
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_title"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="meta_title"><?php esc_html_e( 'Vul meta titel in', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_title"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="meta_title"><?php esc_html_e( 'Vul meta titel in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<span class="groq-ai-apply-status" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea rows="2"></textarea>
|
||||
</div>
|
||||
<div class="groq-ai-result-field" data-field="meta_description" data-target-input="#rank_math_description" data-rankmath-action="updateDescription" data-label="<?php esc_attr_e( 'Rank Math meta description', 'groq-ai-product-text' ); ?>">
|
||||
<div class="groq-ai-result-field" data-field="meta_description" data-target-input="#rank_math_description" data-rankmath-action="updateDescription" data-label="<?php esc_attr_e( 'Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
|
||||
<div class="groq-ai-result-field__header">
|
||||
<strong><?php esc_html_e( 'Rank Math meta description', 'groq-ai-product-text' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
|
||||
<div class="groq-ai-result-field__actions">
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="meta_description"><?php esc_html_e( 'Vul meta description in', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_description"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="meta_description"><?php esc_html_e( 'Vul meta description in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<span class="groq-ai-apply-status" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea rows="3"></textarea>
|
||||
</div>
|
||||
<div class="groq-ai-result-field" data-field="focus_keywords" data-target-input="#rank_math_focus_keyword" data-rankmath-action="updateKeywords" data-label="<?php esc_attr_e( 'Rank Math focus keyphrase', 'groq-ai-product-text' ); ?>">
|
||||
<div class="groq-ai-result-field" data-field="focus_keywords" data-target-input="#rank_math_focus_keyword" data-rankmath-action="updateKeywords" data-label="<?php esc_attr_e( 'Rank Math focus keyphrase', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>">
|
||||
<div class="groq-ai-result-field__header">
|
||||
<strong><?php esc_html_e( 'Rank Math focus keyphrase', 'groq-ai-product-text' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Rank Math focus keyphrase', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></strong>
|
||||
<div class="groq-ai-result-field__actions">
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="focus_keywords"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="focus_keywords"><?php esc_html_e( 'Vul focus keyphrase in', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="focus_keywords"><?php esc_html_e( 'Kopieer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<button type="button" class="button groq-ai-apply-field" data-field="focus_keywords"><?php esc_html_e( 'Vul focus keyphrase in', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
<span class="groq-ai-apply-status" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea rows="2" placeholder="<?php esc_attr_e( 'bijv. luxe massage apparaat, wellness cadeau', 'groq-ai-product-text' ); ?>"></textarea>
|
||||
<textarea rows="2" placeholder="<?php esc_attr_e( 'bijv. luxe massage apparaat, wellness cadeau', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>"></textarea>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="groq-ai-modal__raw">
|
||||
<h4><?php esc_html_e( 'Ruwe JSON-output', 'groq-ai-product-text' ); ?></h4>
|
||||
<h4><?php esc_html_e( 'Ruwe JSON-output', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h4>
|
||||
<pre id="groq-ai-output"></pre>
|
||||
<button type="button" class="button groq-ai-copy-json"><?php esc_html_e( 'Kopieer JSON', 'groq-ai-product-text' ); ?></button>
|
||||
<button type="button" class="button groq-ai-copy-json"><?php esc_html_e( 'Kopieer JSON', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="groq-ai-modal__status" aria-live="polite"></div>
|
||||
@@ -209,4 +250,39 @@ class Groq_AI_Product_Text_Product_UI {
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function get_product_attribute_include_options() {
|
||||
$options = [
|
||||
'__custom__' => __( 'Custom attributen (niet-taxonomie)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
];
|
||||
|
||||
if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
|
||||
$taxonomies = wc_get_attribute_taxonomies();
|
||||
if ( is_array( $taxonomies ) ) {
|
||||
foreach ( $taxonomies as $attr ) {
|
||||
$name = isset( $attr->attribute_name ) ? sanitize_key( (string) $attr->attribute_name ) : '';
|
||||
$label = isset( $attr->attribute_label ) ? sanitize_text_field( (string) $attr->attribute_label ) : '';
|
||||
if ( '' === $name ) {
|
||||
continue;
|
||||
}
|
||||
$taxonomy = 'pa_' . $name;
|
||||
if ( '' === $label ) {
|
||||
$label = function_exists( 'wc_attribute_label' ) ? wc_attribute_label( $taxonomy ) : $taxonomy;
|
||||
}
|
||||
$options[ $taxonomy ] = $label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $options ) > 1 ) {
|
||||
$fixed = [
|
||||
'__custom__' => $options['__custom__'],
|
||||
];
|
||||
unset( $options['__custom__'] );
|
||||
asort( $options, SORT_NATURAL | SORT_FLAG_CASE );
|
||||
$options = $fixed + $options;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,400 @@ class Groq_AI_Ajax_Controller {
|
||||
|
||||
add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] );
|
||||
add_action( 'wp_ajax_groq_ai_refresh_models', [ $this, 'handle_refresh_models' ] );
|
||||
add_action( 'wp_ajax_groq_ai_generate_term_text', [ $this, 'handle_generate_term_text' ] );
|
||||
add_action( 'wp_ajax_groq_ai_bulk_generate_terms', [ $this, 'handle_bulk_generate_terms_request' ] );
|
||||
}
|
||||
|
||||
public function handle_generate_term_text() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'groq_ai_generate_term', 'nonce' );
|
||||
|
||||
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
|
||||
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
|
||||
$term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0;
|
||||
$include_top_products = ! empty( $_POST['include_top_products'] );
|
||||
$top_products_limit = isset( $_POST['top_products_limit'] ) ? absint( $_POST['top_products_limit'] ) : 10;
|
||||
$top_products_limit = max( 1, min( 25, $top_products_limit ) );
|
||||
|
||||
if ( '' === $prompt || '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Prompt, taxonomy en term_id zijn verplicht.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
|
||||
}
|
||||
|
||||
$term = get_term( $term_id, $taxonomy );
|
||||
if ( ! $term || is_wp_error( $term ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
|
||||
}
|
||||
|
||||
$result = $this->run_term_generation(
|
||||
$term,
|
||||
$prompt,
|
||||
[
|
||||
'include_top_products' => $include_top_products,
|
||||
'top_products_limit' => $top_products_limit,
|
||||
'origin' => 'term_manual',
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
|
||||
}
|
||||
|
||||
wp_send_json_success( $result );
|
||||
}
|
||||
|
||||
public function handle_bulk_generate_terms_request() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'groq_ai_bulk_generate_terms', 'nonce' );
|
||||
|
||||
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
|
||||
$term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0;
|
||||
$force = ! empty( $_POST['force'] );
|
||||
|
||||
if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Taxonomie en term_id zijn verplicht.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
|
||||
}
|
||||
|
||||
$term = get_term( $term_id, $taxonomy );
|
||||
if ( ! $term || is_wp_error( $term ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
|
||||
}
|
||||
|
||||
$current_description = isset( $term->description ) ? trim( wp_strip_all_tags( (string) $term->description ) ) : '';
|
||||
if ( '' !== $current_description && ! $force ) {
|
||||
wp_send_json_error(
|
||||
[
|
||||
'message' => __( 'Categorie heeft al een omschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'code' => 'groq_ai_term_has_description',
|
||||
],
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
$options = apply_filters(
|
||||
'groq_ai_bulk_term_generation_options',
|
||||
[
|
||||
'include_top_products' => true,
|
||||
'top_products_limit' => 10,
|
||||
],
|
||||
$term
|
||||
);
|
||||
|
||||
$options['origin'] = $force ? 'term_force_regenerate' : 'term_bulk_auto';
|
||||
$options['force'] = $force;
|
||||
|
||||
$result = $this->run_term_generation( $term, $this->get_term_prompt_text( $term ), $options );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
|
||||
}
|
||||
|
||||
$settings = $this->plugin->get_settings();
|
||||
$saved = $this->save_term_generation_result( $term, $result, $settings );
|
||||
|
||||
if ( is_wp_error( $saved ) ) {
|
||||
wp_send_json_error( [ 'message' => $saved->get_error_message() ], 500 );
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
[
|
||||
'term_id' => $term_id,
|
||||
'name' => isset( $term->name ) ? (string) $term->name : '',
|
||||
'words' => isset( $saved['words'] ) ? absint( $saved['words'] ) : 0,
|
||||
'count' => isset( $term->count ) ? absint( $term->count ) : 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function run_term_generation( $term, $prompt, $options = [] ) {
|
||||
if ( ! $term || ! is_object( $term ) ) {
|
||||
return new WP_Error( 'groq_ai_invalid_term', __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
|
||||
$term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
|
||||
|
||||
$options = wp_parse_args(
|
||||
$options,
|
||||
[
|
||||
'include_top_products' => true,
|
||||
'top_products_limit' => 10,
|
||||
'origin' => 'term_manual',
|
||||
'force' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$origin = isset( $options['origin'] ) ? sanitize_key( (string) $options['origin'] ) : 'term_manual';
|
||||
$force_run = ! empty( $options['force'] );
|
||||
$include_top_products = ! empty( $options['include_top_products'] );
|
||||
$top_products_limit = isset( $options['top_products_limit'] ) ? absint( $options['top_products_limit'] ) : 10;
|
||||
$top_products_limit = max( 1, min( 25, $top_products_limit ) );
|
||||
|
||||
$logger = $this->plugin->get_generation_logger();
|
||||
$settings = $this->plugin->get_settings();
|
||||
$provider_manager = $this->plugin->get_provider_manager();
|
||||
$provider_key = $settings['provider'];
|
||||
$provider = $provider_manager->get_provider( $provider_key );
|
||||
|
||||
if ( ! $provider ) {
|
||||
$provider = $provider_manager->get_provider( 'groq' );
|
||||
$provider_key = 'groq';
|
||||
}
|
||||
|
||||
$conversation_id = $this->plugin->get_conversation_manager()->ensure_id( $provider_key, $settings['store_context'] );
|
||||
$prompt_builder = $this->plugin->get_prompt_builder();
|
||||
$system_prompt = method_exists( $prompt_builder, 'build_term_system_prompt' )
|
||||
? $prompt_builder->build_term_system_prompt( $settings, $conversation_id, $term )
|
||||
: $prompt_builder->build_system_prompt( $settings, $conversation_id );
|
||||
|
||||
$context_block = '';
|
||||
if ( method_exists( $prompt_builder, 'build_term_context_block' ) ) {
|
||||
$context_block = $prompt_builder->build_term_context_block(
|
||||
$term,
|
||||
[
|
||||
'include_top_products' => $include_top_products,
|
||||
'top_products_limit' => $top_products_limit,
|
||||
],
|
||||
$settings
|
||||
);
|
||||
}
|
||||
$prompt_with_context = method_exists( $prompt_builder, 'prepend_term_context_to_prompt' )
|
||||
? $prompt_builder->prepend_term_context_to_prompt( $prompt, $context_block )
|
||||
: $prompt_builder->prepend_context_to_prompt( $prompt, $context_block );
|
||||
|
||||
$usage_meta = [
|
||||
'term_context' => [
|
||||
'taxonomy' => $taxonomy,
|
||||
'term_id' => $term_id,
|
||||
'origin' => $origin,
|
||||
],
|
||||
'term_options' => [
|
||||
'include_top_products' => $include_top_products,
|
||||
'top_products_limit' => $top_products_limit,
|
||||
'force' => $force_run,
|
||||
],
|
||||
];
|
||||
|
||||
$response_format = null;
|
||||
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
|
||||
if ( $use_response_format && method_exists( $prompt_builder, 'get_term_response_format_definition' ) ) {
|
||||
$response_format = $prompt_builder->get_term_response_format_definition( $settings );
|
||||
$final_prompt = $prompt_with_context;
|
||||
} elseif ( method_exists( $prompt_builder, 'append_term_response_instructions' ) ) {
|
||||
$final_prompt = $prompt_builder->append_term_response_instructions( $prompt_with_context, $settings );
|
||||
} else {
|
||||
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
|
||||
}
|
||||
|
||||
$model = $this->plugin->get_selected_model( $provider, $settings );
|
||||
$result = $provider->generate_content(
|
||||
[
|
||||
'prompt' => $final_prompt,
|
||||
'system_prompt' => $system_prompt,
|
||||
'model' => $model,
|
||||
'settings' => $settings,
|
||||
'temperature' => 0.7,
|
||||
'conversation_id' => $conversation_id,
|
||||
'response_format' => $response_format,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
if ( $logger ) {
|
||||
$logger->log_generation_event(
|
||||
[
|
||||
'provider' => $provider_key,
|
||||
'model' => $model,
|
||||
'prompt' => $final_prompt,
|
||||
'response' => '',
|
||||
'usage' => $usage_meta,
|
||||
'status' => 'error',
|
||||
'error_message' => $result->get_error_message(),
|
||||
'post_id' => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
$response_text = $this->extract_content_text( $result );
|
||||
$response_usage = is_array( $result ) && isset( $result['usage'] ) ? $result['usage'] : [];
|
||||
if ( ! is_array( $response_usage ) ) {
|
||||
$response_usage = [];
|
||||
}
|
||||
$response_usage['term_context'] = $usage_meta['term_context'];
|
||||
$response_usage['term_options'] = $usage_meta['term_options'];
|
||||
$parsed = null;
|
||||
if ( method_exists( $prompt_builder, 'parse_term_structured_response' ) ) {
|
||||
$parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings );
|
||||
}
|
||||
if ( is_wp_error( $parsed ) ) {
|
||||
if ( $logger ) {
|
||||
$logger->log_generation_event(
|
||||
[
|
||||
'provider' => $provider_key,
|
||||
'model' => $model,
|
||||
'prompt' => $final_prompt,
|
||||
'response' => $response_text,
|
||||
'usage' => $response_usage,
|
||||
'status' => 'error',
|
||||
'error_message' => $parsed->get_error_message(),
|
||||
'post_id' => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
return $parsed;
|
||||
}
|
||||
if ( ! is_array( $parsed ) ) {
|
||||
$parsed = [
|
||||
'description' => trim( (string) $response_text ),
|
||||
];
|
||||
}
|
||||
|
||||
if ( $logger ) {
|
||||
$logger->log_generation_event(
|
||||
[
|
||||
'provider' => $provider_key,
|
||||
'model' => $model,
|
||||
'prompt' => $final_prompt,
|
||||
'response' => $response_text,
|
||||
'usage' => $response_usage,
|
||||
'status' => 'success',
|
||||
'post_id' => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'top_description' => isset( $parsed['top_description'] ) ? $parsed['top_description'] : ( isset( $parsed['description'] ) ? $parsed['description'] : '' ),
|
||||
'bottom_description' => isset( $parsed['bottom_description'] ) ? $parsed['bottom_description'] : '',
|
||||
'meta_title' => isset( $parsed['meta_title'] ) ? $parsed['meta_title'] : '',
|
||||
'meta_description' => isset( $parsed['meta_description'] ) ? $parsed['meta_description'] : '',
|
||||
'focus_keywords' => isset( $parsed['focus_keywords'] ) ? $parsed['focus_keywords'] : '',
|
||||
'description' => isset( $parsed['description'] ) ? $parsed['description'] : ( isset( $parsed['top_description'] ) ? $parsed['top_description'] : '' ),
|
||||
'raw' => $response_text,
|
||||
];
|
||||
}
|
||||
|
||||
private function save_term_generation_result( $term, $result, $settings ) {
|
||||
$term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
|
||||
$taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
|
||||
|
||||
if ( ! $term_id || '' === $taxonomy ) {
|
||||
return new WP_Error( 'groq_ai_invalid_term', __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$top_description = '';
|
||||
if ( isset( $result['top_description'] ) && '' !== trim( (string) $result['top_description'] ) ) {
|
||||
$top_description = (string) $result['top_description'];
|
||||
} elseif ( isset( $result['description'] ) ) {
|
||||
$top_description = (string) $result['description'];
|
||||
}
|
||||
|
||||
if ( '' === trim( wp_strip_all_tags( $top_description ) ) ) {
|
||||
return new WP_Error( 'groq_ai_missing_description', __( 'De AI gaf geen omschrijving terug.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$update = wp_update_term(
|
||||
$term_id,
|
||||
$taxonomy,
|
||||
[
|
||||
'description' => wp_kses_post( $top_description ),
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $update ) ) {
|
||||
return $update;
|
||||
}
|
||||
|
||||
$bottom_key = $this->get_bottom_meta_key( $term, $settings );
|
||||
if ( '' !== $bottom_key ) {
|
||||
$bottom_description = isset( $result['bottom_description'] ) ? (string) $result['bottom_description'] : '';
|
||||
update_term_meta( $term_id, $bottom_key, wp_kses_post( $bottom_description ) );
|
||||
}
|
||||
|
||||
if ( $this->plugin->is_module_enabled( 'rankmath', $settings ) ) {
|
||||
$rankmath_keys = $this->get_rankmath_term_meta_keys( $term, $settings );
|
||||
update_term_meta( $term_id, $rankmath_keys['title'], sanitize_text_field( isset( $result['meta_title'] ) ? $result['meta_title'] : '' ) );
|
||||
update_term_meta( $term_id, $rankmath_keys['description'], sanitize_text_field( isset( $result['meta_description'] ) ? $result['meta_description'] : '' ) );
|
||||
update_term_meta( $term_id, $rankmath_keys['focus_keyword'], sanitize_text_field( isset( $result['focus_keywords'] ) ? $result['focus_keywords'] : '' ) );
|
||||
}
|
||||
|
||||
return [
|
||||
'words' => $this->count_words( $top_description ),
|
||||
];
|
||||
}
|
||||
|
||||
private function get_bottom_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 ? $key : 'groq_ai_term_bottom_description';
|
||||
}
|
||||
|
||||
private function get_rankmath_term_meta_keys( $term, $settings ) {
|
||||
$defaults = [
|
||||
'title' => 'rank_math_title',
|
||||
'description' => 'rank_math_description',
|
||||
'focus_keyword' => 'rank_math_focus_keyword',
|
||||
];
|
||||
|
||||
$keys = apply_filters( 'groq_ai_rankmath_term_meta_keys', $defaults, $term, $settings );
|
||||
if ( ! is_array( $keys ) ) {
|
||||
$keys = $defaults;
|
||||
}
|
||||
|
||||
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 ) {
|
||||
$prompt = '';
|
||||
|
||||
if ( $term && isset( $term->term_id ) ) {
|
||||
$prompt = (string) get_term_meta( $term->term_id, 'groq_ai_term_custom_prompt', true );
|
||||
}
|
||||
|
||||
$prompt = trim( $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 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 str_word_count( $text );
|
||||
}
|
||||
|
||||
public function handle_generate_text() {
|
||||
if ( ! current_user_can( 'edit_products' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', 'groq-ai-product-text' ) ], 403 );
|
||||
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'groq_ai_generate', 'nonce' );
|
||||
@@ -36,7 +425,25 @@ class Groq_AI_Ajax_Controller {
|
||||
$system_prompt = $prompt_builder->build_system_prompt( $settings, $conversation_id );
|
||||
$model = $this->plugin->get_selected_model( $provider, $settings );
|
||||
$context_fields = $prompt_builder->parse_context_fields_from_request( isset( $_POST['context_fields'] ) ? $_POST['context_fields'] : '', $settings );
|
||||
if ( array_key_exists( 'attribute_includes', $_POST ) ) {
|
||||
$attribute_includes = [];
|
||||
$attribute_raw = (string) wp_unslash( $_POST['attribute_includes'] );
|
||||
$decoded = json_decode( $attribute_raw, true );
|
||||
if ( is_array( $decoded ) ) {
|
||||
foreach ( $decoded as $value ) {
|
||||
$key = sanitize_key( (string) $value );
|
||||
if ( '' === $key ) {
|
||||
continue;
|
||||
}
|
||||
if ( in_array( $key, [ '__custom__', '__all__' ], true ) || 0 === strpos( $key, 'pa_' ) ) {
|
||||
$attribute_includes[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
$settings['product_attribute_includes'] = array_values( array_unique( $attribute_includes ) );
|
||||
}
|
||||
$image_context_mode = $this->plugin->get_image_context_mode( $settings );
|
||||
$image_context_limit = $this->plugin->get_image_context_limit( $settings );
|
||||
|
||||
if ( 'none' === $image_context_mode ) {
|
||||
$context_fields['images'] = false;
|
||||
@@ -44,7 +451,8 @@ class Groq_AI_Ajax_Controller {
|
||||
|
||||
$image_context_enabled = ! empty( $context_fields['images'] );
|
||||
$use_base64_payloads = $image_context_enabled && 'base64' === $image_context_mode && $provider->supports_image_context();
|
||||
$image_context_count = $image_context_enabled ? $prompt_builder->get_product_image_count( $post_id ) : 0;
|
||||
$total_image_count = $image_context_enabled ? $prompt_builder->get_product_image_count( $post_id ) : 0;
|
||||
$image_context_count = $image_context_enabled ? min( $image_context_limit, $total_image_count ) : 0;
|
||||
|
||||
$prompt_image_mode = 'none';
|
||||
if ( $image_context_enabled ) {
|
||||
@@ -59,17 +467,19 @@ class Groq_AI_Ajax_Controller {
|
||||
}
|
||||
}
|
||||
|
||||
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode );
|
||||
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode, $image_context_limit, $settings );
|
||||
$image_context_payloads = [];
|
||||
if ( $use_base64_payloads ) {
|
||||
$image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id );
|
||||
$image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id, $image_context_limit );
|
||||
}
|
||||
$prompt_with_context = $prompt_builder->prepend_context_to_prompt( $prompt, $product_context_text );
|
||||
|
||||
$image_context_meta = [
|
||||
'requested_mode' => $image_context_mode,
|
||||
'effective_mode' => $prompt_image_mode,
|
||||
'available' => $image_context_count,
|
||||
'limit' => $image_context_limit,
|
||||
'available' => $total_image_count,
|
||||
'used' => $image_context_count,
|
||||
'base64_sent' => $use_base64_payloads ? count( $image_context_payloads ) : 0,
|
||||
];
|
||||
|
||||
@@ -160,7 +570,7 @@ class Groq_AI_Ajax_Controller {
|
||||
|
||||
public function handle_refresh_models() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Geen toestemming.', 'groq-ai-product-text' ) ], 403 );
|
||||
wp_send_json_error( [ 'message' => __( 'Geen toestemming.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'groq_ai_refresh_models', 'nonce' );
|
||||
@@ -169,13 +579,13 @@ class Groq_AI_Ajax_Controller {
|
||||
$api_key = isset( $_POST['apiKey'] ) ? sanitize_text_field( wp_unslash( $_POST['apiKey'] ) ) : '';
|
||||
|
||||
if ( empty( $provider_key ) || empty( $api_key ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Provider en API-sleutel zijn verplicht.', 'groq-ai-product-text' ) ], 400 );
|
||||
wp_send_json_error( [ 'message' => __( 'Provider en API-sleutel zijn verplicht.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
|
||||
}
|
||||
|
||||
$provider = $this->plugin->get_provider_manager()->get_provider( $provider_key );
|
||||
|
||||
if ( ! $provider || ! $provider->supports_live_models() ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Deze aanbieder ondersteunt het ophalen van modellen niet.', 'groq-ai-product-text' ) ], 400 );
|
||||
wp_send_json_error( [ 'message' => __( 'Deze aanbieder ondersteunt het ophalen van modellen niet.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
|
||||
}
|
||||
|
||||
$result = $provider->fetch_live_models( $api_key );
|
||||
|
||||
@@ -16,7 +16,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
||||
public function fetch_live_models( $api_key ) {
|
||||
$endpoint = $this->get_models_endpoint();
|
||||
if ( empty( $endpoint ) ) {
|
||||
return new WP_Error( 'groq_ai_models_endpoint_missing', __( 'Geen model-endpoint beschikbaar voor deze aanbieder.', 'groq-ai-product-text' ) );
|
||||
return new WP_Error( 'groq_ai_models_endpoint_missing', __( 'Geen model-endpoint beschikbaar voor deze aanbieder.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$response = wp_remote_get(
|
||||
@@ -41,7 +41,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
||||
}
|
||||
|
||||
if ( empty( $body['data'] ) || ! is_array( $body['data'] ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$models = [];
|
||||
@@ -52,7 +52,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
||||
}
|
||||
|
||||
if ( empty( $models ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
return $models;
|
||||
@@ -66,7 +66,7 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
||||
$api_key = $this->get_api_key( $settings );
|
||||
|
||||
if ( empty( $api_key ) ) {
|
||||
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', 'groq-ai-product-text' ), $this->get_label() ) );
|
||||
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() ) );
|
||||
}
|
||||
|
||||
$messages = [
|
||||
@@ -80,11 +80,20 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
||||
],
|
||||
];
|
||||
|
||||
$max_tokens = isset( $args['max_tokens'] ) ? absint( $args['max_tokens'] ) : 0;
|
||||
if ( $max_tokens <= 0 ) {
|
||||
$max_tokens = isset( $settings['max_output_tokens'] ) ? absint( $settings['max_output_tokens'] ) : 0;
|
||||
}
|
||||
if ( $max_tokens <= 0 ) {
|
||||
$max_tokens = 2048;
|
||||
}
|
||||
$max_tokens = max( 128, min( 8192, $max_tokens ) );
|
||||
|
||||
$request_body = [
|
||||
'model' => $model,
|
||||
'messages' => $messages,
|
||||
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
|
||||
'max_tokens' => 1024,
|
||||
'max_tokens' => $max_tokens,
|
||||
];
|
||||
|
||||
if ( ! empty( $args['response_format'] ) ) {
|
||||
@@ -116,12 +125,16 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
||||
if ( empty( $body['choices'][0]['message']['content'] ) ) {
|
||||
return new WP_Error(
|
||||
'groq_ai_empty_response',
|
||||
sprintf( __( 'Geen antwoord ontvangen van %s.', 'groq-ai-product-text' ), $this->get_label() )
|
||||
sprintf( __( 'Geen antwoord ontvangen van %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() )
|
||||
);
|
||||
}
|
||||
|
||||
$content = trim( $body['choices'][0]['message']['content'] );
|
||||
$usage = isset( $body['usage'] ) && is_array( $body['usage'] ) ? $body['usage'] : [];
|
||||
$finish_reason = isset( $body['choices'][0]['finish_reason'] ) ? sanitize_text_field( (string) $body['choices'][0]['finish_reason'] ) : '';
|
||||
if ( '' !== $finish_reason ) {
|
||||
$usage['finish_reason'] = $finish_reason;
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
|
||||
@@ -6,7 +6,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return __( 'Google AI (Gemini)', 'groq-ai-product-text' );
|
||||
return __( 'Google AI (Gemini)', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
}
|
||||
|
||||
public function get_default_model() {
|
||||
@@ -61,7 +61,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
}
|
||||
|
||||
if ( empty( $body['models'] ) || ! is_array( $body['models'] ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$models = [];
|
||||
@@ -73,7 +73,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
}
|
||||
|
||||
if ( empty( $models ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
return $models;
|
||||
@@ -87,7 +87,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
$api_key = isset( $settings[ $this->get_option_key() ] ) ? $settings[ $this->get_option_key() ] : '';
|
||||
|
||||
if ( empty( $api_key ) ) {
|
||||
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', 'groq-ai-product-text' ), $this->get_label() ) );
|
||||
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() ) );
|
||||
}
|
||||
|
||||
$endpoint = add_query_arg(
|
||||
@@ -123,7 +123,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
$parts[] = [
|
||||
'text' => sprintf(
|
||||
/* translators: %s: image label */
|
||||
__( 'Contextafbeelding: %s', 'groq-ai-product-text' ),
|
||||
__( 'Contextafbeelding: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$label
|
||||
),
|
||||
];
|
||||
@@ -144,6 +144,15 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
];
|
||||
}
|
||||
|
||||
$max_tokens = isset( $args['max_tokens'] ) ? absint( $args['max_tokens'] ) : 0;
|
||||
if ( $max_tokens <= 0 ) {
|
||||
$max_tokens = isset( $settings['max_output_tokens'] ) ? absint( $settings['max_output_tokens'] ) : 0;
|
||||
}
|
||||
if ( $max_tokens <= 0 ) {
|
||||
$max_tokens = 2048;
|
||||
}
|
||||
$max_tokens = max( 128, min( 8192, $max_tokens ) );
|
||||
|
||||
$payload = [
|
||||
'contents' => [
|
||||
[
|
||||
@@ -153,7 +162,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
],
|
||||
'generationConfig' => [
|
||||
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
|
||||
'maxOutputTokens' => 1024,
|
||||
'maxOutputTokens' => $max_tokens,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -181,7 +190,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
if ( empty( $body['candidates'][0]['content']['parts'] ) ) {
|
||||
return new WP_Error(
|
||||
'groq_ai_empty_response',
|
||||
sprintf( __( 'Geen antwoord ontvangen van %s.', 'groq-ai-product-text' ), $this->get_label() )
|
||||
sprintf( __( 'Geen antwoord ontvangen van %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $this->get_label() )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,6 +205,10 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
|
||||
$content = trim( implode( "\n\n", array_filter( $texts ) ) );
|
||||
$usage = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : [];
|
||||
$finish_reason = isset( $body['candidates'][0]['finishReason'] ) ? sanitize_text_field( (string) $body['candidates'][0]['finishReason'] ) : '';
|
||||
if ( '' !== $finish_reason ) {
|
||||
$usage['finish_reason'] = $finish_reason;
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
|
||||
@@ -6,7 +6,7 @@ class Groq_AI_Provider_Groq extends Groq_AI_Abstract_OpenAI_Provider {
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return __( 'Groq', 'groq-ai-product-text' );
|
||||
return __( 'Groq', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
}
|
||||
|
||||
public function get_default_model() {
|
||||
|
||||
@@ -6,7 +6,7 @@ class Groq_AI_Provider_OpenAI extends Groq_AI_Abstract_OpenAI_Provider {
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return __( 'OpenAI', 'groq-ai-product-text' );
|
||||
return __( 'OpenAI', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
}
|
||||
|
||||
public function get_default_model() {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Google_Analytics_Data_Client {
|
||||
/** @var Groq_AI_Google_OAuth_Client */
|
||||
private $oauth;
|
||||
|
||||
public function __construct( Groq_AI_Google_OAuth_Client $oauth ) {
|
||||
$this->oauth = $oauth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple connectivity check for GA4 Data API.
|
||||
*
|
||||
* @param array $settings
|
||||
* @param string $property_id
|
||||
* @param string $start_date
|
||||
* @param string $end_date
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_property_sessions_summary( $settings, $property_id, $start_date, $end_date ) {
|
||||
$property_id = trim( (string) $property_id );
|
||||
if ( '' === $property_id ) {
|
||||
return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$token = $this->oauth->get_access_token( $settings );
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport';
|
||||
$body = [
|
||||
'dateRanges' => [
|
||||
[
|
||||
'startDate' => $start_date,
|
||||
'endDate' => $end_date,
|
||||
],
|
||||
],
|
||||
'metrics' => [
|
||||
[ 'name' => 'sessions' ],
|
||||
[ 'name' => 'engagedSessions' ],
|
||||
],
|
||||
'limit' => 1,
|
||||
];
|
||||
|
||||
$response = wp_remote_post(
|
||||
$endpoint,
|
||||
[
|
||||
'timeout' => 20,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode( $body ),
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$raw_body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( (string) $raw_body, true );
|
||||
|
||||
if ( 200 !== $status_code || ! is_array( $data ) ) {
|
||||
return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
|
||||
$sessions = 0;
|
||||
$engaged = 0;
|
||||
foreach ( $rows as $row ) {
|
||||
$metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : [];
|
||||
if ( isset( $metric_values[0]['value'] ) ) {
|
||||
$sessions += absint( $metric_values[0]['value'] );
|
||||
}
|
||||
if ( isset( $metric_values[1]['value'] ) ) {
|
||||
$engaged += absint( $metric_values[1]['value'] );
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'sessions' => $sessions,
|
||||
'engagedSessions' => $engaged,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns approximate GA4 sessions for a landing page path.
|
||||
*
|
||||
* @param array $settings
|
||||
* @param string $property_id
|
||||
* @param string $page_path e.g. /product-category/foo/
|
||||
* @param string $start_date YYYY-MM-DD
|
||||
* @param string $end_date YYYY-MM-DD
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date ) {
|
||||
$property_id = trim( (string) $property_id );
|
||||
$page_path = trim( (string) $page_path );
|
||||
|
||||
if ( '' === $property_id || '' === $page_path ) {
|
||||
return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID of page path ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$token = $this->oauth->get_access_token( $settings );
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport';
|
||||
|
||||
$body = [
|
||||
'dateRanges' => [
|
||||
[
|
||||
'startDate' => $start_date,
|
||||
'endDate' => $end_date,
|
||||
],
|
||||
],
|
||||
'dimensions' => [
|
||||
[ 'name' => 'landingPagePlusQueryString' ],
|
||||
],
|
||||
'metrics' => [
|
||||
[ 'name' => 'sessions' ],
|
||||
[ 'name' => 'engagedSessions' ],
|
||||
],
|
||||
'dimensionFilter' => [
|
||||
'filter' => [
|
||||
'fieldName' => 'landingPagePlusQueryString',
|
||||
'stringFilter' => [
|
||||
'matchType' => 'CONTAINS',
|
||||
'value' => $page_path,
|
||||
],
|
||||
],
|
||||
],
|
||||
'limit' => 5,
|
||||
];
|
||||
|
||||
$response = wp_remote_post(
|
||||
$endpoint,
|
||||
[
|
||||
'timeout' => 20,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode( $body ),
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$raw_body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( (string) $raw_body, true );
|
||||
|
||||
if ( 200 !== $status_code || ! is_array( $data ) ) {
|
||||
return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
|
||||
$sessions = 0;
|
||||
$engaged = 0;
|
||||
foreach ( $rows as $row ) {
|
||||
$metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : [];
|
||||
if ( isset( $metric_values[0]['value'] ) ) {
|
||||
$sessions += absint( $metric_values[0]['value'] );
|
||||
}
|
||||
if ( isset( $metric_values[1]['value'] ) ) {
|
||||
$engaged += absint( $metric_values[1]['value'] );
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'sessions' => $sessions,
|
||||
'engagedSessions' => $engaged,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Google_Context_Builder {
|
||||
/** @var Groq_AI_Google_Search_Console_Client */
|
||||
private $gsc;
|
||||
|
||||
/** @var Groq_AI_Google_Analytics_Data_Client */
|
||||
private $ga;
|
||||
|
||||
public function __construct( Groq_AI_Google_Search_Console_Client $gsc, Groq_AI_Google_Analytics_Data_Client $ga ) {
|
||||
$this->gsc = $gsc;
|
||||
$this->ga = $ga;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $existing
|
||||
* @param WP_Term $term
|
||||
* @param array $settings
|
||||
* @return string
|
||||
*/
|
||||
public function build_term_google_context( $existing, $term, $settings ) {
|
||||
if ( ! $term || ! is_object( $term ) ) {
|
||||
return (string) $existing;
|
||||
}
|
||||
|
||||
$enabled_gsc = ! empty( $settings['google_enable_gsc'] );
|
||||
$enabled_ga = ! empty( $settings['google_enable_ga'] );
|
||||
|
||||
if ( ! $enabled_gsc && ! $enabled_ga ) {
|
||||
return (string) $existing;
|
||||
}
|
||||
|
||||
$term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
|
||||
$taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
|
||||
|
||||
$range_days = 28;
|
||||
$end_date = gmdate( 'Y-m-d' );
|
||||
$start_date = gmdate( 'Y-m-d', time() - ( $range_days * DAY_IN_SECONDS ) );
|
||||
|
||||
$term_link = get_term_link( $term );
|
||||
if ( is_wp_error( $term_link ) ) {
|
||||
$term_link = '';
|
||||
}
|
||||
|
||||
$page_path = '';
|
||||
if ( is_string( $term_link ) && '' !== $term_link ) {
|
||||
$parts = wp_parse_url( $term_link );
|
||||
if ( is_array( $parts ) && isset( $parts['path'] ) ) {
|
||||
$page_path = (string) $parts['path'];
|
||||
}
|
||||
}
|
||||
|
||||
$cache_key = 'groq_ai_google_term_ctx_' . md5( $taxonomy . '|' . $term_id . '|' . $start_date . '|' . $end_date );
|
||||
$cached = get_transient( $cache_key );
|
||||
if ( is_string( $cached ) && '' !== $cached ) {
|
||||
return trim( (string) $existing . "\n\n" . $cached );
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
$lines[] = sprintf(
|
||||
/* translators: %d: days */
|
||||
__( 'Google data (laatste %d dagen):', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$range_days
|
||||
);
|
||||
|
||||
if ( $enabled_gsc ) {
|
||||
$site_url = isset( $settings['google_gsc_site_url'] ) ? trim( (string) $settings['google_gsc_site_url'] ) : '';
|
||||
if ( '' !== $site_url && '' !== $term_link ) {
|
||||
$queries = $this->gsc->get_top_queries_for_page( $settings, $site_url, $term_link, $start_date, $end_date, 10 );
|
||||
if ( is_wp_error( $queries ) ) {
|
||||
$lines[] = __( 'Search Console: kon queries niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
} elseif ( empty( $queries ) ) {
|
||||
$lines[] = __( 'Search Console: geen query data gevonden voor deze pagina.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
} else {
|
||||
$lines[] = __( 'Search Console top zoekopdrachten (query → clicks/impr):', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
foreach ( $queries as $row ) {
|
||||
$q = isset( $row['query'] ) ? (string) $row['query'] : '';
|
||||
$c = isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0;
|
||||
$i = isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0;
|
||||
if ( '' === $q ) {
|
||||
continue;
|
||||
}
|
||||
$lines[] = sprintf( '- %s → %d/%d', $q, (int) round( $c ), (int) round( $i ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $enabled_ga ) {
|
||||
$property_id = isset( $settings['google_ga4_property_id'] ) ? trim( (string) $settings['google_ga4_property_id'] ) : '';
|
||||
if ( '' !== $property_id && '' !== $page_path ) {
|
||||
$stats = $this->ga->get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date );
|
||||
if ( is_wp_error( $stats ) ) {
|
||||
$lines[] = __( 'Analytics: kon sessies niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
} else {
|
||||
$sessions = isset( $stats['sessions'] ) ? absint( $stats['sessions'] ) : 0;
|
||||
$engaged = isset( $stats['engagedSessions'] ) ? absint( $stats['engagedSessions'] ) : 0;
|
||||
$lines[] = sprintf( __( 'Analytics (GA4): sessies ~%1$d, engaged sessies ~%2$d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $sessions, $engaged );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we only have the header, skip.
|
||||
if ( count( $lines ) <= 1 ) {
|
||||
return (string) $existing;
|
||||
}
|
||||
|
||||
$context = implode( "\n", $lines );
|
||||
set_transient( $cache_key, $context, 15 * MINUTE_IN_SECONDS );
|
||||
|
||||
return trim( (string) $existing . "\n\n" . $context );
|
||||
}
|
||||
}
|
||||
101
includes/Services/Google/class-groq-ai-google-oauth-client.php
Normal file
101
includes/Services/Google/class-groq-ai-google-oauth-client.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Google_OAuth_Client {
|
||||
/**
|
||||
* @param array $settings
|
||||
* @return string|WP_Error
|
||||
*/
|
||||
public function get_access_token( $settings ) {
|
||||
$client_id = isset( $settings['google_oauth_client_id'] ) ? trim( (string) $settings['google_oauth_client_id'] ) : '';
|
||||
$client_secret = isset( $settings['google_oauth_client_secret'] ) ? trim( (string) $settings['google_oauth_client_secret'] ) : '';
|
||||
$refresh_token = isset( $settings['google_oauth_refresh_token'] ) ? trim( (string) $settings['google_oauth_refresh_token'] ) : '';
|
||||
|
||||
if ( '' === $client_id || '' === $client_secret || '' === $refresh_token ) {
|
||||
return new WP_Error( 'groq_ai_google_oauth_missing', __( 'Google OAuth is niet (volledig) geconfigureerd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$cache_key = 'groq_ai_google_access_token_' . md5( $client_id . '|' . $refresh_token );
|
||||
$cached = get_transient( $cache_key );
|
||||
if ( is_string( $cached ) && '' !== $cached ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$response = wp_remote_post(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
[
|
||||
'timeout' => 20,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
],
|
||||
'body' => [
|
||||
'client_id' => $client_id,
|
||||
'client_secret' => $client_secret,
|
||||
'refresh_token' => $refresh_token,
|
||||
'grant_type' => 'refresh_token',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( (string) $body, true );
|
||||
|
||||
if ( 200 !== $status_code || ! is_array( $data ) ) {
|
||||
return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Google token refresh mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$access_token = isset( $data['access_token'] ) ? sanitize_text_field( (string) $data['access_token'] ) : '';
|
||||
$expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0;
|
||||
|
||||
if ( '' === $access_token ) {
|
||||
return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Geen access token ontvangen van Google.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$ttl = max( 60, $expires_in - 60 );
|
||||
set_transient( $cache_key, $access_token, $ttl );
|
||||
|
||||
return $access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics helper: returns scopes for a given access token.
|
||||
*
|
||||
* @param string $access_token
|
||||
* @return array|WP_Error { 'scope' => string, 'expires_in' => int }
|
||||
*/
|
||||
public function get_access_token_info( $access_token ) {
|
||||
$access_token = trim( (string) $access_token );
|
||||
if ( '' === $access_token ) {
|
||||
return new WP_Error( 'groq_ai_google_tokeninfo_missing', __( 'Geen access token om te inspecteren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$response = wp_remote_get(
|
||||
add_query_arg( [ 'access_token' => $access_token ], 'https://oauth2.googleapis.com/tokeninfo' ),
|
||||
[ 'timeout' => 15 ]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( (string) $body, true );
|
||||
|
||||
if ( 200 !== $status_code || ! is_array( $data ) ) {
|
||||
return new WP_Error( 'groq_ai_google_tokeninfo_failed', __( 'Kon tokeninfo niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$scope = isset( $data['scope'] ) ? trim( (string) $data['scope'] ) : '';
|
||||
$expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0;
|
||||
|
||||
return [
|
||||
'scope' => $scope,
|
||||
'expires_in' => $expires_in,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Google_Search_Console_Client {
|
||||
/** @var Groq_AI_Google_OAuth_Client */
|
||||
private $oauth;
|
||||
|
||||
public function __construct( Groq_AI_Google_OAuth_Client $oauth ) {
|
||||
$this->oauth = $oauth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $status_code
|
||||
* @param string $raw_body
|
||||
* @return WP_Error
|
||||
*/
|
||||
private function build_http_error( $status_code, $raw_body ) {
|
||||
$status_code = absint( $status_code );
|
||||
$raw_body = (string) $raw_body;
|
||||
|
||||
$message = __( 'Search Console API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
$details = '';
|
||||
|
||||
$data = json_decode( $raw_body, true );
|
||||
if ( is_array( $data ) ) {
|
||||
// Google APIs often respond with: { error: { code, message, status, details/errors } }
|
||||
$err = isset( $data['error'] ) && is_array( $data['error'] ) ? $data['error'] : [];
|
||||
$google_message = isset( $err['message'] ) ? trim( (string) $err['message'] ) : '';
|
||||
$google_status = isset( $err['status'] ) ? trim( (string) $err['status'] ) : '';
|
||||
if ( '' !== $google_status || '' !== $google_message ) {
|
||||
$details = trim( $google_status . ( $google_status && $google_message ? ': ' : '' ) . $google_message );
|
||||
}
|
||||
}
|
||||
|
||||
if ( '' !== $details ) {
|
||||
$message = sprintf(
|
||||
/* translators: 1: HTTP status, 2: details */
|
||||
__( 'Search Console API call mislukt (HTTP %1$d): %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$status_code,
|
||||
$details
|
||||
);
|
||||
} else {
|
||||
$message = sprintf(
|
||||
/* translators: %d: HTTP status */
|
||||
__( 'Search Console API call mislukt (HTTP %d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$status_code
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error( 'groq_ai_gsc_error', $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $settings
|
||||
* @return array|WP_Error Array of siteUrl strings.
|
||||
*/
|
||||
public function list_sites( $settings ) {
|
||||
$token = $this->oauth->get_access_token( $settings );
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$response = wp_remote_get(
|
||||
'https://searchconsole.googleapis.com/webmasters/v3/sites',
|
||||
[
|
||||
'timeout' => 20,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$raw_body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( (string) $raw_body, true );
|
||||
|
||||
if ( 200 !== $status_code || ! is_array( $data ) ) {
|
||||
return $this->build_http_error( $status_code, $raw_body );
|
||||
}
|
||||
|
||||
$entries = isset( $data['siteEntry'] ) && is_array( $data['siteEntry'] ) ? $data['siteEntry'] : [];
|
||||
$sites = [];
|
||||
foreach ( $entries as $entry ) {
|
||||
if ( ! is_array( $entry ) ) {
|
||||
continue;
|
||||
}
|
||||
$site_url = isset( $entry['siteUrl'] ) ? trim( (string) $entry['siteUrl'] ) : '';
|
||||
if ( '' !== $site_url ) {
|
||||
$sites[] = $site_url;
|
||||
}
|
||||
}
|
||||
|
||||
$sites = array_values( array_unique( $sites ) );
|
||||
sort( $sites, SORT_NATURAL | SORT_FLAG_CASE );
|
||||
|
||||
return $sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $settings
|
||||
* @param string $site_url
|
||||
* @param string $page_url
|
||||
* @param string $start_date YYYY-MM-DD
|
||||
* @param string $end_date YYYY-MM-DD
|
||||
* @param int $limit
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_top_queries_for_page( $settings, $site_url, $page_url, $start_date, $end_date, $limit = 10 ) {
|
||||
$site_url = trim( (string) $site_url );
|
||||
$page_url = trim( (string) $page_url );
|
||||
$limit = max( 1, min( 25, absint( $limit ) ) );
|
||||
|
||||
if ( '' === $site_url || '' === $page_url ) {
|
||||
return new WP_Error( 'groq_ai_gsc_missing', __( 'Search Console site URL of pagina URL ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$token = $this->oauth->get_access_token( $settings );
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$endpoint = 'https://searchconsole.googleapis.com/webmasters/v3/sites/' . rawurlencode( $site_url ) . '/searchAnalytics/query';
|
||||
|
||||
$body = [
|
||||
'startDate' => $start_date,
|
||||
'endDate' => $end_date,
|
||||
'dimensions' => [ 'query' ],
|
||||
'rowLimit' => $limit,
|
||||
'dimensionFilterGroups' => [
|
||||
[
|
||||
'filters' => [
|
||||
[
|
||||
'dimension' => 'page',
|
||||
'operator' => 'equals',
|
||||
'expression' => $page_url,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'aggregationType' => 'auto',
|
||||
];
|
||||
|
||||
$response = wp_remote_post(
|
||||
$endpoint,
|
||||
[
|
||||
'timeout' => 20,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode( $body ),
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$raw_body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( (string) $raw_body, true );
|
||||
|
||||
if ( 200 !== $status_code || ! is_array( $data ) ) {
|
||||
return $this->build_http_error( $status_code, $raw_body );
|
||||
}
|
||||
|
||||
$rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
|
||||
$result = [];
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
if ( ! is_array( $row ) ) {
|
||||
continue;
|
||||
}
|
||||
$keys = isset( $row['keys'] ) && is_array( $row['keys'] ) ? $row['keys'] : [];
|
||||
$query = isset( $keys[0] ) ? sanitize_text_field( (string) $keys[0] ) : '';
|
||||
if ( '' === $query ) {
|
||||
continue;
|
||||
}
|
||||
$result[] = [
|
||||
'query' => $query,
|
||||
'clicks' => isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0,
|
||||
'impressions' => isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0,
|
||||
'ctr' => isset( $row['ctr'] ) ? (float) $row['ctr'] : 0.0,
|
||||
'position' => isset( $row['position'] ) ? (float) $row['position'] : 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -13,22 +13,187 @@ class Groq_AI_Prompt_Builder {
|
||||
|
||||
public function build_system_prompt( $settings, $conversation_id ) {
|
||||
$context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : '';
|
||||
$base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft overtuigende productbeschrijvingen.', 'groq-ai-product-text' );
|
||||
$base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft overtuigende productbeschrijvingen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
|
||||
if ( $context ) {
|
||||
$base_instruction = sprintf(
|
||||
__( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende context indien beschikbaar: %s', 'groq-ai-product-text' ),
|
||||
__( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende context indien beschikbaar: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
__( 'Conversatie-ID: %1$s. %2$s', 'groq-ai-product-text' ),
|
||||
__( 'Conversatie-ID: %1$s. %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$conversation_id,
|
||||
$base_instruction
|
||||
);
|
||||
}
|
||||
|
||||
public function build_term_system_prompt( $settings, $conversation_id, $term ) {
|
||||
$context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : '';
|
||||
$term_name = is_object( $term ) && isset( $term->name ) ? (string) $term->name : '';
|
||||
$base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft SEO-vriendelijke categorie- en merkpagina teksten.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
|
||||
if ( $context ) {
|
||||
$base_instruction = sprintf(
|
||||
__( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende winkelcontext indien beschikbaar: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
if ( '' !== $term_name ) {
|
||||
$base_instruction .= ' ' . sprintf(
|
||||
__( 'Je schrijft nu voor de term: %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$term_name
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
__( 'Conversatie-ID: %1$s. %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$conversation_id,
|
||||
$base_instruction
|
||||
);
|
||||
}
|
||||
|
||||
private function detect_brand_taxonomy() {
|
||||
$candidates = [
|
||||
'product_brand',
|
||||
'pwb-brand',
|
||||
'yith_product_brand',
|
||||
'berocket_brand',
|
||||
];
|
||||
|
||||
if ( taxonomy_exists( 'pa_brand' ) ) {
|
||||
array_unshift( $candidates, 'pa_brand' );
|
||||
}
|
||||
|
||||
$candidates = apply_filters( 'groq_ai_brand_taxonomy_candidates', $candidates );
|
||||
$found = '';
|
||||
foreach ( $candidates as $tax ) {
|
||||
$tax = sanitize_key( (string) $tax );
|
||||
if ( $tax && taxonomy_exists( $tax ) ) {
|
||||
$found = $tax;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$found = apply_filters( 'groq_ai_brand_taxonomy', $found );
|
||||
return sanitize_key( (string) $found );
|
||||
}
|
||||
|
||||
private function get_internal_link_suggestions( $taxonomy, $current_term_id, $limit = 10 ) {
|
||||
$taxonomy = sanitize_key( (string) $taxonomy );
|
||||
$current_term_id = absint( $current_term_id );
|
||||
$limit = max( 0, min( 50, absint( $limit ) ) );
|
||||
if ( '' === $taxonomy || $limit <= 0 || ! taxonomy_exists( $taxonomy ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cache_key = 'groq_ai_internal_links_' . $taxonomy;
|
||||
$cached = get_transient( $cache_key );
|
||||
if ( is_array( $cached ) ) {
|
||||
$all = $cached;
|
||||
} else {
|
||||
$terms = get_terms(
|
||||
[
|
||||
'taxonomy' => $taxonomy,
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
'number' => 0,
|
||||
]
|
||||
);
|
||||
if ( is_wp_error( $terms ) ) {
|
||||
$terms = [];
|
||||
}
|
||||
|
||||
$all = [];
|
||||
foreach ( (array) $terms as $t ) {
|
||||
if ( ! $t || ! is_object( $t ) || empty( $t->term_id ) ) {
|
||||
continue;
|
||||
}
|
||||
$link = get_term_link( $t );
|
||||
if ( is_wp_error( $link ) || ! is_string( $link ) || '' === $link ) {
|
||||
continue;
|
||||
}
|
||||
$name = isset( $t->name ) ? trim( wp_strip_all_tags( (string) $t->name ) ) : '';
|
||||
if ( '' === $name ) {
|
||||
continue;
|
||||
}
|
||||
$all[] = [
|
||||
'term_id' => absint( $t->term_id ),
|
||||
'name' => $name,
|
||||
'url' => esc_url_raw( $link ),
|
||||
];
|
||||
}
|
||||
|
||||
set_transient( $cache_key, $all, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
$suggestions = [];
|
||||
foreach ( $all as $row ) {
|
||||
if ( ! is_array( $row ) ) {
|
||||
continue;
|
||||
}
|
||||
$tid = isset( $row['term_id'] ) ? absint( $row['term_id'] ) : 0;
|
||||
if ( $current_term_id && $tid === $current_term_id ) {
|
||||
continue;
|
||||
}
|
||||
$name = isset( $row['name'] ) ? (string) $row['name'] : '';
|
||||
$url = isset( $row['url'] ) ? (string) $row['url'] : '';
|
||||
if ( '' === $name || '' === $url ) {
|
||||
continue;
|
||||
}
|
||||
$suggestions[] = [
|
||||
'name' => $name,
|
||||
'url' => $url,
|
||||
];
|
||||
if ( count( $suggestions ) >= $limit ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
}
|
||||
|
||||
private function build_internal_links_context( $term ) {
|
||||
if ( ! $term || ! is_object( $term ) ) {
|
||||
return '';
|
||||
}
|
||||
$current_tax = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
|
||||
$current_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
|
||||
|
||||
$links = [];
|
||||
|
||||
// Categories.
|
||||
if ( taxonomy_exists( 'product_cat' ) ) {
|
||||
$links = array_merge( $links, $this->get_internal_link_suggestions( 'product_cat', 'product_cat' === $current_tax ? $current_id : 0, 10 ) );
|
||||
}
|
||||
|
||||
// Brands.
|
||||
$brand_tax = $this->detect_brand_taxonomy();
|
||||
if ( '' !== $brand_tax ) {
|
||||
$links = array_merge( $links, $this->get_internal_link_suggestions( $brand_tax, $brand_tax === $current_tax ? $current_id : 0, 10 ) );
|
||||
}
|
||||
|
||||
if ( empty( $links ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
$lines[] = __( 'Interne links (gebruik 2–5 relevante links in de tekst, als HTML: <a href="URL">Anker</a>):', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
foreach ( $links as $link ) {
|
||||
$name = isset( $link['name'] ) ? (string) $link['name'] : '';
|
||||
$url = isset( $link['url'] ) ? (string) $link['url'] : '';
|
||||
if ( '' === $name || '' === $url ) {
|
||||
continue;
|
||||
}
|
||||
$lines[] = sprintf( '- %s → %s', $name, $url );
|
||||
}
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
|
||||
public function append_response_instructions( $prompt, $settings ) {
|
||||
$instructions = (string) ( $this->get_structured_response_instructions( $settings ) ?? '' );
|
||||
$prompt = trim( (string) $prompt );
|
||||
@@ -46,7 +211,7 @@ class Groq_AI_Prompt_Builder {
|
||||
|
||||
public function parse_structured_response( $raw, $settings = null ) {
|
||||
if ( empty( $raw ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', 'groq-ai-product-text' ) );
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$clean = trim( $raw );
|
||||
@@ -58,7 +223,7 @@ class Groq_AI_Prompt_Builder {
|
||||
$decoded = json_decode( $clean, true );
|
||||
|
||||
if ( ! is_array( $decoded ) ) {
|
||||
return new WP_Error( 'groq_ai_parse_error', __( 'Kon de AI-respons niet als JSON lezen. Probeer het opnieuw.', 'groq-ai-product-text' ) );
|
||||
return new WP_Error( 'groq_ai_parse_error', __( 'Kon de AI-respons niet als JSON lezen. Probeer het opnieuw.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$fields = [
|
||||
@@ -67,6 +232,40 @@ class Groq_AI_Prompt_Builder {
|
||||
'description' => trim( (string) ( $decoded['description'] ?? '' ) ),
|
||||
];
|
||||
|
||||
$title_suggestions = [];
|
||||
if ( isset( $decoded['title_suggestions'] ) && is_array( $decoded['title_suggestions'] ) ) {
|
||||
foreach ( $decoded['title_suggestions'] as $suggestion ) {
|
||||
$suggestion = sanitize_text_field( (string) $suggestion );
|
||||
$suggestion = trim( preg_replace( '/\s+/', ' ', $suggestion ) );
|
||||
|
||||
if ( '' === $suggestion ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$title_suggestions[] = $suggestion;
|
||||
|
||||
if ( count( $title_suggestions ) >= 3 ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $title_suggestions ) && '' !== $fields['title'] ) {
|
||||
$title_suggestions[] = $fields['title'];
|
||||
}
|
||||
|
||||
if ( '' === $fields['title'] && ! empty( $title_suggestions ) ) {
|
||||
$fields['title'] = $title_suggestions[0];
|
||||
}
|
||||
|
||||
$fields['title_suggestions'] = $title_suggestions;
|
||||
|
||||
$slug_value = isset( $decoded['slug'] ) ? sanitize_title( $decoded['slug'] ) : '';
|
||||
if ( '' === $slug_value && '' !== $fields['title'] ) {
|
||||
$slug_value = sanitize_title( $fields['title'] );
|
||||
}
|
||||
$fields['slug'] = $slug_value;
|
||||
|
||||
if ( $this->settings_manager->is_module_enabled( 'rankmath', $settings ) ) {
|
||||
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
|
||||
$focus_keywords = [];
|
||||
@@ -98,8 +297,22 @@ class Groq_AI_Prompt_Builder {
|
||||
$fields['focus_keywords'] = implode( ', ', $focus_keywords );
|
||||
}
|
||||
|
||||
if ( implode( '', $fields ) === '' ) {
|
||||
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen bruikbare velden.', 'groq-ai-product-text' ) );
|
||||
$primary_values = [
|
||||
$fields['title'],
|
||||
$fields['short_description'],
|
||||
$fields['description'],
|
||||
];
|
||||
|
||||
$has_primary_content = false;
|
||||
foreach ( $primary_values as $value ) {
|
||||
if ( '' !== trim( (string) $value ) ) {
|
||||
$has_primary_content = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $has_primary_content ) {
|
||||
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen bruikbare velden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
return $fields;
|
||||
@@ -125,7 +338,7 @@ class Groq_AI_Prompt_Builder {
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
public function build_product_context_block( $post_id, $fields, $image_mode = 'url' ) {
|
||||
public function build_product_context_block( $post_id, $fields, $image_mode = 'url', $image_limit = 3, $settings = null ) {
|
||||
$post_id = absint( $post_id );
|
||||
|
||||
if ( ! $post_id ) {
|
||||
@@ -137,41 +350,104 @@ class Groq_AI_Prompt_Builder {
|
||||
if ( ! empty( $fields['title'] ) ) {
|
||||
$title = get_the_title( $post_id );
|
||||
if ( $title ) {
|
||||
$parts[] = sprintf( __( 'Titel: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $title ) );
|
||||
$parts[] = sprintf( __( 'Titel: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( $title ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['short_description'] ) ) {
|
||||
$excerpt = get_post_field( 'post_excerpt', $post_id );
|
||||
if ( $excerpt ) {
|
||||
$parts[] = sprintf( __( 'Korte beschrijving: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $excerpt ) );
|
||||
$parts[] = sprintf( __( 'Korte beschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( $excerpt ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['description'] ) ) {
|
||||
$content = get_post_field( 'post_content', $post_id );
|
||||
if ( $content ) {
|
||||
$parts[] = sprintf( __( 'Beschrijving: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $content ) );
|
||||
$parts[] = sprintf( __( 'Beschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( $content ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['attributes'] ) ) {
|
||||
$attributes = $this->get_product_attributes_text( $post_id );
|
||||
$attribute_includes = [];
|
||||
if ( is_array( $settings ) && isset( $settings['product_attribute_includes'] ) && is_array( $settings['product_attribute_includes'] ) ) {
|
||||
$attribute_includes = array_values( array_unique( array_map( 'sanitize_key', $settings['product_attribute_includes'] ) ) );
|
||||
}
|
||||
|
||||
$include_attributes = ! empty( $attribute_includes ) || ! empty( $fields['attributes'] );
|
||||
if ( $include_attributes ) {
|
||||
$attributes = $this->get_product_attributes_text( $post_id, $attribute_includes );
|
||||
if ( $attributes ) {
|
||||
$parts[] = sprintf( __( 'Attributen: %s', 'groq-ai-product-text' ), $attributes );
|
||||
$parts[] = sprintf( __( 'Attributen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $attributes );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['images'] ) && 'url' === $image_mode ) {
|
||||
$images = $this->get_product_images_text( $post_id );
|
||||
$images = $this->get_product_images_text( $post_id, $image_limit );
|
||||
if ( $images ) {
|
||||
$parts[] = sprintf( __( 'Afbeeldingen: %s', 'groq-ai-product-text' ), $images );
|
||||
$parts[] = sprintf( __( 'Afbeeldingen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $images );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['brands'] ) ) {
|
||||
$brands_context = $this->get_product_brand_context_text( $post_id );
|
||||
if ( '' !== $brands_context ) {
|
||||
$parts[] = sprintf( __( 'Merken: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $brands_context );
|
||||
}
|
||||
}
|
||||
|
||||
return implode( "\n\n", array_filter( $parts ) );
|
||||
}
|
||||
|
||||
private function get_product_brand_context_text( $post_id ) {
|
||||
$post_id = absint( $post_id );
|
||||
$taxonomy = $this->detect_brand_taxonomy();
|
||||
|
||||
if ( ! $post_id || '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$terms = get_the_terms( $post_id, $taxonomy );
|
||||
if ( empty( $terms ) || is_wp_error( $terms ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $terms as $term ) {
|
||||
if ( ! $term || ! is_object( $term ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = isset( $term->name ) ? trim( wp_strip_all_tags( (string) $term->name ) ) : '';
|
||||
if ( '' === $name ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$description = isset( $term->description ) ? trim( wp_strip_all_tags( (string) $term->description ) ) : '';
|
||||
if ( '' !== $description ) {
|
||||
$entries[] = sprintf( '%s - %s', $name, $description );
|
||||
} else {
|
||||
$entries[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$entries = array_values( array_unique( array_filter( $entries ) ) );
|
||||
if ( empty( $entries ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = implode( '; ', $entries );
|
||||
|
||||
/**
|
||||
* Filters the product brand context string added to prompts.
|
||||
*
|
||||
* @param string $context
|
||||
* @param int $post_id
|
||||
* @param array $terms
|
||||
* @param string $taxonomy
|
||||
*/
|
||||
return (string) apply_filters( 'groq_ai_product_brand_context', $context, $post_id, $terms, $taxonomy );
|
||||
}
|
||||
|
||||
public function prepend_context_to_prompt( $prompt, $context ) {
|
||||
$context = trim( (string) $context );
|
||||
|
||||
@@ -179,31 +455,120 @@ class Groq_AI_Prompt_Builder {
|
||||
return $prompt;
|
||||
}
|
||||
|
||||
$intro = __( 'Gebruik de volgende productcontext bij het schrijven:', 'groq-ai-product-text' );
|
||||
$intro = __( 'Gebruik de volgende productcontext bij het schrijven:', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
|
||||
return $intro . "\n" . $context . "\n\n" . $prompt;
|
||||
}
|
||||
|
||||
public function get_response_format_definition( $settings = null ) {
|
||||
public function build_term_context_block( $term, $options = [], $settings = null ) {
|
||||
if ( ! $term || ! is_object( $term ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
|
||||
$term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
|
||||
if ( '' === $taxonomy || ! $term_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$include_top_products = ! empty( $options['include_top_products'] );
|
||||
$top_products_limit = isset( $options['top_products_limit'] ) ? absint( $options['top_products_limit'] ) : 10;
|
||||
$top_products_limit = max( 1, min( 25, $top_products_limit ) );
|
||||
|
||||
$parts = [];
|
||||
$parts[] = sprintf( __( 'Term: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->name ) );
|
||||
if ( isset( $term->slug ) && '' !== (string) $term->slug ) {
|
||||
$parts[] = sprintf( __( 'Slug: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), sanitize_title( (string) $term->slug ) );
|
||||
}
|
||||
if ( isset( $term->count ) ) {
|
||||
$parts[] = sprintf( __( 'Aantal producten: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (string) absint( $term->count ) );
|
||||
}
|
||||
if ( isset( $term->description ) && '' !== trim( (string) $term->description ) ) {
|
||||
$parts[] = sprintf( __( 'Huidige omschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->description ) );
|
||||
}
|
||||
|
||||
$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';
|
||||
if ( '' !== $bottom_meta_key && $term_id ) {
|
||||
$bottom = (string) get_term_meta( $term_id, $bottom_meta_key, true );
|
||||
$bottom = trim( wp_strip_all_tags( $bottom ) );
|
||||
if ( '' !== $bottom ) {
|
||||
$parts[] = sprintf( __( 'Huidige omschrijving (onderaan): %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $bottom );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $include_top_products ) {
|
||||
$top_products = $this->get_top_products_for_term( $taxonomy, $term_id, $top_products_limit );
|
||||
if ( ! empty( $top_products ) ) {
|
||||
$lines = [];
|
||||
foreach ( $top_products as $product_row ) {
|
||||
$lines[] = sprintf( '- %s', $product_row );
|
||||
}
|
||||
$parts[] = __( 'Top verkochte producten (indicatief):', GROQ_AI_PRODUCT_TEXT_DOMAIN ) . "\n" . implode( "\n", $lines );
|
||||
}
|
||||
}
|
||||
|
||||
$internal_links = $this->build_internal_links_context( $term );
|
||||
$internal_links = trim( (string) $internal_links );
|
||||
if ( '' !== $internal_links ) {
|
||||
$parts[] = $internal_links;
|
||||
}
|
||||
|
||||
$google_context = apply_filters( 'groq_ai_term_google_context', '', $term, $settings );
|
||||
$google_context = trim( (string) $google_context );
|
||||
if ( '' !== $google_context ) {
|
||||
$parts[] = $google_context;
|
||||
}
|
||||
|
||||
return implode( "\n\n", array_filter( $parts ) );
|
||||
}
|
||||
|
||||
public function prepend_term_context_to_prompt( $prompt, $context ) {
|
||||
$context = trim( (string) $context );
|
||||
if ( '' === $context ) {
|
||||
return $prompt;
|
||||
}
|
||||
$intro = __( 'Gebruik de volgende categorie/term-context bij het schrijven:', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
return $intro . "\n" . $context . "\n\n" . $prompt;
|
||||
}
|
||||
|
||||
public function get_term_response_format_definition( $settings = null ) {
|
||||
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
|
||||
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
|
||||
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
|
||||
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
|
||||
$top_char_range = $this->get_char_limit_range_values( $this->settings_manager->get_term_top_description_char_limit( $settings ) );
|
||||
$bottom_char_range = $this->get_char_limit_range_values( $this->settings_manager->get_term_bottom_description_char_limit( $settings ) );
|
||||
|
||||
$top_description_text = __( 'Korte HTML-omschrijving (1 alinea) voor de standaard WordPress term description. Exact één alinea in <p>-tags.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
if ( $top_char_range ) {
|
||||
$top_description_text .= ' ' . sprintf(
|
||||
__( 'Doel: ~%1$d tekens (±10%% ⇒ %2$d–%3$d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$top_char_range['limit'],
|
||||
$top_char_range['min'],
|
||||
$top_char_range['max']
|
||||
);
|
||||
}
|
||||
|
||||
$bottom_description_text = __( 'Uitgebreide HTML-omschrijving (helemaal onderaan), 2–4 alinea’s, met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
if ( $bottom_char_range ) {
|
||||
$bottom_description_text .= ' ' . sprintf(
|
||||
__( 'Doel: ~%1$d tekens (±10%% ⇒ %2$d–%3$d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$bottom_char_range['limit'],
|
||||
$bottom_char_range['min'],
|
||||
$bottom_char_range['max']
|
||||
);
|
||||
}
|
||||
|
||||
$properties = [
|
||||
'title' => [
|
||||
'top_description' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Korte, overtuigende producttitel in het Nederlands.', 'groq-ai-product-text' ),
|
||||
'minLength' => 3,
|
||||
'description' => $top_description_text,
|
||||
'minLength' => 20,
|
||||
],
|
||||
'short_description' => [
|
||||
'bottom_description' => [
|
||||
'type' => 'string',
|
||||
'description' => __( "Korte HTML-beschrijving in <p>-tags (maximaal 2 alinea's).", 'groq-ai-product-text' ),
|
||||
'minLength' => 10,
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Uitgebreide HTML-productbeschrijving met paragrafen en eventueel lijsten.', 'groq-ai-product-text' ),
|
||||
'description' => $bottom_description_text,
|
||||
'minLength' => 20,
|
||||
],
|
||||
];
|
||||
@@ -212,8 +577,7 @@ class Groq_AI_Prompt_Builder {
|
||||
$properties['meta_title'] = [
|
||||
'type' => 'string',
|
||||
'description' => sprintf(
|
||||
/* translators: 1: maximum character count, 2: maximum pixels */
|
||||
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
|
||||
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
60,
|
||||
$title_pixels
|
||||
),
|
||||
@@ -222,8 +586,7 @@ class Groq_AI_Prompt_Builder {
|
||||
$properties['meta_description'] = [
|
||||
'type' => 'string',
|
||||
'description' => sprintf(
|
||||
/* translators: 1: maximum character count, 2: maximum pixels */
|
||||
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
|
||||
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
160,
|
||||
$desc_pixels
|
||||
),
|
||||
@@ -231,7 +594,7 @@ class Groq_AI_Prompt_Builder {
|
||||
];
|
||||
$properties['focus_keywords'] = [
|
||||
'type' => 'array',
|
||||
'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'maxItems' => max( 1, $keyword_limit ),
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
@@ -243,7 +606,281 @@ class Groq_AI_Prompt_Builder {
|
||||
$schema = [
|
||||
'type' => 'object',
|
||||
'properties' => $properties,
|
||||
'required' => [ 'title', 'short_description', 'description' ],
|
||||
'required' => [ 'top_description', 'bottom_description' ],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
|
||||
return [
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => [
|
||||
'name' => 'groq_ai_term_text',
|
||||
'schema' => $schema,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function append_term_response_instructions( $prompt, $settings ) {
|
||||
$instructions = (string) ( $this->get_term_structured_response_instructions( $settings ) ?? '' );
|
||||
$prompt = trim( (string) $prompt );
|
||||
if ( '' === $instructions ) {
|
||||
return $prompt;
|
||||
}
|
||||
if ( false !== strpos( $prompt, $instructions ) ) {
|
||||
return $prompt;
|
||||
}
|
||||
return $prompt . "\n\n" . $instructions;
|
||||
}
|
||||
|
||||
public function parse_term_structured_response( $raw, $settings = null ) {
|
||||
if ( empty( $raw ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$clean = trim( (string) $raw );
|
||||
if ( preg_match( '/```(?:json)?\s*(.*?)```/is', $clean, $matches ) ) {
|
||||
$clean = trim( $matches[1] );
|
||||
}
|
||||
|
||||
$decoded = json_decode( $clean, true );
|
||||
if ( ! is_array( $decoded ) ) {
|
||||
// Fallback: treat as plain text.
|
||||
return [
|
||||
'description' => trim( (string) $raw ),
|
||||
];
|
||||
}
|
||||
|
||||
$top = isset( $decoded['top_description'] ) ? trim( (string) $decoded['top_description'] ) : '';
|
||||
$bottom = isset( $decoded['bottom_description'] ) ? trim( (string) $decoded['bottom_description'] ) : '';
|
||||
// Backward compatibility: older prompts only returned `description`.
|
||||
if ( '' === $top && isset( $decoded['description'] ) ) {
|
||||
$top = trim( (string) $decoded['description'] );
|
||||
}
|
||||
if ( '' === $top && '' === $bottom ) {
|
||||
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen top_description/bottom_description velden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||
}
|
||||
|
||||
$result = [];
|
||||
if ( '' !== $top ) {
|
||||
$result['top_description'] = $top;
|
||||
// For backwards compatibility with existing UI, keep `description` alias.
|
||||
$result['description'] = $top;
|
||||
}
|
||||
if ( '' !== $bottom ) {
|
||||
$result['bottom_description'] = $bottom;
|
||||
}
|
||||
|
||||
if ( isset( $decoded['meta_title'] ) ) {
|
||||
$result['meta_title'] = $this->truncate_meta_field( (string) $decoded['meta_title'], 60 );
|
||||
}
|
||||
if ( isset( $decoded['meta_description'] ) ) {
|
||||
$result['meta_description'] = $this->truncate_meta_field( (string) $decoded['meta_description'], 160 );
|
||||
}
|
||||
if ( isset( $decoded['focus_keywords'] ) ) {
|
||||
if ( is_array( $decoded['focus_keywords'] ) ) {
|
||||
$keywords = [];
|
||||
foreach ( $decoded['focus_keywords'] as $kw ) {
|
||||
$kw = trim( (string) $kw );
|
||||
if ( '' !== $kw ) {
|
||||
$keywords[] = $kw;
|
||||
}
|
||||
}
|
||||
$keywords = array_values( array_unique( $keywords ) );
|
||||
$result['focus_keywords'] = implode( ', ', $keywords );
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function get_term_structured_response_instructions( $settings = null ) {
|
||||
$schema_parts = [
|
||||
'"top_description":"..."',
|
||||
'"bottom_description":"..."',
|
||||
];
|
||||
$top_char_range = $this->get_char_limit_range_values( $this->settings_manager->get_term_top_description_char_limit( $settings ) );
|
||||
$bottom_char_range = $this->get_char_limit_range_values( $this->settings_manager->get_term_bottom_description_char_limit( $settings ) );
|
||||
|
||||
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
|
||||
if ( $rankmath_enabled ) {
|
||||
$schema_parts[] = '"meta_title":"..."';
|
||||
$schema_parts[] = '"meta_description":"..."';
|
||||
$schema_parts[] = '"focus_keywords":["...","..."]';
|
||||
}
|
||||
|
||||
$json_structure = '{' . implode( ',', $schema_parts ) . '}';
|
||||
|
||||
$instruction = sprintf(
|
||||
__( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \n voor regeleinden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$json_structure
|
||||
);
|
||||
|
||||
$instruction .= ' ' . __( 'Zorg dat top_description en bottom_description geldige HTML bevatten. top_description moet exact één alinea zijn in <p>-tags. bottom_description moet 2–4 alinea’s bevatten.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
$instruction .= ' ' . __( 'Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
$instruction .= ' ' . __( 'Als in de context een sectie "Interne links" staat, verwerk dan 2–5 van deze links natuurlijk in bottom_description als HTML-links (<a href="URL">Anker</a>).', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
if ( $top_char_range ) {
|
||||
$instruction .= ' ' . sprintf(
|
||||
__( 'Houd top_description rond %1$d tekens en blijf tussen %2$d en %3$d tekens (±10%% marge).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$top_char_range['limit'],
|
||||
$top_char_range['min'],
|
||||
$top_char_range['max']
|
||||
);
|
||||
}
|
||||
if ( $bottom_char_range ) {
|
||||
$instruction .= ' ' . sprintf(
|
||||
__( 'Houd bottom_description rond %1$d tekens en blijf tussen %2$d en %3$d tekens (±10%% marge).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$bottom_char_range['limit'],
|
||||
$bottom_char_range['min'],
|
||||
$bottom_char_range['max']
|
||||
);
|
||||
}
|
||||
return $instruction;
|
||||
}
|
||||
|
||||
private function get_char_limit_range_values( $limit ) {
|
||||
$limit = absint( $limit );
|
||||
|
||||
if ( $limit <= 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$min = (int) floor( $limit * 0.9 );
|
||||
$max = (int) ceil( $limit * 1.1 );
|
||||
|
||||
return [
|
||||
'limit' => $limit,
|
||||
'min' => max( 1, $min ),
|
||||
'max' => max( $min, $max ),
|
||||
];
|
||||
}
|
||||
|
||||
private function resolve_term_bottom_description_meta_key( $term = null, $settings = null ) {
|
||||
$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 );
|
||||
return sanitize_key( (string) $key );
|
||||
}
|
||||
|
||||
private function get_top_products_for_term( $taxonomy, $term_id, $limit = 10 ) {
|
||||
$taxonomy = sanitize_key( (string) $taxonomy );
|
||||
$term_id = absint( $term_id );
|
||||
$limit = max( 1, min( 25, absint( $limit ) ) );
|
||||
|
||||
$query = new WP_Query(
|
||||
[
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $limit,
|
||||
'no_found_rows' => true,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => '_stock_status',
|
||||
'value' => 'instock',
|
||||
'compare' => '=',
|
||||
],
|
||||
],
|
||||
'tax_query' => [
|
||||
[
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => [ $term_id ],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$rows = [];
|
||||
if ( $query->have_posts() ) {
|
||||
foreach ( $query->posts as $post ) {
|
||||
$title = isset( $post->post_title ) ? wp_strip_all_tags( (string) $post->post_title ) : '';
|
||||
$rows[] = $title;
|
||||
}
|
||||
}
|
||||
wp_reset_postdata();
|
||||
|
||||
return array_values( array_filter( $rows ) );
|
||||
}
|
||||
|
||||
public function get_response_format_definition( $settings = null ) {
|
||||
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
|
||||
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
|
||||
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
|
||||
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
|
||||
|
||||
$properties = [
|
||||
'title_suggestions' => [
|
||||
'type' => 'array',
|
||||
'description' => __( 'Exact drie korte producttitelvoorstellen in het Nederlands. Kies de beste ook als title.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'minItems' => 3,
|
||||
'maxItems' => 3,
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'minLength' => 3,
|
||||
'maxLength' => 120,
|
||||
],
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Korte, overtuigende producttitel in het Nederlands.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'minLength' => 3,
|
||||
],
|
||||
'slug' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Productslug voor de URL (alleen kleine letters, cijfers en koppeltekens).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'minLength' => 3,
|
||||
'pattern' => '^[a-z0-9\\-]+$',
|
||||
],
|
||||
'short_description' => [
|
||||
'type' => 'string',
|
||||
'description' => __( "Korte HTML-beschrijving in <p>-tags (maximaal 2 alinea's).", GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'minLength' => 10,
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Uitgebreide HTML-productbeschrijving met paragrafen en eventueel lijsten.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'minLength' => 20,
|
||||
],
|
||||
];
|
||||
|
||||
if ( $rankmath_enabled ) {
|
||||
$properties['meta_title'] = [
|
||||
'type' => 'string',
|
||||
'description' => sprintf(
|
||||
/* translators: 1: maximum character count, 2: maximum pixels */
|
||||
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
60,
|
||||
$title_pixels
|
||||
),
|
||||
'maxLength' => 120,
|
||||
];
|
||||
$properties['meta_description'] = [
|
||||
'type' => 'string',
|
||||
'description' => sprintf(
|
||||
/* translators: 1: maximum character count, 2: maximum pixels */
|
||||
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
160,
|
||||
$desc_pixels
|
||||
),
|
||||
'maxLength' => 320,
|
||||
];
|
||||
$properties['focus_keywords'] = [
|
||||
'type' => 'array',
|
||||
'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'maxItems' => max( 1, $keyword_limit ),
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'minLength' => 1,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$schema = [
|
||||
'type' => 'object',
|
||||
'properties' => $properties,
|
||||
'required' => [ 'title_suggestions', 'title', 'slug', 'short_description', 'description' ],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
|
||||
@@ -258,7 +895,9 @@ class Groq_AI_Prompt_Builder {
|
||||
|
||||
private function get_structured_response_instructions( $settings = null ) {
|
||||
$schema_parts = [
|
||||
'"title_suggestions":["...","...","..."]',
|
||||
'"title":"..."',
|
||||
'"slug":"..."',
|
||||
'"short_description":"..."',
|
||||
'"description":"..."',
|
||||
];
|
||||
@@ -274,7 +913,7 @@ class Groq_AI_Prompt_Builder {
|
||||
|
||||
$instruction = sprintf(
|
||||
/* translators: %s: JSON structure example */
|
||||
__( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \\n voor regeleinden. Zorg dat zowel short_description als description nooit leeg zijn.', 'groq-ai-product-text' ),
|
||||
__( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \\n voor regeleinden. Zorg dat zowel short_description als description nooit leeg zijn.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$json_structure
|
||||
);
|
||||
|
||||
@@ -284,14 +923,16 @@ class Groq_AI_Prompt_Builder {
|
||||
$desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
|
||||
$instruction .= ' ' . sprintf(
|
||||
/* translators: 1: focus keyword limit, 2: meta title pixel limit, 3: meta description pixel limit */
|
||||
__( 'Beperk meta_title tot maximaal 60 tekens en %2$d pixels en meta_description tot maximaal 160 tekens en %3$d pixels. Lever maximaal %1$d focuskeywords in het focus_keywords-array (korte termen zonder hashtag of extra tekst).', 'groq-ai-product-text' ),
|
||||
__( 'Beperk meta_title tot maximaal 60 tekens en %2$d pixels en meta_description tot maximaal 160 tekens en %3$d pixels. Lever maximaal %1$d focuskeywords in het focus_keywords-array (korte termen zonder hashtag of extra tekst).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
$keyword_limit,
|
||||
$title_pixels,
|
||||
$desc_pixels
|
||||
);
|
||||
}
|
||||
|
||||
$instruction .= ' ' . __( 'Zorg dat short_description en description geldige HTML bevatten (gebruik minimaal <p>-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', 'groq-ai-product-text' );
|
||||
$instruction .= ' ' . __( 'Lever exact drie verschillende titelvoorstellen in title_suggestions en kopieer de beste keuze naar title.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
$instruction .= ' ' . __( 'Zorg dat short_description en description geldige HTML bevatten (gebruik minimaal <p>-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
$instruction .= ' ' . __( 'Maak de slug URL-vriendelijk, gebruik alleen kleine letters, cijfers en koppeltekens en geen spaties.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||
|
||||
return $instruction;
|
||||
}
|
||||
@@ -318,7 +959,7 @@ class Groq_AI_Prompt_Builder {
|
||||
return substr( $text, 0, $limit );
|
||||
}
|
||||
|
||||
private function get_product_attributes_text( $post_id ) {
|
||||
private function get_product_attributes_text( $post_id, $attribute_includes = [] ) {
|
||||
if ( ! function_exists( 'wc_get_product' ) ) {
|
||||
return '';
|
||||
}
|
||||
@@ -335,14 +976,27 @@ class Groq_AI_Prompt_Builder {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attribute_includes = is_array( $attribute_includes ) ? array_values( array_unique( array_map( 'sanitize_key', $attribute_includes ) ) ) : [];
|
||||
$include_all = empty( $attribute_includes ) || in_array( '__all__', $attribute_includes, true );
|
||||
$include_custom = $include_all || in_array( '__custom__', $attribute_includes, true );
|
||||
|
||||
$lines = [];
|
||||
|
||||
foreach ( $attributes as $attribute ) {
|
||||
if ( $attribute->is_taxonomy() ) {
|
||||
$terms = wc_get_product_terms( $post_id, $attribute->get_name(), [ 'fields' => 'names' ] );
|
||||
$taxonomy_name = sanitize_key( (string) $attribute->get_name() );
|
||||
if ( ! $include_all && ! in_array( $taxonomy_name, $attribute_includes, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$terms = wc_get_product_terms( $post_id, $taxonomy_name, [ 'fields' => 'names' ] );
|
||||
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $terms ) );
|
||||
$label = wc_attribute_label( $attribute->get_name() );
|
||||
$label = wc_attribute_label( $taxonomy_name );
|
||||
} else {
|
||||
if ( ! $include_custom ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options = $attribute->get_options();
|
||||
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $options ) );
|
||||
$label = sanitize_text_field( $attribute->get_name() );
|
||||
@@ -358,9 +1012,14 @@ class Groq_AI_Prompt_Builder {
|
||||
return implode( '; ', $lines );
|
||||
}
|
||||
|
||||
private function get_product_images_text( $post_id ) {
|
||||
private function get_product_images_text( $post_id, $limit = 3 ) {
|
||||
$limit = max( 0, (int) $limit );
|
||||
$image_ids = $this->get_product_image_ids( $post_id );
|
||||
|
||||
if ( $limit > 0 ) {
|
||||
$image_ids = array_slice( $image_ids, 0, $limit );
|
||||
}
|
||||
|
||||
if ( empty( $image_ids ) ) {
|
||||
return '';
|
||||
}
|
||||
@@ -380,7 +1039,13 @@ class Groq_AI_Prompt_Builder {
|
||||
}
|
||||
|
||||
public function get_product_image_payloads( $post_id, $limit = 3, $max_filesize = 1572864 ) {
|
||||
$image_ids = array_slice( $this->get_product_image_ids( $post_id ), 0, max( 1, (int) $limit ) );
|
||||
$limit = max( 0, (int) $limit );
|
||||
|
||||
if ( $limit <= 0 ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$image_ids = array_slice( $this->get_product_image_ids( $post_id ), 0, $limit );
|
||||
|
||||
if ( empty( $image_ids ) ) {
|
||||
return [];
|
||||
@@ -476,7 +1141,7 @@ class Groq_AI_Prompt_Builder {
|
||||
$label = trim( wp_strip_all_tags( (string) $label ) );
|
||||
|
||||
if ( '' === $label ) {
|
||||
$label = sprintf( __( 'Afbeelding %d', 'groq-ai-product-text' ), $position );
|
||||
$label = sprintf( __( 'Afbeelding %d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $position );
|
||||
}
|
||||
|
||||
$path = get_attached_file( $attachment_id );
|
||||
|
||||
@@ -32,13 +32,28 @@ class Groq_AI_Settings_Manager {
|
||||
'model' => '',
|
||||
'store_context' => '',
|
||||
'default_prompt' => '',
|
||||
'max_output_tokens' => 2048,
|
||||
'product_attribute_includes' => [],
|
||||
'term_bottom_description_meta_key' => '',
|
||||
'groq_api_key' => '',
|
||||
'openai_api_key' => '',
|
||||
'google_api_key' => '',
|
||||
'google_oauth_client_id' => '',
|
||||
'google_oauth_client_secret' => '',
|
||||
'google_oauth_refresh_token' => '',
|
||||
'google_oauth_connected_email' => '',
|
||||
'google_oauth_connected_at' => 0,
|
||||
'google_enable_gsc' => true,
|
||||
'google_enable_ga' => true,
|
||||
'google_gsc_site_url' => '',
|
||||
'google_ga4_property_id' => '',
|
||||
'context_fields' => $this->get_default_context_fields(),
|
||||
'modules' => $this->get_default_modules_settings(),
|
||||
'image_context_mode' => 'url',
|
||||
'image_context_limit' => 3,
|
||||
'response_format_compat' => false,
|
||||
'term_top_description_char_limit' => 600,
|
||||
'term_bottom_description_char_limit' => 1200,
|
||||
];
|
||||
|
||||
$settings = get_option( $this->option_key, [] );
|
||||
@@ -59,6 +74,22 @@ class Groq_AI_Settings_Manager {
|
||||
$settings['image_context_mode'] = 'url';
|
||||
}
|
||||
|
||||
$limit = isset( $settings['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $settings['image_context_limit'] ) : 3;
|
||||
$settings['image_context_limit'] = $limit;
|
||||
|
||||
$settings['product_attribute_includes'] = $this->sanitize_product_attribute_includes(
|
||||
isset( $settings['product_attribute_includes'] ) ? $settings['product_attribute_includes'] : []
|
||||
);
|
||||
|
||||
$settings['term_top_description_char_limit'] = $this->sanitize_term_description_char_limit_value(
|
||||
isset( $settings['term_top_description_char_limit'] ) ? $settings['term_top_description_char_limit'] : $defaults['term_top_description_char_limit'],
|
||||
$defaults['term_top_description_char_limit']
|
||||
);
|
||||
$settings['term_bottom_description_char_limit'] = $this->sanitize_term_description_char_limit_value(
|
||||
isset( $settings['term_bottom_description_char_limit'] ) ? $settings['term_bottom_description_char_limit'] : $defaults['term_bottom_description_char_limit'],
|
||||
$defaults['term_bottom_description_char_limit']
|
||||
);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -74,13 +105,28 @@ class Groq_AI_Settings_Manager {
|
||||
'model' => '',
|
||||
'store_context' => '',
|
||||
'default_prompt' => '',
|
||||
'max_output_tokens' => 2048,
|
||||
'product_attribute_includes' => [],
|
||||
'term_bottom_description_meta_key' => '',
|
||||
'groq_api_key' => '',
|
||||
'openai_api_key' => '',
|
||||
'google_api_key' => '',
|
||||
'google_oauth_client_id' => '',
|
||||
'google_oauth_client_secret' => '',
|
||||
'google_oauth_refresh_token' => '',
|
||||
'google_oauth_connected_email' => '',
|
||||
'google_oauth_connected_at' => 0,
|
||||
'google_enable_gsc' => true,
|
||||
'google_enable_ga' => true,
|
||||
'google_gsc_site_url' => '',
|
||||
'google_ga4_property_id' => '',
|
||||
'context_fields' => $this->get_default_context_fields(),
|
||||
'modules' => $this->get_default_modules_settings(),
|
||||
'image_context_mode' => 'url',
|
||||
'image_context_limit' => 3,
|
||||
'response_format_compat' => false,
|
||||
'term_top_description_char_limit' => 600,
|
||||
'term_bottom_description_char_limit' => 1200,
|
||||
];
|
||||
|
||||
$current_settings = $this->all();
|
||||
@@ -104,6 +150,12 @@ class Groq_AI_Settings_Manager {
|
||||
$image_mode = 'url';
|
||||
}
|
||||
|
||||
$image_limit = isset( $input['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $input['image_context_limit'] ) : $defaults['image_context_limit'];
|
||||
|
||||
$max_output_tokens = isset( $input['max_output_tokens'] ) ? absint( $input['max_output_tokens'] ) : absint( $defaults['max_output_tokens'] );
|
||||
// Keep within sane bounds across providers.
|
||||
$max_output_tokens = max( 128, min( 8192, $max_output_tokens ) );
|
||||
|
||||
$context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] );
|
||||
|
||||
if ( 'none' === $image_mode ) {
|
||||
@@ -112,16 +164,40 @@ class Groq_AI_Settings_Manager {
|
||||
$context_fields['images'] = true;
|
||||
}
|
||||
|
||||
$top_char_limit = $this->sanitize_term_description_char_limit_value(
|
||||
isset( $raw_input['term_top_description_char_limit'] ) ? $raw_input['term_top_description_char_limit'] : $defaults['term_top_description_char_limit'],
|
||||
$defaults['term_top_description_char_limit']
|
||||
);
|
||||
$bottom_char_limit = $this->sanitize_term_description_char_limit_value(
|
||||
isset( $raw_input['term_bottom_description_char_limit'] ) ? $raw_input['term_bottom_description_char_limit'] : $defaults['term_bottom_description_char_limit'],
|
||||
$defaults['term_bottom_description_char_limit']
|
||||
);
|
||||
|
||||
return [
|
||||
'provider' => $provider,
|
||||
'model' => $model,
|
||||
'store_context' => sanitize_textarea_field( $input['store_context'] ),
|
||||
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
|
||||
'max_output_tokens' => $max_output_tokens,
|
||||
'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'] ),
|
||||
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
|
||||
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
|
||||
'google_api_key' => sanitize_text_field( $input['google_api_key'] ),
|
||||
'google_oauth_client_id' => sanitize_text_field( $input['google_oauth_client_id'] ),
|
||||
'google_oauth_client_secret' => sanitize_text_field( $input['google_oauth_client_secret'] ),
|
||||
'google_oauth_refresh_token' => sanitize_text_field( $input['google_oauth_refresh_token'] ),
|
||||
'google_oauth_connected_email' => sanitize_text_field( $input['google_oauth_connected_email'] ),
|
||||
'google_oauth_connected_at' => absint( $input['google_oauth_connected_at'] ),
|
||||
'google_enable_gsc' => ! empty( $raw_input['google_enable_gsc'] ),
|
||||
'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ),
|
||||
'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'] ),
|
||||
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
|
||||
'image_context_mode' => $image_mode,
|
||||
'image_context_limit' => $image_limit,
|
||||
'term_top_description_char_limit' => $top_char_limit,
|
||||
'term_bottom_description_char_limit' => $bottom_char_limit,
|
||||
'context_fields' => $context_fields,
|
||||
'modules' => $this->sanitize_modules_settings(
|
||||
$modules_posted ? $raw_input['modules'] : [],
|
||||
@@ -132,32 +208,80 @@ class Groq_AI_Settings_Manager {
|
||||
];
|
||||
}
|
||||
|
||||
private function sanitize_product_attribute_includes( $value ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
foreach ( $value as $item ) {
|
||||
$item = sanitize_key( (string) $item );
|
||||
if ( '' === $item ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow special tokens and attribute taxonomies.
|
||||
if ( in_array( $item, [ '__all__', '__custom__' ], true ) || 0 === strpos( $item, 'pa_' ) ) {
|
||||
$clean[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$clean = array_values( array_unique( $clean ) );
|
||||
// Hard cap to avoid overly large option payloads.
|
||||
if ( count( $clean ) > 200 ) {
|
||||
$clean = array_slice( $clean, 0, 200 );
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function sanitize_term_description_char_limit_value( $value, $default ) {
|
||||
$default_value = absint( $default ) > 0 ? absint( $default ) : 600;
|
||||
|
||||
if ( null === $value || '' === $value ) {
|
||||
$value = $default_value;
|
||||
}
|
||||
|
||||
$value = absint( $value );
|
||||
|
||||
if ( $value <= 0 ) {
|
||||
$value = $default_value;
|
||||
}
|
||||
|
||||
return max( 100, min( 5000, $value ) );
|
||||
}
|
||||
|
||||
public function get_context_field_definitions() {
|
||||
if ( null === $this->context_field_definitions ) {
|
||||
$this->context_field_definitions = [
|
||||
'title' => [
|
||||
'label' => __( 'Producttitel', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Voeg de huidige producttitel toe als context.', 'groq-ai-product-text' ),
|
||||
'label' => __( 'Producttitel', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'description' => __( 'Voeg de huidige producttitel toe als context.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'default' => true,
|
||||
],
|
||||
'short_description' => [
|
||||
'label' => __( 'Korte beschrijving', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', 'groq-ai-product-text' ),
|
||||
'label' => __( 'Korte beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'default' => true,
|
||||
],
|
||||
'description' => [
|
||||
'label' => __( 'Volledige beschrijving', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', 'groq-ai-product-text' ),
|
||||
'label' => __( 'Volledige beschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'default' => true,
|
||||
],
|
||||
'attributes' => [
|
||||
'label' => __( 'Attributen', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', 'groq-ai-product-text' ),
|
||||
'label' => __( 'Attributen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'default' => false,
|
||||
],
|
||||
'brands' => [
|
||||
'label' => __( 'Merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'description' => __( 'Voegt gekoppelde productmerken toe (detecteert WooCommerce merk-taxonomieën).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'default' => true,
|
||||
],
|
||||
'images' => [
|
||||
'label' => __( 'Afbeeldingen', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', 'groq-ai-product-text' ),
|
||||
'label' => __( 'Afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||
'default' => false,
|
||||
],
|
||||
];
|
||||
@@ -182,7 +306,7 @@ class Groq_AI_Settings_Manager {
|
||||
$normalized = [];
|
||||
|
||||
foreach ( $definitions as $key => $data ) {
|
||||
$normalized[ $key ] = false;
|
||||
$normalized[ $key ] = ! empty( $data['default'] );
|
||||
}
|
||||
|
||||
if ( ! is_array( $fields ) ) {
|
||||
@@ -240,7 +364,7 @@ class Groq_AI_Settings_Manager {
|
||||
$config = $this->get_module_config( 'rankmath', $settings );
|
||||
$limit = isset( $config['focus_keyword_limit'] ) ? absint( $config['focus_keyword_limit'] ) : 3;
|
||||
|
||||
return max( 1, min( 10, $limit ) );
|
||||
return max( 1, min( 100, $limit ) );
|
||||
}
|
||||
|
||||
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
|
||||
@@ -268,6 +392,36 @@ class Groq_AI_Settings_Manager {
|
||||
return in_array( $mode, $allowed_modes, true ) ? $mode : 'url';
|
||||
}
|
||||
|
||||
public function get_image_context_limit( $settings = null ) {
|
||||
if ( null === $settings ) {
|
||||
$settings = $this->all();
|
||||
}
|
||||
|
||||
$limit = isset( $settings['image_context_limit'] ) ? $settings['image_context_limit'] : 3;
|
||||
|
||||
return $this->sanitize_image_context_limit_value( $limit );
|
||||
}
|
||||
|
||||
public function get_term_top_description_char_limit( $settings = null ) {
|
||||
if ( null === $settings ) {
|
||||
$settings = $this->all();
|
||||
}
|
||||
|
||||
$value = isset( $settings['term_top_description_char_limit'] ) ? $settings['term_top_description_char_limit'] : 600;
|
||||
|
||||
return $this->sanitize_term_description_char_limit_value( $value, 600 );
|
||||
}
|
||||
|
||||
public function get_term_bottom_description_char_limit( $settings = null ) {
|
||||
if ( null === $settings ) {
|
||||
$settings = $this->all();
|
||||
}
|
||||
|
||||
$value = isset( $settings['term_bottom_description_char_limit'] ) ? $settings['term_bottom_description_char_limit'] : 1200;
|
||||
|
||||
return $this->sanitize_term_description_char_limit_value( $value, 1200 );
|
||||
}
|
||||
|
||||
public function is_response_format_compat_enabled( $settings = null ) {
|
||||
if ( null === $settings ) {
|
||||
$settings = $this->all();
|
||||
@@ -323,7 +477,7 @@ class Groq_AI_Settings_Manager {
|
||||
if ( $limit <= 0 ) {
|
||||
$limit = $module_default_config['focus_keyword_limit'];
|
||||
}
|
||||
$result[ $module_key ]['focus_keyword_limit'] = max( 1, min( 10, $limit ) );
|
||||
$result[ $module_key ]['focus_keyword_limit'] = max( 1, min( 100, $limit ) );
|
||||
|
||||
$title_pixel_limit = isset( $raw['meta_title_pixel_limit'] ) ? absint( $raw['meta_title_pixel_limit'] ) : ( isset( $current_config['meta_title_pixel_limit'] ) ? absint( $current_config['meta_title_pixel_limit'] ) : $module_default_config['meta_title_pixel_limit'] );
|
||||
if ( $title_pixel_limit <= 0 ) {
|
||||
@@ -341,4 +495,14 @@ class Groq_AI_Settings_Manager {
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function sanitize_image_context_limit_value( $value ) {
|
||||
$limit = absint( $value );
|
||||
|
||||
if ( $limit <= 0 ) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
return min( 10, $limit );
|
||||
}
|
||||
}
|
||||
|
||||
0
languages/.gitkeep
Normal file
0
languages/.gitkeep
Normal file
Reference in New Issue
Block a user