Skip to content

Commit e20b4d7

Browse files
committed
Dashboard MCP: use big_sky_enabled for AI site counts and lists
- Request big_sky_enabled in SITE_FIELDS for /me/sites payloads - Add big_sky_enabled to Site type (api-core) - ai-sites: derive enabled list from site.big_sky_enabled; invalidate sites query on Big Sky toggle; fix upsell Back button (tertiary, compact) - AI and MCP: replace pluginsQuery count with sites list + big_sky_enabled; use _n for 1 site vs N sites badge text Made-with: Cursor
1 parent 9555dbb commit e20b4d7

File tree

4 files changed

+145
-79
lines changed

4 files changed

+145
-79
lines changed

client/dashboard/me/mcp/ai-sites/index.tsx

Lines changed: 130 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { DotcomFeatures, updateBigSkyPlugin } from '@automattic/api-core';
2-
import { userSettingsQuery, pluginsQuery, invalidatePlugins } from '@automattic/api-queries';
1+
import { HostingFeatures, updateBigSkyPlugin } from '@automattic/api-core';
2+
import {
3+
bigSkyPluginQuery,
4+
invalidatePlugins,
5+
queryClient,
6+
siteQueryFilter,
7+
userSettingsQuery,
8+
} from '@automattic/api-queries';
39
import { useQuery, useSuspenseQuery, useMutation } from '@tanstack/react-query';
410
import { Button, __experimentalVStack as VStack } from '@wordpress/components';
511
import { __ } from '@wordpress/i18n';
6-
import { useState } from 'react';
12+
import { comment } from '@wordpress/icons';
13+
import { useState, useMemo, useEffect } from 'react';
714
import Breadcrumbs from '../../../app/breadcrumbs';
815
import { useAppContext } from '../../../app/context';
916
import { ActionList } from '../../../components/action-list';
@@ -13,6 +20,7 @@ import { PageHeader } from '../../../components/page-header';
1320
import PageLayout from '../../../components/page-layout';
1421
import { SectionHeader } from '../../../components/section-header';
1522
import SiteIcon from '../../../components/site-icon';
23+
import UpsellCallout from '../../../sites/hosting-feature-gated-with-callout/upsell';
1624
import { getSiteDisplayName } from '../../../utils/site-name';
1725
import { getSiteDisplayUrl } from '../../../utils/site-url';
1826
import PreferencesLoginSiteDropdown from '../../preferences-primary-site/site-dropdown';
@@ -24,37 +32,45 @@ export default function McpAiSites() {
2432
const sitesQueryResult = useQuery(
2533
queries.sitesQuery( { site_visibility: 'visible', include_a8c_owned: false } )
2634
);
27-
const sites = ( sitesQueryResult.data as Site[] | undefined ) ?? [];
35+
const sites = useMemo(
36+
() => ( sitesQueryResult.data as Site[] | undefined ) ?? [],
37+
[ sitesQueryResult.data ]
38+
);
2839
const isSiteListLoading = sitesQueryResult.isLoading;
2940

3041
const [ selectedSiteId, setSelectedSiteId ] = useState< string | null >( null );
42+
const [ pendingSiteId, setPendingSiteId ] = useState< number | null >( null );
43+
const [ upsellSite, setUpsellSite ] = useState< Site | null >( null );
3144

32-
const { data: pluginsData } = useQuery( pluginsQuery() );
33-
const aiEnabledSiteIds = new Set(
34-
Object.entries( pluginsData?.sites ?? {} )
35-
.filter( ( [ , plugins ] ) =>
36-
plugins.some( ( p ) => p.slug === DotcomFeatures.BIG_SKY && p.active )
37-
)
38-
.map( ( [ siteId ] ) => Number( siteId ) )
39-
);
45+
const aiEnabledSiteIds = useMemo( () => {
46+
return new Set< number >( sites.filter( ( s ) => s.big_sky_enabled ).map( ( s ) => s.ID ) );
47+
}, [ sites ] );
4048

41-
const enabledSites = sites
42-
.filter( ( site: Site ) => aiEnabledSiteIds.has( site.ID ) )
43-
.map( ( site: Site ) => ( {
44-
id: site.ID,
45-
name: getSiteDisplayName( site ),
46-
displayUrl: getSiteDisplayUrl( site ),
47-
site,
48-
} ) );
49+
const enabledSites = useMemo(
50+
() =>
51+
sites
52+
.filter( ( site: Site ) => aiEnabledSiteIds.has( site.ID ) )
53+
.map( ( site: Site ) => ( {
54+
id: site.ID,
55+
name: getSiteDisplayName( site ),
56+
displayUrl: getSiteDisplayUrl( site ),
57+
site,
58+
} ) ),
59+
[ sites, aiEnabledSiteIds ]
60+
);
4961

50-
const availableSitesForPicker = sites.filter(
51-
( site: Site ) => ! aiEnabledSiteIds.has( site.ID )
62+
const availableSitesForPicker = useMemo(
63+
() => sites.filter( ( site: Site ) => ! aiEnabledSiteIds.has( site.ID ) ),
64+
[ sites, aiEnabledSiteIds ]
5265
);
5366

5467
const { mutate: toggleAiForSite, isPending } = useMutation( {
5568
mutationFn: ( { siteId, enable }: { siteId: number; enable: boolean } ) =>
5669
updateBigSkyPlugin( siteId, { enable } ),
57-
onSuccess: () => {
70+
onSuccess: ( _, { siteId } ) => {
71+
queryClient.invalidateQueries( { queryKey: bigSkyPluginQuery( siteId ).queryKey } );
72+
queryClient.invalidateQueries( siteQueryFilter( siteId ) );
73+
queryClient.invalidateQueries( { queryKey: [ 'sites' ] } );
5874
invalidatePlugins();
5975
},
6076
meta: {
@@ -65,9 +81,27 @@ export default function McpAiSites() {
6581
},
6682
} );
6783

84+
const { data: pendingPluginStatus, isFetching: isCheckingPlan } = useQuery( {
85+
...bigSkyPluginQuery( pendingSiteId! ),
86+
enabled: pendingSiteId !== null,
87+
} );
88+
89+
useEffect( () => {
90+
if ( ! pendingSiteId || ! pendingPluginStatus ) {
91+
return;
92+
}
93+
if ( pendingPluginStatus.available ) {
94+
toggleAiForSite( { siteId: pendingSiteId, enable: true } );
95+
} else {
96+
const site = sites.find( ( s ) => s.ID === pendingSiteId ) ?? null;
97+
setUpsellSite( site );
98+
}
99+
setPendingSiteId( null );
100+
}, [ pendingPluginStatus, pendingSiteId, sites, toggleAiForSite ] );
101+
68102
const handleSitePickerSelect = ( siteIdStr: string | null | undefined ) => {
69103
if ( siteIdStr ) {
70-
toggleAiForSite( { siteId: parseInt( siteIdStr, 10 ), enable: true } );
104+
setPendingSiteId( parseInt( siteIdStr, 10 ) );
71105
setSelectedSiteId( null );
72106
}
73107
};
@@ -91,53 +125,80 @@ export default function McpAiSites() {
91125
>
92126
<ComponentViewTracker eventName="calypso_dashboard_mcp_ai_sites_view" />
93127
<VStack spacing={ 4 }>
94-
<Card>
95-
<CardBody>
96-
<VStack spacing={ 4 }>
97-
<SectionHeader
98-
level={ 3 }
99-
title={ __( 'Add a site' ) }
100-
description={ __( 'Search for a site to enable the AI assistant.' ) }
101-
/>
102-
<PreferencesLoginSiteDropdown
103-
sites={ availableSitesForPicker }
104-
isLoading={ isSiteListLoading }
105-
value={ selectedSiteId ?? '' }
106-
onChange={ handleSitePickerSelect }
107-
hideLabelFromVision
108-
/>
109-
</VStack>
110-
</CardBody>
111-
</Card>
112-
113-
{ enabledSites.length > 0 && (
114-
<VStack spacing={ 2 }>
115-
<SectionHeader
116-
level={ 3 }
117-
title={ __( 'Enabled sites' ) }
118-
description={ __( 'These sites have the AI assistant enabled.' ) }
128+
{ upsellSite ? (
129+
<>
130+
<UpsellCallout
131+
site={ upsellSite }
132+
feature={ HostingFeatures.BIG_SKY }
133+
upsellId="ai-tools"
134+
upsellTitle={ __( 'Your dream site is just a prompt away' ) }
135+
upsellDescription={ __(
136+
'Get AI-powered assistance to help you build, edit, and redesign your site with ease.'
137+
) }
138+
upsellIcon={ comment }
119139
/>
120-
<ActionList>
121-
{ enabledSites.map( ( site ) => (
122-
<ActionList.ActionItem
123-
key={ site.id }
124-
title={ site.name }
125-
description={ site.displayUrl || undefined }
126-
decoration={ site.site ? <SiteIcon site={ site.site } size={ 32 } /> : undefined }
127-
actions={
128-
<Button
129-
variant="secondary"
130-
size="compact"
131-
disabled={ isPending }
132-
onClick={ () => toggleAiForSite( { siteId: site.id, enable: false } ) }
133-
>
134-
{ __( 'Remove' ) }
135-
</Button>
136-
}
140+
<Button
141+
variant="tertiary"
142+
size="compact"
143+
style={ { alignSelf: 'flex-start' } }
144+
onClick={ () => setUpsellSite( null ) }
145+
>
146+
{ __( '← Back' ) }
147+
</Button>
148+
</>
149+
) : (
150+
<>
151+
<Card>
152+
<CardBody>
153+
<VStack spacing={ 4 }>
154+
<SectionHeader
155+
level={ 3 }
156+
title={ __( 'Add a site' ) }
157+
description={ __( 'Search for a site to enable the AI assistant.' ) }
158+
/>
159+
<PreferencesLoginSiteDropdown
160+
sites={ availableSitesForPicker }
161+
isLoading={ isSiteListLoading || isCheckingPlan }
162+
value={ selectedSiteId ?? '' }
163+
onChange={ handleSitePickerSelect }
164+
hideLabelFromVision
165+
/>
166+
</VStack>
167+
</CardBody>
168+
</Card>
169+
170+
{ enabledSites.length > 0 && (
171+
<VStack spacing={ 2 }>
172+
<SectionHeader
173+
level={ 3 }
174+
title={ __( 'Enabled sites' ) }
175+
description={ __( 'These sites have the AI assistant enabled.' ) }
137176
/>
138-
) ) }
139-
</ActionList>
140-
</VStack>
177+
<ActionList>
178+
{ enabledSites.map( ( site ) => (
179+
<ActionList.ActionItem
180+
key={ site.id }
181+
title={ site.name }
182+
description={ site.displayUrl || undefined }
183+
decoration={
184+
site.site ? <SiteIcon site={ site.site } size={ 32 } /> : undefined
185+
}
186+
actions={
187+
<Button
188+
variant="secondary"
189+
size="compact"
190+
disabled={ isPending }
191+
onClick={ () => toggleAiForSite( { siteId: site.id, enable: false } ) }
192+
>
193+
{ __( 'Remove' ) }
194+
</Button>
195+
}
196+
/>
197+
) ) }
198+
</ActionList>
199+
</VStack>
200+
) }
201+
</>
141202
) }
142203
</VStack>
143204
</PageLayout>

client/dashboard/me/mcp/index.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { DotcomFeatures } from '@automattic/api-core';
2-
import { userSettingsQuery, userSettingsMutation, pluginsQuery } from '@automattic/api-queries';
1+
import { userSettingsQuery, userSettingsMutation } from '@automattic/api-queries';
32
import config from '@automattic/calypso-config';
43
import { useSuspenseQuery, useMutation, useQuery } from '@tanstack/react-query';
54
import { __experimentalVStack as VStack, ToggleControl } from '@wordpress/components';
65
import { createInterpolateElement } from '@wordpress/element';
7-
import { __ } from '@wordpress/i18n';
8-
import { linkExternal } from '@wordpress/icons';
6+
import { __, _n, sprintf } from '@wordpress/i18n';
7+
import { external } from '@wordpress/icons';
98
import {
109
getAccountMcpAbilities,
1110
getDisabledSiteIds,
1211
getEnabledSiteIds,
1312
} from '../../../me/mcp/utils';
13+
import { useAppContext } from '../../app/context';
1414
import { Card, CardBody, CardDivider } from '../../components/card';
1515
import ComponentViewTracker from '../../components/component-view-tracker';
1616
import InlineSupportLink from '../../components/inline-support-link';
@@ -19,6 +19,7 @@ import PageLayout from '../../components/page-layout';
1919
import RouterLinkSummaryButton from '../../components/router-link-summary-button';
2020
import { SectionHeader } from '../../components/section-header';
2121
import { isWriteTool } from './categories';
22+
import type { Site } from '@automattic/api-core';
2223

2324
interface McpAbility {
2425
title: string;
@@ -70,7 +71,7 @@ function getSiteCountBadgeText( count: number, noneLabel: string ): string {
7071
}
7172
return sprintf(
7273
/* translators: %d is the number of sites */
73-
__( '%d sites' ),
74+
_n( '%d site', '%d sites', count ),
7475
count
7576
);
7677
}
@@ -94,6 +95,7 @@ function getReadStatus( tools: Array< [ string, McpAbility ] > ): string {
9495
}
9596

9697
function McpComponent() {
98+
const { queries } = useAppContext();
9799
const { data: userSettings } = useSuspenseQuery( userSettingsQuery() );
98100

99101
const mcpAbilities = getAccountMcpAbilities( userSettings || {} );
@@ -123,10 +125,11 @@ function McpComponent() {
123125

124126
const aiAssistantEnabled = userSettings?.ai_assistant ?? false;
125127

126-
const { data: pluginsData } = useQuery( pluginsQuery() );
127-
const aiEnabledSiteCount = Object.values( pluginsData?.sites ?? {} )
128-
.flat()
129-
.filter( ( p ) => p.slug === DotcomFeatures.BIG_SKY && p.active ).length;
128+
const sitesQueryResult = useQuery(
129+
queries.sitesQuery( { site_visibility: 'visible', include_a8c_owned: false } )
130+
);
131+
const sites = ( sitesQueryResult.data as Site[] | undefined ) ?? [];
132+
const aiEnabledSiteCount = sites.filter( ( site ) => site.big_sky_enabled ).length;
130133

131134
const mutation = useMutation( {
132135
...userSettingsMutation(),
@@ -323,7 +326,7 @@ function McpComponent() {
323326
to="/me/mcp/setup"
324327
title={ __( 'Connect external AI assistant' ) }
325328
description={ __( 'Get instructions for connecting your external AI assistant.' ) }
326-
decoration={ linkExternal }
329+
decoration={ external }
327330
/>
328331
</VStack>
329332
</PageLayout>

packages/api-core/src/site/fetchers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const SITE_FIELDS = [
4040
'garden_name',
4141
'garden_partner',
4242
'garden_is_provisioned',
43+
'big_sky_enabled',
4344
];
4445

4546
export const JOINED_SITE_FIELDS = SITE_FIELDS.join( ',' );

packages/api-core/src/site/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface Site {
8282
garden_name: string | null;
8383
garden_partner: string | null;
8484
garden_is_provisioned: boolean | null;
85+
big_sky_enabled?: boolean;
8586

8687
// Injected local properties
8788
__inaccessible_jetpack_error?: Error;

0 commit comments

Comments
 (0)