@eeacms/volto-eea-chatbot 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.coverage.babel.config.js +9 -0
  2. package/.eslintrc.js +68 -0
  3. package/.husky/pre-commit +2 -0
  4. package/.release-it.json +17 -0
  5. package/AGENTS.md +89 -0
  6. package/CHANGELOG.md +770 -0
  7. package/DEVELOP.md +124 -0
  8. package/LICENSE.md +9 -0
  9. package/README.md +170 -0
  10. package/RELEASE.md +74 -0
  11. package/TESTING.md +5 -0
  12. package/babel.config.js +17 -0
  13. package/bootstrap +41 -0
  14. package/cypress.config.js +27 -0
  15. package/docker-compose.yml +32 -0
  16. package/jest-addon.config.js +465 -0
  17. package/jest.setup.js +65 -0
  18. package/locales/de/LC_MESSAGES/volto.po +14 -0
  19. package/locales/en/LC_MESSAGES/volto.po +14 -0
  20. package/locales/it/LC_MESSAGES/volto.po +14 -0
  21. package/locales/ro/LC_MESSAGES/volto.po +14 -0
  22. package/locales/volto.pot +16 -0
  23. package/package.json +98 -0
  24. package/razzle.extend.js +40 -0
  25. package/src/ChatBlock/ChatBlockEdit.jsx +46 -0
  26. package/src/ChatBlock/ChatBlockView.jsx +21 -0
  27. package/src/ChatBlock/chat/AIMessage.tsx +566 -0
  28. package/src/ChatBlock/chat/ChatMessage.tsx +35 -0
  29. package/src/ChatBlock/chat/ChatWindow.tsx +288 -0
  30. package/src/ChatBlock/chat/UserMessage.tsx +27 -0
  31. package/src/ChatBlock/chat/index.ts +4 -0
  32. package/src/ChatBlock/components/AutoResizeTextarea.jsx +67 -0
  33. package/src/ChatBlock/components/BlinkingDot.tsx +3 -0
  34. package/src/ChatBlock/components/ChatMessageFeedback.jsx +77 -0
  35. package/src/ChatBlock/components/EmptyState.jsx +70 -0
  36. package/src/ChatBlock/components/FeedbackModal.jsx +125 -0
  37. package/src/ChatBlock/components/HalloumiFeedback.jsx +126 -0
  38. package/src/ChatBlock/components/Icon.tsx +35 -0
  39. package/src/ChatBlock/components/QualityCheckToggle.jsx +26 -0
  40. package/src/ChatBlock/components/RelatedQuestions.jsx +59 -0
  41. package/src/ChatBlock/components/Source.jsx +93 -0
  42. package/src/ChatBlock/components/SourceChip.tsx +55 -0
  43. package/src/ChatBlock/components/Spinner.jsx +3 -0
  44. package/src/ChatBlock/components/UserActionsToolbar.jsx +44 -0
  45. package/src/ChatBlock/components/WebResultIcon.tsx +42 -0
  46. package/src/ChatBlock/components/markdown/Citation.jsx +70 -0
  47. package/src/ChatBlock/components/markdown/ClaimModal.jsx +98 -0
  48. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +172 -0
  49. package/src/ChatBlock/components/markdown/RenderClaimView.jsx +96 -0
  50. package/src/ChatBlock/components/markdown/colors.js +29 -0
  51. package/src/ChatBlock/components/markdown/colors.less +52 -0
  52. package/src/ChatBlock/components/markdown/colors.test.js +69 -0
  53. package/src/ChatBlock/components/markdown/index.js +115 -0
  54. package/src/ChatBlock/fonts/DejaVuSans.ttf +0 -0
  55. package/src/ChatBlock/hocs/withOnyxData.jsx +46 -0
  56. package/src/ChatBlock/hooks/index.ts +7 -0
  57. package/src/ChatBlock/hooks/useChatController.ts +333 -0
  58. package/src/ChatBlock/hooks/useChatStreaming.ts +82 -0
  59. package/src/ChatBlock/hooks/useDeepCompareMemoize.js +17 -0
  60. package/src/ChatBlock/hooks/useMarked.js +44 -0
  61. package/src/ChatBlock/hooks/useQualityMarkers.js +119 -0
  62. package/src/ChatBlock/hooks/useScrollonStream.ts +131 -0
  63. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +80 -0
  64. package/src/ChatBlock/index.js +32 -0
  65. package/src/ChatBlock/packets/MultiToolRenderer.tsx +235 -0
  66. package/src/ChatBlock/packets/RendererComponent.tsx +115 -0
  67. package/src/ChatBlock/packets/index.ts +4 -0
  68. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +63 -0
  69. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +59 -0
  70. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +62 -0
  71. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +172 -0
  72. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +122 -0
  73. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +323 -0
  74. package/src/ChatBlock/packets/renderers/index.ts +6 -0
  75. package/src/ChatBlock/schema.js +403 -0
  76. package/src/ChatBlock/services/index.ts +3 -0
  77. package/src/ChatBlock/services/messageProcessor.ts +348 -0
  78. package/src/ChatBlock/services/packetUtils.ts +48 -0
  79. package/src/ChatBlock/services/streamingService.ts +342 -0
  80. package/src/ChatBlock/style.less +1881 -0
  81. package/src/ChatBlock/tests/AIMessage.test.jsx +95 -0
  82. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +49 -0
  83. package/src/ChatBlock/tests/BlinkingDot.test.jsx +71 -0
  84. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +73 -0
  85. package/src/ChatBlock/tests/Citation.test.jsx +107 -0
  86. package/src/ChatBlock/tests/EmptyState.test.jsx +137 -0
  87. package/src/ChatBlock/tests/FeedbackModal.test.jsx +138 -0
  88. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +94 -0
  89. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +105 -0
  90. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +215 -0
  91. package/src/ChatBlock/tests/Source.test.jsx +79 -0
  92. package/src/ChatBlock/tests/Spinner.test.jsx +18 -0
  93. package/src/ChatBlock/tests/index.test.js +51 -0
  94. package/src/ChatBlock/tests/messageProcessor.test.jsx +154 -0
  95. package/src/ChatBlock/tests/schema.test.js +166 -0
  96. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +107 -0
  97. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +151 -0
  98. package/src/ChatBlock/types/cssmodules.d.ts +7 -0
  99. package/src/ChatBlock/types/interfaces.ts +154 -0
  100. package/src/ChatBlock/types/slate.d.ts +3 -0
  101. package/src/ChatBlock/types/streamingModels.ts +267 -0
  102. package/src/ChatBlock/types/volto.d.ts +3 -0
  103. package/src/ChatBlock/utils/citations.ts +25 -0
  104. package/src/ChatBlock/utils/index.tsx +114 -0
  105. package/src/halloumi/README.md +1 -0
  106. package/src/halloumi/generative.js +219 -0
  107. package/src/halloumi/generative.test.js +88 -0
  108. package/src/halloumi/middleware.js +70 -0
  109. package/src/halloumi/postprocessing.js +273 -0
  110. package/src/halloumi/postprocessing.test.js +441 -0
  111. package/src/halloumi/preprocessing.js +115 -0
  112. package/src/halloumi/preprocessing.test.js +245 -0
  113. package/src/icons/bot.svg +1 -0
  114. package/src/icons/check.svg +1 -0
  115. package/src/icons/chevron.svg +3 -0
  116. package/src/icons/clear.svg +1 -0
  117. package/src/icons/copy.svg +1 -0
  118. package/src/icons/done.svg +5 -0
  119. package/src/icons/external-link.svg +1 -0
  120. package/src/icons/file.svg +1 -0
  121. package/src/icons/glasses.svg +1 -0
  122. package/src/icons/globe.svg +1 -0
  123. package/src/icons/rotate.svg +1 -0
  124. package/src/icons/search.svg +5 -0
  125. package/src/icons/send.svg +1 -0
  126. package/src/icons/square-pen.svg +1 -0
  127. package/src/icons/stop.svg +9 -0
  128. package/src/icons/thumbs-down.svg +1 -0
  129. package/src/icons/thumbs-up.svg +1 -0
  130. package/src/icons/user.svg +1 -0
  131. package/src/index.js +58 -0
  132. package/src/middleware.js +250 -0
  133. package/tsconfig.json +40 -0
@@ -0,0 +1,288 @@
1
+ import React, {
2
+ useRef,
3
+ useEffect,
4
+ useState,
5
+ useMemo,
6
+ useCallback,
7
+ } from 'react';
8
+ import type { Persona } from '../types/interfaces';
9
+ import { Button, Form, Segment, Checkbox } from 'semantic-ui-react';
10
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable';
11
+ import { trackEvent } from '@eeacms/volto-matomo/utils';
12
+
13
+ import { ChatMessage } from '.';
14
+ import { PacketType } from '../types/streamingModels';
15
+ import AutoResizeTextarea from '../components/AutoResizeTextarea';
16
+ import QualityCheckToggle from '../components/QualityCheckToggle';
17
+ import EmptyState from '../components/EmptyState';
18
+ import { useChatController } from '../hooks';
19
+ import SVGIcon from '../components/Icon';
20
+ import PenIcon from '../../icons/square-pen.svg';
21
+
22
+ import '../style.less';
23
+
24
+ interface ChatWindowProps {
25
+ persona: Persona;
26
+ rehypePrism?: any;
27
+ remarkGfm?: any;
28
+ placeholderPrompt?: string;
29
+ isEditMode?: boolean;
30
+ height?: string;
31
+ qgenAsistantId?: number;
32
+ enableQgen?: boolean;
33
+ enableFeedback?: boolean;
34
+ scrollToInput?: boolean;
35
+ feedbackReasons?: string[];
36
+ qualityCheck?: string;
37
+ qualityCheckStages?: string[];
38
+ qualityCheckContext?: string;
39
+ noSupportDocumentsMessage?: string;
40
+ totalFailMessage?: string;
41
+ enableShowTotalFailMessage?: boolean;
42
+ deepResearch?: string;
43
+ showTools?: PacketType[];
44
+ showAssistantTitle?: boolean;
45
+ showAssistantDescription?: boolean;
46
+ starterPromptsPosition?: 'top' | 'bottom';
47
+ enableMatomoTracking?: boolean;
48
+ onDemandInputToggle?: boolean;
49
+ maxContextSegments?: number;
50
+ [key: string]: any;
51
+ }
52
+
53
+ function ChatWindow({
54
+ persona,
55
+ rehypePrism,
56
+ remarkGfm,
57
+ placeholderPrompt = 'Ask a question',
58
+ isEditMode,
59
+ ...data
60
+ }: ChatWindowProps) {
61
+ const {
62
+ height,
63
+ qgenAsistantId,
64
+ enableQgen,
65
+ enableFeedback = true,
66
+ scrollToInput,
67
+ feedbackReasons,
68
+ qualityCheck = 'disabled',
69
+ qualityCheckStages = [],
70
+ qualityCheckContext = 'citations',
71
+ noSupportDocumentsMessage,
72
+ totalFailMessage,
73
+ enableShowTotalFailMessage,
74
+ deepResearch,
75
+ showTools,
76
+ showAssistantTitle,
77
+ showAssistantDescription,
78
+ starterPromptsPosition = 'top',
79
+ enableMatomoTracking = true,
80
+ onDemandInputToggle = true,
81
+ maxContextSegments = 0,
82
+ } = data;
83
+ const [qualityCheckEnabled, setQualityCheckEnabled] = useState(
84
+ onDemandInputToggle ?? true,
85
+ );
86
+
87
+ const showDeepResearchToggle =
88
+ deepResearch === 'user_on' || deepResearch === 'user_off';
89
+
90
+ useEffect(() => {
91
+ if (isEditMode && qualityCheck === 'ondemand_toggle') {
92
+ setQualityCheckEnabled(onDemandInputToggle ?? true);
93
+ }
94
+ }, [onDemandInputToggle, qualityCheck, isEditMode]);
95
+
96
+ // Memoize libs object to prevent recreation on every render
97
+ const libs = useMemo(
98
+ () => ({ rehypePrism, remarkGfm }),
99
+ [rehypePrism, remarkGfm],
100
+ );
101
+
102
+ const {
103
+ onSubmit,
104
+ onFetchRelatedQuestions,
105
+ messages,
106
+ isStreaming,
107
+ isFetchingRelatedQuestions,
108
+ clearChat,
109
+ setIsDeepResearchEnabled,
110
+ isDeepResearchEnabled,
111
+ } = useChatController({
112
+ personaId: persona.id,
113
+ qgenAsistantId,
114
+ enableQgen,
115
+ deepResearch,
116
+ });
117
+
118
+ const [showLandingPage, setShowLandingPage] = useState(true);
119
+
120
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
121
+ const chatWindowRef = useRef(null);
122
+ const chatWindowEndRef = useRef(null);
123
+
124
+ useEffect(() => {
125
+ setShowLandingPage(messages.length === 0);
126
+ }, [messages]);
127
+
128
+ const handleStarterPromptChoice = useCallback(
129
+ (message: string) => {
130
+ if (enableMatomoTracking) {
131
+ trackEvent({
132
+ category: persona?.name ? `Chatbot - ${persona.name}` : 'Chatbot',
133
+ action: 'Chatbot: Starter prompt click',
134
+ name: 'Message submitted',
135
+ });
136
+ }
137
+ onSubmit({ message });
138
+ setShowLandingPage(false);
139
+ },
140
+ [persona, enableMatomoTracking, onSubmit],
141
+ );
142
+
143
+ return (
144
+ <div className="chat-window">
145
+ <div className="messages">
146
+ {showLandingPage ? (
147
+ <>
148
+ {showAssistantTitle && <h2>{persona.name}</h2>}
149
+ {showAssistantDescription && <p>{persona.description}</p>}
150
+
151
+ {starterPromptsPosition === 'top' && (
152
+ <EmptyState
153
+ {...data}
154
+ persona={persona}
155
+ onChoice={handleStarterPromptChoice}
156
+ />
157
+ )}
158
+ </>
159
+ ) : (
160
+ <>
161
+ {/* @ts-ignore */}
162
+ <Segment clearing basic>
163
+ <Button
164
+ disabled={isStreaming}
165
+ onClick={clearChat}
166
+ className="right floated clear-chat"
167
+ aria-label="Clear chat"
168
+ >
169
+ <SVGIcon name={PenIcon} /> New chat
170
+ </Button>
171
+ </Segment>
172
+ <div
173
+ ref={chatWindowRef}
174
+ className={`conversation ${height ? 'include-scrollbar' : ''}`}
175
+ style={{ maxHeight: height }}
176
+ >
177
+ {messages.map((message, index) => (
178
+ <React.Fragment>
179
+ <ChatMessage
180
+ key={message.messageId}
181
+ message={message}
182
+ isLoading={isStreaming}
183
+ isDeepResearchEnabled={isDeepResearchEnabled}
184
+ libs={libs}
185
+ onChoice={(message) => onSubmit({ message })}
186
+ onFetchRelatedQuestions={onFetchRelatedQuestions}
187
+ enableFeedback={enableFeedback}
188
+ scrollToInput={scrollToInput}
189
+ feedbackReasons={feedbackReasons}
190
+ qualityCheck={qualityCheck}
191
+ qualityCheckStages={qualityCheckStages}
192
+ qualityCheckContext={qualityCheckContext}
193
+ qualityCheckEnabled={qualityCheckEnabled}
194
+ noSupportDocumentsMessage={noSupportDocumentsMessage}
195
+ totalFailMessage={totalFailMessage}
196
+ isFetchingRelatedQuestions={isFetchingRelatedQuestions}
197
+ enableShowTotalFailMessage={enableShowTotalFailMessage}
198
+ enableMatomoTracking={enableMatomoTracking}
199
+ persona={persona.id}
200
+ maxContextSegments={maxContextSegments}
201
+ isLastMessage={index === messages.length - 1}
202
+ className={
203
+ index === messages.length - 1 ? 'most-recent' : ''
204
+ }
205
+ chatWindowRef={chatWindowRef}
206
+ chatWindowEndRef={chatWindowEndRef}
207
+ showTools={showTools}
208
+ />
209
+ </React.Fragment>
210
+ ))}
211
+
212
+ {isStreaming &&
213
+ !isFetchingRelatedQuestions &&
214
+ !messages[messages.length - 1].isFinalMessageComing && (
215
+ <div className="comment">
216
+ <div className="circle assistant placeholder"></div>
217
+ <div className="comment-content">
218
+ <div className="loader-container">
219
+ <div className="loader" />
220
+ </div>
221
+ </div>
222
+ </div>
223
+ )}
224
+ </div>
225
+ </>
226
+ )}
227
+ </div>
228
+
229
+ <div className="chat-form">
230
+ {/* @ts-ignore */}
231
+ <Form>
232
+ <div className="textarea-wrapper">
233
+ <AutoResizeTextarea
234
+ // @ts-ignore TODO: convert AutoResizeTextarea to TypeScript
235
+ maxRows={8}
236
+ minRows={1}
237
+ ref={textareaRef}
238
+ placeholder={
239
+ messages.length > 0 ? 'Ask follow-up...' : placeholderPrompt
240
+ }
241
+ isStreaming={isStreaming}
242
+ enableMatomoTracking={enableMatomoTracking}
243
+ persona={persona}
244
+ onSubmit={onSubmit}
245
+ />
246
+ </div>
247
+ </Form>
248
+ <div className="chat-controls">
249
+ {qualityCheck === 'ondemand_toggle' && (
250
+ <QualityCheckToggle
251
+ isEditMode={isEditMode}
252
+ enabled={qualityCheckEnabled}
253
+ setEnabled={setQualityCheckEnabled}
254
+ />
255
+ )}
256
+
257
+ {showDeepResearchToggle && (
258
+ <div className="deep-research-toggle">
259
+ <Checkbox
260
+ id="deep-research-toggle"
261
+ toggle
262
+ checked={isDeepResearchEnabled}
263
+ label="Deep research"
264
+ onChange={(_, { checked }) => {
265
+ setIsDeepResearchEnabled(checked ?? false);
266
+ textareaRef.current?.focus();
267
+ }}
268
+ />
269
+ </div>
270
+ )}
271
+
272
+ {deepResearch === 'always_on' && <small>Deep research on</small>}
273
+ </div>
274
+ <div ref={chatWindowEndRef} /> {/* End div to mark the bottom */}
275
+ </div>
276
+
277
+ {showLandingPage && starterPromptsPosition === 'bottom' && (
278
+ <EmptyState
279
+ {...data}
280
+ persona={persona}
281
+ onChoice={handleStarterPromptChoice}
282
+ />
283
+ )}
284
+ </div>
285
+ );
286
+ }
287
+
288
+ export default injectLazyLibs(['rehypePrism', 'remarkGfm'])(ChatWindow);
@@ -0,0 +1,27 @@
1
+ import type { ChatMessageProps } from '../types/interfaces';
2
+ import loadable from '@loadable/component';
3
+ import SVGIcon from '../components/Icon';
4
+ import { components } from '../components/markdown';
5
+ import UserIcon from '../../icons/user.svg';
6
+
7
+ const Markdown: any = loadable(() => import('react-markdown'));
8
+
9
+ export function UserMessage({
10
+ message,
11
+ className = '',
12
+ libs,
13
+ }: ChatMessageProps) {
14
+ const { remarkGfm } = libs;
15
+ return (
16
+ <div className={`comment ${className}`}>
17
+ <div className="circle user">
18
+ <SVGIcon name={UserIcon} size={20} color="white" />
19
+ </div>
20
+ <div>
21
+ <Markdown components={components(message)} remarkPlugins={[remarkGfm]}>
22
+ {message.message}
23
+ </Markdown>
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,4 @@
1
+ export { default as ChatWindow } from './ChatWindow';
2
+ export { ChatMessage } from './ChatMessage';
3
+ export { AIMessage } from './AIMessage';
4
+ export { UserMessage } from './UserMessage';
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { Button } from 'semantic-ui-react';
3
+ import { trackEvent } from '@eeacms/volto-matomo/utils';
4
+ import TextareaAutosize from 'react-textarea-autosize';
5
+
6
+ import SVGIcon from './Icon';
7
+ import SendIcon from '../../icons/send.svg';
8
+
9
+ export default React.forwardRef(function AutoResizeTextarea(props, ref) {
10
+ const { onSubmit, isStreaming, enableMatomoTracking, persona, ...rest } =
11
+ props;
12
+ const [input, setInput] = React.useState('');
13
+
14
+ const handleSubmit = (e) => {
15
+ e.preventDefault();
16
+ const trimmedInput = input.trim();
17
+ if (trimmedInput) {
18
+ if (enableMatomoTracking) {
19
+ trackEvent({
20
+ category: persona?.name ? `Chatbot - ${persona.name}` : 'Chatbot',
21
+ action: 'Chatbot: Type a question',
22
+ name: 'Message submitted',
23
+ });
24
+ }
25
+ onSubmit({ message: input });
26
+ setInput('');
27
+ }
28
+ };
29
+
30
+ return (
31
+ <>
32
+ <TextareaAutosize
33
+ value={input}
34
+ onChange={(e) => setInput(e.target.value)}
35
+ onKeyDown={(e) => {
36
+ if (e.key === 'Enter' && !e.shiftKey) {
37
+ handleSubmit(e);
38
+ } else if (e.key === 'Enter' && e.shiftKey) {
39
+ e.preventDefault();
40
+ setInput(input + '\n');
41
+ }
42
+ }}
43
+ {...rest}
44
+ ref={ref}
45
+ />
46
+
47
+ <div className="chat-right-actions">
48
+ <Button
49
+ className="submit-btn"
50
+ type="submit"
51
+ aria-label="Send"
52
+ onKeyDown={(e) => {
53
+ handleSubmit(e);
54
+ }}
55
+ disabled={isStreaming}
56
+ onClick={(e) => {
57
+ handleSubmit(e);
58
+ }}
59
+ >
60
+ <div className="btn-icon">
61
+ <SVGIcon name={SendIcon} size="28" />
62
+ </div>
63
+ </Button>
64
+ </div>
65
+ </>
66
+ );
67
+ });
@@ -0,0 +1,3 @@
1
+ export function BlinkingDot({ addMargin = false }: { addMargin?: boolean }) {
2
+ return <span className={`blinking-dot ${addMargin ? 'with-margin' : ''}`} />;
3
+ }
@@ -0,0 +1,77 @@
1
+ import { useState } from 'react';
2
+ import { Button, Icon } from 'semantic-ui-react';
3
+ import FeedbackModal from './FeedbackModal';
4
+ import SVGIcon from './Icon';
5
+ import ThumbsUpIcon from '../../icons/thumbs-up.svg';
6
+ import ThumbsDownIcon from '../../icons/thumbs-down.svg';
7
+
8
+ const Toast = ({ message, type, isActive }) => (
9
+ <div className={`feedback-toast ${type} ${isActive ? 'active' : ''}`}>
10
+ <Icon name={type === 'success' ? 'check circle' : 'warning circle'} />
11
+ {message}
12
+ </div>
13
+ );
14
+
15
+ const ChatMessageFeedback = (props) => {
16
+ const { message, feedbackReasons, enableMatomoTracking, persona } = props;
17
+ const [modalOpen, setModalOpen] = useState(false);
18
+ const [toast, setToast] = useState(null);
19
+ const [isPositive, setIsPositive] = useState(null);
20
+ const [isToastActive, setIsToastActive] = useState(false);
21
+
22
+ const handleFeedback = (boolean) => {
23
+ setIsPositive(boolean);
24
+ setModalOpen(true);
25
+ };
26
+
27
+ const handleToast = (message, type) => {
28
+ setToast({
29
+ message: message,
30
+ type: type,
31
+ });
32
+ };
33
+
34
+ return (
35
+ <>
36
+ <Button
37
+ basic
38
+ onClick={() => handleFeedback(true)}
39
+ title="Like"
40
+ aria-label="Like"
41
+ >
42
+ <SVGIcon name={ThumbsUpIcon} />
43
+ </Button>
44
+ <Button
45
+ basic
46
+ onClick={() => handleFeedback(false)}
47
+ title="Dislike"
48
+ aria-label="Dislike"
49
+ >
50
+ <SVGIcon name={ThumbsDownIcon} />
51
+ </Button>
52
+
53
+ {toast && (
54
+ <Toast
55
+ message={toast.message}
56
+ type={toast.type}
57
+ isActive={isToastActive}
58
+ />
59
+ )}
60
+
61
+ <FeedbackModal
62
+ modalOpen={modalOpen}
63
+ onClose={() => setModalOpen(false)}
64
+ setToast={setToast}
65
+ onToast={handleToast}
66
+ isPositive={isPositive}
67
+ message={message}
68
+ setIsToastActive={setIsToastActive}
69
+ feedbackReasons={feedbackReasons}
70
+ enableMatomoTracking={enableMatomoTracking}
71
+ persona={persona}
72
+ />
73
+ </>
74
+ );
75
+ };
76
+
77
+ export default ChatMessageFeedback;
@@ -0,0 +1,70 @@
1
+ import { Button } from 'semantic-ui-react';
2
+ import { debounce } from '../utils';
3
+
4
+ function StarterMessage({ msg, onClick }) {
5
+ if (!(msg.name || msg.message)) return null;
6
+
7
+ return (
8
+ <Button onClick={onClick} onKeyDown={onClick} className="starter-message">
9
+ <span className="starter-message-title">{msg.name}</span>
10
+ {msg.description && (
11
+ <div className="starter-message-desc">{msg.description}</div>
12
+ )}
13
+ </Button>
14
+ );
15
+ }
16
+
17
+ let click_signal = { current: null };
18
+
19
+ export default function EmptyState(props) {
20
+ const {
21
+ persona,
22
+ onChoice,
23
+ showAssistantPrompts = true,
24
+ enableStarterPrompts,
25
+ starterPrompts = [],
26
+ starterPromptsHeading,
27
+ } = props;
28
+
29
+ return (
30
+ <div className="empty-state">
31
+ {starterPromptsHeading &&
32
+ (showAssistantPrompts || enableStarterPrompts) && (
33
+ <h4 className="starter-message-heading">{starterPromptsHeading}</h4>
34
+ )}
35
+
36
+ <div className="starter-messages-container">
37
+ {enableStarterPrompts &&
38
+ starterPrompts.map((msg, idx) => (
39
+ <StarterMessage
40
+ key={msg.name || `starter-${idx}`}
41
+ msg={msg}
42
+ onClick={() =>
43
+ debounce(
44
+ () =>
45
+ onChoice(msg.message || `${msg.name}\n${msg.description}`),
46
+ click_signal,
47
+ )
48
+ }
49
+ />
50
+ ))}
51
+
52
+ {showAssistantPrompts &&
53
+ !enableStarterPrompts &&
54
+ persona?.starter_messages?.map((msg, idx) => (
55
+ <StarterMessage
56
+ key={msg.name || `assistant-${idx}`}
57
+ msg={msg}
58
+ onClick={() =>
59
+ debounce(
60
+ () =>
61
+ onChoice(msg.message || `${msg.name}\n${msg.description}`),
62
+ click_signal,
63
+ )
64
+ }
65
+ />
66
+ ))}
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,125 @@
1
+ import React, { useState } from 'react';
2
+ import { trackEvent } from '@eeacms/volto-matomo/utils';
3
+ import { Modal, Button, TextArea, Form, Icon } from 'semantic-ui-react';
4
+ import { createChatMessageFeedback } from '../utils';
5
+
6
+ const FeedbackModal = ({
7
+ modalOpen,
8
+ onClose,
9
+ setToast,
10
+ onToast,
11
+ isPositive,
12
+ message,
13
+ setIsToastActive,
14
+ feedbackReasons,
15
+ enableMatomoTracking,
16
+ persona,
17
+ }) => {
18
+ const [feedbackText, setFeedbackText] = useState('');
19
+ const [selectedReason, setSelectedReason] = useState('');
20
+ const isPositiveFeedback = isPositive;
21
+
22
+ const resetForm = () => {
23
+ setFeedbackText('');
24
+ setSelectedReason('');
25
+ };
26
+
27
+ const handleClose = () => {
28
+ resetForm();
29
+ onClose();
30
+ };
31
+
32
+ const submitFeedback = async () => {
33
+ try {
34
+ await createChatMessageFeedback({
35
+ chat_message_id: message.messageId,
36
+ feedback_text: feedbackText,
37
+ is_positive: isPositive,
38
+ predefined_feedback: selectedReason,
39
+ });
40
+ if (enableMatomoTracking) {
41
+ trackEvent({
42
+ category: persona?.name ? `Chatbot - ${persona.name}` : 'Chatbot',
43
+ action: isPositive
44
+ ? 'Chatbot: Positive feedback submitted'
45
+ : 'Chatbot: Negative feedback submitted',
46
+ name: 'Feedback submitted',
47
+ });
48
+ }
49
+ setIsToastActive(true);
50
+ onToast('Thanks for your feedback!', 'success');
51
+ } catch (error) {
52
+ setIsToastActive(true);
53
+ onToast('Failed to submit feedback.', 'error');
54
+ } finally {
55
+ setTimeout(() => setIsToastActive(false), 5000);
56
+ setTimeout(() => setToast(null), 3500);
57
+ resetForm();
58
+ onClose();
59
+ }
60
+ };
61
+
62
+ return (
63
+ <Modal
64
+ open={modalOpen}
65
+ onClose={handleClose}
66
+ className="feedback-modal"
67
+ size="small"
68
+ >
69
+ <Modal.Header>
70
+ <h3>
71
+ {isPositiveFeedback ? (
72
+ <>
73
+ <Icon name="thumbs up outline" />
74
+ Share your positive feedback
75
+ </>
76
+ ) : (
77
+ <>
78
+ <Icon name="thumbs down outline" />
79
+ Tell us how we can improve
80
+ </>
81
+ )}
82
+ </h3>
83
+ </Modal.Header>
84
+
85
+ <Modal.Content>
86
+ {!isPositiveFeedback && (
87
+ <div className="reason-buttons">
88
+ {feedbackReasons?.map((reason) => (
89
+ <Button
90
+ primary
91
+ size="small"
92
+ key={reason}
93
+ onClick={() => setSelectedReason(reason)}
94
+ inverted={selectedReason !== reason}
95
+ >
96
+ {reason}
97
+ </Button>
98
+ ))}
99
+ </div>
100
+ )}
101
+
102
+ <Form>
103
+ <TextArea
104
+ placeholder={
105
+ isPositiveFeedback
106
+ ? 'What did you like about this response? (Optional)'
107
+ : 'What could be improved? (Optional)'
108
+ }
109
+ value={feedbackText}
110
+ onChange={(e) => setFeedbackText(e.target.value)}
111
+ />
112
+ </Form>
113
+ </Modal.Content>
114
+
115
+ <Modal.Actions>
116
+ <Button onClick={handleClose}>Cancel</Button>
117
+ <Button primary onClick={submitFeedback}>
118
+ Submit Feedback
119
+ </Button>
120
+ </Modal.Actions>
121
+ </Modal>
122
+ );
123
+ };
124
+
125
+ export default FeedbackModal;