@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 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
  [![Releases](https://img.shields.io/github/v/release/eea/volto-eea-chatbot)](https://github.com/eea/volto-eea-chatbot/releases)
4
4
 
5
- [![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-eea-chatbot%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-chatbot/job/master/display/redirect)
6
- [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
7
- [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
8
- [![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
9
- [![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot)
10
-
11
5
  [![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-eea-chatbot%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-eea-chatbot/job/develop/display/redirect)
12
6
  [![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&branch=develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
13
7
  [![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-eea-chatbot&branch=develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-eea-chatbot&branch=develop)
@@ -22,7 +16,7 @@
22
16
 
23
17
  ## Features
24
18
 
25
- https://github.com/user-attachments/assets/f9b5f813-672f-4e4d-81d0-bf2aec35b587
19
+ ![Volto Chatbot](https://raw.githubusercontent.com/eea/volto-eea-chatbot/master/docs/volto-eea-chatbot.gif)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-chatbot",
3
- "version": "4.1.3",
3
+ "version": "4.1.4",
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",
@@ -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: ({ user }) => {
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
- : (message.toolCalls || []).reduce(
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 = getContextSources(
297
- message,
298
- sources,
299
- qualityCheckContext,
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 contextSources.filter((src: any) => {
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
- }, [contextSources]);
375
+ }, [enrichedSources]);
318
376
 
319
377
  const stableContextSources = useDeepCompareMemoize(dedupedSources);
320
378
 
321
- const doQualityControl =
322
- messageDisplayed &&
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
  };