@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.
- package/CHANGELOG.md +28 -0
- package/README.md +8 -8
- package/jest-addon.config.js +1 -1
- package/package.json +2 -1
- package/src/ChatBlock/ChatBlockView.jsx +26 -2
- package/src/ChatBlock/chat/AIMessage.tsx +37 -26
- package/src/ChatBlock/chat/ChatWindow.tsx +13 -3
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +2 -1
- package/src/ChatBlock/components/HalloumiFeedback.jsx +8 -4
- package/src/ChatBlock/components/QualityCheckToggle.jsx +1 -1
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +1 -1
- package/src/ChatBlock/components/markdown/ClaimSegments.jsx +2 -3
- package/src/ChatBlock/components/markdown/RenderClaimView.jsx +1 -1
- package/src/ChatBlock/components/markdown/index.js +41 -15
- package/src/ChatBlock/hooks/useChatController.ts +8 -15
- package/src/ChatBlock/hooks/useQualityMarkers.js +0 -11
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -0
- package/src/ChatBlock/services/streamingService.ts +30 -26
- package/src/ChatBlock/style.less +50 -1
- package/src/ChatBlock/tests/ChatMessage.test.jsx +75 -0
- package/src/ChatBlock/tests/ClaimModal.test.jsx +136 -0
- package/src/ChatBlock/tests/ClaimSegments.test.jsx +206 -0
- package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +241 -0
- package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +161 -0
- package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +178 -0
- package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +227 -0
- package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +134 -0
- package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +163 -0
- package/src/ChatBlock/tests/RenderClaimView.test.jsx +191 -0
- package/src/ChatBlock/tests/RendererComponent.test.jsx +139 -0
- package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +295 -0
- package/src/ChatBlock/tests/SourceChip.test.jsx +108 -0
- package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +135 -0
- package/src/ChatBlock/tests/UserMessage.test.jsx +83 -0
- package/src/ChatBlock/tests/WebResultIcon.test.jsx +61 -0
- package/src/ChatBlock/tests/citations.test.js +114 -0
- package/src/ChatBlock/tests/messageProcessor.test.jsx +285 -1
- package/src/ChatBlock/tests/packetUtils.test.js +158 -0
- package/src/ChatBlock/tests/streamingService.test.js +467 -0
- package/src/ChatBlock/tests/useChatController.test.jsx +268 -0
- package/src/ChatBlock/tests/useChatStreaming.test.jsx +163 -0
- package/src/ChatBlock/tests/useMarked.test.jsx +107 -0
- package/src/ChatBlock/tests/useQualityMarkers.test.jsx +150 -0
- package/src/ChatBlock/tests/useScrollonStream.test.jsx +121 -0
- package/src/ChatBlock/tests/utils.test.jsx +241 -0
- package/src/ChatBlock/tests/withOnyxData.test.jsx +81 -0
- package/src/ChatBlock/types/interfaces.ts +1 -0
- package/src/ChatBlock/utils/citations.ts +1 -1
- package/src/halloumi/filtering.js +143 -0
- package/src/halloumi/filtering.test.js +44 -0
- package/src/halloumi/generative.js +154 -54
- package/src/halloumi/generative.test.js +299 -1
- package/src/halloumi/markdown-splitter.js +172 -0
- package/src/halloumi/markdown-splitter.test.js +133 -0
- package/src/halloumi/middleware.js +5 -6
- package/src/halloumi/middleware.test.js +69 -0
- package/src/halloumi/postprocessing.js +0 -26
- package/src/halloumi/preprocessing.js +78 -76
- package/src/halloumi/preprocessing.test.js +87 -148
- package/src/index.js +1 -0
- package/src/middleware.js +21 -13
- 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
|
[](https://github.com/eea/volto-eea-chatbot/releases)
|
|
4
4
|
|
|
5
5
|
[](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-chatbot/job/master/display/redirect)
|
|
6
|
-
[](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot
|
|
6
|
+
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
|
|
7
|
+
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
|
|
8
|
+
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
|
|
9
|
+
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
|
|
10
10
|
|
|
11
11
|
[](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-chatbot/job/develop/display/redirect)
|
|
12
|
-
[](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot
|
|
12
|
+
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
|
|
13
|
+
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
|
|
14
|
+
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
|
|
15
|
+
[](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
|
|
package/jest-addon.config.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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(
|
|
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
|
|
149
|
-
|
|
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
|
-
{
|
|
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
|
|
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]
|
|
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' &&
|
|
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
|
|
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">{
|
|
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
|
-
|
|
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="
|
|
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
|
|
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
|
|
14
|
-
|
|
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,
|
|
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
|
-
|
|
44
|
+
const claims = markers.claims?.filter(
|
|
45
45
|
(claim) =>
|
|
46
|
-
|
|
47
|
-
(claim.startOffset
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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',
|