@redocly/theme 0.59.0-next.6 → 0.59.0-next.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/lib/components/Search/SearchAiActionButtons.d.ts +10 -0
  2. package/lib/components/Search/SearchAiActionButtons.js +43 -0
  3. package/lib/components/Search/SearchAiDialog.d.ts +3 -6
  4. package/lib/components/Search/SearchAiDialog.js +20 -9
  5. package/lib/components/Search/SearchAiMessage.d.ts +9 -5
  6. package/lib/components/Search/SearchAiMessage.js +146 -22
  7. package/lib/components/Search/SearchAiNegativeFeedbackForm.d.ts +8 -0
  8. package/lib/components/Search/SearchAiNegativeFeedbackForm.js +169 -0
  9. package/lib/components/Search/variables.js +29 -66
  10. package/lib/core/hooks/index.d.ts +1 -0
  11. package/lib/core/hooks/index.js +1 -0
  12. package/lib/core/hooks/menu/use-nested-menu.js +1 -1
  13. package/lib/core/hooks/search/use-feedback-tooltip.d.ts +6 -0
  14. package/lib/core/hooks/search/use-feedback-tooltip.js +26 -0
  15. package/lib/core/hooks/use-product-picker.js +2 -1
  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 +2 -2
  30. package/src/components/Search/SearchAiActionButtons.tsx +76 -0
  31. package/src/components/Search/SearchAiDialog.tsx +52 -23
  32. package/src/components/Search/SearchAiMessage.tsx +172 -43
  33. package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +210 -0
  34. package/src/components/Search/variables.ts +29 -66
  35. package/src/core/hooks/index.ts +1 -0
  36. package/src/core/hooks/menu/use-nested-menu.ts +2 -2
  37. package/src/core/hooks/search/use-feedback-tooltip.ts +32 -0
  38. package/src/core/hooks/use-product-picker.ts +2 -1
  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
@@ -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
 
@@ -1,8 +1,9 @@
1
- import React from 'react';
1
+ import React, { memo, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
5
 
6
+ import { FeedbackType, type SearchAiMessageResource } from '@redocly/theme/core/types';
6
7
  import { Link } from '@redocly/theme/components/Link/Link';
7
8
  import { Tag } from '@redocly/theme/components/Tag/Tag';
8
9
  import { AiSearchConversationRole } from '@redocly/theme/core/constants';
@@ -10,28 +11,72 @@ import { useThemeHooks } from '@redocly/theme/core/hooks';
10
11
  import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
11
12
  import { DocumentIcon } from '@redocly/theme/icons/DocumentIcon/DocumentIcon';
12
13
  import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
14
+ import { CheckmarkOutlineIcon } from '@redocly/theme/icons/CheckmarkOutlineIcon/CheckmarkOutlineIcon';
15
+
16
+ import { SearchAiActionButtons } from './SearchAiActionButtons';
17
+ import { SearchAiNegativeFeedbackForm } from './SearchAiNegativeFeedbackForm';
13
18
 
14
19
  export type SearchAiMessageProps = {
15
20
  role: AiSearchConversationRole;
16
21
  content: string;
17
22
  isThinking?: boolean;
18
- resources?: {
19
- url: string;
20
- title: string;
21
- }[];
23
+ resources?: SearchAiMessageResource[];
22
24
  className?: string;
25
+ messageId?: string;
26
+ feedback?: FeedbackType;
27
+ onFeedbackChange: (messageId: string, feedback: FeedbackType | undefined) => void;
23
28
  };
24
29
 
25
- export function SearchAiMessage({
30
+ function SearchAiMessageComponent({
26
31
  role,
27
32
  content,
28
33
  isThinking,
29
34
  resources,
30
35
  className,
36
+ messageId,
37
+ feedback,
38
+ onFeedbackChange,
31
39
  }: SearchAiMessageProps): JSX.Element {
32
- const { useMarkdownText, useTranslate } = useThemeHooks();
40
+ const { useMarkdownText, useTranslate, useTelemetry } = useThemeHooks();
33
41
  const markDownContent = useMarkdownText(content || '');
34
42
  const { translate } = useTranslate();
43
+ const telemetry = useTelemetry();
44
+ const [feedbackSent, setFeedbackSent] = useState(false);
45
+
46
+ const hasResources = !isThinking && resources && resources.length > 0;
47
+ const resourcesCount = resources?.length ?? 0;
48
+
49
+ const showSuccessMessage = feedbackSent && feedback;
50
+
51
+ const sendFeedbackTelemetry = (feedbackValue: FeedbackType, dislikeReason?: string) => {
52
+ if (!messageId) return;
53
+
54
+ try {
55
+ telemetry.sendSearchAIFeedbackMessage({
56
+ feedback: feedbackValue,
57
+ messageId,
58
+ reason: dislikeReason,
59
+ });
60
+ } catch (error) {
61
+ console.error('Error sending feedback', error);
62
+ }
63
+ };
64
+
65
+ const handleFeedbackClick = (feedbackValue: FeedbackType, reason?: string) => {
66
+ if (!messageId) {
67
+ return;
68
+ }
69
+
70
+ if (!reason) {
71
+ onFeedbackChange(messageId, feedbackValue);
72
+ }
73
+
74
+ sendFeedbackTelemetry(feedbackValue, reason);
75
+
76
+ if (feedbackValue === FeedbackType.Like || reason) {
77
+ setFeedbackSent(true);
78
+ }
79
+ };
35
80
 
36
81
  return (
37
82
  <SearchAiMessageWrapper
@@ -49,46 +94,99 @@ export function SearchAiMessage({
49
94
  margin="0 var(--spacing-xs) 0 0"
50
95
  />
51
96
  )}
52
- <MessageWrapper role={role}>
53
- {role === AiSearchConversationRole.ASSISTANT ? (
54
- <>
55
- <ResponseText as="div" children={markDownContent} data-testid="response-text" />
56
- {!isThinking && resources && resources.length > 0 && (
57
- <ResourcesWrapper data-testid="resources-wrapper">
58
- <ResourcesTitle data-translation-key="search.ai.resourcesFound">
59
- {translate('search.ai.resourcesFound.basedOn', 'Based on')} {resources.length}{' '}
60
- {translate('search.ai.resourcesFound.resources', 'resources')}
61
- </ResourcesTitle>
62
- <ResourceTagsWrapper>
63
- {resources.map((resource, idx) => (
64
- <Link key={idx} to={resource.url} target="_blank">
65
- <ResourceTag
66
- borderless
67
- icon={<DocumentIcon color="--search-ai-resource-tag-icon-color" />}
68
- >
69
- {resource.title}
70
- </ResourceTag>
71
- </Link>
72
- ))}
73
- </ResourceTagsWrapper>
74
- </ResourcesWrapper>
75
- )}
76
- </>
77
- ) : (
78
- content
97
+ <MessageContentWrapper>
98
+ <MessageWrapper role={role}>
99
+ {role === AiSearchConversationRole.ASSISTANT ? (
100
+ <>
101
+ <ResponseText as="div" children={markDownContent} data-testid="response-text" />
102
+ {hasResources && (
103
+ <>
104
+ <ResourcesWrapper data-testid="resources-wrapper">
105
+ <ResourcesTitle data-translation-key="search.ai.resourcesFound">
106
+ {translate('search.ai.resourcesFound.basedOn', 'Based on')} {resourcesCount}{' '}
107
+ {translate('search.ai.resourcesFound.resources', 'resources')}
108
+ </ResourcesTitle>
109
+ <ResourceTagsWrapper>
110
+ {resources?.map((resource, idx) => (
111
+ <Link key={`${resource.url}-${idx}`} to={resource.url} target="_blank">
112
+ <ResourceTag
113
+ borderless
114
+ icon={<DocumentIcon color="--search-ai-resource-tag-icon-color" />}
115
+ >
116
+ {resource.title}
117
+ </ResourceTag>
118
+ </Link>
119
+ ))}
120
+ </ResourceTagsWrapper>
121
+ </ResourcesWrapper>
122
+ <FeedbackWrapper>
123
+ <SearchAiActionButtons
124
+ content={content}
125
+ feedback={feedback}
126
+ onFeedback={handleFeedbackClick}
127
+ disabled={feedbackSent}
128
+ />
129
+ </FeedbackWrapper>
130
+ </>
131
+ )}
132
+ </>
133
+ ) : (
134
+ content
135
+ )}
136
+ {isThinking && content.length === 0 && (
137
+ <ThinkingDotsWrapper data-testid="thinking-dots-wrapper">
138
+ <ThinkingDot />
139
+ <ThinkingDot />
140
+ <ThinkingDot />
141
+ </ThinkingDotsWrapper>
142
+ )}
143
+ </MessageWrapper>
144
+ {messageId && feedback === FeedbackType.Dislike && !showSuccessMessage && (
145
+ <SearchAiNegativeFeedbackForm
146
+ messageId={messageId}
147
+ onClose={onFeedbackChange}
148
+ onSubmit={(reason) => handleFeedbackClick(FeedbackType.Dislike, reason)}
149
+ />
79
150
  )}
80
- {isThinking && content.length === 0 && (
81
- <ThinkingDotsWrapper data-testid="thinking-dots-wrapper">
82
- <ThinkingDot />
83
- <ThinkingDot />
84
- <ThinkingDot />
85
- </ThinkingDotsWrapper>
151
+ {showSuccessMessage && (
152
+ <SuccessMessageWrapper data-component-name="Search/SearchAiMessage/Success">
153
+ <CheckmarkOutlineIcon size="20px" color="var(--color-success-base)" />
154
+ <SuccessMessageText>
155
+ {translate('search.ai.feedback.thanks', 'Thank you for your feedback!')}
156
+ </SuccessMessageText>
157
+ </SuccessMessageWrapper>
86
158
  )}
87
- </MessageWrapper>
159
+ </MessageContentWrapper>
88
160
  </SearchAiMessageWrapper>
89
161
  );
90
162
  }
91
163
 
164
+ function areResourcesEqual(
165
+ prev?: SearchAiMessageResource[],
166
+ next?: SearchAiMessageResource[],
167
+ ): boolean {
168
+ if (prev === next) return true;
169
+
170
+ if (!prev || !next || prev.length !== next.length) return false;
171
+
172
+ return prev.every((resource, index) => {
173
+ const nextResource = next[index];
174
+ return resource.url === nextResource.url && resource.title === nextResource.title;
175
+ });
176
+ }
177
+
178
+ export const SearchAiMessage = memo(SearchAiMessageComponent, (prevProps, nextProps) => {
179
+ return (
180
+ prevProps.role === nextProps.role &&
181
+ prevProps.content === nextProps.content &&
182
+ prevProps.isThinking === nextProps.isThinking &&
183
+ prevProps.messageId === nextProps.messageId &&
184
+ prevProps.feedback === nextProps.feedback &&
185
+ prevProps.onFeedbackChange === nextProps.onFeedbackChange &&
186
+ areResourcesEqual(prevProps.resources, nextProps.resources)
187
+ );
188
+ });
189
+
92
190
  const SearchAiMessageWrapper = styled.div<{ role: string }>`
93
191
  display: flex;
94
192
  flex-direction: row;
@@ -98,6 +196,14 @@ const SearchAiMessageWrapper = styled.div<{ role: string }>`
98
196
  role === AiSearchConversationRole.USER ? 'flex-end' : 'flex-start'};
99
197
  `;
100
198
 
199
+ const MessageContentWrapper = styled.div`
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: var(--spacing-sm);
203
+ max-width: 80%;
204
+ min-width: 0;
205
+ `;
206
+
101
207
  const ResponseText = styled(Markdown)`
102
208
  color: var(--search-ai-text-color);
103
209
  font-size: var(--search-ai-text-font-size);
@@ -120,7 +226,8 @@ const MessageWrapper = styled.div<{ role: string }>`
120
226
  role === AiSearchConversationRole.USER ? 'var(--spacing-sm)' : 'var(--spacing-xs)'}
121
227
  var(--spacing-sm);
122
228
  border-radius: var(--border-radius-lg);
123
- max-width: 80%;
229
+ width: fit-content;
230
+ max-width: 100%;
124
231
  word-wrap: break-word;
125
232
  white-space: pre-wrap;
126
233
  background-color: ${({ role }) =>
@@ -139,7 +246,13 @@ const ResourcesWrapper = styled.div`
139
246
  gap: var(--search-ai-resources-gap);
140
247
  display: flex;
141
248
  flex-direction: column;
142
- margin: var(--spacing-xs) 0;
249
+ margin: 0;
250
+ `;
251
+ const FeedbackWrapper = styled.div`
252
+ display: flex;
253
+ flex-direction: row;
254
+ gap: var(--search-ai-feedback-gap);
255
+ margin-top: var(--spacing-sm);
143
256
  `;
144
257
 
145
258
  const ResourcesTitle = styled.div`
@@ -210,3 +323,19 @@ const ThinkingDot = styled.div`
210
323
  }
211
324
  }
212
325
  `;
326
+
327
+ const SuccessMessageWrapper = styled.div`
328
+ max-width: fit-content;
329
+ display: flex;
330
+ align-items: center;
331
+ gap: var(--spacing-sm);
332
+ padding: var(--spacing-sm);
333
+ background: var(--color-success-bg);
334
+ border: 1px solid var(--color-success-border);
335
+ border-radius: var(--border-radius-lg);
336
+ `;
337
+
338
+ const SuccessMessageText = styled.div`
339
+ font-size: var(--font-size-base);
340
+ color: var(--color-success-darker);
341
+ `;
@@ -0,0 +1,210 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+ import type { FeedbackType } from '@redocly/theme/core/types';
6
+
7
+ import { Button } from '@redocly/theme/components/Button/Button';
8
+ import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
9
+ import { ArrowLeftIcon } from '@redocly/theme/icons/ArrowLeftIcon/ArrowLeftIcon';
10
+ import { SendIcon } from '@redocly/theme/icons/SendIcon/SendIcon';
11
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
12
+
13
+ export type SearchAiNegativeFeedbackFormProps = {
14
+ messageId: string;
15
+ onClose: (messageId: string, feedback: FeedbackType | undefined) => void;
16
+ onSubmit: (reason: string) => void;
17
+ };
18
+
19
+ const NEGATIVE_FEEDBACK_DEFAULT_REASONS = [
20
+ "Didn't answer my question",
21
+ "Couldn't find what I was looking for",
22
+ 'Wrong topic',
23
+ 'Partially helpful, but missing details',
24
+ ];
25
+
26
+ export function SearchAiNegativeFeedbackForm({
27
+ messageId,
28
+ onClose,
29
+ onSubmit,
30
+ }: SearchAiNegativeFeedbackFormProps): JSX.Element {
31
+ const { useTranslate } = useThemeHooks();
32
+ const { translate } = useTranslate();
33
+ const [showMoreInput, setShowMoreInput] = useState(false);
34
+ const [detailedFeedback, setDetailedFeedback] = useState('');
35
+ const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
36
+
37
+ const adjustTextAreaHeight = () => {
38
+ const textArea = textAreaRef.current;
39
+ if (textArea) {
40
+ textArea.style.height = 'auto';
41
+ textArea.style.height = `${textArea.scrollHeight}px`;
42
+ }
43
+ };
44
+
45
+ const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
46
+ setDetailedFeedback(e.target.value);
47
+ adjustTextAreaHeight();
48
+ };
49
+
50
+ useEffect(() => {
51
+ if (showMoreInput) {
52
+ adjustTextAreaHeight();
53
+ }
54
+ }, [showMoreInput]);
55
+
56
+ return (
57
+ <FeedbackFormWrapper data-component-name="Search/SearchAiNegativeFeedbackForm">
58
+ <FeedbackHeader>
59
+ {showMoreInput ? (
60
+ <BackButton
61
+ variant="text"
62
+ size="small"
63
+ icon={<ArrowLeftIcon />}
64
+ onClick={() => setShowMoreInput(false)}
65
+ aria-label="Back to feedback reasons"
66
+ />
67
+ ) : null}
68
+ <FeedbackTitle data-translation-key="search.ai.feedback.title">
69
+ {translate('search.ai.feedback.title', "What didn't you like about this response?")}
70
+ </FeedbackTitle>
71
+ <CloseButton
72
+ variant="text"
73
+ size="small"
74
+ icon={<CloseIcon />}
75
+ onClick={() => onClose(messageId, undefined)}
76
+ aria-label="Close feedback form"
77
+ />
78
+ </FeedbackHeader>
79
+
80
+ {!showMoreInput ? (
81
+ <FeedbackReasonsWrapper>
82
+ {NEGATIVE_FEEDBACK_DEFAULT_REASONS.map((reason) => (
83
+ <Button key={reason} variant="outlined" size="small" onClick={() => onSubmit(reason)}>
84
+ {reason}
85
+ </Button>
86
+ ))}
87
+ <Button variant="outlined" size="small" onClick={() => setShowMoreInput(true)}>
88
+ More...
89
+ </Button>
90
+ </FeedbackReasonsWrapper>
91
+ ) : (
92
+ <FeedbackInputWrapper>
93
+ <FeedbackTextArea
94
+ ref={textAreaRef}
95
+ placeholder={translate('search.ai.feedback.detailsPlaceholder', 'Add specific details')}
96
+ value={detailedFeedback}
97
+ onChange={handleTextChange}
98
+ rows={1}
99
+ autoFocus
100
+ />
101
+ <SendButton
102
+ size="small"
103
+ icon={
104
+ <SendIcon
105
+ color={
106
+ !detailedFeedback.trim()
107
+ ? '--button-content-color-disabled'
108
+ : 'var(--search-ai-conversation-input-send-button-icon-color)'
109
+ }
110
+ />
111
+ }
112
+ onClick={() => onSubmit(detailedFeedback)}
113
+ disabled={!detailedFeedback.trim()}
114
+ aria-label="Send feedback"
115
+ />
116
+ </FeedbackInputWrapper>
117
+ )}
118
+ </FeedbackFormWrapper>
119
+ );
120
+ }
121
+
122
+ const FeedbackFormWrapper = styled.div`
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: var(--spacing-sm);
126
+ padding: var(--spacing-xs);
127
+ background: var(--search-ai-feedback-form-bg-color);
128
+ border: 1px solid var(--search-ai-feedback-form-border-color);
129
+ border-radius: var(--border-radius-lg);
130
+ `;
131
+
132
+ const FeedbackHeader = styled.div`
133
+ display: flex;
134
+ justify-content: space-between;
135
+ align-items: center;
136
+ gap: var(--spacing-sm);
137
+ `;
138
+
139
+ const FeedbackTitle = styled.div`
140
+ font-size: var(--font-size-base);
141
+ color: var(--text-color);
142
+ flex: 1;
143
+ `;
144
+
145
+ const BackButton = styled(Button)`
146
+ flex-shrink: 0;
147
+ `;
148
+
149
+ const CloseButton = styled(Button)`
150
+ flex-shrink: 0;
151
+ `;
152
+
153
+ const FeedbackReasonsWrapper = styled.div`
154
+ display: flex;
155
+ flex-wrap: wrap;
156
+ gap: var(--spacing-xs);
157
+ `;
158
+
159
+ const FeedbackInputWrapper = styled.div`
160
+ position: relative;
161
+ width: 100%;
162
+ `;
163
+
164
+ const FeedbackTextArea = styled.textarea`
165
+ width: 100%;
166
+ min-height: 5rem;
167
+ max-height: 12.5rem;
168
+ padding: var(--spacing-xs);
169
+ padding-right: 3rem;
170
+ border: 1px solid var(--border-color-primary);
171
+ border-radius: var(--border-radius-md);
172
+ font-family: inherit;
173
+ font-size: var(--font-size-base);
174
+ line-height: var(--line-height-base);
175
+ background: var(--background-color);
176
+ color: var(--text-color);
177
+ resize: none;
178
+ overflow-y: auto;
179
+
180
+ &:focus {
181
+ outline: 1px solid var(--button-border-color-focused);
182
+ border-color: var(--button-border-color-focused);
183
+ }
184
+
185
+ &::placeholder {
186
+ color: var(--text-color-helper);
187
+ }
188
+ `;
189
+
190
+ const SendButton = styled(Button)`
191
+ position: absolute;
192
+ right: var(--search-ai-conversation-input-send-button-right);
193
+ bottom: var(--spacing-sm);
194
+ transition: background-color 0.2s ease;
195
+ background-color: var(--search-ai-conversation-input-send-button-bg-color);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ border-radius: var(--search-ai-conversation-input-send-button-border-radius);
200
+ padding: var(--search-ai-conversation-input-send-button-padding);
201
+
202
+ &:hover {
203
+ background-color: var(--search-ai-conversation-input-send-button-bg-color-hover);
204
+ }
205
+
206
+ &:disabled {
207
+ background-color: var(--search-ai-conversation-input-send-button-bg-color-disabled);
208
+ border: var(--search-ai-conversation-input-send-button-border-disabled);
209
+ }
210
+ `;