diff --git a/.vibe/config.toml b/.vibe/config.toml new file mode 100644 index 0000000..6d57383 --- /dev/null +++ b/.vibe/config.toml @@ -0,0 +1,186 @@ +active_model = "mistral-medium-3.5" +vim_keybindings = false +disable_welcome_banner_animation = false +displayed_workdir = "" +auto_compact_threshold = 200000 +context_warnings = false +textual_theme = "catppuccin-mocha" +instructions = "" +system_prompt_id = "vibe" +include_commit_signature = true +include_model_info = true +include_project_context = true +include_prompt_detail = true +enable_update_checks = true +api_timeout = 720.0 +tool_paths = [] +mcp_servers = [] +enabled_tools = [] +disabled_tools = [] + +[[providers]] +name = "mistral" +api_base = "https://api.mistral.ai/v1" +api_key_env_var = "MISTRAL_API_KEY" +api_style = "openai" +backend = "mistral" + +[[providers]] +name = "llamacpp" +api_base = "http://127.0.0.1:8080/v1" +api_key_env_var = "" +api_style = "openai" +backend = "generic" + +[[models]] +name = "mistral-vibe-cli-latest" +provider = "mistral" +alias = "mistral-medium-3.5" +temperature = 1.0 +input_price = 1.5 +output_price = 7.5 +thinking = "high" + +[[models]] +name = "devstral-small-latest" +provider = "mistral" +alias = "devstral-small" +temperature = 0.2 +input_price = 0.1 +output_price = 0.3 + +[[models]] +name = "devstral" +provider = "llamacpp" +alias = "local" +temperature = 0.2 +input_price = 0.0 +output_price = 0.0 + +[project_context] +max_chars = 40000 +default_commit_count = 5 +max_doc_bytes = 32768 +truncation_buffer = 1000 +max_depth = 3 +max_files = 1000 +max_dirs_per_level = 20 +timeout_seconds = 2.0 + +[session_logging] +save_dir = "/Users/jasperfrumau/.vibe/logs/session" +session_prefix = "session" +enabled = true + +[tools.search_replace] +permission = "ask" +allowlist = [] +denylist = [] +max_content_size = 100000 +create_backup = false +fuzzy_threshold = 0.9 + +[tools.bash] +permission = "ask" +allowlist = [ + "cat", + "echo", + "file", + "find", + "git diff", + "git log", + "git status", + "head", + "ls", + "pwd", + "stat", + "tail", + "tree", + "uname", + "wc", + "which", + "whoami", +] +denylist = [ + "gdb", + "pdb", + "passwd", + "nano", + "vim", + "vi", + "emacs", + "bash -i", + "sh -i", + "zsh -i", + "fish -i", + "dash -i", + "screen", + "tmux", +] +max_output_bytes = 16000 +default_timeout = 30 +denylist_standalone = [ + "ipython", + "bash", + "sh", + "nohup", + "vi", + "vim", + "emacs", + "nano", + "su", +] + +[tools.grep] +permission = "always" +allowlist = [] +denylist = [] +max_output_bytes = 64000 +default_max_matches = 100 +default_timeout = 60 +exclude_patterns = [ + ".venv/", + "venv/", + ".env/", + "env/", + "node_modules/", + ".git/", + "__pycache__/", + ".pytest_cache/", + ".mypy_cache/", + ".tox/", + ".nox/", + ".coverage/", + "htmlcov/", + "dist/", + "build/", + ".idea/", + ".vscode/", + "*.egg-info", + "*.pyc", + "*.pyo", + "*.pyd", + ".DS_Store", + "Thumbs.db", +] +codeignore_file = ".vibeignore" + +[tools.read_file] +permission = "always" +allowlist = [] +denylist = [] +max_read_bytes = 64000 +max_state_history = 10 + +[tools.todo] +permission = "always" +allowlist = [] +denylist = [] +max_todos = 100 + +[tools.write_file] +permission = "ask" +allowlist = [] +denylist = [] +max_write_bytes = 64000 +create_parent_dirs = true diff --git a/.vibe/prompts/vibe.md b/.vibe/prompts/vibe.md new file mode 100644 index 0000000..6b7cb82 --- /dev/null +++ b/.vibe/prompts/vibe.md @@ -0,0 +1,86 @@ +# Custom Instructions for Vibe + +## Primary Directive +Always follow the rules and conventions in the project's `CLAUDE.md` file. This is your authoritative source for all project-specific guidance about Waygate. + +## Project Overview +Waygate is a standalone WordPress plugin (`imagewize/waygate`) that provides AI-powered pattern page builder capabilities for the Elayne block theme. It integrates with the WordPress Abilities API (WP 7.0+) and WordPress AI Client to generate pages from natural-language descriptions. + +- **Namespace:** `Imagewize\Waygate` +- **Requires:** PHP 8.3+, WordPress 7.0+ +- **All classes** live in `includes/` with classmap autoloading + +## Before Responding +1. **Review CLAUDE.md** for: + - Project architecture and file structure + - Class responsibilities (Pattern_Lab, Abilities_API, AI_Integration, Admin) + - Initialization order + - Code style and naming conventions + - Git commit guidelines + +2. **Check Current Context**: + - Verify you're working in the plugin directory + - Confirm the current branch and git status + - Review recent commits for relevant changes + +3. **Adhere to Key Principles**: + - Never mention AI tools (Claude, Mistral, Vibe) in commit messages + - Use conventional commit prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:` + - Make atomic commits: each commit is one logical change that builds/passes on its own + - All PHP code must pass `vendor/bin/phpcs --standard=WordPress` + +## Commands +**Install dependencies:** +```bash +composer install +``` + +**PHP syntax check:** +```bash +find . -name "*.php" -not -path "*/vendor/*" -exec php -l {} \; +``` + +**Run PHP CodeSniffer:** +```bash +vendor/bin/phpcs +``` + +**Run PHPUnit:** +```bash +vendor/bin/phpunit --configuration phpunit.xml +``` + +## Architecture +- **Pattern_Lab** (`includes/class-pattern-lab.php`) — Data layer for pattern querying and page composition +- **Abilities_API** (`includes/class-abilities-api.php`) — Registers `elayne/list-patterns` and `elayne/create-page` abilities +- **AI_Integration** (`includes/class-ai-integration.php`) — Orchestrates AI page generation with Mistral provider +- **Admin** (`includes/class-admin.php`) — WordPress admin UI at **Tools → Waygate** + +**Initialization Order:** +``` +plugins_loaded → Pattern_Lab::init() + → AI_Integration::init() + → Abilities_API::init() + → Admin::init() +``` + +`Pattern_Lab` must init first since other classes depend on its pattern data. + +## Response Guidelines +- Be concise and technical +- Reference specific files and line numbers +- Use markdown formatting for code and structure +- Prioritize verification over assumptions +- When unsure, ask for clarification before acting + +## Version Bump Checklist +When releasing a new version, update the version string in **three places**: +1. `waygate.php` line 8 — `* Version:` plugin header +2. `waygate.php` line 19 — `define( 'WAYGATE_VERSION', ... )` constant +3. `CHANGELOG.md` — add a new `## [x.y.z] - YYYY-MM-DD` section at the top + +## NO Mistral Vibe Attribution in Any Commits +- Do NOT include "Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe " attribution +- Keep all commits professional and attribution-free +- This applies to ALL files and directories in the entire repository +- Follow standard git commit message format diff --git a/CHANGELOG.md b/CHANGELOG.md index c328b00..90b9eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to Waygate will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.0] - 2026-05-24 + +### Added +- Six built-in prompt templates (Homepage, About, Services, Contact, Landing Page, Portfolio) in `AI_Integration::get_prompt_templates()`; filterable via the `waygate_prompt_templates` hook so third-party themes/plugins can add or remove templates +- **Quick Template** dropdown in the admin AI generation form — selecting a template pre-fills the description textarea; a confirmation dialog fires when the textarea already has content +- `[placeholder]` substitution note added to the description field hint +- PHPUnit tests for `get_prompt_templates()` and the `waygate_prompt_templates` filter +- `phpcs.xml` PHP CodeSniffer configuration committed so `vendor/bin/phpcs` works without arguments + +### Changed +- Renamed plugin classes to WordPress underscore convention: `Pattern_Lab`, `Abilities_API`, `AI_Integration` (previously `PatternLab`, `AbilitiesApi`, `AiIntegration`) to comply with WordPress coding standards and allow WordPress.org submission +- Removed `WordPress.Files.FileName.InvalidClassFileName` PHPCS exclusion from `phpcs.xml` — the renamed classes now pass the sniff cleanly +- All PHP files now pass WPCS coding standards (resolved via `phpcs.xml` ruleset) + ## [0.5.0] - 2026-05-24 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index fa784e0..ae9166b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ find . -name "*.php" -not -path "*/vendor/*" -exec php -l {} \; **Run PHP CodeSniffer:** ```bash -vendor/bin/phpcs --standard=WordPress includes/ waygate.php +vendor/bin/phpcs ``` **Run PHPUnit:** @@ -38,28 +38,28 @@ All classes live in `includes/` with PSR-4 autoloading (`Imagewize\Waygate` → ### Class Responsibilities -**`PatternLab`** (`includes/class-pattern-lab.php`) — The data layer. Queries WordPress's registered block patterns to extract those belonging to the active Elayne theme, returning structured metadata (slug, title, description, categories, keywords). Also handles composing `wp:pattern` blocks into a page and calling `wp_insert_post`. +**`Pattern_Lab`** (`includes/class-pattern-lab.php`) — The data layer. Queries WordPress's registered block patterns to extract those belonging to the active Elayne theme, returning structured metadata (slug, title, description, categories, keywords). Also handles composing `wp:pattern` blocks into a page and calling `wp_insert_post`. -**`AbilitiesApi`** (`includes/class-abilities-api.php`) — Registers two WordPress Abilities (WP 7.0+ feature): +**`Abilities_API`** (`includes/class-abilities-api.php`) — Registers two WordPress Abilities (WP 7.0+ feature): - `elayne/list-patterns` — lists available patterns, optionally filtered by category; requires `edit_posts` - `elayne/create-page` — creates a draft page from an ordered pattern slug list; requires `publish_pages` Each ability has a JSON input/output schema for capability validation. -**`AiIntegration`** (`includes/class-ai-integration.php`) — Orchestrates AI page generation. Registers a Mistral provider with the WP AI Client registry, then in `generate_page()` builds a prompt from all available patterns, sends it to the AI (with fallback chain: Mistral → Claude → OpenAI → Gemini), parses the JSON response, and delegates to `PatternLab::create_page()`. Enforces layout constraints: 3–7 patterns, hero first, CTA last, no consecutive grid patterns. +**`AI_Integration`** (`includes/class-ai-integration.php`) — Orchestrates AI page generation. Registers a Mistral provider with the WP AI Client registry, then in `generate_page()` builds a prompt from all available patterns, sends it to the AI (with fallback chain: Mistral → Claude → OpenAI → Gemini), parses the JSON response, and delegates to `Pattern_Lab::create_page()`. Enforces layout constraints: 3–7 patterns, hero first, CTA last, no consecutive grid patterns. **`Admin`** (`includes/class-admin.php`) — WordPress admin UI at **Tools → Waygate**. Renders status indicators (WP AI Client, Abilities API, Mistral provider, Elayne patterns), the AI generation form, and a searchable pattern catalog. Handles POST form submission with nonce verification and displays success/error notices. ### Initialization Order ``` -plugins_loaded → PatternLab::init() - → AiIntegration::init() - → AbilitiesApi::init() +plugins_loaded → Pattern_Lab::init() + → AI_Integration::init() + → Abilities_API::init() → Admin::init() ``` -`PatternLab` must init first since the other classes depend on its pattern data. +`Pattern_Lab` must init first since the other classes depend on its pattern data. ### External Dependencies (optional) diff --git a/README.md b/README.md index 6036c76..a44b90f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Waygate lets you assemble WordPress pages from block patterns — manually or via a natural-language AI prompt powered by the WordPress AI Client (WordPress 7.0+). Works with any block theme; [Elayne](https://github.com/imagewize/elayne) is the primary supported theme. -> **Beta** — v0.5.0. Use on staging/development sites; not yet recommended for production. +> **Beta** — v0.6.0. Use on staging/development sites; not yet recommended for production. --- @@ -14,6 +14,7 @@ Waygate lets you assemble WordPress pages from block patterns — manually or vi - **AI page generation** — Describe the page you want; the AI picks patterns and creates a draft - **AI reasoning** — The AI's one-sentence explanation of its pattern choices is shown after generation and persisted as post meta on the created page - **Developer debug info** — When `WP_ENV=development`, the page editor sidebar and the generation notice also show the ordered pattern slugs and generation timestamp +- **Prompt templates** — Six built-in page templates (Homepage, About, Services, Contact, Landing Page, Portfolio) pre-fill the AI prompt; extend via the `waygate_prompt_templates` filter - **Feature detection** — AI form is hidden automatically when no provider supports text generation - **Abilities API** — Exposes `elayne/list-patterns` and `elayne/create-page` abilities for WP 7.0+ - **Multi-provider** — Works with Mistral, Claude, OpenAI, or Gemini via WP AI Client diff --git a/composer.json b/composer.json index a58d99f..32f094e 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "../../plugins/{$name}/": ["type:wordpress-plugin"] }, "archive": { - "exclude": ["docs/", "tests/", ".github/", "*.md", "!README.md"] + "exclude": ["docs/", "tests/", ".github/", ".vibe/", "*.md", "!README.md"] } }, "config": { diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ba2e6fb..5e55304 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,7 +1,7 @@ # Waygate Roadmap & Improvement Ideas -> **Document Version**: 1.0.0 -> **Last Updated**: 2026-05-22 +> **Document Version**: 1.1.0 +> **Last Updated**: 2026-05-24 > **Status**: Draft This document outlines potential improvements and feature additions for Waygate, based on WordPress 7.0 AI Client and Abilities API capabilities. @@ -73,9 +73,9 @@ Based on [Introducing the AI Client in WordPress 7.0](https://make.wordpress.org ## Improvement Ideas -### Phase 1: Quick Wins (Low Effort, High Impact) +### Phase 1: Quick Wins ✅ Complete -#### 1. Add Feature Detection +#### 1. Add Feature Detection ✅ **File**: `includes/class-ai-integration.php` **File**: `includes/class-admin.php` @@ -88,7 +88,7 @@ $image_gen_supported = $builder->is_supported_for_image_generation(); **Benefit**: Hide AI features when no compatible provider is configured. -#### 2. Add Ability Annotations +#### 2. Add Ability Annotations ✅ **File**: `includes/class-abilities-api.php` ```php @@ -121,7 +121,7 @@ wp_register_ability( **Benefit**: Better REST API behavior, clearer ability semantics. -#### 3. Support Generic Pattern Prefixes +#### 3. Support Generic Pattern Prefixes ✅ **File**: `includes/class-pattern-lab.php` ```php @@ -134,14 +134,14 @@ public static function get_patterns(): array { **Benefit**: Works with any block theme, not just Elayne. -#### 4. Add Pattern Category Filter in Admin UI +#### 4. Add Pattern Category Filter in Admin UI ✅ **File**: `includes/class-admin.php` Add a dropdown to filter patterns by category in the catalog table. **Benefit**: Easier pattern discovery for users. -### Phase 2: Medium Effort +### Phase 2: Medium Effort ← **Next Up** #### 5. Add Image Generation for Pattern Previews **New file**: `includes/class-image-generator.php` @@ -446,21 +446,23 @@ public static function track_pattern_usage( string $pattern_slug ): void { ### Recommended Order -1. Feature detection, ability annotations, generic prefixes — *Phase 1* (2–3 days with tests) -2. REST API, client-side abilities, prompt templates — *Phase 2* (3–5 days) -3. Image generation for previews — *Phase 2* (2–3 days) -4. Batch page creation — *Phase 2* (1–2 days) -5. Cost tracking, pattern popularity — *Phase 3* (2–3 days) -6. Advanced features based on user feedback — *Phase 3, speculative* +1. ~~Feature detection, ability annotations, generic prefixes — *Phase 1*~~ ✅ Done +2. Prompt templates — *Phase 2* (1–2 days) ← **Start here** +3. REST API endpoints — *Phase 2* (2–3 days) +4. Client-side abilities for editor integration — *Phase 2* (2–3 days) +5. Image generation for previews — *Phase 2* (2–3 days) +6. Batch page creation — *Phase 2* (1–2 days) +7. Cost tracking, pattern popularity — *Phase 3* (2–3 days) +8. Advanced features based on user feedback — *Phase 3, speculative* -### Quick Start (2–3 days with tests) +### Quick Start — Phase 1 Complete ✅ -> The four items below are low-effort code changes, but "1 day" assumes no test coverage. If you're writing or updating PHPUnit tests alongside each change (recommended), budget 2–3 days. +All four Phase 1 items shipped as of 2026-05-24: -- Add feature detection -- Add ability annotations -- Add generic pattern prefix filter -- Add category filter to admin UI +- ~~Add feature detection~~ ✅ +- ~~Add ability annotations~~ ✅ +- ~~Add generic pattern prefix filter~~ ✅ +- ~~Add category filter to admin UI~~ ✅ --- @@ -574,3 +576,4 @@ const abilities = getAbilities(); | Date | Author | Change | |------|--------|--------| | 2026-05-22 | Initial draft | Created roadmap document | +| 2026-05-24 | Jasper Frumau | Marked Phase 1 complete; Phase 2 is next | diff --git a/includes/class-abilities-api.php b/includes/class-abilities-api.php index 31d44ba..0fea4ab 100644 --- a/includes/class-abilities-api.php +++ b/includes/class-abilities-api.php @@ -1,15 +1,29 @@ __( 'List Elayne Patterns', 'waygate' ), 'description' => __( 'Returns all available Elayne block patterns with slug, title, categories and keywords.', 'waygate' ), 'category' => 'content', - 'input_schema' => [ + 'input_schema' => array( 'type' => 'object', - 'properties' => [ - 'category' => [ + 'properties' => array( + 'category' => array( 'type' => 'string', 'description' => 'Optional category filter, e.g. "hero", "features", "cta".', - ], - ], - ], - 'output_schema' => [ + ), + ), + ), + 'output_schema' => array( 'type' => 'array', - 'items' => [ + 'items' => array( 'type' => 'object', - 'properties' => [ - 'slug' => [ 'type' => 'string' ], - 'title' => [ 'type' => 'string' ], - 'categories' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], - 'keywords' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], - ], - ], - ], - 'execute_callback' => function ( array $params = [] ): array { - $patterns = PatternLab::get_patterns(); + 'properties' => array( + 'slug' => array( 'type' => 'string' ), + 'title' => array( 'type' => 'string' ), + 'categories' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'keywords' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + ), + 'execute_callback' => function ( array $params = array() ): array { + $patterns = Pattern_Lab::get_patterns(); if ( ! empty( $params['category'] ) ) { $cat = 'elayne/' . sanitize_key( $params['category'] ); @@ -55,63 +75,66 @@ public static function register_abilities(): void { return $patterns; }, 'permission_callback' => fn() => current_user_can( 'edit_posts' ), - 'meta' => [ + 'meta' => array( 'show_in_rest' => true, - 'annotations' => [ 'readonly' => true ], - ], - ] + 'annotations' => array( 'readonly' => true ), + ), + ) ); wp_register_ability( 'elayne/create-page', - [ + array( 'label' => __( 'Create Page from Elayne Patterns', 'waygate' ), 'description' => __( 'Creates a new WordPress draft page assembled from Elayne block patterns.', 'waygate' ), 'category' => 'content', - 'input_schema' => [ + 'input_schema' => array( 'type' => 'object', - 'properties' => [ - 'title' => [ + 'properties' => array( + 'title' => array( 'type' => 'string', 'description' => 'Page title.', - ], - 'patterns' => [ + ), + 'patterns' => array( 'type' => 'array', - 'items' => [ 'type' => 'string' ], + 'items' => array( 'type' => 'string' ), 'description' => 'Ordered list of Elayne pattern slugs.', - ], - 'status' => [ + ), + 'status' => array( 'type' => 'string', - 'enum' => [ 'draft', 'publish' ], + 'enum' => array( 'draft', 'publish' ), 'default' => 'draft', - ], - ], - 'required' => [ 'title', 'patterns' ], - ], + ), + ), + 'required' => array( 'title', 'patterns' ), + ), 'execute_callback' => function ( array $params ): array { - $result = PatternLab::create_page( + $result = Pattern_Lab::create_page( $params['title'], $params['patterns'], $params['status'] ?? 'draft' ); if ( is_wp_error( $result ) ) { - return [ 'success' => false, 'error' => $result->get_error_message() ]; + return array( + 'success' => false, + 'error' => $result->get_error_message(), + ); } - return [ + return array( 'success' => true, 'page_id' => $result, 'edit_url' => get_edit_post_link( $result, 'raw' ), 'view_url' => get_permalink( $result ), - ]; + ); }, 'permission_callback' => fn() => current_user_can( 'publish_pages' ), - 'meta' => [ + 'meta' => array( 'show_in_rest' => true, - 'annotations' => [ 'idempotent' => true ], - ], - ] + 'annotations' => array( 'idempotent' => true ), + ), + ) ); } } diff --git a/includes/class-admin.php b/includes/class-admin.php index 5a1f82e..6a830ce 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -1,59 +1,76 @@ 'You do not have permission to create pages.' ]; + $result = array( 'error' => 'You do not have permission to create pages.' ); } elseif ( ! $text_gen_supported ) { - $result = [ 'error' => 'Text generation is not supported by the configured AI provider.' ]; + $result = array( 'error' => 'Text generation is not supported by the configured AI provider.' ); } else { $description = sanitize_textarea_field( wp_unslash( $_POST['description'] ?? '' ) ); if ( empty( $description ) ) { - $result = [ 'error' => 'Please describe the page you want to create.' ]; + $result = array( 'error' => 'Please describe the page you want to create.' ); } else { - $result = AiIntegration::generate_page( $description ); + $result = AI_Integration::generate_page( $description ); } } } - $all_patterns = PatternLab::get_patterns(); + $all_patterns = Pattern_Lab::get_patterns(); usort( $all_patterns, fn( $a, $b ) => strcmp( $a['slug'], $b['slug'] ) ); // Build unique category list (strip namespace prefix for display/filtering). - $all_categories = []; + $all_categories = array(); foreach ( $all_patterns as $p ) { foreach ( $p['categories'] as $cat ) { - $parts = explode( '/', $cat ); + $parts = explode( '/', $cat ); $all_categories[ end( $parts ) ] = true; } } @@ -100,6 +117,20 @@ function ( $p ) use ( $selected_category ) { + + + + @@ -119,6 +150,19 @@ class="large-text" +
@@ -181,6 +225,12 @@ function ( $c ) { ✓ Elayne Patterns - patterns registered + patterns registered

Error: ' . esc_html( $result['error'] ) . '

'; @@ -264,17 +322,25 @@ private static function result_notice( array $result ): void { ID, '_waygate_reasoning', true ); @@ -288,7 +354,7 @@ public static function render_meta_box( \WP_Post $post ): void { if ( self::is_dev() ) { $patterns_json = get_post_meta( $post->ID, '_waygate_patterns', true ); $generated_at = get_post_meta( $post->ID, '_waygate_generated_at', true ); - $patterns = $patterns_json ? json_decode( $patterns_json, true ) : []; + $patterns = $patterns_json ? json_decode( $patterns_json, true ) : array(); if ( $generated_at ) { echo '

Generated: ' . esc_html( $generated_at ) . '

'; diff --git a/includes/class-ai-integration.php b/includes/class-ai-integration.php index 7353bb4..c1d8774 100644 --- a/includes/class-ai-integration.php +++ b/includes/class-ai-integration.php @@ -1,15 +1,76 @@ + */ + private static array $prompt_templates = array( + 'homepage' => array( + 'label' => 'Homepage', + 'description' => 'Hero, features grid, testimonials, and CTA', + 'prompt' => 'Create a homepage for a [industry] business with a hero section, features grid, customer testimonials, and a call-to-action section', + ), + 'about' => array( + 'label' => 'About Page', + 'description' => 'Team bios, company story, and mission', + 'prompt' => 'Create an about page for a [industry] company with team member cards, company history, core values, and a contact form', + ), + 'services' => array( + 'label' => 'Services Page', + 'description' => 'Services listing with benefits and pricing CTA', + 'prompt' => 'Create a services page for a [industry] business showcasing our main services with descriptions, key benefits, and a pricing call-to-action', + ), + 'contact' => array( + 'label' => 'Contact Page', + 'description' => 'Contact form, location, and team info', + 'prompt' => 'Create a contact page with a contact form, office location details, a brief team introduction, and social media links', + ), + 'landing' => array( + 'label' => 'Landing Page', + 'description' => 'Conversion-focused with hero and strong CTA', + 'prompt' => 'Create a landing page for [product or service] with a compelling hero section, key benefits, social proof, and a strong call-to-action', + ), + 'portfolio' => array( + 'label' => 'Portfolio / Work', + 'description' => 'Work showcase with case studies and CTA', + 'prompt' => 'Create a portfolio page for a [industry] studio showcasing selected projects, client logos, a brief process overview, and a hire-us CTA', + ), + ); + + /** + * Returns all prompt templates, allowing third parties to add their own via the filter. + * + * @return array + */ + public static function get_prompt_templates(): array { + return apply_filters( 'waygate_prompt_templates', self::$prompt_templates ); + } + + /** + * Registers the Mistral provider on the init hook. + */ public static function init(): void { - add_action( 'init', [ self::class, 'register_mistral_provider' ], 6 ); + add_action( 'init', array( self::class, 'register_mistral_provider' ), 6 ); } + /** + * Returns true when a configured AI provider supports text generation. + */ public static function is_text_generation_supported(): bool { if ( ! function_exists( 'wp_ai_client_prompt' ) ) { return false; @@ -49,7 +110,7 @@ public static function register_mistral_provider(): void { * @return array{title:string,patterns:string[],pattern_count:int,reasoning:string,edit_url:string,view_url:string}|array{error:string} */ public static function generate_page( string $description ): array { - $patterns = PatternLab::get_patterns(); + $patterns = Pattern_Lab::get_patterns(); $pattern_detail = ''; foreach ( $patterns as $p ) { @@ -57,25 +118,25 @@ public static function generate_page( string $description ): array { $pattern_detail .= "- {$p['slug']} | {$p['title']} | {$cats}\n"; } - $schema = [ + $schema = array( 'type' => 'object', - 'properties' => [ - 'title' => [ + 'properties' => array( + 'title' => array( 'type' => 'string', 'description' => 'Suggested page title.', - ], - 'patterns' => [ + ), + 'patterns' => array( 'type' => 'array', - 'items' => [ 'type' => 'string' ], + 'items' => array( 'type' => 'string' ), 'description' => 'Ordered list of Elayne pattern slugs to assemble the page.', - ], - 'reasoning' => [ + ), + 'reasoning' => array( 'type' => 'string', 'description' => 'One sentence explaining pattern choices.', - ], - ], - 'required' => [ 'title', 'patterns', 'reasoning' ], - ]; + ), + ), + 'required' => array( 'title', 'patterns', 'reasoning' ), + ); $prompt = <<generate_text(); } catch ( \Throwable $e ) { - return [ 'error' => 'AI request failed: ' . $e->getMessage() ]; + return array( 'error' => 'AI request failed: ' . $e->getMessage() ); } if ( is_wp_error( $raw ) ) { - return [ 'error' => 'AI provider error: ' . $raw->get_error_message() ]; + return array( 'error' => 'AI provider error: ' . $raw->get_error_message() ); } if ( ! is_string( $raw ) ) { - return [ 'error' => 'AI returned an unexpected type: ' . gettype( $raw ) ]; + return array( 'error' => 'AI returned an unexpected type: ' . gettype( $raw ) ); } $data = json_decode( $raw, true ); if ( ! is_array( $data ) || empty( $data['patterns'] ) ) { - return [ 'error' => 'AI returned an unexpected response. Raw output: ' . esc_html( substr( $raw, 0, 300 ) ) ]; + return array( 'error' => 'AI returned an unexpected response. Raw output: ' . esc_html( substr( $raw, 0, 300 ) ) ); } - $post_id = PatternLab::create_page( $data['title'] ?? $description, $data['patterns'], 'draft' ); + $post_id = Pattern_Lab::create_page( $data['title'] ?? $description, $data['patterns'], 'draft' ); if ( is_wp_error( $post_id ) ) { - return [ 'error' => $post_id->get_error_message() ]; + return array( 'error' => $post_id->get_error_message() ); } $reasoning = $data['reasoning'] ?? ''; @@ -143,13 +204,13 @@ public static function generate_page( string $description ): array { update_post_meta( $post_id, '_waygate_patterns', wp_json_encode( $data['patterns'] ) ); update_post_meta( $post_id, '_waygate_generated_at', current_time( 'mysql' ) ); - return [ + return array( 'title' => $data['title'] ?? $description, 'patterns' => $data['patterns'], 'pattern_count' => count( $data['patterns'] ), 'reasoning' => $reasoning, 'edit_url' => get_edit_post_link( $post_id, 'raw' ), 'view_url' => get_permalink( $post_id ), - ]; + ); } } diff --git a/includes/class-pattern-lab.php b/includes/class-pattern-lab.php index e0e5310..d00d728 100644 --- a/includes/class-pattern-lab.php +++ b/includes/class-pattern-lab.php @@ -1,11 +1,22 @@ get_all_registered(); - $prefixes = apply_filters( 'waygate_pattern_prefixes', [ 'elayne/' ] ); - $patterns = []; + $prefixes = apply_filters( 'waygate_pattern_prefixes', array( 'elayne/' ) ); + $patterns = array(); foreach ( $all as $p ) { if ( empty( $p['slug'] ) ) { @@ -37,13 +48,13 @@ public static function get_patterns(): array { continue; } - $patterns[] = [ + $patterns[] = array( 'slug' => $p['slug'], 'title' => $p['title'] ?? '', 'description' => $p['description'] ?? '', - 'categories' => $p['categories'] ?? [], - 'keywords' => $p['keywords'] ?? [], - ]; + 'categories' => $p['categories'] ?? array(), + 'keywords' => $p['keywords'] ?? array(), + ); } return $patterns; @@ -80,12 +91,12 @@ public static function create_page( string $title, array $pattern_slugs, string } return wp_insert_post( - [ + array( 'post_title' => sanitize_text_field( $title ), 'post_content' => $content, - 'post_status' => in_array( $status, [ 'draft', 'publish' ], true ) ? $status : 'draft', + 'post_status' => in_array( $status, array( 'draft', 'publish' ), true ) ? $status : 'draft', 'post_type' => 'page', - ], + ), true ); } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..1362cbe --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,9 @@ + + + Waygate PHPCS configuration + + includes/ + waygate.php + + + diff --git a/tests/Unit/AiIntegrationTest.php b/tests/Unit/AiIntegrationTest.php index 3609f77..308f997 100644 --- a/tests/Unit/AiIntegrationTest.php +++ b/tests/Unit/AiIntegrationTest.php @@ -2,7 +2,7 @@ namespace Imagewize\Waygate\Tests\Unit; -use Imagewize\Waygate\AiIntegration; +use Imagewize\Waygate\AI_Integration; use PHPUnit\Framework\TestCase; class AiIntegrationTest extends TestCase { @@ -10,6 +10,62 @@ class AiIntegrationTest extends TestCase { public function test_is_text_generation_not_supported_when_wp_client_missing(): void { // wp_ai_client_prompt is not available in the unit-test environment $this->assertFalse( function_exists( 'wp_ai_client_prompt' ) ); - $this->assertFalse( AiIntegration::is_text_generation_supported() ); + $this->assertFalse( AI_Integration::is_text_generation_supported() ); + } + + public function test_get_prompt_templates_returns_array(): void { + $templates = AI_Integration::get_prompt_templates(); + $this->assertIsArray( $templates ); + } + + public function test_get_prompt_templates_has_expected_keys(): void { + $templates = AI_Integration::get_prompt_templates(); + $this->assertArrayHasKey( 'homepage', $templates ); + $this->assertArrayHasKey( 'about', $templates ); + $this->assertArrayHasKey( 'services', $templates ); + $this->assertArrayHasKey( 'contact', $templates ); + $this->assertArrayHasKey( 'landing', $templates ); + $this->assertArrayHasKey( 'portfolio', $templates ); + } + + public function test_each_template_has_required_fields(): void { + foreach ( AI_Integration::get_prompt_templates() as $key => $tpl ) { + $this->assertArrayHasKey( 'label', $tpl, "Template '{$key}' missing 'label'" ); + $this->assertArrayHasKey( 'description', $tpl, "Template '{$key}' missing 'description'" ); + $this->assertArrayHasKey( 'prompt', $tpl, "Template '{$key}' missing 'prompt'" ); + $this->assertNotEmpty( $tpl['label'], "Template '{$key}' has empty 'label'" ); + $this->assertNotEmpty( $tpl['description'], "Template '{$key}' has empty 'description'" ); + $this->assertNotEmpty( $tpl['prompt'], "Template '{$key}' has empty 'prompt'" ); + } + } + + public function test_get_prompt_templates_is_filterable(): void { + // Simulate a third-party adding a template via the filter. + $extra = array( + 'label' => 'FAQ', + 'description' => 'Frequently asked questions', + 'prompt' => 'Create an FAQ page for a [industry] business', + ); + + // apply_filters is not available in unit tests; call the filter manually. + add_filter( + 'waygate_prompt_templates', + function ( array $templates ) use ( $extra ): array { + $templates['faq'] = $extra; + return $templates; + } + ); + + $templates = AI_Integration::get_prompt_templates(); + + remove_all_filters( 'waygate_prompt_templates' ); + + $this->assertArrayHasKey( 'faq', $templates ); + $this->assertSame( $extra['label'], $templates['faq']['label'] ); + } + + public function test_homepage_template_prompt_contains_placeholder(): void { + $templates = AI_Integration::get_prompt_templates(); + $this->assertStringContainsString( '[industry]', $templates['homepage']['prompt'] ); } } diff --git a/tests/Unit/PatternLabTest.php b/tests/Unit/PatternLabTest.php index d2468e2..b9ea64a 100644 --- a/tests/Unit/PatternLabTest.php +++ b/tests/Unit/PatternLabTest.php @@ -2,7 +2,7 @@ namespace Imagewize\Waygate\Tests\Unit; -use Imagewize\Waygate\PatternLab; +use Imagewize\Waygate\Pattern_Lab; use PHPUnit\Framework\TestCase; use WP_Block_Patterns_Registry; use WP_Error; @@ -33,7 +33,7 @@ public function test_get_patterns_returns_elayne_patterns_by_default(): void { $this->register( 'elayne/hero', 'Hero' ); $this->register( 'othertheme/hero', 'Other Hero' ); - $patterns = PatternLab::get_patterns(); + $patterns = Pattern_Lab::get_patterns(); $this->assertCount( 1, $patterns ); $this->assertSame( 'elayne/hero', $patterns[0]['slug'] ); @@ -45,7 +45,7 @@ public function test_get_patterns_respects_custom_prefix_via_filter(): void { add_filter( 'waygate_pattern_prefixes', fn() => [ 'mytheme/' ] ); - $patterns = PatternLab::get_patterns(); + $patterns = Pattern_Lab::get_patterns(); $this->assertCount( 1, $patterns ); $this->assertSame( 'mytheme/hero', $patterns[0]['slug'] ); @@ -58,7 +58,7 @@ public function test_get_patterns_supports_multiple_prefixes_via_filter(): void add_filter( 'waygate_pattern_prefixes', fn() => [ 'elayne/', 'mytheme/' ] ); - $patterns = PatternLab::get_patterns(); + $patterns = Pattern_Lab::get_patterns(); $slugs = array_column( $patterns, 'slug' ); $this->assertCount( 2, $patterns ); @@ -69,13 +69,13 @@ public function test_get_patterns_supports_multiple_prefixes_via_filter(): void public function test_get_patterns_excludes_entries_without_slug(): void { WP_Block_Patterns_Registry::get_instance()->register( [ 'title' => 'No Slug' ] ); - $this->assertCount( 0, PatternLab::get_patterns() ); + $this->assertCount( 0, Pattern_Lab::get_patterns() ); } public function test_get_patterns_returns_all_metadata_fields(): void { $this->register( 'elayne/hero', 'Hero', [ 'elayne/header' ] ); - $p = PatternLab::get_patterns()[0]; + $p = Pattern_Lab::get_patterns()[0]; $this->assertArrayHasKey( 'slug', $p ); $this->assertArrayHasKey( 'title', $p ); @@ -87,7 +87,7 @@ public function test_get_patterns_returns_all_metadata_fields(): void { // --- create_page() --- public function test_create_page_returns_error_with_no_valid_patterns(): void { - $result = PatternLab::create_page( 'My Page', [ 'nonexistent/slug' ], 'draft' ); + $result = Pattern_Lab::create_page( 'My Page', [ 'nonexistent/slug' ], 'draft' ); $this->assertInstanceOf( WP_Error::class, $result ); $this->assertSame( 'no_valid_patterns', $result->get_error_code() ); @@ -96,7 +96,7 @@ public function test_create_page_returns_error_with_no_valid_patterns(): void { public function test_create_page_skips_slugs_with_invalid_format(): void { $this->register( 'elayne/hero', 'Hero' ); - $result = PatternLab::create_page( 'My Page', [ 'elayne/hero', '../bad', 'noslash' ], 'draft' ); + $result = Pattern_Lab::create_page( 'My Page', [ 'elayne/hero', '../bad', 'noslash' ], 'draft' ); $this->assertIsInt( $result ); } @@ -105,13 +105,13 @@ public function test_create_page_skips_unregistered_slugs(): void { $this->register( 'elayne/hero', 'Hero' ); // elayne/cta passes format check but is not registered - $result = PatternLab::create_page( 'My Page', [ 'elayne/hero', 'elayne/cta' ], 'draft' ); + $result = Pattern_Lab::create_page( 'My Page', [ 'elayne/hero', 'elayne/cta' ], 'draft' ); $this->assertIsInt( $result ); } public function test_create_page_returns_error_when_only_unregistered_slugs(): void { - $result = PatternLab::create_page( 'My Page', [ 'elayne/hero' ], 'draft' ); + $result = Pattern_Lab::create_page( 'My Page', [ 'elayne/hero' ], 'draft' ); $this->assertInstanceOf( WP_Error::class, $result ); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a3d9d3d..7dc32c1 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -22,6 +22,10 @@ function apply_filters( string $tag, mixed $value, mixed ...$args ): mixed { return $value; } +function remove_all_filters( string $tag ): void { + unset( $GLOBALS['wp_filters'][ $tag ] ); +} + function sanitize_text_field( string $str ): string { return trim( strip_tags( $str ) ); } diff --git a/waygate.php b/waygate.php index 2bc21c5..fc8c6c9 100644 --- a/waygate.php +++ b/waygate.php @@ -3,7 +3,7 @@ * Plugin Name: Waygate * Plugin URI: https://github.com/imagewize/waygate * Description: AI-powered pattern page builder for the Elayne block theme. Lists registered patterns, creates pages from pattern slugs, and integrates with WordPress AI Client for natural-language page generation. - * Version: 0.5.0 + * Version: 0.6.0 * Author: Jasper Frumau * Author URI: https://imagewize.com * License: GPL-2.0-or-later @@ -12,11 +12,13 @@ * Domain Path: /languages * Requires at least: 7.0 * Requires PHP: 8.3 + * + * @package Imagewize\Waygate */ defined( 'ABSPATH' ) || exit; -define( 'WAYGATE_VERSION', '0.5.0' ); +define( 'WAYGATE_VERSION', '0.6.0' ); define( 'WAYGATE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'WAYGATE_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); @@ -25,7 +27,7 @@ require_once WAYGATE_PLUGIN_DIR . 'includes/class-ai-integration.php'; require_once WAYGATE_PLUGIN_DIR . 'includes/class-admin.php'; -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\PatternLab', 'init' ] ); -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\AiIntegration', 'init' ] ); -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\AbilitiesApi', 'init' ] ); -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\Admin', 'init' ] ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\Pattern_Lab', 'init' ) ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\AI_Integration', 'init' ) ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\Abilities_API', 'init' ) ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\Admin', 'init' ) );