@redocly/theme 0.59.0-next.7 → 0.59.0-next.9

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 (45) hide show
  1. package/lib/components/Buttons/AIAssistantButton.js +1 -1
  2. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +4 -0
  3. package/lib/components/Search/SearchAiActionButtons.d.ts +10 -0
  4. package/lib/components/Search/SearchAiActionButtons.js +43 -0
  5. package/lib/components/Search/SearchAiDialog.d.ts +3 -6
  6. package/lib/components/Search/SearchAiDialog.js +20 -9
  7. package/lib/components/Search/SearchAiMessage.d.ts +9 -5
  8. package/lib/components/Search/SearchAiMessage.js +146 -22
  9. package/lib/components/Search/SearchAiNegativeFeedbackForm.d.ts +8 -0
  10. package/lib/components/Search/SearchAiNegativeFeedbackForm.js +169 -0
  11. package/lib/components/Search/variables.js +29 -66
  12. package/lib/core/hooks/index.d.ts +1 -0
  13. package/lib/core/hooks/index.js +1 -0
  14. package/lib/core/hooks/search/use-feedback-tooltip.d.ts +6 -0
  15. package/lib/core/hooks/search/use-feedback-tooltip.js +26 -0
  16. package/lib/core/hooks/use-telemetry-fallback.d.ts +1 -0
  17. package/lib/core/hooks/use-telemetry-fallback.js +1 -0
  18. package/lib/core/types/l10n.d.ts +1 -1
  19. package/lib/core/types/search.d.ts +11 -4
  20. package/lib/core/types/search.js +6 -0
  21. package/lib/core/utils/frontmatter-translate.d.ts +6 -0
  22. package/lib/core/utils/frontmatter-translate.js +14 -0
  23. package/lib/core/utils/index.d.ts +1 -0
  24. package/lib/core/utils/index.js +1 -0
  25. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.d.ts +9 -0
  26. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.js +34 -0
  27. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.d.ts +9 -0
  28. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.js +34 -0
  29. package/package.json +3 -3
  30. package/src/components/Buttons/AIAssistantButton.tsx +1 -1
  31. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +5 -0
  32. package/src/components/Search/SearchAiActionButtons.tsx +76 -0
  33. package/src/components/Search/SearchAiDialog.tsx +52 -23
  34. package/src/components/Search/SearchAiMessage.tsx +172 -43
  35. package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +210 -0
  36. package/src/components/Search/variables.ts +29 -66
  37. package/src/core/hooks/index.ts +1 -0
  38. package/src/core/hooks/search/use-feedback-tooltip.ts +32 -0
  39. package/src/core/hooks/use-telemetry-fallback.ts +1 -0
  40. package/src/core/types/l10n.ts +3 -0
  41. package/src/core/types/search.ts +13 -4
  42. package/src/core/utils/frontmatter-translate.ts +9 -0
  43. package/src/core/utils/index.ts +1 -0
  44. package/src/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.tsx +38 -0
  45. package/src/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.tsx +35 -0
@@ -49,7 +49,7 @@ const RedoclyIcon_1 = require("../../icons/RedoclyIcon/RedoclyIcon");
49
49
  const defaultConfig = {
50
50
  hide: false,
51
51
  inputType: 'button',
52
- inputIcon: 'redocly',
52
+ inputIcon: 'sparkles',
53
53
  };
54
54
  const getIcon = (iconType, inputType = 'button') => {
55
55
  const iconSize = inputType === 'icon'
@@ -17,6 +17,10 @@ const filterComponents = {
17
17
  function CatalogFilter({ filter, filterValuesCasing, showCounter = true, className, }) {
18
18
  if (!filter.parentUsed)
19
19
  return null;
20
+ const filteredOptions = filter.filteredOptions || filter.options;
21
+ if (!filteredOptions || filteredOptions.length === 0) {
22
+ return null;
23
+ }
20
24
  const FilterComponent = filterComponents[(filter.type || 'checkboxes')];
21
25
  return (react_1.default.createElement(CatalogFilterGroup, { className: className, "data-component-name": "Catalog/CatalogFilter", key: filter.property + filter.title },
22
26
  react_1.default.createElement(FilterComponent, { filter: filter, filterValuesCasing: filterValuesCasing, showCounter: showCounter })));
@@ -0,0 +1,10 @@
1
+ import type { JSX } from 'react';
2
+ import { FeedbackType } from '../../core/types';
3
+ export type SearchAiActionButtonsProps = {
4
+ content: string;
5
+ className?: string;
6
+ feedback?: FeedbackType;
7
+ onFeedback: (feedback: FeedbackType) => void;
8
+ disabled?: boolean;
9
+ };
10
+ export declare function SearchAiActionButtons({ content, className, feedback, onFeedback, disabled, }: SearchAiActionButtonsProps): JSX.Element;
@@ -0,0 +1,43 @@
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.SearchAiActionButtons = SearchAiActionButtons;
7
+ const react_1 = __importDefault(require("react"));
8
+ const styled_components_1 = __importDefault(require("styled-components"));
9
+ const types_1 = require("../../core/types");
10
+ const Button_1 = require("../../components/Button/Button");
11
+ const ThumbUpIcon_1 = require("../../icons/ThumbUpIcon/ThumbUpIcon");
12
+ const ThumbUpFilledIcon_1 = require("../../icons/ThumbUpFilledIcon/ThumbUpFilledIcon");
13
+ const ThumbDownIcon_1 = require("../../icons/ThumbDownIcon/ThumbDownIcon");
14
+ const ThumbDownFilledIcon_1 = require("../../icons/ThumbDownFilledIcon/ThumbDownFilledIcon");
15
+ const CopyButton_1 = require("../../components/Buttons/CopyButton");
16
+ function SearchAiActionButtons({ content, className, feedback, onFeedback, disabled, }) {
17
+ return (react_1.default.createElement(ActionButtonsWrapper, { className: className, "data-component-name": "Search/SearchAiActionButtons" },
18
+ react_1.default.createElement(CopyButton_1.CopyButton, { data: content }),
19
+ react_1.default.createElement(FeedbackButton, { variant: "text", size: "small", icon: feedback === types_1.FeedbackType.Like ? react_1.default.createElement(ThumbUpFilledIcon_1.ThumbUpFilledIcon, null) : react_1.default.createElement(ThumbUpIcon_1.ThumbUpIcon, null), onClick: () => !disabled && onFeedback(types_1.FeedbackType.Like), extraClass: feedback === types_1.FeedbackType.Like ? 'active' : '', "aria-label": "Like this response", disabled: disabled }),
20
+ react_1.default.createElement(FeedbackButton, { variant: "text", size: "small", icon: feedback === types_1.FeedbackType.Dislike ? react_1.default.createElement(ThumbDownFilledIcon_1.ThumbDownFilledIcon, null) : react_1.default.createElement(ThumbDownIcon_1.ThumbDownIcon, null), onClick: () => !disabled && onFeedback(types_1.FeedbackType.Dislike), extraClass: feedback === types_1.FeedbackType.Dislike ? 'active' : '', "aria-label": "Dislike this response", disabled: disabled })));
21
+ }
22
+ const ActionButtonsWrapper = styled_components_1.default.div `
23
+ display: flex;
24
+ align-items: center;
25
+ gap: var(--search-ai-feedback-gap);
26
+ `;
27
+ const FeedbackButton = (0, styled_components_1.default)(Button_1.Button) `
28
+ &:disabled {
29
+ pointer-events: none;
30
+ cursor: default;
31
+ opacity: 1;
32
+ background-color: var(--button-bg-color);
33
+ color: var(--button-color);
34
+ border-color: var(--button-border-color);
35
+ }
36
+
37
+ &:disabled.active {
38
+ background-color: var(--button-bg-color-active);
39
+ border-color: var(--button-border-color-active);
40
+ color: var(--button-color-active);
41
+ }
42
+ `;
43
+ //# sourceMappingURL=SearchAiActionButtons.js.map
@@ -1,19 +1,16 @@
1
1
  import React from 'react';
2
2
  import type { JSX } from 'react';
3
+ import type { AiSearchConversationItem, SearchAiMessageResource } from '../../core/types';
3
4
  import { AiSearchError } from '../../core/constants';
4
- import { AiSearchConversationItem } from '../../core/types';
5
5
  export type SearchAiDialogProps = {
6
6
  response: string | undefined;
7
7
  isGeneratingResponse: boolean;
8
8
  error: AiSearchError | null;
9
- resources: {
10
- url: string;
11
- title: string;
12
- }[];
9
+ resources: SearchAiMessageResource[];
13
10
  initialMessage?: string;
14
11
  className?: string;
15
12
  conversation: AiSearchConversationItem[];
16
13
  setConversation: React.Dispatch<React.SetStateAction<AiSearchConversationItem[]>>;
17
- onMessageSent: (message: string, history?: AiSearchConversationItem[]) => void;
14
+ onMessageSent: (message: string, history?: AiSearchConversationItem[], messageId?: string) => void;
18
15
  };
19
16
  export declare function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error, resources, onMessageSent, className, conversation, setConversation, }: SearchAiDialogProps): JSX.Element;
@@ -57,11 +57,11 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
57
57
  : conversation.length > 0
58
58
  ? translate('search.ai.followUpQuestion', 'Ask a follow up question?')
59
59
  : translate('search.ai.placeholder', 'Ask a question...');
60
- const scrollToBottom = () => {
60
+ const scrollToBottom = (0, react_1.useCallback)(() => {
61
61
  var _a;
62
62
  (_a = conversationEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'end' });
63
- };
64
- const handleOnMessageSent = (message) => {
63
+ }, []);
64
+ const handleOnMessageSent = (0, react_1.useCallback)((message) => {
65
65
  if (!message.trim()) {
66
66
  return;
67
67
  }
@@ -70,8 +70,11 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
70
70
  content,
71
71
  }));
72
72
  onMessageSent(message, mappedHistory);
73
- setConversation((prev) => [...prev, { role: constants_1.AiSearchConversationRole.USER, content: message }]);
74
- };
73
+ setConversation((prev) => [
74
+ ...prev,
75
+ { role: constants_1.AiSearchConversationRole.USER, content: message },
76
+ ]);
77
+ }, [conversation, onMessageSent, setConversation]);
75
78
  (0, react_1.useEffect)(() => {
76
79
  if (!(initialMessage === null || initialMessage === void 0 ? void 0 : initialMessage.trim().length)) {
77
80
  return;
@@ -91,10 +94,15 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
91
94
  if (lastMessage && lastMessage.role === constants_1.AiSearchConversationRole.ASSISTANT) {
92
95
  return [
93
96
  ...prev.slice(0, -1),
94
- { role: constants_1.AiSearchConversationRole.ASSISTANT, content, resources },
97
+ {
98
+ role: constants_1.AiSearchConversationRole.ASSISTANT,
99
+ content,
100
+ resources,
101
+ messageId: lastMessage.messageId,
102
+ },
95
103
  ];
96
104
  }
97
- return [...prev, { role: constants_1.AiSearchConversationRole.ASSISTANT, content }];
105
+ return [...prev, { role: constants_1.AiSearchConversationRole.ASSISTANT, content, resources }];
98
106
  });
99
107
  }, [response, conversation.length, error, resources, setConversation]);
100
108
  (0, react_1.useEffect)(() => {
@@ -104,7 +112,10 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
104
112
  }, [error, setConversation]);
105
113
  (0, react_1.useEffect)(() => {
106
114
  scrollToBottom();
107
- }, [conversation, isGeneratingResponse]);
115
+ }, [conversation, isGeneratingResponse, scrollToBottom]);
116
+ const handleFeedbackChange = (0, react_1.useCallback)((messageId, feedback) => {
117
+ setConversation((prev) => prev.map((item) => (item.messageId === messageId ? Object.assign(Object.assign({}, item), { feedback }) : item)));
118
+ }, [setConversation]);
108
119
  return (react_1.default.createElement(SearchAiDialogWrapper, { "data-component-name": "Search/SearchAiDialog", className: className },
109
120
  !conversation.length && (react_1.default.createElement(WelcomeWrapper, null,
110
121
  react_1.default.createElement(AiStarsIcon_1.AiStarsIcon, { color: "var(--search-ai-icon-color)", size: "32px", background: "var(--search-ai-icon-bg-color)", borderRadius: "var(--border-radius-lg)", margin: "0 var(--spacing-xs) 0 0" }),
@@ -112,7 +123,7 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
112
123
  react_1.default.createElement(ConversationWrapper, null,
113
124
  conversation.map((item, index) => (react_1.default.createElement(SearchAiMessage_1.SearchAiMessage, { key: `search-ai-message-${index}`, role: item.role, content: item.content, isThinking: item.role === constants_1.AiSearchConversationRole.ASSISTANT &&
114
125
  isGeneratingResponse &&
115
- index === conversation.length - 1, resources: item.resources }))),
126
+ index === conversation.length - 1, resources: item.resources, messageId: item.messageId, feedback: item.feedback, onFeedbackChange: handleFeedbackChange }))),
116
127
  error && (react_1.default.createElement(Admonition_1.Admonition, { type: "danger", name: translate(constants_1.AI_SEARCH_ERROR_CONFIG[error].headerKey, constants_1.AI_SEARCH_ERROR_CONFIG[error].headerDefault) }, translate(constants_1.AI_SEARCH_ERROR_CONFIG[error].messageKey, constants_1.AI_SEARCH_ERROR_CONFIG[error].messageDefault))),
117
128
  !conversation.length && !error && (react_1.default.createElement(SuggestionsWrapper, null, suggestions === null || suggestions === void 0 ? void 0 : suggestions.map((suggestion) => (react_1.default.createElement(Button_1.Button, { key: suggestion, variant: "outlined", onClick: () => handleOnMessageSent(suggestion) }, suggestion))))),
118
129
  react_1.default.createElement("div", { ref: conversationEndRef })),
@@ -1,13 +1,17 @@
1
+ import React from 'react';
1
2
  import type { JSX } from 'react';
3
+ import { FeedbackType, type SearchAiMessageResource } from '../../core/types';
2
4
  import { AiSearchConversationRole } from '../../core/constants';
3
5
  export type SearchAiMessageProps = {
4
6
  role: AiSearchConversationRole;
5
7
  content: string;
6
8
  isThinking?: boolean;
7
- resources?: {
8
- url: string;
9
- title: string;
10
- }[];
9
+ resources?: SearchAiMessageResource[];
11
10
  className?: string;
11
+ messageId?: string;
12
+ feedback?: FeedbackType;
13
+ onFeedbackChange: (messageId: string, feedback: FeedbackType | undefined) => void;
12
14
  };
13
- export declare function SearchAiMessage({ role, content, isThinking, resources, className, }: SearchAiMessageProps): JSX.Element;
15
+ declare function SearchAiMessageComponent({ role, content, isThinking, resources, className, messageId, feedback, onFeedbackChange, }: SearchAiMessageProps): JSX.Element;
16
+ export declare const SearchAiMessage: React.MemoExoticComponent<typeof SearchAiMessageComponent>;
17
+ export {};
@@ -1,11 +1,45 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SearchAiMessage = SearchAiMessage;
7
- const react_1 = __importDefault(require("react"));
39
+ exports.SearchAiMessage = void 0;
40
+ const react_1 = __importStar(require("react"));
8
41
  const styled_components_1 = __importDefault(require("styled-components"));
42
+ const types_1 = require("../../core/types");
9
43
  const Link_1 = require("../../components/Link/Link");
10
44
  const Tag_1 = require("../../components/Tag/Tag");
11
45
  const constants_1 = require("../../core/constants");
@@ -13,29 +47,91 @@ const hooks_1 = require("../../core/hooks");
13
47
  const Markdown_1 = require("../../components/Markdown/Markdown");
14
48
  const DocumentIcon_1 = require("../../icons/DocumentIcon/DocumentIcon");
15
49
  const AiStarsIcon_1 = require("../../icons/AiStarsIcon/AiStarsIcon");
16
- function SearchAiMessage({ role, content, isThinking, resources, className, }) {
17
- const { useMarkdownText, useTranslate } = (0, hooks_1.useThemeHooks)();
50
+ const CheckmarkOutlineIcon_1 = require("../../icons/CheckmarkOutlineIcon/CheckmarkOutlineIcon");
51
+ const SearchAiActionButtons_1 = require("./SearchAiActionButtons");
52
+ const SearchAiNegativeFeedbackForm_1 = require("./SearchAiNegativeFeedbackForm");
53
+ function SearchAiMessageComponent({ role, content, isThinking, resources, className, messageId, feedback, onFeedbackChange, }) {
54
+ var _a;
55
+ const { useMarkdownText, useTranslate, useTelemetry } = (0, hooks_1.useThemeHooks)();
18
56
  const markDownContent = useMarkdownText(content || '');
19
57
  const { translate } = useTranslate();
58
+ const telemetry = useTelemetry();
59
+ const [feedbackSent, setFeedbackSent] = (0, react_1.useState)(false);
60
+ const hasResources = !isThinking && resources && resources.length > 0;
61
+ const resourcesCount = (_a = resources === null || resources === void 0 ? void 0 : resources.length) !== null && _a !== void 0 ? _a : 0;
62
+ const showSuccessMessage = feedbackSent && feedback;
63
+ const sendFeedbackTelemetry = (feedbackValue, dislikeReason) => {
64
+ if (!messageId)
65
+ return;
66
+ try {
67
+ telemetry.sendSearchAIFeedbackMessage({
68
+ feedback: feedbackValue,
69
+ messageId,
70
+ reason: dislikeReason,
71
+ });
72
+ }
73
+ catch (error) {
74
+ console.error('Error sending feedback', error);
75
+ }
76
+ };
77
+ const handleFeedbackClick = (feedbackValue, reason) => {
78
+ if (!messageId) {
79
+ return;
80
+ }
81
+ if (!reason) {
82
+ onFeedbackChange(messageId, feedbackValue);
83
+ }
84
+ sendFeedbackTelemetry(feedbackValue, reason);
85
+ if (feedbackValue === types_1.FeedbackType.Like || reason) {
86
+ setFeedbackSent(true);
87
+ }
88
+ };
20
89
  return (react_1.default.createElement(SearchAiMessageWrapper, { "data-component-name": "Search/SearchAiMessage", role: role, className: className, "data-testid": "search-ai-message" },
21
90
  role === constants_1.AiSearchConversationRole.ASSISTANT && (react_1.default.createElement(AiStarsIcon_1.AiStarsIcon, { size: "32px", background: "var(--search-ai-icon-bg-color)", borderRadius: "var(--border-radius-lg)", color: "var(--search-ai-icon-color)", margin: "0 var(--spacing-xs) 0 0" })),
22
- react_1.default.createElement(MessageWrapper, { role: role },
23
- role === constants_1.AiSearchConversationRole.ASSISTANT ? (react_1.default.createElement(react_1.default.Fragment, null,
24
- react_1.default.createElement(ResponseText, { as: "div", children: markDownContent, "data-testid": "response-text" }),
25
- !isThinking && resources && resources.length > 0 && (react_1.default.createElement(ResourcesWrapper, { "data-testid": "resources-wrapper" },
26
- react_1.default.createElement(ResourcesTitle, { "data-translation-key": "search.ai.resourcesFound" },
27
- translate('search.ai.resourcesFound.basedOn', 'Based on'),
28
- " ",
29
- resources.length,
30
- ' ',
31
- translate('search.ai.resourcesFound.resources', 'resources')),
32
- react_1.default.createElement(ResourceTagsWrapper, null, resources.map((resource, idx) => (react_1.default.createElement(Link_1.Link, { key: idx, to: resource.url, target: "_blank" },
33
- react_1.default.createElement(ResourceTag, { borderless: true, icon: react_1.default.createElement(DocumentIcon_1.DocumentIcon, { color: "--search-ai-resource-tag-icon-color" }) }, resource.title))))))))) : (content),
34
- isThinking && content.length === 0 && (react_1.default.createElement(ThinkingDotsWrapper, { "data-testid": "thinking-dots-wrapper" },
35
- react_1.default.createElement(ThinkingDot, null),
36
- react_1.default.createElement(ThinkingDot, null),
37
- react_1.default.createElement(ThinkingDot, null))))));
91
+ react_1.default.createElement(MessageContentWrapper, null,
92
+ react_1.default.createElement(MessageWrapper, { role: role },
93
+ role === constants_1.AiSearchConversationRole.ASSISTANT ? (react_1.default.createElement(react_1.default.Fragment, null,
94
+ react_1.default.createElement(ResponseText, { as: "div", children: markDownContent, "data-testid": "response-text" }),
95
+ hasResources && (react_1.default.createElement(react_1.default.Fragment, null,
96
+ react_1.default.createElement(ResourcesWrapper, { "data-testid": "resources-wrapper" },
97
+ react_1.default.createElement(ResourcesTitle, { "data-translation-key": "search.ai.resourcesFound" },
98
+ translate('search.ai.resourcesFound.basedOn', 'Based on'),
99
+ " ",
100
+ resourcesCount,
101
+ ' ',
102
+ translate('search.ai.resourcesFound.resources', 'resources')),
103
+ react_1.default.createElement(ResourceTagsWrapper, null, resources === null || resources === void 0 ? void 0 : resources.map((resource, idx) => (react_1.default.createElement(Link_1.Link, { key: `${resource.url}-${idx}`, to: resource.url, target: "_blank" },
104
+ react_1.default.createElement(ResourceTag, { borderless: true, icon: react_1.default.createElement(DocumentIcon_1.DocumentIcon, { color: "--search-ai-resource-tag-icon-color" }) }, resource.title)))))),
105
+ react_1.default.createElement(FeedbackWrapper, null,
106
+ react_1.default.createElement(SearchAiActionButtons_1.SearchAiActionButtons, { content: content, feedback: feedback, onFeedback: handleFeedbackClick, disabled: feedbackSent })))))) : (content),
107
+ isThinking && content.length === 0 && (react_1.default.createElement(ThinkingDotsWrapper, { "data-testid": "thinking-dots-wrapper" },
108
+ react_1.default.createElement(ThinkingDot, null),
109
+ react_1.default.createElement(ThinkingDot, null),
110
+ react_1.default.createElement(ThinkingDot, null)))),
111
+ messageId && feedback === types_1.FeedbackType.Dislike && !showSuccessMessage && (react_1.default.createElement(SearchAiNegativeFeedbackForm_1.SearchAiNegativeFeedbackForm, { messageId: messageId, onClose: onFeedbackChange, onSubmit: (reason) => handleFeedbackClick(types_1.FeedbackType.Dislike, reason) })),
112
+ showSuccessMessage && (react_1.default.createElement(SuccessMessageWrapper, { "data-component-name": "Search/SearchAiMessage/Success" },
113
+ react_1.default.createElement(CheckmarkOutlineIcon_1.CheckmarkOutlineIcon, { size: "20px", color: "var(--color-success-base)" }),
114
+ react_1.default.createElement(SuccessMessageText, null, translate('search.ai.feedback.thanks', 'Thank you for your feedback!')))))));
115
+ }
116
+ function areResourcesEqual(prev, next) {
117
+ if (prev === next)
118
+ return true;
119
+ if (!prev || !next || prev.length !== next.length)
120
+ return false;
121
+ return prev.every((resource, index) => {
122
+ const nextResource = next[index];
123
+ return resource.url === nextResource.url && resource.title === nextResource.title;
124
+ });
38
125
  }
126
+ exports.SearchAiMessage = (0, react_1.memo)(SearchAiMessageComponent, (prevProps, nextProps) => {
127
+ return (prevProps.role === nextProps.role &&
128
+ prevProps.content === nextProps.content &&
129
+ prevProps.isThinking === nextProps.isThinking &&
130
+ prevProps.messageId === nextProps.messageId &&
131
+ prevProps.feedback === nextProps.feedback &&
132
+ prevProps.onFeedbackChange === nextProps.onFeedbackChange &&
133
+ areResourcesEqual(prevProps.resources, nextProps.resources));
134
+ });
39
135
  const SearchAiMessageWrapper = styled_components_1.default.div `
40
136
  display: flex;
41
137
  flex-direction: row;
@@ -43,6 +139,13 @@ const SearchAiMessageWrapper = styled_components_1.default.div `
43
139
  width: 100%;
44
140
  justify-content: ${({ role }) => role === constants_1.AiSearchConversationRole.USER ? 'flex-end' : 'flex-start'};
45
141
  `;
142
+ const MessageContentWrapper = styled_components_1.default.div `
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: var(--spacing-sm);
146
+ max-width: 80%;
147
+ min-width: 0;
148
+ `;
46
149
  const ResponseText = (0, styled_components_1.default)(Markdown_1.Markdown) `
47
150
  color: var(--search-ai-text-color);
48
151
  font-size: var(--search-ai-text-font-size);
@@ -63,7 +166,8 @@ const MessageWrapper = styled_components_1.default.div `
63
166
  padding: ${({ role }) => role === constants_1.AiSearchConversationRole.USER ? 'var(--spacing-sm)' : 'var(--spacing-xs)'}
64
167
  var(--spacing-sm);
65
168
  border-radius: var(--border-radius-lg);
66
- max-width: 80%;
169
+ width: fit-content;
170
+ max-width: 100%;
67
171
  word-wrap: break-word;
68
172
  white-space: pre-wrap;
69
173
  background-color: ${({ role }) => role === constants_1.AiSearchConversationRole.USER
@@ -78,7 +182,13 @@ const ResourcesWrapper = styled_components_1.default.div `
78
182
  gap: var(--search-ai-resources-gap);
79
183
  display: flex;
80
184
  flex-direction: column;
81
- margin: var(--spacing-xs) 0;
185
+ margin: 0;
186
+ `;
187
+ const FeedbackWrapper = styled_components_1.default.div `
188
+ display: flex;
189
+ flex-direction: row;
190
+ gap: var(--search-ai-feedback-gap);
191
+ margin-top: var(--spacing-sm);
82
192
  `;
83
193
  const ResourcesTitle = styled_components_1.default.div `
84
194
  font-weight: var(--search-ai-resources-title-font-weight);
@@ -144,4 +254,18 @@ const ThinkingDot = styled_components_1.default.div `
144
254
  }
145
255
  }
146
256
  `;
257
+ const SuccessMessageWrapper = styled_components_1.default.div `
258
+ max-width: fit-content;
259
+ display: flex;
260
+ align-items: center;
261
+ gap: var(--spacing-sm);
262
+ padding: var(--spacing-sm);
263
+ background: var(--color-success-bg);
264
+ border: 1px solid var(--color-success-border);
265
+ border-radius: var(--border-radius-lg);
266
+ `;
267
+ const SuccessMessageText = styled_components_1.default.div `
268
+ font-size: var(--font-size-base);
269
+ color: var(--color-success-darker);
270
+ `;
147
271
  //# sourceMappingURL=SearchAiMessage.js.map
@@ -0,0 +1,8 @@
1
+ import type { JSX } from 'react';
2
+ import type { FeedbackType } from '../../core/types';
3
+ export type SearchAiNegativeFeedbackFormProps = {
4
+ messageId: string;
5
+ onClose: (messageId: string, feedback: FeedbackType | undefined) => void;
6
+ onSubmit: (reason: string) => void;
7
+ };
8
+ export declare function SearchAiNegativeFeedbackForm({ messageId, onClose, onSubmit, }: SearchAiNegativeFeedbackFormProps): JSX.Element;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SearchAiNegativeFeedbackForm = SearchAiNegativeFeedbackForm;
40
+ const react_1 = __importStar(require("react"));
41
+ const styled_components_1 = __importDefault(require("styled-components"));
42
+ const Button_1 = require("../../components/Button/Button");
43
+ const CloseIcon_1 = require("../../icons/CloseIcon/CloseIcon");
44
+ const ArrowLeftIcon_1 = require("../../icons/ArrowLeftIcon/ArrowLeftIcon");
45
+ const SendIcon_1 = require("../../icons/SendIcon/SendIcon");
46
+ const hooks_1 = require("../../core/hooks");
47
+ const NEGATIVE_FEEDBACK_DEFAULT_REASONS = [
48
+ "Didn't answer my question",
49
+ "Couldn't find what I was looking for",
50
+ 'Wrong topic',
51
+ 'Partially helpful, but missing details',
52
+ ];
53
+ function SearchAiNegativeFeedbackForm({ messageId, onClose, onSubmit, }) {
54
+ const { useTranslate } = (0, hooks_1.useThemeHooks)();
55
+ const { translate } = useTranslate();
56
+ const [showMoreInput, setShowMoreInput] = (0, react_1.useState)(false);
57
+ const [detailedFeedback, setDetailedFeedback] = (0, react_1.useState)('');
58
+ const textAreaRef = react_1.default.useRef(null);
59
+ const adjustTextAreaHeight = () => {
60
+ const textArea = textAreaRef.current;
61
+ if (textArea) {
62
+ textArea.style.height = 'auto';
63
+ textArea.style.height = `${textArea.scrollHeight}px`;
64
+ }
65
+ };
66
+ const handleTextChange = (e) => {
67
+ setDetailedFeedback(e.target.value);
68
+ adjustTextAreaHeight();
69
+ };
70
+ (0, react_1.useEffect)(() => {
71
+ if (showMoreInput) {
72
+ adjustTextAreaHeight();
73
+ }
74
+ }, [showMoreInput]);
75
+ return (react_1.default.createElement(FeedbackFormWrapper, { "data-component-name": "Search/SearchAiNegativeFeedbackForm" },
76
+ react_1.default.createElement(FeedbackHeader, null,
77
+ showMoreInput ? (react_1.default.createElement(BackButton, { variant: "text", size: "small", icon: react_1.default.createElement(ArrowLeftIcon_1.ArrowLeftIcon, null), onClick: () => setShowMoreInput(false), "aria-label": "Back to feedback reasons" })) : null,
78
+ react_1.default.createElement(FeedbackTitle, { "data-translation-key": "search.ai.feedback.title" }, translate('search.ai.feedback.title', "What didn't you like about this response?")),
79
+ react_1.default.createElement(CloseButton, { variant: "text", size: "small", icon: react_1.default.createElement(CloseIcon_1.CloseIcon, null), onClick: () => onClose(messageId, undefined), "aria-label": "Close feedback form" })),
80
+ !showMoreInput ? (react_1.default.createElement(FeedbackReasonsWrapper, null,
81
+ NEGATIVE_FEEDBACK_DEFAULT_REASONS.map((reason) => (react_1.default.createElement(Button_1.Button, { key: reason, variant: "outlined", size: "small", onClick: () => onSubmit(reason) }, reason))),
82
+ react_1.default.createElement(Button_1.Button, { variant: "outlined", size: "small", onClick: () => setShowMoreInput(true) }, "More..."))) : (react_1.default.createElement(FeedbackInputWrapper, null,
83
+ react_1.default.createElement(FeedbackTextArea, { ref: textAreaRef, placeholder: translate('search.ai.feedback.detailsPlaceholder', 'Add specific details'), value: detailedFeedback, onChange: handleTextChange, rows: 1, autoFocus: true }),
84
+ react_1.default.createElement(SendButton, { size: "small", icon: react_1.default.createElement(SendIcon_1.SendIcon, { color: !detailedFeedback.trim()
85
+ ? '--button-content-color-disabled'
86
+ : 'var(--search-ai-conversation-input-send-button-icon-color)' }), onClick: () => onSubmit(detailedFeedback), disabled: !detailedFeedback.trim(), "aria-label": "Send feedback" })))));
87
+ }
88
+ const FeedbackFormWrapper = styled_components_1.default.div `
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: var(--spacing-sm);
92
+ padding: var(--spacing-xs);
93
+ background: var(--search-ai-feedback-form-bg-color);
94
+ border: 1px solid var(--search-ai-feedback-form-border-color);
95
+ border-radius: var(--border-radius-lg);
96
+ `;
97
+ const FeedbackHeader = styled_components_1.default.div `
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ gap: var(--spacing-sm);
102
+ `;
103
+ const FeedbackTitle = styled_components_1.default.div `
104
+ font-size: var(--font-size-base);
105
+ color: var(--text-color);
106
+ flex: 1;
107
+ `;
108
+ const BackButton = (0, styled_components_1.default)(Button_1.Button) `
109
+ flex-shrink: 0;
110
+ `;
111
+ const CloseButton = (0, styled_components_1.default)(Button_1.Button) `
112
+ flex-shrink: 0;
113
+ `;
114
+ const FeedbackReasonsWrapper = styled_components_1.default.div `
115
+ display: flex;
116
+ flex-wrap: wrap;
117
+ gap: var(--spacing-xs);
118
+ `;
119
+ const FeedbackInputWrapper = styled_components_1.default.div `
120
+ position: relative;
121
+ width: 100%;
122
+ `;
123
+ const FeedbackTextArea = styled_components_1.default.textarea `
124
+ width: 100%;
125
+ min-height: 5rem;
126
+ max-height: 12.5rem;
127
+ padding: var(--spacing-xs);
128
+ padding-right: 3rem;
129
+ border: 1px solid var(--border-color-primary);
130
+ border-radius: var(--border-radius-md);
131
+ font-family: inherit;
132
+ font-size: var(--font-size-base);
133
+ line-height: var(--line-height-base);
134
+ background: var(--background-color);
135
+ color: var(--text-color);
136
+ resize: none;
137
+ overflow-y: auto;
138
+
139
+ &:focus {
140
+ outline: 1px solid var(--button-border-color-focused);
141
+ border-color: var(--button-border-color-focused);
142
+ }
143
+
144
+ &::placeholder {
145
+ color: var(--text-color-helper);
146
+ }
147
+ `;
148
+ const SendButton = (0, styled_components_1.default)(Button_1.Button) `
149
+ position: absolute;
150
+ right: var(--search-ai-conversation-input-send-button-right);
151
+ bottom: var(--spacing-sm);
152
+ transition: background-color 0.2s ease;
153
+ background-color: var(--search-ai-conversation-input-send-button-bg-color);
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ border-radius: var(--search-ai-conversation-input-send-button-border-radius);
158
+ padding: var(--search-ai-conversation-input-send-button-padding);
159
+
160
+ &:hover {
161
+ background-color: var(--search-ai-conversation-input-send-button-bg-color-hover);
162
+ }
163
+
164
+ &:disabled {
165
+ background-color: var(--search-ai-conversation-input-send-button-bg-color-disabled);
166
+ border: var(--search-ai-conversation-input-send-button-border-disabled);
167
+ }
168
+ `;
169
+ //# sourceMappingURL=SearchAiNegativeFeedbackForm.js.map