@redocly/theme 0.44.7 → 0.44.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -154,7 +154,7 @@ const StyledButton = styled_components_1.default.button.attrs((props) => ({
154
154
  `}
155
155
  `;
156
156
  const ButtonComponent = (props) => {
157
- const button = (react_1.default.createElement(StyledButton, Object.assign({ "data-component-name": "Button/Button" }, props, { iconOnly: !props.children && props.icon !== null, tabIndex: props.to ? -1 : 0 }),
157
+ const button = (react_1.default.createElement(StyledButton, Object.assign({ "data-component-name": "Button/Button" }, props, { iconOnly: !props.children && props.icon !== null, tabIndex: props.to ? -1 : undefined }),
158
158
  props.icon && props.iconPosition !== 'right' && props.icon,
159
159
  props.children,
160
160
  props.icon && props.iconPosition === 'right' && props.icon));
@@ -0,0 +1,10 @@
1
+ export type SearchAiResponseProps = {
2
+ question: string;
3
+ isGeneratingResponse: boolean;
4
+ response?: string;
5
+ resources: {
6
+ url: string;
7
+ title: string;
8
+ }[];
9
+ };
10
+ export declare function SearchAiResponse(props: SearchAiResponseProps): JSX.Element;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SearchAiResponse = SearchAiResponse;
7
+ const react_1 = __importDefault(require("react"));
8
+ const styled_components_1 = __importDefault(require("styled-components"));
9
+ const Spinner_1 = require("../../icons/Spinner/Spinner");
10
+ const CheckmarkFilledIcon_1 = require("../../icons/CheckmarkFilledIcon/CheckmarkFilledIcon");
11
+ const DocumentIcon_1 = require("../../icons/DocumentIcon/DocumentIcon");
12
+ const Tag_1 = require("../../components/Tag/Tag");
13
+ const Link_1 = require("../../components/Link/Link");
14
+ const hooks_1 = require("../../core/hooks");
15
+ function SearchAiResponse(props) {
16
+ const { question, response, isGeneratingResponse, resources } = props;
17
+ const { useTranslate } = (0, hooks_1.useThemeHooks)();
18
+ const { translate } = useTranslate();
19
+ return (react_1.default.createElement(ResponseWrapper, { "data-component-name": "Search/AiResponse" },
20
+ react_1.default.createElement(ResponseHeader, null,
21
+ isGeneratingResponse ? (react_1.default.createElement(Spinner_1.Spinner, { size: "20px", color: "--search-ai-spinner-icon-color" })) : (react_1.default.createElement(CheckmarkFilledIcon_1.CheckmarkFilledIcon, { size: "20px", color: "--search-ai-checkmark-icon-color" })),
22
+ react_1.default.createElement(Question, null, question)),
23
+ react_1.default.createElement(ResponseBody, null,
24
+ react_1.default.createElement(ResponseText, { "data-translation-key": response ? undefined : 'search.ai.thinkingText' }, response ? response : translate('search.ai.thinkingText', 'Thinking...')),
25
+ resources.length && !isGeneratingResponse ? (react_1.default.createElement(Resources, null,
26
+ react_1.default.createElement(ResourcesTitle, { "data-translation-key": "search.ai.resourcesFound" },
27
+ resources.length,
28
+ " ",
29
+ translate('search.ai.resourcesFound', 'resources found')),
30
+ react_1.default.createElement(ResourceTags, null, resources.map((resource, idx) => (react_1.default.createElement(Link_1.Link, { key: idx, to: resource.url },
31
+ react_1.default.createElement(ResourceTag, { borderless: true, icon: react_1.default.createElement(DocumentIcon_1.DocumentIcon, { color: "--search-ai-resource-tag-icon-color" }) }, resource.title))))))) : null)));
32
+ }
33
+ const ResponseWrapper = styled_components_1.default.div `
34
+ display: flex;
35
+ flex-direction: column;
36
+ flex: 2;
37
+ flex-grow: 2;
38
+ overflow-y: scroll;
39
+ overscroll-behavior: contain;
40
+ padding: var(--search-ai-response-padding);
41
+ gap: var(--search-ai-response-gap);
42
+ `;
43
+ const ResponseHeader = styled_components_1.default.div `
44
+ display: flex;
45
+ flex-direction: row;
46
+ gap: var(--search-ai-response-header-gap);
47
+ align-items: center;
48
+ `;
49
+ const Question = styled_components_1.default.div `
50
+ font-size: var(--search-ai-question-font-size);
51
+ font-weight: var(--search-ai-question-font-weight);
52
+ line-height: var(--search-ai-question-line-height);
53
+ color: var(--search-ai-question-text-color);
54
+ `;
55
+ const ResponseBody = styled_components_1.default.div `
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: var(--search-ai-response-body-gap);
59
+ padding: var(--search-ai-response-body-padding);
60
+ `;
61
+ const ResponseText = styled_components_1.default.pre `
62
+ color: var(--search-ai-response-text-color);
63
+ font-size: var(--search-ai-response-text-font-size);
64
+ line-height: var(--search-ai-response-text-line-height);
65
+ font-family: inherit;
66
+ white-space: break-spaces;
67
+ `;
68
+ const Resources = styled_components_1.default.div `
69
+ gap: var(--search-ai-resources-gap);
70
+ display: flex;
71
+ flex-direction: column;
72
+ `;
73
+ const ResourcesTitle = styled_components_1.default.div `
74
+ font-weight: var(--search-ai-resources-title-font-weight);
75
+ font-size: var(--search-ai-resources-title-font-size);
76
+ line-height: var(--search-ai-resources-title-line-height);
77
+ `;
78
+ const ResourceTags = styled_components_1.default.div `
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ gap: var(--search-ai-resource-tags-gap);
82
+ `;
83
+ const ResourceTag = (0, styled_components_1.default)(Tag_1.Tag) `
84
+ .tag-default {
85
+ --tag-color: --search-ai-resource-tag-text-color;
86
+ }
87
+ `;
88
+ //# sourceMappingURL=SearchAiResponse.js.map
@@ -31,6 +31,7 @@ const react_1 = __importStar(require("react"));
31
31
  const styled_components_1 = __importDefault(require("styled-components"));
32
32
  const SearchInput_1 = require("../../components/Search/SearchInput");
33
33
  const SearchShortcut_1 = require("../../components/Search/SearchShortcut");
34
+ const SearchAiResponse_1 = require("../../components/Search/SearchAiResponse");
34
35
  const Button_1 = require("../../components/Button/Button");
35
36
  const utils_1 = require("../../core/utils");
36
37
  const SearchItem_1 = require("../../components/Search/SearchItem");
@@ -43,13 +44,17 @@ const SearchFilter_1 = require("../../components/Search/SearchFilter");
43
44
  const SearchGroups_1 = require("../../components/Search/SearchGroups");
44
45
  const SpinnerLoader_1 = require("../../components/Loaders/SpinnerLoader");
45
46
  const SettingsIcon_1 = require("../../icons/SettingsIcon/SettingsIcon");
47
+ const AiStarsIcon_1 = require("../../icons/AiStarsIcon/AiStarsIcon");
46
48
  function SearchDialog({ onClose, className }) {
47
- const { useTranslate, useCurrentProduct, useSearch, useProducts } = (0, hooks_1.useThemeHooks)();
49
+ const { useTranslate, useCurrentProduct, useSearch, useProducts, useAiSearch } = (0, hooks_1.useThemeHooks)();
48
50
  const products = useProducts();
49
51
  const currentProduct = useCurrentProduct();
50
52
  const [product, setProduct] = (0, react_1.useState)(currentProduct);
51
- const { query, setQuery, filter, setFilter, items, isSearchLoading, facets, setLoadMore, advancedSearch, } = useSearch(product === null || product === void 0 ? void 0 : product.name);
53
+ const [mode, setMode] = (0, react_1.useState)('search');
54
+ const autoSearchDisabled = mode !== 'search';
55
+ const { query, setQuery, filter, setFilter, items, isSearchLoading, facets, setLoadMore, advancedSearch, askAi, } = useSearch(product === null || product === void 0 ? void 0 : product.name, autoSearchDisabled);
52
56
  const { isFilterOpen, onFilterToggle, onFilterChange, onFilterReset, onFacetReset, onTopFacetsReset, } = (0, hooks_1.useSearchFilter)(filter, setFilter);
57
+ const aiSearch = useAiSearch();
53
58
  const modalRef = (0, react_1.useRef)(null);
54
59
  const { translate } = useTranslate();
55
60
  (0, hooks_1.useDialogHotKeys)(modalRef, onClose);
@@ -88,6 +93,9 @@ function SearchDialog({ onClose, className }) {
88
93
  return needLoadMore;
89
94
  };
90
95
  const showResults = !!((filter && filter.length) || query);
96
+ const showSearchFilterButton = advancedSearch && mode === 'search';
97
+ const showAiSearchButton = askAi && mode === 'search';
98
+ const showHeaderButtons = showSearchFilterButton || showAiSearchButton;
91
99
  return (react_1.default.createElement(SearchOverlay, { "data-component-name": "Search/SearchDialog", ref: modalRef, onClick: handleOverlayClick, className: (0, utils_1.concatClassNames)('overlay', className) },
92
100
  react_1.default.createElement(SearchDialogWrapper, { className: "scroll-lock", role: "dialog" },
93
101
  react_1.default.createElement(SearchDialogHeader, null,
@@ -95,9 +103,22 @@ function SearchDialog({ onClose, className }) {
95
103
  react_1.default.createElement(SearchProductTag, { color: "product" },
96
104
  product.name,
97
105
  react_1.default.createElement(CloseIcon_1.CloseIcon, { onClick: () => setProduct(undefined), color: "--icon-color-additional" })))),
98
- react_1.default.createElement(SearchInput_1.SearchInput, { value: query, onChange: setQuery, placeholder: translate('search.label', 'Search docs...'), isLoading: isSearchLoading, "data-translation-key": "search.label" }),
99
- advancedSearch && (react_1.default.createElement(SearchFilterToggleButton, { icon: react_1.default.createElement(SettingsIcon_1.SettingsIcon, null), onClick: onFilterToggle }))),
100
- react_1.default.createElement(SearchDialogBody, null,
106
+ react_1.default.createElement(SearchInput_1.SearchInput, { value: query, onChange: setQuery, placeholder: mode === 'search'
107
+ ? translate('search.label', 'Search docs...')
108
+ : translate('search.ai.label', 'Ask a follow up question'), isLoading: isSearchLoading, showReturnButton: mode === 'ai-dialog', onReturn: () => setMode('search'), onSubmit: mode === 'ai-dialog'
109
+ ? () => {
110
+ setQuery('');
111
+ aiSearch.askQuestion(query);
112
+ }
113
+ : undefined, "data-translation-key": mode === 'search' ? 'search.label' : 'search.ai.label' }),
114
+ showHeaderButtons && (react_1.default.createElement(SearchHeaderButtons, null,
115
+ showAiSearchButton ? (react_1.default.createElement(SearchAiButton, { disabled: !query.trim(), icon: react_1.default.createElement(AiStarsIcon_1.AiStarsIcon, null), onClick: () => {
116
+ setMode('ai-dialog');
117
+ setQuery('');
118
+ aiSearch.askQuestion(query);
119
+ } }, translate('search.aiButton', 'Search with AI'))) : null,
120
+ showSearchFilterButton && (react_1.default.createElement(SearchFilterToggleButton, { icon: react_1.default.createElement(SettingsIcon_1.SettingsIcon, null), onClick: onFilterToggle }))))),
121
+ react_1.default.createElement(SearchDialogBody, null, mode === 'search' ? (react_1.default.createElement(react_1.default.Fragment, null,
101
122
  react_1.default.createElement(SearchDialogBodyMainView, null,
102
123
  react_1.default.createElement(SearchGroups_1.SearchGroups, { facets: facets, searchFilter: filter, onFilterChange: onFilterChange, onTopFacetsReset: onTopFacetsReset }),
103
124
  showResults ? (items && Object.keys(items).some((key) => { var _a; return (_a = items[key]) === null || _a === void 0 ? void 0 : _a.length; }) ? (Object.keys(items).map((key) => {
@@ -113,9 +134,9 @@ function SearchDialog({ onClose, className }) {
113
134
  translate('search.noResults.description', 'Prease, try with a different query.')))) : (react_1.default.createElement(react_1.default.Fragment, null,
114
135
  react_1.default.createElement(SearchRecent_1.SearchRecent, { onSelect: setQuery }),
115
136
  react_1.default.createElement(SearchSuggestedPages_1.SearchSuggestedPages, null)))),
116
- advancedSearch && isFilterOpen && (react_1.default.createElement(SearchDialogBodyFilterView, null,
117
- react_1.default.createElement(SearchFilter_1.SearchFilter, { facets: facets, filter: filter, query: query, onFilterChange: onFilterChange, onFilterReset: onFilterReset, onFacetReset: onFacetReset })))),
118
- react_1.default.createElement(SearchDialogFooter, null,
137
+ advancedSearch && mode === 'search' && isFilterOpen && (react_1.default.createElement(SearchDialogBodyFilterView, null,
138
+ react_1.default.createElement(SearchFilter_1.SearchFilter, { facets: facets, filter: filter, query: query, onFilterChange: onFilterChange, onFilterReset: onFilterReset, onFacetReset: onFacetReset }))))) : (react_1.default.createElement(SearchAiResponse_1.SearchAiResponse, { question: aiSearch.question, isGeneratingResponse: aiSearch.isGeneratingResponse, response: aiSearch.response, resources: aiSearch.resources }))),
139
+ mode === 'search' && (react_1.default.createElement(SearchDialogFooter, null,
119
140
  react_1.default.createElement(SearchShortcuts, null,
120
141
  react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.navigate", combination: "Tab", text: translate('search.keys.navigate', 'to navigate') }),
121
142
  react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.select", combination: "\u23CE", text: translate('search.keys.select', 'to select') }),
@@ -123,7 +144,7 @@ function SearchDialog({ onClose, className }) {
123
144
  isSearchLoading && (react_1.default.createElement(SearchLoading, null,
124
145
  react_1.default.createElement(SpinnerLoader_1.SpinnerLoader, { size: "16px", color: "var(--search-input-icon-color)" }),
125
146
  translate('search.loading', 'Loading...'))),
126
- react_1.default.createElement(SearchCancelButton, { "data-translation-key": "search.cancel", variant: "secondary", size: "small", onClick: onClose }, translate('search.cancel', 'Cancel'))))));
147
+ react_1.default.createElement(SearchCancelButton, { "data-translation-key": "search.cancel", variant: "secondary", size: "small", onClick: onClose }, translate('search.cancel', 'Cancel')))))));
127
148
  }
128
149
  const SearchOverlay = styled_components_1.default.div `
129
150
  position: fixed;
@@ -219,6 +240,9 @@ const SearchProductTag = (0, styled_components_1.default)(Tag_1.Tag) `
219
240
  const SearchFilterToggleButton = (0, styled_components_1.default)(Button_1.Button) `
220
241
  margin-left: 0;
221
242
  `;
243
+ const SearchAiButton = (0, styled_components_1.default)(Button_1.Button) `
244
+ margin-left: 0;
245
+ `;
222
246
  const SearchCancelButton = (0, styled_components_1.default)(Button_1.Button) `
223
247
  width: 100%;
224
248
 
@@ -247,4 +271,10 @@ const SearchLoading = styled_components_1.default.div `
247
271
  display: flex;
248
272
  }
249
273
  `;
274
+ const SearchHeaderButtons = styled_components_1.default.div `
275
+ display: flex;
276
+ gap: var(--search-header-buttons-gap);
277
+ padding-left: var(--search-header-buttons-padding-left);
278
+ border-left: var(--search-header-buttons-border-left);
279
+ `;
250
280
  //# sourceMappingURL=SearchDialog.js.map
@@ -5,6 +5,9 @@ export type SearchInputProps = {
5
5
  onChange: (value: string) => void;
6
6
  inputRef?: React.RefObject<HTMLInputElement>;
7
7
  isLoading: boolean;
8
+ showReturnButton?: boolean;
9
+ onReturn?: () => void;
10
+ onSubmit?: () => void;
8
11
  className?: string;
9
12
  };
10
- export declare function SearchInput({ placeholder, value, onChange, isLoading, className, }: SearchInputProps): JSX.Element;
13
+ export declare function SearchInput({ placeholder, value, onChange, isLoading, showReturnButton, onReturn, onSubmit, className, }: SearchInputProps): JSX.Element;
@@ -11,7 +11,8 @@ const Spinner_1 = require("../../icons/Spinner/Spinner");
11
11
  const Button_1 = require("../../components/Button/Button");
12
12
  const hooks_1 = require("../../core/hooks");
13
13
  const CloseFilledIcon_1 = require("../../icons/CloseFilledIcon/CloseFilledIcon");
14
- function SearchInput({ placeholder, value, onChange, isLoading, className, }) {
14
+ const ChevronLeftIcon_1 = require("../../icons/ChevronLeftIcon/ChevronLeftIcon");
15
+ function SearchInput({ placeholder, value, onChange, isLoading, showReturnButton, onReturn, onSubmit, className, }) {
15
16
  const { useTelemetry } = (0, hooks_1.useThemeHooks)();
16
17
  const telemetry = useTelemetry();
17
18
  const stopPropagation = (event) => event.stopPropagation();
@@ -22,9 +23,17 @@ function SearchInput({ placeholder, value, onChange, isLoading, className, }) {
22
23
  onChange('');
23
24
  telemetry.send('search_input_reset_button_clicked', {});
24
25
  };
26
+ const handleOnKeyUp = (e) => {
27
+ if (!onSubmit) {
28
+ return;
29
+ }
30
+ if (e.key === 'Enter') {
31
+ onSubmit();
32
+ }
33
+ };
25
34
  return (react_1.default.createElement(SearchInputWrapper, { "data-component-name": "Search/SearchInput", className: className },
26
- value && isLoading ? (react_1.default.createElement(Spinner_1.Spinner, { size: "18px", color: "--search-input-icon-color" })) : (react_1.default.createElement(SearchIcon_1.SearchIcon, { size: "18px", color: "--search-input-icon-color" })),
27
- react_1.default.createElement(SearchInputField, { value: value, placeholder: placeholder, onChange: handleOnChange, onClick: stopPropagation }),
35
+ showReturnButton ? (react_1.default.createElement(Button_1.Button, { icon: react_1.default.createElement(ChevronLeftIcon_1.ChevronLeftIcon, null), onClick: onReturn })) : value && isLoading ? (react_1.default.createElement(Spinner_1.Spinner, { size: "24px", color: "--search-input-icon-color" })) : (react_1.default.createElement(SearchIcon_1.SearchIcon, { size: "24px", color: "--search-input-icon-color" })),
36
+ react_1.default.createElement(SearchInputField, { value: value, placeholder: placeholder, onChange: handleOnChange, onClick: stopPropagation, onKeyUp: handleOnKeyUp }),
28
37
  !!value && (react_1.default.createElement(ResetButton, { variant: "ghost", onClick: handleOnReset, icon: react_1.default.createElement(CloseFilledIcon_1.CloseFilledIcon, null) }))));
29
38
  }
30
39
  const SearchInputWrapper = styled_components_1.default.div `
@@ -96,6 +96,10 @@ exports.search = (0, styled_components_1.css) `
96
96
  --search-message-text-color: var(--text-color-secondary); // @presenter Color
97
97
  --search-message-gap: var(--spacing-md);
98
98
 
99
+ --search-header-buttons-gap: var(--spacing-sm);
100
+ --search-header-buttons-padding-left: var(--spacing-sm);
101
+ --search-header-buttons-border-left: 1px solid var(--border-color-primary);
102
+
99
103
  /**
100
104
  * @tokens Search filter
101
105
  */
@@ -140,5 +144,39 @@ exports.search = (0, styled_components_1.css) `
140
144
  --search-trigger-line-height: var(--line-height-base);
141
145
 
142
146
  // @tokens End
147
+
148
+ /**
149
+ * @tokens Ai Search
150
+ */
151
+
152
+ --search-ai-spinner-icon-color: var(--icon-color-interactive);
153
+ --search-ai-checkmark-icon-color: var(--icon-color-interactive);
154
+ --search-ai-response-padding: var(--spacing-lg);
155
+ --search-ai-response-gap: var(--spacing-sm);
156
+
157
+ --search-ai-response-header-gap: var(--spacing-md);
158
+
159
+ --search-ai-question-font-size: var(--font-size-xl);
160
+ --search-ai-question-font-weight: var(--font-weight-semibold);
161
+ --search-ai-question-line-height: var(--line-height-xl);
162
+ --search-ai-question-text-color: var(--text-color-primary);
163
+
164
+ --search-ai-response-body-gap: var(--spacing-xl);
165
+ --search-ai-response-body-padding: 0 40px;
166
+
167
+ --search-ai-response-text-color: var(--text-color-secondary);
168
+ --search-ai-response-text-font-size: var(--font-size-lg);
169
+ --search-ai-response-text-line-height: var(--line-height-lg);
170
+
171
+ --search-ai-resources-gap: var(--spacing-base);
172
+ --search-ai-resources-title-font-weight: var(--font-weight-medium);
173
+ --search-ai-resources-title-font-size: var(--font-size-lg);
174
+ --search-ai-resources-title-line-height: var(--line-height-lg);
175
+
176
+ --search-ai-resource-tags-gap: var(--spacing-base);
177
+ --search-ai-resource-tag-text-color: var(--text-color-secondary);
178
+ --search-ai-resource-tag-icon-color: var(--text-color-secondary);
179
+
180
+ // @tokens End
143
181
  `;
144
182
  //# sourceMappingURL=variables.js.map
@@ -35,6 +35,10 @@ export declare const useThemeHooks: jest.Mock<{
35
35
  items: never[];
36
36
  isLoading: boolean;
37
37
  }, [], any>;
38
+ useAiSearch: jest.Mock<{
39
+ askQuestion: jest.Mock<any, any, any>;
40
+ references: never[];
41
+ }, [], any>;
38
42
  useFacetQuery: jest.Mock<{
39
43
  searchFacet: null;
40
44
  setSearchFacet: jest.Mock<any, any, any>;
@@ -43,6 +43,10 @@ exports.useThemeHooks = jest.fn(() => ({
43
43
  items: [],
44
44
  isLoading: false,
45
45
  })),
46
+ useAiSearch: jest.fn(() => ({
47
+ askQuestion: jest.fn(),
48
+ references: [],
49
+ })),
46
50
  useFacetQuery: jest.fn(() => ({
47
51
  searchFacet: null,
48
52
  setSearchFacet: jest.fn(),
@@ -45,7 +45,7 @@ export type ThemeHooks = {
45
45
  location: Location;
46
46
  };
47
47
  useBreadcrumbs: () => BreadcrumbItem[];
48
- useSearch: (product?: string) => {
48
+ useSearch: (product?: string, autoSearchDisabled?: boolean) => {
49
49
  query: string;
50
50
  setQuery: React.Dispatch<React.SetStateAction<string>>;
51
51
  filter: SearchFilterItem[];
@@ -58,6 +58,17 @@ export type ThemeHooks = {
58
58
  offset: number;
59
59
  } | undefined>>;
60
60
  advancedSearch?: boolean;
61
+ askAi?: boolean;
62
+ };
63
+ useAiSearch: () => {
64
+ askQuestion: (question: string) => void;
65
+ isGeneratingResponse: boolean;
66
+ question: string;
67
+ response?: string;
68
+ resources: {
69
+ title: string;
70
+ url: string;
71
+ }[];
61
72
  };
62
73
  useFacetQuery: (field: string) => {
63
74
  searchFacet: SearchFacet | null;
@@ -1,5 +1,5 @@
1
1
  import type { TOptions } from 'i18next';
2
- export type TranslationKey = 'dev.newApp' | 'dev.newApp.text' | 'dev.sidebar.header' | 'dev.sidebar.footer.text' | 'dev.create.app.dialog.appName.placeholder' | 'dev.create.app.dialog.appName.error' | 'dev.create.app.dialog.selectAPIs' | 'dev.create.app.dialog.description' | 'dev.create.app.dialog.description.placeholder' | 'dev.create.app.dialog.create' | 'dev.create.app.dialog.cancel' | 'dev.main.tab.appKeys' | 'dev.main.tab.logs' | 'dev.app.description.title' | 'dev.edit.description.dialog.title' | 'dev.edit.description.dialog.save' | 'dev.edit.description.dialog.cancel' | 'dev.edit.apis.dialog.selectedAPIs' | 'dev.app.key.create' | 'dev.create.key.dialog.title' | 'dev.create.key.dialog.create' | 'dev.create.key.dialog.cancel' | 'dev.app.edit' | 'dev.app.delete' | 'dev.edit.app.dialog.title' | 'dev.edit.app.dialog.save' | 'dev.edit.app.dialog.cancel' | 'dev.delete.app.dialog.title' | 'dev.delete.app.dialog.confirmation' | 'dev.delete.app.dialog.delete' | 'dev.delete.app.dialog.cancel' | 'dev.app.key.roll' | 'dev.roll.key.dialog.title' | 'dev.roll.key.dialog.apiKey' | 'dev.roll.key.dialog.expires' | 'dev.roll.key.dialog.confirmation' | 'dev.roll.key.dialog.cancel' | 'dev.roll.key.dialog.roll' | 'dev.update.key.dialog.title' | 'dev.update.key.dialog.update' | 'dev.update.key.dialog.cancel' | 'dev.app.key.api.name' | 'dev.app.key.api.status' | 'dev.app.key.api.edit' | 'dev.edit.apis.dialog.title' | 'dev.edit.apis.dialog.apiKey' | 'dev.edit.apis.dialog.save' | 'dev.edit.apis.dialog.cancel' | 'dev.select.placeholder' | 'dev.app.overview.status.pending' | 'dev.app.overview.status.approved' | 'dev.app.overview.status.revoked' | 'dev.app.overview.status' | 'dev.app.overview.non-production' | 'dev.app.overview.production' | 'dev.app.overview.clientId' | 'dev.app.overview.apiKey' | 'dev.app.key.revoke' | 'dev.revoke.key.dialog.title' | 'dev.revoke.key.dialog.apiKey' | 'dev.revoke.key.dialog.expires' | 'dev.revoke.key.dialog.confirmation' | 'dev.revoke.key.dialog.revoke' | 'dev.revoke.key.dialog.cancel' | 'dev.app.overview.expires' | 'dev.app.overview.created' | 'dev.app.overview.visibilityToggle.hide' | 'dev.app.overview.visibilityToggle.show' | 'search.loading' | 'search.noResults.title' | 'search.noResults.description' | 'search.keys.navigate' | 'search.keys.select' | 'search.keys.exit' | 'search.label' | 'search.cancel' | 'search.recent' | 'search.navbar.label' | 'search.suggested' | 'search.showMore' | 'search.filter.title' | 'search.filter.reset' | 'search.filter.field.reset' | 'toc.header' | 'footer.copyrightText' | 'page.homeButton' | 'page.forbidden.title' | 'page.notFound.title' | 'page.notFound.description' | 'page.lastUpdated.timeago' | 'page.lastUpdated.on' | 'catalog.filters.placeholder' | 'catalog.filters.title' | 'catalog.filters.clearAll' | 'catalog.filters.select.addFilter' | 'catalog.filters.select.all' | 'catalog.filters.done' | 'sidebar.menu.backLabel' | 'sidebar.actions.show' | 'sidebar.actions.hide' | 'sidebar.actions.changeLayout' | 'versionPicker.label' | 'versionPicker.unversioned' | 'codeSnippet.copy.buttonText' | 'codeSnippet.copy.tooltipText' | 'codeSnippet.copy.toasterText' | 'markdown.editPage.text' | 'feedback.settings.comment.submitText' | 'feedback.settings.comment.label' | 'feedback.settings.comment.send' | 'feedback.settings.comment.cancel' | 'feedback.settings.comment.satisfiedLabel' | 'feedback.settings.comment.neutralLabel' | 'feedback.settings.comment.dissatisfiedLabel' | 'feedback.settings.submitText' | 'feedback.settings.label' | 'feedback.settings.reasons.label' | 'feedback.settings.reasons.send' | 'feedback.settings.comment.likeLabel' | 'feedback.settings.comment.dislikeLabel' | 'feedback.sentiment.thumbUp' | 'feedback.sentiment.thumbDown' | 'feedback.settings.leftScaleLabel' | 'feedback.settings.rightScaleLabel' | 'codeSnippet.report.buttonText' | 'codeSnippet.report.tooltipText' | 'codeSnippet.report.label' | 'userMenu.login' | 'userMenu.logout' | 'userMenu.devOnboardingLabel' | 'mobileMenu.mainMenu' | 'mobileMenu.previous' | 'mobileMenu.products' | 'page.nextButton' | 'page.previousButton' | 'openapi.download.description.title' | 'openapi.info.title' | 'openapi.info.contact.url' | 'openapi.info.contact.name' | 'openapi.info.license' | 'openapi.info.termsOfService' | 'openapi.info.metadata.title' | 'openapi.key' | 'openapi.value' | 'openapi.enum' | 'openapi.items' | 'openapi.default' | 'openapi.variable' | 'openapi.variables' | 'openapi.actions.show' | 'openapi.actions.hide' | 'openapi.actions.more' | 'openapi.languages.title' | 'openapi.servers.title' | 'openapi.operations' | 'openapi.webhooks' | 'openapi.description' | 'openapi.badges.deprecated' | 'openapi.badges.required' | 'openapi.badges.webhook' | 'openapi.request' | 'openapi.path' | 'openapi.query' | 'openapi.cookie' | 'openapi.header' | 'openapi.body' | 'openapi.responses' | 'openapi.response' | 'openapi.callbacks' | 'openapi.callbackRequest' | 'openapi.callbackResponse' | 'openapi.payload' | 'openapi.discriminator' | 'openapi.contentType' | 'openapi.tryIt' | 'openapi.loading' | 'openapi.example' | 'openapi.examples' | 'openapi.additionalProperties' | 'openapi.patternProperties' | 'openapi.required' | 'openapi.recursive' | 'openapi.deprecated' | 'openapi.hideExample' | 'openapi.showExample' | 'openapi.expandAll' | 'openapi.collapseAll' | 'openapi.noResponseExample' | 'openapi.noRequestPayload' | 'openapi.hidePattern' | 'openapi.showPattern' | 'openapi.authorizationUrl' | 'openapi.tokenUrl' | 'openapi.refreshUrl' | 'openapi.scopes' | 'openapi.security' | 'openapi.httpAuthorizationScheme' | 'openapi.bearerFormat' | 'openapi.parameterName' | 'openapi.flowType' | 'openapi.connectUrl' | 'openapi.requiredScopes' | 'openapi.unsupportedLanguage' | 'openapi.failedToGenerateCodeSample' | 'graphql.queries' | 'graphql.mutations' | 'graphql.subscriptions' | 'graphql.directives' | 'graphql.objects' | 'graphql.interfaces' | 'graphql.unions' | 'graphql.enums' | 'graphql.inputs' | 'graphql.scalars' | 'graphql.arguments.label' | 'graphql.arguments.show' | 'graphql.arguments.hide' | 'graphql.arguments.here' | 'graphql.returnTypes.label' | 'graphql.returnTypes.show' | 'graphql.returnTypes.hide' | 'graphql.possibleTypes' | 'graphql.defaultValue' | 'graphql.deprecationReason' | 'graphql.implementedInterfaces' | 'graphql.nonNull' | 'graphql.required' | 'graphql.deprecated' | 'graphql.variables' | 'graphql.querySample' | 'graphql.mutationSample' | 'graphql.subscriptionSample' | 'graphql.responseSample' | 'graphql.locations' | 'graphql.sample' | 'graphql.referenced';
2
+ export type TranslationKey = 'dev.newApp' | 'dev.newApp.text' | 'dev.sidebar.header' | 'dev.sidebar.footer.text' | 'dev.create.app.dialog.appName.placeholder' | 'dev.create.app.dialog.appName.error' | 'dev.create.app.dialog.selectAPIs' | 'dev.create.app.dialog.description' | 'dev.create.app.dialog.description.placeholder' | 'dev.create.app.dialog.create' | 'dev.create.app.dialog.cancel' | 'dev.main.tab.appKeys' | 'dev.main.tab.logs' | 'dev.app.description.title' | 'dev.edit.description.dialog.title' | 'dev.edit.description.dialog.save' | 'dev.edit.description.dialog.cancel' | 'dev.edit.apis.dialog.selectedAPIs' | 'dev.app.key.create' | 'dev.create.key.dialog.title' | 'dev.create.key.dialog.create' | 'dev.create.key.dialog.cancel' | 'dev.app.edit' | 'dev.app.delete' | 'dev.edit.app.dialog.title' | 'dev.edit.app.dialog.save' | 'dev.edit.app.dialog.cancel' | 'dev.delete.app.dialog.title' | 'dev.delete.app.dialog.confirmation' | 'dev.delete.app.dialog.delete' | 'dev.delete.app.dialog.cancel' | 'dev.app.key.roll' | 'dev.roll.key.dialog.title' | 'dev.roll.key.dialog.apiKey' | 'dev.roll.key.dialog.expires' | 'dev.roll.key.dialog.confirmation' | 'dev.roll.key.dialog.cancel' | 'dev.roll.key.dialog.roll' | 'dev.update.key.dialog.title' | 'dev.update.key.dialog.update' | 'dev.update.key.dialog.cancel' | 'dev.app.key.api.name' | 'dev.app.key.api.status' | 'dev.app.key.api.edit' | 'dev.edit.apis.dialog.title' | 'dev.edit.apis.dialog.apiKey' | 'dev.edit.apis.dialog.save' | 'dev.edit.apis.dialog.cancel' | 'dev.select.placeholder' | 'dev.app.overview.status.pending' | 'dev.app.overview.status.approved' | 'dev.app.overview.status.revoked' | 'dev.app.overview.status' | 'dev.app.overview.non-production' | 'dev.app.overview.production' | 'dev.app.overview.clientId' | 'dev.app.overview.apiKey' | 'dev.app.key.revoke' | 'dev.revoke.key.dialog.title' | 'dev.revoke.key.dialog.apiKey' | 'dev.revoke.key.dialog.expires' | 'dev.revoke.key.dialog.confirmation' | 'dev.revoke.key.dialog.revoke' | 'dev.revoke.key.dialog.cancel' | 'dev.app.overview.expires' | 'dev.app.overview.created' | 'dev.app.overview.visibilityToggle.hide' | 'dev.app.overview.visibilityToggle.show' | 'search.loading' | 'search.noResults.title' | 'search.noResults.description' | 'search.keys.navigate' | 'search.keys.select' | 'search.keys.exit' | 'search.label' | 'search.cancel' | 'search.recent' | 'search.navbar.label' | 'search.suggested' | 'search.showMore' | 'search.filter.title' | 'search.filter.reset' | 'search.filter.field.reset' | 'search.ai.thinkingText' | 'search.ai.resourcesFound' | 'search.aiButton' | 'search.ai.label' | 'toc.header' | 'footer.copyrightText' | 'page.homeButton' | 'page.forbidden.title' | 'page.notFound.title' | 'page.notFound.description' | 'page.lastUpdated.timeago' | 'page.lastUpdated.on' | 'catalog.filters.placeholder' | 'catalog.filters.title' | 'catalog.filters.clearAll' | 'catalog.filters.select.addFilter' | 'catalog.filters.select.all' | 'catalog.filters.done' | 'sidebar.menu.backLabel' | 'sidebar.actions.show' | 'sidebar.actions.hide' | 'sidebar.actions.changeLayout' | 'versionPicker.label' | 'versionPicker.unversioned' | 'codeSnippet.copy.buttonText' | 'codeSnippet.copy.tooltipText' | 'codeSnippet.copy.toasterText' | 'markdown.editPage.text' | 'feedback.settings.comment.submitText' | 'feedback.settings.comment.label' | 'feedback.settings.comment.send' | 'feedback.settings.comment.cancel' | 'feedback.settings.comment.satisfiedLabel' | 'feedback.settings.comment.neutralLabel' | 'feedback.settings.comment.dissatisfiedLabel' | 'feedback.settings.submitText' | 'feedback.settings.label' | 'feedback.settings.reasons.label' | 'feedback.settings.reasons.send' | 'feedback.settings.comment.likeLabel' | 'feedback.settings.comment.dislikeLabel' | 'feedback.sentiment.thumbUp' | 'feedback.sentiment.thumbDown' | 'feedback.settings.leftScaleLabel' | 'feedback.settings.rightScaleLabel' | 'codeSnippet.report.buttonText' | 'codeSnippet.report.tooltipText' | 'codeSnippet.report.label' | 'userMenu.login' | 'userMenu.logout' | 'userMenu.devOnboardingLabel' | 'mobileMenu.mainMenu' | 'mobileMenu.previous' | 'mobileMenu.products' | 'page.nextButton' | 'page.previousButton' | 'openapi.download.description.title' | 'openapi.info.title' | 'openapi.info.contact.url' | 'openapi.info.contact.name' | 'openapi.info.license' | 'openapi.info.termsOfService' | 'openapi.info.metadata.title' | 'openapi.key' | 'openapi.value' | 'openapi.enum' | 'openapi.items' | 'openapi.default' | 'openapi.variable' | 'openapi.variables' | 'openapi.actions.show' | 'openapi.actions.hide' | 'openapi.actions.more' | 'openapi.languages.title' | 'openapi.servers.title' | 'openapi.operations' | 'openapi.webhooks' | 'openapi.description' | 'openapi.badges.deprecated' | 'openapi.badges.required' | 'openapi.badges.webhook' | 'openapi.request' | 'openapi.path' | 'openapi.query' | 'openapi.cookie' | 'openapi.header' | 'openapi.body' | 'openapi.responses' | 'openapi.response' | 'openapi.callbacks' | 'openapi.callbackRequest' | 'openapi.callbackResponse' | 'openapi.payload' | 'openapi.discriminator' | 'openapi.contentType' | 'openapi.tryIt' | 'openapi.loading' | 'openapi.example' | 'openapi.examples' | 'openapi.additionalProperties' | 'openapi.patternProperties' | 'openapi.required' | 'openapi.recursive' | 'openapi.deprecated' | 'openapi.hideExample' | 'openapi.showExample' | 'openapi.expandAll' | 'openapi.collapseAll' | 'openapi.noResponseExample' | 'openapi.noRequestPayload' | 'openapi.hidePattern' | 'openapi.showPattern' | 'openapi.authorizationUrl' | 'openapi.tokenUrl' | 'openapi.refreshUrl' | 'openapi.scopes' | 'openapi.security' | 'openapi.httpAuthorizationScheme' | 'openapi.bearerFormat' | 'openapi.parameterName' | 'openapi.flowType' | 'openapi.connectUrl' | 'openapi.requiredScopes' | 'openapi.unsupportedLanguage' | 'openapi.failedToGenerateCodeSample' | 'graphql.queries' | 'graphql.mutations' | 'graphql.subscriptions' | 'graphql.directives' | 'graphql.objects' | 'graphql.interfaces' | 'graphql.unions' | 'graphql.enums' | 'graphql.inputs' | 'graphql.scalars' | 'graphql.arguments.label' | 'graphql.arguments.show' | 'graphql.arguments.hide' | 'graphql.arguments.here' | 'graphql.returnTypes.label' | 'graphql.returnTypes.show' | 'graphql.returnTypes.hide' | 'graphql.possibleTypes' | 'graphql.defaultValue' | 'graphql.deprecationReason' | 'graphql.implementedInterfaces' | 'graphql.nonNull' | 'graphql.required' | 'graphql.deprecated' | 'graphql.variables' | 'graphql.querySample' | 'graphql.mutationSample' | 'graphql.subscriptionSample' | 'graphql.responseSample' | 'graphql.locations' | 'graphql.sample' | 'graphql.referenced';
3
3
  export type Locale = {
4
4
  code: string;
5
5
  name: string;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { IconProps } from '../../icons/types';
3
+ export declare const AiStarsIcon: import("styled-components").StyledComponent<(props: IconProps) => React.JSX.Element, any, {
4
+ 'data-component-name': string;
5
+ } & {
6
+ color?: string;
7
+ size?: string;
8
+ className?: string;
9
+ } & React.SVGProps<SVGSVGElement>, "data-component-name">;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AiStarsIcon = void 0;
7
+ const react_1 = __importDefault(require("react"));
8
+ const styled_components_1 = __importDefault(require("styled-components"));
9
+ const utils_1 = require("../../core/utils");
10
+ const Icon = (props) => (react_1.default.createElement("svg", Object.assign({ viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, props),
11
+ react_1.default.createElement("path", { d: "M11.2597 9.12114C8.08498 8.40423 7.59322 7.91247 6.87631 4.73772C6.84346 4.59262 6.7143 4.48929 6.56505 4.48929C6.4158 4.48929 6.28664 4.59262 6.2538 4.73772C5.53657 7.91247 5.04513 8.40423 1.87038 9.12114C1.72495 9.1543 1.62163 9.28314 1.62163 9.43239C1.62163 9.58164 1.72495 9.71048 1.87038 9.74365C5.04513 10.4609 5.53657 10.9526 6.2538 14.1271C6.28664 14.2722 6.4158 14.3755 6.56505 14.3755C6.7143 14.3755 6.84346 14.2722 6.87631 14.1271C7.59354 10.9526 8.08498 10.4609 11.2597 9.74365C11.4052 9.71048 11.5082 9.58164 11.5082 9.43239C11.5082 9.28314 11.4048 9.1543 11.2597 9.12114Z", fill: "#1A1C21" }),
12
+ react_1.default.createElement("path", { d: "M14.1299 4.17834C12.4423 3.79725 12.2053 3.5603 11.8242 1.87294C11.7911 1.72752 11.6622 1.62451 11.513 1.62451C11.3637 1.62451 11.2349 1.72752 11.2017 1.87294C10.8206 3.5603 10.5837 3.79725 8.8963 4.17834C8.75088 4.21151 8.64787 4.34035 8.64787 4.4896C8.64787 4.63885 8.75088 4.76769 8.8963 4.80086C10.5837 5.18195 10.8206 5.4189 11.2017 7.10658C11.2349 7.25168 11.3637 7.35501 11.513 7.35501C11.6622 7.35501 11.7911 7.25168 11.8242 7.10658C12.2053 5.4189 12.4423 5.18195 14.1299 4.80086C14.275 4.76769 14.3784 4.63885 14.3784 4.4896C14.3784 4.34035 14.275 4.21151 14.1299 4.17834Z", fill: "#1A1C21" })));
13
+ exports.AiStarsIcon = (0, styled_components_1.default)(Icon).attrs(() => ({
14
+ 'data-component-name': 'icons/AiStarsIcon/AiStarsIcon',
15
+ })) `
16
+ path {
17
+ fill: ${({ color }) => (0, utils_1.getCssColorVariable)(color)};
18
+ }
19
+
20
+ height: ${({ size }) => size || '16px'};
21
+ width: ${({ size }) => size || '16px'};
22
+ `;
23
+ //# sourceMappingURL=AiStarsIcon.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.44.7",
3
+ "version": "0.44.8",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -81,7 +81,7 @@
81
81
  "timeago.js": "4.0.2",
82
82
  "i18next": "22.4.15",
83
83
  "nprogress": "0.2.0",
84
- "@redocly/config": "0.14.0"
84
+ "@redocly/config": "0.15.0"
85
85
  },
86
86
  "scripts": {
87
87
  "watch": "tsc -p tsconfig.build.json && (concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\")",
@@ -175,7 +175,7 @@ const ButtonComponent: React.FC<ButtonProps> = (props) => {
175
175
  data-component-name="Button/Button"
176
176
  {...props}
177
177
  iconOnly={!props.children && props.icon !== null}
178
- tabIndex={props.to ? -1 : 0}
178
+ tabIndex={props.to ? -1 : undefined}
179
179
  >
180
180
  {props.icon && props.iconPosition !== 'right' && props.icon}
181
181
  {props.children}
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { Spinner } from '@redocly/theme/icons/Spinner/Spinner';
5
+ import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/CheckmarkFilledIcon';
6
+ import { DocumentIcon } from '@redocly/theme/icons/DocumentIcon/DocumentIcon';
7
+ import { Tag } from '@redocly/theme/components/Tag/Tag';
8
+ import { Link } from '@redocly/theme/components/Link/Link';
9
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
10
+
11
+ export type SearchAiResponseProps = {
12
+ question: string;
13
+ isGeneratingResponse: boolean;
14
+ response?: string;
15
+ resources: {
16
+ url: string;
17
+ title: string;
18
+ }[];
19
+ };
20
+
21
+ export function SearchAiResponse(props: SearchAiResponseProps): JSX.Element {
22
+ const { question, response, isGeneratingResponse, resources } = props;
23
+
24
+ const { useTranslate } = useThemeHooks();
25
+ const { translate } = useTranslate();
26
+
27
+ return (
28
+ <ResponseWrapper data-component-name="Search/AiResponse">
29
+ <ResponseHeader>
30
+ {isGeneratingResponse ? (
31
+ <Spinner size="20px" color="--search-ai-spinner-icon-color" />
32
+ ) : (
33
+ <CheckmarkFilledIcon size="20px" color="--search-ai-checkmark-icon-color" />
34
+ )}
35
+ <Question>{question}</Question>
36
+ </ResponseHeader>
37
+ <ResponseBody>
38
+ <ResponseText data-translation-key={response ? undefined : 'search.ai.thinkingText'}>
39
+ {response ? response : translate('search.ai.thinkingText', 'Thinking...')}
40
+ </ResponseText>
41
+ {resources.length && !isGeneratingResponse ? (
42
+ <Resources>
43
+ <ResourcesTitle data-translation-key="search.ai.resourcesFound">
44
+ {resources.length} {translate('search.ai.resourcesFound', 'resources found')}
45
+ </ResourcesTitle>
46
+ <ResourceTags>
47
+ {resources.map((resource, idx) => (
48
+ <Link key={idx} to={resource.url}>
49
+ <ResourceTag
50
+ borderless
51
+ icon={<DocumentIcon color="--search-ai-resource-tag-icon-color" />}
52
+ >
53
+ {resource.title}
54
+ </ResourceTag>
55
+ </Link>
56
+ ))}
57
+ </ResourceTags>
58
+ </Resources>
59
+ ) : null}
60
+ </ResponseBody>
61
+ </ResponseWrapper>
62
+ );
63
+ }
64
+
65
+ const ResponseWrapper = styled.div`
66
+ display: flex;
67
+ flex-direction: column;
68
+ flex: 2;
69
+ flex-grow: 2;
70
+ overflow-y: scroll;
71
+ overscroll-behavior: contain;
72
+ padding: var(--search-ai-response-padding);
73
+ gap: var(--search-ai-response-gap);
74
+ `;
75
+
76
+ const ResponseHeader = styled.div`
77
+ display: flex;
78
+ flex-direction: row;
79
+ gap: var(--search-ai-response-header-gap);
80
+ align-items: center;
81
+ `;
82
+
83
+ const Question = styled.div`
84
+ font-size: var(--search-ai-question-font-size);
85
+ font-weight: var(--search-ai-question-font-weight);
86
+ line-height: var(--search-ai-question-line-height);
87
+ color: var(--search-ai-question-text-color);
88
+ `;
89
+
90
+ const ResponseBody = styled.div`
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: var(--search-ai-response-body-gap);
94
+ padding: var(--search-ai-response-body-padding);
95
+ `;
96
+
97
+ const ResponseText = styled.pre`
98
+ color: var(--search-ai-response-text-color);
99
+ font-size: var(--search-ai-response-text-font-size);
100
+ line-height: var(--search-ai-response-text-line-height);
101
+ font-family: inherit;
102
+ white-space: break-spaces;
103
+ `;
104
+
105
+ const Resources = styled.div`
106
+ gap: var(--search-ai-resources-gap);
107
+ display: flex;
108
+ flex-direction: column;
109
+ `;
110
+
111
+ const ResourcesTitle = styled.div`
112
+ font-weight: var(--search-ai-resources-title-font-weight);
113
+ font-size: var(--search-ai-resources-title-font-size);
114
+ line-height: var(--search-ai-resources-title-line-height);
115
+ `;
116
+
117
+ const ResourceTags = styled.div`
118
+ display: flex;
119
+ flex-wrap: wrap;
120
+ gap: var(--search-ai-resource-tags-gap);
121
+ `;
122
+
123
+ const ResourceTag = styled(Tag)`
124
+ .tag-default {
125
+ --tag-color: --search-ai-resource-tag-text-color;
126
+ }
127
+ `;
@@ -6,6 +6,7 @@ import type { SearchFacetCount, SearchItemData } from '@redocly/theme/core/types
6
6
 
7
7
  import { SearchInput } from '@redocly/theme/components/Search/SearchInput';
8
8
  import { SearchShortcut } from '@redocly/theme/components/Search/SearchShortcut';
9
+ import { SearchAiResponse } from '@redocly/theme/components/Search/SearchAiResponse';
9
10
  import { Button } from '@redocly/theme/components/Button/Button';
10
11
  import { breakpoints, concatClassNames } from '@redocly/theme/core/utils';
11
12
  import { SearchItem } from '@redocly/theme/components/Search/SearchItem';
@@ -18,6 +19,7 @@ import { SearchFilter } from '@redocly/theme/components/Search/SearchFilter';
18
19
  import { SearchGroups } from '@redocly/theme/components/Search/SearchGroups';
19
20
  import { SpinnerLoader } from '@redocly/theme/components/Loaders/SpinnerLoader';
20
21
  import { SettingsIcon } from '@redocly/theme/icons/SettingsIcon/SettingsIcon';
22
+ import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
21
23
 
22
24
  export type SearchDialogProps = {
23
25
  onClose: () => void;
@@ -25,10 +27,12 @@ export type SearchDialogProps = {
25
27
  };
26
28
 
27
29
  export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Element {
28
- const { useTranslate, useCurrentProduct, useSearch, useProducts } = useThemeHooks();
30
+ const { useTranslate, useCurrentProduct, useSearch, useProducts, useAiSearch } = useThemeHooks();
29
31
  const products = useProducts();
30
32
  const currentProduct = useCurrentProduct();
31
33
  const [product, setProduct] = useState(currentProduct);
34
+ const [mode, setMode] = useState<'search' | 'ai-dialog'>('search');
35
+ const autoSearchDisabled = mode !== 'search';
32
36
  const {
33
37
  query,
34
38
  setQuery,
@@ -39,7 +43,8 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
39
43
  facets,
40
44
  setLoadMore,
41
45
  advancedSearch,
42
- } = useSearch(product?.name);
46
+ askAi,
47
+ } = useSearch(product?.name, autoSearchDisabled);
43
48
  const {
44
49
  isFilterOpen,
45
50
  onFilterToggle,
@@ -48,6 +53,8 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
48
53
  onFacetReset,
49
54
  onTopFacetsReset,
50
55
  } = useSearchFilter(filter, setFilter);
56
+ const aiSearch = useAiSearch();
57
+
51
58
  const modalRef = useRef<HTMLDivElement>(null);
52
59
  const { translate } = useTranslate();
53
60
 
@@ -89,6 +96,9 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
89
96
  };
90
97
 
91
98
  const showResults = !!((filter && filter.length) || query);
99
+ const showSearchFilterButton = advancedSearch && mode === 'search';
100
+ const showAiSearchButton = askAi && mode === 'search';
101
+ const showHeaderButtons = showSearchFilterButton || showAiSearchButton;
92
102
 
93
103
  return (
94
104
  <SearchOverlay
@@ -110,106 +120,154 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
110
120
  <SearchInput
111
121
  value={query}
112
122
  onChange={setQuery}
113
- placeholder={translate('search.label', 'Search docs...')}
123
+ placeholder={
124
+ mode === 'search'
125
+ ? translate('search.label', 'Search docs...')
126
+ : translate('search.ai.label', 'Ask a follow up question')
127
+ }
114
128
  isLoading={isSearchLoading}
115
- data-translation-key="search.label"
129
+ showReturnButton={mode === 'ai-dialog'}
130
+ onReturn={() => setMode('search')}
131
+ onSubmit={
132
+ mode === 'ai-dialog'
133
+ ? () => {
134
+ setQuery('');
135
+ aiSearch.askQuestion(query);
136
+ }
137
+ : undefined
138
+ }
139
+ data-translation-key={mode === 'search' ? 'search.label' : 'search.ai.label'}
116
140
  />
117
- {advancedSearch && (
118
- <SearchFilterToggleButton icon={<SettingsIcon />} onClick={onFilterToggle} />
141
+ {showHeaderButtons && (
142
+ <SearchHeaderButtons>
143
+ {showAiSearchButton ? (
144
+ <SearchAiButton
145
+ disabled={!query.trim()}
146
+ icon={<AiStarsIcon />}
147
+ onClick={() => {
148
+ setMode('ai-dialog');
149
+ setQuery('');
150
+ aiSearch.askQuestion(query);
151
+ }}
152
+ >
153
+ {translate('search.aiButton', 'Search with AI')}
154
+ </SearchAiButton>
155
+ ) : null}
156
+ {showSearchFilterButton && (
157
+ <SearchFilterToggleButton icon={<SettingsIcon />} onClick={onFilterToggle} />
158
+ )}
159
+ </SearchHeaderButtons>
119
160
  )}
120
161
  </SearchDialogHeader>
162
+
121
163
  <SearchDialogBody>
122
- <SearchDialogBodyMainView>
123
- <SearchGroups
124
- facets={facets}
125
- searchFilter={filter}
126
- onFilterChange={onFilterChange}
127
- onTopFacetsReset={onTopFacetsReset}
128
- />
129
- {showResults ? (
130
- items && Object.keys(items).some((key) => items[key]?.length) ? (
131
- Object.keys(items).map((key) =>
132
- items[key]?.length ? (
133
- <Fragment key={key}>
134
- <SearchGroupTitle>{key}</SearchGroupTitle>
135
- {items[key]?.map(mapItem)}
136
- {showLoadMore(key, items[key]?.length || 0) && (
137
- <SearchGroupFooter
138
- data-translation-key="search.showMore"
139
- onClick={() =>
140
- setLoadMore({ groupKey: key, offset: items[key]?.length || 0 })
141
- }
142
- >
143
- {translate('search.showMore', 'Show more')}
144
- </SearchGroupFooter>
164
+ {mode === 'search' ? (
165
+ <>
166
+ <SearchDialogBodyMainView>
167
+ <SearchGroups
168
+ facets={facets}
169
+ searchFilter={filter}
170
+ onFilterChange={onFilterChange}
171
+ onTopFacetsReset={onTopFacetsReset}
172
+ />
173
+ {showResults ? (
174
+ items && Object.keys(items).some((key) => items[key]?.length) ? (
175
+ Object.keys(items).map((key) =>
176
+ items[key]?.length ? (
177
+ <Fragment key={key}>
178
+ <SearchGroupTitle>{key}</SearchGroupTitle>
179
+ {items[key]?.map(mapItem)}
180
+ {showLoadMore(key, items[key]?.length || 0) && (
181
+ <SearchGroupFooter
182
+ data-translation-key="search.showMore"
183
+ onClick={() =>
184
+ setLoadMore({ groupKey: key, offset: items[key]?.length || 0 })
185
+ }
186
+ >
187
+ {translate('search.showMore', 'Show more')}
188
+ </SearchGroupFooter>
189
+ )}
190
+ </Fragment>
191
+ ) : null,
192
+ )
193
+ ) : isSearchLoading ? (
194
+ <SearchMessage>
195
+ <SpinnerLoader size="26px" color="var(--search-input-icon-color)" />
196
+ {translate('search.loading', 'Loading...')}
197
+ </SearchMessage>
198
+ ) : (
199
+ <SearchMessage data-translation-key="search.noResults">
200
+ <b>{translate('search.noResults.title', 'No results')}</b>
201
+ {translate(
202
+ 'search.noResults.description',
203
+ 'Prease, try with a different query.',
145
204
  )}
146
- </Fragment>
147
- ) : null,
148
- )
149
- ) : isSearchLoading ? (
150
- <SearchMessage>
151
- <SpinnerLoader size="26px" color="var(--search-input-icon-color)" />
152
- {translate('search.loading', 'Loading...')}
153
- </SearchMessage>
154
- ) : (
155
- <SearchMessage data-translation-key="search.noResults">
156
- <b>{translate('search.noResults.title', 'No results')}</b>
157
- {translate('search.noResults.description', 'Prease, try with a different query.')}
158
- </SearchMessage>
159
- )
160
- ) : (
161
- <>
162
- <SearchRecent onSelect={setQuery} />
163
- <SearchSuggestedPages />
164
- </>
165
- )}
166
- </SearchDialogBodyMainView>
167
- {advancedSearch && isFilterOpen && (
168
- <SearchDialogBodyFilterView>
169
- <SearchFilter
170
- facets={facets}
171
- filter={filter}
172
- query={query}
173
- onFilterChange={onFilterChange}
174
- onFilterReset={onFilterReset}
175
- onFacetReset={onFacetReset}
176
- />
177
- </SearchDialogBodyFilterView>
178
- )}
179
- </SearchDialogBody>
180
- <SearchDialogFooter>
181
- <SearchShortcuts>
182
- <SearchShortcut
183
- data-translation-key="search.keys.navigate"
184
- combination="Tab"
185
- text={translate('search.keys.navigate', 'to navigate')}
186
- />
187
- <SearchShortcut
188
- data-translation-key="search.keys.select"
189
- combination="⏎"
190
- text={translate('search.keys.select', 'to select')}
191
- />
192
- <SearchShortcut
193
- data-translation-key="search.keys.exit"
194
- combination="Esc"
195
- text={translate('search.keys.exit', 'to exit')}
205
+ </SearchMessage>
206
+ )
207
+ ) : (
208
+ <>
209
+ <SearchRecent onSelect={setQuery} />
210
+ <SearchSuggestedPages />
211
+ </>
212
+ )}
213
+ </SearchDialogBodyMainView>
214
+ {advancedSearch && mode === 'search' && isFilterOpen && (
215
+ <SearchDialogBodyFilterView>
216
+ <SearchFilter
217
+ facets={facets}
218
+ filter={filter}
219
+ query={query}
220
+ onFilterChange={onFilterChange}
221
+ onFilterReset={onFilterReset}
222
+ onFacetReset={onFacetReset}
223
+ />
224
+ </SearchDialogBodyFilterView>
225
+ )}
226
+ </>
227
+ ) : (
228
+ <SearchAiResponse
229
+ question={aiSearch.question}
230
+ isGeneratingResponse={aiSearch.isGeneratingResponse}
231
+ response={aiSearch.response}
232
+ resources={aiSearch.resources}
196
233
  />
197
- </SearchShortcuts>
198
- {isSearchLoading && (
199
- <SearchLoading>
200
- <SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
201
- {translate('search.loading', 'Loading...')}
202
- </SearchLoading>
203
234
  )}
204
- <SearchCancelButton
205
- data-translation-key="search.cancel"
206
- variant="secondary"
207
- size="small"
208
- onClick={onClose}
209
- >
210
- {translate('search.cancel', 'Cancel')}
211
- </SearchCancelButton>
212
- </SearchDialogFooter>
235
+ </SearchDialogBody>
236
+ {mode === 'search' && (
237
+ <SearchDialogFooter>
238
+ <SearchShortcuts>
239
+ <SearchShortcut
240
+ data-translation-key="search.keys.navigate"
241
+ combination="Tab"
242
+ text={translate('search.keys.navigate', 'to navigate')}
243
+ />
244
+ <SearchShortcut
245
+ data-translation-key="search.keys.select"
246
+ combination="⏎"
247
+ text={translate('search.keys.select', 'to select')}
248
+ />
249
+ <SearchShortcut
250
+ data-translation-key="search.keys.exit"
251
+ combination="Esc"
252
+ text={translate('search.keys.exit', 'to exit')}
253
+ />
254
+ </SearchShortcuts>
255
+ {isSearchLoading && (
256
+ <SearchLoading>
257
+ <SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
258
+ {translate('search.loading', 'Loading...')}
259
+ </SearchLoading>
260
+ )}
261
+ <SearchCancelButton
262
+ data-translation-key="search.cancel"
263
+ variant="secondary"
264
+ size="small"
265
+ onClick={onClose}
266
+ >
267
+ {translate('search.cancel', 'Cancel')}
268
+ </SearchCancelButton>
269
+ </SearchDialogFooter>
270
+ )}
213
271
  </SearchDialogWrapper>
214
272
  </SearchOverlay>
215
273
  );
@@ -320,6 +378,10 @@ const SearchFilterToggleButton = styled(Button)`
320
378
  margin-left: 0;
321
379
  `;
322
380
 
381
+ const SearchAiButton = styled(Button)`
382
+ margin-left: 0;
383
+ `;
384
+
323
385
  const SearchCancelButton = styled(Button)`
324
386
  width: 100%;
325
387
 
@@ -351,3 +413,10 @@ const SearchLoading = styled.div`
351
413
  display: flex;
352
414
  }
353
415
  `;
416
+
417
+ const SearchHeaderButtons = styled.div`
418
+ display: flex;
419
+ gap: var(--search-header-buttons-gap);
420
+ padding-left: var(--search-header-buttons-padding-left);
421
+ border-left: var(--search-header-buttons-border-left);
422
+ `;
@@ -8,6 +8,7 @@ import { Spinner } from '@redocly/theme/icons/Spinner/Spinner';
8
8
  import { Button } from '@redocly/theme/components/Button/Button';
9
9
  import { useThemeHooks } from '@redocly/theme/core/hooks';
10
10
  import { CloseFilledIcon } from '@redocly/theme/icons/CloseFilledIcon/CloseFilledIcon';
11
+ import { ChevronLeftIcon } from '@redocly/theme/icons/ChevronLeftIcon/ChevronLeftIcon';
11
12
 
12
13
  export type SearchInputProps = {
13
14
  placeholder?: string;
@@ -15,6 +16,9 @@ export type SearchInputProps = {
15
16
  onChange: (value: string) => void;
16
17
  inputRef?: React.RefObject<HTMLInputElement>;
17
18
  isLoading: boolean;
19
+ showReturnButton?: boolean;
20
+ onReturn?: () => void;
21
+ onSubmit?: () => void;
18
22
  className?: string;
19
23
  };
20
24
 
@@ -23,6 +27,9 @@ export function SearchInput({
23
27
  value,
24
28
  onChange,
25
29
  isLoading,
30
+ showReturnButton,
31
+ onReturn,
32
+ onSubmit,
26
33
  className,
27
34
  }: SearchInputProps): JSX.Element {
28
35
  const { useTelemetry } = useThemeHooks();
@@ -39,18 +46,31 @@ export function SearchInput({
39
46
  telemetry.send('search_input_reset_button_clicked', {});
40
47
  };
41
48
 
49
+ const handleOnKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
50
+ if (!onSubmit) {
51
+ return;
52
+ }
53
+
54
+ if (e.key === 'Enter') {
55
+ onSubmit();
56
+ }
57
+ };
58
+
42
59
  return (
43
60
  <SearchInputWrapper data-component-name="Search/SearchInput" className={className}>
44
- {value && isLoading ? (
45
- <Spinner size="18px" color="--search-input-icon-color" />
61
+ {showReturnButton ? (
62
+ <Button icon={<ChevronLeftIcon />} onClick={onReturn} />
63
+ ) : value && isLoading ? (
64
+ <Spinner size="24px" color="--search-input-icon-color" />
46
65
  ) : (
47
- <SearchIcon size="18px" color="--search-input-icon-color" />
66
+ <SearchIcon size="24px" color="--search-input-icon-color" />
48
67
  )}
49
68
  <SearchInputField
50
69
  value={value}
51
70
  placeholder={placeholder}
52
71
  onChange={handleOnChange}
53
72
  onClick={stopPropagation}
73
+ onKeyUp={handleOnKeyUp}
54
74
  />
55
75
  {!!value && (
56
76
  <ResetButton variant="ghost" onClick={handleOnReset} icon={<CloseFilledIcon />} />
@@ -94,6 +94,10 @@ export const search = css`
94
94
  --search-message-text-color: var(--text-color-secondary); // @presenter Color
95
95
  --search-message-gap: var(--spacing-md);
96
96
 
97
+ --search-header-buttons-gap: var(--spacing-sm);
98
+ --search-header-buttons-padding-left: var(--spacing-sm);
99
+ --search-header-buttons-border-left: 1px solid var(--border-color-primary);
100
+
97
101
  /**
98
102
  * @tokens Search filter
99
103
  */
@@ -138,4 +142,38 @@ export const search = css`
138
142
  --search-trigger-line-height: var(--line-height-base);
139
143
 
140
144
  // @tokens End
145
+
146
+ /**
147
+ * @tokens Ai Search
148
+ */
149
+
150
+ --search-ai-spinner-icon-color: var(--icon-color-interactive);
151
+ --search-ai-checkmark-icon-color: var(--icon-color-interactive);
152
+ --search-ai-response-padding: var(--spacing-lg);
153
+ --search-ai-response-gap: var(--spacing-sm);
154
+
155
+ --search-ai-response-header-gap: var(--spacing-md);
156
+
157
+ --search-ai-question-font-size: var(--font-size-xl);
158
+ --search-ai-question-font-weight: var(--font-weight-semibold);
159
+ --search-ai-question-line-height: var(--line-height-xl);
160
+ --search-ai-question-text-color: var(--text-color-primary);
161
+
162
+ --search-ai-response-body-gap: var(--spacing-xl);
163
+ --search-ai-response-body-padding: 0 40px;
164
+
165
+ --search-ai-response-text-color: var(--text-color-secondary);
166
+ --search-ai-response-text-font-size: var(--font-size-lg);
167
+ --search-ai-response-text-line-height: var(--line-height-lg);
168
+
169
+ --search-ai-resources-gap: var(--spacing-base);
170
+ --search-ai-resources-title-font-weight: var(--font-weight-medium);
171
+ --search-ai-resources-title-font-size: var(--font-size-lg);
172
+ --search-ai-resources-title-line-height: var(--line-height-lg);
173
+
174
+ --search-ai-resource-tags-gap: var(--spacing-base);
175
+ --search-ai-resource-tag-text-color: var(--text-color-secondary);
176
+ --search-ai-resource-tag-icon-color: var(--text-color-secondary);
177
+
178
+ // @tokens End
141
179
  `;
@@ -42,6 +42,10 @@ export const useThemeHooks = jest.fn(() => ({
42
42
  items: [],
43
43
  isLoading: false,
44
44
  })),
45
+ useAiSearch: jest.fn(() => ({
46
+ askQuestion: jest.fn(),
47
+ references: [],
48
+ })),
45
49
  useFacetQuery: jest.fn(() => ({
46
50
  searchFacet: null,
47
51
  setSearchFacet: jest.fn(),
@@ -54,7 +54,10 @@ export type ThemeHooks = {
54
54
  location: Location;
55
55
  };
56
56
  useBreadcrumbs: () => BreadcrumbItem[];
57
- useSearch: (product?: string) => {
57
+ useSearch: (
58
+ product?: string,
59
+ autoSearchDisabled?: boolean,
60
+ ) => {
58
61
  query: string;
59
62
  setQuery: React.Dispatch<React.SetStateAction<string>>;
60
63
  filter: SearchFilterItem[];
@@ -72,6 +75,17 @@ export type ThemeHooks = {
72
75
  >
73
76
  >;
74
77
  advancedSearch?: boolean;
78
+ askAi?: boolean;
79
+ };
80
+ useAiSearch: () => {
81
+ askQuestion: (question: string) => void;
82
+ isGeneratingResponse: boolean;
83
+ question: string;
84
+ response?: string;
85
+ resources: {
86
+ title: string;
87
+ url: string;
88
+ }[];
75
89
  };
76
90
  useFacetQuery: (field: string) => {
77
91
  searchFacet: SearchFacet | null;
@@ -84,6 +84,10 @@ export type TranslationKey =
84
84
  | 'search.filter.title'
85
85
  | 'search.filter.reset'
86
86
  | 'search.filter.field.reset'
87
+ | 'search.ai.thinkingText'
88
+ | 'search.ai.resourcesFound'
89
+ | 'search.aiButton'
90
+ | 'search.ai.label'
87
91
  | 'toc.header'
88
92
  | 'footer.copyrightText'
89
93
  | 'page.homeButton'
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { IconProps } from '@redocly/theme/icons/types';
5
+
6
+ import { getCssColorVariable } from '@redocly/theme/core/utils';
7
+
8
+ const Icon = (props: IconProps) => (
9
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
10
+ <path
11
+ d="M11.2597 9.12114C8.08498 8.40423 7.59322 7.91247 6.87631 4.73772C6.84346 4.59262 6.7143 4.48929 6.56505 4.48929C6.4158 4.48929 6.28664 4.59262 6.2538 4.73772C5.53657 7.91247 5.04513 8.40423 1.87038 9.12114C1.72495 9.1543 1.62163 9.28314 1.62163 9.43239C1.62163 9.58164 1.72495 9.71048 1.87038 9.74365C5.04513 10.4609 5.53657 10.9526 6.2538 14.1271C6.28664 14.2722 6.4158 14.3755 6.56505 14.3755C6.7143 14.3755 6.84346 14.2722 6.87631 14.1271C7.59354 10.9526 8.08498 10.4609 11.2597 9.74365C11.4052 9.71048 11.5082 9.58164 11.5082 9.43239C11.5082 9.28314 11.4048 9.1543 11.2597 9.12114Z"
12
+ fill="#1A1C21"
13
+ />
14
+ <path
15
+ d="M14.1299 4.17834C12.4423 3.79725 12.2053 3.5603 11.8242 1.87294C11.7911 1.72752 11.6622 1.62451 11.513 1.62451C11.3637 1.62451 11.2349 1.72752 11.2017 1.87294C10.8206 3.5603 10.5837 3.79725 8.8963 4.17834C8.75088 4.21151 8.64787 4.34035 8.64787 4.4896C8.64787 4.63885 8.75088 4.76769 8.8963 4.80086C10.5837 5.18195 10.8206 5.4189 11.2017 7.10658C11.2349 7.25168 11.3637 7.35501 11.513 7.35501C11.6622 7.35501 11.7911 7.25168 11.8242 7.10658C12.2053 5.4189 12.4423 5.18195 14.1299 4.80086C14.275 4.76769 14.3784 4.63885 14.3784 4.4896C14.3784 4.34035 14.275 4.21151 14.1299 4.17834Z"
16
+ fill="#1A1C21"
17
+ />
18
+ </svg>
19
+ );
20
+
21
+ export const AiStarsIcon = styled(Icon).attrs(() => ({
22
+ 'data-component-name': 'icons/AiStarsIcon/AiStarsIcon',
23
+ }))<IconProps>`
24
+ path {
25
+ fill: ${({ color }) => getCssColorVariable(color)};
26
+ }
27
+
28
+ height: ${({ size }) => size || '16px'};
29
+ width: ${({ size }) => size || '16px'};
30
+ `;