@redocly/theme 0.51.0-next.2 → 0.51.0-next.4

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.
Files changed (85) hide show
  1. package/lib/components/Catalog/Catalog.js +2 -26
  2. package/lib/components/Catalog/CatalogVirtualizedGroups.d.ts +11 -0
  3. package/lib/components/Catalog/CatalogVirtualizedGroups.js +125 -0
  4. package/lib/components/Search/SearchAiConversationInput.d.ts +8 -0
  5. package/lib/components/Search/SearchAiConversationInput.js +114 -0
  6. package/lib/components/Search/SearchAiDialog.d.ts +18 -0
  7. package/lib/components/Search/SearchAiDialog.js +165 -0
  8. package/lib/components/Search/SearchAiMessage.d.ts +12 -0
  9. package/lib/components/Search/SearchAiMessage.js +146 -0
  10. package/lib/components/Search/SearchAiResponse.d.ts +1 -0
  11. package/lib/components/Search/SearchAiResponse.js +39 -3
  12. package/lib/components/Search/SearchDialog.js +83 -25
  13. package/lib/components/Search/variables.js +112 -6
  14. package/lib/core/constants/search.d.ts +4 -0
  15. package/lib/core/constants/search.js +6 -1
  16. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.js +39 -10
  17. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +13 -12
  18. package/lib/core/hooks/index.d.ts +1 -0
  19. package/lib/core/hooks/index.js +1 -0
  20. package/lib/core/hooks/use-element-size.d.ts +7 -0
  21. package/lib/core/hooks/use-element-size.js +51 -0
  22. package/lib/core/types/hooks.d.ts +6 -4
  23. package/lib/core/types/l10n.d.ts +1 -1
  24. package/lib/core/types/search.d.ts +9 -0
  25. package/lib/icons/AiStarsIcon/AiStarsIcon.d.ts +9 -6
  26. package/lib/icons/AiStarsIcon/AiStarsIcon.js +38 -4
  27. package/lib/icons/ChatIcon/ChatIcon.d.ts +9 -0
  28. package/lib/icons/ChatIcon/ChatIcon.js +24 -0
  29. package/lib/icons/CheckboxFilledIcon/CheckboxFilledIcon.d.ts +9 -0
  30. package/lib/icons/CheckboxFilledIcon/CheckboxFilledIcon.js +22 -0
  31. package/lib/icons/DataRefineryIcon/DataRefineryIcon.d.ts +9 -0
  32. package/lib/icons/DataRefineryIcon/DataRefineryIcon.js +24 -0
  33. package/lib/icons/DraggableIcon/DraggableIcon.d.ts +9 -0
  34. package/lib/icons/DraggableIcon/DraggableIcon.js +27 -0
  35. package/lib/icons/FlowIcon/FlowIcon.d.ts +9 -0
  36. package/lib/icons/FlowIcon/FlowIcon.js +22 -0
  37. package/lib/icons/PlaylistIcon/PlaylistIcon.d.ts +9 -0
  38. package/lib/icons/PlaylistIcon/PlaylistIcon.js +24 -0
  39. package/lib/icons/SendIcon/SendIcon.d.ts +5 -0
  40. package/lib/icons/SendIcon/SendIcon.js +22 -0
  41. package/lib/icons/SettingsCogIcon/SettingsCogIcon.d.ts +9 -0
  42. package/lib/icons/SettingsCogIcon/SettingsCogIcon.js +25 -0
  43. package/lib/icons/TaskViewIcon/TaskViewIcon.d.ts +9 -0
  44. package/lib/icons/TaskViewIcon/TaskViewIcon.js +24 -0
  45. package/lib/icons/WarningAltFilled/WarningAltFilled.d.ts +9 -0
  46. package/lib/icons/WarningAltFilled/WarningAltFilled.js +23 -0
  47. package/lib/icons/WarningAltFilledIcon/WarningAltFilledIcon.d.ts +9 -0
  48. package/lib/icons/WarningAltFilledIcon/WarningAltFilledIcon.js +23 -0
  49. package/lib/icons/WorkflowAutomationIcon/WorkflowAutomationIcon.d.ts +9 -0
  50. package/lib/icons/WorkflowAutomationIcon/WorkflowAutomationIcon.js +24 -0
  51. package/lib/index.d.ts +11 -0
  52. package/lib/index.js +11 -0
  53. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +2 -28
  54. package/package.json +5 -4
  55. package/src/components/Catalog/Catalog.tsx +3 -37
  56. package/src/components/Catalog/CatalogVirtualizedGroups.tsx +152 -0
  57. package/src/components/Search/SearchAiConversationInput.tsx +133 -0
  58. package/src/components/Search/SearchAiDialog.tsx +238 -0
  59. package/src/components/Search/SearchAiMessage.tsx +209 -0
  60. package/src/components/Search/SearchAiResponse.tsx +59 -3
  61. package/src/components/Search/SearchDialog.tsx +148 -56
  62. package/src/components/Search/variables.ts +112 -6
  63. package/src/core/constants/search.ts +4 -0
  64. package/src/core/hooks/code-walkthrough/use-code-walkthrough-controls.ts +51 -11
  65. package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +15 -12
  66. package/src/core/hooks/index.ts +1 -0
  67. package/src/core/hooks/use-element-size.ts +61 -0
  68. package/src/core/types/hooks.ts +15 -3
  69. package/src/core/types/l10n.ts +7 -0
  70. package/src/core/types/search.ts +10 -0
  71. package/src/icons/AiStarsIcon/AiStarsIcon.tsx +49 -14
  72. package/src/icons/ChatIcon/ChatIcon.tsx +35 -0
  73. package/src/icons/CheckboxFilledIcon/CheckboxFilledIcon.tsx +23 -0
  74. package/src/icons/DataRefineryIcon/DataRefineryIcon.tsx +34 -0
  75. package/src/icons/DraggableIcon/DraggableIcon.tsx +28 -0
  76. package/src/icons/FlowIcon/FlowIcon.tsx +26 -0
  77. package/src/icons/PlaylistIcon/PlaylistIcon.tsx +25 -0
  78. package/src/icons/SendIcon/SendIcon.tsx +33 -0
  79. package/src/icons/SettingsCogIcon/SettingsCogIcon.tsx +32 -0
  80. package/src/icons/TaskViewIcon/TaskViewIcon.tsx +34 -0
  81. package/src/icons/WarningAltFilled/WarningAltFilled.tsx +24 -0
  82. package/src/icons/WarningAltFilledIcon/WarningAltFilledIcon.tsx +24 -0
  83. package/src/icons/WorkflowAutomationIcon/WorkflowAutomationIcon.tsx +34 -0
  84. package/src/index.ts +11 -0
  85. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +2 -5
@@ -0,0 +1,238 @@
1
+ import React, { useEffect } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks';
5
+ import { Button } from '@redocly/theme/components/Button/Button';
6
+ import { SearchAiConversationInput } from '@redocly/theme/components/Search/SearchAiConversationInput';
7
+ import {
8
+ AiSearchError,
9
+ AI_SEARCH_ERROR_CONFIG as ERROR_CONFIG,
10
+ AiSearchConversationRole,
11
+ } from '@redocly/theme/core/constants';
12
+ import { AiSearchConversationItem } from '@redocly/theme/core/types';
13
+ import { SearchAiMessage } from '@redocly/theme/components/Search/SearchAiMessage';
14
+ import { Admonition } from '@redocly/theme/components/Admonition/Admonition';
15
+ import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
16
+
17
+ export type SearchAiDialogProps = {
18
+ response: string | undefined;
19
+ isGeneratingResponse: boolean;
20
+ error: AiSearchError | null;
21
+ resources: {
22
+ url: string;
23
+ title: string;
24
+ }[];
25
+ initialMessage?: string;
26
+ className?: string;
27
+ conversation: AiSearchConversationItem[];
28
+ setConversation: React.Dispatch<React.SetStateAction<AiSearchConversationItem[]>>;
29
+ onMessageSent: (message: string, history?: AiSearchConversationItem[]) => void;
30
+ };
31
+
32
+ export function SearchAiDialog({
33
+ isGeneratingResponse,
34
+ response,
35
+ initialMessage,
36
+ error,
37
+ resources,
38
+ onMessageSent,
39
+ className,
40
+ conversation,
41
+ setConversation,
42
+ }: SearchAiDialogProps): JSX.Element {
43
+ const { useTranslate } = useThemeHooks();
44
+ const { search } = useThemeConfig();
45
+ const { translate } = useTranslate();
46
+
47
+ const conversationEndRef = React.useRef<HTMLDivElement>(null);
48
+
49
+ const suggestions = search?.ai?.suggestions;
50
+
51
+ const placeholder = isGeneratingResponse
52
+ ? translate('search.ai.generatingResponse', 'Generating response...')
53
+ : conversation.length > 0
54
+ ? translate('search.ai.followUpQuestion', 'Ask a follow up question?')
55
+ : translate('search.ai.placeholder', 'Ask a question...');
56
+
57
+ const scrollToBottom = () => {
58
+ conversationEndRef.current?.scrollIntoView({ block: 'end' });
59
+ };
60
+
61
+ const handleOnMessageSent = (message: string) => {
62
+ if (!message.trim()) {
63
+ return;
64
+ }
65
+ const mappedHistory = conversation.map(({ role, content }) => ({
66
+ role,
67
+ content,
68
+ }));
69
+ onMessageSent(message, mappedHistory);
70
+ setConversation((prev) => [...prev, { role: AiSearchConversationRole.USER, content: message }]);
71
+ };
72
+
73
+ useEffect(() => {
74
+ if (!initialMessage?.trim().length) {
75
+ return;
76
+ }
77
+ setConversation((prev) => [
78
+ ...prev,
79
+ { role: AiSearchConversationRole.USER, content: initialMessage },
80
+ ]);
81
+ }, [initialMessage, setConversation]);
82
+
83
+ useEffect(() => {
84
+ if (response === undefined || conversation.length === 0 || error) {
85
+ return;
86
+ }
87
+
88
+ setConversation((prev) => {
89
+ const lastMessage = prev[prev.length - 1];
90
+ const content: string = response || '';
91
+
92
+ if (lastMessage && lastMessage.role === AiSearchConversationRole.ASSISTANT) {
93
+ return [
94
+ ...prev.slice(0, -1),
95
+ { role: AiSearchConversationRole.ASSISTANT, content, resources },
96
+ ];
97
+ }
98
+
99
+ return [...prev, { role: AiSearchConversationRole.ASSISTANT, content }];
100
+ });
101
+ }, [response, conversation.length, error, resources, setConversation]);
102
+
103
+ useEffect(() => {
104
+ if (error) {
105
+ setConversation((prev) => prev.slice(0, -1));
106
+ }
107
+ }, [error, setConversation]);
108
+
109
+ useEffect(() => {
110
+ scrollToBottom();
111
+ }, [conversation, isGeneratingResponse]);
112
+
113
+ return (
114
+ <SearchAiDialogWrapper data-component-name="Search/SearchAiDialog" className={className}>
115
+ {!conversation.length && (
116
+ <WelcomeWrapper>
117
+ <AiStarsIcon
118
+ color="var(--search-ai-icon-color)"
119
+ size="32px"
120
+ background="var(--search-ai-icon-bg-color)"
121
+ borderRadius="var(--border-radius-lg)"
122
+ margin="0 var(--spacing-xs) 0 0"
123
+ />
124
+ {translate(
125
+ 'search.ai.welcomeText',
126
+ 'Welcome to AI search! Feel free to ask me anything. How can I help you? ',
127
+ )}
128
+ </WelcomeWrapper>
129
+ )}
130
+
131
+ <ConversationWrapper>
132
+ {conversation.map((item, index) => (
133
+ <SearchAiMessage
134
+ key={`search-ai-message-${index}`}
135
+ role={item.role}
136
+ content={item.content}
137
+ isThinking={
138
+ item.role === AiSearchConversationRole.ASSISTANT &&
139
+ isGeneratingResponse &&
140
+ index === conversation.length - 1
141
+ }
142
+ resources={item.resources}
143
+ />
144
+ ))}
145
+
146
+ {error && (
147
+ <Admonition
148
+ type="danger"
149
+ name={translate(ERROR_CONFIG[error].headerKey, ERROR_CONFIG[error].headerDefault)}
150
+ >
151
+ {translate(ERROR_CONFIG[error].messageKey, ERROR_CONFIG[error].messageDefault)}
152
+ </Admonition>
153
+ )}
154
+
155
+ {!conversation.length && !error && (
156
+ <SuggestionsWrapper>
157
+ {suggestions?.map((suggestion) => (
158
+ <Button
159
+ key={suggestion}
160
+ variant="outlined"
161
+ onClick={() => handleOnMessageSent(suggestion)}
162
+ >
163
+ {suggestion}
164
+ </Button>
165
+ ))}
166
+ </SuggestionsWrapper>
167
+ )}
168
+
169
+ <div ref={conversationEndRef} />
170
+ </ConversationWrapper>
171
+
172
+ <ConversationInputWrapper>
173
+ <SearchAiConversationInput
174
+ onMessageSent={handleOnMessageSent}
175
+ isGeneratingResponse={isGeneratingResponse}
176
+ placeholder={placeholder}
177
+ />
178
+ </ConversationInputWrapper>
179
+ </SearchAiDialogWrapper>
180
+ );
181
+ }
182
+
183
+ const SearchAiDialogWrapper = styled.div`
184
+ display: flex;
185
+ flex-direction: column;
186
+ flex: 1 1 auto;
187
+ width: 100%;
188
+ min-width: 0;
189
+ min-height: 0;
190
+ position: relative;
191
+ background: var(--search-ai-dialog-bg-color);
192
+ overflow: hidden;
193
+ `;
194
+
195
+ const ConversationWrapper = styled.div`
196
+ flex: 1 1 auto;
197
+ overflow-y: auto;
198
+ overflow-x: hidden;
199
+ padding: var(--search-ai-dialog-body-padding);
200
+ padding-bottom: 0;
201
+ display: flex;
202
+ flex-direction: column;
203
+ align-items: stretch;
204
+ gap: var(--search-ai-dialog-body-gap);
205
+ min-height: 0;
206
+
207
+ > :first-child {
208
+ margin-top: auto;
209
+ }
210
+ > :last-child {
211
+ margin-bottom: 0;
212
+ gap: 0;
213
+ }
214
+ `;
215
+
216
+ const SuggestionsWrapper = styled.div`
217
+ display: flex;
218
+ flex-direction: row;
219
+ flex-wrap: wrap;
220
+ gap: var(--search-ai-suggestions-gap);
221
+ align-items: center;
222
+ justify-content: center;
223
+ `;
224
+
225
+ const ConversationInputWrapper = styled.div`
226
+ padding: var(--search-ai-dialog-input-padding);
227
+ border-top: var(--search-ai-dialog-input-border);
228
+ background: var(--search-ai-dialog-input-bg-color);
229
+ `;
230
+
231
+ const WelcomeWrapper = styled.div`
232
+ display: flex;
233
+ flex-direction: row;
234
+ align-items: center;
235
+ width: auto;
236
+ margin: var(--search-ai-welcome-margin);
237
+ position: relative;
238
+ `;
@@ -0,0 +1,209 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { Link } from '@redocly/theme/components/Link/Link';
5
+ import { Tag } from '@redocly/theme/components/Tag/Tag';
6
+ import { AiSearchConversationRole } from '@redocly/theme/core/constants';
7
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
8
+ import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
9
+ import { DocumentIcon } from '@redocly/theme/icons/DocumentIcon/DocumentIcon';
10
+ import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
11
+
12
+ export type SearchAiMessageProps = {
13
+ role: AiSearchConversationRole;
14
+ content: string;
15
+ isThinking?: boolean;
16
+ resources?: {
17
+ url: string;
18
+ title: string;
19
+ }[];
20
+ className?: string;
21
+ };
22
+
23
+ export function SearchAiMessage({
24
+ role,
25
+ content,
26
+ isThinking,
27
+ resources,
28
+ className,
29
+ }: SearchAiMessageProps): JSX.Element {
30
+ const { useMarkdownText, useTranslate } = useThemeHooks();
31
+ const markDownContent = useMarkdownText(content || '');
32
+ const { translate } = useTranslate();
33
+
34
+ return (
35
+ <SearchAiMessageWrapper
36
+ data-component-name="Search/SearchAiMessage"
37
+ role={role}
38
+ className={className}
39
+ >
40
+ {role === AiSearchConversationRole.ASSISTANT && (
41
+ <AiStarsIcon
42
+ size="32px"
43
+ background="var(--search-ai-icon-bg-color)"
44
+ borderRadius="var(--border-radius-lg)"
45
+ color="var(--search-ai-icon-color)"
46
+ margin="0 var(--spacing-xs) 0 0"
47
+ />
48
+ )}
49
+ <MessageWrapper role={role}>
50
+ {role === AiSearchConversationRole.ASSISTANT ? (
51
+ <>
52
+ <ResponseText as="div" children={markDownContent} />
53
+ {!isThinking && resources && resources.length > 0 && (
54
+ <ResourcesWrapper>
55
+ <ResourcesTitle data-translation-key="search.ai.resourcesFound">
56
+ {resources.length} {translate('search.ai.resourcesFound', 'resources found')}
57
+ </ResourcesTitle>
58
+ <ResourceTagsWrapper>
59
+ {resources.map((resource, idx) => (
60
+ <Link key={idx} to={resource.url} target="_blank">
61
+ <ResourceTag
62
+ borderless
63
+ icon={<DocumentIcon color="--search-ai-resource-tag-icon-color" />}
64
+ >
65
+ {resource.title}
66
+ </ResourceTag>
67
+ </Link>
68
+ ))}
69
+ </ResourceTagsWrapper>
70
+ </ResourcesWrapper>
71
+ )}
72
+ </>
73
+ ) : (
74
+ content
75
+ )}
76
+ {isThinking && content.length === 0 && (
77
+ <ThinkingDotsWrapper>
78
+ <ThinkingDot />
79
+ <ThinkingDot />
80
+ <ThinkingDot />
81
+ </ThinkingDotsWrapper>
82
+ )}
83
+ </MessageWrapper>
84
+ </SearchAiMessageWrapper>
85
+ );
86
+ }
87
+
88
+ const SearchAiMessageWrapper = styled.div<{ role: string }>`
89
+ display: flex;
90
+ flex-direction: row;
91
+ align-items: flex-start;
92
+ width: 100%;
93
+ justify-content: ${({ role }) =>
94
+ role === AiSearchConversationRole.USER ? 'flex-end' : 'flex-start'};
95
+ `;
96
+
97
+ const ResponseText = styled(Markdown)`
98
+ color: var(--search-ai-text-color);
99
+ font-size: var(--search-ai-text-font-size);
100
+ line-height: var(--search-ai-text-line-height);
101
+ font-family: inherit;
102
+ white-space: break-spaces;
103
+
104
+ article {
105
+ > *:first-child {
106
+ margin-top: 0;
107
+ }
108
+ > *:last-child {
109
+ margin-bottom: 0;
110
+ }
111
+ }
112
+ `;
113
+
114
+ const MessageWrapper = styled.div<{ role: string }>`
115
+ padding: ${({ role }) =>
116
+ role === AiSearchConversationRole.USER ? 'var(--spacing-sm)' : 'var(--spacing-xs)'}
117
+ var(--spacing-sm);
118
+ border-radius: var(--border-radius-lg);
119
+ max-width: 80%;
120
+ word-wrap: break-word;
121
+ white-space: pre-wrap;
122
+ background-color: ${({ role }) =>
123
+ role === AiSearchConversationRole.USER
124
+ ? 'var(--search-ai-user-bg-color)'
125
+ : 'var(--search-ai-assistant-bg-color)'};
126
+ border: ${({ role }) =>
127
+ role === AiSearchConversationRole.USER ? 'none' : 'var(--search-ai-assistant-border)'};
128
+ color: ${({ role }) =>
129
+ role === AiSearchConversationRole.USER
130
+ ? 'var(--search-ai-user-text-color)'
131
+ : 'var(--search-ai-assistant-text-color)'};
132
+ `;
133
+
134
+ const ResourcesWrapper = styled.div`
135
+ gap: var(--search-ai-resources-gap);
136
+ display: flex;
137
+ flex-direction: column;
138
+ margin: var(--spacing-xs) 0;
139
+ `;
140
+
141
+ const ResourcesTitle = styled.div`
142
+ font-weight: var(--search-ai-resources-title-font-weight);
143
+ font-size: var(--search-ai-resources-title-font-size);
144
+ line-height: var(--search-ai-resources-title-line-height);
145
+ `;
146
+
147
+ const ResourceTagsWrapper = styled.div`
148
+ display: flex;
149
+ flex-wrap: wrap;
150
+ gap: var(--search-ai-resource-tags-gap);
151
+ `;
152
+
153
+ const ResourceTag = styled(Tag)`
154
+ .tag-default {
155
+ --tag-color: var(--search-ai-resource-tag-text-color);
156
+ max-width: 100%;
157
+ overflow: hidden;
158
+ white-space: nowrap;
159
+ display: inline-block;
160
+ }
161
+ svg {
162
+ min-width: var(--search-ai-resource-tag-icon-size);
163
+ min-height: var(--search-ai-resource-tag-icon-size);
164
+ flex-shrink: 0;
165
+ }
166
+ > div {
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ max-width: 100%;
171
+ }
172
+ `;
173
+
174
+ const ThinkingDotsWrapper = styled.div`
175
+ display: flex;
176
+ gap: var(--search-ai-thinking-dots-gap);
177
+ padding: var(--search-ai-thinking-dots-padding);
178
+ `;
179
+
180
+ const ThinkingDot = styled.div`
181
+ width: var(--search-ai-thinking-dot-size);
182
+ height: var(--search-ai-thinking-dot-size);
183
+ border-radius: 50%;
184
+ background: var(--search-ai-thinking-dot-color);
185
+ animation: bounce 1.4s infinite ease-in-out;
186
+
187
+ &:nth-child(1) {
188
+ animation-delay: -0.32s;
189
+ }
190
+ &:nth-child(2) {
191
+ animation-delay: -0.16s;
192
+ }
193
+ &:nth-child(3) {
194
+ animation-delay: 0s;
195
+ }
196
+
197
+ @keyframes bounce {
198
+ 0%,
199
+ 80%,
200
+ 100% {
201
+ opacity: 0.2;
202
+ transform: scale(0.8);
203
+ }
204
+ 40% {
205
+ opacity: 1;
206
+ transform: scale(1);
207
+ }
208
+ }
209
+ `;
@@ -6,14 +6,16 @@ import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/Ch
6
6
  import { DocumentIcon } from '@redocly/theme/icons/DocumentIcon/DocumentIcon';
7
7
  import { Tag } from '@redocly/theme/components/Tag/Tag';
8
8
  import { Link } from '@redocly/theme/components/Link/Link';
9
- import { useThemeHooks } from '@redocly/theme/core/hooks';
9
+ import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks';
10
10
  import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
11
+ import { Typography } from '@redocly/theme/components/Typography/Typography';
11
12
  import { Admonition } from '@redocly/theme/components/Admonition/Admonition';
12
13
  import { ErrorFilledIcon } from '@redocly/theme/icons/ErrorFilledIcon/ErrorFilledIcon';
13
14
  import {
14
15
  AiSearchError,
15
16
  AI_SEARCH_ERROR_CONFIG as ERROR_CONFIG,
16
17
  } from '@redocly/theme/core/constants';
18
+ import { ChatIcon } from '@redocly/theme/icons/ChatIcon/ChatIcon';
17
19
 
18
20
  export type SearchAiResponseProps = {
19
21
  question: string;
@@ -24,17 +26,22 @@ export type SearchAiResponseProps = {
24
26
  url: string;
25
27
  title: string;
26
28
  }[];
29
+ onSuggestionClick: (suggestion: string) => void;
27
30
  };
28
31
  export function SearchAiResponse(props: SearchAiResponseProps): JSX.Element {
29
32
  const { useMarkdownText } = useThemeHooks();
30
- const { question, response, isGeneratingResponse, resources, error } = props;
33
+ const { question, response, isGeneratingResponse, resources, error, onSuggestionClick } = props;
31
34
 
35
+ const { search } = useThemeConfig();
32
36
  const { useTranslate } = useThemeHooks();
37
+
33
38
  const { translate } = useTranslate();
34
39
  const markdownResponse = useMarkdownText(response || '');
35
40
 
36
41
  let responseContainer = null;
37
42
 
43
+ const suggestions = search?.ai?.suggestions;
44
+
38
45
  const hasPendingOrReceivedResponse = response || isGeneratingResponse || error;
39
46
  if (hasPendingOrReceivedResponse) {
40
47
  let icon;
@@ -96,6 +103,28 @@ export function SearchAiResponse(props: SearchAiResponseProps): JSX.Element {
96
103
 
97
104
  return (
98
105
  <ResponseWrapper data-component-name="Search/SearchAiResponse">
106
+ {!question && (
107
+ <>
108
+ {suggestions?.length && (
109
+ <SuggestionsWrapper>
110
+ <SuggestionsTitle>
111
+ {translate('search.ai.suggestionsTitle', 'Suggestions')}
112
+ </SuggestionsTitle>
113
+ {suggestions.map((suggestion, idx) => (
114
+ <SuggestionItem
115
+ key={idx.toString()}
116
+ onClick={() => {
117
+ onSuggestionClick(suggestion);
118
+ }}
119
+ >
120
+ <ChatIcon color="--color-blueberry-6" />
121
+ <Typography color="--search-ai-suggestions-text-color">{suggestion}</Typography>
122
+ </SuggestionItem>
123
+ ))}
124
+ </SuggestionsWrapper>
125
+ )}
126
+ </>
127
+ )}
99
128
  {responseContainer}
100
129
  </ResponseWrapper>
101
130
  );
@@ -180,11 +209,38 @@ const ResourceTags = styled.div`
180
209
  flex-wrap: wrap;
181
210
  gap: var(--search-ai-resource-tags-gap);
182
211
  `;
212
+
183
213
  const ResourceTag = styled(Tag).attrs({
184
214
  className: 'tag-resource',
185
- 'data-component-name': 'Tags/SearchAiResourceTag',
186
215
  })`
187
216
  .tag-default {
188
217
  --tag-color: --search-ai-resource-tag-text-color;
189
218
  }
190
219
  `;
220
+
221
+ const SuggestionsWrapper = styled.div`
222
+ display: flex;
223
+ flex-direction: column;
224
+ justify-content: flex-start;
225
+
226
+ gap: var(--spacing-sm);
227
+ margin-left: var(--spacing-xs);
228
+ `;
229
+
230
+ const SuggestionsTitle = styled(Typography)`
231
+ font-size: var(--search-ai-suggestions-title-font-size);
232
+ line-height: var(--search-ai-suggestions-title-line-height);
233
+ font-weight: var(--search-ai-suggestions-title-font-weight);
234
+
235
+ color: var(--search-ai-suggestions-title-text-color);
236
+ `;
237
+
238
+ const SuggestionItem = styled.div`
239
+ cursor: pointer;
240
+
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: flex-start;
244
+
245
+ gap: var(--spacing-xs);
246
+ `;