@topconsultnpm/sdkui-react 6.22.0-dev3.33 → 6.22.0-dev3.34
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.
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import styled, { keyframes } from 'styled-components';
|
|
4
|
-
import {
|
|
4
|
+
import { locale as dxLocale } from 'devextreme/localization';
|
|
5
|
+
import { AppModules, CultureIDs, HolAskDescriptor, SDK_Globals, TopMediaServer } from '@topconsultnpm/sdk-ts';
|
|
5
6
|
import TMTooltip from '../../base/TMTooltip';
|
|
6
7
|
import { TMColors } from '../../../utils/theme';
|
|
7
8
|
import { SDKUI_Localizator } from '../../../helper';
|
|
@@ -20,6 +21,38 @@ const PANEL_DEFAULT_WIDTH = 600;
|
|
|
20
21
|
const PANEL_MIN_WIDTH = 340;
|
|
21
22
|
const PANEL_MAX_WIDTH = 900;
|
|
22
23
|
const SUGGESTIONS_COUNT = 4;
|
|
24
|
+
const FEEDBACK_MAX_LENGTH = 200;
|
|
25
|
+
const NO_ANSWER_TEXT = 'Nessuna risposta ricevuta.';
|
|
26
|
+
const ERROR_TEXT = 'Si è verificato un errore, riprova più tardi.';
|
|
27
|
+
const FEEDBACK_TAGS = {
|
|
28
|
+
like: ['Aggiornata', 'Accurata', 'Utile', 'Pertinente', 'Fonti affidabili', 'Altro'],
|
|
29
|
+
dislike: ['Non aggiornata', 'Imprecisa', 'Troppo lunga', 'Troppo corta', 'Fonti non affidabili', 'Inappropriata', 'Altro'],
|
|
30
|
+
};
|
|
31
|
+
const FEEDBACK_TITLES = {
|
|
32
|
+
like: 'Cosa ti è piaciuto di questa risposta?',
|
|
33
|
+
dislike: 'Cosa non ha funzionato in questa risposta?',
|
|
34
|
+
};
|
|
35
|
+
const copyToClipboard = async (text) => {
|
|
36
|
+
try {
|
|
37
|
+
if (globalThis.isSecureContext && navigator.clipboard?.writeText) {
|
|
38
|
+
await navigator.clipboard.writeText(text);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const textarea = document.createElement('textarea');
|
|
42
|
+
textarea.value = text;
|
|
43
|
+
textarea.setAttribute('readonly', '');
|
|
44
|
+
textarea.style.position = 'fixed';
|
|
45
|
+
textarea.style.opacity = '0';
|
|
46
|
+
document.body.appendChild(textarea);
|
|
47
|
+
textarea.select();
|
|
48
|
+
const copied = document.execCommand('copy');
|
|
49
|
+
document.body.removeChild(textarea);
|
|
50
|
+
return copied;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
23
56
|
const getDefaultSuggestedQuestions = (isLogged) => {
|
|
24
57
|
if (!isLogged)
|
|
25
58
|
return LoginSuggestedQuestions;
|
|
@@ -45,6 +78,15 @@ const getSafeSession = () => {
|
|
|
45
78
|
return undefined;
|
|
46
79
|
}
|
|
47
80
|
};
|
|
81
|
+
const DX_LOCALE_TO_CULTURE_ID = {
|
|
82
|
+
de: CultureIDs.De_DE,
|
|
83
|
+
en: CultureIDs.En_US,
|
|
84
|
+
es: CultureIDs.Es_ES,
|
|
85
|
+
fr: CultureIDs.Fr_FR,
|
|
86
|
+
it: CultureIDs.It_IT,
|
|
87
|
+
pt: CultureIDs.Pt_PT,
|
|
88
|
+
};
|
|
89
|
+
const getCurrentCultureId = () => DX_LOCALE_TO_CULTURE_ID[dxLocale().split('-')[0]] ?? getSafeSession()?.SessionDescr?.cultureID;
|
|
48
90
|
let preLoginSession;
|
|
49
91
|
let preLoginSessionUrl;
|
|
50
92
|
const getPreLoginSession = (endpointUrl) => {
|
|
@@ -62,7 +104,11 @@ const TMHelpOnLineChat = ({ isOpen, showChatButton, isLogged, messages, conversa
|
|
|
62
104
|
const [width, setWidth] = useState(PANEL_DEFAULT_WIDTH);
|
|
63
105
|
const showFab = !isLogged && showChatButton && !isOpen;
|
|
64
106
|
const questions = suggestedQuestions ?? getDefaultSuggestedQuestions(isLogged);
|
|
65
|
-
|
|
107
|
+
const [submittedRatings, setSubmittedRatings] = useState({});
|
|
108
|
+
const onFeedbackSubmitted = useCallback((messageId, rating) => {
|
|
109
|
+
setSubmittedRatings(prev => ({ ...prev, [messageId]: rating }));
|
|
110
|
+
}, []);
|
|
111
|
+
return (_jsxs(_Fragment, { children: [isOpen && (_jsxs(_Fragment, { children: [_jsx(Overlay, { onClick: onClose }), _jsx(ChatPanel, { width: width, onWidthChange: setWidth, messages: messages, conversationId: conversationId, isThinking: isThinking, endpointUrl: endpointUrl, onClose: onClose, onNewConversation: onNewConversation, onAddMessage: onAddMessage, onSetThinking: onSetThinking, onSetConversationId: onSetConversationId, suggestedQuestions: questions, isLogged: isLogged, submittedRatings: submittedRatings, onFeedbackSubmitted: onFeedbackSubmitted })] })), showFab && (_jsx(Fab, { onClick: onToggle, title: "Chiedi a Toppy", "aria-label": "Chiedi a Toppy", children: _jsx(FabAvatar, { src: ToppyImage, alt: "Toppy" }) }))] }));
|
|
66
112
|
};
|
|
67
113
|
export default TMHelpOnLineChat;
|
|
68
114
|
//#region Header button
|
|
@@ -71,7 +117,7 @@ export const TMToppyHeaderButton = ({ showChatButton, onToggle }) => {
|
|
|
71
117
|
return null;
|
|
72
118
|
return (_jsx(TMTooltip, { content: "Chiedi a Toppy", children: _jsxs(ToppyHeaderButton, { onClick: onToggle, title: "Chiedi a Toppy", children: [_jsx(ToppyHeaderAvatar, { src: ToppyImage, alt: "Toppy" }), _jsx(ToppyHeaderText, { children: "Chiedi a Toppy" })] }) }));
|
|
73
119
|
};
|
|
74
|
-
const ChatPanel = ({ width, onWidthChange, messages, conversationId, isThinking, endpointUrl, onClose, onNewConversation, onAddMessage, onSetThinking, onSetConversationId, suggestedQuestions }) => {
|
|
120
|
+
const ChatPanel = ({ width, onWidthChange, messages, conversationId, isThinking, endpointUrl, onClose, onNewConversation, onAddMessage, onSetThinking, onSetConversationId, suggestedQuestions, isLogged, submittedRatings, onFeedbackSubmitted }) => {
|
|
75
121
|
const [chatInput, setChatInput] = useState('');
|
|
76
122
|
const [suggestions, setSuggestions] = useState(() => pickRandom(suggestedQuestions, SUGGESTIONS_COUNT));
|
|
77
123
|
const [hoveredSuggestion, setHoveredSuggestion] = useState(undefined);
|
|
@@ -155,10 +201,10 @@ const ChatPanel = ({ width, onWidthChange, messages, conversationId, isThinking,
|
|
|
155
201
|
else {
|
|
156
202
|
console.error('[HOL] NO SESSION -> nothing called. endpointUrl =', endpointUrl);
|
|
157
203
|
}
|
|
158
|
-
onAddMessage({ id: generateGUID(), text: answer ||
|
|
204
|
+
onAddMessage({ id: generateGUID(), text: answer || NO_ANSWER_TEXT, isUser: false });
|
|
159
205
|
}
|
|
160
206
|
catch (error) {
|
|
161
|
-
onAddMessage({ id: generateGUID(), text:
|
|
207
|
+
onAddMessage({ id: generateGUID(), text: ERROR_TEXT, isUser: false });
|
|
162
208
|
console.error('[HOL]', error);
|
|
163
209
|
}
|
|
164
210
|
finally {
|
|
@@ -170,8 +216,26 @@ const ChatPanel = ({ width, onWidthChange, messages, conversationId, isThinking,
|
|
|
170
216
|
setHoveredSuggestion(undefined);
|
|
171
217
|
ask(question);
|
|
172
218
|
};
|
|
219
|
+
const onFeedbackSubmit = (index, rating, tags, feedback) => {
|
|
220
|
+
const answer = messages[index];
|
|
221
|
+
const question = messages.slice(0, index).reverse().find(m => m.isUser)?.text ?? '';
|
|
222
|
+
const session = getSafeSession();
|
|
223
|
+
console.log('[HOL] feedback', {
|
|
224
|
+
userName: session?.SessionDescr?.userName ?? '',
|
|
225
|
+
app: isLogged ? SDK_Globals.appModule : 'LOGIN',
|
|
226
|
+
cultureId: getCurrentCultureId(),
|
|
227
|
+
question,
|
|
228
|
+
answer: answer.text,
|
|
229
|
+
rating,
|
|
230
|
+
tags,
|
|
231
|
+
feedback,
|
|
232
|
+
conversationId,
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
});
|
|
235
|
+
onFeedbackSubmitted(answer.id, rating);
|
|
236
|
+
};
|
|
173
237
|
const showSuggestions = messages.length === 0 && !isThinking && suggestions.length > 0;
|
|
174
|
-
return (_jsxs(PanelContainer, { $width: width, children: [_jsx(ResizeHandle, { onMouseDown: onResizeStart }), _jsxs(ChatHeader, { children: [_jsxs(HeaderLeft, { children: [_jsx(ToppyAvatar, { src: ToppyImage, alt: "Toppy" }), _jsxs(HeaderText, { children: [_jsxs(HeaderGreeting, { children: ["Ciao ", userName] }), _jsx(HeaderQuestion, { children: "Come posso aiutarti?" })] })] }), _jsxs(HeaderRight, { children: [_jsx(TMTooltip, { content: "Nuova conversazione", children: _jsx(HeaderButton, { onClick: onNewConversation, disabled: !conversationId && messages.length === 0, title: "Nuova conversazione", children: _jsx(IconNewChat, { color: "white", fontSize: 18 }) }) }), _jsx(TMTooltip, { content: "Chiudi", children: _jsx(HeaderButton, { onClick: onClose, title: "Chiudi", children: _jsx(IconClose, { fontSize: 18 }) }) })] })] }), _jsxs(ChatBody, { children: [messages.length === 0 ? (_jsxs(WelcomeMessage, { children: [_jsx(WelcomeIcon, { children: "\uD83D\uDCA1" }), _jsxs(WelcomeText, { children: ["Ciao! Sono ", _jsx("strong", { children: "Toppy" }), ", il tuo assistente.", _jsx("br", {}), "Fai una domanda e ti aiuter\u00F2 a trovare la risposta."] })] })) : (messages.map(msg => (_jsxs(MessageBubble, { $isUser: msg.isUser, children: [!msg.isUser && _jsx(BotAvatar, { src: ToppyImage, alt: "Toppy" }), _jsx(MessageContent, { $isUser: msg.isUser, children: msg.
|
|
238
|
+
return (_jsxs(PanelContainer, { $width: width, children: [_jsx(ResizeHandle, { onMouseDown: onResizeStart }), _jsxs(ChatHeader, { children: [_jsxs(HeaderLeft, { children: [_jsx(ToppyAvatar, { src: ToppyImage, alt: "Toppy" }), _jsxs(HeaderText, { children: [_jsxs(HeaderGreeting, { children: ["Ciao ", userName] }), _jsx(HeaderQuestion, { children: "Come posso aiutarti?" })] })] }), _jsxs(HeaderRight, { children: [_jsx(TMTooltip, { content: "Nuova conversazione", children: _jsx(HeaderButton, { onClick: onNewConversation, disabled: !conversationId && messages.length === 0, title: "Nuova conversazione", children: _jsx(IconNewChat, { color: "white", fontSize: 18 }) }) }), _jsx(TMTooltip, { content: "Chiudi", children: _jsx(HeaderButton, { onClick: onClose, title: "Chiudi", children: _jsx(IconClose, { fontSize: 18 }) }) })] })] }), _jsxs(ChatBody, { children: [messages.length === 0 ? (_jsxs(WelcomeMessage, { children: [_jsx(WelcomeIcon, { children: "\uD83D\uDCA1" }), _jsxs(WelcomeText, { children: ["Ciao! Sono ", _jsx("strong", { children: "Toppy" }), ", il tuo assistente.", _jsx("br", {}), "Fai una domanda e ti aiuter\u00F2 a trovare la risposta."] })] })) : (messages.map((msg, index) => (_jsxs(MessageBubble, { $isUser: msg.isUser, children: [!msg.isUser && _jsx(BotAvatar, { src: ToppyImage, alt: "Toppy" }), msg.isUser ? (_jsx(MessageContent, { $isUser: true, children: msg.text })) : (_jsxs(BotMessageColumn, { children: [_jsx(BotMessageContent, { $isUser: false, children: _jsx(MarkdownContent, { children: renderMarkdown(msg.text) }) }), msg.text !== NO_ANSWER_TEXT && msg.text !== ERROR_TEXT && (_jsx(MessageFeedback, { answer: msg.text, submittedRating: submittedRatings[msg.id], onSubmit: (rating, tags, feedback) => onFeedbackSubmit(index, rating, tags, feedback) }))] }))] }, msg.id)))), isThinking && (_jsxs(MessageBubble, { $isUser: false, children: [_jsx(BotAvatar, { src: ToppyImage, alt: "Toppy" }), _jsx(ThinkingStatus, {}, messages.length)] })), _jsx("div", { ref: messagesEndRef })] }), _jsxs(ChatFooter, { children: [showSuggestions && (_jsxs(SuggestionsSection, { children: [_jsxs(SuggestionsTitle, { children: [_jsx(IconSparkle, { fontSize: 14 }), _jsx("span", { children: "Domande suggerite" })] }), _jsx(SuggestionsGrid, { children: suggestions.map((question, index) => (_jsxs(SuggestionCard, { type: "button", $index: index, title: question, onClick: () => onSuggestionClick(question), onMouseEnter: () => setHoveredSuggestion(question), onMouseLeave: () => setHoveredSuggestion(undefined), onFocus: () => setHoveredSuggestion(question), onBlur: () => setHoveredSuggestion(undefined), children: [_jsx(SuggestionIcon, { children: _jsx(IconQuestion, { fontSize: 16 }) }), _jsx(SuggestionText, { children: question }), _jsx(SuggestionArrow, { children: _jsx(IconArrowForward, { fontSize: 16 }) })] }, question))) })] })), _jsx(TMChatInput, { inputRef: inputRef, value: chatInput, onChange: setChatInput, onSend: askHandler, placeholder: hoveredSuggestion ?? 'Scrivi la tua domanda...', tooltip: "Invia", disabled: isThinking }), _jsx(FooterHint, { children: SDKUI_Localizator.HelpOnLineShiftEnterHint })] })] }));
|
|
175
239
|
};
|
|
176
240
|
const TMChatInput = ({ value, onChange, onSend, placeholder = 'Scrivi la tua domanda...', disabled = false, inputRef, tooltip = 'Ricerca AI' }) => {
|
|
177
241
|
const isDisabled = disabled || value.length < 2;
|
|
@@ -186,6 +250,50 @@ const TMChatInput = ({ value, onChange, onSend, placeholder = 'Scrivi la tua dom
|
|
|
186
250
|
}
|
|
187
251
|
} }), _jsx(TMTooltip, { content: tooltip, children: _jsx(InlineSendButton, { $disabled: isDisabled, disabled: isDisabled, onClick: onSend, title: tooltip, children: _jsx(IconSendFilled, { fontSize: 20 }) }) })] }));
|
|
188
252
|
};
|
|
253
|
+
const MessageFeedback = ({ answer, submittedRating, onSubmit }) => {
|
|
254
|
+
const [draftRating, setDraftRating] = useState(undefined);
|
|
255
|
+
const [selectedTags, setSelectedTags] = useState([]);
|
|
256
|
+
const [feedback, setFeedback] = useState('');
|
|
257
|
+
const [copied, setCopied] = useState(false);
|
|
258
|
+
const copiedTimerRef = useRef();
|
|
259
|
+
useEffect(() => () => clearTimeout(copiedTimerRef.current), []);
|
|
260
|
+
const isSubmitted = submittedRating !== undefined;
|
|
261
|
+
const activeRating = submittedRating ?? draftRating;
|
|
262
|
+
const canSubmit = draftRating === 'like' || selectedTags.length > 0 || feedback.trim().length > 0;
|
|
263
|
+
const resetDraft = () => {
|
|
264
|
+
setDraftRating(undefined);
|
|
265
|
+
setSelectedTags([]);
|
|
266
|
+
setFeedback('');
|
|
267
|
+
};
|
|
268
|
+
const onRatingClick = (rating) => {
|
|
269
|
+
if (isSubmitted)
|
|
270
|
+
return;
|
|
271
|
+
if (draftRating === rating) {
|
|
272
|
+
resetDraft();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
setDraftRating(rating);
|
|
276
|
+
setSelectedTags([]);
|
|
277
|
+
};
|
|
278
|
+
const onTagClick = (tag) => {
|
|
279
|
+
setSelectedTags(prev => prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]);
|
|
280
|
+
};
|
|
281
|
+
const onCopyClick = async () => {
|
|
282
|
+
if (!(await copyToClipboard(answer)))
|
|
283
|
+
return;
|
|
284
|
+
setCopied(true);
|
|
285
|
+
clearTimeout(copiedTimerRef.current);
|
|
286
|
+
copiedTimerRef.current = setTimeout(() => setCopied(false), 2000);
|
|
287
|
+
};
|
|
288
|
+
const onSubmitClick = () => {
|
|
289
|
+
if (!draftRating || !canSubmit)
|
|
290
|
+
return;
|
|
291
|
+
onSubmit(draftRating, selectedTags, feedback.trim());
|
|
292
|
+
resetDraft();
|
|
293
|
+
};
|
|
294
|
+
return (_jsxs(FeedbackContainer, { children: [_jsxs(FeedbackActions, { children: [_jsx(TMTooltip, { content: "Mi piace", children: _jsx(FeedbackActionButton, { type: "button", $active: activeRating === 'like', $locked: isSubmitted, disabled: isSubmitted && submittedRating !== 'like', "aria-pressed": activeRating === 'like', "aria-label": "Mi piace", onClick: () => onRatingClick('like'), children: _jsx(IconThumbUp, { fontSize: 16 }) }) }), _jsx(TMTooltip, { content: "Non mi piace", children: _jsx(FeedbackActionButton, { type: "button", $active: activeRating === 'dislike', $locked: isSubmitted, disabled: isSubmitted && submittedRating !== 'dislike', "aria-pressed": activeRating === 'dislike', "aria-label": "Non mi piace", onClick: () => onRatingClick('dislike'), children: _jsx(IconThumbUp, { fontSize: 16, style: { transform: 'rotate(180deg)' } }) }) }), _jsx(TMTooltip, { content: copied ? 'Copiato' : 'Copia', children: _jsx(FeedbackActionButton, { type: "button", $active: false, $locked: false, $success: copied, "aria-label": "Copia", onClick: onCopyClick, children: copied ? _jsx(IconCheck, { fontSize: 16 }) : _jsx(IconCopy, { fontSize: 16 }) }) }), isSubmitted && (_jsxs(SentBadge, { role: "status", children: [_jsx(IconCheck, { fontSize: 12 }), "Inviato"] }))] }), !isSubmitted && draftRating && (_jsxs(FeedbackPanel, { children: [_jsxs(FeedbackPanelHeader, { children: [_jsx(FeedbackPanelTitle, { children: FEEDBACK_TITLES[draftRating] }), _jsx(FeedbackCloseButton, { type: "button", "aria-label": "Chiudi", title: "Chiudi", onClick: resetDraft, children: _jsx(IconClose, { fontSize: 16 }) })] }), _jsx(FeedbackLabel, { children: "Era..." }), _jsx(FeedbackTags, { children: FEEDBACK_TAGS[draftRating].map(tag => (_jsx(FeedbackTag, { type: "button", $selected: selectedTags.includes(tag), "aria-pressed": selectedTags.includes(tag), onClick: () => onTagClick(tag), children: tag }, tag))) }), _jsx(FeedbackLabel, { children: "Feedback aggiuntivo" }), _jsx(FeedbackTextarea, { value: feedback, maxLength: FEEDBACK_MAX_LENGTH, placeholder: "Raccontaci di pi\u00F9", onChange: (e) => setFeedback(e.target.value.slice(0, FEEDBACK_MAX_LENGTH)), onKeyDown: (e) => { if (e.key === 'Enter')
|
|
295
|
+
e.stopPropagation(); } }), _jsxs(FeedbackPanelFooter, { children: [_jsxs(FeedbackCounter, { $full: feedback.length >= FEEDBACK_MAX_LENGTH, children: [feedback.length, "/", FEEDBACK_MAX_LENGTH] }), _jsx(FeedbackSubmitButton, { type: "button", disabled: !canSubmit, onClick: onSubmitClick, children: "Invia" })] })] }, draftRating))] }));
|
|
296
|
+
};
|
|
189
297
|
//#endregion
|
|
190
298
|
//#region Thinking status (phrases + elapsed seconds)
|
|
191
299
|
const getThinkingPhrases = () => [
|
|
@@ -441,6 +549,15 @@ function IconQuestion(props) {
|
|
|
441
549
|
function IconArrowForward(props) {
|
|
442
550
|
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor", ...props, children: _jsx("path", { d: "M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" }) }));
|
|
443
551
|
}
|
|
552
|
+
function IconThumbUp(props) {
|
|
553
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor", ...props, children: _jsx("path", { d: "M9 21h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.58 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2zM9 9l4.34-4.34L12 10h9v2l-3 7H9V9zM1 9h4v12H1z" }) }));
|
|
554
|
+
}
|
|
555
|
+
function IconCopy(props) {
|
|
556
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor", ...props, children: _jsx("path", { d: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" }) }));
|
|
557
|
+
}
|
|
558
|
+
function IconCheck(props) {
|
|
559
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor", ...props, children: _jsx("path", { d: "M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" }) }));
|
|
560
|
+
}
|
|
444
561
|
//#endregion
|
|
445
562
|
//#region Styled Components
|
|
446
563
|
const fadeIn = keyframes `
|
|
@@ -459,6 +576,15 @@ const suggestionIn = keyframes `
|
|
|
459
576
|
from { opacity: 0; transform: translateY(6px); }
|
|
460
577
|
to { opacity: 1; transform: translateY(0); }
|
|
461
578
|
`;
|
|
579
|
+
const feedbackIn = keyframes `
|
|
580
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
581
|
+
to { opacity: 1; transform: translateY(0); }
|
|
582
|
+
`;
|
|
583
|
+
const badgeIn = keyframes `
|
|
584
|
+
0% { opacity: 0; transform: scale(0.85); }
|
|
585
|
+
60% { opacity: 1; transform: scale(1.05); }
|
|
586
|
+
100% { opacity: 1; transform: scale(1); }
|
|
587
|
+
`;
|
|
462
588
|
const dotCycle = keyframes `
|
|
463
589
|
0% { opacity: 0; }
|
|
464
590
|
25%, 75% { opacity: 1; }
|
|
@@ -693,6 +819,204 @@ const MessageContent = styled.div `
|
|
|
693
819
|
white-space: ${props => (props.$isUser ? 'pre-wrap' : 'normal')};
|
|
694
820
|
border-bottom-${props => (props.$isUser ? 'right' : 'left')}-radius: 4px;
|
|
695
821
|
`;
|
|
822
|
+
const BotMessageColumn = styled.div `
|
|
823
|
+
display: flex;
|
|
824
|
+
flex-direction: column;
|
|
825
|
+
align-items: flex-start;
|
|
826
|
+
width: 80%;
|
|
827
|
+
min-width: 0;
|
|
828
|
+
`;
|
|
829
|
+
const BotMessageContent = styled(MessageContent) `
|
|
830
|
+
max-width: 100%;
|
|
831
|
+
`;
|
|
832
|
+
const FeedbackContainer = styled.div `
|
|
833
|
+
width: 100%;
|
|
834
|
+
margin-top: 4px;
|
|
835
|
+
`;
|
|
836
|
+
const FeedbackActions = styled.div `
|
|
837
|
+
display: flex;
|
|
838
|
+
align-items: center;
|
|
839
|
+
gap: 2px;
|
|
840
|
+
`;
|
|
841
|
+
const FeedbackActionButton = styled.button `
|
|
842
|
+
display: flex;
|
|
843
|
+
align-items: center;
|
|
844
|
+
justify-content: center;
|
|
845
|
+
width: 28px;
|
|
846
|
+
height: 28px;
|
|
847
|
+
padding: 0;
|
|
848
|
+
border-radius: 8px;
|
|
849
|
+
border: 1px solid ${props => (props.$active ? `${TMColors.primary}55` : 'transparent')};
|
|
850
|
+
background: ${props => (props.$active ? `${TMColors.primary}12` : 'transparent')};
|
|
851
|
+
color: ${props => (props.$success ? '#2e7d32' : props.$active ? TMColors.primary : '#8a8f98')};
|
|
852
|
+
cursor: ${props => (props.$locked ? 'default' : 'pointer')};
|
|
853
|
+
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
|
854
|
+
|
|
855
|
+
&:hover:not(:disabled) {
|
|
856
|
+
background: ${props => (props.$active ? `${TMColors.primary}12` : 'rgba(0, 0, 0, 0.05)')};
|
|
857
|
+
color: ${props => (props.$success ? '#2e7d32' : props.$active ? TMColors.primary : '#242424')};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
&:focus-visible {
|
|
861
|
+
outline: 2px solid ${TMColors.primary}55;
|
|
862
|
+
outline-offset: 1px;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
&:disabled {
|
|
866
|
+
opacity: 0.35;
|
|
867
|
+
cursor: default;
|
|
868
|
+
}
|
|
869
|
+
`;
|
|
870
|
+
const SentBadge = styled.span `
|
|
871
|
+
display: inline-flex;
|
|
872
|
+
align-items: center;
|
|
873
|
+
gap: 4px;
|
|
874
|
+
margin-left: 6px;
|
|
875
|
+
padding: 3px 9px 3px 7px;
|
|
876
|
+
border-radius: 999px;
|
|
877
|
+
background: #e8f5e9;
|
|
878
|
+
border: 1px solid #c8e6c9;
|
|
879
|
+
color: #2e7d32;
|
|
880
|
+
font-size: 11px;
|
|
881
|
+
font-weight: 600;
|
|
882
|
+
line-height: 1;
|
|
883
|
+
letter-spacing: 0.02em;
|
|
884
|
+
animation: ${badgeIn} 0.35s ease-out;
|
|
885
|
+
|
|
886
|
+
@media (prefers-reduced-motion: reduce) {
|
|
887
|
+
animation: none;
|
|
888
|
+
}
|
|
889
|
+
`;
|
|
890
|
+
const FeedbackPanel = styled.div `
|
|
891
|
+
margin-top: 6px;
|
|
892
|
+
padding: 14px 16px;
|
|
893
|
+
border: 1px solid #e5e7eb;
|
|
894
|
+
border-radius: 12px;
|
|
895
|
+
background: white;
|
|
896
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
|
897
|
+
animation: ${feedbackIn} 0.2s ease-out;
|
|
898
|
+
`;
|
|
899
|
+
const FeedbackPanelHeader = styled.div `
|
|
900
|
+
display: flex;
|
|
901
|
+
align-items: flex-start;
|
|
902
|
+
justify-content: space-between;
|
|
903
|
+
gap: 12px;
|
|
904
|
+
margin-bottom: 10px;
|
|
905
|
+
`;
|
|
906
|
+
const FeedbackPanelTitle = styled.div `
|
|
907
|
+
font-size: 14px;
|
|
908
|
+
font-weight: 600;
|
|
909
|
+
color: #242424;
|
|
910
|
+
line-height: 1.35;
|
|
911
|
+
`;
|
|
912
|
+
const FeedbackCloseButton = styled.button `
|
|
913
|
+
display: flex;
|
|
914
|
+
align-items: center;
|
|
915
|
+
justify-content: center;
|
|
916
|
+
width: 24px;
|
|
917
|
+
height: 24px;
|
|
918
|
+
padding: 0;
|
|
919
|
+
border: none;
|
|
920
|
+
border-radius: 6px;
|
|
921
|
+
background: transparent;
|
|
922
|
+
color: #6b7280;
|
|
923
|
+
cursor: pointer;
|
|
924
|
+
flex-shrink: 0;
|
|
925
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
926
|
+
|
|
927
|
+
&:hover { background: rgba(0, 0, 0, 0.06); color: #242424; }
|
|
928
|
+
`;
|
|
929
|
+
const FeedbackLabel = styled.div `
|
|
930
|
+
margin: 0 0 8px 0;
|
|
931
|
+
font-size: 12px;
|
|
932
|
+
font-weight: 600;
|
|
933
|
+
color: #6b7280;
|
|
934
|
+
`;
|
|
935
|
+
const FeedbackTags = styled.div `
|
|
936
|
+
display: flex;
|
|
937
|
+
flex-wrap: wrap;
|
|
938
|
+
gap: 8px;
|
|
939
|
+
margin-bottom: 14px;
|
|
940
|
+
`;
|
|
941
|
+
const FeedbackTag = styled.button `
|
|
942
|
+
padding: 6px 12px;
|
|
943
|
+
border-radius: 8px;
|
|
944
|
+
border: 1px solid ${props => (props.$selected ? TMColors.primary : '#dcdfe3')};
|
|
945
|
+
background: ${props => (props.$selected ? `${TMColors.primary}10` : 'white')};
|
|
946
|
+
color: ${props => (props.$selected ? TMColors.primary : '#242424')};
|
|
947
|
+
font-family: inherit;
|
|
948
|
+
font-size: 13px;
|
|
949
|
+
font-weight: ${props => (props.$selected ? 600 : 400)};
|
|
950
|
+
line-height: 1.2;
|
|
951
|
+
cursor: pointer;
|
|
952
|
+
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
|
|
953
|
+
|
|
954
|
+
&:hover { border-color: ${TMColors.primary}88; }
|
|
955
|
+
|
|
956
|
+
&:focus-visible {
|
|
957
|
+
outline: 2px solid ${TMColors.primary}55;
|
|
958
|
+
outline-offset: 1px;
|
|
959
|
+
}
|
|
960
|
+
`;
|
|
961
|
+
const FeedbackTextarea = styled.textarea `
|
|
962
|
+
display: block;
|
|
963
|
+
width: 100%;
|
|
964
|
+
min-height: 70px;
|
|
965
|
+
padding: 9px 11px;
|
|
966
|
+
border: 1px solid #dcdfe3;
|
|
967
|
+
border-radius: 8px;
|
|
968
|
+
font-family: inherit;
|
|
969
|
+
font-size: 13px;
|
|
970
|
+
line-height: 1.45;
|
|
971
|
+
color: #242424;
|
|
972
|
+
background: white;
|
|
973
|
+
resize: none;
|
|
974
|
+
box-sizing: border-box;
|
|
975
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
976
|
+
|
|
977
|
+
&::placeholder { color: #9ca3af; }
|
|
978
|
+
|
|
979
|
+
&:focus {
|
|
980
|
+
outline: none;
|
|
981
|
+
border-color: ${TMColors.primary};
|
|
982
|
+
box-shadow: 0 0 0 3px ${TMColors.primary}15;
|
|
983
|
+
}
|
|
984
|
+
`;
|
|
985
|
+
const FeedbackPanelFooter = styled.div `
|
|
986
|
+
display: flex;
|
|
987
|
+
align-items: center;
|
|
988
|
+
justify-content: space-between;
|
|
989
|
+
margin-top: 10px;
|
|
990
|
+
`;
|
|
991
|
+
const FeedbackCounter = styled.span `
|
|
992
|
+
font-size: 11px;
|
|
993
|
+
color: ${props => (props.$full ? '#c62828' : '#9ca3af')};
|
|
994
|
+
font-variant-numeric: tabular-nums;
|
|
995
|
+
`;
|
|
996
|
+
const FeedbackSubmitButton = styled.button `
|
|
997
|
+
padding: 7px 18px;
|
|
998
|
+
border: none;
|
|
999
|
+
border-radius: 8px;
|
|
1000
|
+
background: ${TMColors.primary};
|
|
1001
|
+
color: white;
|
|
1002
|
+
font-family: inherit;
|
|
1003
|
+
font-size: 13px;
|
|
1004
|
+
font-weight: 600;
|
|
1005
|
+
cursor: pointer;
|
|
1006
|
+
transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
|
1007
|
+
|
|
1008
|
+
&:hover:not(:disabled) {
|
|
1009
|
+
box-shadow: 0 4px 12px ${TMColors.primary}40;
|
|
1010
|
+
transform: translateY(-1px);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
&:active:not(:disabled) { transform: translateY(0); }
|
|
1014
|
+
|
|
1015
|
+
&:disabled {
|
|
1016
|
+
opacity: 0.4;
|
|
1017
|
+
cursor: default;
|
|
1018
|
+
}
|
|
1019
|
+
`;
|
|
696
1020
|
const MarkdownContent = styled.div `
|
|
697
1021
|
& > *:first-child { margin-top: 0; }
|
|
698
1022
|
& > *:last-child { margin-bottom: 0; }
|