@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
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Modal, ModalContent, ModalHeader } from 'semantic-ui-react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
claimBadgeLabel,
|
|
5
|
+
claimHeaderLabel,
|
|
6
|
+
claimScaleState,
|
|
7
|
+
} from '@eeacms/volto-eea-chatbot/ChatBlock/utils';
|
|
4
8
|
import SVGIcon from '@eeacms/volto-eea-chatbot/ChatBlock/components/Icon';
|
|
5
|
-
import {
|
|
9
|
+
import { getVerdictBgColor } from './colors';
|
|
6
10
|
import { ClaimSegments } from './ClaimSegments';
|
|
7
11
|
|
|
8
12
|
import BotIcon from '@eeacms/volto-eea-chatbot/icons/bot.svg';
|
|
@@ -34,11 +38,9 @@ export function ClaimModal({ claim, markers, text, citedSources }) {
|
|
|
34
38
|
|
|
35
39
|
return (
|
|
36
40
|
<Modal
|
|
37
|
-
className={cx('claim-modal',
|
|
41
|
+
className={cx('claim-modal', getVerdictBgColor(claim.score))}
|
|
38
42
|
trigger={
|
|
39
|
-
<span
|
|
40
|
-
className={cx('claim', getSupportedBgColor(claim.score, 'claim'))}
|
|
41
|
-
>
|
|
43
|
+
<span className={cx('claim', getVerdictBgColor(claim.score))}>
|
|
42
44
|
{text}
|
|
43
45
|
</span>
|
|
44
46
|
}
|
|
@@ -49,7 +51,9 @@ export function ClaimModal({ claim, markers, text, citedSources }) {
|
|
|
49
51
|
<div className="circle assistant">
|
|
50
52
|
<SVGIcon name={BotIcon} size="20" color="white" />
|
|
51
53
|
</div>
|
|
52
|
-
<span className="claim-label">
|
|
54
|
+
<span className="claim-label">
|
|
55
|
+
{claimHeaderLabel(claim.score, claim.context_limited)}
|
|
56
|
+
</span>
|
|
53
57
|
</div>
|
|
54
58
|
<blockquote className="claim-quote">
|
|
55
59
|
“
|
|
@@ -70,20 +74,30 @@ export function ClaimModal({ claim, markers, text, citedSources }) {
|
|
|
70
74
|
<div className="score-badge-section">
|
|
71
75
|
<div className="score-badge">
|
|
72
76
|
<span className="score-percentage">
|
|
73
|
-
{
|
|
77
|
+
{claimBadgeLabel(claim.score, claim.context_limited)}
|
|
74
78
|
</span>
|
|
75
|
-
<span className="score-label">
|
|
79
|
+
<span className="score-label">Fact check</span>
|
|
76
80
|
</div>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
{/* Three discrete positions, not a fill. The per-claim score is a
|
|
82
|
+
verdict, not a probability: the old proportional bar made
|
|
83
|
+
not_enough_info look like "40% verified", which was never
|
|
84
|
+
measured. Decorative — the badge text carries the verdict. */}
|
|
85
|
+
<div
|
|
86
|
+
className="verdict-scale"
|
|
87
|
+
data-verdict={claimScaleState(claim.score, claim.context_limited)}
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
>
|
|
90
|
+
<span className="verdict-step" />
|
|
91
|
+
<span className="verdict-step" />
|
|
92
|
+
<span className="verdict-step" />
|
|
82
93
|
</div>
|
|
83
94
|
</div>
|
|
84
95
|
<div className="rationale-section">
|
|
85
96
|
<h5 className="rationale-header">Rationale</h5>
|
|
86
97
|
<p className="claim-rationale">{claim.rationale}</p>
|
|
98
|
+
{/* No snippet caveat here: the header already says "Couldn't be
|
|
99
|
+
checked" and the badge says "Unverified". The explanation lives
|
|
100
|
+
once, at answer level, in HalloumiFeedback. */}
|
|
87
101
|
</div>
|
|
88
102
|
</div>
|
|
89
103
|
|
|
@@ -27,18 +27,50 @@ export function ClaimSegments({ segmentIds, segments, citedSources }) {
|
|
|
27
27
|
.map((segment) => {
|
|
28
28
|
const startOffset = Math.max(0, segment.startOffset); // sometimes startOffset comes as -1
|
|
29
29
|
const endOffset = segment.endOffset;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
|
|
31
|
+
// A claim can cite several passages, possibly in different documents.
|
|
32
|
+
// Attribute by containment instead of requiring the segment to fit
|
|
33
|
+
// entirely inside one source — a segment that straddles a boundary
|
|
34
|
+
// would otherwise disappear from the modal without any warning.
|
|
35
|
+
const source =
|
|
36
|
+
citedSources.find(
|
|
37
|
+
(source) =>
|
|
38
|
+
startOffset >= source.startIndex &&
|
|
39
|
+
startOffset < source.startIndex + source.halloumiContext.length,
|
|
40
|
+
) ||
|
|
41
|
+
citedSources.find(
|
|
42
|
+
(source) =>
|
|
43
|
+
endOffset > source.startIndex &&
|
|
44
|
+
endOffset <= source.startIndex + source.halloumiContext.length,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!source) {
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.warn(
|
|
50
|
+
`Could not attribute segment ${segment.id} to a source`,
|
|
51
|
+
{ startOffset, endOffset },
|
|
52
|
+
citedSources,
|
|
53
|
+
);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Clamp to the attributed source so the snippet never renders text that
|
|
58
|
+
// belongs to the neighbouring document.
|
|
59
|
+
const clippedStart = Math.max(startOffset, source.startIndex);
|
|
60
|
+
const clippedEnd = Math.min(
|
|
61
|
+
endOffset,
|
|
62
|
+
source.startIndex + source.halloumiContext.length,
|
|
35
63
|
);
|
|
64
|
+
|
|
36
65
|
return {
|
|
37
66
|
...segment,
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
startOffset: clippedStart,
|
|
68
|
+
endOffset: clippedEnd,
|
|
69
|
+
text: joinedSources.slice(clippedStart, clippedEnd),
|
|
70
|
+
source_id: source.id,
|
|
40
71
|
};
|
|
41
|
-
})
|
|
72
|
+
})
|
|
73
|
+
.filter((snippet) => !!snippet && snippet.endOffset > snippet.startOffset);
|
|
42
74
|
|
|
43
75
|
const sourcesWithSnippets = citedSources
|
|
44
76
|
.map((source) => ({
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Render a source document with the evidence segments of a claim highlighted.
|
|
5
|
+
*
|
|
6
|
+
* Segment offsets are half-open ranges `[startOffset, endOffset)` expressed in
|
|
7
|
+
* the coordinates of the *joined* sources string, so they are shifted by
|
|
8
|
+
* `sourceStartIndex` before slicing this source's text.
|
|
9
|
+
*
|
|
10
|
+
* A claim can be supported by several evidence quotes, and those quotes can
|
|
11
|
+
* overlap (two passages covering the same sentence, for example). Because the
|
|
12
|
+
* renderer splits the text on segment boundaries, overlapping segments would
|
|
13
|
+
* emit the same characters twice — segments are therefore clipped to what is
|
|
14
|
+
* still uncovered, and a fully covered segment keeps its id on the segment that
|
|
15
|
+
* covers it so its "Jump to citation" chip still scrolls somewhere sensible.
|
|
16
|
+
*/
|
|
3
17
|
export const RenderClaimView = (props) => {
|
|
4
18
|
const {
|
|
5
19
|
value,
|
|
@@ -10,16 +24,36 @@ export const RenderClaimView = (props) => {
|
|
|
10
24
|
sourceStartIndex = 0,
|
|
11
25
|
} = props;
|
|
12
26
|
|
|
13
|
-
const sortedSegments = [...segments]
|
|
14
|
-
(
|
|
15
|
-
|
|
27
|
+
const sortedSegments = [...segments]
|
|
28
|
+
.filter(
|
|
29
|
+
(segment) =>
|
|
30
|
+
Number.isFinite(segment.startOffset) &&
|
|
31
|
+
Number.isFinite(segment.endOffset),
|
|
32
|
+
)
|
|
33
|
+
.sort((a, b) => a.startOffset - b.startOffset);
|
|
16
34
|
|
|
17
35
|
const parts = [];
|
|
18
36
|
let lastIndex = 0;
|
|
37
|
+
let lastSegmentPart = null;
|
|
19
38
|
|
|
20
39
|
sortedSegments.forEach((segment) => {
|
|
21
|
-
const segmentStart =
|
|
22
|
-
|
|
40
|
+
const segmentStart = Math.max(
|
|
41
|
+
0,
|
|
42
|
+
segment.startOffset - sourceStartIndex,
|
|
43
|
+
lastIndex,
|
|
44
|
+
);
|
|
45
|
+
const segmentEnd = Math.min(
|
|
46
|
+
value.length,
|
|
47
|
+
segment.endOffset - sourceStartIndex,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (segmentEnd <= segmentStart) {
|
|
51
|
+
// Nothing left of this segment — it is covered by the previous one.
|
|
52
|
+
if (lastSegmentPart) {
|
|
53
|
+
lastSegmentPart.ids.push(segment.id);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
23
57
|
|
|
24
58
|
// Add the text part before the current segment
|
|
25
59
|
if (segmentStart > lastIndex) {
|
|
@@ -30,12 +64,13 @@ export const RenderClaimView = (props) => {
|
|
|
30
64
|
}
|
|
31
65
|
|
|
32
66
|
// Add the segment part
|
|
33
|
-
|
|
67
|
+
const part = {
|
|
34
68
|
type: 'segment',
|
|
35
|
-
|
|
69
|
+
ids: [segment.id],
|
|
36
70
|
content: value.slice(segmentStart, segmentEnd),
|
|
37
|
-
}
|
|
38
|
-
|
|
71
|
+
};
|
|
72
|
+
parts.push(part);
|
|
73
|
+
lastSegmentPart = part;
|
|
39
74
|
lastIndex = segmentEnd;
|
|
40
75
|
});
|
|
41
76
|
|
|
@@ -55,12 +90,16 @@ export const RenderClaimView = (props) => {
|
|
|
55
90
|
const content = part.content.split('\n');
|
|
56
91
|
|
|
57
92
|
if (part.type === 'segment') {
|
|
58
|
-
const isSelectedSegment = part.
|
|
93
|
+
const isSelectedSegment = part.ids.includes(visibleSegmentId);
|
|
59
94
|
return (
|
|
60
|
-
<React.Fragment key={part.
|
|
95
|
+
<React.Fragment key={part.ids.join('-') || index}>
|
|
61
96
|
<span
|
|
62
97
|
ref={(el) => {
|
|
63
|
-
if (el)
|
|
98
|
+
if (el) {
|
|
99
|
+
part.ids.forEach((id) => {
|
|
100
|
+
spanRefs.current[id] = el;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
64
103
|
}}
|
|
65
104
|
className={`citation-segment ${
|
|
66
105
|
isSelectedSegment ? 'active' : ''
|
|
@@ -68,7 +107,11 @@ export const RenderClaimView = (props) => {
|
|
|
68
107
|
>
|
|
69
108
|
<mark className="citation-highlight">
|
|
70
109
|
{part.content.trim()}
|
|
71
|
-
|
|
110
|
+
{part.ids.map((id) => (
|
|
111
|
+
<sup key={id} className="citation-ref">
|
|
112
|
+
{id}
|
|
113
|
+
</sup>
|
|
114
|
+
))}
|
|
72
115
|
</mark>
|
|
73
116
|
{!endline && <> </>}
|
|
74
117
|
</span>
|
|
@@ -7,6 +7,28 @@ export function getSupportedTextColor(score) {
|
|
|
7
7
|
return 'text-gray-500';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Colour for a single verified claim.
|
|
12
|
+
*
|
|
13
|
+
* A per-claim score is a verdict (supported 1.0, not_enough_info 0.4,
|
|
14
|
+
* contradicted 0.0), not a measurement on a 0-1 scale, so the eight-step gradient
|
|
15
|
+
* in `getSupportedBgColor` implied a confidence level that was never computed -
|
|
16
|
+
* and it put `not_enough_info` in the red family, which reads as "the sources
|
|
17
|
+
* contradict this" when they actually said nothing. Three bands on one green ladder
|
|
18
|
+
* plus red: dark green for support, pale green for sources that said nothing, red for
|
|
19
|
+
* a conflict. The two greens are far apart in lightness on purpose - adjacent greens
|
|
20
|
+
* read as one colour.
|
|
21
|
+
*
|
|
22
|
+
* The class names must exist in `colors.less`: the `claim-*` utilities come from an
|
|
23
|
+
* explicit list of `.generate-colors()` calls, and an unlisted class renders unstyled,
|
|
24
|
+
* silently. `colors.test.js` asserts the two stay in step.
|
|
25
|
+
*/
|
|
26
|
+
export function getVerdictBgColor(score, prefix = 'claim') {
|
|
27
|
+
if (score >= 0.8) return `${prefix}-green-700`;
|
|
28
|
+
if (score >= 0.1) return `${prefix}-green-300`;
|
|
29
|
+
return `${prefix}-red-500`;
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
export function getSupportedBgColor(score, prefix = 'bg') {
|
|
11
33
|
if (0 <= score && score < 0.125) {
|
|
12
34
|
return `${prefix}-red-500`;
|
|
@@ -48,5 +48,14 @@
|
|
|
48
48
|
.generate-colors(claim, red-200, @red-200, 8, 30);
|
|
49
49
|
.generate-colors(claim, green-500, @green-500, 45, 20);
|
|
50
50
|
.generate-colors(claim, green-400, @green-400, 35, 25);
|
|
51
|
-
.generate-colors(claim, green-300, @green-300, 20,
|
|
51
|
+
.generate-colors(claim, green-300, @green-300, 20, 48);
|
|
52
52
|
.generate-colors(claim, green-200, @green-200, 10, 45);
|
|
53
|
+
|
|
54
|
+
// The three claim verdicts are a green ladder plus red: green-700 = supported, the
|
|
55
|
+
// darkest, green-300 = not confirmed by the sources, red-500 = contradicted. Kept
|
|
56
|
+
// deliberately far apart in lightness - two adjacent greens read as one colour and the
|
|
57
|
+
// distinction is lost. The darken value on green-300 is high (48) because it is the
|
|
58
|
+
// chip's text colour on a pale background: at 35 the pair was 2.7:1, unreadable.
|
|
59
|
+
// `getVerdictBgColor` emits these class names, and a test asserts every emitted class
|
|
60
|
+
// is generated here.
|
|
61
|
+
.generate-colors(claim, green-700, @green-700, 32, 8);
|
|
@@ -1,4 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getSupportedTextColor,
|
|
3
|
+
getSupportedBgColor,
|
|
4
|
+
getVerdictBgColor,
|
|
5
|
+
} from './colors';
|
|
6
|
+
|
|
7
|
+
describe('getVerdictBgColor', () => {
|
|
8
|
+
it('gives each verdict one colour band', () => {
|
|
9
|
+
expect(getVerdictBgColor(1.0)).toBe('claim-green-700');
|
|
10
|
+
expect(getVerdictBgColor(0.8)).toBe('claim-green-700');
|
|
11
|
+
expect(getVerdictBgColor(0.4)).toBe('claim-green-300');
|
|
12
|
+
expect(getVerdictBgColor(0.1)).toBe('claim-green-300');
|
|
13
|
+
expect(getVerdictBgColor(0.0)).toBe('claim-red-500');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('keeps not_enough_info out of the red family', () => {
|
|
17
|
+
// red reads as "the sources contradict this"; they said nothing
|
|
18
|
+
expect(getVerdictBgColor(0.4)).not.toContain('red');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('separates the two greens by more than one step', () => {
|
|
22
|
+
// adjacent greens read as one colour, which would erase the distinction
|
|
23
|
+
const step = (cls) => Number(cls.match(/green-(\d+)/)[1]);
|
|
24
|
+
expect(
|
|
25
|
+
step(getVerdictBgColor(1.0)) - step(getVerdictBgColor(0.4)),
|
|
26
|
+
).toBeGreaterThanOrEqual(300);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('only emits classes that colors.less actually generates', () => {
|
|
30
|
+
// an unlisted .generate-colors() call means the class renders unstyled, which
|
|
31
|
+
// fails silently - no colour, no error
|
|
32
|
+
const less = require('fs').readFileSync(
|
|
33
|
+
require('path').join(__dirname, 'colors.less'),
|
|
34
|
+
'utf-8',
|
|
35
|
+
);
|
|
36
|
+
for (const cls of [
|
|
37
|
+
getVerdictBgColor(1.0),
|
|
38
|
+
getVerdictBgColor(0.4),
|
|
39
|
+
getVerdictBgColor(0.0),
|
|
40
|
+
]) {
|
|
41
|
+
expect(less).toContain(
|
|
42
|
+
`.generate-colors(claim, ${cls.replace('claim-', '')},`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
2
47
|
|
|
3
48
|
describe('getSupportedTextColor', () => {
|
|
4
49
|
it('should return "text-red-500" for scores between 0 and 0.5', () => {
|
|
@@ -5,3 +5,4 @@ export { useMarked } from './useMarked';
|
|
|
5
5
|
export { useQualityMarkers } from './useQualityMarkers';
|
|
6
6
|
export { useDeepCompareMemoize } from './useDeepCompareMemoize';
|
|
7
7
|
export { useScrollonStream } from './useScrollonStream';
|
|
8
|
+
export { useChunkEvidence } from './useChunkEvidence';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CHUNK_EVIDENCE_WINDOW,
|
|
5
|
+
fetchEvidence,
|
|
6
|
+
} from '@eeacms/volto-eea-chatbot/ChatBlock/services/chunkEvidence';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve real Onyx chunk text for the documents about to be sent to the
|
|
10
|
+
* fact-checker.
|
|
11
|
+
*
|
|
12
|
+
* Only runs when `enabled` (i.e. quality control is being requested), and
|
|
13
|
+
* re-runs only when the set of `document_id` + `chunk_ind` values changes.
|
|
14
|
+
*
|
|
15
|
+
* `evidenceSettled` is what callers must gate the fact-check call on. It is
|
|
16
|
+
* false for a key that has not been resolved yet, so the check cannot fire with
|
|
17
|
+
* blurbs in the same commit that started the evidence fetch. It also becomes
|
|
18
|
+
* true when a fetch fails — falling back to blurbs is better than never
|
|
19
|
+
* verifying at all.
|
|
20
|
+
*/
|
|
21
|
+
export function useChunkEvidence(
|
|
22
|
+
docs: any[],
|
|
23
|
+
{
|
|
24
|
+
enabled = true,
|
|
25
|
+
window = CHUNK_EVIDENCE_WINDOW,
|
|
26
|
+
}: { enabled?: boolean; window?: number } = {},
|
|
27
|
+
) {
|
|
28
|
+
const [evidence, setEvidence] = useState<Record<string, any>>({});
|
|
29
|
+
const [isFetchingEvidence, setIsFetchingEvidence] = useState(false);
|
|
30
|
+
const [settledKey, setSettledKey] = useState('');
|
|
31
|
+
|
|
32
|
+
const docsRef = useRef(docs);
|
|
33
|
+
docsRef.current = docs;
|
|
34
|
+
|
|
35
|
+
const key = useMemo(
|
|
36
|
+
() =>
|
|
37
|
+
(docs || [])
|
|
38
|
+
.map((doc: any) => `${doc?.document_id}@@${doc?.chunk_ind ?? 0}`)
|
|
39
|
+
.filter((part: string) => !part.startsWith('undefined'))
|
|
40
|
+
.join('|'),
|
|
41
|
+
[docs],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!enabled || !key) {
|
|
46
|
+
setIsFetchingEvidence(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
setIsFetchingEvidence(true);
|
|
52
|
+
|
|
53
|
+
fetchEvidence(docsRef.current, { window })
|
|
54
|
+
.then((result) => {
|
|
55
|
+
if (cancelled) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setEvidence(result);
|
|
59
|
+
setIsFetchingEvidence(false);
|
|
60
|
+
setSettledKey(key);
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {
|
|
63
|
+
// Fall back to whatever the stream gave us (blurbs).
|
|
64
|
+
if (!cancelled) {
|
|
65
|
+
setIsFetchingEvidence(false);
|
|
66
|
+
setSettledKey(key);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true;
|
|
72
|
+
};
|
|
73
|
+
}, [key, enabled, window]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
evidence,
|
|
77
|
+
isFetchingEvidence,
|
|
78
|
+
evidenceSettled: key === '' || settledKey === key,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/ChatBlock/schema.jsx
CHANGED
|
@@ -95,6 +95,7 @@ export function ChatBlockSchema({ assistants, data }) {
|
|
|
95
95
|
'batchSize',
|
|
96
96
|
'noSupportDocumentsMessage',
|
|
97
97
|
'qualityCheckContext',
|
|
98
|
+
'chunkEvidenceWindow',
|
|
98
99
|
'qualityCheckStages',
|
|
99
100
|
]
|
|
100
101
|
: []),
|
|
@@ -214,6 +215,17 @@ export function ChatBlockSchema({ assistants, data }) {
|
|
|
214
215
|
['all', 'All documents passed to LLM'],
|
|
215
216
|
],
|
|
216
217
|
},
|
|
218
|
+
chunkEvidenceWindow: {
|
|
219
|
+
title: 'Evidence window (chunks around each source)',
|
|
220
|
+
type: 'number',
|
|
221
|
+
default: 2,
|
|
222
|
+
description:
|
|
223
|
+
'How many Onyx chunks to fetch on each side of the matched chunk before ' +
|
|
224
|
+
'fact-checking. 0 = centre chunk only (usually not enough), 2 = what Onyx ' +
|
|
225
|
+
'feeds the answer generator by default, 5 = Onyx at its widest. Each extra ' +
|
|
226
|
+
'step costs two more chunk requests per document, and values above 20 are ' +
|
|
227
|
+
'clamped.',
|
|
228
|
+
},
|
|
217
229
|
qualityCheckStages: {
|
|
218
230
|
title: 'Score ranges',
|
|
219
231
|
widget: 'object_list',
|