@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,40 @@
1
+ const path = require('path');
2
+ const makeLoaderFinder = require('razzle-dev-utils/makeLoaderFinder');
3
+
4
+ const modify = (config, { target, dev }, webpack) => {
5
+ const markedPath = path.dirname(require.resolve('marked'));
6
+ // const nodeFetch = path.dirname(require.resolve('node-fetch'));
7
+
8
+ const babelLoaderFinder = makeLoaderFinder('babel-loader');
9
+ const babelLoader = config.module.rules.find(babelLoaderFinder);
10
+
11
+ // config.module.rules.push({
12
+ // test: /node_modules\/vfile\//, // \/lib\/index\.js
13
+ // use: [
14
+ // {
15
+ // loader: 'imports-loader',
16
+ // options: {
17
+ // type: 'commonjs',
18
+ // // imports: ['single process/browser process'],
19
+ // },
20
+ // },
21
+ // ],
22
+ // });
23
+
24
+ const { include } = babelLoader;
25
+
26
+ include.push(markedPath);
27
+ // include.push(nodeFetch);
28
+
29
+ babelLoader.use[0].options.plugins = [
30
+ ...(babelLoader.use[0].options.plugins || []),
31
+ '@babel/plugin-proposal-private-methods',
32
+ ];
33
+
34
+ return config;
35
+ };
36
+
37
+ module.exports = {
38
+ plugins: (plugs) => plugs,
39
+ modify,
40
+ };
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { compose } from 'redux';
3
+ import superagent from 'superagent';
4
+ import { BlockDataForm, SidebarPortal } from '@plone/volto/components';
5
+
6
+ import ChatBlockView from './ChatBlockView';
7
+ import { ChatBlockSchema } from './schema';
8
+ import withOnyxData from './hocs/withOnyxData';
9
+
10
+ const ChatBlockEdit = (props) => {
11
+ const { onChangeBlock, block, assistants, data } = props;
12
+
13
+ const schema = React.useMemo(
14
+ () => ChatBlockSchema({ assistants, data }),
15
+ [assistants, data],
16
+ );
17
+
18
+ return (
19
+ <div>
20
+ <ChatBlockView {...props} isEditMode />
21
+ <SidebarPortal selected={props.selected}>
22
+ <BlockDataForm
23
+ schema={schema}
24
+ title={schema.title}
25
+ block={block}
26
+ onChangeBlock={onChangeBlock}
27
+ onChangeField={(id, value) => {
28
+ onChangeBlock(props.block, {
29
+ ...props.data,
30
+ [id]: value,
31
+ });
32
+ }}
33
+ formData={props.data}
34
+ />
35
+ </SidebarPortal>
36
+ </div>
37
+ );
38
+ };
39
+
40
+ export default compose(
41
+ withOnyxData(() => [
42
+ 'assistants',
43
+ superagent.get('/_da/persona?include_deleted=false').type('json'),
44
+ ]),
45
+ // withOnyxData(() => ['tool', superagent.get('/_da/tool').type('json')]), // May be needed in the future
46
+ )(ChatBlockEdit);
@@ -0,0 +1,21 @@
1
+ import superagent from 'superagent';
2
+ import withOnyxData from './hocs/withOnyxData';
3
+ import { ChatWindow } from './chat';
4
+
5
+ function ChatBlockView(props) {
6
+ const { assistantData, data, isEditMode } = props;
7
+
8
+ return assistantData ? (
9
+ <ChatWindow persona={assistantData} isEditMode={isEditMode} {...data} />
10
+ ) : (
11
+ <div>Chatbot</div>
12
+ );
13
+ }
14
+
15
+ export default withOnyxData((props) => [
16
+ 'assistantData',
17
+ typeof props.data?.assistant !== 'undefined'
18
+ ? superagent.get(`/_da/persona/${props.data.assistant}`).type('json')
19
+ : null,
20
+ props.data?.assistant,
21
+ ])(ChatBlockView);
@@ -0,0 +1,566 @@
1
+ import type { ChatMessageProps } from '../types/interfaces';
2
+ import { useState, useMemo, useEffect } from 'react';
3
+ import cx from 'classnames';
4
+ import visit from 'unist-util-visit';
5
+ import loadable from '@loadable/component';
6
+ import {
7
+ Tab,
8
+ Sidebar,
9
+ Button,
10
+ Message as SemanticMessage,
11
+ } from 'semantic-ui-react';
12
+ import { serializeNodes } from '@plone/volto-slate/editor/render';
13
+ import {
14
+ useDeepCompareMemoize,
15
+ useQualityMarkers,
16
+ useScrollonStream,
17
+ } from '../hooks';
18
+ import { MultiToolRenderer, RendererComponent } from '../packets';
19
+ import { addCitations } from '../utils/citations';
20
+ import SVGIcon from '../components/Icon';
21
+ import BotIcon from '../../icons/bot.svg';
22
+ import ClearIcon from '../../icons/clear.svg';
23
+
24
+ // Lazy load heavy components
25
+ const SourceDetails: any = loadable(() => import('../components/Source'));
26
+ const UserActionsToolbar: any = loadable(
27
+ () => import('../components/UserActionsToolbar'),
28
+ );
29
+ const RelatedQuestions: any = loadable(
30
+ () => import('../components/RelatedQuestions'),
31
+ );
32
+ const HalloumiFeedback: any = loadable(
33
+ () => import('../components/HalloumiFeedback'),
34
+ );
35
+
36
+ function capitalize(str: string) {
37
+ return str.charAt(0).toUpperCase() + str.slice(1);
38
+ }
39
+
40
+ function addQualityMarkersPlugin() {
41
+ return function (tree: any) {
42
+ visit(tree, 'element', function (node: any) {
43
+ node.children?.forEach((child: any, cidx: any) => {
44
+ if (child.type === 'raw' && child.value?.trim() === '<br>') {
45
+ const newNode = {
46
+ ...child,
47
+ type: 'element',
48
+ tagName: 'br',
49
+ children: [],
50
+ value: '',
51
+ };
52
+ node.children[cidx] = newNode;
53
+ }
54
+ });
55
+ });
56
+ visit(tree, 'text', function (node: any, idx: any, parent: any) {
57
+ if (node.value?.trim()) {
58
+ const newNode = {
59
+ type: 'element',
60
+ tagName: 'span',
61
+ children: [node],
62
+ };
63
+ parent.children[idx] = newNode;
64
+ }
65
+ });
66
+ };
67
+ }
68
+
69
+ export function addHalloumiContext(doc: any, text: string) {
70
+ const updatedDate = doc.updated_at
71
+ ? new Date(doc.updated_at).toLocaleString('en-GB', {
72
+ year: 'numeric',
73
+ month: 'long',
74
+ day: '2-digit',
75
+ hour: '2-digit',
76
+ minute: '2-digit',
77
+ })
78
+ : '';
79
+
80
+ const docIndex = doc.index ? `DOCUMENT ${doc.index}: ` : '';
81
+ const sources: any = { web: 'Website', file: 'File' };
82
+
83
+ const sourceType = doc.source_type
84
+ ? sources[doc.source_type] || capitalize(doc.source_type)
85
+ : '';
86
+
87
+ const header = `${docIndex}${doc.semantic_identifier}${
88
+ sourceType ? `\nSource: ${sourceType}` : ''
89
+ }${updatedDate ? `\nUpdated: ${updatedDate}` : ''}`;
90
+
91
+ return `${header}\n${text}`;
92
+ }
93
+
94
+ function mapToolDocumentsToText(message: any) {
95
+ if (!message?.toolCall?.tool_result) {
96
+ return {};
97
+ }
98
+
99
+ const toolResult = message.toolCall.tool_result;
100
+
101
+ if (Array.isArray(toolResult)) {
102
+ return toolResult.reduce((acc: Record<string, string>, doc: any) => {
103
+ if (doc.document_id && doc.content) {
104
+ acc[doc.document_id] = doc.content;
105
+ }
106
+ return acc;
107
+ }, {});
108
+ }
109
+
110
+ return {};
111
+ }
112
+
113
+ function getContextSources(
114
+ message: any,
115
+ sources: any,
116
+ qualityCheckContext: any,
117
+ ) {
118
+ const documentIdToText = mapToolDocumentsToText(message);
119
+
120
+ return qualityCheckContext === 'citations'
121
+ ? sources.map((doc: any) => ({
122
+ ...doc,
123
+ id: doc.document_id,
124
+ text: documentIdToText[doc.document_id] || '',
125
+ halloumiContext: addHalloumiContext(
126
+ doc,
127
+ documentIdToText[doc.document_id] || '',
128
+ ),
129
+ }))
130
+ : (message.toolCalls || []).reduce(
131
+ (acc: any, cur: any) => [
132
+ ...acc,
133
+ ...(cur.tool_result || []).map((doc: any) => ({
134
+ ...doc,
135
+ id: doc.document_id,
136
+ text: doc.content,
137
+ halloumiContext: addHalloumiContext(doc, doc.content),
138
+ })),
139
+ ], // TODO: make sure we don't add multiple times the same doc
140
+ // TODO: this doesn't have the index for source
141
+ [],
142
+ );
143
+ }
144
+
145
+ function getScoreDetails(claims: any, qualityCheckStages: any) {
146
+ const score = (
147
+ (claims.length > 0
148
+ ? claims.reduce((acc: any, { score }: any) => acc + score, 0) /
149
+ claims.length
150
+ : 1) * 100
151
+ ).toFixed(0);
152
+
153
+ const scoreStage = qualityCheckStages?.find(
154
+ ({ start, end }: any) => start <= score && score <= end,
155
+ );
156
+ const isFirstScoreStage =
157
+ qualityCheckStages?.reduce(
158
+ (acc: any, { start, end }: any, curIx: any) =>
159
+ start <= score && score <= end ? curIx : acc,
160
+ -1,
161
+ ) ?? -1;
162
+ const scoreColor = scoreStage?.color || 'black';
163
+ return { score, scoreStage, isFirstScoreStage, scoreColor };
164
+ }
165
+
166
+ export function AIMessage({
167
+ message,
168
+ isLoading,
169
+ libs,
170
+ onChoice,
171
+ onFetchRelatedQuestions,
172
+ enableFeedback,
173
+ scrollToInput,
174
+ feedbackReasons,
175
+ qualityCheck,
176
+ qualityCheckStages,
177
+ qualityCheckContext,
178
+ qualityCheckEnabled,
179
+ noSupportDocumentsMessage,
180
+ totalFailMessage,
181
+ isFetchingRelatedQuestions = false,
182
+ enableShowTotalFailMessage,
183
+ enableMatomoTracking,
184
+ persona,
185
+ maxContextSegments,
186
+ isLastMessage,
187
+ className = '',
188
+ chatWindowEndRef,
189
+ showTools,
190
+ }: ChatMessageProps) {
191
+ const [allToolsDisplayed, setAllToolsDisplayed] = useState(false);
192
+ const [messageDisplayed, setMessageDisplayed] = useState(false);
193
+ const [activeTab, setActiveTab] = useState(0);
194
+ const [showSourcesSidebar, setShowSourcesSidebar] = useState(false);
195
+ // Halloumi
196
+ const [forceHalloumi, setForceHallomi] = useState(qualityCheck === 'enabled');
197
+ const [verificationTriggered, setVerificationTriggered] = useState(false);
198
+ const [isMessageVerified, setIsMessageVerified] = useState(false);
199
+
200
+ useScrollonStream({
201
+ bottomRef: chatWindowEndRef,
202
+ isStreaming: isLoading || !messageDisplayed || isFetchingRelatedQuestions,
203
+ enabled: scrollToInput,
204
+ });
205
+
206
+ const {
207
+ groupedPackets = [],
208
+ toolPackets = [],
209
+ displayPackets = [],
210
+ citations = {},
211
+ documents = [],
212
+ relatedQuestions,
213
+ isComplete = false,
214
+ error,
215
+ } = message;
216
+
217
+ // Separate tool groups from display groups
218
+ const toolGroups = useMemo(() => {
219
+ return groupedPackets.filter((group) => toolPackets.includes(group.ind));
220
+ }, [groupedPackets, toolPackets]);
221
+
222
+ const displayGroups = useMemo(() => {
223
+ return groupedPackets.filter((group) => displayPackets.includes(group.ind));
224
+ }, [groupedPackets, displayPackets]);
225
+
226
+ const onAllToolsDisplayed = useMemo(() => {
227
+ return () => {
228
+ setAllToolsDisplayed(true);
229
+ };
230
+ }, []);
231
+
232
+ // Build sources from citations
233
+ const inverseMap = useMemo(
234
+ () =>
235
+ Object.entries(citations).reduce(
236
+ (acc, [k, v]) => {
237
+ return { ...acc, [v]: k };
238
+ },
239
+ {} as Record<string, string>,
240
+ ),
241
+ [citations],
242
+ );
243
+
244
+ const sources = useMemo(
245
+ () =>
246
+ Object.values(citations).map((doc_id) => {
247
+ const doc = documents?.find((doc: any) => doc.document_id === doc_id);
248
+ return {
249
+ ...(doc || {}),
250
+ index: inverseMap[doc_id],
251
+ };
252
+ }),
253
+ [citations, documents, inverseMap],
254
+ );
255
+
256
+ const showSources = messageDisplayed && sources.length > 0;
257
+
258
+ const contextSources = getContextSources(
259
+ message,
260
+ sources,
261
+ qualityCheckContext,
262
+ );
263
+
264
+ const stableContextSources = useDeepCompareMemoize(contextSources);
265
+
266
+ const doQualityControl =
267
+ messageDisplayed &&
268
+ qualityCheck &&
269
+ qualityCheck !== 'disabled' &&
270
+ forceHalloumi &&
271
+ showSources &&
272
+ message.messageId &&
273
+ message.messageId > -1 &&
274
+ (qualityCheck === 'enabled' ||
275
+ qualityCheckEnabled ||
276
+ verificationTriggered);
277
+
278
+ const { markers, isLoadingHalloumi, retryHalloumi }: any = useQualityMarkers(
279
+ doQualityControl,
280
+ addCitations(message.message, message),
281
+ stableContextSources,
282
+ maxContextSegments,
283
+ );
284
+
285
+ const claims = markers?.claims || [];
286
+ const { score, scoreStage, scoreColor, isFirstScoreStage } = getScoreDetails(
287
+ claims,
288
+ qualityCheckStages,
289
+ );
290
+
291
+ const isFetching = isLoadingHalloumi || isLoading;
292
+ const halloumiMessage =
293
+ isMessageVerified || doQualityControl ? scoreStage?.label : '';
294
+
295
+ const showVerifyClaimsButton =
296
+ messageDisplayed &&
297
+ sources.length > 0 &&
298
+ !isFetching &&
299
+ !markers &&
300
+ (qualityCheck === 'ondemand' ||
301
+ (qualityCheck === 'ondemand_toggle' && !qualityCheckEnabled));
302
+
303
+ const showTotalFailMessage =
304
+ messageDisplayed &&
305
+ sources.length === 0 &&
306
+ !isFetching &&
307
+ enableShowTotalFailMessage;
308
+
309
+ useEffect(() => {
310
+ if (isFetchingRelatedQuestions || typeof relatedQuestions !== 'undefined') {
311
+ return;
312
+ }
313
+ if (messageDisplayed && isComplete && onFetchRelatedQuestions) {
314
+ onFetchRelatedQuestions();
315
+ }
316
+ }, [
317
+ messageDisplayed,
318
+ relatedQuestions,
319
+ isComplete,
320
+ onFetchRelatedQuestions,
321
+ isFetchingRelatedQuestions,
322
+ ]);
323
+
324
+ useEffect(() => {
325
+ if (qualityCheck === 'ondemand_toggle' && qualityCheckEnabled) {
326
+ setForceHallomi(true);
327
+ } else if (qualityCheck !== 'enabled') {
328
+ setForceHallomi(false);
329
+ }
330
+ }, [qualityCheck, qualityCheckEnabled]);
331
+
332
+ useEffect(() => {
333
+ if (markers?.claims?.length > 0) {
334
+ setIsMessageVerified(true);
335
+ }
336
+ }, [markers]);
337
+
338
+ // Answer tab content
339
+ const answerTab = (
340
+ <div className="answer-tab">
341
+ {/* Show first 3 sources inline */}
342
+ {showSources && (
343
+ <div className="sources">
344
+ {sources.slice(0, 3).map((source: any, i: number) => (
345
+ <SourceDetails source={source} key={i} index={source.index} />
346
+ ))}
347
+
348
+ {sources.length > 3 && (
349
+ <Button
350
+ className="source show-all-sources-btn"
351
+ onClick={() => setShowSourcesSidebar(true)}
352
+ >
353
+ <div className="source-header">
354
+ <div>
355
+ {Array.from({ length: 3 }).map((_, i) => (
356
+ <span key={i} className="chat-citation"></span>
357
+ ))}
358
+ </div>
359
+ <div className="source-title">See all sources</div>
360
+ </div>
361
+ </Button>
362
+ )}
363
+ </div>
364
+ )}
365
+
366
+ {/* Main message content */}
367
+ <div className="message-content">
368
+ {/* Render tools if any */}
369
+ {toolGroups.length > 0 && (
370
+ <MultiToolRenderer
371
+ toolGroups={toolGroups}
372
+ showTools={showTools}
373
+ onAllToolsDisplayed={onAllToolsDisplayed}
374
+ message={message}
375
+ libs={libs}
376
+ />
377
+ )}
378
+
379
+ {/* Display error message if present */}
380
+ {!!error && (
381
+ <div className="message-error">
382
+ <SemanticMessage color="red" className="error-message">
383
+ <div className="error-title">Error</div>
384
+ <div className="error-content">{error}</div>
385
+ </SemanticMessage>
386
+ </div>
387
+ )}
388
+
389
+ {/* Display normal content if no error or if we have content to display alongside the error */}
390
+ {(allToolsDisplayed ||
391
+ toolGroups.length === 0 ||
392
+ message.isFinalMessageComing) &&
393
+ !error &&
394
+ displayGroups.map((group) => (
395
+ <div key={group.ind} className="message-display-group">
396
+ <RendererComponent
397
+ packets={group.packets}
398
+ onComplete={() => setMessageDisplayed(true)}
399
+ animate={!messageDisplayed}
400
+ stopPacketSeen={isComplete}
401
+ message={message}
402
+ libs={libs}
403
+ markers={markers}
404
+ stableContextSources={stableContextSources}
405
+ addQualityMarkersPlugin={addQualityMarkersPlugin}
406
+ >
407
+ {({ content }) => (
408
+ <div className="message-text-wrapper">{content}</div>
409
+ )}
410
+ </RendererComponent>
411
+ </div>
412
+ ))}
413
+ </div>
414
+
415
+ {/* Total fail message */}
416
+ {showTotalFailMessage && (
417
+ <SemanticMessage color="red">
418
+ {serializeNodes(totalFailMessage)}
419
+ </SemanticMessage>
420
+ )}
421
+
422
+ {/* Halloumi/Quality feedback */}
423
+ {qualityCheck !== 'disabled' && !error && (
424
+ <HalloumiFeedback
425
+ sources={sources}
426
+ halloumiMessage={halloumiMessage}
427
+ isLoadingHalloumi={isLoadingHalloumi}
428
+ markers={markers}
429
+ score={score}
430
+ scoreColor={scoreColor}
431
+ onManualVerify={() => {
432
+ setForceHallomi(true);
433
+ setVerificationTriggered(true);
434
+ }}
435
+ showVerifyClaimsButton={showVerifyClaimsButton}
436
+ retryHalloumi={retryHalloumi}
437
+ />
438
+ )}
439
+
440
+ {/* User actions toolbar (feedback, copy, etc) */}
441
+ {!isLoading && (
442
+ <UserActionsToolbar
443
+ message={message}
444
+ enableFeedback={enableFeedback}
445
+ feedbackReasons={feedbackReasons}
446
+ enableMatomoTracking={enableMatomoTracking}
447
+ persona={persona}
448
+ />
449
+ )}
450
+
451
+ {isFirstScoreStage === -1 && serializeNodes(noSupportDocumentsMessage)}
452
+
453
+ {isFetchingRelatedQuestions && isLastMessage && !error && (
454
+ <SemanticMessage color="blue">
455
+ <div className="related-questions-loader">
456
+ Finding related questions...
457
+ </div>
458
+ </SemanticMessage>
459
+ )}
460
+
461
+ {/* Related questions */}
462
+
463
+ {!error && (
464
+ <RelatedQuestions
465
+ persona={persona}
466
+ message={message}
467
+ isLoading={isLoading}
468
+ onChoice={onChoice}
469
+ enableMatomoTracking={enableMatomoTracking}
470
+ />
471
+ )}
472
+ </div>
473
+ );
474
+
475
+ // Tab panes - conditionally include Sources tab
476
+ const panes = [
477
+ { menuItem: 'Answer', pane: <Tab.Pane key="answer">{answerTab}</Tab.Pane> },
478
+ ...(showSources && !error
479
+ ? [
480
+ {
481
+ menuItem: {
482
+ key: 'sources',
483
+ content: (
484
+ <span>
485
+ Sources{' '}
486
+ <span className="sources-count">({sources.length})</span>
487
+ </span>
488
+ ),
489
+ },
490
+ pane: (
491
+ <Tab.Pane key="sources">
492
+ <div className="sources-listing">
493
+ <div className="sources">
494
+ {sources.map((source: any, i: number) => (
495
+ <SourceDetails
496
+ source={source}
497
+ key={i}
498
+ index={source.index}
499
+ />
500
+ ))}
501
+ </div>
502
+ </div>
503
+ </Tab.Pane>
504
+ ),
505
+ },
506
+ ]
507
+ : []),
508
+ ];
509
+
510
+ return (
511
+ <div className={`comment ${className}`}>
512
+ <div className="circle assistant">
513
+ <SVGIcon name={BotIcon} size={20} color="white" />
514
+ </div>
515
+
516
+ <div className="comment-content">
517
+ {/* Main content with tabs */}
518
+ <div className="comment-tabs">
519
+ <Tab
520
+ activeIndex={activeTab}
521
+ onTabChange={(_, data: any) => setActiveTab(data.activeIndex)}
522
+ menu={{
523
+ secondary: true,
524
+ pointing: true,
525
+ fluid: true,
526
+ className: cx({ 'without-sources': !showSources }),
527
+ }}
528
+ panes={panes}
529
+ renderActiveOnly={false}
530
+ />
531
+
532
+ {/* Sources sidebar */}
533
+ {showSources && !error && (
534
+ <Sidebar
535
+ visible={showSourcesSidebar}
536
+ animation="overlay"
537
+ icon="labeled"
538
+ width="wide"
539
+ direction="right"
540
+ className="sources-sidebar"
541
+ onHide={() => setShowSourcesSidebar(false)}
542
+ >
543
+ <div className="sources-sidebar-heading">
544
+ <h4>Sources</h4>
545
+ <Button basic onClick={() => setShowSourcesSidebar(false)}>
546
+ <SVGIcon name={ClearIcon} size={24} />
547
+ </Button>
548
+ </div>
549
+ <div className="sources-listing">
550
+ <div className="sources">
551
+ {sources.map((source: any, i: number) => (
552
+ <SourceDetails
553
+ source={source}
554
+ key={i}
555
+ index={source.index}
556
+ />
557
+ ))}
558
+ </div>
559
+ </div>
560
+ </Sidebar>
561
+ )}
562
+ </div>
563
+ </div>
564
+ </div>
565
+ );
566
+ }
@@ -0,0 +1,35 @@
1
+ import { Message as SemanticMessage } from 'semantic-ui-react';
2
+ import type { ChatMessageProps } from '../types/interfaces';
3
+ import { UserMessage, AIMessage } from '.';
4
+
5
+ export function ChatMessage(props: ChatMessageProps) {
6
+ const { message, libs, className = '' } = props;
7
+
8
+ if (message.type === 'user') {
9
+ return (
10
+ <UserMessage
11
+ message={message}
12
+ libs={libs}
13
+ className={className}
14
+ isLoading={props.isLoading}
15
+ />
16
+ );
17
+ }
18
+
19
+ if (message.type === 'assistant') {
20
+ return <AIMessage {...props} />;
21
+ }
22
+
23
+ if (message.type === 'error') {
24
+ return (
25
+ <div className="message-error">
26
+ <SemanticMessage color="red" className="error-message">
27
+ <div className="error-title">Error</div>
28
+ <div className="error-content">{message.error}</div>
29
+ </SemanticMessage>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ return null;
35
+ }