@eeacms/volto-eea-chatbot 3.0.1 → 4.1.0
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 +53 -4
- package/README.md +115 -12
- package/package.json +1 -1
- package/src/ChatBlock/ChatBlockView.jsx +7 -1
- package/src/ChatBlock/chat/AIMessage.tsx +79 -45
- package/src/ChatBlock/chat/ChatMessage.tsx +17 -10
- package/src/ChatBlock/chat/ChatWindow.tsx +11 -0
- package/src/ChatBlock/chat/MessageContext.ts +12 -0
- package/src/ChatBlock/chat/index.ts +1 -0
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
- package/src/ChatBlock/components/markdown/index.jsx +1 -0
- package/src/ChatBlock/hooks/useQualityMarkers.js +21 -4
- package/src/ChatBlock/index.js +9 -1
- package/src/ChatBlock/packets/RendererComponent.tsx +9 -0
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +15 -3
- package/src/ChatBlock/schema.jsx +13 -0
- package/src/ChatBlock/services/messageProcessor.ts +9 -12
- package/src/ChatBlock/types/interfaces.ts +25 -0
- package/src/ChatBlock/utils/index.tsx +10 -0
- package/src/ChatBlock/variations.ts +44 -0
- package/src/halloumi/README.md +27 -1
- package/src/halloumi/middleware.js +41 -50
- package/src/halloumi/middleware.test.js +102 -103
- package/src/halloumi/filtering.js +0 -149
- package/src/halloumi/filtering.test.js +0 -242
- package/src/halloumi/generative.js +0 -324
- package/src/halloumi/generative.test.js +0 -386
- package/src/halloumi/markdown-splitter.js +0 -172
- package/src/halloumi/markdown-splitter.test.js +0 -133
- package/src/halloumi/postprocessing.js +0 -247
- package/src/halloumi/postprocessing.test.js +0 -441
- package/src/halloumi/preprocessing.js +0 -117
- package/src/halloumi/preprocessing.test.js +0 -184
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,59 @@ 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
|
-
### [
|
|
7
|
+
### [4.1.0](https://github.com/eea/volto-eea-chatbot/compare/4.0.0...4.1.0) - 31 August 2026
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- feat: expose message context + thread extra markdown into classic message [Tiberiu Ichim - [`b1600ce`](https://github.com/eea/volto-eea-chatbot/commit/b1600cede2221b846d36be42247f9068f22f06c4)]
|
|
12
|
+
- feat: allow presentations to plug custom components into message rendering [Tiberiu Ichim - [`3f7d82b`](https://github.com/eea/volto-eea-chatbot/commit/3f7d82b9231fc2a8a92c72471068998988dadde5)]
|
|
13
|
+
- feat: block presentation variations registry [Tiberiu Ichim - [`bc92ba7`](https://github.com/eea/volto-eea-chatbot/commit/bc92ba7a98b832cf34f644b8eb7294ae4542f04d)]
|
|
14
|
+
- feat: add configurable batch size for fact-checking [Tiberiu Ichim - [`4e81cb9`](https://github.com/eea/volto-eea-chatbot/commit/4e81cb94075f8f31a70affed6f02ac8495863a57)]
|
|
15
|
+
|
|
16
|
+
#### :bug: Bug Fixes
|
|
17
|
+
|
|
18
|
+
- fix: stop hand-declaring the 'variation' schema field [Tiberiu Ichim - [`f0ada7e`](https://github.com/eea/volto-eea-chatbot/commit/f0ada7e7fb1843c562b0af44235f3a07b5253e7d)]
|
|
19
|
+
- fix: skip citations with empty document_id in CITATION_DELTA [Tiberiu Ichim - [`81c9441`](https://github.com/eea/volto-eea-chatbot/commit/81c9441fa041b966f0e05b13fe271d44d9ee1421)]
|
|
20
|
+
|
|
21
|
+
#### :house: Internal changes
|
|
22
|
+
|
|
23
|
+
- chore: bump version to 4.1.0 (block presentation variations) [Tiberiu Ichim - [`45d7d3d`](https://github.com/eea/volto-eea-chatbot/commit/45d7d3db43158664c960d47585ffe749c490782c)]
|
|
24
|
+
- chore: change default batchSize from 30 to 20 [Tiberiu Ichim - [`c5c5f4c`](https://github.com/eea/volto-eea-chatbot/commit/c5c5f4cb90fd74accd534ccd266f79592e38ec3a)]
|
|
25
|
+
|
|
26
|
+
#### :house: Documentation changes
|
|
27
|
+
|
|
28
|
+
- docs: document block presentation variations API [Tiberiu Ichim - [`8e98061`](https://github.com/eea/volto-eea-chatbot/commit/8e98061c3ce53eb701c78d3cb1eb783c8cb47234)]
|
|
29
|
+
|
|
30
|
+
#### :hammer_and_wrench: Others
|
|
31
|
+
|
|
32
|
+
- test: update citation delta test to match restored behavior [Tiberiu Ichim - [`de9f40e`](https://github.com/eea/volto-eea-chatbot/commit/de9f40e4dfd4a69f800aaf9f1b83fdf7e197b9ab)]
|
|
33
|
+
- revert: restore original CITATION_DELTA handling in messageProcessor [Tiberiu Ichim - [`1d19fd3`](https://github.com/eea/volto-eea-chatbot/commit/1d19fd382bda782d7165d34d7edba4310ea65a25)]
|
|
34
|
+
## [4.0.0](https://github.com/eea/volto-eea-chatbot/compare/3.0.1...4.0.0) - 13 August 2026
|
|
35
|
+
|
|
36
|
+
#### :bug: Bug Fixes
|
|
37
|
+
|
|
38
|
+
- fix: correct 100.00% → High in ClaimModal snapshot [Tiberiu Ichim - [`2f3e620`](https://github.com/eea/volto-eea-chatbot/commit/2f3e620627faae3b050f0cb6ba1e3db4015dc5db)]
|
|
39
|
+
- fix: avoid Betterleaks false positive on LLM_API_KEY example [Tiberiu Ichim - [`44e05bc`](https://github.com/eea/volto-eea-chatbot/commit/44e05bc74a042e7f08cc0e6878930f9acc1964b9)]
|
|
40
|
+
- fix: filter skipped claims in span processor to prevent text duplication [Tiberiu Ichim - [`9477a42`](https://github.com/eea/volto-eea-chatbot/commit/9477a42d363d6961f4def628ed567a2409b316ea)]
|
|
41
|
+
|
|
42
|
+
#### :house: Internal changes
|
|
43
|
+
|
|
44
|
+
- chore: bump version to 4.0.0 (breaking change) [Tiberiu Ichim - [`e1038b6`](https://github.com/eea/volto-eea-chatbot/commit/e1038b6db64612f7c772a898b99ff802b3c133e9)]
|
|
45
|
+
- style: Automated code fix [eea-jenkins - [`03891e2`](https://github.com/eea/volto-eea-chatbot/commit/03891e24e48ccd0eccb55d4ed42aabbcdf34251f)]
|
|
46
|
+
|
|
47
|
+
#### :house: Documentation changes
|
|
48
|
+
|
|
49
|
+
- docs: document rag-facts-check backend dependency for quality checks [Tiberiu Ichim - [`123c7ea`](https://github.com/eea/volto-eea-chatbot/commit/123c7ea5d56ee48e6d5c787049a010b6bebd5aef)]
|
|
50
|
+
- docs: update README for rag-fact-checker integration [Tiberiu Ichim - [`4ae32b4`](https://github.com/eea/volto-eea-chatbot/commit/4ae32b41e838176d4a3ba1ea55367fc21e1a8cae)]
|
|
51
|
+
|
|
52
|
+
#### :hammer_and_wrench: Others
|
|
53
|
+
|
|
54
|
+
- test: update ClaimModal snapshots for categorical score labels [Tiberiu Ichim - [`3794be1`](https://github.com/eea/volto-eea-chatbot/commit/3794be1fc112b9f209a58c6577f99649350b1d59)]
|
|
55
|
+
- Deduplicate sources by text before sending to fact-checker [Tiberiu Ichim - [`67dc793`](https://github.com/eea/volto-eea-chatbot/commit/67dc793f955058d9ba5dbc92ea23a652e4586172)]
|
|
56
|
+
- Fix: use doc.content from final_documents when tool packets lack content [Tiberiu Ichim - [`4174212`](https://github.com/eea/volto-eea-chatbot/commit/4174212a90742c139ad2399f7a07feb99d619fbc)]
|
|
57
|
+
- Replace numeric claim scores with categorical labels [Tiberiu Ichim - [`d0aaf77`](https://github.com/eea/volto-eea-chatbot/commit/d0aaf779a69340386af81e9af4aa5147d93be388)]
|
|
58
|
+
- Send structured sources with document titles to fact-checker [Tiberiu Ichim - [`190c79b`](https://github.com/eea/volto-eea-chatbot/commit/190c79b2a888a7d655157e683633fd9901b8871c)]
|
|
59
|
+
### [3.0.1](https://github.com/eea/volto-eea-chatbot/compare/3.0.0...3.0.1) - 7 August 2026
|
|
8
60
|
|
|
9
61
|
#### :bug: Bug Fixes
|
|
10
62
|
|
|
@@ -35,7 +87,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
35
87
|
#### :house: Internal changes
|
|
36
88
|
|
|
37
89
|
- chore: bump version to 3.0.0 (EEA design system dependency) [skip ci] [Tiberiu Ichim - [`bc5a8c1`](https://github.com/eea/volto-eea-chatbot/commit/bc5a8c1f72d4e706b096cb96d0d24c2c3f5ac0f8)]
|
|
38
|
-
- chore: [JENKINSFILE] sanitize sonarqube command Refs #305084 [valentinab25 - [`83bf08b`](https://github.com/eea/volto-eea-chatbot/commit/83bf08b7188548a50801b8e464a4d113f0cbbac4)]
|
|
39
90
|
|
|
40
91
|
#### :house: Documentation changes
|
|
41
92
|
|
|
@@ -113,7 +164,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
113
164
|
#### :house: Internal changes
|
|
114
165
|
|
|
115
166
|
- style: Automated code fix [eea-jenkins - [`8a8c3c4`](https://github.com/eea/volto-eea-chatbot/commit/8a8c3c4172a4f669661378cf1b5a3569d85609e6)]
|
|
116
|
-
- chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`535d986`](https://github.com/eea/volto-eea-chatbot/commit/535d986b7adc77743a668bc4ac63f835eef58df3)]
|
|
117
167
|
|
|
118
168
|
#### :hammer_and_wrench: Others
|
|
119
169
|
|
|
@@ -124,7 +174,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
124
174
|
#### :house: Internal changes
|
|
125
175
|
|
|
126
176
|
- style: Automated code fix [eea-jenkins - [`fdcd884`](https://github.com/eea/volto-eea-chatbot/commit/fdcd8848fd4c3f990ca5ea021f407487aebd6010)]
|
|
127
|
-
- chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`3d428d7`](https://github.com/eea/volto-eea-chatbot/commit/3d428d72f32c3d05452b0961c76f5db1c416e05c)]
|
|
128
177
|
|
|
129
178
|
#### :hammer_and_wrench: Others
|
|
130
179
|
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ The **Volto Chatbot** block allows the integration of an AI-powered chatbot into
|
|
|
40
40
|
| `enableShowTotalFailMessage` | Show total failure message. | Boolean | `false` |
|
|
41
41
|
| `showAssistantTitle` | Display or hide the assistant's title in the chat interface. | Boolean | `true` |
|
|
42
42
|
| `showAssistantDescription` | Display or hide the assistant's description in the chat interface. | Boolean | `true` |
|
|
43
|
-
| `qualityCheck` | Show
|
|
43
|
+
| `qualityCheck` | Show automated fact-checking of AI answers against source documents. | Dropdown | `Disabled` |
|
|
44
44
|
| `onDemandInputToggle` | Sets the default state of the fact-check AI toggle. | Boolean | `true` |
|
|
45
45
|
| `showTools` | Show or hide tools in the chat interface. | Array | `["internal_search_tool_start"]` |
|
|
46
46
|
| `scrollToInput` | Automatically scroll the page to focus on the chat input when interacting. | Boolean | `false` |
|
|
@@ -116,6 +116,116 @@ Go to http://localhost:3000
|
|
|
116
116
|
|
|
117
117
|
For a legacy Volto 17 project, install the package with `yarn` and restart the frontend as usual.
|
|
118
118
|
|
|
119
|
+
## Block presentation variations
|
|
120
|
+
|
|
121
|
+
Since 4.1.0 the chatbot block's presentation is pluggable. The block ships with the default `classic` presentation (the standard chat window), and other add-ons can register additional presentations ("variations") — e.g. to re-style the chat, replace the sources UI, or render custom elements inline in the assistant's answer. When two or more variations are registered, Volto core automatically adds a **Presentation** choice field to the block's edit sidebar, so editors can pick the variation per block. Old block content without a `variation` value keeps resolving to the default.
|
|
122
|
+
|
|
123
|
+
### Registering a variation
|
|
124
|
+
|
|
125
|
+
Push your variation onto `blocksConfig.eeaChatbot.variations` from your add-on's `applyConfig` (same cross-add-on pattern as `volto-tabs-block`):
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
export default function applyConfig(config) {
|
|
129
|
+
const block = config.blocks.blocksConfig.eeaChatbot;
|
|
130
|
+
if (block) {
|
|
131
|
+
block.variations = block.variations || [];
|
|
132
|
+
if (!block.variations.find((v) => v.id === 'catalogue')) {
|
|
133
|
+
block.variations.push({
|
|
134
|
+
id: 'catalogue',
|
|
135
|
+
title: 'Catalogue',
|
|
136
|
+
isDefault: false,
|
|
137
|
+
view: CatalogueChatView,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A variation is `{ id, title, isDefault, view }` (optionally `edit` / `schemaEnhancer`); `view` is the React component rendered for the block.
|
|
146
|
+
|
|
147
|
+
### Variation view contract
|
|
148
|
+
|
|
149
|
+
The variation `view` receives the block's fields as **top-level props** (there is no `data` prop), plus presentation props:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
<View
|
|
153
|
+
persona={assistantData} // the selected assistant
|
|
154
|
+
block_id // the block's internal id
|
|
155
|
+
isEditMode
|
|
156
|
+
isPlaywrightTest // ?playwright=yes query flag
|
|
157
|
+
initialQuery // ?query=… pre-filled question
|
|
158
|
+
initialDeepResearch // ?deepResearch=… flag
|
|
159
|
+
{...blockFields} // assistant, onyxVersion, height, …
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
If the registry is empty the block falls back to the classic `ChatWindow`, so registering a variation can never break the block.
|
|
164
|
+
|
|
165
|
+
### Reusing the classic chat window
|
|
166
|
+
|
|
167
|
+
A variation does not have to build a presentation from scratch — it can wrap the classic `ChatWindow` and only change what it needs to:
|
|
168
|
+
|
|
169
|
+
```jsx
|
|
170
|
+
import { ChatWindow } from '@eeacms/volto-eea-chatbot/ChatBlock/chat';
|
|
171
|
+
|
|
172
|
+
export default function CatalogueChatView(props) {
|
|
173
|
+
return (
|
|
174
|
+
<ChatWindow
|
|
175
|
+
{...props}
|
|
176
|
+
hideSourcesTab
|
|
177
|
+
extraRemarkPlugins={[myRemarkPlugin]}
|
|
178
|
+
extraMarkdownComponents={{ myElement: MyElementComponent }}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- `hideSourcesTab` — suppresses the classic Sources tab, sidebar and inline citation list only (the answer text and the quality-check logic are unaffected).
|
|
185
|
+
- `extraRemarkPlugins` / `extraRehypePlugins` / `extraMarkdownComponents` — additional remark/rehype plugins and react-markdown component overrides, merged with the built-ins (the same mechanism the quality markers use). This lets a variation render custom inline elements inside the streamed answer.
|
|
186
|
+
|
|
187
|
+
Custom markdown components can read the message that owns them through `ChatMessageContext` (also exported from `@eeacms/volto-eea-chatbot/ChatBlock/chat`): `ChatMessage` wraps every message in the context provider, so a component rendered from the answer text can e.g. match a marker against `message.documents`.
|
|
188
|
+
|
|
189
|
+
> **Note:** the variations registry, `hideSourcesTab`, the extra-markdown pass-through props and `ChatMessageContext` are new in 4.1.0. If your project resolves an older published version, these seams don't exist — import the module namespace (e.g. `import * as chat from '…/ChatBlock/chat'`) and guard against `undefined` instead of using named imports.
|
|
190
|
+
|
|
191
|
+
## Quality Checks (Fact-Checking)
|
|
192
|
+
|
|
193
|
+
When `qualityCheck` is enabled, the chatbot sends AI answers and their source
|
|
194
|
+
documents to a fact-checking backend that extracts claims, verifies each
|
|
195
|
+
against the sources, and returns per-claim verdicts with evidence.
|
|
196
|
+
|
|
197
|
+
### Backend dependency
|
|
198
|
+
|
|
199
|
+
The fact-checking feature requires the
|
|
200
|
+
[eea/rag-facts-check](https://github.com/eea/rag-facts-check) service running
|
|
201
|
+
and reachable at the URL configured in `RAG_FACT_CHECKER_URL`.
|
|
202
|
+
|
|
203
|
+
**Without this backend, quality checks will fail with a connection error.**
|
|
204
|
+
|
|
205
|
+
The backend exposes a halloumi-compatible endpoint (`POST /halloumi/generate`)
|
|
206
|
+
so the frontend can call it without code changes. It also provides a native
|
|
207
|
+
`POST /check` endpoint with a richer response schema.
|
|
208
|
+
|
|
209
|
+
### Deploying the backend
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Clone and build
|
|
213
|
+
git clone https://github.com/eea/rag-facts-check.git
|
|
214
|
+
cd rag-facts-check
|
|
215
|
+
docker build -t rag-fact-check .
|
|
216
|
+
|
|
217
|
+
# Run (requires an LLM endpoint)
|
|
218
|
+
# Set LLM_API_KEY to your actual API key before running
|
|
219
|
+
docker run -p 8000:8000 \
|
|
220
|
+
-e LLM_API_BASE=http://your-llm:4002/v1 \
|
|
221
|
+
-e LLM_API_KEY \
|
|
222
|
+
-e LLM_MODEL=gemma \
|
|
223
|
+
rag-fact-check
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
See the [backend README](https://github.com/eea/rag-facts-check#rag-facts-check)
|
|
227
|
+
for full configuration options.
|
|
228
|
+
|
|
119
229
|
## Environment Configuration
|
|
120
230
|
|
|
121
231
|
To properly configure the middleware and authenticate with the Onyx service, ensure that the following environment variables are set:
|
|
@@ -131,20 +241,13 @@ This document lists the environment variables used in the Volto Chatbot project.
|
|
|
131
241
|
- `JEST_USE_SETUP`
|
|
132
242
|
Used in Jest configuration. When set to 'ON', it enables a specific Jest setup.
|
|
133
243
|
|
|
134
|
-
- `
|
|
135
|
-
The URL for the
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
The token for authenticating with the LLM Gateway service.
|
|
244
|
+
- `RAG_FACT_CHECKER_URL`
|
|
245
|
+
The base URL for the [rag-facts-check](https://github.com/eea/rag-facts-check)
|
|
246
|
+
fact-checking backend. Required when `qualityCheck` is enabled.
|
|
247
|
+
Default: `http://localhost:8000`.
|
|
139
248
|
|
|
140
249
|
### Development-specific environment variables
|
|
141
250
|
|
|
142
|
-
- `MOCK_HALLOUMI_FILE_PATH`
|
|
143
|
-
When set, this specifies the absolute path to the JSON file containing the mocked Halloumi response. Setting this variable enables mocking of Halloumi API calls.
|
|
144
|
-
|
|
145
|
-
- `DUMP_HALLOUMI_FILE_PATH`
|
|
146
|
-
When set, the Halloumi response will be dumped to the specified absolute file path for debugging or to create new mock files.
|
|
147
|
-
|
|
148
251
|
- `MOCK_LLM_FILE_PATH`
|
|
149
252
|
When set, this specifies the absolute path to the JSONL file containing the mocked Onyx stream response. Setting this variable enables mocking of Onyx LLM calls.
|
|
150
253
|
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import superagent from 'superagent';
|
|
|
3
3
|
import { parse } from 'qs';
|
|
4
4
|
import withOnyxData from './hocs/withOnyxData';
|
|
5
5
|
import { ChatWindow } from './chat';
|
|
6
|
+
import { getChatVariation } from './variations';
|
|
6
7
|
|
|
7
8
|
function ChatBlockView(props) {
|
|
8
9
|
const { id, assistantData, data, isEditMode, location } = props;
|
|
@@ -25,8 +26,13 @@ function ChatBlockView(props) {
|
|
|
25
26
|
}
|
|
26
27
|
}, [id, isPlaywrightTest, data]);
|
|
27
28
|
|
|
29
|
+
// Resolve the presentation variation for this block. Falls back to the
|
|
30
|
+
// classic ChatWindow so the block keeps working even if the registry is
|
|
31
|
+
// somehow empty.
|
|
32
|
+
const Presentation = getChatVariation(data)?.view || ChatWindow;
|
|
33
|
+
|
|
28
34
|
return assistantData ? (
|
|
29
|
-
<
|
|
35
|
+
<Presentation
|
|
30
36
|
persona={assistantData}
|
|
31
37
|
isEditMode={isEditMode}
|
|
32
38
|
isPlaywrightTest={isPlaywrightTest}
|
|
@@ -81,31 +81,21 @@ function addQualityMarkersPlugin() {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// const sourceType = doc.source_type
|
|
100
|
-
// ? sources[doc.source_type] || capitalize(doc.source_type)
|
|
101
|
-
// : '';
|
|
102
|
-
|
|
103
|
-
// const header = `${docIndex}${doc.semantic_identifier}${
|
|
104
|
-
// sourceType ? `\nSource: ${sourceType}` : ''
|
|
105
|
-
// }${updatedDate ? `\nUpdated: ${updatedDate}` : ''}`;
|
|
106
|
-
|
|
107
|
-
// return `${header}\n${text}`;
|
|
108
|
-
return text.replace(/\u00A0/g, ' ');
|
|
84
|
+
/**
|
|
85
|
+
* Build a structured source object for the fact-checker backend.
|
|
86
|
+
*
|
|
87
|
+
* The backend accepts {text, title, source_type} dicts so it can
|
|
88
|
+
* include document titles in LLM prompts without polluting the raw
|
|
89
|
+
* text (which would break span-offset matching).
|
|
90
|
+
*/
|
|
91
|
+
export function buildHalloumiSource(doc: any, text: string) {
|
|
92
|
+
const cleanedText = text.replace(/\u00A0/g, ' ');
|
|
93
|
+
return {
|
|
94
|
+
text: cleanedText,
|
|
95
|
+
title: doc.semantic_identifier || null,
|
|
96
|
+
source_type: doc.source_type || null,
|
|
97
|
+
link: doc.link || null,
|
|
98
|
+
};
|
|
109
99
|
}
|
|
110
100
|
|
|
111
101
|
function mapToolDocumentsToText(message: any) {
|
|
@@ -135,24 +125,36 @@ function getContextSources(
|
|
|
135
125
|
const documentIdToText = mapToolDocumentsToText(message);
|
|
136
126
|
|
|
137
127
|
return qualityCheckContext === 'citations'
|
|
138
|
-
? sources.map((doc: any) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
? sources.map((doc: any) => {
|
|
129
|
+
// Prefer content from tool packets (may have enriched text),
|
|
130
|
+
// but fall back to doc.content from final_documents.
|
|
131
|
+
// Without this fallback, sources sent to the fact-checker are
|
|
132
|
+
// empty strings when tool packets don't carry full content.
|
|
133
|
+
const text = documentIdToText[doc.document_id] || doc.content || '';
|
|
134
|
+
const cleanedText = text.replace(/\u00A0/g, ' ');
|
|
135
|
+
return {
|
|
136
|
+
...doc,
|
|
137
|
+
id: doc.document_id,
|
|
138
|
+
text,
|
|
139
|
+
// Keep for ClaimSegments span highlighting (plain text)
|
|
140
|
+
halloumiContext: cleanedText,
|
|
141
|
+
// Structured source for the fact-checker backend
|
|
142
|
+
halloumiSource: buildHalloumiSource(doc, text),
|
|
143
|
+
};
|
|
144
|
+
})
|
|
147
145
|
: (message.toolCalls || []).reduce(
|
|
148
146
|
(acc: any, cur: any) => [
|
|
149
147
|
...acc,
|
|
150
|
-
...(cur.tool_result || []).map((doc: any) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
...(cur.tool_result || []).map((doc: any) => {
|
|
149
|
+
const cleanedText = (doc.content || '').replace(/\u00A0/g, ' ');
|
|
150
|
+
return {
|
|
151
|
+
...doc,
|
|
152
|
+
id: doc.document_id,
|
|
153
|
+
text: doc.content,
|
|
154
|
+
halloumiContext: cleanedText,
|
|
155
|
+
halloumiSource: buildHalloumiSource(doc, doc.content || ''),
|
|
156
|
+
};
|
|
157
|
+
}),
|
|
156
158
|
], // TODO: make sure we don't add multiple times the same doc
|
|
157
159
|
// TODO: this doesn't have the index for source
|
|
158
160
|
[],
|
|
@@ -203,10 +205,15 @@ export function AIMessage({
|
|
|
203
205
|
enableMatomoTracking,
|
|
204
206
|
persona,
|
|
205
207
|
maxContextSegments,
|
|
208
|
+
batchSize,
|
|
206
209
|
isLastMessage,
|
|
207
210
|
className = '',
|
|
208
211
|
chatWindowEndRef,
|
|
209
212
|
showTools,
|
|
213
|
+
hideSourcesTab,
|
|
214
|
+
extraRemarkPlugins,
|
|
215
|
+
extraMarkdownComponents,
|
|
216
|
+
extraRehypePlugins,
|
|
210
217
|
}: ChatMessageProps) {
|
|
211
218
|
const [allToolsDisplayed, setAllToolsDisplayed] = useState(false);
|
|
212
219
|
const [messageDisplayed, setMessageDisplayed] = useState(false);
|
|
@@ -275,13 +282,36 @@ export function AIMessage({
|
|
|
275
282
|
|
|
276
283
|
const showSources = messageDisplayed && sources.length > 0;
|
|
277
284
|
|
|
285
|
+
// Whether to actually render the classic Sources UI (tab, sidebar, inline
|
|
286
|
+
// list). `hideSourcesTab` lets a variation present the cited documents a
|
|
287
|
+
// different way (e.g. inline catalogue cards) while keeping quality-check
|
|
288
|
+
// logic intact.
|
|
289
|
+
const showSourcesTab = showSources && !hideSourcesTab;
|
|
290
|
+
|
|
278
291
|
const contextSources = getContextSources(
|
|
279
292
|
message,
|
|
280
293
|
sources,
|
|
281
294
|
qualityCheckContext,
|
|
282
295
|
);
|
|
283
296
|
|
|
284
|
-
|
|
297
|
+
// Deduplicate sources by text content. The streaming backend often sends
|
|
298
|
+
// the same document in multiple packets (MESSAGE_START, SEARCH_TOOL_DELTA,
|
|
299
|
+
// etc.), resulting in 40+ copies of the same text. Dedup keeps the first
|
|
300
|
+
// occurrence, preserving order. Critical: texts must remain identical
|
|
301
|
+
// between frontend (span highlighting) and backend (evidence spans).
|
|
302
|
+
const dedupedSources = useMemo(() => {
|
|
303
|
+
const seen = new Set<string>();
|
|
304
|
+
return contextSources.filter((src: any) => {
|
|
305
|
+
const key = src.halloumiContext || src.halloumiSource?.text || '';
|
|
306
|
+
if (key && !seen.has(key)) {
|
|
307
|
+
seen.add(key);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
});
|
|
312
|
+
}, [contextSources]);
|
|
313
|
+
|
|
314
|
+
const stableContextSources = useDeepCompareMemoize(dedupedSources);
|
|
285
315
|
|
|
286
316
|
const doQualityControl =
|
|
287
317
|
messageDisplayed &&
|
|
@@ -300,6 +330,7 @@ export function AIMessage({
|
|
|
300
330
|
addCitations(message.message, message),
|
|
301
331
|
stableContextSources,
|
|
302
332
|
maxContextSegments,
|
|
333
|
+
batchSize,
|
|
303
334
|
);
|
|
304
335
|
|
|
305
336
|
const claims = markers?.claims || [];
|
|
@@ -365,7 +396,7 @@ export function AIMessage({
|
|
|
365
396
|
const answerTab = (
|
|
366
397
|
<div className="answer-tab">
|
|
367
398
|
{/* Show first 3 sources inline */}
|
|
368
|
-
{
|
|
399
|
+
{showSourcesTab && (
|
|
369
400
|
<div className="sources">
|
|
370
401
|
{sources.slice(0, 3).map((source: any, i: number) => (
|
|
371
402
|
<SourceDetails source={source} key={i} index={source.index} />
|
|
@@ -429,6 +460,9 @@ export function AIMessage({
|
|
|
429
460
|
markers={markers}
|
|
430
461
|
stableContextSources={stableContextSources}
|
|
431
462
|
addQualityMarkersPlugin={addQualityMarkersPlugin}
|
|
463
|
+
extraRemarkPlugins={extraRemarkPlugins}
|
|
464
|
+
extraMarkdownComponents={extraMarkdownComponents}
|
|
465
|
+
extraRehypePlugins={extraRehypePlugins}
|
|
432
466
|
>
|
|
433
467
|
{({ content }) => (
|
|
434
468
|
<div className="message-text-wrapper">{content}</div>
|
|
@@ -505,7 +539,7 @@ export function AIMessage({
|
|
|
505
539
|
menuItem: { key: 'answer', content: 'Answer', className: 'answer-tab' },
|
|
506
540
|
pane: <Tab.Pane key="answer">{answerTab}</Tab.Pane>,
|
|
507
541
|
},
|
|
508
|
-
...(
|
|
542
|
+
...(showSourcesTab && !error
|
|
509
543
|
? [
|
|
510
544
|
{
|
|
511
545
|
menuItem: {
|
|
@@ -554,14 +588,14 @@ export function AIMessage({
|
|
|
554
588
|
secondary: true,
|
|
555
589
|
pointing: true,
|
|
556
590
|
fluid: true,
|
|
557
|
-
className: cx({ 'without-sources': !
|
|
591
|
+
className: cx({ 'without-sources': !showSourcesTab }),
|
|
558
592
|
}}
|
|
559
593
|
panes={panes}
|
|
560
594
|
renderActiveOnly={false}
|
|
561
595
|
/>
|
|
562
596
|
|
|
563
597
|
{/* Sources sidebar */}
|
|
564
|
-
{
|
|
598
|
+
{showSourcesTab && !error && (
|
|
565
599
|
<Sidebar
|
|
566
600
|
visible={showSourcesSidebar}
|
|
567
601
|
animation="overlay"
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Message as SemanticMessage } from 'semantic-ui-react';
|
|
2
2
|
import type { ChatMessageProps } from '@eeacms/volto-eea-chatbot/ChatBlock/types/interfaces';
|
|
3
3
|
import { UserMessage, AIMessage } from '.';
|
|
4
|
+
import { ChatMessageContext } from './MessageContext';
|
|
4
5
|
|
|
5
6
|
export function ChatMessage(props: ChatMessageProps) {
|
|
6
7
|
const { message, libs, className = '' } = props;
|
|
7
8
|
|
|
9
|
+
let content: React.ReactNode = null;
|
|
10
|
+
|
|
8
11
|
if (message.type === 'user') {
|
|
9
|
-
|
|
12
|
+
content = (
|
|
10
13
|
<UserMessage
|
|
11
14
|
message={message}
|
|
12
15
|
libs={libs}
|
|
@@ -14,14 +17,10 @@ export function ChatMessage(props: ChatMessageProps) {
|
|
|
14
17
|
isLoading={props.isLoading}
|
|
15
18
|
/>
|
|
16
19
|
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (message.type === '
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (message.type === 'error') {
|
|
24
|
-
return (
|
|
20
|
+
} else if (message.type === 'assistant') {
|
|
21
|
+
content = <AIMessage {...props} />;
|
|
22
|
+
} else if (message.type === 'error') {
|
|
23
|
+
content = (
|
|
25
24
|
<div className="message-error">
|
|
26
25
|
<SemanticMessage color="red" className="error-message">
|
|
27
26
|
<div className="error-title">Error</div>
|
|
@@ -31,5 +30,13 @@ export function ChatMessage(props: ChatMessageProps) {
|
|
|
31
30
|
);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
return null;
|
|
33
|
+
if (content === null) return null;
|
|
34
|
+
|
|
35
|
+
// Expose the owning message to deeply-nested descendants (e.g. custom
|
|
36
|
+
// markdown components injected by addons via `extraMarkdownComponents`).
|
|
37
|
+
return (
|
|
38
|
+
<ChatMessageContext.Provider value={message}>
|
|
39
|
+
{content}
|
|
40
|
+
</ChatMessageContext.Provider>
|
|
41
|
+
);
|
|
35
42
|
}
|
|
@@ -49,6 +49,7 @@ interface ChatWindowProps {
|
|
|
49
49
|
enableMatomoTracking?: boolean;
|
|
50
50
|
onDemandInputToggle?: boolean;
|
|
51
51
|
maxContextSegments?: number;
|
|
52
|
+
batchSize?: number;
|
|
52
53
|
onyxVersion?: '2' | '3';
|
|
53
54
|
isPlaywrightTest?: boolean;
|
|
54
55
|
initialQuery?: string | null;
|
|
@@ -89,7 +90,12 @@ function ChatWindow({
|
|
|
89
90
|
enableMatomoTracking = true,
|
|
90
91
|
onDemandInputToggle = true,
|
|
91
92
|
maxContextSegments = 0,
|
|
93
|
+
batchSize,
|
|
92
94
|
onyxVersion = '2',
|
|
95
|
+
hideSourcesTab,
|
|
96
|
+
extraRemarkPlugins,
|
|
97
|
+
extraMarkdownComponents,
|
|
98
|
+
extraRehypePlugins,
|
|
93
99
|
} = data;
|
|
94
100
|
const [qualityCheckEnabled, setQualityCheckEnabled] = useState(
|
|
95
101
|
onDemandInputToggle ?? true,
|
|
@@ -231,6 +237,7 @@ function ChatWindow({
|
|
|
231
237
|
enableMatomoTracking={enableMatomoTracking}
|
|
232
238
|
persona={persona.id}
|
|
233
239
|
maxContextSegments={maxContextSegments}
|
|
240
|
+
batchSize={batchSize}
|
|
234
241
|
isLastMessage={index === messages.length - 1}
|
|
235
242
|
className={
|
|
236
243
|
index === messages.length - 1 ? 'most-recent' : ''
|
|
@@ -238,6 +245,10 @@ function ChatWindow({
|
|
|
238
245
|
chatWindowRef={chatWindowRef}
|
|
239
246
|
chatWindowEndRef={chatWindowEndRef}
|
|
240
247
|
showTools={showTools}
|
|
248
|
+
hideSourcesTab={hideSourcesTab}
|
|
249
|
+
extraRemarkPlugins={extraRemarkPlugins}
|
|
250
|
+
extraMarkdownComponents={extraMarkdownComponents}
|
|
251
|
+
extraRehypePlugins={extraRehypePlugins}
|
|
241
252
|
/>
|
|
242
253
|
</React.Fragment>
|
|
243
254
|
))}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type { Message } from '@eeacms/volto-eea-chatbot/ChatBlock/types/interfaces';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Provides the currently-rendered chat message to deeply-nested descendants
|
|
6
|
+
* (e.g. custom react-markdown components injected by an addon).
|
|
7
|
+
*
|
|
8
|
+
* Set by `ChatMessage`, so any component rendered inside a given message
|
|
9
|
+
* (including custom markdown elements) can read the message that owns it —
|
|
10
|
+
* e.g. to match a `![[doc: Title]]` marker against `message.documents`.
|
|
11
|
+
*/
|
|
12
|
+
export const ChatMessageContext = createContext<Message | null>(null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Modal, ModalContent, ModalHeader } from 'semantic-ui-react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import {
|
|
3
|
+
import { scoreToLabel } from '@eeacms/volto-eea-chatbot/ChatBlock/utils';
|
|
4
4
|
import SVGIcon from '@eeacms/volto-eea-chatbot/ChatBlock/components/Icon';
|
|
5
5
|
import { getSupportedBgColor } from './colors';
|
|
6
6
|
import { ClaimSegments } from './ClaimSegments';
|
|
@@ -70,9 +70,9 @@ export function ClaimModal({ claim, markers, text, citedSources }) {
|
|
|
70
70
|
<div className="score-badge-section">
|
|
71
71
|
<div className="score-badge">
|
|
72
72
|
<span className="score-percentage">
|
|
73
|
-
{
|
|
73
|
+
{scoreToLabel(claim.score)}
|
|
74
74
|
</span>
|
|
75
|
-
<span className="score-label">
|
|
75
|
+
<span className="score-label">Verification</span>
|
|
76
76
|
</div>
|
|
77
77
|
<div className="score-progress-bar">
|
|
78
78
|
<div
|
|
@@ -43,6 +43,7 @@ export function components(message, markers, citedSources) {
|
|
|
43
43
|
const end = child.position.end.offset;
|
|
44
44
|
const claims = markers.claims?.filter(
|
|
45
45
|
(claim) =>
|
|
46
|
+
!claim.skipped &&
|
|
46
47
|
claim.score !== null &&
|
|
47
48
|
((start >= claim.startOffset && end <= claim.endOffset) ||
|
|
48
49
|
(start <= claim.endOffset && end >= claim.endOffset) ||
|