-
Notifications
You must be signed in to change notification settings - Fork 34
Improve AI Experiments page UI with header, accordion, and entry points #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
802ceb8
25b84b4
3eceb83
1c4eca3
6ab17e8
3b65f50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -42,6 +42,24 @@ class Settings_Page { | |||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| private const PAGE_SLUG = 'ai-experiments'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * URL pointing to the plugin repository for contributions. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * @since x.x.x | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * @var string | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| private const CONTRIBUTION_URL = 'https://github.com/WordPress/ai'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * URL pointing to the plugin documentation. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * @since x.x.x | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * @var string | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| private const DOCUMENTATION_URL = 'https://github.com/WordPress/ai/tree/develop/docs'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Constructor. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -125,8 +143,37 @@ public function render_page(): void { | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| $global_enabled = (bool) get_option( Settings_Registration::GLOBAL_OPTION, false ); | ||||||||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||||||||
| <div class="wrap"> | ||||||||||||||||||||||||||||||||||||||||
| <h1><?php echo esc_html( get_admin_page_title() ); ?></h1> | ||||||||||||||||||||||||||||||||||||||||
| <div class="wrap ai-experiments-page"> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-admin-header"> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-admin-header__inner"> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-admin-header__left"> | ||||||||||||||||||||||||||||||||||||||||
| <span class="ai-admin-header__icon"> | ||||||||||||||||||||||||||||||||||||||||
| <?php echo \WordPress\AI\get_ai_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> | ||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-admin-header__title"> | ||||||||||||||||||||||||||||||||||||||||
| <h1><?php echo esc_html( get_admin_page_title() ); ?></h1> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-admin-header__right"> | ||||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||||
| class="button button-secondary" | ||||||||||||||||||||||||||||||||||||||||
| href="<?php echo esc_url( self::DOCUMENTATION_URL ); ?>" | ||||||||||||||||||||||||||||||||||||||||
| target="_blank" | ||||||||||||||||||||||||||||||||||||||||
| rel="noopener noreferrer" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| <?php esc_html_e( 'Docs', 'ai' ); ?> | ||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||||
| class="button button-primary" | ||||||||||||||||||||||||||||||||||||||||
| href="<?php echo esc_url( self::CONTRIBUTION_URL ); ?>" | ||||||||||||||||||||||||||||||||||||||||
| target="_blank" | ||||||||||||||||||||||||||||||||||||||||
| rel="noopener noreferrer" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| <?php esc_html_e( 'Contribute', 'ai' ); ?> | ||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||||||
| // If we don't have proper credentials, show an error message and return early. | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -151,7 +198,6 @@ public function render_page(): void { | |||||||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <?php settings_errors( 'ai_experiments' ); ?> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <form method="post" action="options.php"> | ||||||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||||||
| settings_fields( Settings_Registration::OPTION_GROUP ); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -200,16 +246,19 @@ public function render_page(): void { | |||||||||||||||||||||||||||||||||||||||
| <?php endif; ?> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <ul class="ai-experiments__list"> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-experiments__grid"> | ||||||||||||||||||||||||||||||||||||||||
| <?php foreach ( $this->registry->get_all_experiments() as $experiment ) : ?> | ||||||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||||||
| $experiment_id = $experiment->get_id(); | ||||||||||||||||||||||||||||||||||||||||
| $experiment_option = "ai_experiment_{$experiment_id}_enabled"; | ||||||||||||||||||||||||||||||||||||||||
| $experiment_enabled = (bool) get_option( $experiment_option, false ); | ||||||||||||||||||||||||||||||||||||||||
| $disabled_class = ! $global_enabled ? 'ai-experiments__item--disabled' : ''; | ||||||||||||||||||||||||||||||||||||||||
| $desc_id = "ai-experiment-{$experiment_id}-desc"; | ||||||||||||||||||||||||||||||||||||||||
| $settings_id = "ai-experiment-{$experiment_id}-settings"; | ||||||||||||||||||||||||||||||||||||||||
| $has_settings = $experiment->has_settings(); | ||||||||||||||||||||||||||||||||||||||||
| $entry_points = $experiment->get_entry_points(); | ||||||||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||||||||
| <li class="ai-experiments__item <?php echo esc_attr( $disabled_class ); ?>"> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-experiments__item <?php echo esc_attr( $disabled_class ); ?>"> | ||||||||||||||||||||||||||||||||||||||||
| <div class="ai-experiments__item-header"> | ||||||||||||||||||||||||||||||||||||||||
| <label class="components-toggle-control" for="<?php echo esc_attr( $experiment_option ); ?>"> | ||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -223,10 +272,43 @@ public function render_page(): void { | |||||||||||||||||||||||||||||||||||||||
| aria-describedby="<?php echo esc_attr( $desc_id ); ?>" | ||||||||||||||||||||||||||||||||||||||||
| <?php endif; ?> | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| <span> | ||||||||||||||||||||||||||||||||||||||||
| <span class="ai-experiments__item-title"> | ||||||||||||||||||||||||||||||||||||||||
| <strong><?php echo esc_html( $experiment->get_label() ); ?></strong> | ||||||||||||||||||||||||||||||||||||||||
| <?php if ( ! empty( $entry_points ) ) : ?> | ||||||||||||||||||||||||||||||||||||||||
| <span class="ai-experiments__item-links"> | ||||||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||||||
| $links = array(); | ||||||||||||||||||||||||||||||||||||||||
| foreach ( $entry_points as $action ) { | ||||||||||||||||||||||||||||||||||||||||
| if ( empty( $action['label'] ) || empty( $action['url'] ) ) { | ||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| $links[] = sprintf( | ||||||||||||||||||||||||||||||||||||||||
| '<a href="%s">%s</a>', | ||||||||||||||||||||||||||||||||||||||||
| esc_url( $action['url'] ), | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+285
to
+287
|
||||||||||||||||||||||||||||||||||||||||
| $links[] = sprintf( | |
| '<a href="%s">%s</a>', | |
| esc_url( $action['url'] ), | |
| $action_url = esc_url_raw( (string) $action['url'] ); | |
| if ( empty( $action_url ) ) { | |
| continue; | |
| } | |
| $links[] = sprintf( | |
| '<a href="%s">%s</a>', | |
| esc_url( $action_url ), |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The entry points loop uses continue when label or url is empty, but doesn't validate the array structure before accessing these keys. If an entry point is not an array or is malformed, this could cause PHP notices. Consider adding is_array($action) check before accessing array keys.
| if ( empty( $action['label'] ) || empty( $action['url'] ) ) { | |
| continue; | |
| } | |
| $links[] = sprintf( | |
| '<a href="%s">%s</a>', | |
| esc_url( $action['url'] ), | |
| esc_html( $action['label'] ) | |
| if ( ! is_array( $action ) ) { | |
| continue; | |
| } | |
| $label = $action['label'] ?? ''; | |
| $url = $action['url'] ?? ''; | |
| if ( '' === $label || '' === $url ) { | |
| continue; | |
| } | |
| $links[] = sprintf( | |
| '<a href="%s">%s</a>', | |
| esc_url( $url ), | |
| esc_html( $label ) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline JavaScript should be moved to a separate enqueued script file for better separation of concerns, security, and maintainability. Inline scripts in PHP templates make code harder to test and maintain. Consider creating a separate JS file in src/admin/settings/ and enqueuing it via Asset_Loader.
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JavaScript accordion functionality is added inline on every page load, but it's not checking if experiments with settings actually exist before adding the event listeners. While harmless, this adds unnecessary DOM queries. Consider conditionally rendering the script only when at least one experiment has settings, by checking if any experiment returns true for has_settings() before the loop.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -260,3 +260,63 @@ | |||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Returns the AI icon as an inline SVG. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @since x.x.x | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @param string $width The width of the icon. | ||||||||||||||||||||||||||||||||||||||
| * @param string $height The height of the icon. | ||||||||||||||||||||||||||||||||||||||
| * @return string The AI icon SVG markup. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| function get_ai_icon_svg( string $width = '1em', string $height = '1em' ): string { | ||||||||||||||||||||||||||||||||||||||
| static $svg_content = null; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if ( null === $svg_content ) { | ||||||||||||||||||||||||||||||||||||||
| $svg_path = dirname( __DIR__ ) . '/assets/images/ai-icon.svg'; | ||||||||||||||||||||||||||||||||||||||
| $svg_content = file_exists( $svg_path ) ? file_get_contents( $svg_path ) : ''; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+277
to
+278
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if ( empty( $svg_content ) ) { | ||||||||||||||||||||||||||||||||||||||
| return ''; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Add width and height attributes, and fill="currentColor" for theme compatibility. | ||||||||||||||||||||||||||||||||||||||
| return preg_replace( | ||||||||||||||||||||||||||||||||||||||
| '/<svg\b/', | ||||||||||||||||||||||||||||||||||||||
| sprintf( '<svg width="%s" height="%s" fill="currentColor"', esc_attr( $width ), esc_attr( $height ) ), | ||||||||||||||||||||||||||||||||||||||
| $svg_content, | ||||||||||||||||||||||||||||||||||||||
| 1 | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+286
to
+291
|
||||||||||||||||||||||||||||||||||||||
| return preg_replace( | |
| '/<svg\b/', | |
| sprintf( '<svg width="%s" height="%s" fill="currentColor"', esc_attr( $width ), esc_attr( $height ) ), | |
| $svg_content, | |
| 1 | |
| ); | |
| $result = preg_replace( | |
| '/<svg\b/', | |
| sprintf( '<svg width="%s" height="%s" fill="currentColor"', esc_attr( $width ), esc_attr( $height ) ), | |
| $svg_content, | |
| 1 | |
| ); | |
| if ( null === $result ) { | |
| return $svg_content; | |
| } | |
| return $result; |
Check warning on line 308 in includes/helpers.php
GitHub Actions / Run PHPCS coding standards checks
file_get_contents() is uncached. If the function is being used to fetch a remote file (e.g. a URL starting with https://), please use wpcom_vip_file_get_contents() to ensure the results are cached. For more details, please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The base64_encode function can potentially fail with binary data in edge cases. While SVG content is typically safe, consider using phpcs:ignore for the WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode sniff if it triggers, or wrap the call with error handling for robustness.
| $data_uri = 'data:image/svg+xml;base64,' . base64_encode( $svg_content ); | |
| $encoded = base64_encode( $svg_content ); | |
| if ( false === $encoded ) { | |
| $data_uri = ''; | |
| } else { | |
| $data_uri = 'data:image/svg+xml;base64,' . $encoded; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The phpcs:ignore comment disables output escaping without justification. While get_ai_icon_svg() returns SVG markup that needs to be unescaped, consider adding a comment explaining why this is safe (e.g., "Safe: SVG content is from a trusted static file, not user input").