@eeacms/volto-eea-chatbot 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,34 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
- ### [4.0.0](https://github.com/eea/volto-eea-chatbot/compare/3.0.1...4.0.0) - 13 August 2026
7
+ ### [4.1.0](https://github.com/eea/volto-eea-chatbot/compare/4.0.0...4.1.0) - 31 August 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: expose message context + thread extra markdown into classic message [Tiberiu Ichim - [`b1600ce`](https://github.com/eea/volto-eea-chatbot/commit/b1600cede2221b846d36be42247f9068f22f06c4)]
12
+ - feat: allow presentations to plug custom components into message rendering [Tiberiu Ichim - [`3f7d82b`](https://github.com/eea/volto-eea-chatbot/commit/3f7d82b9231fc2a8a92c72471068998988dadde5)]
13
+ - feat: block presentation variations registry [Tiberiu Ichim - [`bc92ba7`](https://github.com/eea/volto-eea-chatbot/commit/bc92ba7a98b832cf34f644b8eb7294ae4542f04d)]
14
+ - feat: add configurable batch size for fact-checking [Tiberiu Ichim - [`4e81cb9`](https://github.com/eea/volto-eea-chatbot/commit/4e81cb94075f8f31a70affed6f02ac8495863a57)]
15
+
16
+ #### :bug: Bug Fixes
17
+
18
+ - fix: stop hand-declaring the 'variation' schema field [Tiberiu Ichim - [`f0ada7e`](https://github.com/eea/volto-eea-chatbot/commit/f0ada7e7fb1843c562b0af44235f3a07b5253e7d)]
19
+ - fix: skip citations with empty document_id in CITATION_DELTA [Tiberiu Ichim - [`81c9441`](https://github.com/eea/volto-eea-chatbot/commit/81c9441fa041b966f0e05b13fe271d44d9ee1421)]
20
+
21
+ #### :house: Internal changes
22
+
23
+ - chore: bump version to 4.1.0 (block presentation variations) [Tiberiu Ichim - [`45d7d3d`](https://github.com/eea/volto-eea-chatbot/commit/45d7d3db43158664c960d47585ffe749c490782c)]
24
+ - chore: change default batchSize from 30 to 20 [Tiberiu Ichim - [`c5c5f4c`](https://github.com/eea/volto-eea-chatbot/commit/c5c5f4cb90fd74accd534ccd266f79592e38ec3a)]
25
+
26
+ #### :house: Documentation changes
27
+
28
+ - docs: document block presentation variations API [Tiberiu Ichim - [`8e98061`](https://github.com/eea/volto-eea-chatbot/commit/8e98061c3ce53eb701c78d3cb1eb783c8cb47234)]
29
+
30
+ #### :hammer_and_wrench: Others
31
+
32
+ - test: update citation delta test to match restored behavior [Tiberiu Ichim - [`de9f40e`](https://github.com/eea/volto-eea-chatbot/commit/de9f40e4dfd4a69f800aaf9f1b83fdf7e197b9ab)]
33
+ - revert: restore original CITATION_DELTA handling in messageProcessor [Tiberiu Ichim - [`1d19fd3`](https://github.com/eea/volto-eea-chatbot/commit/1d19fd382bda782d7165d34d7edba4310ea65a25)]
34
+ ## [4.0.0](https://github.com/eea/volto-eea-chatbot/compare/3.0.1...4.0.0) - 13 August 2026
8
35
 
9
36
  #### :bug: Bug Fixes
10
37
 
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-chatbot",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
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,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
- <ChatWindow
35
+ <Presentation
30
36
  persona={assistantData}
31
37
  isEditMode={isEditMode}
32
38
  isPlaywrightTest={isPlaywrightTest}
@@ -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
- {showSources && (
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
- ...(showSources && !error
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': !showSources }),
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
- {showSources && !error && (
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
- return (
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 === 'assistant') {
20
- return <AIMessage {...props} />;
21
- }
22
-
23
- if (message.type === 'error') {
24
- return (
20
+ } else if (message.type === 'assistant') {
21
+ content = <AIMessage {...props} />;
22
+ } else if (message.type === 'error') {
23
+ content = (
25
24
  <div className="message-error">
26
25
  <SemanticMessage color="red" className="error-message">
27
26
  <div className="error-title">Error</div>
@@ -31,5 +30,13 @@ export function ChatMessage(props: ChatMessageProps) {
31
30
  );
32
31
  }
33
32
 
34
- return null;
33
+ if (content === null) return null;
34
+
35
+ // Expose the owning message to deeply-nested descendants (e.g. custom
36
+ // markdown components injected by addons via `extraMarkdownComponents`).
37
+ return (
38
+ <ChatMessageContext.Provider value={message}>
39
+ {content}
40
+ </ChatMessageContext.Provider>
41
+ );
35
42
  }
@@ -49,6 +49,7 @@ interface ChatWindowProps {
49
49
  enableMatomoTracking?: boolean;
50
50
  onDemandInputToggle?: boolean;
51
51
  maxContextSegments?: number;
52
+ batchSize?: number;
52
53
  onyxVersion?: '2' | '3';
53
54
  isPlaywrightTest?: boolean;
54
55
  initialQuery?: string | null;
@@ -89,7 +90,12 @@ function ChatWindow({
89
90
  enableMatomoTracking = true,
90
91
  onDemandInputToggle = true,
91
92
  maxContextSegments = 0,
93
+ batchSize,
92
94
  onyxVersion = '2',
95
+ hideSourcesTab,
96
+ extraRemarkPlugins,
97
+ extraMarkdownComponents,
98
+ extraRehypePlugins,
93
99
  } = data;
94
100
  const [qualityCheckEnabled, setQualityCheckEnabled] = useState(
95
101
  onDemandInputToggle ?? true,
@@ -231,6 +237,7 @@ function ChatWindow({
231
237
  enableMatomoTracking={enableMatomoTracking}
232
238
  persona={persona.id}
233
239
  maxContextSegments={maxContextSegments}
240
+ batchSize={batchSize}
234
241
  isLastMessage={index === messages.length - 1}
235
242
  className={
236
243
  index === messages.length - 1 ? 'most-recent' : ''
@@ -238,6 +245,10 @@ function ChatWindow({
238
245
  chatWindowRef={chatWindowRef}
239
246
  chatWindowEndRef={chatWindowEndRef}
240
247
  showTools={showTools}
248
+ hideSourcesTab={hideSourcesTab}
249
+ extraRemarkPlugins={extraRemarkPlugins}
250
+ extraMarkdownComponents={extraMarkdownComponents}
251
+ extraRehypePlugins={extraRehypePlugins}
241
252
  />
242
253
  </React.Fragment>
243
254
  ))}
@@ -0,0 +1,12 @@
1
+ import { createContext } from 'react';
2
+ import type { Message } from '@eeacms/volto-eea-chatbot/ChatBlock/types/interfaces';
3
+
4
+ /**
5
+ * Provides the currently-rendered chat message to deeply-nested descendants
6
+ * (e.g. custom react-markdown components injected by an addon).
7
+ *
8
+ * Set by `ChatMessage`, so any component rendered inside a given message
9
+ * (including custom markdown elements) can read the message that owns it —
10
+ * e.g. to match a `![[doc: Title]]` marker against `message.documents`.
11
+ */
12
+ export const ChatMessageContext = createContext<Message | null>(null);
@@ -1,4 +1,5 @@
1
1
  export { default as ChatWindow } from './ChatWindow';
2
2
  export { ChatMessage } from './ChatMessage';
3
+ export { ChatMessageContext } from './MessageContext';
3
4
  export { AIMessage } from './AIMessage';
4
5
  export { UserMessage } from './UserMessage';
@@ -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({ answer, sources, maxContextSegments }),
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) {
@@ -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={components(message, markers, stableContextSources)}
167
- remarkPlugins={remarkGfm ? [remarkGfm.default] : []}
168
- rehypePlugins={addQualityMarkersPlugin ? [addQualityMarkersPlugin] : []}
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>
@@ -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
- const explicitCitations = (citationDelta.citations || []).filter(
262
- (citation: StreamingCitation) =>
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