Skip to content

Commit

Permalink
Merge branch 'trunk' into add/welcome-message-brompts
Browse files Browse the repository at this point in the history
  • Loading branch information
katinthehatsite authored Jun 14, 2024
2 parents 5e041df + e984584 commit 322868a
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 98 deletions.
24 changes: 2 additions & 22 deletions src/components/add-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useI18n } from '@wordpress/react-i18n';
import { FormEvent, useCallback, useEffect, useState } from 'react';
import { useAddSite } from '../hooks/use-add-site';
import { useIpcListener } from '../hooks/use-ipc-listener';
import { cx } from '../lib/cx';
import { generateSiteName } from '../lib/generate-site-name';
import { getIpcApi } from '../lib/get-ipc-api';
import Button from './button';
Expand All @@ -15,25 +14,6 @@ interface AddSiteProps {
className?: string;
}

/**
* The arbitrary Tailwind variants below (e.g., `[&.is-secondary]`) are used to
* achieve the specificity required to override the default button styles
* without `!important`, which often creates specificity collisions.
*/
const buttonStyles = `
add-site
text-white
[&.components-button]:hover:text-black
[&.components-button]:hover:bg-gray-100
[&.components-button]:active:text-black
[&.components-button]:active:bg-gray-100
[&.components-button]:shadow-[inset_0_0_0_1px_white]
[&.components-button.add-site]:focus:shadow-[inset_0_0_0_1px_white]
[&.components-button]:focus-visible:outline-none
[&.components-button.add-site]:focus-visible:shadow-[inset_0_0_0_1px_#3858E9]
[&.components-button]:focus-visible:shadow-a8c-blueberry
`.replace( /\n/g, ' ' );

export default function AddSite( { className }: AddSiteProps ) {
const { __ } = useI18n();
const [ showModal, setShowModal ] = useState( false );
Expand Down Expand Up @@ -144,15 +124,15 @@ export default function AddSite( { className }: AddSiteProps ) {
type="submit"
variant="primary"
isBusy={ isAddingSite }
disabled={ !! error || ! siteName?.trim() }
disabled={ isAddingSite || !! error || ! siteName?.trim() }
>
{ isAddingSite ? __( 'Adding site…' ) : __( 'Add site' ) }
</Button>
</div>
</SiteForm>
</Modal>
) }
<Button className={ cx( buttonStyles, className ) } onClick={ openModal }>
<Button variant="outlined" className={ className } onClick={ openModal }>
{ __( 'Add site' ) }
</Button>
</>
Expand Down
44 changes: 30 additions & 14 deletions src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { cx } from '../lib/cx';
type MappedOmit< T, K extends PropertyKey > = { [ P in keyof T as Exclude< P, K > ]: T[ P ] };

export type ButtonProps = MappedOmit< ComponentProps< typeof Button >, 'variant' > & {
variant?: 'primary' | 'secondary' | 'tertiary' | 'link' | 'icon';
variant?: ButtonVariant;
truncate?: boolean;
};

type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outlined' | 'link' | 'icon';

/**
* The arbitrary Tailwind variants below (e.g., `[&.is-secondary]`) are used to
* achieve the specificity required to override the default button styles
Expand Down Expand Up @@ -59,17 +61,18 @@ const secondaryStyles = `
[&.is-secondary:not(:focus)]:aria-disabled:hover:shadow-a8c-gray-5
`.replace( /\n/g, ' ' );

const tertiaryStyles = `
[&.is-tertiary]:text-white
[&.is-tertiary]:bg-gray-700
[&.is-tertiary]:focus:bg-gray-600
[&.is-tertiary]:focus:text-white
[&.is-tertiary:not(.is-destructive,:disabled,[aria-disabled=true])]:hover:bg-gray-600
[&.is-tertiary:not(.is-destructive,:disabled,[aria-disabled=true])]:hover:text-white
[&.is-tertiary]:hover:bg-white
[&.is-tertiary]:hover:text-white
[&.is-tertiary]:disabled:bg-gray-700
[&.is-tertiary]:disabled:text-a8c-gray-50
const outlinedStyles = `
outlined
text-white
[&.components-button]:hover:text-black
[&.components-button]:hover:bg-gray-100
[&.components-button]:active:text-black
[&.components-button]:active:bg-gray-100
[&.components-button]:shadow-[inset_0_0_0_1px_white]
[&.components-button.outlined]:focus:shadow-[inset_0_0_0_1px_white]
[&.components-button]:focus-visible:outline-none
[&.components-button.outlined]:focus-visible:shadow-[inset_0_0_0_1px_#3858E9]
[&.components-button]:focus-visible:shadow-a8c-blueberry
`.replace( /\n/g, ' ' );

const destructiveStyles = `
Expand All @@ -95,6 +98,19 @@ hover:bg-white
hover:bg-opacity-10
`.replace( /\n/g, ' ' );

/**
* Filter out custom values from the `variant` prop to avoid passing invalid
* values to the core WordPress components.
*
* @param variant - The button variant.
* @returns The variant value or, if the value is a Studio-specific, `undefined`.
*/
function sansCustomValues( variant: ButtonVariant | undefined ) {
return !! variant && [ 'outlined', 'icon' ].includes( variant )
? undefined
: ( variant as Exclude< ButtonVariant, 'outlined' | 'icon' > | undefined );
}

export default function ButtonComponent( {
className,
variant,
Expand All @@ -115,12 +131,12 @@ export default function ButtonComponent( {
return (
<Button
{ ...props }
variant={ variant === 'icon' ? undefined : variant }
variant={ sansCustomValues( variant ) }
className={ cx(
baseStyles,
variant === 'primary' && primaryStyles,
variant === 'secondary' && secondaryStyles,
variant === 'tertiary' && tertiaryStyles,
variant === 'outlined' && outlinedStyles,
variant === 'link' && linkStyles,
variant === 'icon' && iconStyles,
props.isDestructive && destructiveStyles,
Expand Down
61 changes: 32 additions & 29 deletions src/components/content-tab-assistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useAssistantApi } from '../hooks/use-assistant-api';
import { useAuth } from '../hooks/use-auth';
import { useFetchWelcomeMessages } from '../hooks/use-fetch-welcome-messages';
import { useOffline } from '../hooks/use-offline';
import { usePromptUsage } from '../hooks/use-prompt-usage';
import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
import { AIInput } from './ai-input';
Expand Down Expand Up @@ -76,8 +77,8 @@ const ActionButton = ( {
return (
<Button
onClick={ handleClick }
variant="tertiary"
className="mr-2 font-sans select-none"
variant="outlined"
className="h-auto mr-2 !px-2.5 py-0.5 font-sans select-none"
disabled={ disabled }
>
{ icon }
Expand Down Expand Up @@ -233,7 +234,8 @@ const UnauthenticatedView = ( { onAuthenticate }: { onAuthenticate: () => void }
);

export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps ) {
const { messages, addMessage, clearMessages } = useAssistant( selectedSite.name );
const { messages, addMessage, chatId, clearMessages } = useAssistant( selectedSite.name );
const { userCanSendMessage } = usePromptUsage();
const { fetchAssistant, isLoading: isAssistantThinking } = useAssistantApi();
const {
messages: welcomeMessages,
Expand All @@ -246,37 +248,38 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
const isOffline = useOffline();
const { __ } = useI18n();


useEffect( () => {
fetchWelcomeMessages();
}, [ fetchWelcomeMessages, selectedSite ] );

const handleSend = async ( messageToSend?: string ) => {
const message = messageToSend || input;
if ( message.trim() ) {
addMessage( message, 'user' );
setInput( '' );
try {
const { message: assistantMessage } = await fetchAssistant( [
...messages,
{ content: message, role: 'user' },
] );
if ( assistantMessage ) {
addMessage( assistantMessage, 'assistant' );
}
} catch ( error ) {
setTimeout(
() =>
getIpcApi().showMessageBox( {
type: 'warning',
message: __( 'Failed to send message' ),
detail: __( "We couldn't send the latest message. Please try again." ),
buttons: [ __( 'OK' ) ],
} ),
100
);
const handleSend = async (messageToSend?: string) => {
const chatMessage = messageToSend || input;
if (chatMessage.trim()) {
addMessage(chatMessage, 'user', chatId);
setInput('');
try {
const { message, chatId: fetchedChatId } = await fetchAssistant(chatId, [
...messages,
{ content: chatMessage, role: 'user' },
]);
if (message) {
addMessage(message, 'assistant', chatId ?? fetchedChatId);
}
} catch (error) {
setTimeout(
() =>
getIpcApi().showMessageBox({
type: 'warning',
message: __('Failed to send message'),
detail: __("We couldn't send the latest message. Please try again."),
buttons: [__('OK')],
}),
100
);
}
};
}
};

const handleKeyDown = ( e: React.KeyboardEvent< HTMLTextAreaElement > ) => {
if ( e.key === 'Enter' ) {
Expand All @@ -295,7 +298,7 @@ export function ContentTabAssistant( { selectedSite }: ContentTabAssistantProps
}
}, [ messages ] );

const disabled = isOffline || ! isAuthenticated;
const disabled = isOffline || ! isAuthenticated || ! userCanSendMessage;

return (
<div className="h-full flex flex-col bg-gray-50">
Expand Down
21 changes: 21 additions & 0 deletions src/components/tests/add-site.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,25 @@ describe( 'AddSite', () => {
);
} );
} );

it( 'should disable submissions while the site is being added', async () => {
const user = userEvent.setup();
mockGenerateProposedSitePath.mockResolvedValue( {
path: '/default_path/my-wordpress-website',
name: 'My WordPress Website',
isEmpty: true,
isWordPress: false,
} );
mockCreateSite.mockImplementationOnce( () => {
return new Promise( () => {
// no-op
} );
} );
render( <AddSite /> );

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );

expect( screen.getByRole( 'button', { name: 'Adding site…' } ) ).toBeDisabled();
} );
} );
1 change: 1 addition & 0 deletions src/components/tests/onboarding.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jest.mock( '../../lib/get-ipc-api', () => ( {
isEmpty: true,
isWordPress: false,
} ),
promptWindowsSpeedUpSites: jest.fn(),
} ),
} ) );

Expand Down
4 changes: 2 additions & 2 deletions src/components/user-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { sprintf } from '@wordpress/i18n';
import { moreVertical, trash } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useState, useEffect } from 'react';
import { WPCOM_PROFILE_URL } from '../constants';
import { LIMIT_OF_PROMPTS_PER_USER, WPCOM_PROFILE_URL } from '../constants';
import { useAuth } from '../hooks/use-auth';
import { useDeleteSnapshot } from '../hooks/use-delete-snapshot';
import { useFetchSnapshots } from '../hooks/use-fetch-snapshots';
Expand Down Expand Up @@ -142,7 +142,7 @@ const SnapshotInfo = ( {

function PromptInfo() {
const { __ } = useI18n();
const { promptCount = 0, promptLimit = 10 } = usePromptUsage();
const { promptCount = 0, promptLimit = LIMIT_OF_PROMPTS_PER_USER } = usePromptUsage();
const assistantEnabled = getAppGlobals().assistantEnabled;
if ( ! assistantEnabled ) {
return null;
Expand Down
11 changes: 8 additions & 3 deletions src/hooks/tests/use-assistant-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { renderHook, act } from '@testing-library/react';
import { Message } from '../use-assistant';
import { useAssistantApi } from '../use-assistant-api';
import { useAuth } from '../use-auth';
import { usePromptUsage } from '../use-prompt-usage';

jest.mock( '../use-auth' );
jest.mock( '../use-prompt-usage' );

const chatId = 'test-chat-id';

describe( 'useAssistantApi', () => {
const clientReqPost = jest.fn();
beforeEach( () => {
Expand Down Expand Up @@ -53,7 +56,7 @@ describe( 'useAssistantApi', () => {
const { result } = renderHook( () => useAssistantApi() );

await act( async () => {
await expect( result.current.fetchAssistant( [] ) ).rejects.toThrow(
await expect( result.current.fetchAssistant( chatId, [] ) ).rejects.toThrow(
'WPcom client not initialized'
);
} );
Expand All @@ -64,7 +67,9 @@ describe( 'useAssistantApi', () => {

let response = { message: '' };
await act( async () => {
response = await result.current.fetchAssistant( [ { content: 'test', role: 'user' } ] );
response = await result.current.fetchAssistant( chatId, [
{ content: 'test', role: 'user' },
] );
} );

expect( response?.message ).toBe( 'Hello! How can I assist you today?' );
Expand All @@ -79,7 +84,7 @@ describe( 'useAssistantApi', () => {

await act( async () => {
await expect(
result.current.fetchAssistant( [ { content: 'test', role: 'user' } ] )
result.current.fetchAssistant( chatId, [ { content: 'test', role: 'user' } ] )
).rejects.toThrow( 'Failed to fetch assistant' );
} );
} );
Expand Down
15 changes: 10 additions & 5 deletions src/hooks/use-assistant-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ export function useAssistantApi() {
const { updatePromptUsage } = usePromptUsage();

const fetchAssistant = useCallback(
async ( messages: Message[] ) => {
async ( chatId: string | undefined, messages: Message[] ) => {
if ( ! client ) {
throw new Error( 'WPcom client not initialized' );
}
setIsLoading( true );
const body = {
messages,
chat_id: chatId,
context: [],
};
let response;
let headers;
try {
const { data, response_headers } = await new Promise< {
data: { choices: { message: { content: string } }[] };
data: { choices: { message: { content: string } }[]; id: string };
response_headers: Record< string, string >;
} >( ( resolve, reject ) => {
client.req.post(
Expand All @@ -32,7 +34,7 @@ export function useAssistantApi() {
},
(
error: Error,
data: { choices: { message: { content: string } }[] },
data: { choices: { message: { content: string } }[]; id: string },
headers: Record< string, string >
) => {
if ( error ) {
Expand All @@ -48,8 +50,11 @@ export function useAssistantApi() {
setIsLoading( false );
}
const message = response?.choices?.[ 0 ]?.message?.content;
updatePromptUsage( headers );
return { message };
updatePromptUsage( {
maxQuota: headers[ 'x-quota-max' ] || '',
remainingQuota: headers[ 'x-quota-remaining' ] || '',
} );
return { message, chatId: response?.id };
},
[ client, updatePromptUsage ]
);
Expand Down
Loading

0 comments on commit 322868a

Please sign in to comment.