@redocly/theme 0.59.0-next.7 → 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.
- package/lib/components/Search/SearchAiActionButtons.d.ts +10 -0
- package/lib/components/Search/SearchAiActionButtons.js +43 -0
- package/lib/components/Search/SearchAiDialog.d.ts +3 -6
- package/lib/components/Search/SearchAiDialog.js +20 -9
- package/lib/components/Search/SearchAiMessage.d.ts +9 -5
- package/lib/components/Search/SearchAiMessage.js +146 -22
- package/lib/components/Search/SearchAiNegativeFeedbackForm.d.ts +8 -0
- package/lib/components/Search/SearchAiNegativeFeedbackForm.js +169 -0
- package/lib/components/Search/variables.js +29 -66
- package/lib/core/hooks/index.d.ts +1 -0
- package/lib/core/hooks/index.js +1 -0
- package/lib/core/hooks/search/use-feedback-tooltip.d.ts +6 -0
- package/lib/core/hooks/search/use-feedback-tooltip.js +26 -0
- package/lib/core/hooks/use-telemetry-fallback.d.ts +1 -0
- package/lib/core/hooks/use-telemetry-fallback.js +1 -0
- package/lib/core/types/l10n.d.ts +1 -1
- package/lib/core/types/search.d.ts +11 -4
- package/lib/core/types/search.js +6 -0
- package/lib/core/utils/frontmatter-translate.d.ts +6 -0
- package/lib/core/utils/frontmatter-translate.js +14 -0
- package/lib/core/utils/index.d.ts +1 -0
- package/lib/core/utils/index.js +1 -0
- package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.d.ts +9 -0
- package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.js +34 -0
- package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.d.ts +9 -0
- package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.js +34 -0
- package/package.json +2 -2
- package/src/components/Search/SearchAiActionButtons.tsx +76 -0
- package/src/components/Search/SearchAiDialog.tsx +52 -23
- package/src/components/Search/SearchAiMessage.tsx +172 -43
- package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +210 -0
- package/src/components/Search/variables.ts +29 -66
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/search/use-feedback-tooltip.ts +32 -0
- package/src/core/hooks/use-telemetry-fallback.ts +1 -0
- package/src/core/types/l10n.ts +3 -0
- package/src/core/types/search.ts +13 -4
- package/src/core/utils/frontmatter-translate.ts +9 -0
- package/src/core/utils/index.ts +1 -0
- package/src/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.tsx +38 -0
- 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: (
|
|
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 = (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
role,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
<
|
|
53
|
-
{role
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
{
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
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:
|
|
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
|
+
`;
|