@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
@@ -1,4 +1,4 @@
1
- import React, { Fragment, useRef, useState } from 'react';
1
+ import React, { Fragment, useRef, useState, useEffect } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { MouseEvent } from 'react';
@@ -6,7 +6,6 @@ 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';
10
9
  import { Button } from '@redocly/theme/components/Button/Button';
11
10
  import { breakpoints, concatClassNames } from '@redocly/theme/core/utils';
12
11
  import { SearchItem } from '@redocly/theme/components/Search/SearchItem';
@@ -17,9 +16,13 @@ import { Tag } from '@redocly/theme/components/Tag/Tag';
17
16
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
18
17
  import { SearchFilter } from '@redocly/theme/components/Search/SearchFilter';
19
18
  import { SearchGroups } from '@redocly/theme/components/Search/SearchGroups';
19
+ import { Typography } from '@redocly/theme/components/Typography/Typography';
20
20
  import { SpinnerLoader } from '@redocly/theme/components/Loaders/SpinnerLoader';
21
+ import { SearchAiDialog } from '@redocly/theme/components/Search/SearchAiDialog';
21
22
  import { SettingsIcon } from '@redocly/theme/icons/SettingsIcon/SettingsIcon';
22
23
  import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
24
+ import { ChevronLeftIcon } from '@redocly/theme/icons/ChevronLeftIcon/ChevronLeftIcon';
25
+ import { EditIcon } from '@redocly/theme/icons/EditIcon/EditIcon';
23
26
 
24
27
  export type SearchDialogProps = {
25
28
  onClose: () => void;
@@ -63,6 +66,12 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
63
66
 
64
67
  useDialogHotKeys(modalRef, onClose);
65
68
 
69
+ useEffect(() => {
70
+ if (mode === 'ai-dialog' && aiSearch.isGeneratingResponse) {
71
+ setQuery('');
72
+ }
73
+ }, [mode, aiSearch.isGeneratingResponse, setQuery]);
74
+
66
75
  const handleOverlayClick = (event: MouseEvent<HTMLElement>) => {
67
76
  const target = event.target as HTMLElement;
68
77
  if (typeof target.className !== 'string') return;
@@ -117,6 +126,7 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
117
126
  const showResults = !!((filter && filter.length) || query);
118
127
  const showSearchFilterButton = advancedSearch && mode === 'search';
119
128
  const showAiSearchButton = askAi && mode === 'search';
129
+ const showAiSearchItem = showAiSearchButton && query;
120
130
  const showHeaderButtons = showSearchFilterButton || showAiSearchButton;
121
131
 
122
132
  return (
@@ -129,57 +139,70 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
129
139
  <SearchDialogWrapper className="scroll-lock" role="dialog">
130
140
  <SearchDialogHeader>
131
141
  {product && (
132
- <>
133
- <SearchProductTag color="product">
134
- {product.name}
135
- <CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
136
- </SearchProductTag>
137
- </>
142
+ <SearchProductTag color="product">
143
+ {product.name}
144
+ <CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
145
+ </SearchProductTag>
138
146
  )}
139
- <SearchInput
140
- value={query}
141
- onChange={setQuery}
142
- placeholder={
143
- mode === 'search'
144
- ? translate('search.label', 'Search docs...')
145
- : translate('search.ai.label', 'Ask AI assistant')
146
- }
147
- isLoading={isSearchLoading}
148
- showReturnButton={mode === 'ai-dialog'}
149
- onReturn={() => {
150
- setMode('search');
151
- aiSearch.clearAiSearchState();
152
- }}
153
- onSubmit={
154
- mode === 'ai-dialog'
155
- ? () => {
156
- setQuery('');
157
- aiSearch.askQuestion(query);
158
- }
159
- : undefined
160
- }
161
- data-translation-key={mode === 'search' ? 'search.label' : 'search.ai.label'}
162
- />
163
- {showHeaderButtons && (
164
- <SearchHeaderButtons>
165
- {showAiSearchButton ? (
166
- <SearchAiButton
167
- icon={<AiStarsIcon />}
168
- onClick={() => {
169
- setMode('ai-dialog');
170
- if (query.trim()) {
171
- setQuery('');
172
- aiSearch.askQuestion(query);
173
- }
174
- }}
175
- >
176
- {translate('search.ai.button', 'Search with AI')}
177
- </SearchAiButton>
178
- ) : null}
179
- {showSearchFilterButton && (
180
- <SearchFilterToggleButton icon={<SettingsIcon />} onClick={onFilterToggle} />
147
+ {mode === 'search' ? (
148
+ <>
149
+ <SearchInput
150
+ value={query}
151
+ onChange={setQuery}
152
+ placeholder={translate('search.label', 'Search docs...')}
153
+ isLoading={isSearchLoading}
154
+ onSubmit={
155
+ showAiSearchButton
156
+ ? () => {
157
+ setMode('ai-dialog');
158
+ aiSearch.askQuestion(query);
159
+ }
160
+ : undefined
161
+ }
162
+ data-translation-key="search.label"
163
+ />
164
+ {showHeaderButtons && (
165
+ <SearchHeaderButtons>
166
+ {showAiSearchButton ? (
167
+ <SearchAiButton
168
+ icon={<AiStarsIcon gradient />}
169
+ onClick={() => {
170
+ setMode('ai-dialog');
171
+ if (query.trim()) {
172
+ aiSearch.askQuestion(query);
173
+ }
174
+ }}
175
+ >
176
+ {translate('search.ai.button', 'Search with AI')}
177
+ </SearchAiButton>
178
+ ) : null}
179
+ {showSearchFilterButton && (
180
+ <SearchFilterToggleButton icon={<SettingsIcon />} onClick={onFilterToggle} />
181
+ )}
182
+ </SearchHeaderButtons>
181
183
  )}
182
- </SearchHeaderButtons>
184
+ </>
185
+ ) : (
186
+ <AiDialogHeaderWrapper>
187
+ <Button
188
+ variant="secondary"
189
+ onClick={() => {
190
+ setMode('search');
191
+ aiSearch.clearConversation();
192
+ }}
193
+ icon={<ChevronLeftIcon />}
194
+ >
195
+ {translate('search.ai.backToSearch', 'Back to search')}
196
+ </Button>
197
+ <Button
198
+ variant="secondary"
199
+ disabled={!aiSearch.conversation.length}
200
+ onClick={() => aiSearch.clearConversation()}
201
+ icon={<EditIcon />}
202
+ >
203
+ {translate('search.ai.newConversation', 'New conversation')}
204
+ </Button>
205
+ </AiDialogHeaderWrapper>
183
206
  )}
184
207
  </SearchDialogHeader>
185
208
 
@@ -194,6 +217,31 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
194
217
  onQuickFilterReset={onQuickFilterReset}
195
218
  groupField={groupField}
196
219
  />
220
+
221
+ {showAiSearchItem && (
222
+ <SearchWithAI
223
+ onClick={() => {
224
+ setMode('ai-dialog');
225
+ if (query.trim()) {
226
+ aiSearch.askQuestion(query);
227
+ }
228
+ }}
229
+ tabIndex={0}
230
+ role="option"
231
+ aria-selected="true"
232
+ >
233
+ <AiStarsIcon
234
+ color="var(--search-ai-icon-color)"
235
+ size="36px"
236
+ background="var(--search-ai-icon-bg-color)"
237
+ margin="0 var(--spacing-md) 0 0"
238
+ borderRadius="var(--border-radius-lg)"
239
+ />
240
+ <Typography fontWeight="var(--font-weight-semibold)">{query}</Typography>
241
+ <Typography>- {translate('search.ai.label', 'Ask AI assistant')}</Typography>
242
+ <ReturnKey>⏎</ReturnKey>
243
+ </SearchWithAI>
244
+ )}
197
245
  {showResults ? (
198
246
  items && Object.keys(items).some((key) => items[key]?.length) ? (
199
247
  Object.keys(items).map((key) =>
@@ -248,12 +296,15 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
248
296
  )}
249
297
  </>
250
298
  ) : (
251
- <SearchAiResponse
252
- question={aiSearch.question}
253
- isGeneratingResponse={aiSearch.isGeneratingResponse}
299
+ <SearchAiDialog
300
+ initialMessage={query}
254
301
  response={aiSearch.response}
255
- resources={aiSearch.resources}
302
+ isGeneratingResponse={aiSearch.isGeneratingResponse}
256
303
  error={aiSearch.error}
304
+ resources={aiSearch.resources}
305
+ conversation={aiSearch.conversation}
306
+ setConversation={aiSearch.setConversation}
307
+ onMessageSent={aiSearch.askQuestion}
257
308
  />
258
309
  )}
259
310
  </SearchDialogBody>
@@ -363,10 +414,18 @@ const SearchDialogHeader = styled.header`
363
414
  padding: var(--search-modal-header-padding);
364
415
  `;
365
416
 
417
+ const AiDialogHeaderWrapper = styled.div`
418
+ display: flex;
419
+ justify-content: space-between;
420
+ align-items: center;
421
+ width: 100%;
422
+ `;
423
+
366
424
  const SearchDialogBody = styled.div`
367
425
  display: flex;
368
426
  flex-direction: row;
369
- flex-grow: 1;
427
+ flex: 1;
428
+ min-height: 0;
370
429
  overflow: hidden;
371
430
 
372
431
  @media screen and (max-width: ${breakpoints.small}) {
@@ -476,3 +535,36 @@ const AiDisclaimer = styled.div`
476
535
  color: var(--search-ai-disclaimer-text-color);
477
536
  margin: 0 auto;
478
537
  `;
538
+
539
+ const SearchWithAI = styled.div`
540
+ display: flex;
541
+ justify-content: flex-start;
542
+ align-items: center;
543
+ cursor: pointer;
544
+ gap: var(--spacing-unit);
545
+ padding: var(--spacing-md);
546
+
547
+ color: var(--search-item-text-color);
548
+ background-color: var(--search-item-bg-color);
549
+ text-decoration: none;
550
+ white-space: normal;
551
+ outline: none;
552
+ border: none;
553
+
554
+ transition: all 0.3s ease;
555
+
556
+ &:focus,
557
+ &:hover {
558
+ color: var(--search-item-text-color-hover);
559
+ background-color: var(--search-item-bg-color-hover);
560
+ }
561
+
562
+ & > :first-child {
563
+ margin-right: var(--spacing-xs);
564
+ }
565
+ `;
566
+
567
+ const ReturnKey = styled(Typography)`
568
+ color: var(--search-item-text-color);
569
+ margin-left: auto;
570
+ `;
@@ -142,12 +142,12 @@ export const search = css`
142
142
  --search-trigger-icon-size: 16px;
143
143
  --search-trigger-line-height: var(--line-height-base);
144
144
 
145
- // @tokens End
146
-
147
145
  /**
148
- * @tokens Ai Search
146
+ * @tokens AI Search
149
147
  */
150
148
 
149
+ --search-ai-gradient: linear-gradient(to right, #715efe, #ff5cdc);
150
+
151
151
  --search-ai-spinner-icon-color: var(--icon-color-interactive);
152
152
  --search-ai-checkmark-icon-color: var(--icon-color-interactive);
153
153
  --search-ai-response-padding: var(--spacing-lg);
@@ -163,9 +163,15 @@ export const search = css`
163
163
  --search-ai-response-body-gap: var(--spacing-xl);
164
164
  --search-ai-response-body-padding: 0 40px;
165
165
 
166
- --search-ai-response-text-color: var(--text-color-secondary);
167
- --search-ai-response-text-font-size: var(--font-size-lg);
168
- --search-ai-response-text-line-height: var(--line-height-lg);
166
+ --search-ai-text-color: var(--text-color-secondary);
167
+ --search-ai-text-font-size: var(--font-size-lg);
168
+ --search-ai-text-line-height: var(--line-height-lg);
169
+
170
+ --search-ai-user-bg-color: var(--color-blue-6);
171
+ --search-ai-user-text-color: var(--color-static-white);
172
+ --search-ai-assistant-bg-color: var(--layer-color);
173
+ --search-ai-assistant-text-color: var(--text-color-primary);
174
+ --search-ai-assistant-border: 1px solid var(--border-color-primary);
169
175
 
170
176
  --search-ai-resources-gap: var(--spacing-base);
171
177
  --search-ai-resources-title-font-weight: var(--font-weight-medium);
@@ -175,10 +181,110 @@ export const search = css`
175
181
  --search-ai-resource-tags-gap: var(--spacing-base);
176
182
  --search-ai-resource-tag-text-color: var(--text-color-secondary);
177
183
  --search-ai-resource-tag-icon-color: var(--text-color-secondary);
184
+ --search-ai-resource-tag-icon-size: 16px;
185
+
186
+ --search-ai-icon-size: 32px;
187
+ --search-ai-icon-bg-color: var(--search-ai-gradient);
188
+ --search-ai-icon-color: var(--color-static-white);
189
+
190
+ --search-ai-thinking-dots-gap: 4px;
191
+ --search-ai-thinking-dots-padding: 4px 0;
192
+ --search-ai-thinking-dot-size: 6px;
193
+ --search-ai-thinking-dot-color: var(--search-ai-gradient);
178
194
 
179
195
  --search-ai-disclaimer-font-size: var(--font-size-sm);
180
196
  --search-ai-disclaimer-line-height: var(--line-height-sm);
181
197
  --search-ai-disclaimer-text-color: var(--text-color-secondary);
182
198
 
199
+
200
+ --search-ai-welcome-margin: var(--spacing-md);
201
+ --search-ai-icon-wrapper-padding: var(--spacing-xs);
202
+
203
+ --search-ai-suggestions-title-text-color: var(--text-color-description);
204
+ --search-ai-suggestions-title-font-size: var(--font-size-base);
205
+ --search-ai-suggestions-title-line-height: var(--line-height-base);
206
+ --search-ai-suggestions-title-font-weight: var(--font-weight-light);
207
+ --search-ai-suggestions-text-color: var(--text-color-description);
208
+
209
+ --search-ai-conversation-input-send-button-bg-color: var(--button-bg-color-primary);
210
+ --search-ai-conversation-input-send-button-bg-color-hover: var(--button-bg-color-primary-hover);
211
+ --search-ai-conversation-input-send-button-bg-color-disabled: var(--button-bg-color-disabled);
212
+ --search-ai-conversation-input-send-button-border-color-disabled: var(--button-border-color-disabled);
213
+ --search-ai-conversation-input-send-button-icon-color: var(--color-static-white);
214
+
215
+ /**
216
+ * @tokens AI Search Dialog
217
+ */
218
+ --search-ai-dialog-bg-color: var(--bg-color);
219
+ --search-ai-dialog-header-border: var(--search-modal-border);
220
+ --search-ai-dialog-header-bg-color: var(--search-modal-header-bg-color);
221
+ --search-ai-dialog-header-padding: var(--search-modal-header-padding);
222
+
223
+ --search-ai-dialog-body-padding: var(--search-ai-response-padding);
224
+ --search-ai-dialog-body-gap: var(--spacing-sm);
225
+
226
+ --search-ai-dialog-input-padding: var(--spacing-sm) var(--search-ai-response-padding);
227
+ --search-ai-dialog-input-border: 1px solid var(--border-color-secondary);
228
+ --search-ai-dialog-input-bg-color: var(--bg-color);
229
+
230
+ /**
231
+ * @tokens AI Search Conversation Input
232
+ */
233
+ --search-ai-conversation-input-bg-color: var(--bg-color);
234
+ --search-ai-conversation-input-padding: var(--spacing-sm) var(--spacing-md);
235
+ --search-ai-conversation-input-border: 1px solid var(--border-color-secondary);
236
+ --search-ai-conversation-input-border-radius: var(--border-radius-lg);
237
+ --search-ai-conversation-input-font-size: var(--font-size-base);
238
+ --search-ai-conversation-input-placeholder-color: var(--search-input-placeholder-color);
239
+ --search-ai-conversation-input-border-color-focus: var(--color-blue-6);
240
+ --search-ai-conversation-input-border-color-disabled: var(--border-color-secondary);
241
+
242
+ --search-ai-conversation-input-send-button-right: 12px;
243
+ --search-ai-conversation-input-send-button-bg-color: var(--button-bg-color-primary);
244
+ --search-ai-conversation-input-send-button-bg-color-hover: var(--button-bg-color-primary-hover);
245
+ --search-ai-conversation-input-send-button-bg-color-disabled: var(--button-bg-color-disabled);
246
+ --search-ai-conversation-input-send-button-border-disabled: 1px solid var(--button-border-color-disabled);
247
+
248
+ /**
249
+ * @tokens AI Search Response
250
+ */
251
+ --search-ai-response-padding: var(--spacing-lg);
252
+ --search-ai-response-gap: var(--spacing-sm);
253
+ --search-ai-response-header-gap: var(--spacing-md);
254
+ --search-ai-response-body-gap: var(--spacing-xl);
255
+ --search-ai-response-body-padding: 0 40px;
256
+
257
+ --search-ai-text-color: var(--text-color-secondary);
258
+ --search-ai-text-font-size: var(--font-size-lg);
259
+ --search-ai-text-line-height: var(--line-height-lg);
260
+
261
+ --search-ai-thinking-text-margin: var(--md-pre-margin) 0;
262
+
263
+ --search-ai-question-font-size: var(--font-size-xl);
264
+ --search-ai-question-font-weight: var(--font-weight-semibold);
265
+ --search-ai-question-line-height: var(--line-height-xl);
266
+ --search-ai-question-text-color: var(--text-color-primary);
267
+
268
+ --search-ai-resources-gap: var(--spacing-base);
269
+ --search-ai-resources-title-font-weight: var(--font-weight-medium);
270
+ --search-ai-resources-title-font-size: var(--font-size-lg);
271
+ --search-ai-resources-title-line-height: var(--line-height-lg);
272
+
273
+ --search-ai-resource-tags-gap: var(--spacing-base);
274
+ --search-ai-resource-tag-text-color: var(--text-color-secondary);
275
+ --search-ai-resource-tag-icon-color: var(--text-color-secondary);
276
+
277
+ --search-ai-suggestions-gap: var(--spacing-sm);
278
+ --search-ai-suggestions-margin-left: var(--spacing-xs);
279
+ --search-ai-suggestion-item-gap: var(--spacing-xs);
280
+
281
+ --search-ai-suggestions-title-text-color: var(--text-color-description);
282
+ --search-ai-suggestions-title-font-size: var(--font-size-base);
283
+ --search-ai-suggestions-title-line-height: var(--line-height-base);
284
+ --search-ai-suggestions-title-font-weight: var(--font-weight-light);
285
+
286
+ --search-ai-spinner-icon-color: var(--icon-color-interactive);
287
+ --search-ai-checkmark-icon-color: var(--icon-color-interactive);
288
+
183
289
  // @tokens End
184
290
  `;
@@ -7,6 +7,10 @@ export enum AiSearchError {
7
7
  EmptyResponse = 'empty_response',
8
8
  ErrorProcessingResponse = 'error_processing_response',
9
9
  }
10
+ export const enum AiSearchConversationRole {
11
+ USER = 'user',
12
+ ASSISTANT = 'assistant',
13
+ }
10
14
 
11
15
  const defaultErrorConfig: AiSearchErrorConfig = {
12
16
  headerKey: 'search.ai.error.header',
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useMemo } from 'react';
1
+ import { useState, useEffect, useMemo, useRef } from 'react';
2
2
  import { useLocation, useNavigate } from 'react-router-dom';
3
3
 
4
4
  import type {
@@ -52,11 +52,15 @@ export function useCodeWalkthroughControls(
52
52
  const navigate = useNavigate();
53
53
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
54
54
 
55
- const [controlsState, setControlsState] = useState(() => {
56
- const initialState: CodeWalkthroughControlsState = {};
55
+ const filtersRef = useRef(filters);
56
+ const inputsRef = useRef(inputs);
57
+ const togglesRef = useRef(toggles);
58
+
59
+ const getInitialState = () => {
60
+ const state: CodeWalkthroughControlsState = {};
57
61
 
58
62
  for (const [id, toggle] of Object.entries(toggles)) {
59
- initialState[id] = {
63
+ state[id] = {
60
64
  ...toggle,
61
65
  render: true,
62
66
  type: 'toggle',
@@ -65,7 +69,7 @@ export function useCodeWalkthroughControls(
65
69
  }
66
70
 
67
71
  for (const [id, input] of Object.entries(inputs)) {
68
- initialState[id] = {
72
+ state[id] = {
69
73
  ...input,
70
74
  render: true,
71
75
  type: 'input',
@@ -75,7 +79,7 @@ export function useCodeWalkthroughControls(
75
79
 
76
80
  for (const [id, filter] of Object.entries(filters)) {
77
81
  const defaultValue = filter?.items?.[0]?.value || '';
78
- initialState[id] = {
82
+ state[id] = {
79
83
  ...filter,
80
84
  render: true,
81
85
  type: 'filter',
@@ -83,8 +87,43 @@ export function useCodeWalkthroughControls(
83
87
  };
84
88
  }
85
89
 
86
- return initialState;
87
- });
90
+ return state;
91
+ };
92
+
93
+ const [controlsState, setControlsState] = useState(getInitialState);
94
+
95
+ useEffect(() => {
96
+ const sameProps = [
97
+ JSON.stringify(filters) === JSON.stringify(filtersRef.current),
98
+ JSON.stringify(inputs) === JSON.stringify(inputsRef.current),
99
+ JSON.stringify(toggles) === JSON.stringify(togglesRef.current),
100
+ ];
101
+
102
+ if (sameProps.every(Boolean)) {
103
+ return;
104
+ }
105
+
106
+ filtersRef.current = filters;
107
+ inputsRef.current = inputs;
108
+ togglesRef.current = toggles;
109
+
110
+ const newState = getInitialState();
111
+
112
+ // Preserve existing values where control type hasn't changed
113
+ Object.entries(newState).forEach(([id, control]) => {
114
+ const existingControl = controlsState[id];
115
+ if (existingControl && existingControl.type === control.type) {
116
+ // @ts-ignore
117
+ newState[id] = {
118
+ ...control,
119
+ value: existingControl.value,
120
+ };
121
+ }
122
+ });
123
+
124
+ setControlsState(newState);
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, [filters, inputs, toggles, enableDeepLink]);
88
127
 
89
128
  const changeControlState = (id: string, value: string | boolean) => {
90
129
  setControlsState((prev) => {
@@ -220,7 +259,8 @@ export function useCodeWalkthroughControls(
220
259
  getFileText,
221
260
  populateInputsWithValue,
222
261
  };
223
- }, [filters, controlsState]);
262
+ // eslint-disable-next-line react-hooks/exhaustive-deps
263
+ }, [controlsState]);
224
264
 
225
265
  /**
226
266
  * Update the URL search params with the current state of the filters and inputs
@@ -242,10 +282,10 @@ export function useCodeWalkthroughControls(
242
282
 
243
283
  const newSearch = newSearchParams.toString();
244
284
  if (newSearch === location.search.substring(1)) return;
245
- navigate({ search: newSearch });
285
+ navigate({ search: newSearch }, { replace: true });
246
286
  // Ignore searchParams in dependency array to avoid infinite re-renders
247
287
  // eslint-disable-next-line react-hooks/exhaustive-deps
248
- }, [filters, controlsState, navigate, location]);
288
+ }, [controlsState]);
249
289
 
250
290
  return {
251
291
  changeControlState,
@@ -38,14 +38,17 @@ export function useCodeWalkthroughSteps(
38
38
  enableDeepLink ? searchParams.get(ACTIVE_STEP_QUERY_PARAM) : null,
39
39
  );
40
40
 
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ const _steps = useMemo(() => steps, [JSON.stringify(steps)]);
43
+
41
44
  const register = useCallback(
42
45
  (element: HTMLElement) => {
43
46
  // for some reason, the observer is not ready immediately
44
47
  setTimeout(() => {
45
48
  if (observerRef.current) {
46
49
  const stepKey = Number(element.dataset.stepKey);
47
- if (Number.isInteger(stepKey) && stepKey >= 0) {
48
- steps[stepKey].compRef = element;
50
+ if (Number.isInteger(stepKey) && stepKey >= 0 && _steps[stepKey]) {
51
+ _steps[stepKey].compRef = element;
49
52
  }
50
53
 
51
54
  observerRef.current.observe(element);
@@ -53,22 +56,22 @@ export function useCodeWalkthroughSteps(
53
56
  }
54
57
  }, 10);
55
58
  },
56
- [steps],
59
+ [_steps],
57
60
  );
58
61
 
59
62
  const unregister = useCallback(
60
63
  (element: HTMLElement) => {
61
64
  if (observerRef.current) {
62
65
  const stepKey = Number(element.dataset.stepKey);
63
- if (Number.isInteger(stepKey) && stepKey >= 0) {
64
- steps[stepKey].compRef = undefined;
66
+ if (Number.isInteger(stepKey) && stepKey >= 0 && _steps[stepKey]) {
67
+ _steps[stepKey].compRef = undefined;
65
68
  }
66
69
 
67
70
  observerRef.current.unobserve(element);
68
71
  observedElementsRef.current.delete(element);
69
72
  }
70
73
  },
71
- [steps],
74
+ [_steps],
72
75
  );
73
76
 
74
77
  const observerCallback = useCallback(
@@ -77,7 +80,7 @@ export function useCodeWalkthroughSteps(
77
80
  return;
78
81
  }
79
82
 
80
- const renderedSteps = steps.filter((step) => Boolean(step.compRef));
83
+ const renderedSteps = _steps.filter((step) => Boolean(step.compRef));
81
84
 
82
85
  if (renderedSteps.length < 2) {
83
86
  setActiveStep(renderedSteps[0]?.id || null);
@@ -92,7 +95,7 @@ export function useCodeWalkthroughSteps(
92
95
  }
93
96
 
94
97
  const { intersectionRatio, boundingClientRect, rootBounds, isIntersecting } = entry;
95
- const step = steps[stepKey];
98
+ const step = _steps[stepKey];
96
99
 
97
100
  const stepIndex = renderedSteps.findIndex(
98
101
  (renderedStep) => renderedStep.stepKey === step.stepKey,
@@ -130,7 +133,7 @@ export function useCodeWalkthroughSteps(
130
133
  }
131
134
  }
132
135
  },
133
- [steps, activeStep],
136
+ [_steps, activeStep],
134
137
  );
135
138
  useEffect(() => {
136
139
  const filtersElementHeight = filtersElementRef.current?.clientHeight || 0;
@@ -167,11 +170,11 @@ export function useCodeWalkthroughSteps(
167
170
 
168
171
  const newSearch = newSearchParams.toString();
169
172
  if (newSearch === location.search.substring(1)) return;
170
- navigate({ search: newSearch });
171
173
 
172
- // Ignore searchParams in dependency array to avoid infinite re-renders
174
+ navigate({ search: newSearch }, { replace: true });
175
+
173
176
  // eslint-disable-next-line react-hooks/exhaustive-deps
174
- }, [activeStep, navigate, location]);
177
+ }, [activeStep]);
175
178
 
176
179
  return { register, unregister, lockObserver, filtersElementRef, activeStep, setActiveStep };
177
180
  }
@@ -33,3 +33,4 @@ export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-walkthrough-s
33
33
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-walkthrough-controls';
34
34
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-panel';
35
35
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-renderable-files';
36
+ export * from '@redocly/theme/core/hooks/use-element-size';