Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/images/showcase-image-generation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions assets/images/showcase-title-generation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 49 additions & 4 deletions includes/Abstracts/Abstract_Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ abstract class Abstract_Feature implements Feature {
*/
private string $stability;

/**
* The presentation style for the settings UI.
*
* @since x.x.x
* @var string 'toggle' (default) or 'visual-card'.
*/
protected string $presentation;
Comment on lines +71 to +77
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not reviewed the rest of the PR yet but I don't think we need to introduce a new argument here to control this. The plan is that any non-experimental Feature should be showcased in this new UI and not in the toggle list below. We haven't "promoted" anything to be non-experimental yet (I have a PR that I'll open soon that does that for Image Generation) but the main thing that controls that is the existing $stability argument. Right now that is set to experimental for all existing Features but for "promoted" Features, that will be set to stable. I'd suggest we use that to determine the UI

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note I've opened #418 which brings Image Generation from an experimental Feature to a stable Feature. It does not touch any of the UI though as I'm leaving that for this PR. So these two PRs will go hand-in-hand

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good @dkotter, I will review your PR, and then update this one to use the mechanism we land on yours to specify a feature is stable.


/**
* The image URL for visual-card presentation.
*
* @since x.x.x
* @var string
*/
protected string $image;

/**
* Constructor.
*
Expand Down Expand Up @@ -102,10 +118,15 @@ final public function __construct() {
$metadata['category'] = Feature_Category::OTHER;
}

$this->label = $metadata['label'];
$this->description = $metadata['description'];
$this->category = $metadata['category'];
$this->stability = $metadata['stability'] ?? 'experimental';
$this->label = $metadata['label'];
$this->description = $metadata['description'];
$this->category = $metadata['category'];
$this->stability = $metadata['stability'] ?? 'experimental';
$presentation = $metadata['presentation'] ?? 'toggle';
$this->presentation = in_array( $presentation, array( 'toggle', 'visual-card' ), true )
? $presentation
: 'toggle';
$this->image = $metadata['image'] ?? '';
}

/**
Expand All @@ -121,6 +142,8 @@ final public function __construct() {
* description: string,
* category?: string,
* stability?: 'deprecated'|'experimental'|'stable',
* presentation?: 'toggle'|'visual-card',
* image?: string,
* } Feature metadata.
*/
abstract protected function load_metadata(): array;
Expand Down Expand Up @@ -201,6 +224,28 @@ final public function get_stability(): string {
return $this->stability;
}

/**
* Gets the presentation style for the settings UI.
*
* @since x.x.x
*
* @return string The presentation style ('toggle' or 'visual-card').
*/
public function get_presentation(): string {
return $this->presentation;
}

/**
* Gets the image URL for visual-card presentation.
*
* @since x.x.x
*
* @return string The image URL, or empty string if not set.
*/
public function get_image(): string {
return $this->image;
}

/**
* Registers feature-specific settings.
*
Expand Down
18 changes: 18 additions & 0 deletions includes/Contracts/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,22 @@ public function is_enabled(): bool;
* }> Array of field definitions with full option names.
*/
public function get_settings_fields_metadata(): array;

/**
* Gets the presentation style for the settings UI.
*
* @since x.x.x
*
* @return string The presentation style ('toggle' or 'visual-card').
*/
public function get_presentation(): string;

/**
* Gets the image URL for visual-card presentation.
*
* @since x.x.x
*
* @return string The image URL, or empty string if not set.
*/
public function get_image(): string;
}
8 changes: 5 additions & 3 deletions includes/Experiments/Image_Generation/Image_Generation.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ public static function get_id(): string {
*/
protected function load_metadata(): array {
return array(
'label' => __( 'Image Generation and Editing', 'ai' ),
'description' => __( 'Generate and edit images using AI', 'ai' ),
'category' => Experiment_Category::EDITOR,
'label' => __( 'Image Generation and Editing', 'ai' ),
'description' => __( 'Generate and edit images using AI', 'ai' ),
'category' => Experiment_Category::EDITOR,
'presentation' => 'visual-card',
'image' => WPAI_PLUGIN_URL . 'assets/images/showcase-image-generation.png',
);
}

Expand Down
8 changes: 5 additions & 3 deletions includes/Experiments/Title_Generation/Title_Generation.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ public static function get_id(): string {
*/
protected function load_metadata(): array {
return array(
'label' => __( 'Title Generation', 'ai' ),
'description' => __( 'Generates title suggestions from content', 'ai' ),
'category' => Experiment_Category::EDITOR,
'label' => __( 'Title Generation', 'ai' ),
'description' => __( 'Generates title suggestions from content', 'ai' ),
'category' => Experiment_Category::EDITOR,
'presentation' => 'visual-card',
'image' => WPAI_PLUGIN_URL . 'assets/images/showcase-title-generation.svg',
);
}

Expand Down
2 changes: 2 additions & 0 deletions includes/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ function get_settings_feature_metadata( Registry $registry ): array {
'description' => wp_strip_all_tags( $feature->get_description() ),
'category' => $category,
'settingsFields' => $feature->get_settings_fields_metadata(),
'presentation' => $feature->get_presentation(),
'image' => esc_url( $feature->get_image() ),
);
}

Expand Down
110 changes: 105 additions & 5 deletions routes/ai-home/stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ interface FeatureData {
description: string;
category: string;
settingsFields: SettingsFieldData[];
presentation: string;
image: string;
}

interface PageData {
Expand Down Expand Up @@ -120,6 +122,8 @@ function parseFeature( value: unknown ): FeatureData | null {
description: toStringValue( feature.description ),
category: toStringValue( feature.category ) || 'other',
settingsFields: ( rawFields as unknown[] ).filter( isSettingsField ),
presentation: toStringValue( feature.presentation ) || 'toggle',
image: toStringValue( feature.image ),
};
}

Expand Down Expand Up @@ -407,6 +411,70 @@ function FeatureToggleWithSettings( {
);
}

const VISUAL_CARD_FEATURES = new Map(
PAGE_DATA.features
.filter( ( f ) => f.presentation === 'visual-card' )
.map( ( f ) => [ f.settingName, f ] as const )
);

function VisualCardToggle( {
field,
data,
onChange,
}: DataFormControlProps< AISettings > ) {
const feature = VISUAL_CARD_FEATURES.get( field.id );
const globalEnabled = !! data[ GLOBAL_FIELD_ID ];
const checked = !! field.getValue( { item: data } );

return (
<div
className={ `ai-showcase-card${
! globalEnabled ? ' ai-showcase-card--disabled' : ''
}` }
>
{ feature?.image && (
<div className="ai-showcase-card__image">
<img src={ feature.image } alt="" loading="lazy" />
</div>
) }
<div className="ai-showcase-card__content">
<h3 className="ai-showcase-card__title">{ field.label }</h3>
<p className="ai-showcase-card__description">
{ field.description }
</p>
<div className="ai-showcase-card__actions">
<Button
variant={ checked ? 'secondary' : 'primary' }
onClick={ () =>
onChange( { [ field.id ]: ! checked } )
}
disabled={ ! globalEnabled }
size="compact"
>
{ checked
? __( 'Disable', 'ai' )
: __( 'Enable', 'ai' ) }
</Button>
{ checked && (
<span className="ai-showcase-card__enabled-badge">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={ 16 }
height={ 16 }
fill="currentColor"
>
<path d="M16.5 7.5 10 13.9l-2.5-2.4-1 1 3.5 3.6 7.5-7.6z" />
</svg>
{ __( 'Enabled', 'ai' ) }
</span>
) }
</div>
</div>
</div>
);
}

function AISettingsPage() {
const { editedRecord, isLoading } = useSelect( ( select ) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- core-data store selectors aren't fully typed for 'root'/'site' entity args.
Expand Down Expand Up @@ -449,6 +517,8 @@ function AISettingsPage() {
description: '',
category: 'other',
settingsFields: [],
presentation: 'toggle',
image: '',
};
} );

Expand Down Expand Up @@ -505,7 +575,9 @@ function AISettingsPage() {
type: 'boolean' as const,
};

if ( ! globalEnabled ) {
if ( VISUAL_CARD_FEATURES.has( feature.settingName ) ) {
baseField.Edit = VisualCardToggle;
} else if ( ! globalEnabled ) {
baseField.Edit = DisabledToggle;
} else if ( feature.settingsFields.length > 0 ) {
baseField.Edit = FeatureToggleWithSettings;
Expand All @@ -520,15 +592,43 @@ function AISettingsPage() {
);

const form = useMemo< Form >( () => {
const showcaseChildren: string[] = [];
const groupedFields = new Map< string, string[] >();

for ( const feature of featureDefinitions ) {
const category = feature.category || 'other';
const categoryFields = groupedFields.get( category ) ?? [];
categoryFields.push( feature.settingName );
groupedFields.set( category, categoryFields );
if ( VISUAL_CARD_FEATURES.has( feature.settingName ) ) {
showcaseChildren.push( feature.settingName );
} else {
const category = feature.category || 'other';
const categoryFields = groupedFields.get( category ) ?? [];
categoryFields.push( feature.settingName );
groupedFields.set( category, categoryFields );
}
}

const sectionFields: NonNullable< Form[ 'fields' ] > = [];

// Add showcase section with row layout (2 per row).
if ( showcaseChildren.length > 0 ) {
const rows: NonNullable< Form[ 'fields' ] > = [];
for ( let i = 0; i < showcaseChildren.length; i += 2 ) {
rows.push( {
id: `showcase-row-${ i }`,
layout: { type: 'row' as const },
children: showcaseChildren.slice( i, i + 2 ),
} );
}

sectionFields.push( {
id: 'feature-group-showcase',
layout: {
type: 'regular',
labelPosition: 'none',
},
children: rows,
} );
}

const seenCategories = new Set< string >();

for ( const group of featureGroups ) {
Expand Down
68 changes: 68 additions & 0 deletions routes/ai-home/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,71 @@
justify-content: flex-end;
margin-top: 12px;
}

.ai-showcase-card {
display: flex;
flex-direction: column;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: #fff;
}

.ai-showcase-card__image {
width: 100%;
aspect-ratio: 16 / 9;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;

img {
width: 100%;
height: 100%;
object-fit: cover;
}
}

.ai-showcase-card__content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}

.ai-showcase-card__title {
font-size: 14px;
font-weight: 600;
margin: 0;
line-height: 1.4;
}

.ai-showcase-card__description {
font-size: 13px;
color: #757575;
margin: 0;
flex: 1;
}

.ai-showcase-card__actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}

.ai-showcase-card__enabled-badge {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 12px;
font-weight: 500;
color: #00a32a;
white-space: nowrap;
}

.ai-showcase-card--disabled {
opacity: 0.6;
}
Loading