@eeacms/volto-eea-chatbot 1.0.9 → 1.0.11
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 +15 -752
- package/package.json +1 -1
- package/razzle.extend.js +8 -4
- package/src/ChatBlock/ChatBlockView.jsx +26 -2
- package/src/ChatBlock/chat/AIMessage.tsx +5 -1
- package/src/ChatBlock/chat/ChatWindow.tsx +12 -3
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +2 -1
- package/src/ChatBlock/components/QualityCheckToggle.jsx +1 -1
- package/src/ChatBlock/hooks/useChatController.ts +10 -2
- package/src/ChatBlock/index.js +1 -1
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -0
- package/src/ChatBlock/services/streamingService.ts +30 -26
- package/src/ChatBlock/style.less +3 -1
- package/src/ChatBlock/tests/ChatMessage.test.jsx +75 -0
- package/src/ChatBlock/tests/ClaimModal.test.jsx +136 -0
- package/src/ChatBlock/tests/ClaimSegments.test.jsx +206 -0
- package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +241 -0
- package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +161 -0
- package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +178 -0
- package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +227 -0
- package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +134 -0
- package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +163 -0
- package/src/ChatBlock/tests/RenderClaimView.test.jsx +191 -0
- package/src/ChatBlock/tests/RendererComponent.test.jsx +139 -0
- package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +295 -0
- package/src/ChatBlock/tests/SourceChip.test.jsx +108 -0
- package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +135 -0
- package/src/ChatBlock/tests/UserMessage.test.jsx +83 -0
- package/src/ChatBlock/tests/WebResultIcon.test.jsx +61 -0
- package/src/ChatBlock/tests/citations.test.js +114 -0
- package/src/ChatBlock/tests/messageProcessor.test.jsx +285 -1
- package/src/ChatBlock/tests/packetUtils.test.js +158 -0
- package/src/ChatBlock/tests/streamingService.test.js +467 -0
- package/src/ChatBlock/tests/useChatController.test.jsx +268 -0
- package/src/ChatBlock/tests/useChatStreaming.test.jsx +163 -0
- package/src/ChatBlock/tests/useMarked.test.jsx +107 -0
- package/src/ChatBlock/tests/useQualityMarkers.test.jsx +150 -0
- package/src/ChatBlock/tests/useScrollonStream.test.jsx +121 -0
- package/src/ChatBlock/tests/utils.test.jsx +241 -0
- package/src/ChatBlock/tests/withOnyxData.test.jsx +81 -0
- package/src/ChatBlock/utils/citations.ts +1 -1
- package/src/halloumi/generative.js +1 -0
- package/src/halloumi/generative.test.js +278 -0
- package/src/halloumi/middleware.test.js +69 -0
- package/src/index.js +1 -0
- package/src/middleware.js +21 -13
- package/src/middleware.test.js +221 -0
package/package.json
CHANGED
package/razzle.extend.js
CHANGED
|
@@ -26,10 +26,14 @@ const modify = (config, { target, dev }, webpack) => {
|
|
|
26
26
|
include.push(markedPath);
|
|
27
27
|
// include.push(nodeFetch);
|
|
28
28
|
|
|
29
|
-
babelLoader.use[0].options.plugins
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const plugs = babelLoader.use[0].options.plugins || [];
|
|
30
|
+
|
|
31
|
+
if (plugs.indexOf('@babel/plugin-proposal-private-methods') < 0) {
|
|
32
|
+
babelLoader.use[0].options.plugins = [
|
|
33
|
+
...(plugs || []),
|
|
34
|
+
'@babel/plugin-proposal-private-methods',
|
|
35
|
+
];
|
|
36
|
+
}
|
|
33
37
|
|
|
34
38
|
return config;
|
|
35
39
|
};
|
|
@@ -1,12 +1,36 @@
|
|
|
1
|
+
import { useMemo, useEffect } from 'react';
|
|
1
2
|
import superagent from 'superagent';
|
|
3
|
+
import { parse } from 'qs';
|
|
2
4
|
import withOnyxData from './hocs/withOnyxData';
|
|
3
5
|
import { ChatWindow } from './chat';
|
|
4
6
|
|
|
5
7
|
function ChatBlockView(props) {
|
|
6
|
-
const { assistantData, data, isEditMode } = props;
|
|
8
|
+
const { id, assistantData, data, isEditMode, location } = props;
|
|
9
|
+
|
|
10
|
+
const query = useMemo(
|
|
11
|
+
() => parse(location?.search.replace('?', '')) || {},
|
|
12
|
+
[location],
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const isPlaywrightTest = query.playwright === 'yes';
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isPlaywrightTest) {
|
|
19
|
+
window.__EEA_CHATBOT_TEST_CONFIG__ = {
|
|
20
|
+
block_id: id,
|
|
21
|
+
...data,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}, [id, isPlaywrightTest, data]);
|
|
7
25
|
|
|
8
26
|
return assistantData ? (
|
|
9
|
-
<ChatWindow
|
|
27
|
+
<ChatWindow
|
|
28
|
+
persona={assistantData}
|
|
29
|
+
isEditMode={isEditMode}
|
|
30
|
+
isPlaywrightTest={isPlaywrightTest}
|
|
31
|
+
block_id={id}
|
|
32
|
+
{...data}
|
|
33
|
+
/>
|
|
10
34
|
) : (
|
|
11
35
|
<div>Chatbot</div>
|
|
12
36
|
);
|
|
@@ -474,7 +474,10 @@ export function AIMessage({
|
|
|
474
474
|
|
|
475
475
|
// Tab panes - conditionally include Sources tab
|
|
476
476
|
const panes = [
|
|
477
|
-
{
|
|
477
|
+
{
|
|
478
|
+
menuItem: { key: 'answer', content: 'Answer', className: 'answer-tab' },
|
|
479
|
+
pane: <Tab.Pane key="answer">{answerTab}</Tab.Pane>,
|
|
480
|
+
},
|
|
478
481
|
...(showSources && !error
|
|
479
482
|
? [
|
|
480
483
|
{
|
|
@@ -486,6 +489,7 @@ export function AIMessage({
|
|
|
486
489
|
<span className="sources-count">({sources.length})</span>
|
|
487
490
|
</span>
|
|
488
491
|
),
|
|
492
|
+
className: 'sources-tab',
|
|
489
493
|
},
|
|
490
494
|
pane: (
|
|
491
495
|
<Tab.Pane key="sources">
|
|
@@ -22,6 +22,7 @@ import PenIcon from '../../icons/square-pen.svg';
|
|
|
22
22
|
import '../style.less';
|
|
23
23
|
|
|
24
24
|
interface ChatWindowProps {
|
|
25
|
+
block_id?: string;
|
|
25
26
|
persona: Persona;
|
|
26
27
|
rehypePrism?: any;
|
|
27
28
|
remarkGfm?: any;
|
|
@@ -47,15 +48,18 @@ interface ChatWindowProps {
|
|
|
47
48
|
enableMatomoTracking?: boolean;
|
|
48
49
|
onDemandInputToggle?: boolean;
|
|
49
50
|
maxContextSegments?: number;
|
|
51
|
+
isPlaywrightTest?: boolean;
|
|
50
52
|
[key: string]: any;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function ChatWindow({
|
|
56
|
+
block_id,
|
|
54
57
|
persona,
|
|
55
58
|
rehypePrism,
|
|
56
59
|
remarkGfm,
|
|
57
60
|
placeholderPrompt = 'Ask a question',
|
|
58
61
|
isEditMode,
|
|
62
|
+
isPlaywrightTest,
|
|
59
63
|
...data
|
|
60
64
|
}: ChatWindowProps) {
|
|
61
65
|
const {
|
|
@@ -141,7 +145,10 @@ function ChatWindow({
|
|
|
141
145
|
);
|
|
142
146
|
|
|
143
147
|
return (
|
|
144
|
-
<div
|
|
148
|
+
<div
|
|
149
|
+
className="chat-window"
|
|
150
|
+
data-playwright-block-id={isPlaywrightTest ? block_id : undefined}
|
|
151
|
+
>
|
|
145
152
|
<div className="messages">
|
|
146
153
|
{showLandingPage ? (
|
|
147
154
|
<>
|
|
@@ -211,7 +218,7 @@ function ChatWindow({
|
|
|
211
218
|
|
|
212
219
|
{isStreaming &&
|
|
213
220
|
!isFetchingRelatedQuestions &&
|
|
214
|
-
!messages[messages.length - 1]
|
|
221
|
+
!messages[messages.length - 1]?.isFinalMessageComing && (
|
|
215
222
|
<div className="comment">
|
|
216
223
|
<div className="circle assistant placeholder"></div>
|
|
217
224
|
<div className="comment-content">
|
|
@@ -269,7 +276,9 @@ function ChatWindow({
|
|
|
269
276
|
</div>
|
|
270
277
|
)}
|
|
271
278
|
|
|
272
|
-
{deepResearch === 'always_on' &&
|
|
279
|
+
{deepResearch === 'always_on' && (
|
|
280
|
+
<small className="deep-research-toggle">Deep research on</small>
|
|
281
|
+
)}
|
|
273
282
|
</div>
|
|
274
283
|
<div ref={chatWindowEndRef} /> {/* End div to mark the bottom */}
|
|
275
284
|
</div>
|
|
@@ -40,6 +40,7 @@ export default React.forwardRef(function AutoResizeTextarea(props, ref) {
|
|
|
40
40
|
setInput(input + '\n');
|
|
41
41
|
}
|
|
42
42
|
}}
|
|
43
|
+
disabled={isStreaming}
|
|
43
44
|
{...rest}
|
|
44
45
|
ref={ref}
|
|
45
46
|
/>
|
|
@@ -52,7 +53,7 @@ export default React.forwardRef(function AutoResizeTextarea(props, ref) {
|
|
|
52
53
|
onKeyDown={(e) => {
|
|
53
54
|
handleSubmit(e);
|
|
54
55
|
}}
|
|
55
|
-
disabled={isStreaming}
|
|
56
|
+
disabled={isStreaming || input.trim() === ''}
|
|
56
57
|
onClick={(e) => {
|
|
57
58
|
handleSubmit(e);
|
|
58
59
|
}}
|
|
@@ -10,7 +10,7 @@ const QualityCheckToggle = ({ isEditMode, enabled, setEnabled }) => {
|
|
|
10
10
|
content="Checks the AI's statements against cited sources to highlight possible inaccuracies and hallucinations."
|
|
11
11
|
trigger={
|
|
12
12
|
<Checkbox
|
|
13
|
-
id="
|
|
13
|
+
id="quality-check-toggle"
|
|
14
14
|
toggle
|
|
15
15
|
label="Fact-check AI answer"
|
|
16
16
|
disabled={isEditMode}
|
|
@@ -18,6 +18,10 @@ interface RelatedQuestion {
|
|
|
18
18
|
|
|
19
19
|
// Extract JSON array from related questions response
|
|
20
20
|
function extractRelatedQuestions(str: string): RelatedQuestion[] {
|
|
21
|
+
if (str.toLowerCase().includes('no_response')) {
|
|
22
|
+
throw new Error('Related questions were not generated properly');
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
const regex = /\[[\s\S]*?\]/;
|
|
22
26
|
const match = str.match(regex);
|
|
23
27
|
|
|
@@ -65,7 +69,7 @@ async function fetchRelatedQuestions(
|
|
|
65
69
|
};
|
|
66
70
|
|
|
67
71
|
let result = '';
|
|
68
|
-
for await (const packets of sendMessage(params)) {
|
|
72
|
+
for await (const packets of sendMessage(params, true)) {
|
|
69
73
|
for (const packet of packets) {
|
|
70
74
|
if (packet.obj.type === PacketType.MESSAGE_DELTA) {
|
|
71
75
|
result += packet.obj.content;
|
|
@@ -88,6 +92,7 @@ export function useChatController({
|
|
|
88
92
|
}: UseChatControllerProps) {
|
|
89
93
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
90
94
|
const [chatSessionId, setChatSessionId] = useState<string | null>(null);
|
|
95
|
+
const [chatSessionLoading, setChatSessionLoading] = useState(false);
|
|
91
96
|
const [isDeepResearchEnabled, setIsDeepResearchEnabled] = useState(
|
|
92
97
|
deepResearch === 'always_on' || deepResearch === 'user_on',
|
|
93
98
|
);
|
|
@@ -183,6 +188,7 @@ export function useChatController({
|
|
|
183
188
|
let sessionId = chatSessionId;
|
|
184
189
|
|
|
185
190
|
if (!sessionId) {
|
|
191
|
+
setChatSessionLoading(true);
|
|
186
192
|
sessionId = await createChatSession(personaId, 'Chat session');
|
|
187
193
|
setChatSessionId(sessionId);
|
|
188
194
|
}
|
|
@@ -241,6 +247,8 @@ export function useChatController({
|
|
|
241
247
|
);
|
|
242
248
|
} catch (error) {
|
|
243
249
|
console.error('Failed to submit message:', error);
|
|
250
|
+
} finally {
|
|
251
|
+
setChatSessionLoading(false);
|
|
244
252
|
}
|
|
245
253
|
},
|
|
246
254
|
[
|
|
@@ -319,7 +327,7 @@ export function useChatController({
|
|
|
319
327
|
|
|
320
328
|
return {
|
|
321
329
|
messages,
|
|
322
|
-
isStreaming,
|
|
330
|
+
isStreaming: isStreaming || chatSessionLoading,
|
|
323
331
|
isCancelled,
|
|
324
332
|
isFetchingRelatedQuestions,
|
|
325
333
|
onSubmit,
|
package/src/ChatBlock/index.js
CHANGED
|
@@ -46,6 +46,14 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
|
|
|
46
46
|
const [displayedPacketCount, setDisplayedPacketCount] =
|
|
47
47
|
useState(initialPacketCount);
|
|
48
48
|
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const audioCtx = new AudioContext();
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
audioCtx.close();
|
|
54
|
+
};
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
49
57
|
// Animation effect - gradually increase displayed packets at controlled rate
|
|
50
58
|
// Adaptive animation: ensures visible typing effect even for fast streams
|
|
51
59
|
useEffect(() => {
|
|
@@ -8,7 +8,7 @@ export interface SendMessageParams {
|
|
|
8
8
|
parentMessageId: number | null;
|
|
9
9
|
chatSessionId: string;
|
|
10
10
|
filters: Filters | null;
|
|
11
|
-
selectedDocumentIds: number[] | null;
|
|
11
|
+
selectedDocumentIds: number[] | string[] | null;
|
|
12
12
|
queryOverride?: string;
|
|
13
13
|
forceSearch?: boolean;
|
|
14
14
|
modelProvider?: string;
|
|
@@ -166,30 +166,33 @@ export async function* handleStream(
|
|
|
166
166
|
/**
|
|
167
167
|
* Send a message and stream the response
|
|
168
168
|
*/
|
|
169
|
-
export async function* sendMessage(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
169
|
+
export async function* sendMessage(
|
|
170
|
+
{
|
|
171
|
+
regenerate,
|
|
172
|
+
retrieval_options,
|
|
173
|
+
message,
|
|
174
|
+
fileDescriptors,
|
|
175
|
+
currentMessageFiles,
|
|
176
|
+
parentMessageId,
|
|
177
|
+
chatSessionId,
|
|
178
|
+
filters,
|
|
179
|
+
selectedDocumentIds,
|
|
180
|
+
queryOverride,
|
|
181
|
+
forceSearch,
|
|
182
|
+
modelProvider,
|
|
183
|
+
modelVersion,
|
|
184
|
+
temperature,
|
|
185
|
+
systemPromptOverride,
|
|
186
|
+
taskPromptOverride,
|
|
187
|
+
useExistingUserMessage,
|
|
188
|
+
alternateAssistantId,
|
|
189
|
+
signal,
|
|
190
|
+
useAgentSearch,
|
|
191
|
+
enabledToolIds,
|
|
192
|
+
forcedToolIds,
|
|
193
|
+
}: SendMessageParams,
|
|
194
|
+
isRelatedQuestion: boolean = false,
|
|
195
|
+
): AsyncGenerator<Packet[], void, unknown> {
|
|
193
196
|
const documentsAreSelected =
|
|
194
197
|
selectedDocumentIds && selectedDocumentIds.length > 0;
|
|
195
198
|
|
|
@@ -233,7 +236,8 @@ export async function* sendMessage({
|
|
|
233
236
|
|
|
234
237
|
const body = JSON.stringify(payload);
|
|
235
238
|
|
|
236
|
-
const
|
|
239
|
+
const middleware = isRelatedQuestion ? '_rq' : '_da';
|
|
240
|
+
const sendMessageResponse = await fetch(`/${middleware}/chat/send-message`, {
|
|
237
241
|
method: 'POST',
|
|
238
242
|
headers: {
|
|
239
243
|
'Content-Type': 'application/json',
|
package/src/ChatBlock/style.less
CHANGED
|
@@ -610,6 +610,8 @@ mark {
|
|
|
610
610
|
}
|
|
611
611
|
|
|
612
612
|
.quality-check-toggle {
|
|
613
|
+
margin-top: 0;
|
|
614
|
+
|
|
613
615
|
.ui.toggle.checkbox {
|
|
614
616
|
input:checked ~ label {
|
|
615
617
|
color: @grey !important;
|
|
@@ -1306,7 +1308,7 @@ mark {
|
|
|
1306
1308
|
}
|
|
1307
1309
|
}
|
|
1308
1310
|
|
|
1309
|
-
#
|
|
1311
|
+
#quality-check-toggle[disabled] ~ label {
|
|
1310
1312
|
opacity: 1;
|
|
1311
1313
|
}
|
|
1312
1314
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
2
|
+
import configureStore from 'redux-mock-store';
|
|
3
|
+
import renderer from 'react-test-renderer';
|
|
4
|
+
|
|
5
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
6
|
+
import { Provider } from 'react-intl-redux';
|
|
7
|
+
import { ChatMessage } from '../chat/ChatMessage';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
// Mock loadable components
|
|
12
|
+
jest.mock('@loadable/component', () => {
|
|
13
|
+
const loadable = () => {
|
|
14
|
+
const MockComponent = ({ children }) => <div>{children}</div>;
|
|
15
|
+
return MockComponent;
|
|
16
|
+
};
|
|
17
|
+
loadable.lib = () => {
|
|
18
|
+
const MockComponent = ({ children }) =>
|
|
19
|
+
children ? children({ default: {} }) : null;
|
|
20
|
+
return MockComponent;
|
|
21
|
+
};
|
|
22
|
+
return { __esModule: true, default: loadable };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('ChatMessage', () => {
|
|
26
|
+
let store;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
store = mockStore({
|
|
30
|
+
userSession: { token: '1234' },
|
|
31
|
+
intl: { locale: 'en', messages: {} },
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const renderComponent = (props) =>
|
|
36
|
+
renderer.create(
|
|
37
|
+
<Provider store={store}>
|
|
38
|
+
<MemoryRouter>
|
|
39
|
+
<ChatMessage {...props} />
|
|
40
|
+
</MemoryRouter>
|
|
41
|
+
</Provider>,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it('renders error message correctly', () => {
|
|
45
|
+
const props = {
|
|
46
|
+
message: {
|
|
47
|
+
messageId: 3,
|
|
48
|
+
type: 'error',
|
|
49
|
+
error: 'Something went wrong',
|
|
50
|
+
},
|
|
51
|
+
libs: { remarkGfm: { default: [] } },
|
|
52
|
+
isLoading: false,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const component = renderComponent(props);
|
|
56
|
+
const json = component.toJSON();
|
|
57
|
+
expect(json).toMatchSnapshot();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns null for unknown message type', () => {
|
|
61
|
+
const props = {
|
|
62
|
+
message: {
|
|
63
|
+
messageId: 4,
|
|
64
|
+
message: 'Unknown type',
|
|
65
|
+
type: 'unknown',
|
|
66
|
+
},
|
|
67
|
+
libs: { remarkGfm: { default: [] } },
|
|
68
|
+
isLoading: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const component = renderComponent(props);
|
|
72
|
+
const json = component.toJSON();
|
|
73
|
+
expect(json).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer from 'react-test-renderer';
|
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
4
|
+
import { ClaimModal } from '../components/markdown/ClaimModal';
|
|
5
|
+
|
|
6
|
+
// Mock semantic-ui-react Modal
|
|
7
|
+
jest.mock('semantic-ui-react', () => ({
|
|
8
|
+
Modal: ({ children, trigger, className }) => (
|
|
9
|
+
<div className={className} data-testid="modal">
|
|
10
|
+
<div data-testid="trigger">{trigger}</div>
|
|
11
|
+
<div data-testid="content">{children}</div>
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
ModalHeader: ({ children }) => <div data-testid="header">{children}</div>,
|
|
15
|
+
ModalContent: ({ children }) => (
|
|
16
|
+
<div data-testid="modal-content">{children}</div>
|
|
17
|
+
),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Mock ClaimSegments
|
|
21
|
+
jest.mock('../components/markdown/ClaimSegments', () => ({
|
|
22
|
+
ClaimSegments: () => <div data-testid="claim-segments">ClaimSegments</div>,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('ClaimModal', () => {
|
|
26
|
+
const defaultProps = {
|
|
27
|
+
claim: {
|
|
28
|
+
score: 0.85,
|
|
29
|
+
claimString: 'This is a claim about something important.',
|
|
30
|
+
rationale: 'The claim is supported by multiple sources.',
|
|
31
|
+
segmentIds: [1, 2, 3],
|
|
32
|
+
},
|
|
33
|
+
markers: {
|
|
34
|
+
segments: {
|
|
35
|
+
1: { id: 1, text: 'segment 1' },
|
|
36
|
+
2: { id: 2, text: 'segment 2' },
|
|
37
|
+
3: { id: 3, text: 'segment 3' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
text: ['something important'],
|
|
41
|
+
citedSources: [
|
|
42
|
+
{ id: 1, semantic_identifier: 'Source 1', link: 'https://example.com' },
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
it('renders the claim modal with high score', () => {
|
|
47
|
+
const component = renderer.create(<ClaimModal {...defaultProps} />);
|
|
48
|
+
const json = component.toJSON();
|
|
49
|
+
expect(json).toMatchSnapshot();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders with low score', () => {
|
|
53
|
+
const props = {
|
|
54
|
+
...defaultProps,
|
|
55
|
+
claim: {
|
|
56
|
+
...defaultProps.claim,
|
|
57
|
+
score: 0.3,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const component = renderer.create(<ClaimModal {...props} />);
|
|
61
|
+
const json = component.toJSON();
|
|
62
|
+
expect(json).toMatchSnapshot();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders with medium score', () => {
|
|
66
|
+
const props = {
|
|
67
|
+
...defaultProps,
|
|
68
|
+
claim: {
|
|
69
|
+
...defaultProps.claim,
|
|
70
|
+
score: 0.6,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const component = renderer.create(<ClaimModal {...props} />);
|
|
74
|
+
const json = component.toJSON();
|
|
75
|
+
expect(json).toMatchSnapshot();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('handles empty text array', () => {
|
|
79
|
+
const props = {
|
|
80
|
+
...defaultProps,
|
|
81
|
+
text: [],
|
|
82
|
+
};
|
|
83
|
+
const component = renderer.create(<ClaimModal {...props} />);
|
|
84
|
+
const json = component.toJSON();
|
|
85
|
+
expect(json).toMatchSnapshot();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handles claim with markdown formatting', () => {
|
|
89
|
+
const props = {
|
|
90
|
+
...defaultProps,
|
|
91
|
+
claim: {
|
|
92
|
+
...defaultProps.claim,
|
|
93
|
+
claimString: '**Bold claim** with *italic* and [[1]](url)',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const component = renderer.create(<ClaimModal {...props} />);
|
|
97
|
+
const json = component.toJSON();
|
|
98
|
+
expect(json).toMatchSnapshot();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('renders with empty markers', () => {
|
|
102
|
+
const props = {
|
|
103
|
+
...defaultProps,
|
|
104
|
+
markers: {},
|
|
105
|
+
};
|
|
106
|
+
const component = renderer.create(<ClaimModal {...props} />);
|
|
107
|
+
const json = component.toJSON();
|
|
108
|
+
expect(json).toMatchSnapshot();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('handles zero score', () => {
|
|
112
|
+
const props = {
|
|
113
|
+
...defaultProps,
|
|
114
|
+
claim: {
|
|
115
|
+
...defaultProps.claim,
|
|
116
|
+
score: 0,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const component = renderer.create(<ClaimModal {...props} />);
|
|
120
|
+
const json = component.toJSON();
|
|
121
|
+
expect(json).toMatchSnapshot();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('handles perfect score', () => {
|
|
125
|
+
const props = {
|
|
126
|
+
...defaultProps,
|
|
127
|
+
claim: {
|
|
128
|
+
...defaultProps.claim,
|
|
129
|
+
score: 1.0,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const component = renderer.create(<ClaimModal {...props} />);
|
|
133
|
+
const json = component.toJSON();
|
|
134
|
+
expect(json).toMatchSnapshot();
|
|
135
|
+
});
|
|
136
|
+
});
|