Skip to content
Closed
38 changes: 38 additions & 0 deletions includes/Abstracts/Abstract_Experiment.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,44 @@ public function render_settings_fields(): void {
// Child classes can override to render custom settings UI.
}

/**
* Provides contextual entry points for the experiment.
*
* Child classes can override to return an array of links, for example:
* array(
* array(
* 'label' => __( 'Try', 'ai' ),
* 'url' => admin_url( 'post-new.php' ),
* 'type' => 'try',
* ),
* array(
* 'label' => __( 'Dashboard', 'ai' ),
* 'url' => admin_url( 'admin.php?page=ai-mcp' ),
* 'type' => 'dashboard',
* ),
* );
*
* @since 0.1.0
*
* @return array<int, array{label: string, url: string, type?: string}>
*/
public function get_entry_points(): array {
return array();
}

/**
* Checks if the experiment has custom settings.
*
* Override this method in child classes that have settings to return true.
*
* @since 0.1.0
*
* @return bool True if the experiment has settings, false otherwise.
*/
public function has_settings(): bool {
return false;
}

/**
* Gets the option name for a custom experiment setting field.
*
Expand Down
27 changes: 27 additions & 0 deletions includes/Contracts/Experiment.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,31 @@ public function register(): void;
* @return bool True if enabled, false otherwise.
*/
public function is_enabled(): bool;

/**
* Provides contextual entry points for the experiment.
*
* @since 0.1.0
*
* @return array<int, array{label: string, url: string, type?: string}>
*/
public function get_entry_points(): array;

/**
* Checks if the experiment has custom settings.
*
* @since 0.1.0
*
* @return bool True if the experiment has settings, false otherwise.
*/
public function has_settings(): bool;

/**
* Renders experiment-specific settings fields.
*
* @since 0.1.0
*
* @return void
*/
public function render_settings_fields(): void;
}
132 changes: 118 additions & 14 deletions includes/Settings/Settings_Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ class Settings_Page {
*/
private const PAGE_SLUG = 'ai-experiments';

/**
* URL pointing to the plugin repository for contributions.
*
* @since 0.1.0
*
* @var string
*/
private const CONTRIBUTION_URL = 'https://github.com/WordPress/ai';

/**
* URL pointing to the plugin documentation.
*
* @since 0.1.0
*
* @var string
*/
private const DOCUMENTATION_URL = 'https://github.com/WordPress/ai/tree/develop/docs';

/**
* Constructor.
*
Expand Down Expand Up @@ -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.
Expand All @@ -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 );
Expand Down Expand Up @@ -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
Expand All @@ -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'] ),
esc_html( $action['label'] )
);
}

if ( ! empty( $links ) ) {
echo wp_kses_post( '(' . implode( ' · ', $links ) . ')' );
}
?>
</span>
<?php endif; ?>
</span>
</label>
<?php if ( $has_settings ) : ?>
<button
type="button"
class="ai-experiments__settings-toggle"
aria-expanded="false"
aria-controls="<?php echo esc_attr( $settings_id ); ?>"
title="<?php esc_attr_e( 'Toggle settings', 'ai' ); ?>"
>
<span class="dashicons dashicons-admin-generic"></span>
<span class="screen-reader-text"><?php esc_html_e( 'Settings', 'ai' ); ?></span>
</button>
<?php endif; ?>
</div>
<?php if ( $experiment->get_description() ) : ?>
<p class="description" id="<?php echo esc_attr( $desc_id ); ?>">
Expand All @@ -249,15 +331,37 @@ public function render_page(): void {
?>
</p>
<?php endif; ?>
<?php
// Allow experiments to render their own custom settings fields.
if ( method_exists( $experiment, 'render_settings_fields' ) ) {
$experiment->render_settings_fields();
}
?>
</li>
<?php if ( $has_settings ) : ?>
<div
id="<?php echo esc_attr( $settings_id ); ?>"
class="ai-experiments__settings-drawer"
hidden
>
<?php $experiment->render_settings_fields(); ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</ul>
</div>
<script>
( function() {
document.querySelectorAll( '.ai-experiments__settings-toggle' ).forEach( function( btn ) {
btn.addEventListener( 'click', function() {
var expanded = btn.getAttribute( 'aria-expanded' ) === 'true';
var drawerId = btn.getAttribute( 'aria-controls' );
var drawer = document.getElementById( drawerId );
if ( drawer ) {
btn.setAttribute( 'aria-expanded', String( ! expanded ) );
if ( expanded ) {
drawer.setAttribute( 'hidden', '' );
} else {
drawer.removeAttribute( 'hidden' );
}
}
} );
} );
} )();
</script>
</div>
<?php endif; ?>
</div>
Expand Down
60 changes: 60 additions & 0 deletions includes/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,63 @@
return false;
}
}

/**
* Returns the AI icon as an inline SVG.
*
* @since 0.1.0
*
* @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 ) : '';
}

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
);
}

/**
* Returns the AI icon as a base64 data URI for use in admin menu icons.
*
* @since 0.1.0
*
* @return string The base64-encoded data URI for the AI icon.
*/
function get_ai_icon_data_uri(): string {
static $data_uri = null;

if ( null === $data_uri ) {
$svg_path = dirname( __DIR__ ) . '/assets/images/ai-icon.svg';

if ( file_exists( $svg_path ) ) {
$svg_content = file_get_contents( $svg_path );

Check warning on line 308 in includes/helpers.php

View workflow job for this annotation

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/
if ( false === $svg_content ) {
$data_uri = '';
return $data_uri;
}
// Replace currentColor with a neutral color for admin menu compatibility.
$svg_content = str_replace( 'fill="currentColor"', 'fill="black"', $svg_content );
$data_uri = 'data:image/svg+xml;base64,' . base64_encode( $svg_content );
} else {
$data_uri = '';
}
}

return $data_uri;
}
Loading
Loading