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 ?
: null}
+ {imageValue ?
: null}
✕
);
@@ -155,7 +155,7 @@ class SelectedFilters extends Component {
tabIndex="0"
>
{selectedValues[component].label}:
-
+
✕
);
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 (
-
+
);
}
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 (
-
+
);
}
return ;
@@ -1771,7 +1775,7 @@ const SearchBox = (props) => {
return (
);
@@ -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()}
-
-
-
-
- {renderRightIcons()}
- {renderInputAddonAfter()}
- {renderAskButtonElement()}
- {renderEnterButtonElement()}
-
-
+ inputRef={_inputRef}
+ inputProps={getInputProps({
+ className: getClassName(props.innerClass, 'input'),
+ placeholder: props.placeholder,
+ value: Object.hasOwn(props, 'value')
+ ? props.value
+ : currentValue || '',
+ onChange: onInputChange,
+ onBlur: withTriggerQuery(props.onBlur),
+ onFocus: handleFocus,
+ onClick: () => {
+ setHighlightedIndex(null);
+ },
+ onKeyPress: withTriggerQuery(props.onKeyPress),
+ onKeyDown: e => handleKeyDown(e, highlightedIndex),
+ onKeyUp: withTriggerQuery(props.onKeyUp),
+ autoFocus: props.autoFocus,
+ id: `${props.componentId}-input`,
+ })}
+ renderLeftIcons={renderLeftIcons}
+ renderInputAddonBefore={renderInputAddonBefore}
+ renderRightIcons={renderRightIcons}
+ renderInputAddonAfter={renderInputAddonAfter}
+ renderAskButtonElement={renderAskButtonElement}
+ renderEnterButtonElement={renderEnterButtonElement}
+ themePreset={props.themePreset}
+ type={props.type}
+ showFocusShortcutsIcon={props.showFocusShortcutsIcon}
+ showVoiceSearch={props.showVoiceSearch}
+ iconPosition={props.iconPosition}
+ showIcon={props.showIcon}
+ showClear={props.showClear}
+ expandSuggestionsContainer={props.expandSuggestionsContainer}
+ renderSuggestionsDropdown={() =>
+ renderSuggestionsDropdown(
+ getRootProps,
+ getInputProps,
+ getItemProps,
+ isOpen,
+ highlightedIndex,
+ setHighlightedIndex,
+ ...rest,
+ )
+ }
+ />
{showImageDropdown ? (
{
/>
) : (
-
-
-
- {renderLeftIcons()}
- {renderInputAddonBefore()}
-
-
-
-
-
- {renderRightIcons()}
- {renderInputAddonAfter()}
- {renderAskButtonElement()}
- {renderEnterButtonElement()}
-
-
+
{showImageDropdown ?
(
+
+
+ {renderLeftIcons()}
+ {renderInputAddonBefore()}
+
+
+
+ {!expandSuggestionsContainer && renderSuggestionsDropdown && renderSuggestionsDropdown()}
+
+
+ {renderRightIcons()}
+ {renderInputAddonAfter()}
+ {renderAskButtonElement()}
+ {renderEnterButtonElement()}
+
+
+);
+
+SearchInput.propTypes = {
+ inputGroupRef: PropTypes.object,
+ isOpen: PropTypes.bool,
+ inputRef: PropTypes.object,
+ inputProps: PropTypes.object.isRequired,
+ renderLeftIcons: PropTypes.func.isRequired,
+ renderInputAddonBefore: PropTypes.func.isRequired,
+ renderRightIcons: PropTypes.func.isRequired,
+ renderInputAddonAfter: PropTypes.func.isRequired,
+ renderAskButtonElement: PropTypes.func.isRequired,
+ renderEnterButtonElement: PropTypes.func.isRequired,
+ themePreset: PropTypes.string,
+ type: PropTypes.string,
+ showFocusShortcutsIcon: PropTypes.bool,
+ showVoiceSearch: PropTypes.bool,
+ iconPosition: PropTypes.string,
+ showIcon: PropTypes.bool,
+ showClear: PropTypes.bool,
+ renderSuggestionsDropdown: PropTypes.func,
+ expandSuggestionsContainer: PropTypes.bool,
+};
+
+export default SearchInput;
diff --git a/packages/web/src/components/search/SuggestionsSection.js b/packages/web/src/components/search/SuggestionsSection.js
new file mode 100644
index 000000000..983dd5006
--- /dev/null
+++ b/packages/web/src/components/search/SuggestionsSection.js
@@ -0,0 +1,221 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import XSS from 'xss';
+import { getClassName } from '@appbaseio/reactivecore/lib/utils/helper';
+import CustomSvg from '../shared/CustomSvg';
+import SuggestionItem from './addons/SuggestionItem';
+import TextWithTooltip from './addons/TextWithTooltip';
+import { FallbackRender } from './addons/FallbackRender';
+import { Suggestion, SuggestionDescription } from '../../styles/Input';
+
+const SuggestionsSection = ({
+ showAIScreen,
+ parsedSuggestions,
+ indexOffset = 0,
+ innerClass,
+ getItemProps,
+ highlightedIndex,
+ renderItem,
+ getIcon,
+ getActionIcon,
+ currentValue,
+ showSuggestionsFooter,
+ SuggestionsFooter,
+ hasSuggestions,
+ renderNoSuggestion,
+}) => {
+ if (showAIScreen) return null;
+
+ const suggestionsList = parsedSuggestions();
+
+ if (!hasSuggestions()) {
+ return renderNoSuggestion(suggestionsList);
+ }
+
+ let localIndexOffset = indexOffset;
+
+ return (
+
+ {suggestionsList.map((item, itemIndex) => {
+ const index = localIndexOffset + itemIndex;
+ if (Array.isArray(item)) {
+ const sectionHtml = XSS(item[0].sectionLabel);
+ localIndexOffset += 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 (
+
+
+
+
+
+
+
+ {getActionIcon(item)}
+
+
+
+ );
+ })}
+
+ {showSuggestionsFooter ? : null}
+
+ );
+};
+
+SuggestionsSection.propTypes = {
+ showAIScreen: PropTypes.bool.isRequired,
+ parsedSuggestions: PropTypes.func.isRequired,
+ indexOffset: PropTypes.number,
+ innerClass: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
+ getItemProps: PropTypes.func.isRequired,
+ highlightedIndex: PropTypes.number,
+ renderItem: PropTypes.func,
+ getIcon: PropTypes.func.isRequired,
+ getActionIcon: PropTypes.func.isRequired,
+ currentValue: PropTypes.string,
+ showSuggestionsFooter: PropTypes.bool.isRequired,
+ SuggestionsFooter: PropTypes.func.isRequired,
+ hasSuggestions: PropTypes.func.isRequired,
+ renderNoSuggestion: PropTypes.func.isRequired,
+};
+
+export default SuggestionsSection;
diff --git a/packages/web/src/utils/index.js b/packages/web/src/utils/index.js
index 7e58cce37..99d3368af 100644
--- a/packages/web/src/utils/index.js
+++ b/packages/web/src/utils/index.js
@@ -231,3 +231,17 @@ export function useAnalytics() {
const ctx = useContext(ReactReduxContext);
return ctx && ctx.storeState.analyticsRef;
}
+
+export function sanitizeImageUrl(url) {
+ if (!url) return null;
+ try {
+ const parsed = new URL(url, 'http://localhost');
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:' || parsed.protocol === 'data:') {
+ return url;
+ }
+ } catch (e) {
+ /* invalid URL */
+ }
+ return null;
+}
+