@eeacms/volto-eea-chatbot 4.0.0 → 4.1.1
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 +32 -1
- package/README.md +72 -0
- package/package.json +1 -1
- package/src/ChatBlock/ChatBlockView.jsx +7 -1
- package/src/ChatBlock/chat/AIMessage.tsx +23 -8
- 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/hooks/useQualityMarkers.js +10 -2
- 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 +2 -9
- package/src/ChatBlock/types/interfaces.ts +25 -0
- package/src/ChatBlock/variations.ts +44 -0
- package/src/halloumi/middleware.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,35 @@ 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.
|
|
7
|
+
### [4.1.1](https://github.com/eea/volto-eea-chatbot/compare/4.1.0...4.1.1) - 14 September 2026
|
|
8
|
+
|
|
9
|
+
### [4.1.0](https://github.com/eea/volto-eea-chatbot/compare/4.0.0...4.1.0) - 31 August 2026
|
|
10
|
+
|
|
11
|
+
#### :rocket: New Features
|
|
12
|
+
|
|
13
|
+
- feat: expose message context + thread extra markdown into classic message [Tiberiu Ichim - [`b1600ce`](https://github.com/eea/volto-eea-chatbot/commit/b1600cede2221b846d36be42247f9068f22f06c4)]
|
|
14
|
+
- feat: allow presentations to plug custom components into message rendering [Tiberiu Ichim - [`3f7d82b`](https://github.com/eea/volto-eea-chatbot/commit/3f7d82b9231fc2a8a92c72471068998988dadde5)]
|
|
15
|
+
- feat: block presentation variations registry [Tiberiu Ichim - [`bc92ba7`](https://github.com/eea/volto-eea-chatbot/commit/bc92ba7a98b832cf34f644b8eb7294ae4542f04d)]
|
|
16
|
+
- feat: add configurable batch size for fact-checking [Tiberiu Ichim - [`4e81cb9`](https://github.com/eea/volto-eea-chatbot/commit/4e81cb94075f8f31a70affed6f02ac8495863a57)]
|
|
17
|
+
|
|
18
|
+
#### :bug: Bug Fixes
|
|
19
|
+
|
|
20
|
+
- fix: stop hand-declaring the 'variation' schema field [Tiberiu Ichim - [`f0ada7e`](https://github.com/eea/volto-eea-chatbot/commit/f0ada7e7fb1843c562b0af44235f3a07b5253e7d)]
|
|
21
|
+
- fix: skip citations with empty document_id in CITATION_DELTA [Tiberiu Ichim - [`81c9441`](https://github.com/eea/volto-eea-chatbot/commit/81c9441fa041b966f0e05b13fe271d44d9ee1421)]
|
|
22
|
+
|
|
23
|
+
#### :house: Internal changes
|
|
24
|
+
|
|
25
|
+
- chore: bump version to 4.1.0 (block presentation variations) [Tiberiu Ichim - [`45d7d3d`](https://github.com/eea/volto-eea-chatbot/commit/45d7d3db43158664c960d47585ffe749c490782c)]
|
|
26
|
+
- chore: change default batchSize from 30 to 20 [Tiberiu Ichim - [`c5c5f4c`](https://github.com/eea/volto-eea-chatbot/commit/c5c5f4cb90fd74accd534ccd266f79592e38ec3a)]
|
|
27
|
+
|
|
28
|
+
#### :house: Documentation changes
|
|
29
|
+
|
|
30
|
+
- docs: document block presentation variations API [Tiberiu Ichim - [`8e98061`](https://github.com/eea/volto-eea-chatbot/commit/8e98061c3ce53eb701c78d3cb1eb783c8cb47234)]
|
|
31
|
+
|
|
32
|
+
#### :hammer_and_wrench: Others
|
|
33
|
+
|
|
34
|
+
- test: update citation delta test to match restored behavior [Tiberiu Ichim - [`de9f40e`](https://github.com/eea/volto-eea-chatbot/commit/de9f40e4dfd4a69f800aaf9f1b83fdf7e197b9ab)]
|
|
35
|
+
## [4.0.0](https://github.com/eea/volto-eea-chatbot/compare/3.0.1...4.0.0) - 13 August 2026
|
|
8
36
|
|
|
9
37
|
#### :bug: Bug Fixes
|
|
10
38
|
|
|
@@ -60,6 +88,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
60
88
|
#### :house: Internal changes
|
|
61
89
|
|
|
62
90
|
- 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)]
|
|
91
|
+
- chore: [JENKINSFILE] sanitize sonarqube command Refs #305084 [valentinab25 - [`83bf08b`](https://github.com/eea/volto-eea-chatbot/commit/83bf08b7188548a50801b8e464a4d113f0cbbac4)]
|
|
63
92
|
|
|
64
93
|
#### :house: Documentation changes
|
|
65
94
|
|
|
@@ -137,6 +166,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
137
166
|
#### :house: Internal changes
|
|
138
167
|
|
|
139
168
|
- style: Automated code fix [eea-jenkins - [`8a8c3c4`](https://github.com/eea/volto-eea-chatbot/commit/8a8c3c4172a4f669661378cf1b5a3569d85609e6)]
|
|
169
|
+
- chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`535d986`](https://github.com/eea/volto-eea-chatbot/commit/535d986b7adc77743a668bc4ac63f835eef58df3)]
|
|
140
170
|
|
|
141
171
|
#### :hammer_and_wrench: Others
|
|
142
172
|
|
|
@@ -147,6 +177,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
147
177
|
#### :house: Internal changes
|
|
148
178
|
|
|
149
179
|
- style: Automated code fix [eea-jenkins - [`fdcd884`](https://github.com/eea/volto-eea-chatbot/commit/fdcd8848fd4c3f990ca5ea021f407487aebd6010)]
|
|
180
|
+
- chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`3d428d7`](https://github.com/eea/volto-eea-chatbot/commit/3d428d72f32c3d05452b0961c76f5db1c416e05c)]
|
|
150
181
|
|
|
151
182
|
#### :hammer_and_wrench: Others
|
|
152
183
|
|
package/README.md
CHANGED
|
@@ -116,6 +116,78 @@ 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
|
+
|
|
119
191
|
## Quality Checks (Fact-Checking)
|
|
120
192
|
|
|
121
193
|
When `qualityCheck` is enabled, the chatbot sends AI answers and their source
|
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}
|
|
@@ -117,14 +117,14 @@ function mapToolDocumentsToText(message: any) {
|
|
|
117
117
|
return {};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function getContextSources(
|
|
120
|
+
export function getContextSources(
|
|
121
121
|
message: any,
|
|
122
122
|
sources: any,
|
|
123
|
-
qualityCheckContext: any,
|
|
123
|
+
qualityCheckContext: any = 'citations',
|
|
124
124
|
) {
|
|
125
125
|
const documentIdToText = mapToolDocumentsToText(message);
|
|
126
126
|
|
|
127
|
-
return qualityCheckContext
|
|
127
|
+
return qualityCheckContext !== 'all'
|
|
128
128
|
? sources.map((doc: any) => {
|
|
129
129
|
// Prefer content from tool packets (may have enriched text),
|
|
130
130
|
// but fall back to doc.content from final_documents.
|
|
@@ -196,7 +196,7 @@ export function AIMessage({
|
|
|
196
196
|
feedbackReasons,
|
|
197
197
|
qualityCheck,
|
|
198
198
|
qualityCheckStages,
|
|
199
|
-
qualityCheckContext,
|
|
199
|
+
qualityCheckContext = 'citations',
|
|
200
200
|
qualityCheckEnabled,
|
|
201
201
|
noSupportDocumentsMessage,
|
|
202
202
|
totalFailMessage,
|
|
@@ -205,10 +205,15 @@ export function AIMessage({
|
|
|
205
205
|
enableMatomoTracking,
|
|
206
206
|
persona,
|
|
207
207
|
maxContextSegments,
|
|
208
|
+
batchSize,
|
|
208
209
|
isLastMessage,
|
|
209
210
|
className = '',
|
|
210
211
|
chatWindowEndRef,
|
|
211
212
|
showTools,
|
|
213
|
+
hideSourcesTab,
|
|
214
|
+
extraRemarkPlugins,
|
|
215
|
+
extraMarkdownComponents,
|
|
216
|
+
extraRehypePlugins,
|
|
212
217
|
}: ChatMessageProps) {
|
|
213
218
|
const [allToolsDisplayed, setAllToolsDisplayed] = useState(false);
|
|
214
219
|
const [messageDisplayed, setMessageDisplayed] = useState(false);
|
|
@@ -277,6 +282,12 @@ export function AIMessage({
|
|
|
277
282
|
|
|
278
283
|
const showSources = messageDisplayed && sources.length > 0;
|
|
279
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
|
+
|
|
280
291
|
const contextSources = getContextSources(
|
|
281
292
|
message,
|
|
282
293
|
sources,
|
|
@@ -319,6 +330,7 @@ export function AIMessage({
|
|
|
319
330
|
addCitations(message.message, message),
|
|
320
331
|
stableContextSources,
|
|
321
332
|
maxContextSegments,
|
|
333
|
+
batchSize,
|
|
322
334
|
);
|
|
323
335
|
|
|
324
336
|
const claims = markers?.claims || [];
|
|
@@ -384,7 +396,7 @@ export function AIMessage({
|
|
|
384
396
|
const answerTab = (
|
|
385
397
|
<div className="answer-tab">
|
|
386
398
|
{/* Show first 3 sources inline */}
|
|
387
|
-
{
|
|
399
|
+
{showSourcesTab && (
|
|
388
400
|
<div className="sources">
|
|
389
401
|
{sources.slice(0, 3).map((source: any, i: number) => (
|
|
390
402
|
<SourceDetails source={source} key={i} index={source.index} />
|
|
@@ -448,6 +460,9 @@ export function AIMessage({
|
|
|
448
460
|
markers={markers}
|
|
449
461
|
stableContextSources={stableContextSources}
|
|
450
462
|
addQualityMarkersPlugin={addQualityMarkersPlugin}
|
|
463
|
+
extraRemarkPlugins={extraRemarkPlugins}
|
|
464
|
+
extraMarkdownComponents={extraMarkdownComponents}
|
|
465
|
+
extraRehypePlugins={extraRehypePlugins}
|
|
451
466
|
>
|
|
452
467
|
{({ content }) => (
|
|
453
468
|
<div className="message-text-wrapper">{content}</div>
|
|
@@ -524,7 +539,7 @@ export function AIMessage({
|
|
|
524
539
|
menuItem: { key: 'answer', content: 'Answer', className: 'answer-tab' },
|
|
525
540
|
pane: <Tab.Pane key="answer">{answerTab}</Tab.Pane>,
|
|
526
541
|
},
|
|
527
|
-
...(
|
|
542
|
+
...(showSourcesTab && !error
|
|
528
543
|
? [
|
|
529
544
|
{
|
|
530
545
|
menuItem: {
|
|
@@ -573,14 +588,14 @@ export function AIMessage({
|
|
|
573
588
|
secondary: true,
|
|
574
589
|
pointing: true,
|
|
575
590
|
fluid: true,
|
|
576
|
-
className: cx({ 'without-sources': !
|
|
591
|
+
className: cx({ 'without-sources': !showSourcesTab }),
|
|
577
592
|
}}
|
|
578
593
|
panes={panes}
|
|
579
594
|
renderActiveOnly={false}
|
|
580
595
|
/>
|
|
581
596
|
|
|
582
597
|
{/* Sources sidebar */}
|
|
583
|
-
{
|
|
598
|
+
{showSourcesTab && !error && (
|
|
584
599
|
<Sidebar
|
|
585
600
|
visible={showSourcesSidebar}
|
|
586
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);
|
|
@@ -5,13 +5,18 @@ const Sentry = loadable.lib(
|
|
|
5
5
|
() => import(/* webpackChunkName: "s_entry-browser" */ '@sentry/browser'), // chunk name avoids ad blockers
|
|
6
6
|
);
|
|
7
7
|
|
|
8
|
-
async function fetchHalloumi(answer, sources, maxContextSegments) {
|
|
8
|
+
async function fetchHalloumi(answer, sources, maxContextSegments, batchSize) {
|
|
9
9
|
const halloumiResponse = await fetch('/_ha/generate', {
|
|
10
10
|
method: 'POST',
|
|
11
11
|
headers: {
|
|
12
12
|
'Content-Type': 'application/json',
|
|
13
13
|
},
|
|
14
|
-
body: JSON.stringify({
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
answer,
|
|
16
|
+
sources,
|
|
17
|
+
maxContextSegments,
|
|
18
|
+
...(batchSize != null && { batch_size: batchSize }),
|
|
19
|
+
}),
|
|
15
20
|
});
|
|
16
21
|
return halloumiResponse;
|
|
17
22
|
}
|
|
@@ -38,6 +43,7 @@ export function useQualityMarkers(
|
|
|
38
43
|
message,
|
|
39
44
|
sources,
|
|
40
45
|
maxContextSegments = 0,
|
|
46
|
+
batchSize,
|
|
41
47
|
) {
|
|
42
48
|
const [halloumiResponse, setHalloumiResponse] = React.useState(null);
|
|
43
49
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
@@ -70,6 +76,7 @@ export function useQualityMarkers(
|
|
|
70
76
|
message,
|
|
71
77
|
halloumiSources,
|
|
72
78
|
maxContextSegments,
|
|
79
|
+
batchSize,
|
|
73
80
|
);
|
|
74
81
|
const body = await feedback.json();
|
|
75
82
|
// console.log({ message, sources, body });
|
|
@@ -97,6 +104,7 @@ export function useQualityMarkers(
|
|
|
97
104
|
message,
|
|
98
105
|
sources,
|
|
99
106
|
maxContextSegments,
|
|
107
|
+
batchSize,
|
|
100
108
|
]);
|
|
101
109
|
|
|
102
110
|
if (halloumiResponse !== null) {
|
package/src/ChatBlock/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import codeSVG from '@plone/volto/icons/code.svg';
|
|
2
2
|
import ChatBlockView from './ChatBlockView';
|
|
3
3
|
import ChatBlockEdit from './ChatBlockEdit';
|
|
4
|
+
import ChatWindow from './chat/ChatWindow';
|
|
4
5
|
import { ChatBlockSchema } from './schema';
|
|
5
6
|
|
|
6
7
|
export default function installChatBlock(config) {
|
|
@@ -26,7 +27,14 @@ export default function installChatBlock(config) {
|
|
|
26
27
|
addPermission: [],
|
|
27
28
|
view: [],
|
|
28
29
|
},
|
|
29
|
-
variations: [
|
|
30
|
+
variations: [
|
|
31
|
+
{
|
|
32
|
+
id: 'classic',
|
|
33
|
+
title: 'Classic',
|
|
34
|
+
isDefault: true,
|
|
35
|
+
view: ChatWindow,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
30
38
|
};
|
|
31
39
|
|
|
32
40
|
if (!config.blocks.blocksConfig.danswerChat) {
|
|
@@ -87,6 +87,9 @@ export function RendererComponent({
|
|
|
87
87
|
markers,
|
|
88
88
|
stableContextSources,
|
|
89
89
|
addQualityMarkersPlugin,
|
|
90
|
+
extraMarkdownComponents,
|
|
91
|
+
extraRemarkPlugins,
|
|
92
|
+
extraRehypePlugins,
|
|
90
93
|
}: {
|
|
91
94
|
packets: Packet[];
|
|
92
95
|
onComplete: () => void;
|
|
@@ -98,6 +101,9 @@ export function RendererComponent({
|
|
|
98
101
|
markers?: any;
|
|
99
102
|
stableContextSources?: any;
|
|
100
103
|
addQualityMarkersPlugin?: any;
|
|
104
|
+
extraMarkdownComponents?: Record<string, React.ComponentType<any>>;
|
|
105
|
+
extraRemarkPlugins?: any[];
|
|
106
|
+
extraRehypePlugins?: any[];
|
|
101
107
|
}) {
|
|
102
108
|
const RendererFn = findRenderer({ packets });
|
|
103
109
|
|
|
@@ -116,6 +122,9 @@ export function RendererComponent({
|
|
|
116
122
|
markers={markers}
|
|
117
123
|
stableContextSources={stableContextSources}
|
|
118
124
|
addQualityMarkersPlugin={addQualityMarkersPlugin}
|
|
125
|
+
extraMarkdownComponents={extraMarkdownComponents}
|
|
126
|
+
extraRemarkPlugins={extraRemarkPlugins}
|
|
127
|
+
extraRehypePlugins={extraRehypePlugins}
|
|
119
128
|
>
|
|
120
129
|
{children}
|
|
121
130
|
</RendererFn>
|
|
@@ -30,6 +30,9 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
|
|
|
30
30
|
markers,
|
|
31
31
|
stableContextSources,
|
|
32
32
|
addQualityMarkersPlugin,
|
|
33
|
+
extraMarkdownComponents,
|
|
34
|
+
extraRemarkPlugins = [],
|
|
35
|
+
extraRehypePlugins = [],
|
|
33
36
|
}) => {
|
|
34
37
|
const { remarkGfm } = libs;
|
|
35
38
|
|
|
@@ -163,9 +166,18 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
|
|
|
163
166
|
const renderedContent = (
|
|
164
167
|
<div className="message-text-content">
|
|
165
168
|
<Markdown
|
|
166
|
-
components={
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
components={{
|
|
170
|
+
...components(message, markers, stableContextSources),
|
|
171
|
+
...extraMarkdownComponents,
|
|
172
|
+
}}
|
|
173
|
+
remarkPlugins={[
|
|
174
|
+
...(remarkGfm ? [remarkGfm.default] : []),
|
|
175
|
+
...extraRemarkPlugins,
|
|
176
|
+
]}
|
|
177
|
+
rehypePlugins={[
|
|
178
|
+
...(addQualityMarkersPlugin ? [addQualityMarkersPlugin] : []),
|
|
179
|
+
...extraRehypePlugins,
|
|
180
|
+
]}
|
|
169
181
|
>
|
|
170
182
|
{displayContent}
|
|
171
183
|
</Markdown>
|
package/src/ChatBlock/schema.jsx
CHANGED
|
@@ -92,6 +92,7 @@ export function ChatBlockSchema({ assistants, data }) {
|
|
|
92
92
|
...(data.qualityCheck && data.qualityCheck !== 'disabled'
|
|
93
93
|
? [
|
|
94
94
|
'maxContextSegments',
|
|
95
|
+
'batchSize',
|
|
95
96
|
'noSupportDocumentsMessage',
|
|
96
97
|
'qualityCheckContext',
|
|
97
98
|
'qualityCheckStages',
|
|
@@ -109,6 +110,11 @@ export function ChatBlockSchema({ assistants, data }) {
|
|
|
109
110
|
},
|
|
110
111
|
],
|
|
111
112
|
properties: {
|
|
113
|
+
// NOTE: the `variation` field is NOT declared here on purpose. Volto
|
|
114
|
+
// core injects it automatically in the block sidebar
|
|
115
|
+
// (withVariationSchemaEnhancer, wired into BlockDataForm) from
|
|
116
|
+
// blocksConfig.eeaChatbot.variations whenever 2+ variations are
|
|
117
|
+
// registered. See @plone/volto/helpers/Extensions.
|
|
112
118
|
enableShowTotalFailMessage: {
|
|
113
119
|
title: 'Show total failure message',
|
|
114
120
|
type: 'boolean',
|
|
@@ -252,6 +258,13 @@ range is from 0 to 100`,
|
|
|
252
258
|
description:
|
|
253
259
|
'Optimize quality check performance by grouping sentences into a max number of segments. Set to 0 to disable grouping and instead use one sentence per segment.',
|
|
254
260
|
},
|
|
261
|
+
batchSize: {
|
|
262
|
+
title: 'Batch size',
|
|
263
|
+
type: 'number',
|
|
264
|
+
default: 20,
|
|
265
|
+
description:
|
|
266
|
+
'Number of claims to verify per LLM call. Higher values mean fewer LLM calls but larger prompts. Set to 1 for sequential per-claim verification.',
|
|
267
|
+
},
|
|
255
268
|
feedbackReasons: {
|
|
256
269
|
title: 'Feedback reasons',
|
|
257
270
|
description: 'Select the reasons for negative feedback.',
|
|
@@ -258,15 +258,8 @@ export class MessageProcessor {
|
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
260
|
const citationDelta = packet.obj as CitationDelta;
|
|
261
|
-
|
|
262
|
-
(citation
|
|
263
|
-
citation.citation_num != null && Boolean(citation.document_id),
|
|
264
|
-
);
|
|
265
|
-
if (explicitCitations.length > 0) {
|
|
266
|
-
this.clearFallbackCitations();
|
|
267
|
-
}
|
|
268
|
-
explicitCitations.forEach((citation: StreamingCitation) => {
|
|
269
|
-
if (!this._citations.has(citation.citation_num)) {
|
|
261
|
+
citationDelta.citations?.forEach((citation: StreamingCitation) => {
|
|
262
|
+
if (citation.document_id && !this._citations.has(citation.citation_num)) {
|
|
270
263
|
this._citations.set(citation.citation_num, citation.document_id);
|
|
271
264
|
}
|
|
272
265
|
});
|
|
@@ -82,6 +82,17 @@ export interface MessageRendererProps<T extends Packet = Packet> {
|
|
|
82
82
|
markers?: any;
|
|
83
83
|
stableContextSources?: any;
|
|
84
84
|
addQualityMarkersPlugin?: any;
|
|
85
|
+
/**
|
|
86
|
+
* Extra react-markdown component overrides (e.g. for custom elements
|
|
87
|
+
* produced by remark/rehype plugins), merged on top of the built-in
|
|
88
|
+
* mapping. Allows presentations to plug custom components inline in the
|
|
89
|
+
* message text.
|
|
90
|
+
*/
|
|
91
|
+
extraMarkdownComponents?: Record<string, React.ComponentType<any>>;
|
|
92
|
+
/** Extra remark plugins, appended after the built-in ones. */
|
|
93
|
+
extraRemarkPlugins?: any[];
|
|
94
|
+
/** Extra rehype plugins, appended after the built-in ones. */
|
|
95
|
+
extraRehypePlugins?: any[];
|
|
85
96
|
onComplete: () => void;
|
|
86
97
|
animate: boolean;
|
|
87
98
|
stopPacketSeen: boolean;
|
|
@@ -141,11 +152,25 @@ export interface ChatMessageProps {
|
|
|
141
152
|
enableMatomoTracking?: boolean;
|
|
142
153
|
persona?: number;
|
|
143
154
|
maxContextSegments?: number;
|
|
155
|
+
batchSize?: number;
|
|
144
156
|
isLastMessage?: boolean;
|
|
145
157
|
className?: string;
|
|
146
158
|
chatWindowRef?: React.RefObject<HTMLDivElement>;
|
|
147
159
|
chatWindowEndRef?: React.RefObject<HTMLDivElement>;
|
|
148
160
|
showTools?: PacketType[];
|
|
161
|
+
/**
|
|
162
|
+
* Hide the classic "Sources" tab, sidebar and inline source list for this
|
|
163
|
+
* presentation (the answer text still renders, and any injected markdown
|
|
164
|
+
* components still work). Used by variations that present the cited
|
|
165
|
+
* documents differently (e.g. inline catalogue cards).
|
|
166
|
+
*/
|
|
167
|
+
hideSourcesTab?: boolean;
|
|
168
|
+
/** Extra remark plugins merged after the built-in ones. */
|
|
169
|
+
extraRemarkPlugins?: any[];
|
|
170
|
+
/** Extra react-markdown components merged after the built-in ones. */
|
|
171
|
+
extraMarkdownComponents?: any;
|
|
172
|
+
/** Extra rehype plugins merged after the built-in ones. */
|
|
173
|
+
extraRehypePlugins?: any[];
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
export interface Persona {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import config from '@plone/volto/registry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A chatbot block presentation variation.
|
|
6
|
+
*
|
|
7
|
+
* The `view` component is a complete presentation of the chatbot block.
|
|
8
|
+
* It receives the same props the classic presentation (ChatWindow) receives
|
|
9
|
+
* today: `{ persona, data, block_id, isEditMode, isPlaywrightTest,
|
|
10
|
+
* initialQuery, initialDeepResearch, ... }` plus whatever the HOCs inject
|
|
11
|
+
* (e.g. `rehypePrism`, `remarkGfm` when the view wraps itself with
|
|
12
|
+
* `injectLazyLibs`).
|
|
13
|
+
*/
|
|
14
|
+
export interface ChatBlockVariation {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
isDefault?: boolean;
|
|
18
|
+
view: ComponentType<any>;
|
|
19
|
+
edit?: ComponentType<any>;
|
|
20
|
+
schemaEnhancer?: (args: { schema: any; formData: any }) => any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the active variation from the block data.
|
|
25
|
+
*
|
|
26
|
+
* Semantics follow the established `variations` pattern of the EEA Volto
|
|
27
|
+
* addons (volto-tabs-block, volto-block-data-table, listing block):
|
|
28
|
+
* explicit `data.variation` → `isDefault` → first registered → undefined.
|
|
29
|
+
* Old block content without a `variation` field resolves to the default.
|
|
30
|
+
*/
|
|
31
|
+
export function getChatVariation(
|
|
32
|
+
data: Record<string, any>,
|
|
33
|
+
): ChatBlockVariation | undefined {
|
|
34
|
+
const variations: ChatBlockVariation[] =
|
|
35
|
+
config.blocks?.blocksConfig?.eeaChatbot?.variations ?? [];
|
|
36
|
+
if (variations.length === 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
variations.find((v) => v.id === data?.variation) ??
|
|
41
|
+
variations.find((v) => v.isDefault) ??
|
|
42
|
+
variations[0]
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -33,7 +33,7 @@ export default async function middleware(req, res, next) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const body = req.body;
|
|
36
|
-
const { sources, answer } = body;
|
|
36
|
+
const { sources, answer, batch_size } = body;
|
|
37
37
|
|
|
38
38
|
log('Halloumi request body', {
|
|
39
39
|
answer,
|
|
@@ -52,6 +52,7 @@ export default async function middleware(req, res, next) {
|
|
|
52
52
|
answer,
|
|
53
53
|
sources,
|
|
54
54
|
max_context_segments: 0,
|
|
55
|
+
...(batch_size != null && { batch_size }),
|
|
55
56
|
}),
|
|
56
57
|
});
|
|
57
58
|
|