@eeacms/volto-eea-chatbot 1.0.10 → 1.0.12

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 (62) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +8 -8
  3. package/jest-addon.config.js +1 -1
  4. package/package.json +2 -1
  5. package/src/ChatBlock/ChatBlockView.jsx +26 -2
  6. package/src/ChatBlock/chat/AIMessage.tsx +37 -26
  7. package/src/ChatBlock/chat/ChatWindow.tsx +13 -3
  8. package/src/ChatBlock/components/AutoResizeTextarea.jsx +2 -1
  9. package/src/ChatBlock/components/HalloumiFeedback.jsx +8 -4
  10. package/src/ChatBlock/components/QualityCheckToggle.jsx +1 -1
  11. package/src/ChatBlock/components/markdown/ClaimModal.jsx +1 -1
  12. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +2 -3
  13. package/src/ChatBlock/components/markdown/RenderClaimView.jsx +1 -1
  14. package/src/ChatBlock/components/markdown/index.js +41 -15
  15. package/src/ChatBlock/hooks/useChatController.ts +8 -15
  16. package/src/ChatBlock/hooks/useQualityMarkers.js +0 -11
  17. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -0
  18. package/src/ChatBlock/services/streamingService.ts +30 -26
  19. package/src/ChatBlock/style.less +50 -1
  20. package/src/ChatBlock/tests/ChatMessage.test.jsx +75 -0
  21. package/src/ChatBlock/tests/ClaimModal.test.jsx +136 -0
  22. package/src/ChatBlock/tests/ClaimSegments.test.jsx +206 -0
  23. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +241 -0
  24. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +161 -0
  25. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +178 -0
  26. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +227 -0
  27. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +134 -0
  28. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +163 -0
  29. package/src/ChatBlock/tests/RenderClaimView.test.jsx +191 -0
  30. package/src/ChatBlock/tests/RendererComponent.test.jsx +139 -0
  31. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +295 -0
  32. package/src/ChatBlock/tests/SourceChip.test.jsx +108 -0
  33. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +135 -0
  34. package/src/ChatBlock/tests/UserMessage.test.jsx +83 -0
  35. package/src/ChatBlock/tests/WebResultIcon.test.jsx +61 -0
  36. package/src/ChatBlock/tests/citations.test.js +114 -0
  37. package/src/ChatBlock/tests/messageProcessor.test.jsx +285 -1
  38. package/src/ChatBlock/tests/packetUtils.test.js +158 -0
  39. package/src/ChatBlock/tests/streamingService.test.js +467 -0
  40. package/src/ChatBlock/tests/useChatController.test.jsx +268 -0
  41. package/src/ChatBlock/tests/useChatStreaming.test.jsx +163 -0
  42. package/src/ChatBlock/tests/useMarked.test.jsx +107 -0
  43. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +150 -0
  44. package/src/ChatBlock/tests/useScrollonStream.test.jsx +121 -0
  45. package/src/ChatBlock/tests/utils.test.jsx +241 -0
  46. package/src/ChatBlock/tests/withOnyxData.test.jsx +81 -0
  47. package/src/ChatBlock/types/interfaces.ts +1 -0
  48. package/src/ChatBlock/utils/citations.ts +1 -1
  49. package/src/halloumi/filtering.js +143 -0
  50. package/src/halloumi/filtering.test.js +44 -0
  51. package/src/halloumi/generative.js +154 -54
  52. package/src/halloumi/generative.test.js +299 -1
  53. package/src/halloumi/markdown-splitter.js +172 -0
  54. package/src/halloumi/markdown-splitter.test.js +133 -0
  55. package/src/halloumi/middleware.js +5 -6
  56. package/src/halloumi/middleware.test.js +69 -0
  57. package/src/halloumi/postprocessing.js +0 -26
  58. package/src/halloumi/preprocessing.js +78 -76
  59. package/src/halloumi/preprocessing.test.js +87 -148
  60. package/src/index.js +1 -0
  61. package/src/middleware.js +21 -13
  62. package/src/middleware.test.js +221 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.0.12](https://github.com/eea/volto-eea-chatbot/compare/1.0.11...1.0.12) - 23 February 2026
8
+
9
+ #### :house: Internal changes
10
+
11
+ - style: Automated code fix [eea-jenkins - [`fdcd884`](https://github.com/eea/volto-eea-chatbot/commit/fdcd8848fd4c3f990ca5ea021f407487aebd6010)]
12
+ - chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`3d428d7`](https://github.com/eea/volto-eea-chatbot/commit/3d428d72f32c3d05452b0961c76f5db1c416e05c)]
13
+
14
+ #### :hammer_and_wrench: Others
15
+
16
+ - fix tests [Miu Razvan - [`1b08a47`](https://github.com/eea/volto-eea-chatbot/commit/1b08a4760f0f16c0a3c2a5be295196f510b8e4f0)]
17
+ - fix tests [Miu Razvan - [`0e1c2a2`](https://github.com/eea/volto-eea-chatbot/commit/0e1c2a23cf17d0ead25132c126892778a74954e0)]
18
+ - update [Miu Razvan - [`b9d3066`](https://github.com/eea/volto-eea-chatbot/commit/b9d306623b9034eb08fc77781a48e4ca59146e54)]
19
+ - Filter non-verifiable sentences [Miu Razvan - [`988a4c7`](https://github.com/eea/volto-eea-chatbot/commit/988a4c71293fda99ffaf02d9750e137332d2a182)]
20
+ ### [1.0.11](https://github.com/eea/volto-eea-chatbot/compare/1.0.10...1.0.11) - 7 February 2026
21
+
22
+ #### :house: Internal changes
23
+
24
+ - style: Automated code fix [eea-jenkins - [`0a2e3c8`](https://github.com/eea/volto-eea-chatbot/commit/0a2e3c8d000d48dcd949bd3332c7692b2248be53)]
25
+ - chore: JENKINS remove java installation [valentinab25 - [`f224875`](https://github.com/eea/volto-eea-chatbot/commit/f2248756df7dfc89cfe2aea5609473092f331cc6)]
26
+ - style: Automated code fix [eea-jenkins - [`26ae106`](https://github.com/eea/volto-eea-chatbot/commit/26ae106e8364ce7a7d00e75d72c1d77d4cfb7385)]
27
+
28
+ #### :hammer_and_wrench: Others
29
+
30
+ - Update tests [Miu Razvan - [`bfe9e0f`](https://github.com/eea/volto-eea-chatbot/commit/bfe9e0f6bfb446a8e59996fb87d5a151bc895c3f)]
31
+ - fix eslint [Miu Razvan - [`9fecdf7`](https://github.com/eea/volto-eea-chatbot/commit/9fecdf747a9c00063c4cf865f515cf1c88f5afc7)]
32
+ - fix eslint [Miu Razvan - [`25b2eba`](https://github.com/eea/volto-eea-chatbot/commit/25b2ebaf1ba7eb186285e8fa1e51e625167a7fd7)]
33
+ - improve coverage to 80% [Miu Razvan - [`7bed26f`](https://github.com/eea/volto-eea-chatbot/commit/7bed26fd8ee9622755f43c5bc625e7a60c5cc09c)]
34
+ - update jest snapshots [Miu Razvan - [`e9e0732`](https://github.com/eea/volto-eea-chatbot/commit/e9e073286fd41fc8f22a5eba248a6cc34eda57d0)]
7
35
  ### [1.0.10](https://github.com/eea/volto-eea-chatbot/compare/1.0.9...1.0.10) - 27 January 2026
8
36
 
9
37
  #### :hammer_and_wrench: Others
package/README.md CHANGED
@@ -3,16 +3,16 @@
3
3
  [![Releases](https://img.shields.io/github/v/release/eea/volto-eea-chatbot)](https://github.com/eea/volto-eea-chatbot/releases)
4
4
 
5
5
  [![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-eea-chatbot%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-chatbot/job/master/display/redirect)
6
- [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-master&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-master)
7
- [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-master&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-master)
8
- [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-master&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-master)
9
- [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-master&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-master)
6
+ [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
7
+ [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
8
+ [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
9
+ [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
10
10
 
11
11
  [![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-eea-chatbot%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-chatbot/job/develop/display/redirect)
12
- [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-develop)
13
- [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-develop)
14
- [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-develop)
15
- [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot-develop)
12
+ [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&branch=develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
13
+ [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&branch=develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
14
+ [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&branch=develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
15
+ [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&branch=develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
16
16
 
17
17
  [Volto](https://github.com/plone/volto) add-on that integrates an AI-powered chatbot with a customizable interface and advanced settings to tailor its behavior and enhance user interactions.
18
18
 
@@ -430,7 +430,7 @@ module.exports = {
430
430
  '<rootDir>/node_modules/@plone/volto/jest-addons-loader.js',
431
431
  },
432
432
  transformIgnorePatterns: [
433
- '/node_modules/(?!(@plone|@root|@package|@eeacms)/).*/',
433
+ '/node_modules/(?!(@plone|@root|@package|@eeacms|compromise|efrt|grad-school|suffix-thumb)/).*/',
434
434
  ],
435
435
  transform: {
436
436
  '^.+\\.js(x)?$': 'babel-jest',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-chatbot",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "@eeacms/volto-eea-chatbot: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -43,6 +43,7 @@
43
43
  "@eeacms/volto-matomo": "*",
44
44
  "@microsoft/fetch-event-source": "2.0.1",
45
45
  "@plone-collective/volto-sentry": "*",
46
+ "compromise": "14.14.5",
46
47
  "fast-json-patch": "3.1.1",
47
48
  "highlight.js": "11.10.0",
48
49
  "luxon": "3.5.0",
@@ -1,12 +1,36 @@
1
+ import { useMemo, useEffect } from 'react';
1
2
  import superagent from 'superagent';
3
+ import { parse } from 'qs';
2
4
  import withOnyxData from './hocs/withOnyxData';
3
5
  import { ChatWindow } from './chat';
4
6
 
5
7
  function ChatBlockView(props) {
6
- const { assistantData, data, isEditMode } = props;
8
+ const { id, assistantData, data, isEditMode, location } = props;
9
+
10
+ const query = useMemo(
11
+ () => parse(location?.search.replace('?', '')) || {},
12
+ [location],
13
+ );
14
+
15
+ const isPlaywrightTest = query.playwright === 'yes';
16
+
17
+ useEffect(() => {
18
+ if (isPlaywrightTest) {
19
+ window.__EEA_CHATBOT_TEST_CONFIG__ = {
20
+ block_id: id,
21
+ ...data,
22
+ };
23
+ }
24
+ }, [id, isPlaywrightTest, data]);
7
25
 
8
26
  return assistantData ? (
9
- <ChatWindow persona={assistantData} isEditMode={isEditMode} {...data} />
27
+ <ChatWindow
28
+ persona={assistantData}
29
+ isEditMode={isEditMode}
30
+ isPlaywrightTest={isPlaywrightTest}
31
+ block_id={id}
32
+ {...data}
33
+ />
10
34
  ) : (
11
35
  <div>Chatbot</div>
12
36
  );
@@ -67,28 +67,30 @@ function addQualityMarkersPlugin() {
67
67
  }
68
68
 
69
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}`;
70
+ // TODO: CLEAN UP
71
+ // const updatedDate = doc.updated_at
72
+ // ? new Date(doc.updated_at).toLocaleString('en-GB', {
73
+ // year: 'numeric',
74
+ // month: 'long',
75
+ // day: '2-digit',
76
+ // hour: '2-digit',
77
+ // minute: '2-digit',
78
+ // })
79
+ // : '';
80
+
81
+ // const docIndex = doc.index ? `DOCUMENT ${doc.index}: ` : '';
82
+ // const sources: any = { web: 'Website', file: 'File' };
83
+
84
+ // const sourceType = doc.source_type
85
+ // ? sources[doc.source_type] || capitalize(doc.source_type)
86
+ // : '';
87
+
88
+ // const header = `${docIndex}${doc.semantic_identifier}${
89
+ // sourceType ? `\nSource: ${sourceType}` : ''
90
+ // }${updatedDate ? `\nUpdated: ${updatedDate}` : ''}`;
91
+
92
+ // return `${header}\n${text}`;
93
+ return text.replace(/\u00A0/g, ' ');
92
94
  }
93
95
 
94
96
  function mapToolDocumentsToText(message: any) {
@@ -142,11 +144,13 @@ function getContextSources(
142
144
  );
143
145
  }
144
146
 
145
- function getScoreDetails(claims: any, qualityCheckStages: any) {
147
+ function getScoreDetails(rawClaims: any, qualityCheckStages: any) {
148
+ const claims = rawClaims.filter((claim: any) => !claim.skipped);
146
149
  const score = (
147
150
  (claims.length > 0
148
- ? claims.reduce((acc: any, { score }: any) => acc + score, 0) /
149
- claims.length
151
+ ? claims
152
+ .filter((claim: any) => !claim.skipped)
153
+ .reduce((acc: any, { score }: any) => acc + score, 0) / claims.length
150
154
  : 1) * 100
151
155
  ).toFixed(0);
152
156
 
@@ -165,6 +169,7 @@ function getScoreDetails(claims: any, qualityCheckStages: any) {
165
169
 
166
170
  export function AIMessage({
167
171
  message,
172
+ prevMessage,
168
173
  isLoading,
169
174
  libs,
170
175
  onChoice,
@@ -283,6 +288,7 @@ export function AIMessage({
283
288
  );
284
289
 
285
290
  const claims = markers?.claims || [];
291
+ const emptyClaims = markers?.empty || false;
286
292
  const { score, scoreStage, scoreColor, isFirstScoreStage } = getScoreDetails(
287
293
  claims,
288
294
  qualityCheckStages,
@@ -434,6 +440,7 @@ export function AIMessage({
434
440
  }}
435
441
  showVerifyClaimsButton={showVerifyClaimsButton}
436
442
  retryHalloumi={retryHalloumi}
443
+ emptyClaims={emptyClaims}
437
444
  />
438
445
  )}
439
446
 
@@ -474,7 +481,10 @@ export function AIMessage({
474
481
 
475
482
  // Tab panes - conditionally include Sources tab
476
483
  const panes = [
477
- { menuItem: 'Answer', pane: <Tab.Pane key="answer">{answerTab}</Tab.Pane> },
484
+ {
485
+ menuItem: { key: 'answer', content: 'Answer', className: 'answer-tab' },
486
+ pane: <Tab.Pane key="answer">{answerTab}</Tab.Pane>,
487
+ },
478
488
  ...(showSources && !error
479
489
  ? [
480
490
  {
@@ -486,6 +496,7 @@ export function AIMessage({
486
496
  <span className="sources-count">({sources.length})</span>
487
497
  </span>
488
498
  ),
499
+ className: 'sources-tab',
489
500
  },
490
501
  pane: (
491
502
  <Tab.Pane key="sources">
@@ -22,6 +22,7 @@ import PenIcon from '../../icons/square-pen.svg';
22
22
  import '../style.less';
23
23
 
24
24
  interface ChatWindowProps {
25
+ block_id?: string;
25
26
  persona: Persona;
26
27
  rehypePrism?: any;
27
28
  remarkGfm?: any;
@@ -47,15 +48,18 @@ interface ChatWindowProps {
47
48
  enableMatomoTracking?: boolean;
48
49
  onDemandInputToggle?: boolean;
49
50
  maxContextSegments?: number;
51
+ isPlaywrightTest?: boolean;
50
52
  [key: string]: any;
51
53
  }
52
54
 
53
55
  function ChatWindow({
56
+ block_id,
54
57
  persona,
55
58
  rehypePrism,
56
59
  remarkGfm,
57
60
  placeholderPrompt = 'Ask a question',
58
61
  isEditMode,
62
+ isPlaywrightTest,
59
63
  ...data
60
64
  }: ChatWindowProps) {
61
65
  const {
@@ -141,7 +145,10 @@ function ChatWindow({
141
145
  );
142
146
 
143
147
  return (
144
- <div className="chat-window">
148
+ <div
149
+ className="chat-window"
150
+ data-playwright-block-id={isPlaywrightTest ? block_id : undefined}
151
+ >
145
152
  <div className="messages">
146
153
  {showLandingPage ? (
147
154
  <>
@@ -178,6 +185,7 @@ function ChatWindow({
178
185
  <React.Fragment>
179
186
  <ChatMessage
180
187
  key={message.messageId}
188
+ prevMessage={messages[index - 1]}
181
189
  message={message}
182
190
  isLoading={isStreaming}
183
191
  isDeepResearchEnabled={isDeepResearchEnabled}
@@ -211,7 +219,7 @@ function ChatWindow({
211
219
 
212
220
  {isStreaming &&
213
221
  !isFetchingRelatedQuestions &&
214
- !messages[messages.length - 1].isFinalMessageComing && (
222
+ !messages[messages.length - 1]?.isFinalMessageComing && (
215
223
  <div className="comment">
216
224
  <div className="circle assistant placeholder"></div>
217
225
  <div className="comment-content">
@@ -269,7 +277,9 @@ function ChatWindow({
269
277
  </div>
270
278
  )}
271
279
 
272
- {deepResearch === 'always_on' && <small>Deep research on</small>}
280
+ {deepResearch === 'always_on' && (
281
+ <small className="deep-research-toggle">Deep research on</small>
282
+ )}
273
283
  </div>
274
284
  <div ref={chatWindowEndRef} /> {/* End div to mark the bottom */}
275
285
  </div>
@@ -40,6 +40,7 @@ export default React.forwardRef(function AutoResizeTextarea(props, ref) {
40
40
  setInput(input + '\n');
41
41
  }
42
42
  }}
43
+ disabled={isStreaming}
43
44
  {...rest}
44
45
  ref={ref}
45
46
  />
@@ -52,7 +53,7 @@ export default React.forwardRef(function AutoResizeTextarea(props, ref) {
52
53
  onKeyDown={(e) => {
53
54
  handleSubmit(e);
54
55
  }}
55
- disabled={isStreaming}
56
+ disabled={isStreaming || input.trim() === ''}
56
57
  onClick={(e) => {
57
58
  handleSubmit(e);
58
59
  }}
@@ -72,8 +72,10 @@ const HalloumiFeedback = ({
72
72
  showVerifyClaimsButton,
73
73
  sources,
74
74
  retryHalloumi,
75
+ emptyClaims,
75
76
  }) => {
76
- const noClaimsScore = markers?.claims[0]?.score === null;
77
+ const claims = (markers?.claims || []).filter((claim) => !claim.skipped);
78
+ const noClaimsScore = claims[0]?.score === null;
77
79
  const messageBySource =
78
80
  'Please allow a few minutes for claim verification when many references are involved.';
79
81
 
@@ -98,7 +100,7 @@ const HalloumiFeedback = ({
98
100
 
99
101
  {noClaimsScore && (
100
102
  <>
101
- <Message color="red">{markers?.claims?.[0].rationale}</Message>
103
+ <Message color="red">{claims[0].rationale}</Message>
102
104
  <Button onClick={retryHalloumi} className="icon">
103
105
  <SVGIcon name={RotateIcon} /> Retry Fact-check AI answer
104
106
  </Button>
@@ -110,12 +112,14 @@ const HalloumiFeedback = ({
110
112
  color={scoreColor}
111
113
  className={cx(
112
114
  'claim-message',
113
- getSupportedBgColor(score / 100, 'claim'),
115
+ emptyClaims
116
+ ? 'claim-empty claim-gray-500'
117
+ : getSupportedBgColor(score / 100, 'claim'),
114
118
  )}
115
119
  icon
116
120
  >
117
121
  <MessageContent>
118
- {printSlate(halloumiMessage, `${score}%`)}
122
+ {emptyClaims || printSlate(halloumiMessage, `${score}%`)}
119
123
  </MessageContent>
120
124
  </Message>
121
125
  )}
@@ -10,7 +10,7 @@ const QualityCheckToggle = ({ isEditMode, enabled, setEnabled }) => {
10
10
  content="Checks the AI's statements against cited sources to highlight possible inaccuracies and hallucinations."
11
11
  trigger={
12
12
  <Checkbox
13
- id="fact-check-toggle"
13
+ id="quality-check-toggle"
14
14
  toggle
15
15
  label="Fact-check AI answer"
16
16
  disabled={isEditMode}
@@ -30,7 +30,7 @@ const trimNonAlphanumeric = (str) =>
30
30
  stripMarkdown(str).replace(/(?:^[^a-zA-Z0-9]+)|(?:[^a-zA-Z0-9]+$)/g, '');
31
31
 
32
32
  export function ClaimModal({ claim, markers, text, citedSources }) {
33
- const highlightText = trimNonAlphanumeric(text?.[0] || '');
33
+ const highlightText = trimNonAlphanumeric(text || '');
34
34
 
35
35
  return (
36
36
  <Modal
@@ -10,9 +10,8 @@ const VISIBLE_SEGMENTS = 50; // Number of citations to show by default
10
10
 
11
11
  export function ClaimSegments({ segmentIds, segments, citedSources }) {
12
12
  const joinedSources = citedSources.reduce((acc, source) => {
13
- source.startIndex = acc.length ? acc.length + 1 : 0;
14
- const sep = acc ? '\n' : '';
15
- return acc + sep + source.halloumiContext; // + '\n---\n';
13
+ source.startIndex = acc.length;
14
+ return acc + source.halloumiContext;
16
15
  }, '');
17
16
 
18
17
  const snippets = (segmentIds || [])
@@ -19,7 +19,7 @@ export const RenderClaimView = (props) => {
19
19
 
20
20
  sortedSegments.forEach((segment) => {
21
21
  const segmentStart = segment.startOffset - sourceStartIndex;
22
- const segmentEnd = segment.endOffset - sourceStartIndex;
22
+ const segmentEnd = segment.endOffset - sourceStartIndex + 1;
23
23
 
24
24
  // Add the text part before the current segment
25
25
  if (segmentStart > lastIndex) {
@@ -33,31 +33,57 @@ export function components(message, markers, citedSources) {
33
33
  return <td {...rest}>{processedChildren}</td>;
34
34
  },
35
35
  span: (props) => {
36
- const { node, ...rest } = props;
36
+ const { node, children } = props;
37
37
  const child = node.children[0];
38
- let claim;
39
38
 
40
39
  // identifies if the current text belongs to a claim
41
40
  if (child.type === 'text' && child.position && markers) {
41
+ const text = child.value || '';
42
42
  const start = child.position.start.offset;
43
43
  const end = child.position.end.offset;
44
- claim = markers.claims?.find(
44
+ const claims = markers.claims?.filter(
45
45
  (claim) =>
46
- (start >= claim.startOffset && end <= claim.endOffset) ||
47
- (claim.startOffset >= start && end <= claim.endOffset),
46
+ claim.score !== null &&
47
+ ((start >= claim.startOffset && end <= claim.endOffset) ||
48
+ (start <= claim.endOffset && end >= claim.endOffset) ||
49
+ (start <= claim.startOffset && end >= claim.startOffset)),
48
50
  );
51
+
52
+ if (claims && claims.length > 0) {
53
+ let relStart = 0;
54
+ const claimsSegments = claims.map((claim) => ({
55
+ claim,
56
+ start: Math.max(0, claim.startOffset - start),
57
+ end: Math.min(text.length, claim.endOffset - start),
58
+ }));
59
+ const segments = claimsSegments.reduce((acc, segment) => {
60
+ if (relStart < segment.start) {
61
+ acc.push(child.value.substring(relStart, segment.start));
62
+ }
63
+ const claimText = child.value.substring(segment.start, segment.end);
64
+ acc.push(
65
+ <ClaimModal
66
+ claim={segment.claim}
67
+ markers={markers}
68
+ text={claimText}
69
+ citedSources={citedSources}
70
+ />,
71
+ );
72
+ relStart = segment.end;
73
+ return acc;
74
+ }, []);
75
+
76
+ if (relStart < text.length) {
77
+ segments.push(text.substring(relStart));
78
+ }
79
+
80
+ return segments;
81
+ }
82
+
83
+ return text;
49
84
  }
50
85
 
51
- return !claim || claim?.score === null ? (
52
- rest.children || []
53
- ) : (
54
- <ClaimModal
55
- claim={claim}
56
- markers={markers}
57
- text={rest.children}
58
- citedSources={citedSources}
59
- />
60
- );
86
+ return children || [];
61
87
  },
62
88
  a: (props) => {
63
89
  const { node, children, href, ...rest } = props;
@@ -18,19 +18,8 @@ interface RelatedQuestion {
18
18
 
19
19
  // Extract JSON array from related questions response
20
20
  function extractRelatedQuestions(str: string): RelatedQuestion[] {
21
- const regex = /\[[\s\S]*?\]/;
22
- const match = str.match(regex);
23
-
24
- if (match) {
25
- try {
26
- return JSON.parse(match[0]);
27
- } catch {
28
- // Fallback to line-by-line parsing
29
- return str
30
- .split('\n')
31
- .filter((line) => line.trim())
32
- .map((question) => ({ question }));
33
- }
21
+ if (str.toLowerCase().includes('no_response')) {
22
+ throw new Error('Related questions were not generated properly');
34
23
  }
35
24
 
36
25
  return str
@@ -65,7 +54,7 @@ async function fetchRelatedQuestions(
65
54
  };
66
55
 
67
56
  let result = '';
68
- for await (const packets of sendMessage(params)) {
57
+ for await (const packets of sendMessage(params, true)) {
69
58
  for (const packet of packets) {
70
59
  if (packet.obj.type === PacketType.MESSAGE_DELTA) {
71
60
  result += packet.obj.content;
@@ -88,6 +77,7 @@ export function useChatController({
88
77
  }: UseChatControllerProps) {
89
78
  const [messages, setMessages] = useState<Message[]>([]);
90
79
  const [chatSessionId, setChatSessionId] = useState<string | null>(null);
80
+ const [chatSessionLoading, setChatSessionLoading] = useState(false);
91
81
  const [isDeepResearchEnabled, setIsDeepResearchEnabled] = useState(
92
82
  deepResearch === 'always_on' || deepResearch === 'user_on',
93
83
  );
@@ -183,6 +173,7 @@ export function useChatController({
183
173
  let sessionId = chatSessionId;
184
174
 
185
175
  if (!sessionId) {
176
+ setChatSessionLoading(true);
186
177
  sessionId = await createChatSession(personaId, 'Chat session');
187
178
  setChatSessionId(sessionId);
188
179
  }
@@ -241,6 +232,8 @@ export function useChatController({
241
232
  );
242
233
  } catch (error) {
243
234
  console.error('Failed to submit message:', error);
235
+ } finally {
236
+ setChatSessionLoading(false);
244
237
  }
245
238
  },
246
239
  [
@@ -319,7 +312,7 @@ export function useChatController({
319
312
 
320
313
  return {
321
314
  messages,
322
- isStreaming,
315
+ isStreaming: isStreaming || chatSessionLoading,
323
316
  isCancelled,
324
317
  isFetchingRelatedQuestions,
325
318
  onSubmit,
@@ -54,17 +54,6 @@ export function useQualityMarkers(
54
54
  return;
55
55
  }
56
56
 
57
- // // console.log('Halloumi sources:', sources.length, sources);
58
- // if (sources.length > 40) {
59
- // // eslint-disable-next-line no-console
60
- // console.warn(
61
- // `Warning: Too many sources (${sources.length}). Skipping quality control.`,
62
- // );
63
- //
64
- // setHalloumiResponse(empty(message, TOOLARGE_RATIONALE));
65
- // return;
66
- // }
67
-
68
57
  setIsLoading(true);
69
58
 
70
59
  try {
@@ -46,6 +46,14 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
46
46
  const [displayedPacketCount, setDisplayedPacketCount] =
47
47
  useState(initialPacketCount);
48
48
 
49
+ useEffect(() => {
50
+ const audioCtx = new AudioContext();
51
+
52
+ return () => {
53
+ audioCtx.close();
54
+ };
55
+ }, []);
56
+
49
57
  // Animation effect - gradually increase displayed packets at controlled rate
50
58
  // Adaptive animation: ensures visible typing effect even for fast streams
51
59
  useEffect(() => {
@@ -8,7 +8,7 @@ export interface SendMessageParams {
8
8
  parentMessageId: number | null;
9
9
  chatSessionId: string;
10
10
  filters: Filters | null;
11
- selectedDocumentIds: number[] | null;
11
+ selectedDocumentIds: number[] | string[] | null;
12
12
  queryOverride?: string;
13
13
  forceSearch?: boolean;
14
14
  modelProvider?: string;
@@ -166,30 +166,33 @@ export async function* handleStream(
166
166
  /**
167
167
  * Send a message and stream the response
168
168
  */
169
- export async function* sendMessage({
170
- regenerate,
171
- retrieval_options,
172
- message,
173
- fileDescriptors,
174
- currentMessageFiles,
175
- parentMessageId,
176
- chatSessionId,
177
- filters,
178
- selectedDocumentIds,
179
- queryOverride,
180
- forceSearch,
181
- modelProvider,
182
- modelVersion,
183
- temperature,
184
- systemPromptOverride,
185
- taskPromptOverride,
186
- useExistingUserMessage,
187
- alternateAssistantId,
188
- signal,
189
- useAgentSearch,
190
- enabledToolIds,
191
- forcedToolIds,
192
- }: SendMessageParams): AsyncGenerator<Packet[], void, unknown> {
169
+ export async function* sendMessage(
170
+ {
171
+ regenerate,
172
+ retrieval_options,
173
+ message,
174
+ fileDescriptors,
175
+ currentMessageFiles,
176
+ parentMessageId,
177
+ chatSessionId,
178
+ filters,
179
+ selectedDocumentIds,
180
+ queryOverride,
181
+ forceSearch,
182
+ modelProvider,
183
+ modelVersion,
184
+ temperature,
185
+ systemPromptOverride,
186
+ taskPromptOverride,
187
+ useExistingUserMessage,
188
+ alternateAssistantId,
189
+ signal,
190
+ useAgentSearch,
191
+ enabledToolIds,
192
+ forcedToolIds,
193
+ }: SendMessageParams,
194
+ isRelatedQuestion: boolean = false,
195
+ ): AsyncGenerator<Packet[], void, unknown> {
193
196
  const documentsAreSelected =
194
197
  selectedDocumentIds && selectedDocumentIds.length > 0;
195
198
 
@@ -233,7 +236,8 @@ export async function* sendMessage({
233
236
 
234
237
  const body = JSON.stringify(payload);
235
238
 
236
- const sendMessageResponse = await fetch('/_da/chat/send-message', {
239
+ const middleware = isRelatedQuestion ? '_rq' : '_da';
240
+ const sendMessageResponse = await fetch(`/${middleware}/chat/send-message`, {
237
241
  method: 'POST',
238
242
  headers: {
239
243
  'Content-Type': 'application/json',