@eeacms/volto-eea-chatbot 4.1.3 → 4.1.4
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/AGENTS.md +4 -0
- package/CHANGELOG.md +23 -3
- package/README.md +1 -14
- package/artifacts/I18N-SPEC.md +257 -0
- package/package.json +1 -1
- package/src/AISearchInput/index.js +20 -6
- package/src/ChatBlock/chat/AIMessage.tsx +71 -21
- package/src/ChatBlock/chat/ChatWindow.tsx +3 -0
- package/src/ChatBlock/components/HalloumiFeedback.jsx +19 -0
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +28 -14
- package/src/ChatBlock/components/markdown/ClaimSegments.jsx +40 -8
- package/src/ChatBlock/components/markdown/RenderClaimView.jsx +56 -13
- package/src/ChatBlock/components/markdown/colors.js +22 -0
- package/src/ChatBlock/components/markdown/colors.less +10 -1
- package/src/ChatBlock/components/markdown/colors.test.js +46 -1
- package/src/ChatBlock/hooks/index.ts +1 -0
- package/src/ChatBlock/hooks/useChunkEvidence.ts +80 -0
- package/src/ChatBlock/schema.jsx +12 -0
- package/src/ChatBlock/services/chunkEvidence.ts +251 -0
- package/src/ChatBlock/style.less +35 -7
- package/src/ChatBlock/types/interfaces.ts +6 -0
- package/src/ChatBlock/utils/index.tsx +56 -6
- package/src/halloumi/README.md +91 -0
- package/src/index.js +1 -0
- package/src/middleware.js +9 -0
- package/src/middleware.test.js +56 -0
package/AGENTS.md
CHANGED
|
@@ -84,6 +84,10 @@ and consistency.
|
|
|
84
84
|
- **Git Workflow:** When committing changes, always use `git status` to review
|
|
85
85
|
changes and then `git add <relevant files changed>` to stage specific files,
|
|
86
86
|
instead of `git add .`.
|
|
87
|
+
- **Documentation & Badges:** Keep a single set of develop badges in `README.md`
|
|
88
|
+
to avoid badge duplication. Store demo animations in `docs/volto-eea-chatbot.gif`
|
|
89
|
+
and reference via `https://raw.githubusercontent.com/eea/volto-eea-chatbot/master/docs/volto-eea-chatbot.gif`.
|
|
90
|
+
|
|
87
91
|
|
|
88
92
|
For more detailed contribution guidelines, refer to `DEVELOP.md`.
|
|
89
93
|
For release procedures, refer to `RELEASE.md`.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
### [4.1.4](https://github.com/eea/volto-eea-chatbot/compare/4.1.3...4.1.4) - 12 October 2026
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- feat(chat): make the evidence window a block setting [Tiberiu Ichim - [`d7bf4aa`](https://github.com/eea/volto-eea-chatbot/commit/d7bf4aa624df6f7dfb933bb7dc5793061a5771b7)]
|
|
12
|
+
- feat(chat): show when a fact-check ran on partial context [Tiberiu Ichim - [`23dddfa`](https://github.com/eea/volto-eea-chatbot/commit/23dddfab0a0e682fff48ba6d84e3df5fd63800fe)]
|
|
13
|
+
- feat(chat): fact-check against real Onyx chunk text, not search blurbs [Tiberiu Ichim - [`e47a593`](https://github.com/eea/volto-eea-chatbot/commit/e47a593b14ba0bac81c28f9a6a9b7d6d77eb8a82)]
|
|
14
|
+
|
|
15
|
+
#### :bug: Bug Fixes
|
|
16
|
+
|
|
17
|
+
- fix(claims): put not_enough_info on a lighter green instead of yellow [Tiberiu Ichim - [`52db6bb`](https://github.com/eea/volto-eea-chatbot/commit/52db6bbb45a2509ce57c5a5ad3dca2fa7f135d46)]
|
|
18
|
+
- fix(claims): yellow for not_enough_info, guard for ungenerated colour classes [Tiberiu Ichim - [`f34f613`](https://github.com/eea/volto-eea-chatbot/commit/f34f613b29d41d3769d26d971b33402d82e8c865)]
|
|
19
|
+
- fix(claims): one explanation per answer, verdict colours that match verdicts [Tiberiu Ichim - [`3a017b9`](https://github.com/eea/volto-eea-chatbot/commit/3a017b963c908f1230b23051b096fb1e30c232e2)]
|
|
20
|
+
- fix(claim-modal): replace the score bar with a three-position verdict scale [Tiberiu Ichim - [`6cd595f`](https://github.com/eea/volto-eea-chatbot/commit/6cd595f623cd79003f0291b1f5264640febc0432)]
|
|
21
|
+
- fix(claim-modal): title the modal by the verdict, not the process [Tiberiu Ichim - [`ac9f982`](https://github.com/eea/volto-eea-chatbot/commit/ac9f982e27132c44ce9140e415a2fc06e9e711a2)]
|
|
22
|
+
- fix(proxy): propagate upstream status; treat empty chunk bodies as missing [Tiberiu Ichim - [`e3ccf47`](https://github.com/eea/volto-eea-chatbot/commit/e3ccf47e46805c40b2685dada3f9fe3426bcde24)]
|
|
23
|
+
- fix(chat): render several evidence segments per claim [Tiberiu Ichim - [`ac3d7e1`](https://github.com/eea/volto-eea-chatbot/commit/ac3d7e1d9bde2ab5441731a04aee40ffa9a0e760)]
|
|
24
|
+
|
|
25
|
+
#### :house: Documentation changes
|
|
26
|
+
|
|
27
|
+
- docs: specify the i18n pass for the whole add-on [Tiberiu Ichim - [`b37291a`](https://github.com/eea/volto-eea-chatbot/commit/b37291aac16313eb05c092b2cf3bc13998f5a1e2)]
|
|
28
|
+
- docs: remove duplicate badges and fix demo video [Tiberiu Ichim - [`5e29d36`](https://github.com/eea/volto-eea-chatbot/commit/5e29d368bfe17f6da09f0419542e44a8c499aa78)]
|
|
29
|
+
|
|
7
30
|
### [4.1.3](https://github.com/eea/volto-eea-chatbot/compare/4.1.2...4.1.3) - 21 September 2026
|
|
8
31
|
|
|
9
32
|
#### :bug: Bug Fixes
|
|
@@ -106,7 +129,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
106
129
|
#### :house: Internal changes
|
|
107
130
|
|
|
108
131
|
- 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)]
|
|
109
|
-
- chore: [JENKINSFILE] sanitize sonarqube command Refs #305084 [valentinab25 - [`83bf08b`](https://github.com/eea/volto-eea-chatbot/commit/83bf08b7188548a50801b8e464a4d113f0cbbac4)]
|
|
110
132
|
|
|
111
133
|
#### :house: Documentation changes
|
|
112
134
|
|
|
@@ -184,7 +206,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
184
206
|
#### :house: Internal changes
|
|
185
207
|
|
|
186
208
|
- style: Automated code fix [eea-jenkins - [`8a8c3c4`](https://github.com/eea/volto-eea-chatbot/commit/8a8c3c4172a4f669661378cf1b5a3569d85609e6)]
|
|
187
|
-
- chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`535d986`](https://github.com/eea/volto-eea-chatbot/commit/535d986b7adc77743a668bc4ac63f835eef58df3)]
|
|
188
209
|
|
|
189
210
|
#### :hammer_and_wrench: Others
|
|
190
211
|
|
|
@@ -195,7 +216,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
195
216
|
#### :house: Internal changes
|
|
196
217
|
|
|
197
218
|
- style: Automated code fix [eea-jenkins - [`fdcd884`](https://github.com/eea/volto-eea-chatbot/commit/fdcd8848fd4c3f990ca5ea021f407487aebd6010)]
|
|
198
|
-
- chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`3d428d7`](https://github.com/eea/volto-eea-chatbot/commit/3d428d72f32c3d05452b0961c76f5db1c416e05c)]
|
|
199
219
|
|
|
200
220
|
#### :hammer_and_wrench: Others
|
|
201
221
|
|
package/README.md
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/eea/volto-eea-chatbot/releases)
|
|
4
4
|
|
|
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/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
|
-
|
|
11
5
|
[](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-chatbot/job/develop/display/redirect)
|
|
12
6
|
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
|
|
13
7
|
[](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
|
|
@@ -22,7 +16,7 @@
|
|
|
22
16
|
|
|
23
17
|
## Features
|
|
24
18
|
|
|
25
|
-
https://
|
|
19
|
+

|
|
26
20
|
|
|
27
21
|
The **Volto Chatbot** block allows the integration of an AI-powered chatbot into your Volto project. It offers a customizable interface and advanced settings to tailor the chatbot's behavior to your needs. Below is an overview of its features and configuration options.
|
|
28
22
|
|
|
@@ -334,11 +328,4 @@ See [LICENSE.md](https://github.com/eea/volto-eea-chatbot/blob/master/LICENSE.md
|
|
|
334
328
|
## Funding
|
|
335
329
|
|
|
336
330
|
[European Environment Agency (EU)](http://eea.europa.eu)
|
|
337
|
-
de is European Environment Agency (EEA).
|
|
338
|
-
All Rights Reserved.
|
|
339
|
-
|
|
340
|
-
See [LICENSE.md](https://github.com/eea/volto-addon-template/blob/master/LICENSE.md) for details.
|
|
341
331
|
|
|
342
|
-
## Funding
|
|
343
|
-
|
|
344
|
-
[European Environment Agency (EU)](http://eea.europa.eu)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# i18n specification — volto-eea-chatbot
|
|
2
|
+
|
|
3
|
+
**Status: not implemented.** Written 2026-10-08 for a dedicated branch that handles
|
|
4
|
+
internationalisation across the whole add-on. Nothing here is urgent: no locale is
|
|
5
|
+
filled today, so every string already falls back to English.
|
|
6
|
+
|
|
7
|
+
Line numbers are from `develop` at the time of writing. Re-run §4's command to
|
|
8
|
+
regenerate the inventory before starting.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Current state (measured)
|
|
13
|
+
|
|
14
|
+
The Volto add-on i18n stack is present and wired:
|
|
15
|
+
|
|
16
|
+
| piece | location | state |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| locale catalogues | `locales/{de,en,it,ro}/LC_MESSAGES/volto.po` | present, **empty** |
|
|
19
|
+
| template | `locales/volto.pot` | present, dated **2026-09-17** (stale) |
|
|
20
|
+
| extractor | `make i18n` → `node_modules/.bin/i18n --addon` | works, **manual** — no CI step |
|
|
21
|
+
| message definitions | `src/AISearchInput/messages.js` (`defineMessages`) | pattern to copy |
|
|
22
|
+
| consumption | `<FormattedMessage {...messages.x} />` | 48 call sites |
|
|
23
|
+
|
|
24
|
+
Coverage is the problem:
|
|
25
|
+
|
|
26
|
+
| measure | value |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| msgids in `volto.pot` | **11** — all from `AISearchInput` |
|
|
29
|
+
| msgstr filled, any locale | **0 of 10** in de, it, ro and en |
|
|
30
|
+
| intl call sites in `ChatBlock/` | **8** (ChatWindow 3, AIMessage 2, AutoResizeTextarea 2) |
|
|
31
|
+
| hardcoded English literals in `ChatBlock/` | **~40** (28 JSX/prop literals + ~12 in JS constants and utils) |
|
|
32
|
+
| fact-check UI strings extracted | **0** |
|
|
33
|
+
|
|
34
|
+
So the add-on is translatable in principle and English-only in practice. The
|
|
35
|
+
`en/volto.po` being empty is normal and should stay empty — `defaultMessage` is the
|
|
36
|
+
source of truth.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Target design
|
|
41
|
+
|
|
42
|
+
**One messages file per feature**, matching the existing pattern:
|
|
43
|
+
|
|
44
|
+
- `src/ChatBlock/messages.js` (new)
|
|
45
|
+
- `src/AISearchInput/messages.js` (exists, keep)
|
|
46
|
+
|
|
47
|
+
The extractor scans `src/**/messages.js`; keep that exact filename.
|
|
48
|
+
|
|
49
|
+
**Message ids are the English text** (Volto convention, already used in the `.pot`).
|
|
50
|
+
Do not invent key names for ids — `defaultMessage` carries the copy, `id` is the key.
|
|
51
|
+
|
|
52
|
+
**Components** use `<FormattedMessage {...messages.x} />`, or `useIntl().formatMessage(...)`
|
|
53
|
+
when the string must be a plain value (e.g. an `aria-label` prop, a `title` attribute).
|
|
54
|
+
|
|
55
|
+
**Pure helpers return message descriptors, not strings.** This is the decision that
|
|
56
|
+
matters for the fact-check UI: `ChatBlock/utils/index.tsx` maps a verdict to copy and
|
|
57
|
+
is unit-tested without any React or intl context. Returning descriptors keeps it that way.
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// src/ChatBlock/messages.js
|
|
61
|
+
import { defineMessages } from 'react-intl';
|
|
62
|
+
|
|
63
|
+
export default defineMessages({
|
|
64
|
+
verdictSupportedHeader: {
|
|
65
|
+
id: 'Supported by the sources',
|
|
66
|
+
defaultMessage: 'Supported by the sources',
|
|
67
|
+
},
|
|
68
|
+
// …one entry per verdict
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// ChatBlock/utils/index.tsx — descriptor maps instead of string maps
|
|
74
|
+
import messages from '@eeacms/volto-eea-chatbot/ChatBlock/messages';
|
|
75
|
+
|
|
76
|
+
const VERDICT_HEADER = {
|
|
77
|
+
supported: messages.verdictSupportedHeader,
|
|
78
|
+
not_enough_info: messages.verdictNotConfirmedHeader,
|
|
79
|
+
contradicted: messages.verdictContradictedHeader,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function claimHeaderLabel(score, contextLimited = false) {
|
|
83
|
+
if (contextLimited) return messages.verdictUnverifiedHeader;
|
|
84
|
+
return VERDICT_HEADER[scoreToVerdict(score)];
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
// ChatBlock/components/markdown/ClaimModal.jsx
|
|
90
|
+
<span className="claim-label">
|
|
91
|
+
<FormattedMessage {...claimHeaderLabel(claim.score, claim.context_limited)} />
|
|
92
|
+
</span>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Tests then assert on the descriptor (`id` / `defaultMessage`) rather than on English
|
|
96
|
+
prose, so they stay intl-free.
|
|
97
|
+
|
|
98
|
+
**Plurals and counts move into ICU**, replacing JS string building. Today
|
|
99
|
+
`HalloumiFeedback` composes the banner sentence from a ternary and a template literal;
|
|
100
|
+
that cannot be translated into languages with more than two plural forms.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// messages.js
|
|
104
|
+
partialContext: {
|
|
105
|
+
id: '{count, plural, one {# claim was} other {# claims were}} checked against short excerpts, so “not confirmed” may mean missing text rather than a mistake.',
|
|
106
|
+
defaultMessage:
|
|
107
|
+
'{count, plural, one {# claim was} other {# claims were}} checked against short excerpts, so “not confirmed” may mean missing text rather than a mistake.',
|
|
108
|
+
},
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
<MessageContent>
|
|
113
|
+
<strong><FormattedMessage {...messages.partialContextLead} /></strong>{' '}
|
|
114
|
+
<FormattedMessage {...messages.partialContext} values={{ count: limitedClaims.length }} />
|
|
115
|
+
</MessageContent>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 3. What must not be extracted
|
|
121
|
+
|
|
122
|
+
- Answer content, source titles, chunk text — data, not UI copy.
|
|
123
|
+
- `${score}%`, `num_tokens`, and similar interpolations — data.
|
|
124
|
+
- The `claim-quote` block in `ClaimModal`, which is rendered through
|
|
125
|
+
`dangerouslySetInnerHTML` from the answer itself.
|
|
126
|
+
- Log messages and `debug()` labels.
|
|
127
|
+
- Anything in `services/` or `hooks/` that is only compared or thrown internally —
|
|
128
|
+
but see `VERIFY_CLAIM_MESSAGES` in `HalloumiFeedback.jsx:12-15`, which **is** shown
|
|
129
|
+
to the user and must be extracted.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 4. Inventory
|
|
134
|
+
|
|
135
|
+
Regenerate the candidate list with:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
cd src && rg -n ">[A-Z“][^<>{}]{3,80}<|(placeholder|title|aria-label|alt|label)=\"?" \
|
|
139
|
+
ChatBlock AISearchInput -g '!tests' | less
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### A. Fact-check UI — 14 strings, added or touched 2026-10-08, zero extracted
|
|
143
|
+
|
|
144
|
+
| file | line | string |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `ChatBlock/utils/index.tsx` | 82–84 | `Supported by the sources` / `Not confirmed by the sources` / `Contradicted by the sources` |
|
|
147
|
+
| `ChatBlock/utils/index.tsx` | 88–90 | `Supported` / `Not confirmed` / `Contradicted` |
|
|
148
|
+
| `ChatBlock/utils/index.tsx` | 99 | `Couldn't be checked` |
|
|
149
|
+
| `ChatBlock/utils/index.tsx` | 104 | `Unverified` |
|
|
150
|
+
| `ChatBlock/components/markdown/ClaimModal.jsx` | 79 | `Fact check` |
|
|
151
|
+
| `ChatBlock/components/markdown/ClaimModal.jsx` | 96 | `Rationale` |
|
|
152
|
+
| `ChatBlock/components/HalloumiFeedback.jsx` | 136 | `Some sources could not be loaded in full.` |
|
|
153
|
+
| `ChatBlock/components/HalloumiFeedback.jsx` | 137–140 | banner sentence with claim count (→ ICU plural) |
|
|
154
|
+
| `ChatBlock/components/HalloumiFeedback.jsx` | 141 | `Every claim was still matched against the text that was available, so this did not change the result.` |
|
|
155
|
+
| `ChatBlock/components/markdown/ClaimSegments.jsx` | 139 | `Jump to Citation` |
|
|
156
|
+
|
|
157
|
+
### B. Fact-check UI — pre-existing
|
|
158
|
+
|
|
159
|
+
| file | line | string |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `ChatBlock/components/HalloumiFeedback.jsx` | 12–15 | `VERIFY_CLAIM_MESSAGES` (3 progress lines) |
|
|
162
|
+
| `ChatBlock/components/HalloumiFeedback.jsx` | 82 | `Please allow a few minutes for claim verification…` |
|
|
163
|
+
| `ChatBlock/components/HalloumiFeedback.jsx` | 89, 107 | `Fact-check AI answer`, `Retry Fact-check AI answer` |
|
|
164
|
+
| `ChatBlock/components/markdown/Citation.jsx` | 39 | `This doc doesn't have a link.` |
|
|
165
|
+
| `ChatBlock/chat/AIMessage.tsx` | 470, 700 | `See all sources`, `Sources` |
|
|
166
|
+
|
|
167
|
+
### C. Chat shell
|
|
168
|
+
|
|
169
|
+
| file | line | string |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `ChatBlock/ChatBlockView.jsx` | 45 | `Chatbot` |
|
|
172
|
+
| `ChatBlock/chat/ChatWindow.tsx` | 409 | `Deep research on` (partially done — 3 call sites exist) |
|
|
173
|
+
| `ChatBlock/chat/ChatMessage.tsx` | 26 | `Error` |
|
|
174
|
+
| `ChatBlock/components/FeedbackModal.jsx` | 116 | `Cancel` |
|
|
175
|
+
| `ChatBlock/components/RelatedQuestions.jsx` | 29 | `Related questions:` |
|
|
176
|
+
| `ChatBlock/packets/MultiToolRenderer.tsx` | 267 | `Done` |
|
|
177
|
+
| `ChatBlock/packets/renderers/FetchToolRenderer.tsx` | 35 | `Fetching Documents` |
|
|
178
|
+
| `ChatBlock/packets/renderers/ImageToolRenderer.tsx` | 38 | `Generated Images` |
|
|
179
|
+
|
|
180
|
+
### D. Accessibility strings — same priority as visible copy
|
|
181
|
+
|
|
182
|
+
`title` / `aria-label` / `alt` in `ChatMessageFeedback.jsx:39-40`,
|
|
183
|
+
`UserActionsToolbar.jsx:25-26`, `QualityCheckToggle.jsx:15`, `Source.jsx:23`,
|
|
184
|
+
`WebResultIcon.tsx:31`, `AIMessage.tsx:537`, `ChatWindow.tsx:223`. An untranslated
|
|
185
|
+
`aria-label` is a screen-reader defect, not a cosmetic gap.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 5. Testing
|
|
190
|
+
|
|
191
|
+
- **Existing tests assert English prose.** `utils.test.jsx` (verdict labels),
|
|
192
|
+
`ClaimModal.test.jsx` (header, badge, `data-verdict`), `HalloumiFeedback.test.jsx`
|
|
193
|
+
(banner copy, pluralisation). With descriptors, assert `id`/`defaultMessage`
|
|
194
|
+
instead. Do not wrap these tests in `IntlProvider` — that is the point of returning
|
|
195
|
+
descriptors.
|
|
196
|
+
- **Component tests and snapshots** will churn: `ClaimModal.test.jsx.snap` and the
|
|
197
|
+
HalloumiFeedback assertions contain the current English. Expect to re-run with `-u`.
|
|
198
|
+
- **Add a drift guard**: one test that every descriptor exported from
|
|
199
|
+
`ChatBlock/messages.js` has a matching `id` in `locales/volto.pot`. Cheap, and it is
|
|
200
|
+
the only thing that stops copy drifting away from the catalogue once translators
|
|
201
|
+
are involved.
|
|
202
|
+
- Frontend suite command:
|
|
203
|
+
`CI=true RAZZLE_JEST_CONFIG=src/addons/volto-eea-chatbot/jest-addon.config.js yarn test src/addons/volto-eea-chatbot/src --watchAll=false`
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 6. Workflow and CI
|
|
208
|
+
|
|
209
|
+
`make i18n` is manual and the `.pot` predates the fact-check work, so the catalogue is
|
|
210
|
+
already out of sync with the code. Add a CI step:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
make i18n
|
|
214
|
+
git diff --exit-code locales/ # fails when a string was added without extracting
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Translation fill (de/it/ro) is a separate, human task — the EEA translation workflow,
|
|
218
|
+
not a code task. Until it happens, English fallback applies and nothing changes for
|
|
219
|
+
users.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 7. Suggested commit order
|
|
224
|
+
|
|
225
|
+
1. `src/ChatBlock/messages.js` + descriptors for the fact-check UI (§4A) and the
|
|
226
|
+
components that render them. One reviewable commit, no behaviour change.
|
|
227
|
+
2. Banner pluralisation to ICU.
|
|
228
|
+
3. Pre-existing fact-check copy (§4B) and `ClaimSegments`.
|
|
229
|
+
4. Chat shell (§4C) and accessibility strings (§4D).
|
|
230
|
+
5. CI extraction check + regenerated `locales/`.
|
|
231
|
+
|
|
232
|
+
Each step keeps the suite green on its own; do not mix 1–4 with the `.pot` regeneration.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 8. Gotchas
|
|
237
|
+
|
|
238
|
+
- **Do not key translations by English text at render time** (no
|
|
239
|
+
`translate(labelMap[score])` lookup tables). Descriptors only.
|
|
240
|
+
- `utils/index.tsx` is imported by tests with no `IntlProvider` in scope. Descriptors
|
|
241
|
+
preserve that; passing `intl` into the helpers would force a provider into every test.
|
|
242
|
+
- The extractor only sees `messages.js` files and literal `<FormattedMessage>` /
|
|
243
|
+
`defineMessages` usage — computed or concatenated strings are invisible to it.
|
|
244
|
+
- Curly quotes in the banner (`“not confirmed”`) must survive extraction; the `.po`
|
|
245
|
+
files are `charset=utf-8`, so this is fine, but do not let a formatter normalise them.
|
|
246
|
+
- `en/volto.po` stays empty on purpose.
|
|
247
|
+
- Volto core already translates generic words (`Cancel`, `Error`); check for an
|
|
248
|
+
existing id before adding a duplicate.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 9. Out of scope
|
|
253
|
+
|
|
254
|
+
- Translating answer text, source titles or chunk content — that is Onyx's corpus.
|
|
255
|
+
- Date/number localisation (`${score}%` formatting) — separate concern, and the
|
|
256
|
+
percentage is verdict-derived, not a measurement.
|
|
257
|
+
- Filling the de/it/ro catalogues.
|
package/package.json
CHANGED
|
@@ -3,6 +3,25 @@ import AISearchInputView from './AISearchInputView';
|
|
|
3
3
|
import AISearchInputEdit from './AISearchInputEdit';
|
|
4
4
|
import { AISearchInputSchema } from './schema';
|
|
5
5
|
|
|
6
|
+
// The block is offered to Managers everywhere, and to any role on content
|
|
7
|
+
// living under a sandbox path (e.g. /en/sandbox), so that non-Managers can
|
|
8
|
+
// add and experiment with it there.
|
|
9
|
+
export const SANDBOX_PATH = '/sandbox';
|
|
10
|
+
|
|
11
|
+
export const isAISearchInputRestricted = ({ user, properties }) => {
|
|
12
|
+
// properties['@id'] is the backend API url of the page being edited
|
|
13
|
+
const id = properties?.['@id'];
|
|
14
|
+
if (typeof id === 'string' && id.includes(SANDBOX_PATH)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (user?.roles) {
|
|
19
|
+
return !user.roles.find((role) => role === 'Manager');
|
|
20
|
+
}
|
|
21
|
+
// backward compatibility for older Volto versions
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
|
|
6
25
|
export default function installAISearchInputBlock(config) {
|
|
7
26
|
config.blocks.blocksConfig.eeaAISearchInput = {
|
|
8
27
|
id: 'eeaAISearchInput',
|
|
@@ -11,12 +30,7 @@ export default function installAISearchInputBlock(config) {
|
|
|
11
30
|
group: 'common',
|
|
12
31
|
view: AISearchInputView,
|
|
13
32
|
edit: AISearchInputEdit,
|
|
14
|
-
restricted:
|
|
15
|
-
if (user?.roles) {
|
|
16
|
-
return !user.roles.find((role) => role === 'Manager');
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
},
|
|
33
|
+
restricted: isAISearchInputRestricted,
|
|
20
34
|
mostUsed: false,
|
|
21
35
|
blockHasOwnFocusManagement: false,
|
|
22
36
|
sidebarTab: 1,
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
} from 'semantic-ui-react';
|
|
12
12
|
import { serializeNodes } from '@plone/volto-slate/editor/render';
|
|
13
13
|
import {
|
|
14
|
+
useChunkEvidence,
|
|
14
15
|
useDeepCompareMemoize,
|
|
15
16
|
useQualityMarkers,
|
|
16
17
|
useScrollonStream,
|
|
17
18
|
} from '@eeacms/volto-eea-chatbot/ChatBlock/hooks';
|
|
19
|
+
import { CHUNK_EVIDENCE_WINDOW } from '@eeacms/volto-eea-chatbot/ChatBlock/services/chunkEvidence';
|
|
18
20
|
import {
|
|
19
21
|
MultiToolRenderer,
|
|
20
22
|
RendererComponent,
|
|
@@ -90,14 +92,19 @@ function addQualityMarkersPlugin() {
|
|
|
90
92
|
* The backend accepts {text, title, source_type} dicts so it can
|
|
91
93
|
* include document titles in LLM prompts without polluting the raw
|
|
92
94
|
* text (which would break span-offset matching).
|
|
95
|
+
*
|
|
96
|
+
* `kind` tells the fact-checker how much text it actually got:
|
|
97
|
+
* `chunk` = real Onyx chunk text, `snippet` = a search blurb, which can
|
|
98
|
+
* never prove a claim false.
|
|
93
99
|
*/
|
|
94
|
-
export function buildHalloumiSource(doc: any, text: string) {
|
|
100
|
+
export function buildHalloumiSource(doc: any, text: string, kind = 'snippet') {
|
|
95
101
|
const cleanedText = text.replace(/\u00A0/g, ' ');
|
|
96
102
|
return {
|
|
97
103
|
text: cleanedText,
|
|
98
104
|
title: doc.semantic_identifier || null,
|
|
99
105
|
source_type: doc.source_type || null,
|
|
100
106
|
link: doc.link || null,
|
|
107
|
+
kind,
|
|
101
108
|
};
|
|
102
109
|
}
|
|
103
110
|
|
|
@@ -145,10 +152,14 @@ export function getContextSources(
|
|
|
145
152
|
halloumiSource: buildHalloumiSource(doc, text),
|
|
146
153
|
};
|
|
147
154
|
})
|
|
148
|
-
: (
|
|
155
|
+
: // `messageProcessor.getMessage()` emits `toolCall` (singular); older
|
|
156
|
+
// Onyx v2 message shapes used `toolCalls`. Accept both — reading only the
|
|
157
|
+
// plural made 'all' mode send zero sources, which the fact-checker then
|
|
158
|
+
// reported as "Answer cannot be verified due to empty sources."
|
|
159
|
+
[message.toolCall, ...(message.toolCalls || [])].reduce(
|
|
149
160
|
(acc: any, cur: any) => [
|
|
150
161
|
...acc,
|
|
151
|
-
...(cur.tool_result || []).map((doc: any) => {
|
|
162
|
+
...((cur && cur.tool_result) || []).map((doc: any) => {
|
|
152
163
|
const cleanedText = (doc.content || '').replace(/\u00A0/g, ' ');
|
|
153
164
|
return {
|
|
154
165
|
...doc,
|
|
@@ -209,6 +220,7 @@ export function AIMessage({
|
|
|
209
220
|
persona,
|
|
210
221
|
maxContextSegments,
|
|
211
222
|
batchSize,
|
|
223
|
+
chunkEvidenceWindow = CHUNK_EVIDENCE_WINDOW,
|
|
212
224
|
isLastMessage,
|
|
213
225
|
className = '',
|
|
214
226
|
chatWindowEndRef,
|
|
@@ -293,10 +305,56 @@ export function AIMessage({
|
|
|
293
305
|
// logic intact.
|
|
294
306
|
const showSourcesTab = showSources && !hideSourcesTab;
|
|
295
307
|
|
|
296
|
-
const contextSources =
|
|
297
|
-
message,
|
|
298
|
-
sources,
|
|
299
|
-
|
|
308
|
+
const contextSources = useMemo(
|
|
309
|
+
() => getContextSources(message, sources, qualityCheckContext),
|
|
310
|
+
[message, sources, qualityCheckContext],
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const qualityControlRequested =
|
|
314
|
+
messageDisplayed &&
|
|
315
|
+
qualityCheck &&
|
|
316
|
+
qualityCheck !== 'disabled' &&
|
|
317
|
+
forceHalloumi &&
|
|
318
|
+
showSources &&
|
|
319
|
+
message.messageId &&
|
|
320
|
+
message.messageId > -1 &&
|
|
321
|
+
(qualityCheck === 'enabled' ||
|
|
322
|
+
qualityCheckEnabled ||
|
|
323
|
+
verificationTriggered);
|
|
324
|
+
|
|
325
|
+
// The Onyx stream only carries search blurbs (~600 chars per document), while
|
|
326
|
+
// the answer was written from full chunk text. Resolve the real text for
|
|
327
|
+
// exactly the documents we are about to send, otherwise the fact-checker
|
|
328
|
+
// marks supported claims as unsupported. See services/chunkEvidence.ts.
|
|
329
|
+
const { evidence, isFetchingEvidence, evidenceSettled } = useChunkEvidence(
|
|
330
|
+
contextSources,
|
|
331
|
+
{ enabled: qualityControlRequested, window: chunkEvidenceWindow },
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const enrichedSources = useMemo(
|
|
335
|
+
() =>
|
|
336
|
+
contextSources.map((src: any) => {
|
|
337
|
+
const resolved = src.document_id ? evidence[src.document_id] : null;
|
|
338
|
+
if (!resolved || !resolved.text) {
|
|
339
|
+
return src;
|
|
340
|
+
}
|
|
341
|
+
// Same nbsp cleaning as the blurb path: the frontend joins these
|
|
342
|
+
// strings to highlight segments, the backend indexes the same text, so
|
|
343
|
+
// both sides must stay byte-identical.
|
|
344
|
+
const cleanedText = resolved.text.replace(/\u00A0/g, ' ');
|
|
345
|
+
return {
|
|
346
|
+
...src,
|
|
347
|
+
text: resolved.text,
|
|
348
|
+
halloumiContext: cleanedText,
|
|
349
|
+
halloumiSource: {
|
|
350
|
+
...(src.halloumiSource || {}),
|
|
351
|
+
text: cleanedText,
|
|
352
|
+
kind: resolved.kind,
|
|
353
|
+
},
|
|
354
|
+
evidenceChunks: resolved.chunks,
|
|
355
|
+
};
|
|
356
|
+
}),
|
|
357
|
+
[contextSources, evidence],
|
|
300
358
|
);
|
|
301
359
|
|
|
302
360
|
// Deduplicate sources by text content. The streaming backend often sends
|
|
@@ -306,7 +364,7 @@ export function AIMessage({
|
|
|
306
364
|
// between frontend (span highlighting) and backend (evidence spans).
|
|
307
365
|
const dedupedSources = useMemo(() => {
|
|
308
366
|
const seen = new Set<string>();
|
|
309
|
-
return
|
|
367
|
+
return enrichedSources.filter((src: any) => {
|
|
310
368
|
const key = src.halloumiContext || src.halloumiSource?.text || '';
|
|
311
369
|
if (key && !seen.has(key)) {
|
|
312
370
|
seen.add(key);
|
|
@@ -314,21 +372,12 @@ export function AIMessage({
|
|
|
314
372
|
}
|
|
315
373
|
return false;
|
|
316
374
|
});
|
|
317
|
-
}, [
|
|
375
|
+
}, [enrichedSources]);
|
|
318
376
|
|
|
319
377
|
const stableContextSources = useDeepCompareMemoize(dedupedSources);
|
|
320
378
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
qualityCheck &&
|
|
324
|
-
qualityCheck !== 'disabled' &&
|
|
325
|
-
forceHalloumi &&
|
|
326
|
-
showSources &&
|
|
327
|
-
message.messageId &&
|
|
328
|
-
message.messageId > -1 &&
|
|
329
|
-
(qualityCheck === 'enabled' ||
|
|
330
|
-
qualityCheckEnabled ||
|
|
331
|
-
verificationTriggered);
|
|
379
|
+
// Don't spend a fact-check call on blurbs while the real text is in flight.
|
|
380
|
+
const doQualityControl = qualityControlRequested && evidenceSettled;
|
|
332
381
|
|
|
333
382
|
const { markers, isLoadingHalloumi, retryHalloumi }: any = useQualityMarkers(
|
|
334
383
|
doQualityControl,
|
|
@@ -345,7 +394,7 @@ export function AIMessage({
|
|
|
345
394
|
qualityCheckStages,
|
|
346
395
|
);
|
|
347
396
|
|
|
348
|
-
const isFetching = isLoadingHalloumi || isLoading;
|
|
397
|
+
const isFetching = isLoadingHalloumi || isLoading || isFetchingEvidence;
|
|
349
398
|
const halloumiMessage =
|
|
350
399
|
isMessageVerified || doQualityControl ? scoreStage?.label : '';
|
|
351
400
|
|
|
@@ -536,6 +585,7 @@ export function AIMessage({
|
|
|
536
585
|
showVerifyClaimsButton={showVerifyClaimsButton}
|
|
537
586
|
retryHalloumi={retryHalloumi}
|
|
538
587
|
emptyClaims={emptyClaims}
|
|
588
|
+
contextQuality={markers?.context_quality}
|
|
539
589
|
/>
|
|
540
590
|
)}
|
|
541
591
|
|
|
@@ -54,6 +54,7 @@ interface ChatWindowProps {
|
|
|
54
54
|
onDemandInputToggle?: boolean;
|
|
55
55
|
maxContextSegments?: number;
|
|
56
56
|
batchSize?: number;
|
|
57
|
+
chunkEvidenceWindow?: number;
|
|
57
58
|
onyxVersion?: '2' | '3';
|
|
58
59
|
isPlaywrightTest?: boolean;
|
|
59
60
|
initialQuery?: string | null;
|
|
@@ -97,6 +98,7 @@ function ChatWindow({
|
|
|
97
98
|
onDemandInputToggle = true,
|
|
98
99
|
maxContextSegments = 0,
|
|
99
100
|
batchSize,
|
|
101
|
+
chunkEvidenceWindow,
|
|
100
102
|
onyxVersion = '3',
|
|
101
103
|
hideSourcesTab,
|
|
102
104
|
extraRemarkPlugins,
|
|
@@ -253,6 +255,7 @@ function ChatWindow({
|
|
|
253
255
|
persona={persona.id}
|
|
254
256
|
maxContextSegments={maxContextSegments}
|
|
255
257
|
batchSize={batchSize}
|
|
258
|
+
chunkEvidenceWindow={chunkEvidenceWindow}
|
|
256
259
|
isLastMessage={index === messages.length - 1}
|
|
257
260
|
className={
|
|
258
261
|
index === messages.length - 1 ? 'most-recent' : ''
|
|
@@ -73,8 +73,10 @@ const HalloumiFeedback = ({
|
|
|
73
73
|
sources,
|
|
74
74
|
retryHalloumi,
|
|
75
75
|
emptyClaims,
|
|
76
|
+
contextQuality,
|
|
76
77
|
}) => {
|
|
77
78
|
const claims = (markers?.claims || []).filter((claim) => !claim.skipped);
|
|
79
|
+
const limitedClaims = claims.filter((claim) => claim.context_limited);
|
|
78
80
|
const noClaimsScore = claims[0]?.score === null;
|
|
79
81
|
const messageBySource =
|
|
80
82
|
'Please allow a few minutes for claim verification when many references are involved.';
|
|
@@ -123,6 +125,23 @@ const HalloumiFeedback = ({
|
|
|
123
125
|
</MessageContent>
|
|
124
126
|
</Message>
|
|
125
127
|
)}
|
|
128
|
+
|
|
129
|
+
{/* The fact-checker only saw part of the answer's sources, so a "not
|
|
130
|
+
confirmed" verdict may mean missing text rather than a mistake. Said
|
|
131
|
+
once, at answer level - the claim modal already says "Couldn't be
|
|
132
|
+
checked" and needs no repeat of this. */}
|
|
133
|
+
{contextQuality?.level === 'partial' && (
|
|
134
|
+
<Message color="yellow" className="context-quality-message">
|
|
135
|
+
<MessageContent>
|
|
136
|
+
<strong>Some sources could not be loaded in full.</strong>{' '}
|
|
137
|
+
{limitedClaims.length > 0
|
|
138
|
+
? `${limitedClaims.length} ${
|
|
139
|
+
limitedClaims.length === 1 ? 'claim was' : 'claims were'
|
|
140
|
+
} checked against short excerpts, so “not confirmed” may mean missing text rather than a mistake.`
|
|
141
|
+
: 'Every claim was still matched against the text that was available, so this did not change the result.'}
|
|
142
|
+
</MessageContent>
|
|
143
|
+
</Message>
|
|
144
|
+
)}
|
|
126
145
|
</>
|
|
127
146
|
);
|
|
128
147
|
};
|