@redocly/theme 0.59.0-rc.1 → 0.59.0

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 (195) hide show
  1. package/LICENSE +7 -1
  2. package/lib/components/Buttons/AIAssistantButton.js +6 -2
  3. package/lib/components/Buttons/ConnectMCPButton.d.ts +8 -0
  4. package/lib/components/Buttons/ConnectMCPButton.js +145 -0
  5. package/lib/components/Buttons/variables.d.ts +1 -0
  6. package/lib/components/Buttons/variables.js +42 -2
  7. package/lib/components/Catalog/CatalogEntity/CatalogEntityInfoBar.js +1 -0
  8. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  9. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  10. package/lib/components/Catalog/CatalogEntityIcon.js +2 -1
  11. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +4 -0
  12. package/lib/components/Catalog/CatalogTagsWithTooltip.js +1 -1
  13. package/lib/components/Catalog/variables.js +1 -1
  14. package/lib/components/Dropdown/Dropdown.d.ts +16 -2
  15. package/lib/components/Dropdown/Dropdown.js +5 -5
  16. package/lib/components/Menu/MenuItem.js +1 -1
  17. package/lib/components/Navbar/NavbarItem.js +3 -3
  18. package/lib/components/PageActions/PageActions.js +4 -1
  19. package/lib/components/PageActions/variables.js +2 -0
  20. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +1 -2
  21. package/lib/components/Search/SearchAiActionButtons.d.ts +10 -0
  22. package/lib/components/Search/SearchAiActionButtons.js +43 -0
  23. package/lib/components/Search/SearchAiConversationInput.d.ts +3 -1
  24. package/lib/components/Search/SearchAiConversationInput.js +39 -7
  25. package/lib/components/Search/SearchAiDialog.d.ts +3 -6
  26. package/lib/components/Search/SearchAiDialog.js +20 -9
  27. package/lib/components/Search/SearchAiMessage.d.ts +9 -5
  28. package/lib/components/Search/SearchAiMessage.js +146 -22
  29. package/lib/components/Search/SearchAiNegativeFeedbackForm.d.ts +8 -0
  30. package/lib/components/Search/SearchAiNegativeFeedbackForm.js +169 -0
  31. package/lib/components/Search/SearchDialog.js +36 -5
  32. package/lib/components/Search/SearchGroups.js +2 -2
  33. package/lib/components/Search/variables.js +36 -64
  34. package/lib/components/Segmented/Segmented.d.ts +1 -8
  35. package/lib/components/Segmented/Segmented.js +3 -1
  36. package/lib/components/Select/SelectInput.js +1 -1
  37. package/lib/components/Select/variables.js +2 -2
  38. package/lib/components/Tag/Tag.d.ts +2 -1
  39. package/lib/components/Tag/Tag.js +66 -17
  40. package/lib/components/Tag/variables.dark.js +135 -36
  41. package/lib/components/Tag/variables.js +78 -61
  42. package/lib/core/constants/index.d.ts +1 -0
  43. package/lib/core/constants/index.js +1 -0
  44. package/lib/core/constants/mcp.d.ts +1 -0
  45. package/lib/core/constants/mcp.js +5 -0
  46. package/lib/core/constants/search.d.ts +5 -4
  47. package/lib/core/constants/search.js +4 -5
  48. package/lib/core/hooks/index.d.ts +3 -0
  49. package/lib/core/hooks/index.js +3 -0
  50. package/lib/core/hooks/menu/use-nested-menu.js +1 -1
  51. package/lib/core/hooks/search/use-feedback-tooltip.d.ts +6 -0
  52. package/lib/core/hooks/search/use-feedback-tooltip.js +26 -0
  53. package/lib/core/hooks/use-connect-mcp-button.d.ts +13 -0
  54. package/lib/core/hooks/use-connect-mcp-button.js +50 -0
  55. package/lib/core/hooks/use-mcp-config.d.ts +9 -0
  56. package/lib/core/hooks/use-mcp-config.js +27 -0
  57. package/lib/core/hooks/use-page-actions.d.ts +1 -1
  58. package/lib/core/hooks/use-page-actions.js +98 -95
  59. package/lib/core/hooks/use-product-picker.js +2 -1
  60. package/lib/core/hooks/use-tabs.d.ts +3 -2
  61. package/lib/core/hooks/use-tabs.js +115 -57
  62. package/lib/core/hooks/use-telemetry-fallback.d.ts +10 -8
  63. package/lib/core/hooks/use-telemetry-fallback.js +10 -8
  64. package/lib/core/openapi/index.d.ts +1 -0
  65. package/lib/core/styles/dark.js +4 -0
  66. package/lib/core/styles/global.js +5 -0
  67. package/lib/core/types/hooks.d.ts +2 -2
  68. package/lib/core/types/index.d.ts +1 -0
  69. package/lib/core/types/index.js +1 -0
  70. package/lib/core/types/l10n.d.ts +1 -1
  71. package/lib/core/types/mcp.d.ts +6 -0
  72. package/lib/core/types/mcp.js +3 -0
  73. package/lib/core/types/search.d.ts +11 -4
  74. package/lib/core/types/search.js +6 -0
  75. package/lib/core/types/segmented.d.ts +12 -0
  76. package/lib/core/types/segmented.js +3 -0
  77. package/lib/core/utils/frontmatter-translate.d.ts +6 -0
  78. package/lib/core/utils/frontmatter-translate.js +14 -0
  79. package/lib/core/utils/index.d.ts +2 -0
  80. package/lib/core/utils/index.js +2 -0
  81. package/lib/core/utils/mcp.d.ts +2 -0
  82. package/lib/core/utils/mcp.js +31 -0
  83. package/lib/icons/AiStarsGradientIcon/AiStarsGradientIcon.js +44 -4
  84. package/lib/icons/AiStarsIcon/AiStarsIcon.js +11 -2
  85. package/lib/icons/ConnectIcon/ConnectIcon.d.ts +9 -0
  86. package/lib/icons/ConnectIcon/ConnectIcon.js +17 -0
  87. package/lib/icons/CubeIcon/CubeIcon.d.ts +9 -0
  88. package/lib/icons/CubeIcon/CubeIcon.js +17 -0
  89. package/lib/icons/HashtagIcon/HashtagIcon.d.ts +9 -0
  90. package/lib/icons/HashtagIcon/HashtagIcon.js +22 -0
  91. package/lib/icons/RedoclyIcon/RedoclyIcon.js +4 -7
  92. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.d.ts +9 -0
  93. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.js +34 -0
  94. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.d.ts +9 -0
  95. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.js +34 -0
  96. package/lib/icons/VSCodeIcon/VSCodeIcon.d.ts +9 -0
  97. package/lib/icons/VSCodeIcon/VSCodeIcon.js +17 -0
  98. package/lib/index.d.ts +1 -2
  99. package/lib/index.js +1 -2
  100. package/lib/markdoc/components/Cards/Card.js +1 -28
  101. package/lib/markdoc/components/ConnectMCP/ConnectMCP.d.ts +8 -0
  102. package/lib/markdoc/components/ConnectMCP/ConnectMCP.js +19 -0
  103. package/lib/markdoc/components/Tabs/TabList.d.ts +3 -1
  104. package/lib/markdoc/components/Tabs/TabList.js +197 -47
  105. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -1
  106. package/lib/markdoc/components/Tabs/Tabs.js +57 -12
  107. package/lib/markdoc/components/default.d.ts +1 -0
  108. package/lib/markdoc/components/default.js +1 -0
  109. package/lib/markdoc/default.d.ts +6 -0
  110. package/lib/markdoc/default.js +2 -0
  111. package/lib/markdoc/tags/card.js +0 -1
  112. package/lib/markdoc/tags/connect-mcp.d.ts +2 -0
  113. package/lib/markdoc/tags/connect-mcp.js +27 -0
  114. package/package.json +6 -6
  115. package/src/components/Buttons/AIAssistantButton.tsx +6 -2
  116. package/src/components/Buttons/ConnectMCPButton.tsx +180 -0
  117. package/src/components/Buttons/variables.ts +42 -1
  118. package/src/components/Catalog/CatalogEntity/CatalogEntityInfoBar.tsx +1 -0
  119. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  120. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  121. package/src/components/Catalog/CatalogEntityIcon.tsx +2 -1
  122. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +5 -0
  123. package/src/components/Catalog/CatalogTagsWithTooltip.tsx +1 -5
  124. package/src/components/Catalog/variables.ts +1 -1
  125. package/src/components/Dropdown/Dropdown.tsx +84 -79
  126. package/src/components/Menu/MenuItem.tsx +1 -0
  127. package/src/components/Navbar/NavbarItem.tsx +6 -5
  128. package/src/components/PageActions/PageActions.tsx +5 -1
  129. package/src/components/PageActions/variables.ts +2 -0
  130. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +3 -3
  131. package/src/components/Search/SearchAiActionButtons.tsx +76 -0
  132. package/src/components/Search/SearchAiConversationInput.tsx +61 -18
  133. package/src/components/Search/SearchAiDialog.tsx +52 -23
  134. package/src/components/Search/SearchAiMessage.tsx +172 -43
  135. package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +210 -0
  136. package/src/components/Search/SearchDialog.tsx +49 -13
  137. package/src/components/Search/SearchGroups.tsx +2 -0
  138. package/src/components/Search/variables.ts +36 -64
  139. package/src/components/Segmented/Segmented.tsx +15 -20
  140. package/src/components/Select/SelectInput.tsx +1 -0
  141. package/src/components/Select/variables.ts +2 -2
  142. package/src/components/Tag/Tag.tsx +35 -19
  143. package/src/components/Tag/variables.dark.ts +135 -36
  144. package/src/components/Tag/variables.ts +78 -61
  145. package/src/core/constants/index.ts +1 -0
  146. package/src/core/constants/mcp.ts +1 -0
  147. package/src/core/constants/search.ts +8 -4
  148. package/src/core/hooks/index.ts +3 -0
  149. package/src/core/hooks/menu/use-nested-menu.ts +2 -2
  150. package/src/core/hooks/search/use-feedback-tooltip.ts +32 -0
  151. package/src/core/hooks/use-connect-mcp-button.ts +79 -0
  152. package/src/core/hooks/use-mcp-config.ts +43 -0
  153. package/src/core/hooks/use-page-actions.ts +148 -126
  154. package/src/core/hooks/use-product-picker.ts +2 -1
  155. package/src/core/hooks/use-tabs.ts +168 -86
  156. package/src/core/hooks/use-telemetry-fallback.ts +10 -8
  157. package/src/core/openapi/index.ts +1 -0
  158. package/src/core/styles/dark.ts +4 -0
  159. package/src/core/styles/global.ts +6 -1
  160. package/src/core/types/hooks.ts +5 -1
  161. package/src/core/types/index.ts +1 -0
  162. package/src/core/types/l10n.ts +13 -0
  163. package/src/core/types/mcp.ts +8 -0
  164. package/src/core/types/search.ts +13 -4
  165. package/src/core/types/segmented.ts +14 -0
  166. package/src/core/utils/frontmatter-translate.ts +9 -0
  167. package/src/core/utils/index.ts +2 -0
  168. package/src/core/utils/mcp.ts +34 -0
  169. package/src/icons/AiStarsGradientIcon/AiStarsGradientIcon.tsx +13 -4
  170. package/src/icons/AiStarsIcon/AiStarsIcon.tsx +11 -2
  171. package/src/icons/ConnectIcon/ConnectIcon.tsx +27 -0
  172. package/src/icons/CubeIcon/CubeIcon.tsx +27 -0
  173. package/src/icons/HashtagIcon/HashtagIcon.tsx +23 -0
  174. package/src/icons/RedoclyIcon/RedoclyIcon.tsx +4 -22
  175. package/src/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.tsx +38 -0
  176. package/src/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.tsx +35 -0
  177. package/src/icons/VSCodeIcon/VSCodeIcon.tsx +29 -0
  178. package/src/index.ts +1 -2
  179. package/src/markdoc/components/Cards/Card.tsx +1 -28
  180. package/src/markdoc/components/ConnectMCP/ConnectMCP.tsx +28 -0
  181. package/src/markdoc/components/Tabs/TabList.tsx +312 -105
  182. package/src/markdoc/components/Tabs/Tabs.tsx +136 -11
  183. package/src/markdoc/components/default.ts +1 -0
  184. package/src/markdoc/default.ts +2 -0
  185. package/src/markdoc/tags/card.ts +0 -1
  186. package/src/markdoc/tags/connect-mcp.ts +25 -0
  187. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.d.ts +0 -1
  188. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.js +0 -11
  189. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.d.ts +0 -1
  190. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.js +0 -5
  191. package/lib/ext/process-scorecard.d.ts +0 -5
  192. package/lib/ext/process-scorecard.js +0 -11
  193. package/src/components/OpenApiDocs/hooks/AdditionalOverviewInfo.tsx +0 -9
  194. package/src/components/OpenApiDocs/hooks/AfterOpenApiDescription.tsx +0 -1
  195. package/src/ext/process-scorecard.ts +0 -13
@@ -1,7 +1,7 @@
1
- import React, { cloneElement, useRef } from 'react';
1
+ import React, { cloneElement, useRef, forwardRef } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
- import type { PropsWithChildren, ReactElement, JSX } from 'react';
4
+ import type { PropsWithChildren, ReactElement } from 'react';
5
5
 
6
6
  import { useOutsideClick, useControlledState } from '@redocly/theme/core/hooks';
7
7
  import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
@@ -32,84 +32,89 @@ export type DropdownProps = PropsWithChildren<{
32
32
  onClose?: () => void;
33
33
  }>;
34
34
 
35
- export function Dropdown({
36
- children,
37
- className,
38
- active,
39
- trigger,
40
- triggerEvent = 'click',
41
- closeOnClick = true,
42
- withArrow,
43
- dataAttributes,
44
- placement,
45
- alignment,
46
- onClick,
47
- onClose,
48
- }: DropdownProps): JSX.Element {
49
- const dropdownRef = useRef<HTMLDivElement | null>(null);
50
- const [isOpen, setIsOpen] = useControlledState<boolean>(false, active);
51
-
52
- const handleOpen = () => {
53
- setIsOpen(true);
54
- };
55
-
56
- const handleClose = () => {
57
- setIsOpen(false);
58
- onClose?.();
59
- };
60
-
61
- const handleChildClick = () => {
62
- handleClose();
63
- };
64
-
65
- const handleToggle = (event: React.UIEvent) => {
66
- event.stopPropagation();
67
- event.preventDefault();
68
- setIsOpen(!isOpen);
69
- };
70
-
71
- const handleKeyDown = (event: React.KeyboardEvent) => {
72
- if (event.key === 'Enter' || event.key === ' ') {
73
- handleToggle(event);
74
- }
75
- };
76
-
77
- useOutsideClick(dropdownRef, handleClose);
78
-
79
- const triggerChild = React.Children.only(trigger) as ReactElement<TriggerProps>;
80
-
81
- const dropdownTrigger = cloneElement(triggerChild, {
82
- onClick: triggerEvent === 'click' ? handleToggle : undefined,
83
- icon: withArrow ? isOpen ? <ChevronUpIcon /> : <ChevronDownIcon /> : undefined,
84
- ...(withArrow ? { iconPosition: 'right' } : {}),
85
- ...triggerChild.props,
86
- onKeyDown: triggerEvent === 'click' ? handleKeyDown : undefined,
87
- });
88
-
89
- return (
90
- <DropdownWrapper
91
- data-component-name="Dropdown/Dropdown"
92
- data-testid="dropdown"
93
- {...dataAttributes}
94
- className={className}
95
- ref={dropdownRef}
96
- onPointerEnter={triggerEvent === 'hover' ? handleOpen : undefined}
97
- onPointerLeave={triggerEvent === 'hover' ? handleClose : undefined}
98
- onClick={onClick}
99
- >
100
- {dropdownTrigger}
101
-
102
- <ChildrenWrapper
103
- placement={placement}
104
- alignment={alignment}
105
- isOpen={isOpen}
106
- onClick={closeOnClick ? handleChildClick : undefined}
35
+ export const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
36
+ (
37
+ {
38
+ children,
39
+ className,
40
+ active,
41
+ trigger,
42
+ triggerEvent = 'click',
43
+ closeOnClick = true,
44
+ withArrow,
45
+ dataAttributes,
46
+ placement,
47
+ alignment,
48
+ onClick,
49
+ onClose,
50
+ },
51
+ ref,
52
+ ) => {
53
+ const dropdownRef = useRef<HTMLDivElement | null>(null);
54
+ const [isOpen, setIsOpen] = useControlledState<boolean>(false, active);
55
+
56
+ const handleOpen = () => {
57
+ setIsOpen(true);
58
+ };
59
+
60
+ const handleClose = () => {
61
+ setIsOpen(false);
62
+ onClose?.();
63
+ };
64
+
65
+ const handleChildClick = () => {
66
+ handleClose();
67
+ };
68
+
69
+ const handleToggle = (event: React.UIEvent) => {
70
+ event.stopPropagation();
71
+ event.preventDefault();
72
+ setIsOpen(!isOpen);
73
+ };
74
+
75
+ const handleKeyDown = (event: React.KeyboardEvent) => {
76
+ if (event.key === 'Enter' || event.key === ' ') {
77
+ handleToggle(event);
78
+ }
79
+ };
80
+
81
+ useOutsideClick((ref as React.RefObject<HTMLElement | null>) || dropdownRef, handleClose);
82
+
83
+ const triggerChild = React.Children.only(trigger) as ReactElement<TriggerProps>;
84
+
85
+ const dropdownTrigger = cloneElement(triggerChild, {
86
+ onClick: triggerEvent === 'click' ? handleToggle : undefined,
87
+ icon: withArrow ? isOpen ? <ChevronUpIcon /> : <ChevronDownIcon /> : undefined,
88
+ ...(withArrow ? { iconPosition: 'right' } : {}),
89
+ ...triggerChild.props,
90
+ onKeyDown: triggerEvent === 'click' ? handleKeyDown : undefined,
91
+ });
92
+
93
+ return (
94
+ <DropdownWrapper
95
+ data-component-name="Dropdown/Dropdown"
96
+ data-testid="dropdown"
97
+ {...dataAttributes}
98
+ className={className}
99
+ ref={ref || dropdownRef}
100
+ onPointerEnter={triggerEvent === 'hover' ? handleOpen : undefined}
101
+ onPointerLeave={triggerEvent === 'hover' ? handleClose : undefined}
102
+ onClick={onClick}
107
103
  >
108
- {children}
109
- </ChildrenWrapper>
110
- </DropdownWrapper>
111
- );
112
- }
104
+ {dropdownTrigger}
105
+
106
+ <ChildrenWrapper
107
+ placement={placement}
108
+ alignment={alignment}
109
+ isOpen={isOpen}
110
+ onClick={closeOnClick ? handleChildClick : undefined}
111
+ >
112
+ {children}
113
+ </ChildrenWrapper>
114
+ </DropdownWrapper>
115
+ );
116
+ },
117
+ );
113
118
 
114
119
  const DropdownWrapper = styled.div`
115
120
  --button-gap: var(--spacing-xxs);
@@ -82,6 +82,7 @@ export function MenuItem(props: React.PropsWithChildren<MenuItemProps>): JSX.Ele
82
82
  role={item.link ? 'none' : 'link'}
83
83
  tabIndex={!item.link ? 0 : undefined}
84
84
  data-testid="menu-item-label"
85
+ data-active={item.active}
85
86
  >
86
87
  {hasChevron ? <ChevronWrapper>{chevron}</ChevronWrapper> : null}
87
88
  <MenuItemIcon icon={item.icon} srcSet={item.srcSet} />
@@ -34,12 +34,13 @@ export function NavbarItem({ navItem, className }: NavbarItemProps): JSX.Element
34
34
  if (navItem.type !== 'link' && !navItem.items) return null;
35
35
 
36
36
  const item = navItem as ResolvedNavLinkItem;
37
- const normalizedPath =
38
- (item.link && item.link !== '/' ? removeTrailingSlash(item.link) : item.link) || '';
37
+ const normalizedPath = (item.link ? removeTrailingSlash(item.link) : item.link) || '';
39
38
 
40
- const isActive =
41
- pathname ===
42
- withPathPrefix(getPathnameForLocale(normalizedPath, defaultLocale, currentLocale, locales));
39
+ const pathWithPathPrefix = withPathPrefix(
40
+ getPathnameForLocale(normalizedPath, defaultLocale, currentLocale, locales),
41
+ );
42
+
43
+ const isActive = removeTrailingSlash(pathname) === removeTrailingSlash(pathWithPathPrefix);
43
44
 
44
45
  const itemContent = (
45
46
  <NavbarMenuItem
@@ -77,7 +77,7 @@ export function PageActions(props: PageActionProps): JSX.Element | null {
77
77
  </Button>
78
78
  {actions.length > 1 ? (
79
79
  <Dropdown withArrow trigger={<Button />} placement="bottom" alignment="end">
80
- <DropdownMenu items={menuItems} />
80
+ <StyledDropdownMenu items={menuItems} />
81
81
  </Dropdown>
82
82
  ) : null}
83
83
  </ButtonGroup>
@@ -110,3 +110,7 @@ const LinkMenuItem = styled(Link)`
110
110
  text-decoration: none;
111
111
  --link-decoration-hover: none;
112
112
  `;
113
+
114
+ const StyledDropdownMenu = styled(DropdownMenu)`
115
+ --dropdown-menu-max-height: var(--page-actions-dropdown-max-height);
116
+ `;
@@ -7,6 +7,8 @@ export const pageActions = css`
7
7
  --page-actions-button-text-color: var(--text-color-secondary);
8
8
  --page-actions-button-padding: 5px 14px 5px var(--spacing-sm);
9
9
 
10
+ --page-actions-dropdown-max-height: fit-content;
11
+
10
12
  --page-actions-menu-item-padding: 3px 0;
11
13
  --page-actions-menu-item-gap: var(--spacing-xs);
12
14
  --page-actions-menu-item-icon-color: var(--icon-color-secondary);
@@ -3,7 +3,7 @@ import styled from 'styled-components';
3
3
 
4
4
  import type { SearchFacet, SearchFacetCount } from '@redocly/theme/core/types';
5
5
 
6
- import { Tag, type TagProps } from '@redocly/theme/components/Tag/Tag';
6
+ import { Tag } from '@redocly/theme/components/Tag/Tag';
7
7
 
8
8
  type SearchFilterFieldTagsProps = {
9
9
  className?: string;
@@ -47,6 +47,7 @@ export function SearchFilterFieldTags({
47
47
  }}
48
48
  active={active}
49
49
  borderless
50
+ selectable
50
51
  >
51
52
  {value} {isCounterVisible && <span>{count}</span>}
52
53
  </FilterTagWrapper>
@@ -62,9 +63,8 @@ const FilterTagsWrapper = styled.div`
62
63
  gap: var(--search-filter-field-tags-gap);
63
64
  `;
64
65
 
65
- const FilterTagWrapper = styled(Tag)<{ color: TagProps['color'] }>`
66
+ const FilterTagWrapper = styled(Tag)`
66
67
  text-transform: uppercase;
67
68
  cursor: pointer;
68
- ${({ color }) => color && `background-color: var(--tag-operation-bg-color-${color});`}
69
69
  margin: var(--search-filter-field-tags-tag-margin);
70
70
  `;
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+
6
+ import { FeedbackType } from '@redocly/theme/core/types';
7
+ import { Button } from '@redocly/theme/components/Button/Button';
8
+ import { ThumbUpIcon } from '@redocly/theme/icons/ThumbUpIcon/ThumbUpIcon';
9
+ import { ThumbUpFilledIcon } from '@redocly/theme/icons/ThumbUpFilledIcon/ThumbUpFilledIcon';
10
+ import { ThumbDownIcon } from '@redocly/theme/icons/ThumbDownIcon/ThumbDownIcon';
11
+ import { ThumbDownFilledIcon } from '@redocly/theme/icons/ThumbDownFilledIcon/ThumbDownFilledIcon';
12
+ import { CopyButton } from '@redocly/theme/components/Buttons/CopyButton';
13
+
14
+ export type SearchAiActionButtonsProps = {
15
+ content: string;
16
+ className?: string;
17
+ feedback?: FeedbackType;
18
+ onFeedback: (feedback: FeedbackType) => void;
19
+ disabled?: boolean;
20
+ };
21
+
22
+ export function SearchAiActionButtons({
23
+ content,
24
+ className,
25
+ feedback,
26
+ onFeedback,
27
+ disabled,
28
+ }: SearchAiActionButtonsProps): JSX.Element {
29
+ return (
30
+ <ActionButtonsWrapper className={className} data-component-name="Search/SearchAiActionButtons">
31
+ <CopyButton data={content} />
32
+ <FeedbackButton
33
+ variant="text"
34
+ size="small"
35
+ icon={feedback === FeedbackType.Like ? <ThumbUpFilledIcon /> : <ThumbUpIcon />}
36
+ onClick={() => !disabled && onFeedback(FeedbackType.Like)}
37
+ extraClass={feedback === FeedbackType.Like ? 'active' : ''}
38
+ aria-label="Like this response"
39
+ disabled={disabled}
40
+ />
41
+
42
+ <FeedbackButton
43
+ variant="text"
44
+ size="small"
45
+ icon={feedback === FeedbackType.Dislike ? <ThumbDownFilledIcon /> : <ThumbDownIcon />}
46
+ onClick={() => !disabled && onFeedback(FeedbackType.Dislike)}
47
+ extraClass={feedback === FeedbackType.Dislike ? 'active' : ''}
48
+ aria-label="Dislike this response"
49
+ disabled={disabled}
50
+ />
51
+ </ActionButtonsWrapper>
52
+ );
53
+ }
54
+
55
+ const ActionButtonsWrapper = styled.div`
56
+ display: flex;
57
+ align-items: center;
58
+ gap: var(--search-ai-feedback-gap);
59
+ `;
60
+
61
+ const FeedbackButton = styled(Button)`
62
+ &:disabled {
63
+ pointer-events: none;
64
+ cursor: default;
65
+ opacity: 1;
66
+ background-color: var(--button-bg-color);
67
+ color: var(--button-color);
68
+ border-color: var(--button-border-color);
69
+ }
70
+
71
+ &:disabled.active {
72
+ background-color: var(--button-bg-color-active);
73
+ border-color: var(--button-border-color-active);
74
+ color: var(--button-color-active);
75
+ }
76
+ `;
@@ -13,6 +13,8 @@ type SearchAiConversationInputProps = {
13
13
  isGeneratingResponse: boolean;
14
14
  placeholder?: string;
15
15
  className?: string;
16
+ disabled?: boolean;
17
+ multiline?: boolean;
16
18
  };
17
19
 
18
20
  export function SearchAiConversationInput({
@@ -20,11 +22,13 @@ export function SearchAiConversationInput({
20
22
  onMessageSent,
21
23
  className,
22
24
  placeholder,
25
+ disabled,
26
+ multiline = false,
23
27
  }: SearchAiConversationInputProps): JSX.Element {
24
28
  const { useTranslate } = useThemeHooks();
25
29
  const { translate } = useTranslate();
26
30
 
27
- const inputRef = useRef<HTMLInputElement>(null);
31
+ const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
28
32
  const [query, setQuery] = useState('');
29
33
 
30
34
  useEffect(() => {
@@ -36,35 +40,54 @@ export function SearchAiConversationInput({
36
40
  }, [isGeneratingResponse]);
37
41
 
38
42
  const handleSendMessage = () => {
43
+ if (disabled) return;
44
+
39
45
  setQuery('');
40
46
  onMessageSent(query);
41
47
  };
42
48
 
43
- const handleOnKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
49
+ const handleOnKeyDown = (
50
+ e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>,
51
+ ): void => {
44
52
  if (e.key === 'Enter' && !isGeneratingResponse) {
53
+ if (multiline && e.shiftKey) {
54
+ return; // Allow new line in textarea
55
+ }
56
+ e.preventDefault();
45
57
  handleSendMessage();
46
58
  }
47
59
  };
48
60
 
49
- const isDisabled = isGeneratingResponse || query.trim().length === 0;
61
+ const isDisabled = disabled || isGeneratingResponse || query.trim().length === 0;
62
+
63
+ const commonProps = {
64
+ placeholder:
65
+ placeholder || translate('search.ai.followUpQuestion', 'Ask a follow up question?'),
66
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
67
+ setQuery(e.target.value),
68
+ onKeyDown: handleOnKeyDown,
69
+ value: query,
70
+ disabled: disabled || isGeneratingResponse,
71
+ maxLength: AI_SEARCH_MAX_MESSAGE_LENGTH,
72
+ };
50
73
 
51
74
  return (
52
75
  <SearchAiConversationInputWrapper
53
76
  data-component-name="Search/SearchAiConversationInput"
54
77
  className={className}
55
78
  >
56
- <ConversationInput
57
- ref={inputRef}
58
- type="text"
59
- placeholder={
60
- placeholder || translate('search.ai.followUpQuestion', 'Ask a follow up question?')
61
- }
62
- onChange={(e) => setQuery(e.target.value)}
63
- onKeyUp={handleOnKeyUp}
64
- value={query}
65
- disabled={isGeneratingResponse}
66
- maxLength={AI_SEARCH_MAX_MESSAGE_LENGTH}
67
- />
79
+ {multiline ? (
80
+ <ConversationTextarea
81
+ ref={inputRef as React.RefObject<HTMLTextAreaElement>}
82
+ {...commonProps}
83
+ />
84
+ ) : (
85
+ <ConversationInput
86
+ ref={inputRef as React.RefObject<HTMLInputElement>}
87
+ type="text"
88
+ {...commonProps}
89
+ />
90
+ )}
68
91
 
69
92
  <SendButton
70
93
  disabled={isDisabled}
@@ -93,9 +116,10 @@ const SearchAiConversationInputWrapper = styled.div`
93
116
  position: relative;
94
117
  `;
95
118
 
96
- const ConversationInput = styled.input`
119
+ const inputStyles = `
97
120
  width: 100%;
98
121
  padding: var(--search-ai-conversation-input-padding);
122
+ padding-right: var(--search-ai-conversation-input-padding-right, 52px);
99
123
  border: var(--search-ai-conversation-input-border);
100
124
  border-radius: var(--search-ai-conversation-input-border-radius);
101
125
  background-color: var(--search-ai-conversation-input-bg-color);
@@ -111,21 +135,40 @@ const ConversationInput = styled.input`
111
135
  border-color: var(--search-ai-conversation-input-border-color-focus);
112
136
  }
113
137
 
138
+ &:disabled {
139
+ background-color: var(--search-ai-conversation-input-bg-color-disabled);
140
+ }
141
+
114
142
  &:focus:disabled {
115
143
  border-color: var(--search-ai-conversation-input-border-color-disabled);
116
144
  }
117
145
  `;
118
146
 
147
+ const ConversationInput = styled.input`
148
+ ${inputStyles}
149
+ height: var(--search-ai-conversation-input-min-height);
150
+ `;
151
+
152
+ const ConversationTextarea = styled.textarea`
153
+ ${inputStyles}
154
+ min-height: var(--search-ai-conversation-input-min-height);
155
+ max-height: var(--search-ai-conversation-input-max-height);
156
+ resize: none;
157
+ field-sizing: content;
158
+ `;
159
+
119
160
  const SendButton = styled(Button)`
120
161
  position: absolute;
121
162
  right: var(--search-ai-conversation-input-send-button-right);
122
- top: 50%;
123
- transform: translateY(-50%);
163
+ bottom: var(--search-ai-conversation-input-send-button-bottom);
164
+ transform: translateY(50%);
124
165
  transition: background-color 0.2s ease;
125
166
  background-color: var(--search-ai-conversation-input-send-button-bg-color);
126
167
  display: flex;
127
168
  align-items: center;
128
169
  justify-content: center;
170
+ border-radius: var(--search-ai-conversation-input-send-button-border-radius);
171
+ padding: var(--search-ai-conversation-input-send-button-padding);
129
172
 
130
173
  &:hover {
131
174
  background-color: var(--search-ai-conversation-input-send-button-bg-color-hover);
@@ -1,7 +1,12 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useCallback } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
+ import type {
6
+ AiSearchConversationItem,
7
+ SearchAiMessageResource,
8
+ FeedbackType,
9
+ } from '@redocly/theme/core/types';
5
10
 
6
11
  import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks';
7
12
  import { Button } from '@redocly/theme/components/Button/Button';
@@ -11,7 +16,6 @@ import {
11
16
  AI_SEARCH_ERROR_CONFIG as ERROR_CONFIG,
12
17
  AiSearchConversationRole,
13
18
  } from '@redocly/theme/core/constants';
14
- import { AiSearchConversationItem } from '@redocly/theme/core/types';
15
19
  import { SearchAiMessage } from '@redocly/theme/components/Search/SearchAiMessage';
16
20
  import { Admonition } from '@redocly/theme/components/Admonition/Admonition';
17
21
  import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
@@ -20,15 +24,16 @@ export type SearchAiDialogProps = {
20
24
  response: string | undefined;
21
25
  isGeneratingResponse: boolean;
22
26
  error: AiSearchError | null;
23
- resources: {
24
- url: string;
25
- title: string;
26
- }[];
27
+ resources: SearchAiMessageResource[];
27
28
  initialMessage?: string;
28
29
  className?: string;
29
30
  conversation: AiSearchConversationItem[];
30
31
  setConversation: React.Dispatch<React.SetStateAction<AiSearchConversationItem[]>>;
31
- onMessageSent: (message: string, history?: AiSearchConversationItem[]) => void;
32
+ onMessageSent: (
33
+ message: string,
34
+ history?: AiSearchConversationItem[],
35
+ messageId?: string,
36
+ ) => void;
32
37
  };
33
38
 
34
39
  export function SearchAiDialog({
@@ -56,21 +61,28 @@ export function SearchAiDialog({
56
61
  ? translate('search.ai.followUpQuestion', 'Ask a follow up question?')
57
62
  : translate('search.ai.placeholder', 'Ask a question...');
58
63
 
59
- const scrollToBottom = () => {
64
+ const scrollToBottom = useCallback(() => {
60
65
  conversationEndRef.current?.scrollIntoView({ block: 'end' });
61
- };
66
+ }, []);
62
67
 
63
- const handleOnMessageSent = (message: string) => {
64
- if (!message.trim()) {
65
- return;
66
- }
67
- const mappedHistory = conversation.map(({ role, content }) => ({
68
- role,
69
- content,
70
- }));
71
- onMessageSent(message, mappedHistory);
72
- setConversation((prev) => [...prev, { role: AiSearchConversationRole.USER, content: message }]);
73
- };
68
+ const handleOnMessageSent = useCallback(
69
+ (message: string) => {
70
+ if (!message.trim()) {
71
+ return;
72
+ }
73
+ const mappedHistory = conversation.map(({ role, content }) => ({
74
+ role,
75
+ content,
76
+ }));
77
+
78
+ onMessageSent(message, mappedHistory);
79
+ setConversation((prev) => [
80
+ ...prev,
81
+ { role: AiSearchConversationRole.USER, content: message },
82
+ ]);
83
+ },
84
+ [conversation, onMessageSent, setConversation],
85
+ );
74
86
 
75
87
  useEffect(() => {
76
88
  if (!initialMessage?.trim().length) {
@@ -94,11 +106,16 @@ export function SearchAiDialog({
94
106
  if (lastMessage && lastMessage.role === AiSearchConversationRole.ASSISTANT) {
95
107
  return [
96
108
  ...prev.slice(0, -1),
97
- { role: AiSearchConversationRole.ASSISTANT, content, resources },
109
+ {
110
+ role: AiSearchConversationRole.ASSISTANT,
111
+ content,
112
+ resources,
113
+ messageId: lastMessage.messageId,
114
+ },
98
115
  ];
99
116
  }
100
117
 
101
- return [...prev, { role: AiSearchConversationRole.ASSISTANT, content }];
118
+ return [...prev, { role: AiSearchConversationRole.ASSISTANT, content, resources }];
102
119
  });
103
120
  }, [response, conversation.length, error, resources, setConversation]);
104
121
 
@@ -110,7 +127,16 @@ export function SearchAiDialog({
110
127
 
111
128
  useEffect(() => {
112
129
  scrollToBottom();
113
- }, [conversation, isGeneratingResponse]);
130
+ }, [conversation, isGeneratingResponse, scrollToBottom]);
131
+
132
+ const handleFeedbackChange = useCallback(
133
+ (messageId: string, feedback: FeedbackType | undefined) => {
134
+ setConversation((prev) =>
135
+ prev.map((item) => (item.messageId === messageId ? { ...item, feedback } : item)),
136
+ );
137
+ },
138
+ [setConversation],
139
+ );
114
140
 
115
141
  return (
116
142
  <SearchAiDialogWrapper data-component-name="Search/SearchAiDialog" className={className}>
@@ -142,6 +168,9 @@ export function SearchAiDialog({
142
168
  index === conversation.length - 1
143
169
  }
144
170
  resources={item.resources}
171
+ messageId={item.messageId}
172
+ feedback={item.feedback}
173
+ onFeedbackChange={handleFeedbackChange}
145
174
  />
146
175
  ))}
147
176