From 4ae3ad56d4a64859083741a3b0f6ba516a3043ab Mon Sep 17 00:00:00 2001 From: Talha12Shiekh Date: Thu, 30 Jul 2026 22:53:49 +0500 Subject: [PATCH] refactor(web): extract SearchBox subcomponents & sanitize image URLs against XSS (#2320, #2323) --- .../src/components/basic/SelectedFilters.js | 6 +- .../src/components/search/AIAnswer/Chat.js | 3 +- .../web/src/components/search/AISection.js | 182 +++++ .../web/src/components/search/SearchBox.js | 642 ++++-------------- .../web/src/components/search/SearchInput.js | 80 +++ .../components/search/SuggestionsSection.js | 221 ++++++ packages/web/src/utils/index.js | 14 + 7 files changed, 634 insertions(+), 514 deletions(-) create mode 100644 packages/web/src/components/search/AISection.js create mode 100644 packages/web/src/components/search/SearchInput.js create mode 100644 packages/web/src/components/search/SuggestionsSection.js diff --git a/packages/web/src/components/basic/SelectedFilters.js b/packages/web/src/components/basic/SelectedFilters.js index a49e2bdb8..18a8a3dc3 100644 --- a/packages/web/src/components/basic/SelectedFilters.js +++ b/packages/web/src/components/basic/SelectedFilters.js @@ -13,7 +13,7 @@ import { getClassName, handleA11yAction } from '@appbaseio/reactivecore/lib/util import Button, { Filter } from '../../styles/Button'; import Container from '../../styles/Container'; import Title from '../../styles/Title'; -import { connect, decodeHtml } from '../../utils'; +import { connect, decodeHtml, sanitizeImageUrl } from '../../utils'; class SelectedFilters extends Component { constructor(props) { @@ -141,7 +141,7 @@ class SelectedFilters extends Component { { selectedValues[component].label}: {decodeHtml(valueToRender)} - {imageValue ? thumbnail : null} + {imageValue ? thumbnail : null} ); @@ -155,7 +155,7 @@ class SelectedFilters extends Component { tabIndex="0" > {selectedValues[component].label}: - thumbnail + thumbnail ); diff --git a/packages/web/src/components/search/AIAnswer/Chat.js b/packages/web/src/components/search/AIAnswer/Chat.js index 5d9944482..72277d855 100644 --- a/packages/web/src/components/search/AIAnswer/Chat.js +++ b/packages/web/src/components/search/AIAnswer/Chat.js @@ -31,6 +31,7 @@ import Mic from '../addons/Mic'; import Button from '../../../styles/Button'; import AIFeedback from '../../shared/AIFeedback'; import { Footer, SourceTags } from '../../../styles/SearchBoxAI'; +import { sanitizeImageUrl } from '../../../utils'; const md = new Remarkable(); @@ -76,7 +77,7 @@ const Chat = (props) => { } if (props.iconURL) { return ( - search-icon + search-icon ); } return ; diff --git a/packages/web/src/components/search/AISection.js b/packages/web/src/components/search/AISection.js new file mode 100644 index 000000000..c30c53176 --- /dev/null +++ b/packages/web/src/components/search/AISection.js @@ -0,0 +1,182 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Remarkable } from 'remarkable'; +import { getClassName } from '@appbaseio/reactivecore/lib/utils/helper'; +import { SearchBoxAISection, Answer } from '../../styles/SearchBoxAI'; +import TypingEffect from '../shared/TypingEffect'; +import AIFeedback from '../shared/AIFeedback'; + +const md = new Remarkable(); +md.set({ + html: true, + breaks: true, + xhtmlOut: true, + linkify: true, + linkTarget: '_blank', +}); + +const AISection = ({ + showAIScreen, + themePreset, + renderAIAnswer, + mergedAIQuestion, + mergedAIAnswer, + AIResponse, + isAIResponseLoading, + isLoading, + getAISourceObjects, + AIResponseError, + renderAIScreenLoader, + currentValue, + prevPropsRefIsAITyping, + setShowAIScreenFooter, + AIUIConfig, + showTypingEffect, + setShowFeedbackComponent, + setShowTypingEffect, + _dropdownULRef, + isUserScrolling, + setLastScrollTop, + renderAIScreenFooter, + showFeedbackComponent, + innerClass, + feedbackState, + sessionIdFromStore, + setFeedbackState, + trackUsefullness, + renderError, + isTypingAIAnswer, +}) => { + if (!showAIScreen) return null; + + return ( + + {typeof renderAIAnswer === 'function' ? ( + renderAIAnswer({ + question: mergedAIQuestion, + answer: mergedAIAnswer, + documentIds: + (AIResponse + && AIResponse.response + && AIResponse.response.answer + && AIResponse.response.answer.documentIds) + || [], + loading: isAIResponseLoading || isLoading, + sources: getAISourceObjects(), + error: AIResponseError, + }) + ) : ( + + {isAIResponseLoading || isLoading ? ( + renderAIScreenLoader() + ) : ( + + + { + if (prevPropsRefIsAITyping.current === undefined) { + setShowAIScreenFooter(true); + } + if ( + (AIUIConfig + && typeof AIUIConfig.showFeedback === 'boolean' + ? AIUIConfig.showFeedback + : true) && + showTypingEffect + ) { + setShowFeedbackComponent(true); + } + + if (mergedAIAnswer) { + setShowTypingEffect(false); + } + + setTimeout(() => { + if (_dropdownULRef.current) { + _dropdownULRef.current.scrollTo({ + top: _dropdownULRef.current.scrollHeight, + behavior: 'smooth', + }); + } + }, 100); + }} + onWhileTyping={() => { + if (!isUserScrolling && _dropdownULRef.current) { + _dropdownULRef.current.scrollTo({ + top: _dropdownULRef.current.scrollHeight, + behavior: 'smooth', + }); + setLastScrollTop(_dropdownULRef.current.scrollHeight); + } + }} + showTypingEffect={isTypingAIAnswer} + /> + + {renderAIScreenFooter()} + + {showFeedbackComponent && ( +
+ {' '} + { + setFeedbackState({ + isRecorded: true, + feedbackType: useful ? 'positive' : 'negative', + }); + trackUsefullness(sessionIdFromStore, { + useful, + reason, + }); + }} + /> +
+ )} +
+ )} +
+ )} + {renderError(true)} +
+ ); +}; + +AISection.propTypes = { + showAIScreen: PropTypes.bool.isRequired, + themePreset: PropTypes.string, + renderAIAnswer: PropTypes.func, + mergedAIQuestion: PropTypes.string, + mergedAIAnswer: PropTypes.string, + AIResponse: PropTypes.object, + isAIResponseLoading: PropTypes.bool, + isLoading: PropTypes.bool, + getAISourceObjects: PropTypes.func.isRequired, + AIResponseError: PropTypes.object, + renderAIScreenLoader: PropTypes.func.isRequired, + currentValue: PropTypes.string, + prevPropsRefIsAITyping: PropTypes.object.isRequired, + setShowAIScreenFooter: PropTypes.func.isRequired, + AIUIConfig: PropTypes.object, + showTypingEffect: PropTypes.bool.isRequired, + setShowFeedbackComponent: PropTypes.func.isRequired, + setShowTypingEffect: PropTypes.func.isRequired, + _dropdownULRef: PropTypes.object.isRequired, + isUserScrolling: PropTypes.bool.isRequired, + setLastScrollTop: PropTypes.func.isRequired, + renderAIScreenFooter: PropTypes.func.isRequired, + showFeedbackComponent: PropTypes.bool.isRequired, + innerClass: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + feedbackState: PropTypes.object, + sessionIdFromStore: PropTypes.string, + setFeedbackState: PropTypes.func.isRequired, + trackUsefullness: PropTypes.func.isRequired, + renderError: PropTypes.func.isRequired, + isTypingAIAnswer: PropTypes.bool.isRequired, +}; + +export default AISection; diff --git a/packages/web/src/components/search/SearchBox.js b/packages/web/src/components/search/SearchBox.js index 9af1f5eb9..b72b6cb9f 100644 --- a/packages/web/src/components/search/SearchBox.js +++ b/packages/web/src/components/search/SearchBox.js @@ -63,6 +63,7 @@ import { extractModifierKeysFromFocusShortcuts, isEmpty, parseFocusShortcuts, + sanitizeImageUrl, } from '../../utils'; import Mic from './addons/Mic'; import CancelSvg from '../shared/CancelSvg'; @@ -80,6 +81,9 @@ import { innerText } from '../shared/innerText'; import TextWithTooltip from './addons/TextWithTooltip'; import { Thumbnail, CameraIcon } from './addons/CameraIcon'; import { FallbackRender } from './addons/FallbackRender'; +import AISection from './AISection'; +import SuggestionsSection from './SuggestionsSection'; +import SearchInput from './SearchInput'; const md = new Remarkable(); @@ -1156,7 +1160,7 @@ const SearchBox = (props) => { } if (props.iconURL) { return ( - search-icon + search-icon ); } return ; @@ -1771,7 +1775,7 @@ const SearchBox = (props) => { return ( {item.value} ); @@ -1839,410 +1843,54 @@ const SearchBox = (props) => { ref={_dropdownULRef} className={`${getClassName(props.innerClass, 'list')}`} > - {showAIScreen ? ( - - {typeof props.renderAIAnswer === 'function' ? ( - props.renderAIAnswer({ - question: mergedAIQuestion, - answer: mergedAIAnswer, - documentIds: - (props.AIResponse - && props.AIResponse.response - && props.AIResponse.response - .answer - && props.AIResponse.response.answer - .documentIds) - || [], - loading: - props.isAIResponseLoading - || props.isLoading, - sources: getAISourceObjects(), - error: props.AIResponseError, - }) - ) : ( - - {props.isAIResponseLoading - || props.isLoading ? ( - renderAIScreenLoader() - ) : ( - - - { - if ( - prevPropsRefIsAITyping.current - === undefined - ) { - setShowAIScreenFooter( - true, - ); - } - if ( - (props.AIUIConfig - && typeof props - .AIUIConfig - .showFeedback - === 'boolean' - ? props - .AIUIConfig - .showFeedback - : true) - && showTypingEffect - ) { - setShowFeedbackComponent( - true, - ); - } - - if ( - mergedAIAnswer - ) { - setShowTypingEffect( - false, - ); - } - - setTimeout(() => { - _dropdownULRef.current.scrollTo( - { - top: - _dropdownULRef - .current - .scrollHeight, - behavior: - 'smooth', - }, - ); - }, 100); - }} - onWhileTyping={() => { - if (!isUserScrolling) { - _dropdownULRef.current.scrollTo({ - top: _dropdownULRef.current.scrollHeight, - behavior: 'smooth', - }); - setLastScrollTop(_dropdownULRef.current.scrollHeight); - } - }} - showTypingEffect={ - isTypingAIAnswer - } - /> - - {renderAIScreenFooter()} - - {showFeedbackComponent && ( -
- {' '} - { - setFeedbackState( - { - isRecorded: true, - feedbackType: useful - ? 'positive' - : 'negative', - }, - ); - props.trackUsefullness( - props.sessionIdFromStore, - { - useful, - reason, - }, - ); - }} - /> -
- )} -
- )} -
- )} - {renderError(true)} -
- ) : null} - {!showAIScreen ? ( - - {parsedSuggestions().map((item, itemIndex) => { - const index = indexOffset + itemIndex; - if (Array.isArray(item)) { - const sectionHtml = XSS( - item[0].sectionLabel, - ); - indexOffset += item.length - 1; - return ( -
- {sectionHtml && ( -
- )} -
    - {item.map( - ( - sectionItem, - sectionIndex, - ) => ( -
  • - - -
    - -
    - - {sectionItem.label && ( - - )} - {sectionItem.description && ( - - )} - - {getActionIcon( - sectionItem, - )} -
    -
    -
  • - ), - )} -
-
- ); - } - - if ( - item._suggestion_type - === '_internal_a_i_trigger' - ) { - return ( -
  • - - - - - -
  • - ); - } - return ( -
  • - - - {/* eslint-disable */} - -
    - -
    - {/* eslint-enable */} - - - {getActionIcon(item)} -
    -
    -
  • - ); - })} - - {showSuggestionsFooter ? ( - - ) : null} - - ) : null} - {!showAIScreen && !hasSuggestions() - ? renderNoSuggestion(parsedSuggestions()) : null - } + + ) : null} @@ -2256,69 +1904,54 @@ const SearchBox = (props) => { { suppressRefError: true }, )} > - - - {renderLeftIcons()} - {renderInputAddonBefore()} - - -