diff --git a/packages/instantsearch.css/src/themes/satellite.scss b/packages/instantsearch.css/src/themes/satellite.scss index 23bc6e1992..0585d5d253 100644 --- a/packages/instantsearch.css/src/themes/satellite.scss +++ b/packages/instantsearch.css/src/themes/satellite.scss @@ -546,17 +546,86 @@ $break-medium: 767px; display: none; } -.ais-SearchBox-form .ais-AiModeButton { +.ais-SearchBox-form .ais-AiModeButton-pill { position: absolute; right: 0.375rem; top: 50%; transform: translateY(-50%); + display: inline-flex; + align-items: stretch; +} + +.ais-AiModeButton-pill .ais-AiModeButton { + border-bottom-right-radius: 0; + border-right: 0; + border-top-right-radius: 0; + + transition: background-color var(--ais-transition-duration) + var(--ais-transition-timing-function), + border-color var(--ais-transition-duration) + var(--ais-transition-timing-function), + color var(--ais-transition-duration) + var(--ais-transition-timing-function), + border-radius var(--ais-transition-duration) + var(--ais-transition-timing-function); +} + +.ais-AiModeButton-enterHint { + font-size: calc(var(--ais-font-size) * 0.75); + opacity: 0.7; + + transition: opacity var(--ais-transition-duration) + var(--ais-transition-timing-function); +} + +.ais-AiModeButton-pill--disabled .ais-AiModeButton, +.ais-AiModeButton-pill--disabled .ais-AiModeButton-dismiss { + background-color: transparent; +} + +.ais-AiModeButton-pill--disabled .ais-AiModeButton-enterHint { + display: none; +} + +@media (hover: hover) { + .ais-AiModeButton-pill--disabled .ais-AiModeButton:hover, + .ais-AiModeButton-pill--disabled .ais-AiModeButton-dismiss:hover { + background-color: rgba(var(--ais-primary-color-rgb), 0.08); + } +} + +.ais-AiModeButton-dismiss { + align-items: center; + background-color: rgba(var(--ais-primary-color-rgb), 0.08); + border: 1px solid rgba(var(--ais-primary-color-rgb), 0.3); + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-radius: 0 var(--ais-border-radius-sm) var(--ais-border-radius-sm) 0; + color: rgba(var(--ais-primary-color-rgb), 1); + cursor: pointer; + display: inline-flex; + font: inherit; + font-size: 1rem; + justify-content: center; + line-height: 1; + padding: 0 0.5rem; + transition: background-color var(--ais-transition-duration) + var(--ais-transition-timing-function), + border-color var(--ais-transition-duration) + var(--ais-transition-timing-function); + + @media (hover: hover) { + &:hover { + background-color: rgba(var(--ais-primary-color-rgb), 0.15); + border-color: rgba(var(--ais-primary-color-rgb), 1); + } + } } -.ais-SearchBox-form:has(.ais-AiModeButton) .ais-SearchBox-reset, -.ais-SearchBox-form:has(.ais-AiModeButton) +.ais-SearchBox-form:has(.ais-AiModeButton-pill) .ais-SearchBox-reset, +.ais-SearchBox-form:has(.ais-AiModeButton-pill) .ais-SearchBox-loadingIndicator { - right: 7rem; + right: 8.5rem; } .ais-Menu-searchBox, diff --git a/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx b/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx index 66292970ed..1982734bc5 100644 --- a/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx +++ b/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx @@ -73,6 +73,7 @@ type SearchBoxPropsWithDefaultProps = SearchBoxProps & type SearchBoxState = { query: string; focused: boolean; + aiModeActive: boolean; }; class SearchBox extends Component< @@ -84,6 +85,7 @@ class SearchBox extends Component< public state = { query: this.props.query, focused: false, + aiModeActive: true, }; private input = createRef(); @@ -133,7 +135,9 @@ class SearchBox extends Component< } private onSubmit = (event: Event) => { - const { searchAsYouType, refine, onSubmit } = this.props; + const { searchAsYouType, refine, onSubmit, onAiModeClick } = this.props; + const aiModeSubmitsToChat = + Boolean(onAiModeClick) && this.state.aiModeActive; event.preventDefault(); event.stopPropagation(); @@ -141,7 +145,10 @@ class SearchBox extends Component< this.input.current.blur(); } - if (!searchAsYouType) { + if (aiModeSubmitsToChat && onAiModeClick) { + // AI mode pill is active: route Enter to the chat instead of the search. + onAiModeClick(this.state.query); + } else if (!searchAsYouType) { refine(this.state.query); } @@ -150,6 +157,11 @@ class SearchBox extends Component< return false; }; + private onAiModeToggleShortcut = (event: Event) => { + event.preventDefault(); + this.setState((prev) => ({ aiModeActive: !prev.aiModeActive })); + }; + private onReset = (event: Event) => { const { refine, onReset } = this.props; const query = ''; @@ -180,6 +192,8 @@ class SearchBox extends Component< private onAiModeClick = (event: Event) => { event.preventDefault(); + // Pill body always submits to chat. The Enter shortcut is what gets + // disabled via the side toggle. this.props.onAiModeClick?.(this.state.query); }; @@ -277,19 +291,46 @@ class SearchBox extends Component< )} {onAiModeClick && ( -