Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cff0b6f58 | |||
| 26aabdb2d8 | |||
| 3e74bcbf3a | |||
| 051db0febc | |||
| 5ddd3f8104 | |||
| 9df41ca85c | |||
| 934cbf0f73 | |||
| 5cc6e869bf | |||
| 79d411f35a | |||
| 58a9b37ccf | |||
| 5b256f1374 | |||
| d878bb7805 | |||
| 43ddbddd11 | |||
| 7b9f26e966 | |||
| 6f488c5c6d |
1
.phpunit.result.cache
Normal file
1
.phpunit.result.cache
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":1,"defects":{"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":3,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":4,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":4,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":4,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":4},"times":{"ModelExclusionsTest::test_ensure_allowed_blocks_excluded_model":0.002,"ModelExclusionsTest::test_filter_models_removes_excluded_entries":0,"SettingsManagerTest::test_logs_retention_days_sanitized_and_capped":0,"SettingsManagerTest::test_logs_retention_days_allows_zero":0,"SettingsManagerTest::test_logs_retention_days_negative_becomes_zero":0,"SettingsManagerTest::test_sanitize_accepts_all_settings_keys":0,"ProviderRequestBuilderTest::test_openai_request_payload_respects_settings":0,"ProviderRequestBuilderTest::test_groq_request_payload_uses_default_model_when_missing":0,"ProviderRequestBuilderTest::test_google_request_payload_builds_schema_and_images":0,"TermSaveTest::test_save_term_generation_result_saves_descriptions_and_filtered_meta_key":0}}
|
||||||
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)
|
# 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.
|
## Installatie & activatie
|
||||||
- WooCommerce (de plugin controleert dit en deactiveert zichzelf als WooCommerce ontbreekt).
|
### Plugin installeren
|
||||||
- Minimaal één API-sleutel voor Groq, OpenAI of Google Gemini.
|
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.
|
||||||
- (Optioneel) Rank Math SEO wanneer je de extra SEO-velden wilt gebruiken.
|
2. Upload via **Plugins → Nieuwe plugin → Plugin uploaden** of plaats de map onder `wp-content/plugins/`.
|
||||||
|
|
||||||
### Installatie
|
|
||||||
|
|
||||||
1. Download de nieuwste release (`siti-ai-product-content-generator-x.y.z.zip`) vanaf de [GitHub Releases](https://github.com/SitiWeb/siti-ai-product-content-generator/releases) of gebruik het zip-bestand dat door de workflow in `dist/` wordt geplaatst.
|
|
||||||
2. Ga in WordPress naar **Plugins → Nieuwe plugin → Plugin uploaden** en upload het zipbestand. Je kunt de map ook handmatig naar `wp-content/plugins/` uploaden.
|
|
||||||
3. Activeer **SitiAI Product Teksten** en controleer dat WooCommerce actief is.
|
3. Activeer **SitiAI Product Teksten** en controleer dat WooCommerce actief is.
|
||||||
|
|
||||||
### Configuratie
|
### Basisconfiguratie
|
||||||
|
1. Ga naar **Instellingen → Siti AI**.
|
||||||
1. Navigeer 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.
|
||||||
2. Kies een AI-aanbieder, vul de bijbehorende API-sleutel in en (optioneel) klik op **Live modellen ophalen** om beschikbare modellen te laden.
|
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.
|
||||||
3. Stel een standaard prompt en winkelcontext in zodat het AI-venster vooraf gevuld is.
|
4. Stel modules (Rank Math) in via **Instellingen → Siti AI → Modules** om keywordlimieten/pixellimieten te wijzigen en de module te activeren/deactiveren.
|
||||||
4. Selecteer welke productvelden standaard als context dienen (titel, beschrijvingen, attributen, …).
|
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.
|
||||||
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. 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%.
|
||||||
6. Via **Bekijk AI-logboek** zie je alle eerdere generaties inclusief foutmeldingen of token usage.
|
|
||||||
|
|
||||||
### Productteksten genereren
|
### 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.
|
### Categorie- en merkteksten
|
||||||
2. Vul (of hergebruik) een prompt, kies welke contextvelden meegestuurd worden en klik op **Genereer tekst**.
|
- Ga naar **Instellingen → Siti AI → Categorieën** of **Merken** om een overzicht te zien met productcounts en woordtellingen. Lege termen worden gemarkeerd.
|
||||||
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.
|
- Bovenaan staat een bulk-paneel dat een achtergrondproces start (`groq_ai_bulk_generate_terms`) voor lege termen; optioneel kun je bestaande teksten forceren.
|
||||||
4. Via de geavanceerde sectie kun je contextvelden tijdelijk uitschakelen; dit heeft alleen effect voor de huidige generatie.
|
- 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.
|
||||||
5. Iedere generatie wordt opgeslagen in het AI-logboek zodat je binnen WordPress kunt terugzoeken wat er is gebeurd.
|
- 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):
|
## 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
|
```bash
|
||||||
docker compose up --build -d
|
docker compose up --build -d
|
||||||
```
|
```
|
||||||
2. Open http://localhost:8080 om de WordPress installatie te doorlopen. Gebruik `db` als host en de volgende databasegegevens:
|
- WordPress: http://localhost:8082
|
||||||
- database: `wordpress`
|
- phpMyAdmin: http://localhost:8085
|
||||||
- gebruiker: `wordpress`
|
- MariaDB: poort 3307 (database, user, wachtwoord = `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
|
### 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`.
|
||||||
|
|
||||||
- Shell in de WordPress container om bijvoorbeeld `wp` CLI of git te draaien binnen de container:
|
### Handige commando’s
|
||||||
```bash
|
- Shell binnen de WordPress-container: `docker compose exec wordpress bash`
|
||||||
docker compose exec wordpress bash
|
- WP-CLI gebruiken: `docker compose exec wordpress wp plugin list`
|
||||||
```
|
- Logs volgen: `docker compose logs -f wordpress`
|
||||||
- WP-CLI is al aanwezig:
|
- Containers stoppen: `docker compose down`
|
||||||
```bash
|
- Helemaal opnieuw beginnen (verwijdert volumes): `docker compose down -v`
|
||||||
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
|
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.
|
||||||
|
|
||||||
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:
|
## 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.
|
||||||
|
|
||||||
```bash
|
## Mappenstructuur
|
||||||
git status
|
- `groq-ai-product-text.php`: hoofdbestand dat services, providers, admin-schermen en hooks initialiseert.
|
||||||
git add .
|
- `includes/Core`: service container, AJAX-controller en model-exclusiehulpen.
|
||||||
git commit -m "Beschrijf je wijziging"
|
- `includes/Admin`: instellingenpagina’s, meta-box UI, logboek en termoverzichten.
|
||||||
git push origin <branch>
|
- `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.
|
||||||
|
|
||||||
Je kunt optioneel vanuit de container git gebruiken (zelfde codepad) wanneer je liever binnen Docker werkt.
|
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.
|
||||||
|
|
||||||
## Tips
|
|
||||||
|
|
||||||
- De databank (`db_data`) en WordPress bestanden (`wordpress_data`) worden in Docker volumes opgeslagen zodat je data behouden blijft tussen sessies.
|
|
||||||
- Wil je helemaal opnieuw beginnen? Voer `docker compose down -v` uit om de volumes te verwijderen.
|
|
||||||
|
|
||||||
## Releasen via GitHub Actions
|
|
||||||
|
|
||||||
De workflow `.github/workflows/release.yml` bouwt automatisch een distributie-zip van de plugin, maakt een git-tag (`vX.Y.Z`) op basis van de versie in `groq-ai-product-text.php` en publiceert een GitHub Release met het zipbestand als asset.
|
|
||||||
|
|
||||||
1. Werk de `Version`-header in `groq-ai-product-text.php` bij en commit de wijzigingen.
|
|
||||||
2. Push naar `main` of start handmatig de workflow **Build & Release Plugin** via **Actions → Run workflow** (optioneel met extra release notes).
|
|
||||||
3. De workflow slaat releases over wanneer een tag met dezelfde versie al bestaat.
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class SitiWebUpdater {
|
|||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->plugin = get_plugin_data( $this->file );
|
$this->plugin = get_plugin_data( $this->file, false, false );
|
||||||
$this->basename = plugin_basename( $this->file );
|
$this->basename = plugin_basename( $this->file );
|
||||||
$this->active = is_plugin_active( $this->basename );
|
$this->active = is_plugin_active( $this->basename );
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,8 @@ class SitiWebUpdater {
|
|||||||
'url' => $this->plugin["PluginURI"],
|
'url' => $this->plugin["PluginURI"],
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'package' => $new_files,
|
'package' => $new_files,
|
||||||
'new_version' => $latest_version
|
'new_version' => $latest_version,
|
||||||
|
'icons' => $this->prepare_icon_set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
$transient->response[$this->basename] = (object) $plugin; // Return it in response
|
$transient->response[$this->basename] = (object) $plugin; // Return it in response
|
||||||
@@ -160,10 +161,11 @@ class SitiWebUpdater {
|
|||||||
'homepage' => $this->plugin["PluginURI"],
|
'homepage' => $this->plugin["PluginURI"],
|
||||||
'short_description' => $this->plugin["Description"],
|
'short_description' => $this->plugin["Description"],
|
||||||
'sections' => array(
|
'sections' => array(
|
||||||
'Description' => $this->plugin["Description"],
|
'description' => wp_kses_post( wpautop( $this->plugin["Description"] ) ),
|
||||||
'Updates' => $this->github_response['body'],
|
'changelog' => $this->get_release_notes_html(),
|
||||||
),
|
),
|
||||||
'download_link' => $this->github_response['zipball_url']
|
'download_link' => $this->github_response['zipball_url'],
|
||||||
|
'icons' => $this->prepare_icon_set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (object) $plugin; // Return the data
|
return (object) $plugin; // Return the data
|
||||||
@@ -199,4 +201,37 @@ class SitiWebUpdater {
|
|||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function get_release_notes_html() {
|
||||||
|
$notes = isset( $this->github_response['body'] ) ? (string) $this->github_response['body'] : '';
|
||||||
|
|
||||||
|
if ( '' === trim( $notes ) ) {
|
||||||
|
return __( 'Nog geen changelog beschikbaar.', 'siti-ai-product-content-generator' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return wp_kses_post( wpautop( $notes ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_plugin_icon_url() {
|
||||||
|
$file = plugin_dir_path( $this->file ) . 'assets/images/plugin-icon.svg';
|
||||||
|
if ( ! file_exists( $file ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins_url( 'assets/images/plugin-icon.svg', $this->file );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepare_icon_set() {
|
||||||
|
$icon_url = $this->get_plugin_icon_url();
|
||||||
|
if ( '' === $icon_url ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'1x' => $icon_url,
|
||||||
|
'2x' => $icon_url,
|
||||||
|
'svg' => $icon_url,
|
||||||
|
'default' => $icon_url,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,3 +29,80 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 8px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
11
assets/images/plugin-icon.svg
Normal file
11
assets/images/plugin-icon.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#6C38FF"/>
|
||||||
|
<stop offset="100%" stop-color="#00C7C7"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="48" fill="url(#g)"/>
|
||||||
|
<path fill="#ffffff" d="M68 176c-8.8 0-16-7.2-16-16v-64c0-8.8 7.2-16 16-16h56c8.8 0 16 7.2 16 16v12h-24v-4c0-3.3-2.7-6-6-6H84c-3.3 0-6 2.7-6 6v44c0 3.3 2.7 6 6 6h26c3.3 0 6-2.7 6-6v-4h24v12c0 8.8-7.2 16-16 16H68zm120-32h-48v-32h48v-16l32 32-32 32v-16z"/>
|
||||||
|
<text x="128" y="210" text-anchor="middle" font-family="'Montserrat', Arial, sans-serif" font-size="28" fill="#ffffff" opacity="0.9">SitiAI</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 735 B |
@@ -13,6 +13,7 @@
|
|||||||
const resultField = document.getElementById('groq-ai-output');
|
const resultField = document.getElementById('groq-ai-output');
|
||||||
const jsonCopyButton = modal.querySelector('.groq-ai-copy-json');
|
const jsonCopyButton = modal.querySelector('.groq-ai-copy-json');
|
||||||
const contextToggles = modal.querySelectorAll('.groq-ai-context-toggle');
|
const contextToggles = modal.querySelectorAll('.groq-ai-context-toggle');
|
||||||
|
const attributeToggles = modal.querySelectorAll('.groq-ai-attribute-toggle');
|
||||||
const resultFields = {};
|
const resultFields = {};
|
||||||
modal.querySelectorAll('.groq-ai-result-field').forEach((field) => {
|
modal.querySelectorAll('.groq-ai-result-field').forEach((field) => {
|
||||||
const key = field.getAttribute('data-field');
|
const key = field.getAttribute('data-field');
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
promptField.value = GroqAIGenerator.defaultPrompt;
|
promptField.value = GroqAIGenerator.defaultPrompt;
|
||||||
}
|
}
|
||||||
resetContextToggles();
|
resetContextToggles();
|
||||||
|
resetAttributeToggles();
|
||||||
setTimeout(() => promptField.focus(), 50);
|
setTimeout(() => promptField.focus(), 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +96,35 @@
|
|||||||
statusField.setAttribute('data-status', type);
|
statusField.setAttribute('data-status', type);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...';
|
const localized = (window.GroqAIGenerator && GroqAIGenerator.strings) || {};
|
||||||
const retryText = window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.';
|
const loadingText = localized.loading || (window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'siti-ai-product-content-generator') : 'AI is bezig met schrijven...');
|
||||||
|
const retryText = localized.retry || (window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'siti-ai-product-content-generator') : 'Probeer het opnieuw of pas je prompt/context aan.');
|
||||||
|
const errorDefaultText = localized.errorDefault || (window.wp && wp.i18n ? wp.i18n.__('Er ging iets mis bij het genereren.', 'siti-ai-product-content-generator') : 'Er ging iets mis bij het genereren.');
|
||||||
|
const errorUnknownText = localized.errorUnknown || (window.wp && wp.i18n ? wp.i18n.__('Onbekende fout.', 'siti-ai-product-content-generator') : 'Onbekende fout.');
|
||||||
|
const successText = localized.success || (window.wp && wp.i18n ? wp.i18n.__('Structuur gegenereerd. Kopieer of vul velden in.', 'siti-ai-product-content-generator') : 'Structuur gegenereerd. Kopieer of vul velden in.');
|
||||||
|
const fieldAppliedText = localized.fieldApplied || (window.wp && wp.i18n ? wp.i18n.__('%s ingevuld.', 'siti-ai-product-content-generator') : '%s ingevuld.');
|
||||||
|
const fieldApplyErrorText = localized.fieldApplyError || (window.wp && wp.i18n ? wp.i18n.__('Kon het veld niet automatisch invullen.', 'siti-ai-product-content-generator') : 'Kon het veld niet automatisch invullen.');
|
||||||
|
const fieldCopiedText = localized.fieldCopied || (window.wp && wp.i18n ? wp.i18n.__('%s gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : '%s gekopieerd naar het klembord.');
|
||||||
|
const jsonCopiedText = localized.jsonCopied || (window.wp && wp.i18n ? wp.i18n.__('JSON gekopieerd naar het klembord.', 'siti-ai-product-content-generator') : 'JSON gekopieerd naar het klembord.');
|
||||||
|
const copyFailedText = localized.copyFailed || (window.wp && wp.i18n ? wp.i18n.__('Kopiëren mislukt.', 'siti-ai-product-content-generator') : 'Kopiëren mislukt.');
|
||||||
|
|
||||||
|
function formatString(template, values) {
|
||||||
|
if (!template) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let autoIndex = 0;
|
||||||
|
return template.replace(/%(\d+\$)?s/g, (match, position) => {
|
||||||
|
let valueIndex;
|
||||||
|
if (position) {
|
||||||
|
valueIndex = parseInt(position, 10) - 1;
|
||||||
|
} else {
|
||||||
|
valueIndex = autoIndex;
|
||||||
|
autoIndex += 1;
|
||||||
|
}
|
||||||
|
const replacement = values[valueIndex];
|
||||||
|
return typeof replacement === 'undefined' ? '' : String(replacement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toggleLoading(isLoading) {
|
function toggleLoading(isLoading) {
|
||||||
modal.classList.toggle('is-loading', isLoading);
|
modal.classList.toggle('is-loading', isLoading);
|
||||||
@@ -115,6 +144,7 @@
|
|||||||
payload.append('prompt', prompt);
|
payload.append('prompt', prompt);
|
||||||
payload.append('post_id', GroqAIGenerator.postId || 0);
|
payload.append('post_id', GroqAIGenerator.postId || 0);
|
||||||
payload.append('context_fields', JSON.stringify(collectContextSelection()));
|
payload.append('context_fields', JSON.stringify(collectContextSelection()));
|
||||||
|
payload.append('attribute_includes', JSON.stringify(collectAttributeSelection()));
|
||||||
|
|
||||||
toggleLoading(true);
|
toggleLoading(true);
|
||||||
resultWrapper.hidden = true;
|
resultWrapper.hidden = true;
|
||||||
@@ -134,7 +164,7 @@
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (!json.success) {
|
if (!json.success) {
|
||||||
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout';
|
const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText;
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,13 +181,12 @@
|
|||||||
if (jsonCopyButton) {
|
if (jsonCopyButton) {
|
||||||
jsonCopyButton.disabled = false;
|
jsonCopyButton.disabled = false;
|
||||||
}
|
}
|
||||||
setStatus('Structuur gegenereerd. Kopieer of vul velden in.', 'success');
|
setStatus(successText, 'success');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const message = error && error.message ? error.message : 'Er ging iets mis bij het genereren.';
|
const message = error && error.message ? error.message : errorDefaultText;
|
||||||
setStatus(loadingText, 'error');
|
const fullMessage = `${errorDefaultText} ${message}. ${retryText}`;
|
||||||
const fullMessage = `${loadingText} ${message}. ${retryText}`;
|
setStatus(fullMessage, 'error');
|
||||||
statusField.textContent = fullMessage;
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
toggleLoading(false);
|
toggleLoading(false);
|
||||||
@@ -305,10 +334,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (applied) {
|
if (applied) {
|
||||||
setStatus(entry.label + ' ingevuld.', 'success');
|
setStatus(formatString(fieldAppliedText, [entry.label]), 'success');
|
||||||
setFieldStatus(fieldKey, 'success');
|
setFieldStatus(fieldKey, 'success');
|
||||||
} else {
|
} else {
|
||||||
setStatus('Kon het veld niet automatisch invullen.', 'error');
|
setStatus(fieldApplyErrorText, 'error');
|
||||||
setFieldStatus(fieldKey, 'error');
|
setFieldStatus(fieldKey, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,10 +366,10 @@
|
|||||||
}
|
}
|
||||||
copyToClipboard(entry.textarea.value)
|
copyToClipboard(entry.textarea.value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setStatus(entry.label + ' gekopieerd naar het klembord.', 'success');
|
setStatus(formatString(fieldCopiedText, [entry.label]), 'success');
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setStatus('Kopiëren mislukt.', 'error');
|
setStatus(copyFailedText, 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,10 +384,10 @@
|
|||||||
const text = resultField ? resultField.textContent.trim() : '';
|
const text = resultField ? resultField.textContent.trim() : '';
|
||||||
copyToClipboard(text)
|
copyToClipboard(text)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setStatus('JSON gekopieerd naar het klembord.', 'success');
|
setStatus(jsonCopiedText, 'success');
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setStatus('Kopiëren mislukt.', 'error');
|
setStatus(copyFailedText, 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -458,6 +487,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() {
|
function collectContextSelection() {
|
||||||
const selected = [];
|
const selected = [];
|
||||||
contextToggles.forEach((toggle) => {
|
contextToggles.forEach((toggle) => {
|
||||||
@@ -467,4 +510,18 @@
|
|||||||
});
|
});
|
||||||
return selected;
|
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);
|
})(jQuery);
|
||||||
|
|||||||
@@ -14,6 +14,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const strings = data.strings || {};
|
||||||
|
const providerUnsupportedText = strings.providerUnsupported || 'Deze aanbieder ondersteunt dit niet.';
|
||||||
|
const apiKeyRequiredText = strings.apiKeyRequired || 'Vul eerst de API-sleutel in.';
|
||||||
|
const loadingModelsText = strings.loadingModels || 'Modellen worden opgehaald…';
|
||||||
|
const errorUnknownText = strings.errorUnknown || 'Onbekende fout';
|
||||||
|
const successModelsText = strings.successModels || 'Modellen bijgewerkt.';
|
||||||
|
const errorFetchText = strings.errorFetch || 'Ophalen mislukt.';
|
||||||
|
|
||||||
const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]');
|
const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]');
|
||||||
const modelSelect = document.getElementById('groq-ai-model-select');
|
const modelSelect = document.getElementById('groq-ai-model-select');
|
||||||
const refreshButton = document.getElementById('groq-ai-refresh-models');
|
const refreshButton = document.getElementById('groq-ai-refresh-models');
|
||||||
@@ -164,19 +172,19 @@
|
|||||||
const provider = providerSelect ? providerSelect.value : data.currentProvider;
|
const provider = providerSelect ? providerSelect.value : data.currentProvider;
|
||||||
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
|
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
|
||||||
if (!providerData || !providerData.supports_live) {
|
if (!providerData || !providerData.supports_live) {
|
||||||
setRefreshStatus('Deze aanbieder ondersteunt dit niet.', 'error');
|
setRefreshStatus(providerUnsupportedText, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyField = document.querySelector('[data-provider-row="' + provider + '"] input');
|
const keyField = document.querySelector('[data-provider-row="' + provider + '"] input');
|
||||||
const apiKey = keyField ? keyField.value.trim() : '';
|
const apiKey = keyField ? keyField.value.trim() : '';
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
setRefreshStatus('Vul eerst de API-sleutel in.', 'error');
|
setRefreshStatus(apiKeyRequiredText, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshButton.disabled = true;
|
refreshButton.disabled = true;
|
||||||
setRefreshStatus('Modellen worden opgehaald…', 'loading');
|
setRefreshStatus(loadingModelsText, 'loading');
|
||||||
|
|
||||||
const payload = new URLSearchParams();
|
const payload = new URLSearchParams();
|
||||||
payload.append('action', 'groq_ai_refresh_models');
|
payload.append('action', 'groq_ai_refresh_models');
|
||||||
@@ -194,14 +202,14 @@
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (!json.success || !json.data || !Array.isArray(json.data.models)) {
|
if (!json.success || !json.data || !Array.isArray(json.data.models)) {
|
||||||
throw new Error((json.data && json.data.message) || 'Onbekende fout');
|
throw new Error((json.data && json.data.message) || errorUnknownText);
|
||||||
}
|
}
|
||||||
data.providers[provider].models = json.data.models;
|
data.providers[provider].models = json.data.models;
|
||||||
buildModelOptions();
|
buildModelOptions();
|
||||||
setRefreshStatus('Modellen bijgewerkt.', 'success');
|
setRefreshStatus(successModelsText, 'success');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setRefreshStatus(error.message || 'Ophalen mislukt.', 'error');
|
setRefreshStatus(error.message || errorFetchText, 'error');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
refreshButton.disabled = false;
|
refreshButton.disabled = false;
|
||||||
|
|||||||
@@ -9,13 +9,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const promptField = document.getElementById('groq-ai-term-prompt');
|
const promptField = document.getElementById('groq-ai-term-prompt');
|
||||||
const outputField = document.getElementById('groq-ai-term-generated');
|
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 rawField = document.getElementById('groq-ai-term-raw');
|
||||||
const statusField = document.getElementById('groq-ai-term-status');
|
const statusField = document.getElementById('groq-ai-term-status');
|
||||||
const applyButton = document.getElementById('groq-ai-term-apply');
|
const applyButton = document.getElementById('groq-ai-term-apply');
|
||||||
const includeTopProducts = document.getElementById('groq-ai-term-include-top-products');
|
const includeTopProducts = document.getElementById('groq-ai-term-include-top-products');
|
||||||
const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit');
|
const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit');
|
||||||
|
|
||||||
|
const strings = (window.GroqAITermGenerator && GroqAITermGenerator.strings) || {};
|
||||||
|
const promptRequiredText = strings.promptRequired || 'Vul eerst een prompt in.';
|
||||||
|
const loadingText = strings.loading || 'AI is bezig met schrijven...';
|
||||||
|
const successText = strings.success || 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.';
|
||||||
|
const applySuccessText = strings.applySuccess || 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.';
|
||||||
|
const errorDefaultText = strings.errorDefault || 'Er ging iets mis bij het genereren.';
|
||||||
|
const errorUnknownText = strings.errorUnknown || 'Onbekende fout';
|
||||||
|
|
||||||
function setStatus(message, type) {
|
function setStatus(message, type) {
|
||||||
if (!statusField) {
|
if (!statusField) {
|
||||||
return;
|
return;
|
||||||
@@ -47,11 +59,31 @@
|
|||||||
if (applyButton) {
|
if (applyButton) {
|
||||||
applyButton.addEventListener('click', () => {
|
applyButton.addEventListener('click', () => {
|
||||||
const descriptionField = document.getElementById('description');
|
const descriptionField = document.getElementById('description');
|
||||||
if (!descriptionField || !outputField) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
descriptionField.value = outputField.value || '';
|
|
||||||
setStatus('Tekst ingevuld in het beschrijving-veld. Vergeet niet op "Opslaan" te klikken.', 'success');
|
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(applySuccessText, 'success');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +91,22 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const prompt = promptField ? (promptField.value || '').trim() : '';
|
const prompt = promptField ? (promptField.value || '').trim() : '';
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
setStatus('Vul eerst een prompt in.', 'error');
|
setStatus(promptRequiredText, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStatus('AI is bezig met schrijven...', 'loading');
|
setStatus(loadingText, 'loading');
|
||||||
if (rawField) {
|
if (rawField) {
|
||||||
rawField.textContent = '';
|
rawField.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outputTopField) outputTopField.value = '';
|
||||||
|
if (outputBottomField) outputBottomField.value = '';
|
||||||
|
if (outputMetaTitleField) outputMetaTitleField.value = '';
|
||||||
|
if (outputMetaDescriptionField) outputMetaDescriptionField.value = '';
|
||||||
|
if (outputFocusKeywordsField) outputFocusKeywordsField.value = '';
|
||||||
|
|
||||||
fetch(GroqAITermGenerator.ajaxUrl, {
|
fetch(GroqAITermGenerator.ajaxUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -79,21 +117,38 @@
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (!json.success) {
|
if (!json.success) {
|
||||||
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout';
|
const errorMessage = json.data && json.data.message ? json.data.message : errorUnknownText;
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputField) {
|
if (outputTopField) {
|
||||||
outputField.value = (json.data && json.data.description ? json.data.description : '').trim();
|
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) {
|
if (rawField) {
|
||||||
rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();
|
rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success');
|
setStatus(successText, 'success');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error');
|
setStatus(error && error.message ? error.message : errorDefaultText, 'error');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
295
assets/js/term-bulk.js
Normal file
295
assets/js/term-bulk.js
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
(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 unknownErrorText = strings.unknownError || 'Onbekende fout';
|
||||||
|
const unknownTermText = strings.unknownTerm || 'Onbekende term.';
|
||||||
|
const confirmStopFallbackText = strings.confirmStopFallback || 'Stoppen?';
|
||||||
|
const logErrorDefaultText = strings.logErrorDefault || '%1$s: %2$s';
|
||||||
|
const logSuccessDefaultText = strings.logSuccessDefault || '%1$s gevuld.';
|
||||||
|
const regenerateErrorDefaultText = strings.regenerateErrorDefault || '%1$s mislukt: %2$s';
|
||||||
|
const regenerateDoneDefaultText = strings.regenerateDoneDefault || '%s is bijgewerkt.';
|
||||||
|
const terms = (Array.isArray(data.terms) ? data.terms : [])
|
||||||
|
.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) || unknownErrorText;
|
||||||
|
appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, errorMessage]), 'error');
|
||||||
|
if (context === 'single') {
|
||||||
|
setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [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 || logSuccessDefaultText, [term.name || term.id, term.words]), 'success');
|
||||||
|
if (context === 'single') {
|
||||||
|
setStatus(formatString(strings.regenerateDone || regenerateDoneDefaultText, [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(confirmStopFallbackText);
|
||||||
|
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(unknownTermText, '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 : unknownErrorText;
|
||||||
|
appendLog(formatString(strings.logError || logErrorDefaultText, [term.name || term.id, message]), 'error');
|
||||||
|
setStatus(formatString(strings.regenerateError || regenerateErrorDefaultText, [term.name || term.id, message]), 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
button.disabled = false;
|
||||||
|
button.classList.remove('is-busy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getPendingTerms().length === 0) {
|
||||||
|
setStatus(strings.statusEmpty || '', 'info');
|
||||||
|
}
|
||||||
|
})();
|
||||||
16
composer.json
Normal file
16
composer.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "sitiweb/siti-ai-product-content-generator",
|
||||||
|
"description": "SitiAI Product Teksten WordPress plugin",
|
||||||
|
"type": "wordpress-plugin",
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.6"
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"classmap": [
|
||||||
|
"includes/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit"
|
||||||
|
}
|
||||||
|
}
|
||||||
1815
composer.lock
generated
Normal file
1815
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* Plugin Name: SitiAI Product Teksten
|
* Plugin Name: SitiAI Product Teksten
|
||||||
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
|
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
|
||||||
* Version: 1.4.2
|
* Version: 1.9.0
|
||||||
* Author: SitiAI
|
* Author: Roberto Guagliardo | SitiWeb
|
||||||
|
* Author URI: https://sitiweb.nl/
|
||||||
* Text Domain: siti-ai-product-content-generator
|
* Text Domain: siti-ai-product-content-generator
|
||||||
* Domain Path: /languages
|
* Domain Path: /languages
|
||||||
*/
|
*/
|
||||||
@@ -44,6 +45,9 @@ if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEB
|
|||||||
require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php';
|
require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php';
|
||||||
require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php';
|
require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php';
|
||||||
require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php';
|
require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php';
|
||||||
|
require_once __DIR__ . '/includes/Core/class-groq-ai-compatibility-service.php';
|
||||||
|
require_once __DIR__ . '/includes/Core/class-groq-ai-model-service.php';
|
||||||
|
require_once __DIR__ . '/includes/Core/class-groq-ai-log-scheduler.php';
|
||||||
require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.php';
|
require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.php';
|
||||||
require_once __DIR__ . '/includes/Providers/class-groq-ai-abstract-openai-provider.php';
|
require_once __DIR__ . '/includes/Providers/class-groq-ai-abstract-openai-provider.php';
|
||||||
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php';
|
require_once __DIR__ . '/includes/Providers/class-groq-ai-provider-groq.php';
|
||||||
@@ -58,9 +62,15 @@ require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-oauth-cli
|
|||||||
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-search-console-client.php';
|
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-search-console-client.php';
|
||||||
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-analytics-data-client.php';
|
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-analytics-data-client.php';
|
||||||
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-context-builder.php';
|
require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-context-builder.php';
|
||||||
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-admin-base.php';
|
||||||
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-term-admin-base.php';
|
||||||
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-categories-admin.php';
|
||||||
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-brands-admin.php';
|
||||||
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php';
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php';
|
||||||
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-admin.php';
|
||||||
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php';
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php';
|
||||||
require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php';
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php';
|
||||||
|
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-renderer.php';
|
||||||
|
|
||||||
if( ! class_exists( 'SitiWebUpdater' ) ){
|
if( ! class_exists( 'SitiWebUpdater' ) ){
|
||||||
include_once( plugin_dir_path( __FILE__ ) . 'SitiWebUpdater.php' );
|
include_once( plugin_dir_path( __FILE__ ) . 'SitiWebUpdater.php' );
|
||||||
@@ -85,14 +95,29 @@ final class Groq_AI_Product_Text_Plugin {
|
|||||||
/** @var Groq_AI_Service_Container */
|
/** @var Groq_AI_Service_Container */
|
||||||
private $container;
|
private $container;
|
||||||
|
|
||||||
/** @var */
|
/** @var Groq_AI_Product_Text_Settings_Page */
|
||||||
private $settings_page;
|
private $settings_page;
|
||||||
|
|
||||||
|
/** @var Groq_AI_Categories_Admin */
|
||||||
|
private $categories_admin;
|
||||||
|
|
||||||
|
/** @var Groq_AI_Brands_Admin */
|
||||||
|
private $brands_admin;
|
||||||
|
|
||||||
|
/** @var Groq_AI_Logs_Admin */
|
||||||
|
private $logs_admin;
|
||||||
|
|
||||||
/** @var Groq_AI_Product_Text_Product_UI */
|
/** @var Groq_AI_Product_Text_Product_UI */
|
||||||
private $product_ui;
|
private $product_ui;
|
||||||
|
|
||||||
/** @var bool */
|
/** @var Groq_AI_Compatibility_Service */
|
||||||
private $missing_wc_notice = false;
|
private $compatibility_service;
|
||||||
|
|
||||||
|
/** @var Groq_AI_Model_Service */
|
||||||
|
private $model_service;
|
||||||
|
|
||||||
|
/** @var Groq_AI_Log_Scheduler */
|
||||||
|
private $log_scheduler;
|
||||||
|
|
||||||
public static function instance() {
|
public static function instance() {
|
||||||
if ( null === self::$instance ) {
|
if ( null === self::$instance ) {
|
||||||
@@ -104,14 +129,22 @@ final class Groq_AI_Product_Text_Plugin {
|
|||||||
|
|
||||||
private function __construct() {
|
private function __construct() {
|
||||||
$this->register_services();
|
$this->register_services();
|
||||||
|
$this->compatibility_service = new Groq_AI_Compatibility_Service();
|
||||||
|
$this->model_service = new Groq_AI_Model_Service();
|
||||||
|
$this->log_scheduler = new Groq_AI_Log_Scheduler( $this->get_settings_manager(), $this->get_generation_logger() );
|
||||||
|
|
||||||
$this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() );
|
$this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() );
|
||||||
|
$this->categories_admin = new Groq_AI_Categories_Admin( $this );
|
||||||
|
$this->brands_admin = new Groq_AI_Brands_Admin( $this );
|
||||||
|
$this->logs_admin = new Groq_AI_Logs_Admin( $this );
|
||||||
$this->product_ui = new Groq_AI_Product_Text_Product_UI( $this );
|
$this->product_ui = new Groq_AI_Product_Text_Product_UI( $this );
|
||||||
|
|
||||||
add_action( 'plugins_loaded', [ $this, 'maybe_load_textdomain_early' ], 0 );
|
|
||||||
add_action( 'init', [ $this, 'load_textdomain' ] );
|
add_action( 'init', [ $this, 'load_textdomain' ] );
|
||||||
add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] );
|
$logger = $this->container->get( 'generation_logger' );
|
||||||
add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] );
|
add_action( 'plugins_loaded', [ $logger, 'maybe_create_table' ] );
|
||||||
|
add_action( 'load-plugins.php', [ $this->compatibility_service, 'maybe_deactivate_if_woocommerce_missing' ] );
|
||||||
|
add_action( 'init', [ $this->log_scheduler, 'ensure_logs_cleanup_schedule' ] );
|
||||||
|
add_action( 'groq_ai_cleanup_logs', [ $this->log_scheduler, 'cleanup_logs' ] );
|
||||||
add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 );
|
add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,14 +164,6 @@ final class Groq_AI_Product_Text_Plugin {
|
|||||||
$this->textdomain_loaded = true;
|
$this->textdomain_loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function maybe_load_textdomain_early() {
|
|
||||||
if ( did_action( 'init' ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->load_textdomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function register_services() {
|
private function register_services() {
|
||||||
$this->container = new Groq_AI_Service_Container();
|
$this->container = new Groq_AI_Service_Container();
|
||||||
|
|
||||||
@@ -229,60 +254,84 @@ final class Groq_AI_Product_Text_Plugin {
|
|||||||
return self::OPTION_KEY;
|
return self::OPTION_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_provider_manager() {
|
public function __call( $name, $arguments ) {
|
||||||
|
switch ( $name ) {
|
||||||
|
case 'get_provider_manager':
|
||||||
return $this->container->get( 'provider_manager' );
|
return $this->container->get( 'provider_manager' );
|
||||||
}
|
case 'get_settings_manager':
|
||||||
|
|
||||||
public function get_settings_manager() {
|
|
||||||
return $this->container->get( 'settings_manager' );
|
return $this->container->get( 'settings_manager' );
|
||||||
}
|
case 'get_prompt_builder':
|
||||||
|
|
||||||
public function get_prompt_builder() {
|
|
||||||
return $this->container->get( 'prompt_builder' );
|
return $this->container->get( 'prompt_builder' );
|
||||||
}
|
case 'get_conversation_manager':
|
||||||
|
|
||||||
public function get_conversation_manager() {
|
|
||||||
return $this->container->get( 'conversation_manager' );
|
return $this->container->get( 'conversation_manager' );
|
||||||
}
|
case 'get_generation_logger':
|
||||||
|
|
||||||
public function get_generation_logger() {
|
|
||||||
return $this->container->get( 'generation_logger' );
|
return $this->container->get( 'generation_logger' );
|
||||||
|
case 'get_settings':
|
||||||
|
return $this->container->get( 'settings_manager' )->all();
|
||||||
|
case 'sanitize_settings':
|
||||||
|
return $this->container->get( 'settings_manager' )->sanitize( $arguments[0] ?? [] );
|
||||||
|
case 'get_context_field_definitions':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_context_field_definitions();
|
||||||
|
case 'get_default_modules_settings':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_default_modules_settings();
|
||||||
|
case 'get_default_context_fields':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_default_context_fields();
|
||||||
|
case 'normalize_context_fields':
|
||||||
|
return $this->container->get( 'settings_manager' )->normalize_context_fields( $arguments[0] ?? [] );
|
||||||
|
case 'get_module_config':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_module_config( $arguments[0] ?? '', $arguments[1] ?? null );
|
||||||
|
case 'is_module_enabled':
|
||||||
|
return $this->container->get( 'settings_manager' )->is_module_enabled( $arguments[0] ?? '', $arguments[1] ?? null );
|
||||||
|
case 'get_rankmath_focus_keyword_limit':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_rankmath_focus_keyword_limit( $arguments[0] ?? null );
|
||||||
|
case 'get_rankmath_meta_title_pixel_limit':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_rankmath_meta_title_pixel_limit( $arguments[0] ?? null );
|
||||||
|
case 'get_rankmath_meta_description_pixel_limit':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_rankmath_meta_description_pixel_limit( $arguments[0] ?? null );
|
||||||
|
case 'is_response_format_compat_enabled':
|
||||||
|
return $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $arguments[0] ?? null );
|
||||||
|
case 'get_image_context_mode':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_image_context_mode( $arguments[0] ?? null );
|
||||||
|
case 'get_image_context_limit':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_image_context_limit( $arguments[0] ?? null );
|
||||||
|
case 'get_term_top_description_char_limit':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_term_top_description_char_limit( $arguments[0] ?? null );
|
||||||
|
case 'get_term_bottom_description_char_limit':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_term_bottom_description_char_limit( $arguments[0] ?? null );
|
||||||
|
case 'get_google_safety_settings':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_google_safety_settings( $arguments[0] ?? null );
|
||||||
|
case 'get_google_safety_categories':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_google_safety_categories();
|
||||||
|
case 'get_google_safety_thresholds':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_google_safety_thresholds();
|
||||||
|
case 'get_loggable_settings_snapshot':
|
||||||
|
return $this->container->get( 'settings_manager' )->get_loggable_settings_snapshot( $arguments[0] ?? null );
|
||||||
|
case 'create_settings_renderer':
|
||||||
|
$values = $arguments[0] ?? null;
|
||||||
|
if ( null === $values ) {
|
||||||
|
$values = $this->container->get( 'settings_manager' )->all();
|
||||||
|
}
|
||||||
|
return new Groq_AI_Settings_Renderer( self::OPTION_KEY, $values );
|
||||||
|
case 'should_use_response_format':
|
||||||
|
$provider = $arguments[0] ?? null;
|
||||||
|
$settings = $arguments[1] ?? null;
|
||||||
|
if ( ! $provider instanceof Groq_AI_Provider_Interface ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ! $this->container->get( 'settings_manager' )->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
|
||||||
|
case 'is_rankmath_active':
|
||||||
|
return $this->compatibility_service->is_rankmath_active();
|
||||||
|
case 'is_woocommerce_active':
|
||||||
|
return $this->compatibility_service->is_woocommerce_active();
|
||||||
|
case 'get_selected_model':
|
||||||
|
return $this->model_service->get_selected_model( $arguments[0], $arguments[1] ?? [] );
|
||||||
|
case 'get_cached_models_for_provider':
|
||||||
|
return $this->model_service->get_cached_models_for_provider( $arguments[0] ?? '' );
|
||||||
|
case 'update_cached_models_for_provider':
|
||||||
|
return $this->model_service->update_cached_models_for_provider( $arguments[0] ?? '', $arguments[1] ?? [] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_settings() {
|
throw new BadMethodCallException( sprintf( 'Method %s does not exist.', $name ) );
|
||||||
return $this->get_settings_manager()->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sanitize_settings( $input ) {
|
|
||||||
return $this->get_settings_manager()->sanitize( $input );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function maybe_deactivate_if_woocommerce_missing() {
|
|
||||||
if ( $this->is_woocommerce_active() ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! function_exists( 'deactivate_plugins' ) ) {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
deactivate_plugins( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) );
|
|
||||||
$this->missing_wc_notice = true;
|
|
||||||
|
|
||||||
add_action( 'admin_notices', [ $this, 'render_missing_wc_notice' ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render_missing_wc_notice() {
|
|
||||||
if ( ! $this->missing_wc_notice ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="notice notice-error">
|
|
||||||
<p>
|
|
||||||
<?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function build_prompt_template_preview( $settings ) {
|
public function build_prompt_template_preview( $settings ) {
|
||||||
@@ -303,171 +352,6 @@ final class Groq_AI_Product_Text_Plugin {
|
|||||||
return implode( "\n\n", $parts );
|
return implode( "\n\n", $parts );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_context_field_definitions() {
|
|
||||||
return $this->get_settings_manager()->get_context_field_definitions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_default_modules_settings() {
|
|
||||||
return $this->get_settings_manager()->get_default_modules_settings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_default_context_fields() {
|
|
||||||
return $this->get_settings_manager()->get_default_context_fields();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function normalize_context_fields( $fields ) {
|
|
||||||
return $this->get_settings_manager()->normalize_context_fields( $fields );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_module_config( $module, $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->get_module_config( $module, $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function is_module_enabled( $module, $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->is_module_enabled( $module, $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_rankmath_focus_keyword_limit( $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->get_rankmath_focus_keyword_limit( $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->get_rankmath_meta_title_pixel_limit( $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_rankmath_meta_description_pixel_limit( $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->get_rankmath_meta_description_pixel_limit( $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function is_response_format_compat_enabled( $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->is_response_format_compat_enabled( $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_image_context_mode( $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->get_image_context_mode( $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_image_context_limit( $settings = null ) {
|
|
||||||
return $this->get_settings_manager()->get_image_context_limit( $settings );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) {
|
|
||||||
return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function is_rankmath_active() {
|
|
||||||
if ( class_exists( 'RankMath' ) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! function_exists( 'is_plugin_active' ) ) {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'seo-by-rank-math/rank-math.php' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function is_woocommerce_active() {
|
|
||||||
if ( class_exists( 'WooCommerce' ) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! function_exists( 'is_plugin_active' ) ) {
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'woocommerce/woocommerce.php' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
|
|
||||||
$provider_key = $provider->get_key();
|
|
||||||
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
|
|
||||||
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
|
|
||||||
|
|
||||||
if ( '' === $model ) {
|
|
||||||
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
|
|
||||||
if ( '' !== $default ) {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
|
|
||||||
if ( ! empty( $available ) ) {
|
|
||||||
return $available[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_cached_models_for_provider( $provider ) {
|
|
||||||
$provider = sanitize_key( (string) $provider );
|
|
||||||
$cache = $this->get_models_cache();
|
|
||||||
|
|
||||||
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update_cached_models_for_provider( $provider, $models ) {
|
|
||||||
$provider = sanitize_key( (string) $provider );
|
|
||||||
$models = $this->sanitize_models_list( $models );
|
|
||||||
|
|
||||||
$cache = $this->get_models_cache();
|
|
||||||
$cache[ $provider ] = $models;
|
|
||||||
|
|
||||||
update_option( self::MODELS_CACHE_OPTION_KEY, $cache );
|
|
||||||
|
|
||||||
return $models;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function get_models_cache() {
|
|
||||||
$cache = get_option( self::MODELS_CACHE_OPTION_KEY, [] );
|
|
||||||
|
|
||||||
if ( ! is_array( $cache ) ) {
|
|
||||||
$cache = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $cache as $provider => $models ) {
|
|
||||||
$cache[ $provider ] = $this->sanitize_models_list( $models );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sanitize_models_list( $models ) {
|
|
||||||
if ( ! is_array( $models ) ) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$models = array_map( 'sanitize_text_field', $models );
|
|
||||||
$models = array_filter(
|
|
||||||
$models,
|
|
||||||
function ( $model ) {
|
|
||||||
return '' !== $model;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
$models = array_values( array_unique( $models ) );
|
|
||||||
|
|
||||||
if ( ! empty( $models ) ) {
|
|
||||||
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $models;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function log_debug( $message, $context = [] ) {
|
|
||||||
$this->get_generation_logger()->log_debug( $message, $context );
|
|
||||||
}
|
|
||||||
private function extract_content_text( $result ) {
|
|
||||||
if ( is_array( $result ) && isset( $result['content'] ) ) {
|
|
||||||
return (string) $result['content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (string) $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function maybe_create_logs_table() {
|
|
||||||
$this->get_generation_logger()->maybe_create_table();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function activate() {
|
public static function activate() {
|
||||||
$logger = new Groq_AI_Generation_Logger();
|
$logger = new Groq_AI_Generation_Logger();
|
||||||
|
|||||||
46
includes/Admin/class-groq-ai-admin-base.php
Normal file
46
includes/Admin/class-groq-ai-admin-base.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class Groq_AI_Admin_Base {
|
||||||
|
/** @var Groq_AI_Product_Text_Plugin */
|
||||||
|
protected $plugin;
|
||||||
|
|
||||||
|
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
|
||||||
|
$this->plugin = $plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_page_url( $slug = 'groq-ai-product-text', $args = [] ) {
|
||||||
|
$slug = sanitize_key( (string) $slug );
|
||||||
|
$url = add_query_arg(
|
||||||
|
[
|
||||||
|
'page' => $slug,
|
||||||
|
],
|
||||||
|
admin_url( 'options-general.php' )
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $args ) ) {
|
||||||
|
$url = add_query_arg( $args, $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function current_user_can_manage() {
|
||||||
|
return current_user_can( 'manage_options' );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function enqueue_admin_styles() {
|
||||||
|
wp_enqueue_style(
|
||||||
|
'groq-ai-settings',
|
||||||
|
plugins_url( 'assets/css/admin.css', GROQ_AI_PRODUCT_TEXT_FILE ),
|
||||||
|
[],
|
||||||
|
GROQ_AI_PRODUCT_TEXT_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'groq-ai-settings-extra',
|
||||||
|
plugins_url( 'assets/css/settings.css', GROQ_AI_PRODUCT_TEXT_FILE ),
|
||||||
|
[ 'groq-ai-settings' ],
|
||||||
|
GROQ_AI_PRODUCT_TEXT_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
includes/Admin/class-groq-ai-brands-admin.php
Normal file
177
includes/Admin/class-groq-ai-brands-admin.php
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Groq_AI_Brands_Admin extends Groq_AI_Term_Admin_Base {
|
||||||
|
private $brand_taxonomy = null;
|
||||||
|
|
||||||
|
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
|
||||||
|
parent::__construct( $plugin );
|
||||||
|
add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
|
||||||
|
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_brand_assets' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register_menu_pages() {
|
||||||
|
add_submenu_page(
|
||||||
|
'options-general.php',
|
||||||
|
__( 'Siti AI Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
__( 'Siti AI Merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'manage_options',
|
||||||
|
'groq-ai-product-text-brands',
|
||||||
|
[ $this, 'render_brands_overview_page' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->register_term_page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_brands_overview_page() {
|
||||||
|
if ( ! $this->current_user_can_manage() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$taxonomy = $this->detect_brand_taxonomy();
|
||||||
|
if ( '' === $taxonomy ) {
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
|
||||||
|
<p><?php esc_html_e( 'Geen merk-taxonomie gevonden. Installeer/activeer een merken-plugin of stel een taxonomie in via de filter groq_ai_brand_taxonomy.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$overview = $this->get_term_overview_data( $taxonomy );
|
||||||
|
$rows = isset( $overview['rows'] ) ? $overview['rows'] : [];
|
||||||
|
$empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0;
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: taxonomy key */
|
||||||
|
esc_html__( 'Gedetecteerde merk-taxonomie: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
esc_html( $taxonomy )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
<?php $this->render_term_bulk_panel( __( 'merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Gebruik de knop "Genereer opnieuw" in de tabel om bestaande merkteksten opnieuw laten schrijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Merk', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Slug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Producten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Woorden (omschrijving)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Acties', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ( empty( $rows ) ) : ?>
|
||||||
|
<tr><td colspan="5"><?php esc_html_e( 'Geen merken gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></td></tr>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php foreach ( $rows as $row ) : ?>
|
||||||
|
<?php
|
||||||
|
$row_classes = [ 'groq-ai-term-row' ];
|
||||||
|
if ( empty( $row['has_description'] ) ) {
|
||||||
|
$row_classes[] = 'groq-ai-term-missing';
|
||||||
|
}
|
||||||
|
$link = isset( $row['url'] ) ? $row['url'] : '';
|
||||||
|
$count = isset( $row['count'] ) ? (int) $row['count'] : 0;
|
||||||
|
$words = isset( $row['words'] ) ? (int) $row['words'] : 0;
|
||||||
|
?>
|
||||||
|
<tr class="<?php echo esc_attr( implode( ' ', $row_classes ) ); ?>" data-groq-ai-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo esc_url( $link ); ?>"><strong><?php echo esc_html( isset( $row['name'] ) ? $row['name'] : '' ); ?></strong></a>
|
||||||
|
</td>
|
||||||
|
<td><?php echo esc_html( isset( $row['slug'] ) ? $row['slug'] : '' ); ?></td>
|
||||||
|
<td><?php echo esc_html( (string) $count ); ?></td>
|
||||||
|
<td class="groq-ai-word-cell"><span class="groq-ai-word-count"><?php echo esc_html( (string) $words ); ?></span></td>
|
||||||
|
<td class="groq-ai-term-actions">
|
||||||
|
<button type="button" class="button button-secondary groq-ai-regenerate-term" data-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
|
||||||
|
<?php esc_html_e( 'Genereer opnieuw', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enqueue_brand_assets( $hook ) {
|
||||||
|
if ( 0 !== strpos( (string) $hook, 'settings_page_groq-ai-product-text-brands' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->enqueue_admin_styles();
|
||||||
|
|
||||||
|
$taxonomy = $this->detect_brand_taxonomy();
|
||||||
|
if ( '' === $taxonomy ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'groq-ai-term-bulk',
|
||||||
|
plugins_url( 'assets/js/term-bulk.js', GROQ_AI_PRODUCT_TEXT_FILE ),
|
||||||
|
[],
|
||||||
|
GROQ_AI_PRODUCT_TEXT_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->localize_term_bulk_script(
|
||||||
|
$taxonomy,
|
||||||
|
[
|
||||||
|
'allowRegenerate' => true,
|
||||||
|
'strings' => [
|
||||||
|
'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde merken bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusProgress' => __( 'Merk %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusDone' => __( 'Klaar! %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusStopped' => __( 'Bulk generatie gestopt. %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusEmpty' => __( 'Geen merken zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? Het huidige merk kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'confirmRegenerate' => __( 'Wil je %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een merk opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detect_brand_taxonomy() {
|
||||||
|
if ( null !== $this->brand_taxonomy ) {
|
||||||
|
return $this->brand_taxonomy;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = [
|
||||||
|
'product_brand',
|
||||||
|
'pwb-brand',
|
||||||
|
'yith_product_brand',
|
||||||
|
'berocket_brand',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ( taxonomy_exists( 'pa_brand' ) ) {
|
||||||
|
array_unshift( $candidates, 'pa_brand' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = apply_filters( 'groq_ai_brand_taxonomy_candidates', $candidates );
|
||||||
|
$found = '';
|
||||||
|
foreach ( $candidates as $tax ) {
|
||||||
|
$tax = sanitize_key( (string) $tax );
|
||||||
|
if ( $tax && taxonomy_exists( $tax ) ) {
|
||||||
|
$found = $tax;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$found = apply_filters( 'groq_ai_brand_taxonomy', $found );
|
||||||
|
$this->brand_taxonomy = sanitize_key( (string) $found );
|
||||||
|
|
||||||
|
return $this->brand_taxonomy;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
includes/Admin/class-groq-ai-categories-admin.php
Normal file
119
includes/Admin/class-groq-ai-categories-admin.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Groq_AI_Categories_Admin extends Groq_AI_Term_Admin_Base {
|
||||||
|
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
|
||||||
|
parent::__construct( $plugin );
|
||||||
|
add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
|
||||||
|
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_category_assets' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register_menu_pages() {
|
||||||
|
add_submenu_page(
|
||||||
|
'options-general.php',
|
||||||
|
__( 'Siti AI Categorie teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
__( 'Siti AI Categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'manage_options',
|
||||||
|
'groq-ai-product-text-categories',
|
||||||
|
[ $this, 'render_categories_overview_page' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->register_term_page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_categories_overview_page() {
|
||||||
|
if ( ! $this->current_user_can_manage() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$taxonomy = 'product_cat';
|
||||||
|
$overview = $this->get_term_overview_data( $taxonomy );
|
||||||
|
$rows = isset( $overview['rows'] ) ? $overview['rows'] : [];
|
||||||
|
$empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0;
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Categorie teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
|
||||||
|
<p><?php esc_html_e( 'Klik op een categorie om teksten te genereren en instellingen te beheren. De tabel toont de huidige woordlengte van de categorie-omschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
<?php $this->render_term_bulk_panel( __( 'categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Categorie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Slug', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Producten', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Woorden (omschrijving)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Acties', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ( empty( $rows ) ) : ?>
|
||||||
|
<tr><td colspan="5"><?php esc_html_e( 'Geen categorieën gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></td></tr>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php foreach ( $rows as $row ) : ?>
|
||||||
|
<?php
|
||||||
|
$row_classes = [ 'groq-ai-term-row' ];
|
||||||
|
if ( empty( $row['has_description'] ) ) {
|
||||||
|
$row_classes[] = 'groq-ai-term-missing';
|
||||||
|
}
|
||||||
|
$link = isset( $row['url'] ) ? $row['url'] : '';
|
||||||
|
$count = isset( $row['count'] ) ? (int) $row['count'] : 0;
|
||||||
|
$words = isset( $row['words'] ) ? (int) $row['words'] : 0;
|
||||||
|
?>
|
||||||
|
<tr class="<?php echo esc_attr( implode( ' ', $row_classes ) ); ?>" data-groq-ai-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo esc_url( $link ); ?>"><strong><?php echo esc_html( isset( $row['name'] ) ? $row['name'] : '' ); ?></strong></a>
|
||||||
|
</td>
|
||||||
|
<td><?php echo esc_html( isset( $row['slug'] ) ? $row['slug'] : '' ); ?></td>
|
||||||
|
<td><?php echo esc_html( (string) $count ); ?></td>
|
||||||
|
<td class="groq-ai-word-cell"><span class="groq-ai-word-count"><?php echo esc_html( (string) $words ); ?></span></td>
|
||||||
|
<td class="groq-ai-term-actions">
|
||||||
|
<button type="button" class="button button-secondary groq-ai-regenerate-term" data-term-id="<?php echo esc_attr( isset( $row['id'] ) ? (string) $row['id'] : '' ); ?>">
|
||||||
|
<?php esc_html_e( 'Genereer opnieuw', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enqueue_category_assets( $hook ) {
|
||||||
|
if ( 0 !== strpos( (string) $hook, 'settings_page_groq-ai-product-text-categories' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->enqueue_admin_styles();
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'groq-ai-term-bulk',
|
||||||
|
plugins_url( 'assets/js/term-bulk.js', GROQ_AI_PRODUCT_TEXT_FILE ),
|
||||||
|
[],
|
||||||
|
GROQ_AI_PRODUCT_TEXT_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->localize_term_bulk_script(
|
||||||
|
'product_cat',
|
||||||
|
[
|
||||||
|
'allowRegenerate' => true,
|
||||||
|
'strings' => [
|
||||||
|
'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde categorieën bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusProgress' => __( 'Categorie %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusDone' => __( 'Klaar! %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusStopped' => __( 'Bulk generatie gestopt. %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'statusEmpty' => __( 'Geen categorieën zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? De huidige categorie kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'confirmRegenerate' => __( 'Wil je categorie %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een categorie opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
includes/Admin/class-groq-ai-logs-admin.php
Normal file
180
includes/Admin/class-groq-ai-logs-admin.php
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Groq_AI_Logs_Admin extends Groq_AI_Admin_Base {
|
||||||
|
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
|
||||||
|
parent::__construct( $plugin );
|
||||||
|
add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register_menu_pages() {
|
||||||
|
add_submenu_page(
|
||||||
|
'options-general.php',
|
||||||
|
__( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
__( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'manage_options',
|
||||||
|
'groq-ai-product-text-logs',
|
||||||
|
[ $this, 'render_logs_page' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'options-general.php',
|
||||||
|
__( 'Siti AI Log detail', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
__( 'Siti AI Log detail', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'manage_options',
|
||||||
|
'groq-ai-product-text-log',
|
||||||
|
[ $this, 'render_log_detail_page' ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_logs_page() {
|
||||||
|
if ( ! $this->current_user_can_manage() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs_table = new Groq_AI_Logs_Table( $this->plugin );
|
||||||
|
$logs_table->prepare_items();
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
|
||||||
|
<form method="get">
|
||||||
|
<input type="hidden" name="page" value="groq-ai-product-text-logs" />
|
||||||
|
<?php $logs_table->search_box( __( 'Zoek logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'groq-ai-logs' ); ?>
|
||||||
|
<?php $logs_table->display(); ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_log_detail_page() {
|
||||||
|
if ( ! $this->current_user_can_manage() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log_id = isset( $_GET['log_id'] ) ? absint( $_GET['log_id'] ) : 0;
|
||||||
|
$back_url = $this->get_page_url( 'groq-ai-product-text-logs' );
|
||||||
|
$log = null;
|
||||||
|
|
||||||
|
if ( $log_id ) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'groq_ai_generation_logs';
|
||||||
|
$query = $wpdb->prepare(
|
||||||
|
"SELECT l.*, p.post_title FROM {$table} l LEFT JOIN {$wpdb->posts} p ON p.ID = l.post_id WHERE l.id = %d",
|
||||||
|
$log_id
|
||||||
|
);
|
||||||
|
$log = $wpdb->get_row( $query, ARRAY_A );
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Logdetail', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo esc_url( $back_url ); ?>" class="button">← <?php esc_html_e( 'Terug naar logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php if ( ! $log ) : ?>
|
||||||
|
<p><?php esc_html_e( 'Log niet gevonden of verwijderd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
<?php else : ?>
|
||||||
|
<table class="widefat striped" style="margin-top:16px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Datum', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td><?php echo esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $log['created_at'] ) ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Gebruiker', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
if ( $log['user_id'] ) {
|
||||||
|
$user = get_userdata( $log['user_id'] );
|
||||||
|
echo $user ? esc_html( $user->display_name ) : esc_html( (string) $log['user_id'] );
|
||||||
|
} else {
|
||||||
|
echo '—';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Product', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
if ( $log['post_id'] ) {
|
||||||
|
$link = get_edit_post_link( $log['post_id'] );
|
||||||
|
$title = $log['post_title'] ? $log['post_title'] : sprintf( __( 'Product #%d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (int) $log['post_id'] );
|
||||||
|
echo $link ? '<a href="' . esc_url( $link ) . '">' . esc_html( $title ) . '</a>' : esc_html( $title );
|
||||||
|
} else {
|
||||||
|
echo '—';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Provider', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td><?php echo esc_html( $log['provider'] ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Model', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td><?php echo esc_html( $log['model'] ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Status', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td><?php echo esc_html( $log['status'] ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Tokens', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
esc_html__( 'Prompt: %1$s — Completion: %2$s — Totaal: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
isset( $log['tokens_prompt'] ) ? number_format_i18n( (int) $log['tokens_prompt'] ) : '—',
|
||||||
|
isset( $log['tokens_completion'] ) ? number_format_i18n( (int) $log['tokens_completion'] ) : '—',
|
||||||
|
isset( $log['tokens_total'] ) ? number_format_i18n( (int) $log['tokens_total'] ) : '—'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Foutmelding', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td><?php echo $log['error_message'] ? esc_html( $log['error_message'] ) : '—'; ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Prompt', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
|
||||||
|
<pre style="background:#fff;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
|
||||||
|
<?php echo esc_html( $log['prompt'] ); ?>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'AI-respons', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
|
||||||
|
<pre style="background:#f9f9f9;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
|
||||||
|
<?php echo esc_html( $log['response'] ); ?>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $log['request_json'] ) ) :
|
||||||
|
$request_params = json_decode( $log['request_json'], true );
|
||||||
|
$request_params = is_array( $request_params ) ? $request_params : [];
|
||||||
|
if ( ! empty( $request_params ) ) :
|
||||||
|
$request_pretty = wp_json_encode( $request_params, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
|
||||||
|
$request_pretty = $request_pretty ? $request_pretty : wp_json_encode( $request_params );
|
||||||
|
?>
|
||||||
|
<h2><?php esc_html_e( 'Request parameters', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
|
||||||
|
<pre style="background:#fff;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
|
||||||
|
<?php echo esc_html( $request_pretty ); ?>
|
||||||
|
</pre>
|
||||||
|
<?php endif; endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $log['usage_json'] ) ) :
|
||||||
|
$usage_meta = json_decode( $log['usage_json'], true );
|
||||||
|
$usage_meta = is_array( $usage_meta ) ? $usage_meta : [];
|
||||||
|
if ( ! empty( $usage_meta ) ) :
|
||||||
|
$usage_pretty = wp_json_encode( $usage_meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
|
||||||
|
$usage_pretty = $usage_pretty ? $usage_pretty : wp_json_encode( $usage_meta );
|
||||||
|
?>
|
||||||
|
<h2><?php esc_html_e( 'Usage metadata', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
|
||||||
|
<pre style="background:#f6f7f7;border:1px solid #dcdcde;padding:12px;white-space:pre-wrap;">
|
||||||
|
<?php echo esc_html( $usage_pretty ); ?>
|
||||||
|
</pre>
|
||||||
|
<?php endif; endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,10 +60,14 @@ class Groq_AI_Logs_Table extends WP_List_Table {
|
|||||||
$current_page = $this->get_pagenum();
|
$current_page = $this->get_pagenum();
|
||||||
$offset = ( $current_page - 1 ) * $per_page;
|
$offset = ( $current_page - 1 ) * $per_page;
|
||||||
|
|
||||||
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at';
|
$allowed_orderby = [
|
||||||
if ( ! $orderby ) {
|
'created_at' => 'created_at',
|
||||||
$orderby = 'created_at';
|
'provider' => 'provider',
|
||||||
}
|
'model' => 'model',
|
||||||
|
'status' => 'status',
|
||||||
|
];
|
||||||
|
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_key( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at';
|
||||||
|
$orderby = isset( $allowed_orderby[ $orderby ] ) ? $allowed_orderby[ $orderby ] : 'created_at';
|
||||||
$order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC';
|
$order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC';
|
||||||
$order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';
|
$order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';
|
||||||
|
|
||||||
@@ -136,28 +140,15 @@ class Groq_AI_Logs_Table extends WP_List_Table {
|
|||||||
|
|
||||||
protected function column_created_at( $item ) {
|
protected function column_created_at( $item ) {
|
||||||
$date = esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
|
$date = esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
|
||||||
$usage = $this->get_usage_meta( $item );
|
$url = add_query_arg(
|
||||||
$payload = [
|
[
|
||||||
'created_at' => $item['created_at'],
|
'page' => 'groq-ai-product-text-log',
|
||||||
'user' => $this->column_default( $item, 'user_id' ),
|
'log_id' => isset( $item['id'] ) ? (int) $item['id'] : 0,
|
||||||
'post_title' => $item['post_title'],
|
],
|
||||||
'provider' => $item['provider'],
|
admin_url( 'options-general.php' )
|
||||||
'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
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return sprintf( '<a href="%s">%s</a>', esc_url( $url ), $date );
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get_usage_meta( $item ) {
|
private function get_usage_meta( $item ) {
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ class Groq_AI_Product_Text_Product_UI {
|
|||||||
$post_id = ( $post && isset( $post->ID ) ) ? (int) $post->ID : 0;
|
$post_id = ( $post && isset( $post->ID ) ) ? (int) $post->ID : 0;
|
||||||
|
|
||||||
$settings = $this->plugin->get_settings();
|
$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(
|
wp_localize_script(
|
||||||
'groq-ai-admin',
|
'groq-ai-admin',
|
||||||
@@ -69,6 +72,19 @@ class Groq_AI_Product_Text_Product_UI {
|
|||||||
'defaultPrompt' => $settings['default_prompt'],
|
'defaultPrompt' => $settings['default_prompt'],
|
||||||
'postId' => $post_id,
|
'postId' => $post_id,
|
||||||
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
|
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
|
||||||
|
'attributeIncludesDefaults' => $attribute_defaults,
|
||||||
|
'strings' => [
|
||||||
|
'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'retry' => __( 'Probeer het opnieuw of pas je prompt/context aan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'errorUnknown' => __( 'Onbekende fout.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'success' => __( 'Structuur gegenereerd. Kopieer of vul velden in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'fieldApplied' => __( '%s ingevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'fieldApplyError' => __( 'Kon het veld niet automatisch invullen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'fieldCopied' => __( '%s gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'jsonCopied' => __( 'JSON gekopieerd naar het klembord.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'copyFailed' => __( 'Kopiëren mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,6 +98,7 @@ class Groq_AI_Product_Text_Product_UI {
|
|||||||
|
|
||||||
$settings = $this->plugin->get_settings();
|
$settings = $this->plugin->get_settings();
|
||||||
$rankmath_enabled = $this->plugin->is_rankmath_active() && $this->plugin->is_module_enabled( 'rankmath', $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 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">
|
<div class="groq-ai-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="groq-ai-modal-title">
|
||||||
@@ -109,6 +126,9 @@ class Groq_AI_Product_Text_Product_UI {
|
|||||||
$context_definitions = $this->plugin->get_context_field_definitions();
|
$context_definitions = $this->plugin->get_context_field_definitions();
|
||||||
$context_defaults = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();
|
$context_defaults = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();
|
||||||
foreach ( $context_definitions as $context_key => $context_info ) :
|
foreach ( $context_definitions as $context_key => $context_info ) :
|
||||||
|
if ( 'attributes' === $context_key ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$checked = ! empty( $context_defaults[ $context_key ] );
|
$checked = ! empty( $context_defaults[ $context_key ] );
|
||||||
?>
|
?>
|
||||||
<label class="groq-ai-context-option">
|
<label class="groq-ai-context-option">
|
||||||
@@ -122,6 +142,23 @@ class Groq_AI_Product_Text_Product_UI {
|
|||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -225,4 +262,39 @@ class Groq_AI_Product_Text_Product_UI {
|
|||||||
</div>
|
</div>
|
||||||
<?php
|
<?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
262
includes/Admin/class-groq-ai-settings-renderer.php
Normal file
262
includes/Admin/class-groq-ai-settings-renderer.php
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Groq_AI_Settings_Renderer {
|
||||||
|
/** @var string */
|
||||||
|
private $option_key;
|
||||||
|
|
||||||
|
/** @var array */
|
||||||
|
private $values = [];
|
||||||
|
|
||||||
|
public function __construct( $option_key, $values = [] ) {
|
||||||
|
$this->option_key = $option_key;
|
||||||
|
$this->set_values( $values );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_values( $values ) {
|
||||||
|
$this->values = is_array( $values ) ? $values : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function open_table( $args = [] ) {
|
||||||
|
$defaults = [
|
||||||
|
'class' => 'form-table',
|
||||||
|
'role' => 'presentation',
|
||||||
|
];
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
|
||||||
|
printf( '<table %s>', $this->build_attr_string( $args ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close_table() {
|
||||||
|
echo '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function field( $args ) {
|
||||||
|
$defaults = [
|
||||||
|
'key' => '',
|
||||||
|
'name' => '',
|
||||||
|
'id' => '',
|
||||||
|
'label' => '',
|
||||||
|
'description' => '',
|
||||||
|
'type' => 'text',
|
||||||
|
'placeholder' => '',
|
||||||
|
'options' => [],
|
||||||
|
'attributes' => [],
|
||||||
|
'default' => '',
|
||||||
|
'value' => null,
|
||||||
|
'renderer' => null,
|
||||||
|
'row_attributes' => [],
|
||||||
|
'row_class' => '',
|
||||||
|
];
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
|
||||||
|
if ( '' === $args['name'] && '' !== $args['key'] ) {
|
||||||
|
$args['name'] = $this->build_field_name( $args['key'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( '' === $args['id'] && '' !== $args['key'] ) {
|
||||||
|
$args['id'] = $this->build_field_id( $args['key'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( null === $args['value'] && '' !== $args['key'] ) {
|
||||||
|
$args['value'] = $this->get_value( $args['key'], $args['default'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $args['attributes']['id'] ) && '' !== $args['id'] ) {
|
||||||
|
$args['attributes']['id'] = $args['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $args['type'];
|
||||||
|
|
||||||
|
$row_attributes = $this->prepare_row_attributes( $args );
|
||||||
|
$row_attr_string = $row_attributes ? ' ' . $this->build_attr_string( $row_attributes ) : '';
|
||||||
|
|
||||||
|
echo '<tr' . $row_attr_string . '>';
|
||||||
|
$this->render_label_cell( $args );
|
||||||
|
echo '<td>';
|
||||||
|
|
||||||
|
if ( is_callable( $args['renderer'] ) ) {
|
||||||
|
call_user_func( $args['renderer'], $args, $this );
|
||||||
|
} else {
|
||||||
|
switch ( $type ) {
|
||||||
|
case 'textarea':
|
||||||
|
$this->render_textarea( $args );
|
||||||
|
break;
|
||||||
|
case 'password':
|
||||||
|
$this->render_input( 'password', $args );
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
$this->render_input( 'number', $args );
|
||||||
|
break;
|
||||||
|
case 'select':
|
||||||
|
$this->render_select( $args );
|
||||||
|
break;
|
||||||
|
case 'checkbox':
|
||||||
|
$this->render_checkbox( $args );
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
$this->render_toggle( $args );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->render_input( 'text', $args );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->render_description( $args['description'] );
|
||||||
|
|
||||||
|
echo '</td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_label_cell( $args ) {
|
||||||
|
$label = $args['label'];
|
||||||
|
$id = $args['id'];
|
||||||
|
echo '<th scope="row">';
|
||||||
|
if ( '' !== $label ) {
|
||||||
|
printf( '<label for="%s">%s</label>', esc_attr( $id ), esc_html( $label ) );
|
||||||
|
}
|
||||||
|
echo '</th>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_input( $type, $args ) {
|
||||||
|
$attributes = $this->prepare_input_attributes( $args );
|
||||||
|
printf( '<input type="%s" %s />', esc_attr( $type ), $attributes );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_textarea( $args ) {
|
||||||
|
$attributes = $this->prepare_input_attributes( $args, [ 'rows' => 4, 'class' => 'large-text' ] );
|
||||||
|
printf( '<textarea %s>%s</textarea>', $attributes, esc_textarea( $args['value'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_select( $args ) {
|
||||||
|
$attributes = $this->prepare_input_attributes( $args );
|
||||||
|
printf( '<select %s>', $attributes );
|
||||||
|
foreach ( (array) $args['options'] as $value => $label ) {
|
||||||
|
printf( '<option value="%s" %s>%s</option>', esc_attr( $value ), selected( $args['value'], $value, false ), esc_html( $label ) );
|
||||||
|
}
|
||||||
|
echo '</select>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_checkbox( $args ) {
|
||||||
|
$value = ! empty( $args['value'] );
|
||||||
|
$attributes = $this->prepare_input_attributes( $args, [ 'class' => '' ] );
|
||||||
|
printf( '<label><input type="checkbox" %s %s /> %s</label>', $attributes, checked( $value, true, false ), esc_html( $args['checkbox_label'] ?? '' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_toggle( $args ) {
|
||||||
|
$value = ! empty( $args['value'] );
|
||||||
|
$attributes = $this->prepare_input_attributes( $args, [ 'class' => '' ] );
|
||||||
|
printf( '<label class="groq-ai-toggle"><input type="checkbox" %s %s /> <span class="groq-ai-toggle__slider"></span> %s</label>', $attributes, checked( $value, true, false ), esc_html( $args['checkbox_label'] ?? '' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_description( $text ) {
|
||||||
|
$text = trim( (string) $text );
|
||||||
|
if ( '' === $text ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf( '<p class="description">%s</p>', wp_kses_post( $text ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepare_input_attributes( $args, $defaults = [] ) {
|
||||||
|
$attributes = wp_parse_args( $args['attributes'], $defaults );
|
||||||
|
$attributes['name'] = $args['name'];
|
||||||
|
|
||||||
|
if ( ! isset( $attributes['id'] ) ) {
|
||||||
|
$attributes['id'] = $args['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( '' !== $args['placeholder'] ) {
|
||||||
|
$attributes['placeholder'] = $args['placeholder'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $attributes['class'] ) ) {
|
||||||
|
$attributes['class'] = 'regular-text';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array( $args['type'], [ 'checkbox', 'toggle', 'select', 'textarea' ], true ) ) {
|
||||||
|
$attributes['value'] = $args['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->build_attr_string( $attributes );
|
||||||
|
}
|
||||||
|
private function prepare_row_attributes( $args ) {
|
||||||
|
$attributes = [];
|
||||||
|
if ( isset( $args['row_attributes'] ) && is_array( $args['row_attributes'] ) ) {
|
||||||
|
$attributes = $args['row_attributes'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$row_class = isset( $args['row_class'] ) ? trim( (string) $args['row_class'] ) : '';
|
||||||
|
if ( '' !== $row_class ) {
|
||||||
|
if ( isset( $attributes['class'] ) ) {
|
||||||
|
$attributes['class'] .= ' ' . $row_class;
|
||||||
|
} else {
|
||||||
|
$attributes['class'] = $row_class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter(
|
||||||
|
$attributes,
|
||||||
|
function ( $value ) {
|
||||||
|
return '' !== $value || 0 === $value || '0' === $value;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function build_attr_string( $attributes ) {
|
||||||
|
$buffer = [];
|
||||||
|
foreach ( $attributes as $key => $value ) {
|
||||||
|
if ( '' === $value && 0 !== $value && '0' !== $value ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode( ' ', $buffer );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function build_field_name( $key ) {
|
||||||
|
$segments = $this->split_key( $key );
|
||||||
|
$name = $this->option_key;
|
||||||
|
|
||||||
|
foreach ( $segments as $segment ) {
|
||||||
|
$name .= '[' . $segment . ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function build_field_id( $key ) {
|
||||||
|
$segments = $this->split_key( $key );
|
||||||
|
|
||||||
|
return 'groq-ai-' . implode( '-', $segments );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_value( $key, $default = '' ) {
|
||||||
|
$segments = $this->split_key( $key );
|
||||||
|
$value = $this->values;
|
||||||
|
|
||||||
|
foreach ( $segments as $segment ) {
|
||||||
|
if ( is_array( $value ) && array_key_exists( $segment, $value ) ) {
|
||||||
|
$value = $value[ $segment ];
|
||||||
|
} else {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function split_key( $key ) {
|
||||||
|
if ( is_array( $key ) ) {
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = trim( (string) $key );
|
||||||
|
if ( '' === $key ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map( 'sanitize_key', explode( '.', $key ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
538
includes/Admin/class-groq-ai-term-admin-base.php
Normal file
538
includes/Admin/class-groq-ai-term-admin-base.php
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class Groq_AI_Term_Admin_Base extends Groq_AI_Admin_Base {
|
||||||
|
protected $term_overview_cache = [];
|
||||||
|
|
||||||
|
private static $term_page_registered = false;
|
||||||
|
private static $term_handler_registered = false;
|
||||||
|
private static $term_assets_hook_registered = false;
|
||||||
|
|
||||||
|
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
|
||||||
|
parent::__construct( $plugin );
|
||||||
|
$this->ensure_term_handler_registered();
|
||||||
|
$this->ensure_term_assets_hook();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function register_term_page() {
|
||||||
|
if ( self::$term_page_registered ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'options-general.php',
|
||||||
|
__( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
__( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'manage_options',
|
||||||
|
'groq-ai-product-text-term',
|
||||||
|
[ $this, 'render_term_generator_page' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::$term_page_registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function render_term_bulk_panel( $label_plural, $empty_count ) {
|
||||||
|
$label_plural = (string) $label_plural;
|
||||||
|
?>
|
||||||
|
<div class="groq-ai-bulk-panel">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
if ( $empty_count > 0 ) {
|
||||||
|
printf(
|
||||||
|
/* translators: 1: amount, 2: label plural (e.g. categorieën) */
|
||||||
|
esc_html__( 'Er zijn %1$d %2$s zonder omschrijving. Klik op de knop hieronder om automatisch teksten te genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
(int) $empty_count,
|
||||||
|
esc_html( $label_plural )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printf(
|
||||||
|
esc_html__( 'Alle %s hebben al een omschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
esc_html( $label_plural )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
<p class="groq-ai-bulk-actions">
|
||||||
|
<?php
|
||||||
|
$button_label = sprintf(
|
||||||
|
esc_html__( 'Genereer teksten voor lege %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
$label_plural
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<button type="button" class="button button-primary" id="groq-ai-bulk-generate"><?php echo esc_html( $button_label ); ?></button>
|
||||||
|
<button type="button" class="button" id="groq-ai-bulk-cancel" hidden><?php esc_html_e( 'Stop bulk generatie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||||
|
</p>
|
||||||
|
<div id="groq-ai-bulk-status" class="description"></div>
|
||||||
|
<ol id="groq-ai-bulk-log" class="groq-ai-bulk-log"></ol>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function localize_term_bulk_script( $taxonomy, $overrides = [] ) {
|
||||||
|
$overview = $this->get_term_overview_data( $taxonomy );
|
||||||
|
$rows = isset( $overview['rows'] ) ? $overview['rows'] : [];
|
||||||
|
|
||||||
|
$terms = [];
|
||||||
|
foreach ( $rows as $row ) {
|
||||||
|
$terms[] = [
|
||||||
|
'id' => isset( $row['id'] ) ? (int) $row['id'] : 0,
|
||||||
|
'name' => isset( $row['name'] ) ? (string) $row['name'] : '',
|
||||||
|
'slug' => isset( $row['slug'] ) ? (string) $row['slug'] : '',
|
||||||
|
'count' => isset( $row['count'] ) ? (int) $row['count'] : 0,
|
||||||
|
'words' => isset( $row['words'] ) ? (int) $row['words'] : 0,
|
||||||
|
'hasDescription' => ! empty( $row['has_description'] ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaults = [
|
||||||
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
|
'nonce' => wp_create_nonce( 'groq_ai_bulk_generate_terms' ),
|
||||||
|
'taxonomy' => $taxonomy,
|
||||||
|
'terms' => $terms,
|
||||||
|
'allowRegenerate' => false,
|
||||||
|
'strings' => [
|
||||||
|
'unknownError' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'unknownTerm' => __( 'Onbekende term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'confirmStopFallback' => __( 'Stoppen?', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'logErrorDefault' => __( '%1$s: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'logSuccessDefault' => __( '%1$s gevuld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateErrorDefault' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'regenerateDoneDefault' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$config = wp_parse_args( $overrides, $defaults );
|
||||||
|
$override_strings = isset( $overrides['strings'] ) && is_array( $overrides['strings'] ) ? $overrides['strings'] : [];
|
||||||
|
$config['strings'] = array_merge( $defaults['strings'], $override_strings );
|
||||||
|
|
||||||
|
wp_localize_script( 'groq-ai-term-bulk', 'GroqAITermBulk', $config );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_term_overview_data( $taxonomy ) {
|
||||||
|
$taxonomy = sanitize_key( (string) $taxonomy );
|
||||||
|
|
||||||
|
if ( isset( $this->term_overview_cache[ $taxonomy ] ) ) {
|
||||||
|
return $this->term_overview_cache[ $taxonomy ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$empty_rows = [];
|
||||||
|
|
||||||
|
if ( '' !== $taxonomy && taxonomy_exists( $taxonomy ) ) {
|
||||||
|
$terms = get_terms(
|
||||||
|
[
|
||||||
|
'taxonomy' => $taxonomy,
|
||||||
|
'hide_empty' => false,
|
||||||
|
'orderby' => 'name',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'number' => 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $terms ) ) {
|
||||||
|
$terms = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $terms as $term ) {
|
||||||
|
if ( ! $term || ! is_object( $term ) || empty( $term->term_id ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$words = $this->count_words( isset( $term->description ) ? $term->description : '' );
|
||||||
|
$has_description = $words > 0;
|
||||||
|
|
||||||
|
$row = [
|
||||||
|
'id' => absint( $term->term_id ),
|
||||||
|
'name' => (string) $term->name,
|
||||||
|
'slug' => (string) $term->slug,
|
||||||
|
'count' => isset( $term->count ) ? absint( $term->count ) : 0,
|
||||||
|
'words' => $words,
|
||||||
|
'has_description' => $has_description,
|
||||||
|
'url' => $this->get_term_page_url( $taxonomy, $term->term_id ),
|
||||||
|
];
|
||||||
|
|
||||||
|
$rows[] = $row;
|
||||||
|
if ( ! $has_description ) {
|
||||||
|
$empty_rows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'rows' => $rows,
|
||||||
|
'empty_rows' => $empty_rows,
|
||||||
|
'empty_count' => count( $empty_rows ),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->term_overview_cache[ $taxonomy ] = $data;
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function count_words( $text ) {
|
||||||
|
$text = wp_strip_all_tags( (string) $text );
|
||||||
|
$text = trim( preg_replace( '/\s+/u', ' ', $text ) );
|
||||||
|
if ( '' === $text ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ( preg_match_all( '/\pL[\pL\pN\']*/u', $text, $matches ) ) {
|
||||||
|
return count( $matches[0] );
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_term_page_url( $taxonomy, $term_id ) {
|
||||||
|
return add_query_arg(
|
||||||
|
[
|
||||||
|
'page' => 'groq-ai-product-text-term',
|
||||||
|
'taxonomy' => sanitize_key( (string) $taxonomy ),
|
||||||
|
'term_id' => absint( $term_id ),
|
||||||
|
],
|
||||||
|
admin_url( 'options-general.php' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enqueue_term_assets( $hook ) {
|
||||||
|
if ( 0 !== strpos( (string) $hook, 'settings_page_groq-ai-product-text-term' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->enqueue_admin_styles();
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'groq-ai-term-admin',
|
||||||
|
plugins_url( 'assets/js/term-admin.js', GROQ_AI_PRODUCT_TEXT_FILE ),
|
||||||
|
[],
|
||||||
|
GROQ_AI_PRODUCT_TEXT_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '';
|
||||||
|
$term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0;
|
||||||
|
|
||||||
|
wp_localize_script(
|
||||||
|
'groq-ai-term-admin',
|
||||||
|
'GroqAITermGenerator',
|
||||||
|
[
|
||||||
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
|
'nonce' => wp_create_nonce( 'groq_ai_generate_term' ),
|
||||||
|
'taxonomy' => $taxonomy,
|
||||||
|
'termId' => $term_id,
|
||||||
|
'strings' => [
|
||||||
|
'promptRequired' => __( 'Vul eerst een prompt in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'loading' => __( 'AI is bezig met schrijven...', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'success' => __( 'Tekst gegenereerd. Je kunt hem toepassen en opslaan.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'applySuccess' => __( 'Tekst ingevuld. Vergeet niet op "Opslaan" te klikken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'errorDefault' => __( 'Er ging iets mis bij het genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'errorUnknown' => __( 'Onbekende fout', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_term_generator_page() {
|
||||||
|
if ( ! $this->current_user_can_manage() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '';
|
||||||
|
$term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0;
|
||||||
|
|
||||||
|
if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
|
||||||
|
<p><?php esc_html_e( 'Ongeldige term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$term = get_term( $term_id, $taxonomy );
|
||||||
|
if ( ! $term || is_wp_error( $term ) ) {
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h1>
|
||||||
|
<p><?php esc_html_e( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$term_notice = isset( $_GET['groq_ai_term_notice'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_term_notice'] ) ) : '';
|
||||||
|
$term_notice_status = isset( $_GET['groq_ai_term_status'] ) ? sanitize_key( wp_unslash( $_GET['groq_ai_term_status'] ) ) : 'success';
|
||||||
|
$term_notice_message = '';
|
||||||
|
if ( isset( $_GET['groq_ai_term_notice_message'] ) ) {
|
||||||
|
$term_notice_message = sanitize_text_field( rawurldecode( wp_unslash( $_GET['groq_ai_term_notice_message'] ) ) );
|
||||||
|
}
|
||||||
|
if ( $term_notice && '' === $term_notice_message ) {
|
||||||
|
if ( 'saved' === $term_notice ) {
|
||||||
|
$term_notice_message = __( 'Term succesvol opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||||
|
} else {
|
||||||
|
$term_notice_message = __( 'Actie voltooid.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$term_label = ( 'product_cat' === $taxonomy ) ? __( 'Categorie', GROQ_AI_PRODUCT_TEXT_DOMAIN ) : __( 'Term', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||||
|
$word_count = $this->count_words( $term->description );
|
||||||
|
$meta_prompt = get_term_meta( $term_id, 'groq_ai_term_custom_prompt', true );
|
||||||
|
$settings = $this->plugin->get_settings();
|
||||||
|
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings );
|
||||||
|
$effective_bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description';
|
||||||
|
$bottom_description = (string) get_term_meta( $term_id, $effective_bottom_meta_key, true );
|
||||||
|
$rankmath_module_enabled = $this->plugin->is_module_enabled( 'rankmath', $settings );
|
||||||
|
$rankmath_active = $this->plugin->is_rankmath_active();
|
||||||
|
$rankmath_title = '';
|
||||||
|
$rankmath_description = '';
|
||||||
|
$rankmath_focus_keywords = '';
|
||||||
|
if ( $rankmath_module_enabled ) {
|
||||||
|
$rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings );
|
||||||
|
$rankmath_title = (string) get_term_meta( $term_id, $rankmath_keys['title'], true );
|
||||||
|
$rankmath_description = (string) get_term_meta( $term_id, $rankmath_keys['description'], true );
|
||||||
|
$rankmath_focus_keywords = (string) get_term_meta( $term_id, $rankmath_keys['focus_keyword'], true );
|
||||||
|
}
|
||||||
|
$default_prompt = $this->get_term_prompt_text( $term, $meta_prompt );
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>
|
||||||
|
<?php echo esc_html( $term_label ); ?>: <?php echo esc_html( $term->name ); ?>
|
||||||
|
</h1>
|
||||||
|
<?php if ( $term_notice ) : ?>
|
||||||
|
<?php $notice_class = ( 'error' === $term_notice_status ) ? 'notice notice-error' : 'notice notice-success'; ?>
|
||||||
|
<div class="<?php echo esc_attr( $notice_class ); ?>">
|
||||||
|
<p><?php echo esc_html( $term_notice_message ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Taxonomie', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td><?php echo esc_html( $taxonomy ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Huidige woordtelling', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></th>
|
||||||
|
<td><?php echo esc_html( (string) $word_count ); ?></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" id="groq-ai-term-form">
|
||||||
|
<?php wp_nonce_field( 'groq_ai_save_term_content' ); ?>
|
||||||
|
<input type="hidden" name="action" value="groq_ai_save_term_content" />
|
||||||
|
<input type="hidden" name="taxonomy" value="<?php echo esc_attr( $taxonomy ); ?>" />
|
||||||
|
<input type="hidden" name="term_id" value="<?php echo esc_attr( $term_id ); ?>" />
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="groq-ai-term-description"><?php esc_html_e( 'Omschrijving (top description)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<textarea id="groq-ai-term-description" class="large-text" rows="8" name="description"><?php echo esc_textarea( $term->description ); ?></textarea>
|
||||||
|
<p class="description"><?php esc_html_e( 'Bovenste omschrijving van de term. Wordt op de term-archive bovenaan getoond.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="groq-ai-term-bottom"><?php esc_html_e( 'Onderste omschrijving', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<textarea id="groq-ai-term-bottom" class="large-text" rows="10" name="groq_ai_term_bottom_description"><?php echo esc_textarea( $bottom_description ); ?></textarea>
|
||||||
|
<p class="description"><?php esc_html_e( 'Wordt onderaan op de term-archive geplaatst. Laat leeg wanneer je dit niet wilt gebruiken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="groq-ai-term-custom-prompt"><?php esc_html_e( 'Eigen prompt (optioneel)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<textarea id="groq-ai-term-custom-prompt" class="large-text" rows="5" name="groq_ai_term_custom_prompt"><?php echo esc_textarea( $meta_prompt ); ?></textarea>
|
||||||
|
<p class="description"><?php esc_html_e( 'Overschrijft de standaard prompt alleen voor deze term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php if ( $rankmath_module_enabled ) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="groq-ai-rankmath-title"><?php esc_html_e( 'Rank Math meta title', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<textarea id="groq-ai-rankmath-title" class="large-text" rows="2" name="groq_ai_rankmath_meta_title" <?php disabled( ! $rankmath_active ); ?>><?php echo esc_textarea( $rankmath_title ); ?></textarea>
|
||||||
|
<p class="description"><?php esc_html_e( 'Wordt opgeslagen in Rank Math. Alleen beschikbaar als Rank Math actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="groq-ai-rankmath-description"><?php esc_html_e( 'Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<textarea id="groq-ai-rankmath-description" class="large-text" rows="3" name="groq_ai_rankmath_meta_description" <?php disabled( ! $rankmath_active ); ?>><?php echo esc_textarea( $rankmath_description ); ?></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="groq-ai-rankmath-keywords"><?php esc_html_e( 'Rank Math focus keywords', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<textarea id="groq-ai-rankmath-keywords" class="large-text" rows="2" name="groq_ai_rankmath_focus_keywords" <?php disabled( ! $rankmath_active ); ?>><?php echo esc_textarea( $rankmath_focus_keywords ); ?></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</table>
|
||||||
|
<?php submit_button( __( 'Term opslaan', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); ?>
|
||||||
|
</form>
|
||||||
|
<hr />
|
||||||
|
<form id="groq-ai-term-generator" action="javascript:void(0);">
|
||||||
|
<h2><?php esc_html_e( 'AI-term generator', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h2>
|
||||||
|
<p><?php esc_html_e( 'Gebruik de AI om automatisch teksten te genereren. Pas deze aan voordat je opslaat.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></p>
|
||||||
|
<textarea id="groq-ai-term-prompt" class="large-text" rows="5"><?php echo esc_textarea( $default_prompt ); ?></textarea>
|
||||||
|
<p>
|
||||||
|
<button type="submit" class="button button-primary"><?php esc_html_e( 'Genereer', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||||
|
<button type="button" class="button" id="groq-ai-term-apply"><?php esc_html_e( 'Zet in velden', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></button>
|
||||||
|
</p>
|
||||||
|
<div id="groq-ai-term-status" class="description" aria-live="polite"></div>
|
||||||
|
<h3><?php esc_html_e( 'Gegenereerde tekst (omschrijving, 1 alinea)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||||
|
<textarea id="groq-ai-term-generated-top" class="large-text" rows="6"></textarea>
|
||||||
|
<h3><?php esc_html_e( 'Gegenereerde tekst (onderaan)', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||||
|
<textarea id="groq-ai-term-generated-bottom" class="large-text" rows="10"></textarea>
|
||||||
|
<?php if ( $rankmath_module_enabled ) : ?>
|
||||||
|
<h3><?php esc_html_e( 'Gegenereerde Rank Math meta title', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||||
|
<textarea id="groq-ai-term-generated-meta-title" class="large-text" rows="2"></textarea>
|
||||||
|
<h3><?php esc_html_e( 'Gegenereerde Rank Math meta description', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||||
|
<textarea id="groq-ai-term-generated-meta-description" class="large-text" rows="3"></textarea>
|
||||||
|
<h3><?php esc_html_e( 'Gegenereerde Rank Math focus keywords', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||||
|
<textarea id="groq-ai-term-generated-focus-keywords" class="large-text" rows="2"></textarea>
|
||||||
|
<?php endif; ?>
|
||||||
|
<h3><?php esc_html_e( 'Ruwe JSON-output', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?></h3>
|
||||||
|
<pre id="groq-ai-term-raw" style="background:#fff;border:1px solid #ddd;padding:12px;max-height:240px;overflow:auto;"></pre>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle_save_term_content() {
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_die( esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), '', [ 'response' => 403 ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
check_admin_referer( 'groq_ai_save_term_content' );
|
||||||
|
|
||||||
|
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
|
||||||
|
$term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0;
|
||||||
|
|
||||||
|
if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
|
||||||
|
$this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Ongeldige term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$term = get_term( $term_id, $taxonomy );
|
||||||
|
if ( ! $term || is_wp_error( $term ) ) {
|
||||||
|
$this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$description = isset( $_POST['description'] ) ? wp_kses_post( wp_unslash( $_POST['description'] ) ) : '';
|
||||||
|
$bottom_description = isset( $_POST['groq_ai_term_bottom_description'] ) ? wp_kses_post( wp_unslash( $_POST['groq_ai_term_bottom_description'] ) ) : '';
|
||||||
|
$custom_prompt = isset( $_POST['groq_ai_term_custom_prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_term_custom_prompt'] ) ) : '';
|
||||||
|
|
||||||
|
$update = wp_update_term(
|
||||||
|
$term_id,
|
||||||
|
$taxonomy,
|
||||||
|
[
|
||||||
|
'description' => $description,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $update ) ) {
|
||||||
|
$this->redirect_with_term_notice( $taxonomy, $term_id, 'error', $update->get_error_message(), 'error' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $this->plugin->get_settings();
|
||||||
|
$bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings );
|
||||||
|
$bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description';
|
||||||
|
update_term_meta( $term_id, $bottom_meta_key, $bottom_description );
|
||||||
|
|
||||||
|
if ( '' === trim( $custom_prompt ) ) {
|
||||||
|
delete_term_meta( $term_id, 'groq_ai_term_custom_prompt' );
|
||||||
|
} else {
|
||||||
|
update_term_meta( $term_id, 'groq_ai_term_custom_prompt', $custom_prompt );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['groq_ai_term_bottom_description'] ) && $bottom_meta_key !== 'groq_ai_term_bottom_description' ) {
|
||||||
|
update_term_meta( $term_id, 'groq_ai_term_bottom_description', $bottom_description );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->plugin->is_module_enabled( 'rankmath', $settings ) ) {
|
||||||
|
$rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings );
|
||||||
|
$rankmath_title = isset( $_POST['groq_ai_rankmath_meta_title'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_meta_title'] ) ) : '';
|
||||||
|
$rankmath_description = isset( $_POST['groq_ai_rankmath_meta_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_rankmath_meta_description'] ) ) : '';
|
||||||
|
$rankmath_keywords = isset( $_POST['groq_ai_rankmath_focus_keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_focus_keywords'] ) ) : '';
|
||||||
|
|
||||||
|
update_term_meta( $term_id, $rankmath_keys['title'], $rankmath_title );
|
||||||
|
update_term_meta( $term_id, $rankmath_keys['description'], $rankmath_description );
|
||||||
|
update_term_meta( $term_id, $rankmath_keys['focus_keyword'], $rankmath_keywords );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect_with_term_notice( $taxonomy, $term_id, 'saved', __( 'Term opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolve_term_bottom_description_meta_key( $term, $settings ) {
|
||||||
|
$default_key = '';
|
||||||
|
if ( is_array( $settings ) && isset( $settings['term_bottom_description_meta_key'] ) ) {
|
||||||
|
$default_key = sanitize_key( (string) $settings['term_bottom_description_meta_key'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = apply_filters( 'groq_ai_term_bottom_description_meta_key', $default_key, $term, $settings );
|
||||||
|
$key = sanitize_key( (string) $key );
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolve_rankmath_term_meta_keys( $term, $settings ) {
|
||||||
|
$keys = [
|
||||||
|
'title' => 'rank_math_title',
|
||||||
|
'description' => 'rank_math_description',
|
||||||
|
'focus_keyword' => 'rank_math_focus_keyword',
|
||||||
|
];
|
||||||
|
$keys = apply_filters( 'groq_ai_rankmath_term_meta_keys', $keys, $term, $settings );
|
||||||
|
if ( ! is_array( $keys ) ) {
|
||||||
|
$keys = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => isset( $keys['title'] ) ? sanitize_key( (string) $keys['title'] ) : 'rank_math_title',
|
||||||
|
'description' => isset( $keys['description'] ) ? sanitize_key( (string) $keys['description'] ) : 'rank_math_description',
|
||||||
|
'focus_keyword' => isset( $keys['focus_keyword'] ) ? sanitize_key( (string) $keys['focus_keyword'] ) : 'rank_math_focus_keyword',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_term_prompt_text( $term, $custom_prompt = null ) {
|
||||||
|
$prompt = ( null !== $custom_prompt ) ? $custom_prompt : '';
|
||||||
|
|
||||||
|
if ( null === $custom_prompt && $term && isset( $term->term_id ) ) {
|
||||||
|
$prompt = get_term_meta( $term->term_id, 'groq_ai_term_custom_prompt', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
$prompt = trim( (string) $prompt );
|
||||||
|
if ( '' !== $prompt ) {
|
||||||
|
return $prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
$default_prompt = __( 'Schrijf een SEO-vriendelijke categorieomschrijving in het Nederlands. Gebruik duidelijke tussenkoppen en <p>-tags. Voeg geen prijsinformatie toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
|
||||||
|
|
||||||
|
return apply_filters( 'groq_ai_default_term_prompt', $default_prompt, $term );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function redirect_with_term_notice( $taxonomy, $term_id, $type, $message = '', $status = 'success' ) {
|
||||||
|
$url = ( $taxonomy && $term_id ) ? $this->get_term_page_url( $taxonomy, $term_id ) : $this->get_page_url( 'groq-ai-product-text-categories' );
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'groq_ai_term_notice' => sanitize_key( (string) $type ),
|
||||||
|
'groq_ai_term_status' => sanitize_key( (string) $status ),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ( '' !== $message ) {
|
||||||
|
$args['groq_ai_term_notice_message'] = rawurlencode( (string) $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_safe_redirect( add_query_arg( $args, $url ) );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensure_term_handler_registered() {
|
||||||
|
if ( self::$term_handler_registered ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action( 'admin_post_groq_ai_save_term_content', [ $this, 'handle_save_term_content' ] );
|
||||||
|
self::$term_handler_registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensure_term_assets_hook() {
|
||||||
|
if ( self::$term_assets_hook_registered ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_term_assets' ] );
|
||||||
|
self::$term_assets_hook_registered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ class Groq_AI_Ajax_Controller {
|
|||||||
add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] );
|
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_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_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() {
|
public function handle_generate_term_text() {
|
||||||
@@ -35,6 +36,113 @@ class Groq_AI_Ajax_Controller {
|
|||||||
wp_send_json_error( [ 'message' => __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
|
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();
|
$settings = $this->plugin->get_settings();
|
||||||
$provider_manager = $this->plugin->get_provider_manager();
|
$provider_manager = $this->plugin->get_provider_manager();
|
||||||
$provider_key = $settings['provider'];
|
$provider_key = $settings['provider'];
|
||||||
@@ -51,7 +159,6 @@ class Groq_AI_Ajax_Controller {
|
|||||||
? $prompt_builder->build_term_system_prompt( $settings, $conversation_id, $term )
|
? $prompt_builder->build_term_system_prompt( $settings, $conversation_id, $term )
|
||||||
: $prompt_builder->build_system_prompt( $settings, $conversation_id );
|
: $prompt_builder->build_system_prompt( $settings, $conversation_id );
|
||||||
|
|
||||||
$model = $this->plugin->get_selected_model( $provider, $settings );
|
|
||||||
$context_block = '';
|
$context_block = '';
|
||||||
if ( method_exists( $prompt_builder, 'build_term_context_block' ) ) {
|
if ( method_exists( $prompt_builder, 'build_term_context_block' ) ) {
|
||||||
$context_block = $prompt_builder->build_term_context_block(
|
$context_block = $prompt_builder->build_term_context_block(
|
||||||
@@ -67,6 +174,19 @@ class Groq_AI_Ajax_Controller {
|
|||||||
? $prompt_builder->prepend_term_context_to_prompt( $prompt, $context_block )
|
? $prompt_builder->prepend_term_context_to_prompt( $prompt, $context_block )
|
||||||
: $prompt_builder->prepend_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;
|
$response_format = null;
|
||||||
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
|
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
|
||||||
if ( $use_response_format && method_exists( $prompt_builder, 'get_term_response_format_definition' ) ) {
|
if ( $use_response_format && method_exists( $prompt_builder, 'get_term_response_format_definition' ) ) {
|
||||||
@@ -78,6 +198,26 @@ class Groq_AI_Ajax_Controller {
|
|||||||
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
|
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$request_parameters = $this->build_request_parameters_snapshot(
|
||||||
|
$settings,
|
||||||
|
[
|
||||||
|
'provider' => $provider_key,
|
||||||
|
'conversation_id' => $conversation_id,
|
||||||
|
'temperature' => 0.7,
|
||||||
|
'response_format_mode' => $use_response_format ? 'structured' : 'prompt',
|
||||||
|
'response_format_definition' => $response_format,
|
||||||
|
'term_context' => [
|
||||||
|
'term_id' => $term_id,
|
||||||
|
'taxonomy' => $taxonomy,
|
||||||
|
],
|
||||||
|
'term_options' => $usage_meta['term_options'],
|
||||||
|
'origin' => $origin,
|
||||||
|
'google_safety_settings' => isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$model = $this->plugin->get_selected_model( $provider, $settings );
|
||||||
|
$request_parameters['model'] = $model;
|
||||||
$result = $provider->generate_content(
|
$result = $provider->generate_content(
|
||||||
[
|
[
|
||||||
'prompt' => $final_prompt,
|
'prompt' => $final_prompt,
|
||||||
@@ -90,29 +230,204 @@ class Groq_AI_Ajax_Controller {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$logged_parameters = $request_parameters;
|
||||||
|
if ( is_array( $result ) && isset( $result['request_payload'] ) ) {
|
||||||
|
$logged_parameters['http_request'] = $result['request_payload'];
|
||||||
|
unset( $result['request_payload'] );
|
||||||
|
}
|
||||||
|
|
||||||
if ( is_wp_error( $result ) ) {
|
if ( is_wp_error( $result ) ) {
|
||||||
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
|
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,
|
||||||
|
'parameters' => $logged_parameters,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$response_text = $this->extract_content_text( $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;
|
$parsed = null;
|
||||||
if ( method_exists( $prompt_builder, 'parse_term_structured_response' ) ) {
|
if ( method_exists( $prompt_builder, 'parse_term_structured_response' ) ) {
|
||||||
$parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings );
|
$parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings );
|
||||||
}
|
}
|
||||||
|
if ( is_wp_error( $parsed ) ) {
|
||||||
|
if ( $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,
|
||||||
|
'parameters' => $logged_parameters,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
if ( ! is_array( $parsed ) ) {
|
if ( ! is_array( $parsed ) ) {
|
||||||
$parsed = [
|
$parsed = [
|
||||||
'description' => trim( (string) $response_text ),
|
'description' => trim( (string) $response_text ),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_send_json_success(
|
if ( $logger ) {
|
||||||
|
$logger->log_generation_event(
|
||||||
[
|
[
|
||||||
'description' => isset( $parsed['description'] ) ? $parsed['description'] : '',
|
'provider' => $provider_key,
|
||||||
'raw' => $response_text,
|
'model' => $model,
|
||||||
|
'prompt' => $final_prompt,
|
||||||
|
'response' => $response_text,
|
||||||
|
'usage' => $response_usage,
|
||||||
|
'status' => 'success',
|
||||||
|
'post_id' => 0,
|
||||||
|
'parameters' => $logged_parameters,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
public function handle_generate_text() {
|
||||||
if ( ! current_user_can( 'edit_products' ) ) {
|
if ( ! current_user_can( 'edit_products' ) ) {
|
||||||
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
|
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
|
||||||
@@ -123,6 +438,19 @@ class Groq_AI_Ajax_Controller {
|
|||||||
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
|
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
|
||||||
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
|
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
|
||||||
|
|
||||||
|
if ( ! $post_id ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( 'Post-ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = get_post( $post_id );
|
||||||
|
if ( ! $post || is_wp_error( $post ) || 'product' !== $post->post_type ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( 'Product niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming om dit product te bewerken.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
|
||||||
|
}
|
||||||
|
|
||||||
$settings = $this->plugin->get_settings();
|
$settings = $this->plugin->get_settings();
|
||||||
$provider_manager = $this->plugin->get_provider_manager();
|
$provider_manager = $this->plugin->get_provider_manager();
|
||||||
$provider_key = $settings['provider'];
|
$provider_key = $settings['provider'];
|
||||||
@@ -138,6 +466,23 @@ class Groq_AI_Ajax_Controller {
|
|||||||
$system_prompt = $prompt_builder->build_system_prompt( $settings, $conversation_id );
|
$system_prompt = $prompt_builder->build_system_prompt( $settings, $conversation_id );
|
||||||
$model = $this->plugin->get_selected_model( $provider, $settings );
|
$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 );
|
$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_mode = $this->plugin->get_image_context_mode( $settings );
|
||||||
$image_context_limit = $this->plugin->get_image_context_limit( $settings );
|
$image_context_limit = $this->plugin->get_image_context_limit( $settings );
|
||||||
|
|
||||||
@@ -163,7 +508,7 @@ class Groq_AI_Ajax_Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode, $image_context_limit );
|
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode, $image_context_limit, $settings );
|
||||||
$image_context_payloads = [];
|
$image_context_payloads = [];
|
||||||
if ( $use_base64_payloads ) {
|
if ( $use_base64_payloads ) {
|
||||||
$image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id, $image_context_limit );
|
$image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id, $image_context_limit );
|
||||||
@@ -188,6 +533,23 @@ class Groq_AI_Ajax_Controller {
|
|||||||
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
|
$final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$request_parameters = $this->build_request_parameters_snapshot(
|
||||||
|
$settings,
|
||||||
|
[
|
||||||
|
'provider' => $provider_key,
|
||||||
|
'model' => $model,
|
||||||
|
'post_id' => $post_id,
|
||||||
|
'conversation_id' => $conversation_id,
|
||||||
|
'temperature' => 0.7,
|
||||||
|
'response_format_mode' => $use_response_format ? 'structured' : 'prompt',
|
||||||
|
'response_format_definition' => $response_format,
|
||||||
|
'context_fields' => $context_fields,
|
||||||
|
'attribute_includes' => isset( $settings['product_attribute_includes'] ) ? $settings['product_attribute_includes'] : [],
|
||||||
|
'image_context' => $image_context_meta,
|
||||||
|
'google_safety_settings' => isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$result = $provider->generate_content(
|
$result = $provider->generate_content(
|
||||||
[
|
[
|
||||||
'prompt' => $final_prompt,
|
'prompt' => $final_prompt,
|
||||||
@@ -201,6 +563,12 @@ class Groq_AI_Ajax_Controller {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$logged_parameters = $request_parameters;
|
||||||
|
if ( is_array( $result ) && isset( $result['request_payload'] ) ) {
|
||||||
|
$logged_parameters['http_request'] = $result['request_payload'];
|
||||||
|
unset( $result['request_payload'] );
|
||||||
|
}
|
||||||
|
|
||||||
if ( is_wp_error( $result ) ) {
|
if ( is_wp_error( $result ) ) {
|
||||||
$this->plugin->get_generation_logger()->log_generation_event(
|
$this->plugin->get_generation_logger()->log_generation_event(
|
||||||
[
|
[
|
||||||
@@ -214,6 +582,7 @@ class Groq_AI_Ajax_Controller {
|
|||||||
'post_id' => $post_id,
|
'post_id' => $post_id,
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'error_message' => $result->get_error_message(),
|
'error_message' => $result->get_error_message(),
|
||||||
|
'parameters' => $logged_parameters,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
|
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
|
||||||
@@ -239,6 +608,7 @@ class Groq_AI_Ajax_Controller {
|
|||||||
'post_id' => $post_id,
|
'post_id' => $post_id,
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'error_message' => $response->get_error_message(),
|
'error_message' => $response->get_error_message(),
|
||||||
|
'parameters' => $logged_parameters,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
wp_send_json_error( [ 'message' => $response->get_error_message() ], 500 );
|
wp_send_json_error( [ 'message' => $response->get_error_message() ], 500 );
|
||||||
@@ -253,6 +623,7 @@ class Groq_AI_Ajax_Controller {
|
|||||||
'usage' => $response_usage,
|
'usage' => $response_usage,
|
||||||
'post_id' => $post_id,
|
'post_id' => $post_id,
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
|
'parameters' => $logged_parameters,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -303,4 +674,16 @@ class Groq_AI_Ajax_Controller {
|
|||||||
|
|
||||||
return (string) $result;
|
return (string) $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function build_request_parameters_snapshot( $settings, array $additional = [] ) {
|
||||||
|
$snapshot = [
|
||||||
|
'settings' => $this->plugin->get_loggable_settings_snapshot( $settings ),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ( $additional as $key => $value ) {
|
||||||
|
$snapshot[ $key ] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $snapshot;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
includes/Core/class-groq-ai-compatibility-service.php
Normal file
58
includes/Core/class-groq-ai-compatibility-service.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Groq_AI_Compatibility_Service {
|
||||||
|
/** @var bool */
|
||||||
|
private $missing_wc_notice = false;
|
||||||
|
|
||||||
|
public function maybe_deactivate_if_woocommerce_missing() {
|
||||||
|
if ( $this->is_woocommerce_active() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'deactivate_plugins' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate_plugins( plugin_basename( GROQ_AI_PRODUCT_TEXT_FILE ) );
|
||||||
|
$this->missing_wc_notice = true;
|
||||||
|
|
||||||
|
add_action( 'admin_notices', [ $this, 'render_missing_wc_notice' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_missing_wc_notice() {
|
||||||
|
if ( ! $this->missing_wc_notice ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="notice notice-error">
|
||||||
|
<p>
|
||||||
|
<?php esc_html_e( 'SitiAI Product Teksten vereist WooCommerce en is gedeactiveerd omdat WooCommerce niet actief is.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function is_rankmath_active() {
|
||||||
|
if ( class_exists( 'RankMath' ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'is_plugin_active' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'seo-by-rank-math/rank-math.php' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function is_woocommerce_active() {
|
||||||
|
if ( class_exists( 'WooCommerce' ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'is_plugin_active' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
return function_exists( 'is_plugin_active' ) && is_plugin_active( 'woocommerce/woocommerce.php' );
|
||||||
|
}
|
||||||
|
}
|
||||||
28
includes/Core/class-groq-ai-log-scheduler.php
Normal file
28
includes/Core/class-groq-ai-log-scheduler.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Groq_AI_Log_Scheduler {
|
||||||
|
/** @var Groq_AI_Settings_Manager */
|
||||||
|
private $settings_manager;
|
||||||
|
|
||||||
|
/** @var Groq_AI_Generation_Logger */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
public function __construct( Groq_AI_Settings_Manager $settings_manager, Groq_AI_Generation_Logger $logger ) {
|
||||||
|
$this->settings_manager = $settings_manager;
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ensure_logs_cleanup_schedule() {
|
||||||
|
if ( wp_next_scheduled( 'groq_ai_cleanup_logs' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'groq_ai_cleanup_logs' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cleanup_logs() {
|
||||||
|
$settings = $this->settings_manager->all();
|
||||||
|
$retention_days = $this->settings_manager->get_logs_retention_days( $settings );
|
||||||
|
$this->logger->cleanup_old_logs( $retention_days );
|
||||||
|
}
|
||||||
|
}
|
||||||
78
includes/Core/class-groq-ai-model-service.php
Normal file
78
includes/Core/class-groq-ai-model-service.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Groq_AI_Model_Service {
|
||||||
|
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
|
||||||
|
$provider_key = $provider->get_key();
|
||||||
|
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
|
||||||
|
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
|
||||||
|
|
||||||
|
if ( '' === $model ) {
|
||||||
|
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
|
||||||
|
if ( '' !== $default ) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
|
||||||
|
if ( ! empty( $available ) ) {
|
||||||
|
return $available[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_cached_models_for_provider( $provider ) {
|
||||||
|
$provider = sanitize_key( (string) $provider );
|
||||||
|
$cache = $this->get_models_cache();
|
||||||
|
|
||||||
|
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update_cached_models_for_provider( $provider, $models ) {
|
||||||
|
$provider = sanitize_key( (string) $provider );
|
||||||
|
$models = $this->sanitize_models_list( $models );
|
||||||
|
|
||||||
|
$cache = $this->get_models_cache();
|
||||||
|
$cache[ $provider ] = $models;
|
||||||
|
|
||||||
|
update_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, $cache );
|
||||||
|
|
||||||
|
return $models;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_models_cache() {
|
||||||
|
$cache = get_option( Groq_AI_Product_Text_Plugin::MODELS_CACHE_OPTION_KEY, [] );
|
||||||
|
|
||||||
|
if ( ! is_array( $cache ) ) {
|
||||||
|
$cache = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $cache as $provider => $models ) {
|
||||||
|
$cache[ $provider ] = $this->sanitize_models_list( $models );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitize_models_list( $models ) {
|
||||||
|
if ( ! is_array( $models ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$models = array_map( 'sanitize_text_field', $models );
|
||||||
|
$models = array_filter(
|
||||||
|
$models,
|
||||||
|
function ( $model ) {
|
||||||
|
return '' !== $model;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$models = array_values( array_unique( $models ) );
|
||||||
|
|
||||||
|
if ( ! empty( $models ) ) {
|
||||||
|
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $models;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,8 +100,10 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
|||||||
$request_body['response_format'] = $args['response_format'];
|
$request_body['response_format'] = $args['response_format'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$endpoint = $this->get_endpoint();
|
||||||
|
|
||||||
$response = wp_remote_post(
|
$response = wp_remote_post(
|
||||||
$this->get_endpoint(),
|
$endpoint,
|
||||||
[
|
[
|
||||||
'headers' => [
|
'headers' => [
|
||||||
'Authorization' => 'Bearer ' . $api_key,
|
'Authorization' => 'Bearer ' . $api_key,
|
||||||
@@ -140,6 +142,10 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
|
|||||||
'content' => $content,
|
'content' => $content,
|
||||||
'usage' => $usage,
|
'usage' => $usage,
|
||||||
'raw_response' => $body,
|
'raw_response' => $body,
|
||||||
|
'request_payload' => [
|
||||||
|
'url' => $endpoint,
|
||||||
|
'body' => $request_body,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function supports_response_format() {
|
public function supports_response_format() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports_image_context() {
|
public function supports_image_context() {
|
||||||
@@ -153,6 +153,18 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
|||||||
}
|
}
|
||||||
$max_tokens = max( 128, min( 8192, $max_tokens ) );
|
$max_tokens = max( 128, min( 8192, $max_tokens ) );
|
||||||
|
|
||||||
|
$generation_config = [
|
||||||
|
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
|
||||||
|
'maxOutputTokens' => $max_tokens,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response_format = isset( $args['response_format'] ) ? $args['response_format'] : null;
|
||||||
|
$schema_payload = $this->prepare_response_schema_payload( $response_format );
|
||||||
|
if ( ! empty( $schema_payload ) ) {
|
||||||
|
$generation_config['responseMimeType'] = 'application/json';
|
||||||
|
$generation_config['responseJsonSchema'] = $schema_payload;
|
||||||
|
}
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'contents' => [
|
'contents' => [
|
||||||
[
|
[
|
||||||
@@ -160,12 +172,17 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
|||||||
'parts' => $parts,
|
'parts' => $parts,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'generationConfig' => [
|
'generationConfig' => $generation_config,
|
||||||
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
|
|
||||||
'maxOutputTokens' => $max_tokens,
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$safety_settings_payload = $this->build_safety_settings_payload(
|
||||||
|
isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : []
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $safety_settings_payload ) ) {
|
||||||
|
$payload['safetySettings'] = $safety_settings_payload;
|
||||||
|
}
|
||||||
|
|
||||||
$response = wp_remote_post(
|
$response = wp_remote_post(
|
||||||
$endpoint,
|
$endpoint,
|
||||||
[
|
[
|
||||||
@@ -204,7 +221,11 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$content = trim( implode( "\n\n", array_filter( $texts ) ) );
|
$content = trim( implode( "\n\n", array_filter( $texts ) ) );
|
||||||
$usage = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : [];
|
$usage_metadata = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : [];
|
||||||
|
$usage = $usage_metadata;
|
||||||
|
if ( ! empty( $usage_metadata ) ) {
|
||||||
|
$usage = array_merge( $usage, $this->map_usage_metadata_counts( $usage_metadata ) );
|
||||||
|
}
|
||||||
$finish_reason = isset( $body['candidates'][0]['finishReason'] ) ? sanitize_text_field( (string) $body['candidates'][0]['finishReason'] ) : '';
|
$finish_reason = isset( $body['candidates'][0]['finishReason'] ) ? sanitize_text_field( (string) $body['candidates'][0]['finishReason'] ) : '';
|
||||||
if ( '' !== $finish_reason ) {
|
if ( '' !== $finish_reason ) {
|
||||||
$usage['finish_reason'] = $finish_reason;
|
$usage['finish_reason'] = $finish_reason;
|
||||||
@@ -214,6 +235,118 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
|||||||
'content' => $content,
|
'content' => $content,
|
||||||
'usage' => $usage,
|
'usage' => $usage,
|
||||||
'raw_response' => $body,
|
'raw_response' => $body,
|
||||||
|
'request_payload' => [
|
||||||
|
'url' => $endpoint,
|
||||||
|
'body' => $payload,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function build_safety_settings_payload( $settings ) {
|
||||||
|
if ( empty( $settings ) || ! is_array( $settings ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = class_exists( 'Groq_AI_Settings_Manager' ) ? array_keys( Groq_AI_Settings_Manager::get_google_safety_categories_list() ) : [];
|
||||||
|
$thresholds = class_exists( 'Groq_AI_Settings_Manager' ) ? array_keys( Groq_AI_Settings_Manager::get_google_safety_thresholds_list() ) : [];
|
||||||
|
|
||||||
|
if ( empty( $categories ) || empty( $thresholds ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [];
|
||||||
|
foreach ( $settings as $category => $threshold ) {
|
||||||
|
$category = sanitize_text_field( (string) $category );
|
||||||
|
$threshold = sanitize_text_field( (string) $threshold );
|
||||||
|
|
||||||
|
if ( ! in_array( $category, $categories, true ) || ! in_array( $threshold, $thresholds, true ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload[] = [
|
||||||
|
'category' => $category,
|
||||||
|
'threshold' => $threshold,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepare_response_schema_payload( $response_format ) {
|
||||||
|
if ( empty( $response_format ) || ! is_array( $response_format ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $response_format['type'] ) && 'json_schema' === $response_format['type'] ) {
|
||||||
|
if ( isset( $response_format['json_schema']['schema'] ) && is_array( $response_format['json_schema']['schema'] ) ) {
|
||||||
|
return $this->sanitize_schema_definition( $response_format['json_schema']['schema'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $response_format['schema'] ) && is_array( $response_format['schema'] ) ) {
|
||||||
|
return $this->sanitize_schema_definition( $response_format['schema'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitize_schema_definition( $schema ) {
|
||||||
|
if ( ! is_array( $schema ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$encoded = wp_json_encode( $schema );
|
||||||
|
if ( ! $encoded ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode( $encoded, true );
|
||||||
|
|
||||||
|
if ( ! is_array( $decoded ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->remove_disallowed_schema_keys( $decoded );
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function remove_disallowed_schema_keys( array &$schema ) {
|
||||||
|
$disallowed = [ 'additionalProperties' ];
|
||||||
|
|
||||||
|
foreach ( $schema as $key => &$value ) {
|
||||||
|
if ( in_array( $key, $disallowed, true ) ) {
|
||||||
|
unset( $schema[ $key ] );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_array( $value ) ) {
|
||||||
|
$this->remove_disallowed_schema_keys( $value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset( $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function map_usage_metadata_counts( $metadata ) {
|
||||||
|
if ( ! is_array( $metadata ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapped = [];
|
||||||
|
|
||||||
|
if ( isset( $metadata['promptTokenCount'] ) ) {
|
||||||
|
$mapped['prompt_tokens'] = absint( $metadata['promptTokenCount'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $metadata['candidatesTokenCount'] ) ) {
|
||||||
|
$mapped['completion_tokens'] = absint( $metadata['candidatesTokenCount'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $metadata['totalTokenCount'] ) ) {
|
||||||
|
$mapped['total_tokens'] = absint( $metadata['totalTokenCount'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapped;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,32 @@ class Groq_AI_Generation_Logger {
|
|||||||
$table = $this->get_logs_table_name();
|
$table = $this->get_logs_table_name();
|
||||||
|
|
||||||
$usage = isset( $args['usage'] ) && is_array( $args['usage'] ) ? $args['usage'] : [];
|
$usage = isset( $args['usage'] ) && is_array( $args['usage'] ) ? $args['usage'] : [];
|
||||||
$prompt_tokens = isset( $usage['prompt_tokens'] ) ? absint( $usage['prompt_tokens'] ) : null;
|
$parameters = isset( $args['parameters'] ) && is_array( $args['parameters'] ) ? $args['parameters'] : [];
|
||||||
$completion_tokens = isset( $usage['completion_tokens'] ) ? absint( $usage['completion_tokens'] ) : null;
|
$prompt_tokens = $this->extract_usage_token_value(
|
||||||
$total_tokens = isset( $usage['total_tokens'] ) ? absint( $usage['total_tokens'] ) : null;
|
$usage,
|
||||||
|
[
|
||||||
|
'prompt_tokens',
|
||||||
|
'promptTokenCount',
|
||||||
|
'input_tokens',
|
||||||
|
'inputTokenCount',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$completion_tokens = $this->extract_usage_token_value(
|
||||||
|
$usage,
|
||||||
|
[
|
||||||
|
'completion_tokens',
|
||||||
|
'output_tokens',
|
||||||
|
'candidatesTokenCount',
|
||||||
|
'outputTokenCount',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$total_tokens = $this->extract_usage_token_value(
|
||||||
|
$usage,
|
||||||
|
[
|
||||||
|
'total_tokens',
|
||||||
|
'totalTokenCount',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$wpdb->insert(
|
$wpdb->insert(
|
||||||
$table,
|
$table,
|
||||||
@@ -46,6 +69,7 @@ class Groq_AI_Generation_Logger {
|
|||||||
'status' => isset( $args['status'] ) ? sanitize_text_field( $args['status'] ) : 'success',
|
'status' => isset( $args['status'] ) ? sanitize_text_field( $args['status'] ) : 'success',
|
||||||
'error_message' => isset( $args['error_message'] ) ? $args['error_message'] : '',
|
'error_message' => isset( $args['error_message'] ) ? $args['error_message'] : '',
|
||||||
'usage_json' => ! empty( $usage ) ? wp_json_encode( $usage ) : null,
|
'usage_json' => ! empty( $usage ) ? wp_json_encode( $usage ) : null,
|
||||||
|
'request_json' => ! empty( $parameters ) ? wp_json_encode( $parameters ) : null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,6 +101,7 @@ class Groq_AI_Generation_Logger {
|
|||||||
public function maybe_create_table() {
|
public function maybe_create_table() {
|
||||||
if ( get_option( self::OPTION_TABLE_CREATED ) ) {
|
if ( get_option( self::OPTION_TABLE_CREATED ) ) {
|
||||||
$this->logs_table_exists = true;
|
$this->logs_table_exists = true;
|
||||||
|
$this->maybe_upgrade_table_schema();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +131,7 @@ class Groq_AI_Generation_Logger {
|
|||||||
status varchar(20) NOT NULL,
|
status varchar(20) NOT NULL,
|
||||||
error_message text DEFAULT NULL,
|
error_message text DEFAULT NULL,
|
||||||
usage_json longtext DEFAULT NULL,
|
usage_json longtext DEFAULT NULL,
|
||||||
|
request_json longtext DEFAULT NULL,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY provider (provider),
|
KEY provider (provider),
|
||||||
KEY post_id (post_id)
|
KEY post_id (post_id)
|
||||||
@@ -117,6 +143,40 @@ class Groq_AI_Generation_Logger {
|
|||||||
update_option( self::OPTION_TABLE_CREATED, 1 );
|
update_option( self::OPTION_TABLE_CREATED, 1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cleanup_old_logs( $retention_days ) {
|
||||||
|
$retention_days = absint( $retention_days );
|
||||||
|
if ( $retention_days <= 0 || ! $this->logs_table_exists() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cutoff = time() - ( $retention_days * DAY_IN_SECONDS );
|
||||||
|
$cutoff = gmdate( 'Y-m-d H:i:s', $cutoff );
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = $this->get_logs_table_name();
|
||||||
|
$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extract_usage_token_value( $usage, $keys ) {
|
||||||
|
foreach ( (array) $keys as $key ) {
|
||||||
|
if ( isset( $usage[ $key ] ) ) {
|
||||||
|
return absint( $usage[ $key ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function maybe_upgrade_table_schema() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = $this->get_logs_table_name();
|
||||||
|
$column = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM {$table} LIKE %s", 'request_json' ) );
|
||||||
|
if ( ! $column ) {
|
||||||
|
$this->create_table();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function get_logs_table_name() {
|
private function get_logs_table_name() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ class Groq_AI_Prompt_Builder {
|
|||||||
return $normalized;
|
return $normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function build_product_context_block( $post_id, $fields, $image_mode = 'url', $image_limit = 3 ) {
|
public function build_product_context_block( $post_id, $fields, $image_mode = 'url', $image_limit = 3, $settings = null ) {
|
||||||
$post_id = absint( $post_id );
|
$post_id = absint( $post_id );
|
||||||
|
|
||||||
if ( ! $post_id ) {
|
if ( ! $post_id ) {
|
||||||
@@ -368,8 +368,14 @@ class Groq_AI_Prompt_Builder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $fields['attributes'] ) ) {
|
$attribute_includes = [];
|
||||||
$attributes = $this->get_product_attributes_text( $post_id );
|
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 ) {
|
if ( $attributes ) {
|
||||||
$parts[] = sprintf( __( 'Attributen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $attributes );
|
$parts[] = sprintf( __( 'Attributen: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $attributes );
|
||||||
}
|
}
|
||||||
@@ -382,9 +388,66 @@ class Groq_AI_Prompt_Builder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ) );
|
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 ) {
|
public function prepend_context_to_prompt( $prompt, $context ) {
|
||||||
$context = trim( (string) $context );
|
$context = trim( (string) $context );
|
||||||
|
|
||||||
@@ -424,6 +487,16 @@ class Groq_AI_Prompt_Builder {
|
|||||||
$parts[] = sprintf( __( 'Huidige omschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (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 ) {
|
if ( $include_top_products ) {
|
||||||
$top_products = $this->get_top_products_for_term( $taxonomy, $term_id, $top_products_limit );
|
$top_products = $this->get_top_products_for_term( $taxonomy, $term_id, $top_products_limit );
|
||||||
if ( ! empty( $top_products ) ) {
|
if ( ! empty( $top_products ) ) {
|
||||||
@@ -464,11 +537,38 @@ class Groq_AI_Prompt_Builder {
|
|||||||
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
|
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
|
||||||
$title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_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 );
|
$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 = [
|
$properties = [
|
||||||
'description' => [
|
'top_description' => [
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'description' => __( 'HTML-omschrijving voor de categorie/term met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
'description' => $top_description_text,
|
||||||
|
'minLength' => 20,
|
||||||
|
],
|
||||||
|
'bottom_description' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => $bottom_description_text,
|
||||||
'minLength' => 20,
|
'minLength' => 20,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -506,7 +606,7 @@ class Groq_AI_Prompt_Builder {
|
|||||||
$schema = [
|
$schema = [
|
||||||
'type' => 'object',
|
'type' => 'object',
|
||||||
'properties' => $properties,
|
'properties' => $properties,
|
||||||
'required' => [ 'description' ],
|
'required' => [ 'top_description', 'bottom_description' ],
|
||||||
'additionalProperties' => false,
|
'additionalProperties' => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -549,14 +649,25 @@ class Groq_AI_Prompt_Builder {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$description = isset( $decoded['description'] ) ? trim( (string) $decoded['description'] ) : '';
|
$top = isset( $decoded['top_description'] ) ? trim( (string) $decoded['top_description'] ) : '';
|
||||||
if ( '' === $description ) {
|
$bottom = isset( $decoded['bottom_description'] ) ? trim( (string) $decoded['bottom_description'] ) : '';
|
||||||
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen description veld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
|
// 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 = [
|
$result = [];
|
||||||
'description' => $description,
|
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'] ) ) {
|
if ( isset( $decoded['meta_title'] ) ) {
|
||||||
$result['meta_title'] = $this->truncate_meta_field( (string) $decoded['meta_title'], 60 );
|
$result['meta_title'] = $this->truncate_meta_field( (string) $decoded['meta_title'], 60 );
|
||||||
@@ -583,8 +694,11 @@ class Groq_AI_Prompt_Builder {
|
|||||||
|
|
||||||
private function get_term_structured_response_instructions( $settings = null ) {
|
private function get_term_structured_response_instructions( $settings = null ) {
|
||||||
$schema_parts = [
|
$schema_parts = [
|
||||||
'"description":"..."',
|
'"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 );
|
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
|
||||||
if ( $rankmath_enabled ) {
|
if ( $rankmath_enabled ) {
|
||||||
@@ -600,11 +714,54 @@ class Groq_AI_Prompt_Builder {
|
|||||||
$json_structure
|
$json_structure
|
||||||
);
|
);
|
||||||
|
|
||||||
$instruction .= ' ' . __( 'Zorg dat description geldige HTML bevat (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 .= ' ' . __( '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 .= ' ' . __( 'Als in de context een sectie "Interne links" staat, verwerk dan 2–5 van deze links natuurlijk in de description als HTML-links (<a href="URL">Anker</a>).', 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;
|
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 ) {
|
private function get_top_products_for_term( $taxonomy, $term_id, $limit = 10 ) {
|
||||||
$taxonomy = sanitize_key( (string) $taxonomy );
|
$taxonomy = sanitize_key( (string) $taxonomy );
|
||||||
$term_id = absint( $term_id );
|
$term_id = absint( $term_id );
|
||||||
@@ -616,9 +773,15 @@ class Groq_AI_Prompt_Builder {
|
|||||||
'post_status' => 'publish',
|
'post_status' => 'publish',
|
||||||
'posts_per_page' => $limit,
|
'posts_per_page' => $limit,
|
||||||
'no_found_rows' => true,
|
'no_found_rows' => true,
|
||||||
'meta_key' => 'total_sales',
|
'orderby' => 'date',
|
||||||
'orderby' => 'meta_value_num',
|
|
||||||
'order' => 'DESC',
|
'order' => 'DESC',
|
||||||
|
'meta_query' => [
|
||||||
|
[
|
||||||
|
'key' => '_stock_status',
|
||||||
|
'value' => 'instock',
|
||||||
|
'compare' => '=',
|
||||||
|
],
|
||||||
|
],
|
||||||
'tax_query' => [
|
'tax_query' => [
|
||||||
[
|
[
|
||||||
'taxonomy' => $taxonomy,
|
'taxonomy' => $taxonomy,
|
||||||
@@ -796,7 +959,7 @@ class Groq_AI_Prompt_Builder {
|
|||||||
return substr( $text, 0, $limit );
|
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' ) ) {
|
if ( ! function_exists( 'wc_get_product' ) ) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -813,14 +976,27 @@ class Groq_AI_Prompt_Builder {
|
|||||||
return '';
|
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 = [];
|
$lines = [];
|
||||||
|
|
||||||
foreach ( $attributes as $attribute ) {
|
foreach ( $attributes as $attribute ) {
|
||||||
if ( $attribute->is_taxonomy() ) {
|
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 ) );
|
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $terms ) );
|
||||||
$label = wc_attribute_label( $attribute->get_name() );
|
$label = wc_attribute_label( $taxonomy_name );
|
||||||
} else {
|
} else {
|
||||||
|
if ( ! $include_custom ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$options = $attribute->get_options();
|
$options = $attribute->get_options();
|
||||||
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $options ) );
|
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $options ) );
|
||||||
$label = sanitize_text_field( $attribute->get_name() );
|
$label = sanitize_text_field( $attribute->get_name() );
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class Groq_AI_Settings_Manager {
|
|||||||
'store_context' => '',
|
'store_context' => '',
|
||||||
'default_prompt' => '',
|
'default_prompt' => '',
|
||||||
'max_output_tokens' => 2048,
|
'max_output_tokens' => 2048,
|
||||||
|
'logs_retention_days' => 90,
|
||||||
|
'product_attribute_includes' => [],
|
||||||
|
'term_bottom_description_meta_key' => '',
|
||||||
'groq_api_key' => '',
|
'groq_api_key' => '',
|
||||||
'openai_api_key' => '',
|
'openai_api_key' => '',
|
||||||
'google_api_key' => '',
|
'google_api_key' => '',
|
||||||
@@ -45,18 +48,24 @@ class Groq_AI_Settings_Manager {
|
|||||||
'google_enable_ga' => true,
|
'google_enable_ga' => true,
|
||||||
'google_gsc_site_url' => '',
|
'google_gsc_site_url' => '',
|
||||||
'google_ga4_property_id' => '',
|
'google_ga4_property_id' => '',
|
||||||
|
'google_safety_settings' => [],
|
||||||
'context_fields' => $this->get_default_context_fields(),
|
'context_fields' => $this->get_default_context_fields(),
|
||||||
'modules' => $this->get_default_modules_settings(),
|
'modules' => $this->get_default_modules_settings(),
|
||||||
'image_context_mode' => 'url',
|
'image_context_mode' => 'url',
|
||||||
'image_context_limit' => 3,
|
'image_context_limit' => 3,
|
||||||
'response_format_compat' => false,
|
'response_format_compat' => false,
|
||||||
|
'term_top_description_char_limit' => 600,
|
||||||
|
'term_bottom_description_char_limit' => 1200,
|
||||||
];
|
];
|
||||||
|
|
||||||
$settings = get_option( $this->option_key, [] );
|
$settings = get_option( $this->option_key, [] );
|
||||||
$settings = wp_parse_args( (array) $settings, $defaults );
|
$settings = wp_parse_args( (array) $settings, $defaults );
|
||||||
$settings['context_fields'] = $this->normalize_context_fields( isset( $settings['context_fields'] ) ? $settings['context_fields'] : [] );
|
$settings['context_fields'] = $this->normalize_context_fields( isset( $settings['context_fields'] ) ? $settings['context_fields'] : [] );
|
||||||
$settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] );
|
$settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] );
|
||||||
|
$settings['google_safety_settings'] = $this->sanitize_google_safety_settings( isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [] );
|
||||||
$settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' );
|
$settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' );
|
||||||
|
$logs_retention_days = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90;
|
||||||
|
$settings['logs_retention_days'] = max( 0, min( 3650, $logs_retention_days ) );
|
||||||
|
|
||||||
$image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
|
$image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
|
||||||
if ( 'none' === $image_mode ) {
|
if ( 'none' === $image_mode ) {
|
||||||
@@ -73,6 +82,19 @@ class Groq_AI_Settings_Manager {
|
|||||||
$limit = isset( $settings['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $settings['image_context_limit'] ) : 3;
|
$limit = isset( $settings['image_context_limit'] ) ? $this->sanitize_image_context_limit_value( $settings['image_context_limit'] ) : 3;
|
||||||
$settings['image_context_limit'] = $limit;
|
$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;
|
return $settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +111,9 @@ class Groq_AI_Settings_Manager {
|
|||||||
'store_context' => '',
|
'store_context' => '',
|
||||||
'default_prompt' => '',
|
'default_prompt' => '',
|
||||||
'max_output_tokens' => 2048,
|
'max_output_tokens' => 2048,
|
||||||
|
'logs_retention_days' => 90,
|
||||||
|
'product_attribute_includes' => [],
|
||||||
|
'term_bottom_description_meta_key' => '',
|
||||||
'groq_api_key' => '',
|
'groq_api_key' => '',
|
||||||
'openai_api_key' => '',
|
'openai_api_key' => '',
|
||||||
'google_api_key' => '',
|
'google_api_key' => '',
|
||||||
@@ -101,11 +126,14 @@ class Groq_AI_Settings_Manager {
|
|||||||
'google_enable_ga' => true,
|
'google_enable_ga' => true,
|
||||||
'google_gsc_site_url' => '',
|
'google_gsc_site_url' => '',
|
||||||
'google_ga4_property_id' => '',
|
'google_ga4_property_id' => '',
|
||||||
|
'google_safety_settings' => [],
|
||||||
'context_fields' => $this->get_default_context_fields(),
|
'context_fields' => $this->get_default_context_fields(),
|
||||||
'modules' => $this->get_default_modules_settings(),
|
'modules' => $this->get_default_modules_settings(),
|
||||||
'image_context_mode' => 'url',
|
'image_context_mode' => 'url',
|
||||||
'image_context_limit' => 3,
|
'image_context_limit' => 3,
|
||||||
'response_format_compat' => false,
|
'response_format_compat' => false,
|
||||||
|
'term_top_description_char_limit' => 600,
|
||||||
|
'term_bottom_description_char_limit' => 1200,
|
||||||
];
|
];
|
||||||
|
|
||||||
$current_settings = $this->all();
|
$current_settings = $this->all();
|
||||||
@@ -135,6 +163,10 @@ class Groq_AI_Settings_Manager {
|
|||||||
// Keep within sane bounds across providers.
|
// Keep within sane bounds across providers.
|
||||||
$max_output_tokens = max( 128, min( 8192, $max_output_tokens ) );
|
$max_output_tokens = max( 128, min( 8192, $max_output_tokens ) );
|
||||||
|
|
||||||
|
$logs_retention_days = isset( $input['logs_retention_days'] ) ? (int) $input['logs_retention_days'] : (int) $defaults['logs_retention_days'];
|
||||||
|
// 0 = keep indefinitely, otherwise cap at 10 years.
|
||||||
|
$logs_retention_days = max( 0, min( 3650, $logs_retention_days ) );
|
||||||
|
|
||||||
$context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] );
|
$context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] );
|
||||||
|
|
||||||
if ( 'none' === $image_mode ) {
|
if ( 'none' === $image_mode ) {
|
||||||
@@ -143,12 +175,24 @@ class Groq_AI_Settings_Manager {
|
|||||||
$context_fields['images'] = true;
|
$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 [
|
return [
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
'store_context' => sanitize_textarea_field( $input['store_context'] ),
|
'store_context' => sanitize_textarea_field( $input['store_context'] ),
|
||||||
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
|
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
|
||||||
'max_output_tokens' => $max_output_tokens,
|
'max_output_tokens' => $max_output_tokens,
|
||||||
|
'logs_retention_days' => $logs_retention_days,
|
||||||
|
'product_attribute_includes' => $this->sanitize_product_attribute_includes( isset( $raw_input['product_attribute_includes'] ) ? $raw_input['product_attribute_includes'] : [] ),
|
||||||
|
'term_bottom_description_meta_key' => sanitize_key( (string) $input['term_bottom_description_meta_key'] ),
|
||||||
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
|
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
|
||||||
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
|
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
|
||||||
'google_api_key' => sanitize_text_field( $input['google_api_key'] ),
|
'google_api_key' => sanitize_text_field( $input['google_api_key'] ),
|
||||||
@@ -161,9 +205,12 @@ class Groq_AI_Settings_Manager {
|
|||||||
'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ),
|
'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ),
|
||||||
'google_gsc_site_url' => esc_url_raw( (string) $input['google_gsc_site_url'] ),
|
'google_gsc_site_url' => esc_url_raw( (string) $input['google_gsc_site_url'] ),
|
||||||
'google_ga4_property_id' => sanitize_text_field( (string) $input['google_ga4_property_id'] ),
|
'google_ga4_property_id' => sanitize_text_field( (string) $input['google_ga4_property_id'] ),
|
||||||
|
'google_safety_settings' => $this->sanitize_google_safety_settings( isset( $raw_input['google_safety_settings'] ) ? $raw_input['google_safety_settings'] : [] ),
|
||||||
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
|
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
|
||||||
'image_context_mode' => $image_mode,
|
'image_context_mode' => $image_mode,
|
||||||
'image_context_limit' => $image_limit,
|
'image_context_limit' => $image_limit,
|
||||||
|
'term_top_description_char_limit' => $top_char_limit,
|
||||||
|
'term_bottom_description_char_limit' => $bottom_char_limit,
|
||||||
'context_fields' => $context_fields,
|
'context_fields' => $context_fields,
|
||||||
'modules' => $this->sanitize_modules_settings(
|
'modules' => $this->sanitize_modules_settings(
|
||||||
$modules_posted ? $raw_input['modules'] : [],
|
$modules_posted ? $raw_input['modules'] : [],
|
||||||
@@ -174,6 +221,49 @@ 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() {
|
public function get_context_field_definitions() {
|
||||||
if ( null === $this->context_field_definitions ) {
|
if ( null === $this->context_field_definitions ) {
|
||||||
$this->context_field_definitions = [
|
$this->context_field_definitions = [
|
||||||
@@ -197,6 +287,11 @@ class Groq_AI_Settings_Manager {
|
|||||||
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
'default' => false,
|
'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' => [
|
'images' => [
|
||||||
'label' => __( 'Afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
'label' => __( 'Afbeeldingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
@@ -224,7 +319,7 @@ class Groq_AI_Settings_Manager {
|
|||||||
$normalized = [];
|
$normalized = [];
|
||||||
|
|
||||||
foreach ( $definitions as $key => $data ) {
|
foreach ( $definitions as $key => $data ) {
|
||||||
$normalized[ $key ] = false;
|
$normalized[ $key ] = ! empty( $data['default'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! is_array( $fields ) ) {
|
if ( ! is_array( $fields ) ) {
|
||||||
@@ -282,7 +377,7 @@ class Groq_AI_Settings_Manager {
|
|||||||
$config = $this->get_module_config( 'rankmath', $settings );
|
$config = $this->get_module_config( 'rankmath', $settings );
|
||||||
$limit = isset( $config['focus_keyword_limit'] ) ? absint( $config['focus_keyword_limit'] ) : 3;
|
$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 ) {
|
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
|
||||||
@@ -320,6 +415,124 @@ class Groq_AI_Settings_Manager {
|
|||||||
return $this->sanitize_image_context_limit_value( $limit );
|
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 get_google_safety_settings( $settings = null ) {
|
||||||
|
if ( null === $settings ) {
|
||||||
|
$settings = $this->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sanitize_google_safety_settings( isset( $settings['google_safety_settings'] ) ? $settings['google_safety_settings'] : [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_google_safety_categories() {
|
||||||
|
return self::get_google_safety_categories_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_google_safety_thresholds() {
|
||||||
|
return self::get_google_safety_thresholds_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_logs_retention_days( $settings = null ) {
|
||||||
|
if ( null === $settings ) {
|
||||||
|
$settings = $this->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = isset( $settings['logs_retention_days'] ) ? (int) $settings['logs_retention_days'] : 90;
|
||||||
|
return max( 0, min( 3650, $value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_loggable_settings_snapshot( $settings = null ) {
|
||||||
|
if ( null === $settings ) {
|
||||||
|
$settings = $this->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_keys = [
|
||||||
|
'store_context',
|
||||||
|
'default_prompt',
|
||||||
|
'max_output_tokens',
|
||||||
|
'logs_retention_days',
|
||||||
|
'product_attribute_includes',
|
||||||
|
'context_fields',
|
||||||
|
'modules',
|
||||||
|
'image_context_mode',
|
||||||
|
'image_context_limit',
|
||||||
|
'response_format_compat',
|
||||||
|
'term_top_description_char_limit',
|
||||||
|
'term_bottom_description_char_limit',
|
||||||
|
'term_bottom_description_meta_key',
|
||||||
|
'google_safety_settings',
|
||||||
|
'google_enable_gsc',
|
||||||
|
'google_enable_ga',
|
||||||
|
'google_gsc_site_url',
|
||||||
|
'google_ga4_property_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
$snapshot = [];
|
||||||
|
|
||||||
|
foreach ( $allowed_keys as $key ) {
|
||||||
|
if ( array_key_exists( $key, $settings ) ) {
|
||||||
|
$snapshot[ $key ] = $settings[ $key ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_google_safety_categories_list() {
|
||||||
|
return [
|
||||||
|
'HARM_CATEGORY_HARASSMENT' => [
|
||||||
|
'label' => __( 'Harassment & intimidatie', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'description' => __( 'Detecteert bedreigingen en pesterijen in de output.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
'HARM_CATEGORY_HATE_SPEECH' => [
|
||||||
|
'label' => __( 'Haatspraak', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'description' => __( 'Beperkt discriminerende of denigrerende taal.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
'HARM_CATEGORY_SEXUALLY_EXPLICIT' => [
|
||||||
|
'label' => __( 'Seksueel expliciet', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'description' => __( 'Filtert beschrijvingen van seksuele handelingen of fetish-content.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
'HARM_CATEGORY_DANGEROUS_CONTENT' => [
|
||||||
|
'label' => __( 'Gevaarlijke activiteiten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'description' => __( 'Voorkomt instructies rond geweld, wapens of gevaarlijke middelen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
'HARM_CATEGORY_CIVIC_INTEGRITY' => [
|
||||||
|
'label' => __( 'Civieke integriteit', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'description' => __( 'Vermindert desinformatie rond verkiezingen en burgerprocessen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_google_safety_thresholds_list() {
|
||||||
|
return [
|
||||||
|
'' => __( 'Google standaard (niet meesturen)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'HARM_BLOCK_THRESHOLD_UNSPECIFIED' => __( 'Onbekende drempel (laat Google beslissen)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'BLOCK_LOW_AND_ABOVE' => __( 'Blokkeer lage ernst en hoger', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'BLOCK_MEDIUM_AND_ABOVE' => __( 'Blokkeer middel en hoger', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'BLOCK_ONLY_HIGH' => __( 'Blokkeer alleen hoge ernst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
'BLOCK_NONE' => __( 'Sta alles toe (geen blokkade)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function is_response_format_compat_enabled( $settings = null ) {
|
public function is_response_format_compat_enabled( $settings = null ) {
|
||||||
if ( null === $settings ) {
|
if ( null === $settings ) {
|
||||||
$settings = $this->all();
|
$settings = $this->all();
|
||||||
@@ -375,7 +588,7 @@ class Groq_AI_Settings_Manager {
|
|||||||
if ( $limit <= 0 ) {
|
if ( $limit <= 0 ) {
|
||||||
$limit = $module_default_config['focus_keyword_limit'];
|
$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'] );
|
$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 ) {
|
if ( $title_pixel_limit <= 0 ) {
|
||||||
@@ -403,4 +616,27 @@ class Groq_AI_Settings_Manager {
|
|||||||
|
|
||||||
return min( 10, $limit );
|
return min( 10, $limit );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function sanitize_google_safety_settings( $settings ) {
|
||||||
|
if ( ! is_array( $settings ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = array_keys( self::get_google_safety_categories_list() );
|
||||||
|
$thresholds = array_keys( self::get_google_safety_thresholds_list() );
|
||||||
|
$clean = [];
|
||||||
|
|
||||||
|
foreach ( $settings as $category => $threshold ) {
|
||||||
|
$category = sanitize_text_field( (string) $category );
|
||||||
|
$threshold = sanitize_text_field( (string) $threshold );
|
||||||
|
|
||||||
|
if ( '' === $threshold || ! in_array( $category, $categories, true ) || ! in_array( $threshold, $thresholds, true ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clean[ $category ] = $threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
202
languages/siti-ai-product-content-generator.pot
Normal file
202
languages/siti-ai-product-content-generator.pot
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: SitiAI Product Teksten\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-01-31 00:00+0000\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Domain: siti-ai-product-content-generator\n"
|
||||||
|
|
||||||
|
#: assets/js/settings.js:166
|
||||||
|
msgid "Deze aanbieder ondersteunt dit niet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/settings.js:173
|
||||||
|
msgid "Vul eerst de API-sleutel in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/settings.js:177
|
||||||
|
msgid "Modellen worden opgehaald…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/settings.js:197
|
||||||
|
msgid "Onbekende fout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/settings.js:202
|
||||||
|
msgid "Modellen bijgewerkt."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/settings.js:205
|
||||||
|
msgid "Ophalen mislukt."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-settings-page.php:844
|
||||||
|
msgid "Deze aanbieder ondersteunt dit niet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-settings-page.php:845
|
||||||
|
msgid "Vul eerst de API-sleutel in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-settings-page.php:846
|
||||||
|
msgid "Modellen worden opgehaald…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-settings-page.php:847
|
||||||
|
msgid "Onbekende fout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-settings-page.php:848
|
||||||
|
msgid "Modellen bijgewerkt."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-settings-page.php:849
|
||||||
|
msgid "Ophalen mislukt."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-admin.js:24
|
||||||
|
msgid "Vul eerst een prompt in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-admin.js:25
|
||||||
|
msgid "AI is bezig met schrijven..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-admin.js:26
|
||||||
|
msgid "Tekst gegenereerd. Je kunt hem toepassen en opslaan."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-admin.js:27
|
||||||
|
msgid "Tekst ingevuld. Vergeet niet op \"Opslaan\" te klikken."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-admin.js:28
|
||||||
|
msgid "Er ging iets mis bij het genereren."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-admin.js:119
|
||||||
|
msgid "Onbekende fout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:212
|
||||||
|
msgid "Onbekende fout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:100
|
||||||
|
msgid "AI is bezig met schrijven..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:101
|
||||||
|
msgid "Probeer het opnieuw of pas je prompt/context aan."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:102
|
||||||
|
msgid "Er ging iets mis bij het genereren."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:103
|
||||||
|
msgid "Onbekende fout."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:104
|
||||||
|
msgid "Structuur gegenereerd. Kopieer of vul velden in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:105
|
||||||
|
msgid "%s ingevuld."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:106
|
||||||
|
msgid "Kon het veld niet automatisch invullen."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:107
|
||||||
|
msgid "%s gekopieerd naar het klembord."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:108
|
||||||
|
msgid "JSON gekopieerd naar het klembord."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/admin.js:109
|
||||||
|
msgid "Kopiëren mislukt."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-product-ui.php:84
|
||||||
|
msgid "%s ingevuld."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-product-ui.php:85
|
||||||
|
msgid "Kon het veld niet automatisch invullen."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-product-ui.php:86
|
||||||
|
msgid "%s gekopieerd naar het klembord."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-product-ui.php:87
|
||||||
|
msgid "JSON gekopieerd naar het klembord."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-product-ui.php:88
|
||||||
|
msgid "Kopiëren mislukt."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-bulk.js:149
|
||||||
|
msgid "Onbekende fout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-bulk.js:150
|
||||||
|
msgid "%1$s: %2$s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-bulk.js:152
|
||||||
|
msgid "%1$s mislukt: %2$s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-bulk.js:159
|
||||||
|
msgid "%1$s gevuld."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-bulk.js:161
|
||||||
|
msgid "%s is bijgewerkt."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-bulk.js:236
|
||||||
|
msgid "Stoppen?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: assets/js/term-bulk.js:254
|
||||||
|
msgid "Onbekende term."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:101
|
||||||
|
msgid "Onbekende fout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:102
|
||||||
|
msgid "Onbekende term."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:103
|
||||||
|
msgid "Stoppen?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:104
|
||||||
|
msgid "%1$s: %2$s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:105
|
||||||
|
msgid "%1$s gevuld."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:106
|
||||||
|
msgid "%1$s mislukt: %2$s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/class-groq-ai-term-admin-base.php:107
|
||||||
|
msgid "%s is bijgewerkt."
|
||||||
|
msgstr ""
|
||||||
13
phpunit.xml
Normal file
13
phpunit.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
failOnWarning="false"
|
||||||
|
failOnRisky="false"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Plugin Tests">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
17
tests/ModelExclusionsTest.php
Normal file
17
tests/ModelExclusionsTest.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ModelExclusionsTest extends TestCase {
|
||||||
|
public function test_ensure_allowed_blocks_excluded_model() {
|
||||||
|
$blocked = Groq_AI_Model_Exclusions::ensure_allowed( 'groq', 'whisper-large-v3' );
|
||||||
|
$this->assertSame( '', $blocked );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_filter_models_removes_excluded_entries() {
|
||||||
|
$models = [ 'llama3-70b-8192', 'whisper-large-v3', 'mixtral-8x7b-32768' ];
|
||||||
|
$filtered = Groq_AI_Model_Exclusions::filter_models( 'groq', $models );
|
||||||
|
|
||||||
|
$this->assertSame( [ 'llama3-70b-8192', 'mixtral-8x7b-32768' ], $filtered );
|
||||||
|
}
|
||||||
|
}
|
||||||
102
tests/ProviderRequestBuilderTest.php
Normal file
102
tests/ProviderRequestBuilderTest.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ProviderRequestBuilderTest extends TestCase {
|
||||||
|
public function test_openai_request_payload_respects_settings() {
|
||||||
|
$provider = new Groq_AI_Provider_OpenAI();
|
||||||
|
$result = $provider->generate_content(
|
||||||
|
[
|
||||||
|
'prompt' => 'Hallo',
|
||||||
|
'system_prompt' => 'System',
|
||||||
|
'model' => 'gpt-4o-mini',
|
||||||
|
'settings' => [
|
||||||
|
'openai_api_key' => 'test-key',
|
||||||
|
'max_output_tokens' => 512,
|
||||||
|
],
|
||||||
|
'temperature' => 0.5,
|
||||||
|
'response_format' => [
|
||||||
|
'type' => 'json_object',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertIsArray( $result );
|
||||||
|
$payload = $result['request_payload']['body'];
|
||||||
|
$this->assertSame( 'gpt-4o-mini', $payload['model'] );
|
||||||
|
$this->assertSame( 0.5, $payload['temperature'] );
|
||||||
|
$this->assertSame( 512, $payload['max_tokens'] );
|
||||||
|
$this->assertSame( 'json_object', $payload['response_format']['type'] );
|
||||||
|
$this->assertSame( 'System', $payload['messages'][0]['content'] );
|
||||||
|
$this->assertSame( 'Hallo', $payload['messages'][1]['content'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_groq_request_payload_uses_default_model_when_missing() {
|
||||||
|
$provider = new Groq_AI_Provider_Groq();
|
||||||
|
$result = $provider->generate_content(
|
||||||
|
[
|
||||||
|
'prompt' => 'Hallo',
|
||||||
|
'system_prompt' => 'System',
|
||||||
|
'settings' => [
|
||||||
|
'groq_api_key' => 'test-key',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertIsArray( $result );
|
||||||
|
$payload = $result['request_payload']['body'];
|
||||||
|
$this->assertSame( $provider->get_default_model(), $payload['model'] );
|
||||||
|
$this->assertSame( 'System', $payload['messages'][0]['content'] );
|
||||||
|
$this->assertSame( 'Hallo', $payload['messages'][1]['content'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_google_request_payload_builds_schema_and_images() {
|
||||||
|
$provider = new Groq_AI_Provider_Google();
|
||||||
|
$result = $provider->generate_content(
|
||||||
|
[
|
||||||
|
'prompt' => 'Hallo',
|
||||||
|
'system_prompt' => 'System',
|
||||||
|
'model' => 'gemini-1.5-flash',
|
||||||
|
'settings' => [
|
||||||
|
'google_api_key' => 'test-key',
|
||||||
|
'max_output_tokens' => 256,
|
||||||
|
'google_safety_settings' => [
|
||||||
|
'HARM_CATEGORY_HARASSMENT' => 'BLOCK_LOW_AND_ABOVE',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'temperature' => 0.2,
|
||||||
|
'response_format' => [
|
||||||
|
'type' => 'json_schema',
|
||||||
|
'json_schema' => [
|
||||||
|
'schema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'name' => [
|
||||||
|
'type' => 'string',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'image_context' => [
|
||||||
|
[
|
||||||
|
'label' => 'Image 1',
|
||||||
|
'mime_type' => 'image/png',
|
||||||
|
'data' => 'BASE64DATA',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertIsArray( $result );
|
||||||
|
$payload = $result['request_payload']['body'];
|
||||||
|
$this->assertSame( 0.2, $payload['generationConfig']['temperature'] );
|
||||||
|
$this->assertSame( 256, $payload['generationConfig']['maxOutputTokens'] );
|
||||||
|
$this->assertSame( 'application/json', $payload['generationConfig']['responseMimeType'] );
|
||||||
|
$this->assertArrayHasKey( 'responseJsonSchema', $payload['generationConfig'] );
|
||||||
|
$this->assertSame( 'System', $payload['contents'][0]['parts'][0]['text'] );
|
||||||
|
$this->assertSame( 'Hallo', $payload['contents'][0]['parts'][1]['text'] );
|
||||||
|
$this->assertSame( 'image/png', $payload['contents'][0]['parts'][3]['inline_data']['mime_type'] );
|
||||||
|
$this->assertSame( 'BASE64DATA', $payload['contents'][0]['parts'][3]['inline_data']['data'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
112
tests/SettingsManagerTest.php
Normal file
112
tests/SettingsManagerTest.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class SettingsManagerTest extends TestCase {
|
||||||
|
private function make_manager() {
|
||||||
|
$provider_manager = new Groq_AI_Provider_Manager();
|
||||||
|
return new Groq_AI_Settings_Manager( 'groq_ai_test_settings', $provider_manager );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_logs_retention_days_sanitized_and_capped() {
|
||||||
|
$manager = $this->make_manager();
|
||||||
|
|
||||||
|
$result = $manager->sanitize( [
|
||||||
|
'logs_retention_days' => 5000,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertSame( 3650, $result['logs_retention_days'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_logs_retention_days_allows_zero() {
|
||||||
|
$manager = $this->make_manager();
|
||||||
|
|
||||||
|
$result = $manager->sanitize( [
|
||||||
|
'logs_retention_days' => 0,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertSame( 0, $result['logs_retention_days'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_logs_retention_days_negative_becomes_zero() {
|
||||||
|
$manager = $this->make_manager();
|
||||||
|
|
||||||
|
$result = $manager->sanitize( [
|
||||||
|
'logs_retention_days' => -5,
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->assertSame( 0, $result['logs_retention_days'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sanitize_accepts_all_settings_keys() {
|
||||||
|
$manager = $this->make_manager();
|
||||||
|
$context_fields = $manager->get_default_context_fields();
|
||||||
|
$modules = $manager->get_default_modules_settings();
|
||||||
|
$google_categories = Groq_AI_Settings_Manager::get_google_safety_categories_list();
|
||||||
|
$first_category = array_key_first( $google_categories );
|
||||||
|
|
||||||
|
$input = [
|
||||||
|
'provider' => 'openai',
|
||||||
|
'model' => 'gpt-4o-mini',
|
||||||
|
'store_context' => 'Test winkelcontext',
|
||||||
|
'default_prompt' => 'Schrijf een korte tekst',
|
||||||
|
'max_output_tokens' => 2048,
|
||||||
|
'logs_retention_days' => 30,
|
||||||
|
'product_attribute_includes' => [ '__all__', '__custom__', 'pa_color', 'invalid key' ],
|
||||||
|
'term_bottom_description_meta_key' => 'custom_bottom_key',
|
||||||
|
'groq_api_key' => 'groq-key',
|
||||||
|
'openai_api_key' => 'openai-key',
|
||||||
|
'google_api_key' => 'google-key',
|
||||||
|
'google_oauth_client_id' => 'client-id',
|
||||||
|
'google_oauth_client_secret' => 'client-secret',
|
||||||
|
'google_oauth_refresh_token' => 'refresh-token',
|
||||||
|
'google_oauth_connected_email' => 'user@example.com',
|
||||||
|
'google_oauth_connected_at' => 123456,
|
||||||
|
'google_enable_gsc' => true,
|
||||||
|
'google_enable_ga' => false,
|
||||||
|
'google_gsc_site_url' => 'https://example.com/',
|
||||||
|
'google_ga4_property_id' => '123456',
|
||||||
|
'google_safety_settings' => $first_category ? [ $first_category => 'BLOCK_LOW_AND_ABOVE' ] : [],
|
||||||
|
'context_fields' => $context_fields,
|
||||||
|
'modules' => $modules,
|
||||||
|
'image_context_mode' => 'base64',
|
||||||
|
'image_context_limit' => 5,
|
||||||
|
'response_format_compat' => true,
|
||||||
|
'term_top_description_char_limit' => 700,
|
||||||
|
'term_bottom_description_char_limit' => 1400,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $manager->sanitize( $input );
|
||||||
|
|
||||||
|
$this->assertSame( 'openai', $result['provider'] );
|
||||||
|
$this->assertSame( 'gpt-4o-mini', $result['model'] );
|
||||||
|
$this->assertSame( 'Test winkelcontext', $result['store_context'] );
|
||||||
|
$this->assertSame( 'Schrijf een korte tekst', $result['default_prompt'] );
|
||||||
|
$this->assertSame( 2048, $result['max_output_tokens'] );
|
||||||
|
$this->assertSame( 30, $result['logs_retention_days'] );
|
||||||
|
$this->assertContains( '__all__', $result['product_attribute_includes'] );
|
||||||
|
$this->assertContains( '__custom__', $result['product_attribute_includes'] );
|
||||||
|
$this->assertContains( 'pa_color', $result['product_attribute_includes'] );
|
||||||
|
$this->assertSame( 'custom_bottom_key', $result['term_bottom_description_meta_key'] );
|
||||||
|
$this->assertSame( 'groq-key', $result['groq_api_key'] );
|
||||||
|
$this->assertSame( 'openai-key', $result['openai_api_key'] );
|
||||||
|
$this->assertSame( 'google-key', $result['google_api_key'] );
|
||||||
|
$this->assertSame( 'client-id', $result['google_oauth_client_id'] );
|
||||||
|
$this->assertSame( 'client-secret', $result['google_oauth_client_secret'] );
|
||||||
|
$this->assertSame( 'refresh-token', $result['google_oauth_refresh_token'] );
|
||||||
|
$this->assertSame( 'user@example.com', $result['google_oauth_connected_email'] );
|
||||||
|
$this->assertSame( 123456, $result['google_oauth_connected_at'] );
|
||||||
|
$this->assertTrue( $result['google_enable_gsc'] );
|
||||||
|
$this->assertFalse( $result['google_enable_ga'] );
|
||||||
|
$this->assertSame( 'https://example.com/', $result['google_gsc_site_url'] );
|
||||||
|
$this->assertSame( '123456', $result['google_ga4_property_id'] );
|
||||||
|
$this->assertIsArray( $result['google_safety_settings'] );
|
||||||
|
$this->assertIsArray( $result['context_fields'] );
|
||||||
|
$this->assertIsArray( $result['modules'] );
|
||||||
|
$this->assertSame( 'base64', $result['image_context_mode'] );
|
||||||
|
$this->assertSame( 5, $result['image_context_limit'] );
|
||||||
|
$this->assertTrue( $result['response_format_compat'] );
|
||||||
|
$this->assertSame( 700, $result['term_top_description_char_limit'] );
|
||||||
|
$this->assertSame( 1400, $result['term_bottom_description_char_limit'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
67
tests/TermSaveTest.php
Normal file
67
tests/TermSaveTest.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class TermSaveTest extends TestCase {
|
||||||
|
protected function setUp(): void {
|
||||||
|
$GLOBALS['wp_term_updates'] = [];
|
||||||
|
$GLOBALS['wp_term_meta_updates'] = [];
|
||||||
|
$GLOBALS['wp_filters'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_save_term_generation_result_saves_descriptions_and_filtered_meta_key() {
|
||||||
|
$plugin = new class {
|
||||||
|
public function get_settings() {
|
||||||
|
return [ 'term_bottom_description_meta_key' => '' ];
|
||||||
|
}
|
||||||
|
public function is_module_enabled( $module, $settings = null ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$controller_ref = new ReflectionClass( Groq_AI_Ajax_Controller::class );
|
||||||
|
$controller = $controller_ref->newInstanceWithoutConstructor();
|
||||||
|
$plugin_prop = $controller_ref->getProperty( 'plugin' );
|
||||||
|
$plugin_prop->setAccessible( true );
|
||||||
|
$plugin_prop->setValue( $controller, $plugin );
|
||||||
|
|
||||||
|
add_filter(
|
||||||
|
'groq_ai_term_bottom_description_meta_key',
|
||||||
|
function ( $default_key ) {
|
||||||
|
return 'Custom Key';
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
$term = (object) [
|
||||||
|
'term_id' => 12,
|
||||||
|
'taxonomy' => 'product_cat',
|
||||||
|
'name' => 'Test',
|
||||||
|
'description' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'top_description' => '<p>Dit is een test.</p>',
|
||||||
|
'bottom_description' => '<p>Onderste tekst.</p>',
|
||||||
|
];
|
||||||
|
|
||||||
|
$settings = $plugin->get_settings();
|
||||||
|
|
||||||
|
$method = $controller_ref->getMethod( 'save_term_generation_result' );
|
||||||
|
$method->setAccessible( true );
|
||||||
|
$saved = $method->invoke( $controller, $term, $result, $settings );
|
||||||
|
|
||||||
|
$this->assertIsArray( $saved );
|
||||||
|
$this->assertSame( 4, $saved['words'] );
|
||||||
|
|
||||||
|
$this->assertCount( 1, $GLOBALS['wp_term_updates'] );
|
||||||
|
$this->assertSame( 12, $GLOBALS['wp_term_updates'][0]['term_id'] );
|
||||||
|
$this->assertSame( 'product_cat', $GLOBALS['wp_term_updates'][0]['taxonomy'] );
|
||||||
|
$this->assertSame( '<p>Dit is een test.</p>', $GLOBALS['wp_term_updates'][0]['args']['description'] );
|
||||||
|
|
||||||
|
$this->assertArrayHasKey( 12, $GLOBALS['wp_term_meta_updates'] );
|
||||||
|
$this->assertArrayHasKey( 'customkey', $GLOBALS['wp_term_meta_updates'][12] );
|
||||||
|
$this->assertSame( '<p>Onderste tekst.</p>', $GLOBALS['wp_term_meta_updates'][12]['customkey'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
258
tests/bootstrap.php
Normal file
258
tests/bootstrap.php
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if ( ! defined( 'GROQ_AI_PRODUCT_TEXT_DOMAIN' ) ) {
|
||||||
|
define( 'GROQ_AI_PRODUCT_TEXT_DOMAIN', 'siti-ai-product-content-generator' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
|
||||||
|
define( 'HOUR_IN_SECONDS', 3600 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'DAY_IN_SECONDS' ) ) {
|
||||||
|
define( 'DAY_IN_SECONDS', 86400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! class_exists( 'WP_Error' ) ) {
|
||||||
|
class WP_Error {
|
||||||
|
private $message;
|
||||||
|
|
||||||
|
public function __construct( $code = '', $message = '' ) {
|
||||||
|
$this->message = (string) $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_error_message() {
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'is_wp_error' ) ) {
|
||||||
|
function is_wp_error( $thing ) {
|
||||||
|
return $thing instanceof WP_Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( '__' ) ) {
|
||||||
|
function __( $text ) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_parse_args' ) ) {
|
||||||
|
function wp_parse_args( $args, $defaults = [] ) {
|
||||||
|
if ( is_object( $args ) ) {
|
||||||
|
$args = get_object_vars( $args );
|
||||||
|
}
|
||||||
|
if ( ! is_array( $args ) ) {
|
||||||
|
$args = [];
|
||||||
|
}
|
||||||
|
return array_merge( $defaults, $args );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'sanitize_text_field' ) ) {
|
||||||
|
function sanitize_text_field( $text ) {
|
||||||
|
return trim( (string) $text );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'sanitize_textarea_field' ) ) {
|
||||||
|
function sanitize_textarea_field( $text ) {
|
||||||
|
return trim( (string) $text );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'sanitize_key' ) ) {
|
||||||
|
function sanitize_key( $key ) {
|
||||||
|
$key = strtolower( (string) $key );
|
||||||
|
return preg_replace( '/[^a-z0-9_\-]/', '', $key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'absint' ) ) {
|
||||||
|
function absint( $number ) {
|
||||||
|
return abs( (int) $number );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'esc_url_raw' ) ) {
|
||||||
|
function esc_url_raw( $url ) {
|
||||||
|
return (string) $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'add_filter' ) ) {
|
||||||
|
function add_filter( $tag, $callback, $priority = 10, $accepted_args = 1 ) {
|
||||||
|
$GLOBALS['wp_filters'][ $tag ][ $priority ][] = [
|
||||||
|
'callback' => $callback,
|
||||||
|
'accepted_args' => (int) $accepted_args,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'apply_filters' ) ) {
|
||||||
|
function apply_filters( $tag, $value ) {
|
||||||
|
$args = func_get_args();
|
||||||
|
if ( empty( $GLOBALS['wp_filters'][ $tag ] ) ) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
ksort( $GLOBALS['wp_filters'][ $tag ] );
|
||||||
|
foreach ( $GLOBALS['wp_filters'][ $tag ] as $callbacks ) {
|
||||||
|
foreach ( $callbacks as $filter ) {
|
||||||
|
$accepted = isset( $filter['accepted_args'] ) ? (int) $filter['accepted_args'] : 1;
|
||||||
|
$call_args = array_slice( $args, 0, max( 1, $accepted ) );
|
||||||
|
$call_args[0] = $value;
|
||||||
|
$value = call_user_func_array( $filter['callback'], $call_args );
|
||||||
|
$args[0] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_kses_post' ) ) {
|
||||||
|
function wp_kses_post( $content ) {
|
||||||
|
return (string) $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_strip_all_tags' ) ) {
|
||||||
|
function wp_strip_all_tags( $text ) {
|
||||||
|
return strip_tags( (string) $text );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_update_term' ) ) {
|
||||||
|
function wp_update_term( $term_id, $taxonomy, $args = [] ) {
|
||||||
|
$GLOBALS['wp_term_updates'][] = [
|
||||||
|
'term_id' => (int) $term_id,
|
||||||
|
'taxonomy' => (string) $taxonomy,
|
||||||
|
'args' => $args,
|
||||||
|
];
|
||||||
|
return [ 'term_id' => (int) $term_id ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'update_term_meta' ) ) {
|
||||||
|
function update_term_meta( $term_id, $meta_key, $meta_value ) {
|
||||||
|
$term_id = (int) $term_id;
|
||||||
|
if ( ! isset( $GLOBALS['wp_term_meta_updates'][ $term_id ] ) ) {
|
||||||
|
$GLOBALS['wp_term_meta_updates'][ $term_id ] = [];
|
||||||
|
}
|
||||||
|
$GLOBALS['wp_term_meta_updates'][ $term_id ][ (string) $meta_key ] = $meta_value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_json_encode' ) ) {
|
||||||
|
function wp_json_encode( $data, $options = 0, $depth = 512 ) {
|
||||||
|
return json_encode( $data, $options, $depth );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'add_query_arg' ) ) {
|
||||||
|
function add_query_arg( $args, $url = '' ) {
|
||||||
|
if ( is_string( $args ) ) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
$query = http_build_query( (array) $args );
|
||||||
|
$separator = strpos( $url, '?' ) === false ? '?' : '&';
|
||||||
|
return $url . $separator . $query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_remote_post' ) ) {
|
||||||
|
function wp_remote_post( $url, $args = [] ) {
|
||||||
|
$GLOBALS['wp_last_http_request'] = [
|
||||||
|
'url' => $url,
|
||||||
|
'args' => $args,
|
||||||
|
];
|
||||||
|
|
||||||
|
$body = json_encode(
|
||||||
|
[
|
||||||
|
'choices' => [
|
||||||
|
[
|
||||||
|
'message' => [
|
||||||
|
'content' => 'ok',
|
||||||
|
],
|
||||||
|
'finish_reason' => 'stop',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'usage' => [
|
||||||
|
'prompt_tokens' => 10,
|
||||||
|
'completion_tokens' => 20,
|
||||||
|
'total_tokens' => 30,
|
||||||
|
],
|
||||||
|
'candidates' => [
|
||||||
|
[
|
||||||
|
'content' => [
|
||||||
|
'parts' => [
|
||||||
|
[ 'text' => 'ok' ],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'finishReason' => 'STOP',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'usageMetadata' => [
|
||||||
|
'promptTokenCount' => 10,
|
||||||
|
'candidatesTokenCount' => 20,
|
||||||
|
'totalTokenCount' => 30,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'body' => $body,
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_remote_get' ) ) {
|
||||||
|
function wp_remote_get( $url, $args = [] ) {
|
||||||
|
$GLOBALS['wp_last_http_request'] = [
|
||||||
|
'url' => $url,
|
||||||
|
'args' => $args,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'body' => json_encode( [ 'data' => [], 'models' => [] ] ),
|
||||||
|
'response' => [ 'code' => 200 ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_remote_retrieve_body' ) ) {
|
||||||
|
function wp_remote_retrieve_body( $response ) {
|
||||||
|
return isset( $response['body'] ) ? $response['body'] : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'wp_remote_retrieve_response_code' ) ) {
|
||||||
|
function wp_remote_retrieve_response_code( $response ) {
|
||||||
|
return isset( $response['response']['code'] ) ? (int) $response['response']['code'] : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'get_option' ) ) {
|
||||||
|
function get_option( $key, $default = false ) {
|
||||||
|
return isset( $GLOBALS['wp_options'][ $key ] ) ? $GLOBALS['wp_options'][ $key ] : $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'update_option' ) ) {
|
||||||
|
function update_option( $key, $value ) {
|
||||||
|
$GLOBALS['wp_options'][ $key ] = $value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../includes/Core/class-groq-ai-model-exclusions.php';
|
||||||
|
require_once __DIR__ . '/../includes/Contracts/interface-groq-ai-provider.php';
|
||||||
|
require_once __DIR__ . '/../includes/Providers/class-groq-ai-abstract-openai-provider.php';
|
||||||
|
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-groq.php';
|
||||||
|
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-openai.php';
|
||||||
|
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-google.php';
|
||||||
|
require_once __DIR__ . '/../includes/Providers/class-groq-ai-provider-manager.php';
|
||||||
|
require_once __DIR__ . '/../includes/Services/Settings/class-groq-ai-settings-manager.php';
|
||||||
|
require_once __DIR__ . '/../includes/Core/class-groq-ai-ajax-controller.php';
|
||||||
Reference in New Issue
Block a user