@topconsultnpm/sdkui-react 6.22.0-dev3.33 → 6.22.0-dev3.35

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 { AppModules, HolAskDescriptor, SDK_Globals, TopMediaServer } from '@topconsultnpm/sdk-ts';
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
- 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 })] })), showFab && (_jsx(Fab, { onClick: onToggle, title: "Chiedi a Toppy", "aria-label": "Chiedi a Toppy", children: _jsx(FabAvatar, { src: ToppyImage, alt: "Toppy" }) }))] }));
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 || 'Nessuna risposta ricevuta.', isUser: false });
204
+ onAddMessage({ id: generateGUID(), text: answer || NO_ANSWER_TEXT, isUser: false });
159
205
  }
160
206
  catch (error) {
161
- onAddMessage({ id: generateGUID(), text: 'Si è verificato un errore, riprova più tardi.', isUser: false });
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.isUser ? msg.text : _jsx(MarkdownContent, { children: renderMarkdown(msg.text) }) })] }, 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 })] })] }));
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,55 @@ 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
+ const panelRef = useRef(null);
260
+ useEffect(() => () => clearTimeout(copiedTimerRef.current), []);
261
+ useEffect(() => {
262
+ if (draftRating)
263
+ panelRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
264
+ }, [draftRating]);
265
+ const isSubmitted = submittedRating !== undefined;
266
+ const activeRating = submittedRating ?? draftRating;
267
+ const canSubmit = draftRating === 'like' || selectedTags.length > 0 || feedback.trim().length > 0;
268
+ const resetDraft = () => {
269
+ setDraftRating(undefined);
270
+ setSelectedTags([]);
271
+ setFeedback('');
272
+ };
273
+ const onRatingClick = (rating) => {
274
+ if (isSubmitted)
275
+ return;
276
+ if (draftRating === rating) {
277
+ resetDraft();
278
+ return;
279
+ }
280
+ setDraftRating(rating);
281
+ setSelectedTags([]);
282
+ };
283
+ const onTagClick = (tag) => {
284
+ setSelectedTags(prev => prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]);
285
+ };
286
+ const onCopyClick = async () => {
287
+ if (!(await copyToClipboard(answer)))
288
+ return;
289
+ setCopied(true);
290
+ clearTimeout(copiedTimerRef.current);
291
+ copiedTimerRef.current = setTimeout(() => setCopied(false), 2000);
292
+ };
293
+ const onSubmitClick = () => {
294
+ if (!draftRating || !canSubmit)
295
+ return;
296
+ onSubmit(draftRating, selectedTags, feedback.trim());
297
+ resetDraft();
298
+ };
299
+ 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, { ref: panelRef, 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')
300
+ 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))] }));
301
+ };
189
302
  //#endregion
190
303
  //#region Thinking status (phrases + elapsed seconds)
191
304
  const getThinkingPhrases = () => [
@@ -441,6 +554,15 @@ function IconQuestion(props) {
441
554
  function IconArrowForward(props) {
442
555
  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
556
  }
557
+ function IconThumbUp(props) {
558
+ 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" }) }));
559
+ }
560
+ function IconCopy(props) {
561
+ 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" }) }));
562
+ }
563
+ function IconCheck(props) {
564
+ 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" }) }));
565
+ }
444
566
  //#endregion
445
567
  //#region Styled Components
446
568
  const fadeIn = keyframes `
@@ -459,6 +581,15 @@ const suggestionIn = keyframes `
459
581
  from { opacity: 0; transform: translateY(6px); }
460
582
  to { opacity: 1; transform: translateY(0); }
461
583
  `;
584
+ const feedbackIn = keyframes `
585
+ from { opacity: 0; transform: translateY(-4px); }
586
+ to { opacity: 1; transform: translateY(0); }
587
+ `;
588
+ const badgeIn = keyframes `
589
+ 0% { opacity: 0; transform: scale(0.85); }
590
+ 60% { opacity: 1; transform: scale(1.05); }
591
+ 100% { opacity: 1; transform: scale(1); }
592
+ `;
462
593
  const dotCycle = keyframes `
463
594
  0% { opacity: 0; }
464
595
  25%, 75% { opacity: 1; }
@@ -693,6 +824,205 @@ const MessageContent = styled.div `
693
824
  white-space: ${props => (props.$isUser ? 'pre-wrap' : 'normal')};
694
825
  border-bottom-${props => (props.$isUser ? 'right' : 'left')}-radius: 4px;
695
826
  `;
827
+ const BotMessageColumn = styled.div `
828
+ display: flex;
829
+ flex-direction: column;
830
+ align-items: flex-start;
831
+ width: 80%;
832
+ min-width: 0;
833
+ `;
834
+ const BotMessageContent = styled(MessageContent) `
835
+ max-width: 100%;
836
+ `;
837
+ const FeedbackContainer = styled.div `
838
+ width: 100%;
839
+ margin-top: 4px;
840
+ `;
841
+ const FeedbackActions = styled.div `
842
+ display: flex;
843
+ align-items: center;
844
+ gap: 2px;
845
+ `;
846
+ const FeedbackActionButton = styled.button `
847
+ display: flex;
848
+ align-items: center;
849
+ justify-content: center;
850
+ width: 28px;
851
+ height: 28px;
852
+ padding: 0;
853
+ border-radius: 8px;
854
+ border: 1px solid ${props => (props.$active ? `${TMColors.primary}55` : 'transparent')};
855
+ background: ${props => (props.$active ? `${TMColors.primary}12` : 'transparent')};
856
+ color: ${props => (props.$success ? '#2e7d32' : props.$active ? TMColors.primary : '#8a8f98')};
857
+ cursor: ${props => (props.$locked ? 'default' : 'pointer')};
858
+ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
859
+
860
+ &:hover:not(:disabled) {
861
+ background: ${props => (props.$active ? `${TMColors.primary}12` : 'rgba(0, 0, 0, 0.05)')};
862
+ color: ${props => (props.$success ? '#2e7d32' : props.$active ? TMColors.primary : '#242424')};
863
+ }
864
+
865
+ &:focus-visible {
866
+ outline: 2px solid ${TMColors.primary}55;
867
+ outline-offset: 1px;
868
+ }
869
+
870
+ &:disabled {
871
+ opacity: 0.35;
872
+ cursor: default;
873
+ }
874
+ `;
875
+ const SentBadge = styled.span `
876
+ display: inline-flex;
877
+ align-items: center;
878
+ gap: 4px;
879
+ margin-left: 6px;
880
+ padding: 3px 9px 3px 7px;
881
+ border-radius: 999px;
882
+ background: #e8f5e9;
883
+ border: 1px solid #c8e6c9;
884
+ color: #2e7d32;
885
+ font-size: 11px;
886
+ font-weight: 600;
887
+ line-height: 1;
888
+ letter-spacing: 0.02em;
889
+ animation: ${badgeIn} 0.35s ease-out;
890
+
891
+ @media (prefers-reduced-motion: reduce) {
892
+ animation: none;
893
+ }
894
+ `;
895
+ const FeedbackPanel = styled.div `
896
+ margin-top: 6px;
897
+ padding: 14px 16px;
898
+ border: 1px solid #e5e7eb;
899
+ border-radius: 12px;
900
+ background: white;
901
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
902
+ scroll-margin: 12px;
903
+ animation: ${feedbackIn} 0.2s ease-out;
904
+ `;
905
+ const FeedbackPanelHeader = styled.div `
906
+ display: flex;
907
+ align-items: flex-start;
908
+ justify-content: space-between;
909
+ gap: 12px;
910
+ margin-bottom: 10px;
911
+ `;
912
+ const FeedbackPanelTitle = styled.div `
913
+ font-size: 14px;
914
+ font-weight: 600;
915
+ color: #242424;
916
+ line-height: 1.35;
917
+ `;
918
+ const FeedbackCloseButton = styled.button `
919
+ display: flex;
920
+ align-items: center;
921
+ justify-content: center;
922
+ width: 24px;
923
+ height: 24px;
924
+ padding: 0;
925
+ border: none;
926
+ border-radius: 6px;
927
+ background: transparent;
928
+ color: #6b7280;
929
+ cursor: pointer;
930
+ flex-shrink: 0;
931
+ transition: background 0.2s ease, color 0.2s ease;
932
+
933
+ &:hover { background: rgba(0, 0, 0, 0.06); color: #242424; }
934
+ `;
935
+ const FeedbackLabel = styled.div `
936
+ margin: 0 0 8px 0;
937
+ font-size: 12px;
938
+ font-weight: 600;
939
+ color: #6b7280;
940
+ `;
941
+ const FeedbackTags = styled.div `
942
+ display: flex;
943
+ flex-wrap: wrap;
944
+ gap: 8px;
945
+ margin-bottom: 14px;
946
+ `;
947
+ const FeedbackTag = styled.button `
948
+ padding: 6px 12px;
949
+ border-radius: 8px;
950
+ border: 1px solid ${props => (props.$selected ? TMColors.primary : '#dcdfe3')};
951
+ background: ${props => (props.$selected ? `${TMColors.primary}10` : 'white')};
952
+ color: ${props => (props.$selected ? TMColors.primary : '#242424')};
953
+ font-family: inherit;
954
+ font-size: 13px;
955
+ font-weight: ${props => (props.$selected ? 600 : 400)};
956
+ line-height: 1.2;
957
+ cursor: pointer;
958
+ transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
959
+
960
+ &:hover { border-color: ${TMColors.primary}88; }
961
+
962
+ &:focus-visible {
963
+ outline: 2px solid ${TMColors.primary}55;
964
+ outline-offset: 1px;
965
+ }
966
+ `;
967
+ const FeedbackTextarea = styled.textarea `
968
+ display: block;
969
+ width: 100%;
970
+ min-height: 70px;
971
+ padding: 9px 11px;
972
+ border: 1px solid #dcdfe3;
973
+ border-radius: 8px;
974
+ font-family: inherit;
975
+ font-size: 13px;
976
+ line-height: 1.45;
977
+ color: #242424;
978
+ background: white;
979
+ resize: none;
980
+ box-sizing: border-box;
981
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
982
+
983
+ &::placeholder { color: #9ca3af; }
984
+
985
+ &:focus {
986
+ outline: none;
987
+ border-color: ${TMColors.primary};
988
+ box-shadow: 0 0 0 3px ${TMColors.primary}15;
989
+ }
990
+ `;
991
+ const FeedbackPanelFooter = styled.div `
992
+ display: flex;
993
+ align-items: center;
994
+ justify-content: space-between;
995
+ margin-top: 10px;
996
+ `;
997
+ const FeedbackCounter = styled.span `
998
+ font-size: 11px;
999
+ color: ${props => (props.$full ? '#c62828' : '#9ca3af')};
1000
+ font-variant-numeric: tabular-nums;
1001
+ `;
1002
+ const FeedbackSubmitButton = styled.button `
1003
+ padding: 7px 18px;
1004
+ border: none;
1005
+ border-radius: 8px;
1006
+ background: ${TMColors.primary};
1007
+ color: white;
1008
+ font-family: inherit;
1009
+ font-size: 13px;
1010
+ font-weight: 600;
1011
+ cursor: pointer;
1012
+ transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
1013
+
1014
+ &:hover:not(:disabled) {
1015
+ box-shadow: 0 4px 12px ${TMColors.primary}40;
1016
+ transform: translateY(-1px);
1017
+ }
1018
+
1019
+ &:active:not(:disabled) { transform: translateY(0); }
1020
+
1021
+ &:disabled {
1022
+ opacity: 0.4;
1023
+ cursor: default;
1024
+ }
1025
+ `;
696
1026
  const MarkdownContent = styled.div `
697
1027
  & > *:first-child { margin-top: 0; }
698
1028
  & > *:last-child { margin-bottom: 0; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.22.0-dev3.33",
3
+ "version": "6.22.0-dev3.35",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",