diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b2d8f7..6821234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.25.4-alpha] - unreleased + +This is an alpha version! The changes listed here are not final. + +### Added +- AI Client: Add thumbs feedback on AI Assistant + ## [0.25.3] - 2024-12-23 ### Added - Jetpack AI: Add thumbs up/down component to AI logo generator [#40610] @@ -487,6 +494,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - AI Client: stop using smart document visibility handling on the fetchEventSource library, so it does not restart the completion when changing tabs. [#32004] - Updated package dependencies. [#31468] [#31659] [#31785] +[0.25.4-alpha]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.3...v0.25.4-alpha [0.25.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.2...v0.25.3 [0.25.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.1...v0.25.2 [0.25.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.0...v0.25.1 diff --git a/build/ai-client/src/components/ai-control/block-ai-control.js b/build/ai-client/src/components/ai-control/block-ai-control.js index 22f3996..64c9ba0 100644 --- a/build/ai-client/src/components/ai-control/block-ai-control.js +++ b/build/ai-client/src/components/ai-control/block-ai-control.js @@ -76,7 +76,10 @@ export function BlockAIControl({ disabled = false, value = '', placeholder = '', target: promptUserInputRef, }); const actions = (_jsxs(_Fragment, { children: [(!showAccept || editRequest) && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: !loading ? (_jsxs(_Fragment, { children: [editRequest && (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: cancelEdit, variant: "secondary", label: __('Cancel', 'jetpack-ai-client'), children: showButtonLabels ? (__('Cancel', 'jetpack-ai-client')) : (_jsx(Icon, { icon: closeSmall })) })), showRemove && !editRequest && !value?.length && onDiscard && (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: discardHandler, variant: "secondary", label: __('Cancel', 'jetpack-ai-client'), children: showButtonLabels ? (__('Cancel', 'jetpack-ai-client')) : (_jsx(Icon, { icon: closeSmall })) })), value?.length > 0 && (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: sendHandler, variant: "primary", disabled: !value?.length || disabled, label: __('Send request', 'jetpack-ai-client'), children: showButtonLabels ? (__('Generate', 'jetpack-ai-client')) : (_jsx(Icon, { icon: arrowUp })) }))] })) : (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: onStop, variant: "secondary", label: __('Stop request', 'jetpack-ai-client'), children: showButtonLabels ? (__('Stop', 'jetpack-ai-client')) : (_jsx(Icon, { icon: closeSmall })) })) })), showAccept && !editRequest && (_jsxs("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: [(value?.length > 0 || lastValue === null) && (_jsxs(ButtonGroup, { children: [_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Discard', 'jetpack-ai-client'), onClick: discardHandler, tooltipPosition: "top", children: _jsx(Icon, { icon: trash }) }), _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Regenerate', 'jetpack-ai-client'), onClick: () => onSend?.(value), tooltipPosition: "top", disabled: !value?.length || value === null || disabled, children: _jsx(Icon, { icon: regenerate }) })] })), _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: onAccept, variant: "primary", label: acceptLabel, children: showButtonLabels ? acceptLabel : _jsx(Icon, { icon: check }) })] }))] })); - const message = showGuideLine && !loading && !editRequest && (customFooter || _jsx(GuidelineMessage, {})); + const message = showGuideLine && + !loading && + !editRequest && + (customFooter || (_jsx(GuidelineMessage, { showAIFeedbackThumbs: true, ratedItem: 'ai-assistant', prompt: value }))); return (_jsx(AIControl, { disabled: disabled || loading, value: value, placeholder: placeholder, isTransparent: isTransparent, state: state, onChange: changeHandler, banner: banner, error: error, actions: actions, message: message, promptUserInputRef: promptUserInputRef })); } export default forwardRef(BlockAIControl); diff --git a/build/ai-client/src/components/ai-control/extension-ai-control.js b/build/ai-client/src/components/ai-control/extension-ai-control.js index 8f38bf6..a160978 100644 --- a/build/ai-client/src/components/ai-control/extension-ai-control.js +++ b/build/ai-client/src/components/ai-control/extension-ai-control.js @@ -26,6 +26,9 @@ export function ExtensionAIControl({ className, disabled = false, value = '', pl const [editRequest, setEditRequest] = useState(false); const [lastValue, setLastValue] = useState(value || null); const promptUserInputRef = useRef(null); + const isDone = value?.length <= 0 && state === 'done'; + const [initialPlaceholder] = useState(placeholder); + const [prompt, setPrompt] = useState(null); // Pass the ref to forwardRef. useImperativeHandle(ref, () => promptUserInputRef.current); useEffect(() => { @@ -33,8 +36,15 @@ export function ExtensionAIControl({ className, disabled = false, value = '', pl promptUserInputRef?.current?.focus(); } }, [editRequest]); + useEffect(() => { + if (placeholder !== initialPlaceholder) { + // The prompt is used to determine if there was a toolbar action + setPrompt(placeholder); + } + }, [placeholder]); const sendHandler = useCallback(() => { setLastValue(value); + setPrompt(value); setEditRequest(false); onSend?.(value); }, [onSend, value]); @@ -73,7 +83,7 @@ export function ExtensionAIControl({ className, disabled = false, value = '', pl }, { target: promptUserInputRef, }); - const actions = (_jsx(_Fragment, { children: loading ? (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: stopHandler, variant: "secondary", label: __('Stop request', 'jetpack-ai-client'), children: showButtonLabels ? __('Stop', 'jetpack-ai-client') : _jsx(Icon, { icon: closeSmall }) })) : (_jsxs(_Fragment, { children: [value?.length > 0 && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: sendHandler, variant: "primary", disabled: !value?.length || disabled, label: __('Send request', 'jetpack-ai-client'), children: showButtonLabels ? (__('Generate', 'jetpack-ai-client')) : (_jsx(Icon, { icon: arrowUp })) }) })), value?.length <= 0 && state === 'done' && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: _jsxs(ButtonGroup, { children: [_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Undo', 'jetpack-ai-client'), onClick: undoHandler, tooltipPosition: "top", children: _jsx(Icon, { icon: undo }) }), _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Close', 'jetpack-ai-client'), onClick: closeHandler, variant: "tertiary", children: __('Close', 'jetpack-ai-client') })] }) }))] })) })); + const actions = (_jsx(_Fragment, { children: loading ? (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: stopHandler, variant: "secondary", label: __('Stop request', 'jetpack-ai-client'), children: showButtonLabels ? __('Stop', 'jetpack-ai-client') : _jsx(Icon, { icon: closeSmall }) })) : (_jsxs(_Fragment, { children: [value?.length > 0 && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: sendHandler, variant: "primary", disabled: !value?.length || disabled, label: __('Send request', 'jetpack-ai-client'), children: showButtonLabels ? (__('Generate', 'jetpack-ai-client')) : (_jsx(Icon, { icon: arrowUp })) }) })), isDone && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: _jsxs(ButtonGroup, { children: [_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Undo', 'jetpack-ai-client'), onClick: undoHandler, tooltipPosition: "top", children: _jsx(Icon, { icon: undo }) }), _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Close', 'jetpack-ai-client'), onClick: closeHandler, variant: "tertiary", children: __('Close', 'jetpack-ai-client') })] }) }))] })) })); let message = null; if (error?.message) { message = (_jsx(ErrorMessage, { error: error.message, code: error.code, onTryAgainClick: tryAgainHandler, onUpgradeClick: upgradeHandler, upgradeUrl: upgradeUrl })); @@ -85,7 +95,7 @@ export function ExtensionAIControl({ className, disabled = false, value = '', pl message = (_jsx(UpgradeMessage, { requestsRemaining: requestsRemaining, onUpgradeClick: upgradeHandler, upgradeUrl: upgradeUrl })); } else if (showGuideLine) { - message = _jsx(GuidelineMessage, {}); + message = isDone ? (_jsx(GuidelineMessage, { showAIFeedbackThumbs: true, ratedItem: 'ai-assistant', prompt: prompt })) : (_jsx(GuidelineMessage, {})); } return (_jsx(AIControl, { className: className, disabled: disabled || loading, value: value, placeholder: placeholder, isTransparent: isTransparent, state: state, onChange: changeHandler, actions: actions, message: message, promptUserInputRef: promptUserInputRef, wrapperRef: wrapperRef })); } diff --git a/build/ai-client/src/components/message/index.d.ts b/build/ai-client/src/components/message/index.d.ts index bf15973..fd549ed 100644 --- a/build/ai-client/src/components/message/index.d.ts +++ b/build/ai-client/src/components/message/index.d.ts @@ -12,13 +12,20 @@ export declare const MESSAGE_SEVERITY_ERROR = "error"; export declare const MESSAGE_SEVERITY_SUCCESS = "success"; export declare const MESSAGE_SEVERITY_INFO = "info"; export type MessageSeverityProp = typeof MESSAGE_SEVERITY_WARNING | typeof MESSAGE_SEVERITY_ERROR | typeof MESSAGE_SEVERITY_SUCCESS | typeof MESSAGE_SEVERITY_INFO | null; +type RateProps = { + ratedItem?: string; + prompt?: string; + onRate?: (rating: string) => void; +}; export type MessageProps = { icon?: React.ReactNode; severity?: MessageSeverityProp; - showSidebarIcon?: boolean; - onSidebarIconClick?: () => void; + showAIFeedbackThumbs?: boolean; children: React.ReactNode; -}; +} & RateProps; +export type GuidelineMessageProps = { + showAIFeedbackThumbs?: boolean; +} & RateProps; export type OnUpgradeClick = (event?: React.MouseEvent) => void; export type UpgradeMessageProps = { requestsRemaining: number; @@ -39,13 +46,14 @@ export type ErrorMessageProps = { * @param {MessageProps} props - Component props. * @return {React.ReactElement} Banner component. */ -export default function Message({ severity, icon, showSidebarIcon, onSidebarIconClick, children, }: MessageProps): React.ReactElement; +export default function Message({ severity, icon, showAIFeedbackThumbs, ratedItem, prompt, onRate, children, }: MessageProps): React.ReactElement; /** * React component to render a guideline message. * + * @param {GuidelineMessageProps} props - Component props. * @return {React.ReactElement} - Message component. */ -export declare function GuidelineMessage(): React.ReactElement; +export declare function GuidelineMessage({ showAIFeedbackThumbs, ...props }: GuidelineMessageProps): React.ReactElement; /** * React component to render a fair usage limit message. * @@ -66,3 +74,4 @@ export declare function UpgradeMessage({ requestsRemaining, severity, onUpgradeC * @return {React.ReactElement} - Message component. */ export declare function ErrorMessage({ error, code, onTryAgainClick, onUpgradeClick, upgradeUrl, }: ErrorMessageProps): React.ReactElement; +export {}; diff --git a/build/ai-client/src/components/message/index.js b/build/ai-client/src/components/message/index.js index 627c3b2..ffee748 100644 --- a/build/ai-client/src/components/message/index.js +++ b/build/ai-client/src/components/message/index.js @@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { ExternalLink, Button } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { Icon, check, arrowRight } from '@wordpress/icons'; +import { Icon, check } from '@wordpress/icons'; import clsx from 'clsx'; /** * Internal dependencies @@ -13,6 +13,7 @@ import clsx from 'clsx'; import './style.scss'; import errorExclamation from '../../icons/error-exclamation.js'; import { ERROR_QUOTA_EXCEEDED } from '../../types.js'; +import AiFeedbackThumbs from '../ai-feedback/index.js'; export const MESSAGE_SEVERITY_WARNING = 'warning'; export const MESSAGE_SEVERITY_ERROR = 'error'; export const MESSAGE_SEVERITY_SUCCESS = 'success'; @@ -29,8 +30,10 @@ const messageIconsMap = { * @param {MessageProps} props - Component props. * @return {React.ReactElement} Banner component. */ -export default function Message({ severity = MESSAGE_SEVERITY_INFO, icon = null, showSidebarIcon = false, onSidebarIconClick = () => { }, children, }) { - return (_jsxs("div", { className: clsx('jetpack-ai-assistant__message', `jetpack-ai-assistant__message-severity-${severity}`), children: [(messageIconsMap[severity] || icon) && (_jsx(Icon, { icon: messageIconsMap[severity] || icon })), _jsx("div", { className: "jetpack-ai-assistant__message-content", children: children }), showSidebarIcon && (_jsx(Button, { className: "jetpack-ai-assistant__message-sidebar", onClick: onSidebarIconClick, children: _jsx(Icon, { size: 20, icon: arrowRight }) }))] })); +export default function Message({ severity = MESSAGE_SEVERITY_INFO, icon = null, showAIFeedbackThumbs = false, ratedItem = '', prompt = '', onRate = () => { }, children, }) { + return (_jsxs("div", { className: clsx('jetpack-ai-assistant__message', `jetpack-ai-assistant__message-severity-${severity}`), children: [(messageIconsMap[severity] || icon) && (_jsx(Icon, { icon: messageIconsMap[severity] || icon })), _jsx("div", { className: "jetpack-ai-assistant__message-content", children: children }), showAIFeedbackThumbs && (_jsx(AiFeedbackThumbs, { disabled: false, ratedItem: ratedItem, feature: "ai-assistant", options: { + prompt, + }, onRate: onRate }))] })); } /** * React component to render a learn more link. @@ -43,10 +46,11 @@ function LearnMoreLink() { /** * React component to render a guideline message. * + * @param {GuidelineMessageProps} props - Component props. * @return {React.ReactElement} - Message component. */ -export function GuidelineMessage() { - return (_jsxs(Message, { children: [_jsx("span", { children: __('AI-generated content could be inaccurate or biased.', 'jetpack-ai-client') }), _jsx(LearnMoreLink, {})] })); +export function GuidelineMessage({ showAIFeedbackThumbs = false, ...props }) { + return (_jsxs(Message, { showAIFeedbackThumbs: showAIFeedbackThumbs, ...props, children: [_jsx("span", { children: __('AI-generated content could be inaccurate or biased.', 'jetpack-ai-client') }), _jsx(LearnMoreLink, {})] })); } /** * React component to render a fair usage limit message. diff --git a/package.json b/package.json index 2ffdd5e..9b2d46d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@automattic/jetpack-ai-client", - "version": "0.25.3", + "version": "0.25.4-alpha", "description": "A JS client for consuming Jetpack AI services", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/ai-client/#readme", "bugs": { diff --git a/src/components/ai-control/block-ai-control.tsx b/src/components/ai-control/block-ai-control.tsx index 6d25d30..69b0965 100644 --- a/src/components/ai-control/block-ai-control.tsx +++ b/src/components/ai-control/block-ai-control.tsx @@ -256,7 +256,16 @@ export function BlockAIControl( ); const message = - showGuideLine && ! loading && ! editRequest && ( customFooter || ); + showGuideLine && + ! loading && + ! editRequest && + ( customFooter || ( + + ) ); return ( promptUserInputRef.current ); @@ -95,8 +98,16 @@ export function ExtensionAIControl( } }, [ editRequest ] ); + useEffect( () => { + if ( placeholder !== initialPlaceholder ) { + // The prompt is used to determine if there was a toolbar action + setPrompt( placeholder ); + } + }, [ placeholder ] ); + const sendHandler = useCallback( () => { setLastValue( value ); + setPrompt( value ); setEditRequest( false ); onSend?.( value ); }, [ onSend, value ] ); @@ -183,7 +194,7 @@ export function ExtensionAIControl( ) } - { value?.length <= 0 && state === 'done' && ( + { isDone && (
+ {
{ children }
} + { showAIFeedbackThumbs && ( + ) }
); @@ -111,11 +129,15 @@ function LearnMoreLink(): React.ReactElement { /** * React component to render a guideline message. * + * @param {GuidelineMessageProps} props - Component props. * @return {React.ReactElement} - Message component. */ -export function GuidelineMessage(): React.ReactElement { +export function GuidelineMessage( { + showAIFeedbackThumbs = false, + ...props +}: GuidelineMessageProps ): React.ReactElement { return ( - + { __( 'AI-generated content could be inaccurate or biased.', 'jetpack-ai-client' ) }